UNPKG

@underpostnet/underpost

Version:
847 lines (796 loc) 24.4 kB
import { getId, newInstance, range, s4, splitEveryXChar } from './CommonJs.js'; import { CssCoreDark, CssCoreLight } from './CssCore.js'; import { DropDown } from './DropDown.js'; import { Modal } from './Modal.js'; import { Translate } from './Translate.js'; import { append, getProxyPath, htmls, s } from './VanillaJs.js'; let ThemesScope = []; // https://css.github.io/csso/csso.html // https://www.fontspace.com/ // https://www.1001fonts.com/ const Css = { loadThemes: async function (themes = []) { ThemesScope = []; for (const themeOptions of themes) addTheme(themeOptions); // if (!ThemesScope.find((t) => t.dark)) addTheme(CssCoreDark); // if (!ThemesScope.find((t) => !t.dark)) addTheme(CssCoreLight); if (ThemesScope.length === 0) { addTheme(CssCoreDark); addTheme(CssCoreLight); } const localStorageTheme = localStorage.getItem('_theme'); if (localStorageTheme && Themes[localStorageTheme]) { const themeOption = ThemesScope.find((t) => t.theme === localStorageTheme); if (themeOption) return await this.Init(themeOption); } await this.Init(); }, Init: async function (options) { if (!options) options = ThemesScope[0]; const { theme } = options; return await Themes[theme](options); }, RenderSetting: async function () { return html` <div class="in section-mp"> ${await DropDown.Render({ id: 'settings-theme', value: Css.currentTheme, label: html`${Translate.Render('theme')}`, data: ThemesScope.map((themeOption) => { return { display: html`<i class="fa-solid fa-brush"></i> ${themeOption.theme}`, value: themeOption.theme, onClick: async () => await Themes[themeOption.theme](), }; }), })} </div>`; }, }; const barLabels = (options) => { return { img: { close: html`<img class="inl bar-default-modal-icon ${options.iconClass ? options.iconClass : ''}" src="${getProxyPath()}assets/icons/close.png" />`, maximize: html`<img class="inl bar-default-modal-icon ${options.iconClass ? options.iconClass : ''}" src="${getProxyPath()}assets/icons/maximize.png" />`, minimize: html`<img class="inl bar-default-modal-icon ${options.iconClass ? options.iconClass : ''}" src="${getProxyPath()}assets/icons/minimize.png" />`, restore: html`<img class="inl bar-default-modal-icon ${options.iconClass ? options.iconClass : ''}" src="${getProxyPath()}assets/icons/restore.png" />`, menu: html`<img class="inl bar-default-modal-icon ${options.iconClass ? options.iconClass : ''}" src="${getProxyPath()}assets/icons/menu.png" />`, }, fontawesome: { close: html`<i class="fa-solid fa-xmark ${options.iconClass ? options.iconClass : ''}"></i>`, maximize: html`<i class="fa-regular fa-square ${options.iconClass ? options.iconClass : ''}"></i>`, minimize: html`<i class="fa-solid fa-window-minimize ${options.iconClass ? options.iconClass : ''}"></i>`, restore: html`<i class="fa-regular fa-window-restore ${options.iconClass ? options.iconClass : ''}"></i>`, menu: html`<i class="fa-solid fa-bars ${options.iconClass ? options.iconClass : ''}"></i>`, }, default: { close: html`X`, maximize: html`▢`, minimize: html`_`, restore: html`□`, menu: html`≡`, }, }; }; const barConfig = (options) => { const { barButtonsIconTheme } = options; return { buttons: { close: { disabled: false, label: barLabels(options)[barButtonsIconTheme].close, }, maximize: { disabled: false, label: barLabels(options)[barButtonsIconTheme].maximize, }, minimize: { disabled: false, label: barLabels(options)[barButtonsIconTheme].minimize, }, restore: { disabled: false, label: barLabels(options)[barButtonsIconTheme].restore, }, menu: { disabled: true, label: barLabels(options)[barButtonsIconTheme].menu, }, }, }; }; const renderDefaultWindowsModalButtonContent = (options) => { const { barButtonsIconTheme, htmlRender } = options; const barConfigInstance = barConfig(options); if (htmlRender) Object.keys(Modal.Data).map((idModal) => { if (s(`.btn-minimize-${idModal}`)) htmls(`.btn-minimize-${idModal}`, barConfigInstance.buttons.minimize.label); if (s(`.btn-restore-${idModal}`)) htmls(`.btn-restore-${idModal}`, barConfigInstance.buttons.restore.label); if (s(`.btn-maximize-${idModal}`)) htmls(`.btn-maximize-${idModal}`, barConfigInstance.buttons.maximize.label); if (s(`.btn-close-${idModal}`)) htmls(`.btn-close-${idModal}`, barConfigInstance.buttons.close.label); if (s(`.btn-menu-${idModal}`)) htmls(`.btn-menu-${idModal}`, barConfigInstance.buttons.menu.label); }); return { barConfig: barConfigInstance }; }; let darkTheme = true; const ThemeEvents = {}; const TriggerThemeEvents = () => { localStorage.setItem('_theme', Css.currentTheme); Object.keys(ThemeEvents).map((keyEvent) => ThemeEvents[keyEvent]()); }; const Themes = {}; const addTheme = (options) => { ThemesScope.push(options); Themes[options.theme] = async (barOptions) => { if (!options.dark) options.dark = false; if (!options.barButtonsIconTheme) options.barButtonsIconTheme = 'fontawesome'; const htmlRender = Css.currentTheme !== options.theme; if (htmlRender) { Css.currentTheme = options.theme; darkTheme = options.dark; let render = ''; if (!['core', 'css-core'].includes(options.theme)) render += darkTheme ? await CssCoreDark.render() : await CssCoreLight.render(); render += await options.render(); htmls('.theme', render); TriggerThemeEvents(); } return { ...renderDefaultWindowsModalButtonContent({ barButtonsIconTheme: options.barButtonsIconTheme, htmlRender, ...barOptions, }), }; }; }; const borderChar = (px, color, selectors) => { if (selectors) { return selectors .map( (selector) => html` <style> ${selector} { text-shadow: ${px}px -${px}px ${px}px ${color}, -${px}px ${px}px ${px}px ${color}, -${px}px -${px}px ${px}px ${color}, ${px}px ${px}px ${px}px ${color}; } </style> `, ) .join(''); } return html` text-shadow: ${px}px -${px}px ${px}px ${color}, -${px}px ${px}px ${px}px ${color}, -${px}px -${px}px ${px}px ${color}, ${px}px ${px}px ${px}px ${color}; `; }; const boxShadow = ({ selector }) => html` <style> ${selector} { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } ${selector}:hover { box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2), 0 10px 30px 0 rgba(0, 0, 0, 0.3); } </style> `; const renderMediaQuery = (mediaData) => { // first limit should be '0' return html` ${mediaData .map( (mediaState) => html` <style> @media only screen and (min-width: ${mediaState.limit}px) { ${mediaState.css} } </style> `, ) .join('')} `; }; const renderStatus = (status, options) => { switch (status) { case 'success': return html`<div class="${options?.class ? options.class : 'abs center'}"> <i style="color: green" class="fa-solid fa-check"></i> </div>`; case 'error': return html`<div class="${options?.class ? options.class : 'abs center'}"> <i style="color: red" class="fa-solid fa-xmark"></i> </div>`; case 'warning': return html`<div class="${options?.class ? options.class : 'abs center'}"> <i style="color: yellow" class="fa-solid fa-triangle-exclamation"></i> </div>`; default: return html``; } }; const dynamicColTokens = {}; const dynamicCol = (options = { containerSelector: '', id: '', type: '', limit: 900 }) => { const { containerSelector, id } = options; const limitCol = options?.limit ? options.limit : 900; if (!(id in dynamicColTokens)) dynamicColTokens[id] = {}; dynamicColTokens[id].options = options; if (dynamicColTokens[id].observer) dynamicColTokens[id].observer.disconnect(); setTimeout(() => { dynamicColTokens[id].observer = new ResizeObserver(() => { if (s(`.${containerSelector}`)) { switch (options.type) { case 'a-50-b-50': if (s(`.${containerSelector}`).offsetWidth < limitCol) htmls( `.style-${id}-col`, css` .${id}-col-a, .${id}-col-b { width: 100%; } `, ); else htmls( `.style-${id}-col`, css` .${id}-col-a { width: 50%; } .${id}-col-b { width: 50%; } `, ); break; default: if (s(`.${containerSelector}`).offsetWidth < 900) htmls( `.style-${id}-col`, css` .${id}-col-a, .${id}-col-b { width: 100%; } `, ); else htmls( `.style-${id}-col`, css` .${id}-col-a { width: 30%; } .${id}-col-b { width: 70%; } `, ); break; } } else { dynamicColTokens[id].observer.disconnect(); delete dynamicColTokens[id]; if (s(`.style-${id}-col`)) s(`.style-${id}-col`).remove(); } }); dynamicColTokens[id].observer.observe(s(`.${containerSelector}`)); }); return html` <style class="style-${id}-col"></style>`; }; const renderBubbleDialog = async function ( options = { id: '', html: async () => '', classSelectors, triangleType: 'down', trianglePositionCss: '', triangleCss: '', triangleDim: 0, bubbleCss: '', }, ) { const { id, html } = options; let cssTrianglePosition = ` bottom: -45px; left: 5px; `; let whiteTriangleStyle = `top: 43%`; let blackTriangleStyle = ``; switch (options.triangleType) { case 'right': cssTrianglePosition = ` right: -40px; top: 5px; `; blackTriangleStyle = ` top: 43%; left: 57%; `; break; default: break; } if (options.trianglePositionCss) cssTrianglePosition = options.trianglePositionCss; return html` <div class="${options?.classSelectors ? options.classSelectors : 'inl'} bubble-dialog bubble-dialog-${id}" ${options.bubbleCss ? `style='${options.bubbleCss}'` : ''} > <style class="style-bubble-dialog-triangle-${id}"> .bubble-dialog-triangle-${id} { width: ${options.triangleDim ? options.triangleDim : 60}px; height: ${options.triangleDim ? options.triangleDim : 60}px; /* border: 2px solid red; */ box-sizing: border-box; ${cssTrianglePosition} } </style> <div class="abs bubble-dialog-triangle bubble-dialog-triangle-${id}"> <div class="abs center" style="${blackTriangleStyle}"> ${triangle[options?.triangleType ? options.triangleType : 'down']({ dim: 25, id: id + '-triangle-black', color: 'black', classList: 'inl', customStyle: options.triangleCss, })} </div> <div class="abs center" style="${whiteTriangleStyle}"> ${triangle[options?.triangleType ? options.triangleType : 'down']({ dim: 24, id: id + '-triangle-white', color: 'white', classList: 'inl', customStyle: options.triangleCss, })} </div> </div> ${await html()} </div>`; }; const typeWriter = async function ({ id, html, seconds, endHideBlink, container }) { if (!seconds) seconds = 2; return new Promise((resolve) => { // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function // https://www.w3schools.com/cssref/css3_pr_animation-fill-mode.php const typingAnimationTransitionStyle = [ `1s linear`, `${seconds}s steps(${html.split(' ').length * 6}, end)`, `1s forwards`, ]; const render = html` <style class="style-${id}"> .tw-${id}-typed-out { overflow: hidden; border-right: 0.15em solid orange; white-space: nowrap; animation: typing-${id} ${typingAnimationTransitionStyle[1]}, blink-caret-${id} 0.5s step-end infinite; animation-fill-mode: forwards; width: 0; } </style> <style> .tw-${id}-container { } @keyframes typing-${id} { from { width: 0; } to { width: 100%; } } @keyframes blink-caret-${id} { from, to { border-color: transparent; } 50% { border-color: orange; } } </style> <div class="inl tw-${id}-container"> <div class="tw-${id}-typed-out">${html}</div> </div> `; htmls(`.${container}`, render); setTimeout(() => { if (endHideBlink && s(`.style-${id}`)) s(`.style-${id}`).remove(); resolve(render); }, seconds * 1000); }); }; const renderCssAttr = (options) => `${ options && options.style ? Object.keys(options.style) .map((keyStyle) => `${keyStyle}: ${options.style[keyStyle]};`) .join(`\n`) : '' }`; const renderStyleTag = (styleSelector = 'style-abc', selector, options) => html`<style class="${styleSelector}"> ${selector} { ${renderCssAttr(options)} } </style>`; function getTranslate3d(el) { const values = el.style.transform.split(/\w+\(|\);?/); if (!values[1] || !values[1].length) { return []; } return values[1].split(/,\s?/g); } const dashRange = ({ selector, color }) => { return html` <style> .${selector} { background: linear-gradient(90deg, ${color} 50%, transparent 50%), linear-gradient(90deg, ${color} 50%, transparent 50%), linear-gradient(0deg, ${color} 50%, transparent 50%), linear-gradient(0deg, ${color} 50%, transparent 50%); background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; background-size: 16px 4px, 16px 4px, 4px 16px, 4px 16px; background-position: 0% 0%, 100% 100%, 0% 100%, 100% 0px; border-radius: 5px; padding: 10px; animation: ${selector}_dash_range 5s linear infinite; } @keyframes ${selector}_dash_range { to { background-position: 100% 0%, 0% 100%, 0% 0%, 100% 100%; } } </style> `; }; const triangle = { up: ({ id, dim, color, classList, customStyle }) => { return html`<style class="style-${id}"> .arrow-up-${id} { width: 0; height: 0; border-left: ${dim}px solid transparent; border-right: ${dim}px solid transparent; border-bottom: ${dim}px solid ${color}; } </style> <div class="arrow-up-${id} ${classList}" ${customStyle ? `style="${customStyle ? customStyle : ''}"` : ''} ></div>`; }, down: ({ id, dim, color, classList, customStyle }) => { return html`<style class="style-${id}"> .arrow-down-${id} { width: 0; height: 0; border-left: ${dim}px solid transparent; border-right: ${dim}px solid transparent; border-top: ${dim}px solid ${color}; } </style> <div class="arrow-down-${id} ${classList}" ${customStyle ? `style="${customStyle ? customStyle : ''}"` : ''} ></div>`; }, right: ({ id, dim, color, classList, customStyle }) => { return html` <style class="style-${id}"> .arrow-right-${id} { width: 0; height: 0; border-top: ${dim}px solid transparent; border-bottom: ${dim}px solid transparent; border-left: ${dim}px solid ${color}; } </style> <div class="arrow-right-${id} ${classList}" ${customStyle ? `style="${customStyle ? customStyle : ''}"` : ''} ></div>`; }, left: ({ id, dim, color, classList, customStyle }) => { return html`<style class="style-${id}"> .arrow-left-${id} { width: 0; height: 0; border-top: ${dim}px solid transparent; border-bottom: ${dim}px solid transparent; border-right: ${dim}px solid ${color}; } </style> <div class="arrow-left-${id} ${classList}" ${customStyle ? `style="${customStyle ? customStyle : ''}"` : ''} ></div>`; }, }; const getSectionsStringData = (offsetWidth, text) => { const sectionsIndex = []; const everyXChar = parseInt(offsetWidth / 4); const phraseArray = text .split('.') .map((t) => splitEveryXChar(t + '.', everyXChar, ['.', ' '])) .flat() .filter((p) => p !== '.' && p.trim()); let currentIndex = [0]; let pi = -1; for (const p of phraseArray) { pi++; if (p.indexOf('.') !== -1) { currentIndex.push(newInstance(pi)); sectionsIndex.push(newInstance(currentIndex)); if (phraseArray[pi + 1]) currentIndex = [newInstance(pi + 1)]; else currentIndex = []; } } if (currentIndex[0] && !currentIndex[1]) { currentIndex[1] = phraseArray.length - 1; sectionsIndex.push(newInstance(currentIndex)); } return { phraseArray, sectionsIndex }; }; const typeWriteSectionsString = ({ container, phraseArray, rangeArraySectionIndex }) => new Promise((resolve) => { let cumulativeSeconds = 0; for (const index of range(...rangeArraySectionIndex)) { const subIdSalt = s4() + s4() + s4(); const seconds = phraseArray[index].trim().length * 0.05; append(`.${container}`, html` <div class="${container}-${subIdSalt}"></div> `); setTimeout(async () => { if (s(`.${container}-${subIdSalt}`)) { append(`.${container}-${subIdSalt}`, html` <div class="render-typeWriter-${container}-${subIdSalt}"></div> `); await typeWriter({ id: `typeWriter-${index}-${container}`, html: phraseArray[index].trim(), endHideBlink: index < rangeArraySectionIndex[1], seconds, container: `render-typeWriter-${container}-${subIdSalt}`, }); } if (index === rangeArraySectionIndex[1]) resolve(); }, cumulativeSeconds * 1000); cumulativeSeconds += seconds; } }); const cssBrowserCodes = ['webkit', 'moz', 'ms', 'o']; const scrollBarLightRender = () => { return cssBrowserCodes .map( (b) => html`<style> ::-` + b + `-scrollbar { width: 5px; height: 5px; } /* Track */ ::-` + b + `-scrollbar-track { background: none !important; } /* Handle */ ::-` + b + `-scrollbar-thumb { background: #15151557; border-radius: 3px; } /* Handle on hover */ ::-` + b + `-scrollbar-thumb:hover { background: #4d4d4dbb; } </style>`, ) .join(''); }; const scrollBarDarkRender = () => { return cssBrowserCodes .map( (b) => html`<style> ::-` + b + `-scrollbar { width: 5px; height: 5px; /* line-height: 1em; */ } /* Track */ ::-` + b + `-scrollbar-track { background: none !important; } /* Handle */ ::-` + b + `-scrollbar-thumb { background: #74747457; border-radius: 3px; } /* Handle on hover */ ::-` + b + `-scrollbar-thumb:hover { background: #98989857; } </style>`, ) .join(''); }; const renderWave = ({ id }) => { return html` <style> .wave-animation-container-${id} { height: 200px; } .wave-animation-container-${id} { background: linear-gradient( 315deg, rgba(101, 0, 94, 1) 3%, rgba(60, 132, 206, 1) 38%, rgba(48, 238, 226, 1) 68%, rgba(255, 25, 25, 1) 98% ); animation: gradient-${id} 15s ease infinite; background-size: 400% 400%; overflow: hidden; } @keyframes gradient-${id} { 0% { background-position: 0% 0%; } 50% { background-position: 100% 100%; } 100% { background-position: 0% 0%; } } .wave-${id} { background: rgb(255 255 255 / 25%); border-radius: 1000% 1000% 0 0; width: 200%; height: 12em; animation: wave-${id} 10s -3s linear infinite; opacity: 0.8; bottom: 0; left: 0; top: 30%; } .wave-${id}:nth-of-type(2) { animation: wave-${id} 18s linear reverse infinite; opacity: 0.8; top: 50%; } .wave-${id}:nth-of-type(3) { animation: wave-${id} 20s -1s reverse infinite; opacity: 0.9; top: 70%; } @keyframes wave-${id} { 2% { transform: translateX(1); } 25% { transform: translateX(-25%); } 50% { transform: translateX(-50%); } 75% { transform: translateX(-25%); } 100% { transform: translateX(1); } } </style> <div class="in wave-animation-container-${id} ${id}"> <div class="in wave-${id}"></div> <div class="abs wave-${id}"></div> <div class="abs wave-${id}"></div> </div> `; }; const cssTokensEffect = {}; const cssTokensContainer = {}; const cssEffect = async (containerSelector, event) => { // Array.from(event.target.classList) let offsetX, offsetY; if (Array.from(event.srcElement.classList).includes('ripple') && cssTokensContainer[containerSelector]) { offsetX = cssTokensContainer[containerSelector].lastOffsetX; offsetY = cssTokensContainer[containerSelector].lastOffsetY; } else { cssTokensContainer[containerSelector] = { lastOffsetX: event.offsetX, lastOffsetY: event.offsetY }; offsetX = event.offsetX; offsetY = event.offsetY; } const element = s(containerSelector); // element.style.overflow = 'hidden'; const id = getId(cssTokensEffect, 'btn-effect-'); cssTokensEffect[id] = { containerSelector, event }; append(containerSelector, html`<span class="abs ${id} ripple" style="display: none"></span>`); const circle = s(`.${id}`); circle.style.width = circle.style.height = `40px`; circle.style.left = `${offsetX}px`; circle.style.top = `${offsetY}px`; circle.style.display = null; setTimeout(() => { circle.remove(); delete cssTokensEffect[id]; }, 600); }; const imageShimmer = () => html`<div class="abs center ssr-shimmer-search-box" style="${renderCssAttr({ style: { width: '95%', height: '95%', 'border-radius': '10px', overflow: 'hidden', }, })}" > <div class="abs center" style="${renderCssAttr({ style: { 'font-size': '70px', color: `#bababa`, }, })}" > <i class="fa-solid fa-photo-film"></i> </div> </div>`; export { Css, Themes, barLabels, barConfig, borderChar, renderMediaQuery, renderDefaultWindowsModalButtonContent, renderStatus, dynamicCol, dynamicColTokens, boxShadow, addTheme, darkTheme, ThemeEvents, TriggerThemeEvents, ThemesScope, renderBubbleDialog, typeWriter, renderStyleTag, renderCssAttr, getTranslate3d, dashRange, triangle, getSectionsStringData, typeWriteSectionsString, cssBrowserCodes, scrollBarDarkRender, scrollBarLightRender, renderWave, cssEffect, imageShimmer, };