mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-23 23:11:12 +02:00
71
src/components/Dropdown.jsx
Normal file
71
src/components/Dropdown.jsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
|
const DropdownMenu = ({
|
||||||
|
btnProps = {},
|
||||||
|
btnContent,
|
||||||
|
menuItems,
|
||||||
|
position = 'top'
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
const toggleDropdown = () => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = event => {
|
||||||
|
if (
|
||||||
|
menuRef.current &&
|
||||||
|
!menuRef.current.contains(event.target) &&
|
||||||
|
!triggerRef.current.contains(event.target)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dropdown">
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
{...btnProps}
|
||||||
|
className={`dropdown-trigger ${btnProps?.className}`}
|
||||||
|
>
|
||||||
|
{btnContent}
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<ul
|
||||||
|
ref={menuRef}
|
||||||
|
role="menu"
|
||||||
|
className={`popup dropdown-menu dropdown-menu-${position}`}
|
||||||
|
>
|
||||||
|
{menuItems.map((item, index) => (
|
||||||
|
<li key={index} role="menuitem">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
item.onClick();
|
||||||
|
}}
|
||||||
|
className="dropdown-item"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DropdownMenu };
|
@@ -4,6 +4,7 @@ import { I18n } from '@lingui/react';
|
|||||||
import { ProBadge } from './ProBadge';
|
import { ProBadge } from './ProBadge';
|
||||||
import { HStack } from './Stack';
|
import { HStack } from './Stack';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import { DropdownMenu } from './Dropdown';
|
||||||
|
|
||||||
const JS13K = props => {
|
const JS13K = props => {
|
||||||
const [daysLeft, setDaysLeft] = useState(0);
|
const [daysLeft, setDaysLeft] = useState(0);
|
||||||
@@ -201,16 +202,36 @@ export const Footer = props => {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div class="footer__right">
|
<div class="footer__right">
|
||||||
<button
|
<DropdownMenu
|
||||||
onClick={props.saveHtmlBtnClickHandler}
|
triggerText="More"
|
||||||
id="saveHtmlBtn"
|
menuItems={[
|
||||||
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
|
{
|
||||||
aria-label={i18n._(t`Save as HTML file`)}
|
label: 'Download HTML',
|
||||||
>
|
onClick: () => {
|
||||||
<svg viewBox="0 0 24 24">
|
props.saveHtmlBtnClickHandler();
|
||||||
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
|
}
|
||||||
</svg>
|
},
|
||||||
</button>
|
{
|
||||||
|
label: 'Download HTML (assets inlined)',
|
||||||
|
onClick: () => {
|
||||||
|
props.saveHtmlBtnClickHandler(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
position="top"
|
||||||
|
btnProps={{
|
||||||
|
id: 'saveHtmlBtn',
|
||||||
|
className:
|
||||||
|
'mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode',
|
||||||
|
ariaLabel: i18n._(t`Save as HTML file`)
|
||||||
|
}}
|
||||||
|
btnContent={
|
||||||
|
<svg viewBox="0 0 24 24">
|
||||||
|
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
|
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
|
||||||
<symbol id="codepen-logo" viewBox="0 0 120 120">
|
<symbol id="codepen-logo" viewBox="0 0 120 120">
|
||||||
<path
|
<path
|
||||||
|
@@ -1245,8 +1245,8 @@ export default class App extends Component {
|
|||||||
trackEvent('ui', 'openInCodepen');
|
trackEvent('ui', 'openInCodepen');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
saveHtmlBtnClickHandler(e) {
|
saveHtmlBtnClickHandler(inlineAssets) {
|
||||||
saveAsHtml(this.state.currentItem);
|
saveAsHtml(this.state.currentItem, { inlineAssets });
|
||||||
trackEvent('ui', 'saveHtmlClick');
|
trackEvent('ui', 'saveHtmlClick');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@@ -1099,6 +1099,7 @@ body:not(.light-version) .modal {
|
|||||||
bottom: calc(100% + 0.2rem);
|
bottom: calc(100% + 0.2rem);
|
||||||
transition: 0.25s ease;
|
transition: 0.25s ease;
|
||||||
}
|
}
|
||||||
|
.popup,
|
||||||
.modal__content {
|
.modal__content {
|
||||||
--opaque: 68%;
|
--opaque: 68%;
|
||||||
background: var(--color-popup);
|
background: var(--color-popup);
|
||||||
@@ -2422,6 +2423,51 @@ while the theme CSS file is loading */
|
|||||||
} */
|
} */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* DROPDOWN */
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
--opaque: 68%;
|
||||||
|
background: var(--color-popup);
|
||||||
|
width: max-content;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-top {
|
||||||
|
bottom: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-bottom {
|
||||||
|
top: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover,
|
||||||
|
.dropdown-item:focus {
|
||||||
|
background-color: #f0f0f033;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
|
79
src/utils.js
79
src/utils.js
@@ -430,30 +430,35 @@ export function getCompleteHtml(html, css, js, item, isForExport) {
|
|||||||
'"></script>';
|
'"></script>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof js === 'string') {
|
if (js) {
|
||||||
contents += js ? '<script>\n' + js + '\n//# sourceURL=userscript.js' : '';
|
if (typeof js === 'string') {
|
||||||
} else {
|
contents += js ? '<script>\n' + js + '\n//# sourceURL=userscript.js' : '';
|
||||||
var origin = chrome.i18n.getMessage()
|
} else {
|
||||||
? `chrome-extension://${chrome.i18n.getMessage('@@extension_id')}`
|
var origin = chrome.i18n.getMessage()
|
||||||
: `${location.origin}`;
|
? `chrome-extension://${chrome.i18n.getMessage('@@extension_id')}`
|
||||||
contents +=
|
: `${location.origin}`;
|
||||||
'<script src="' + `filesystem:${origin}/temporary/script.js` + '">';
|
contents +=
|
||||||
|
'<script src="' + `filesystem:${origin}/temporary/script.js` + '">';
|
||||||
|
}
|
||||||
|
contents += '\n</script>';
|
||||||
}
|
}
|
||||||
contents += '\n</script>\n</body>\n</html>';
|
contents += '\n</body>\n</html>';
|
||||||
|
|
||||||
return contents;
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveAsHtml(item) {
|
export function saveAsHtml(item, { inlineAssets }) {
|
||||||
var htmlPromise = computeHtml(item.html, item.htmlMode);
|
var htmlPromise = computeHtml(item.html, item.htmlMode);
|
||||||
var cssPromise = computeCss(item.css, item.cssMode);
|
var cssPromise = computeCss(item.css, item.cssMode);
|
||||||
var jsPromise = computeJs(item.js, item.jsMode, false);
|
var jsPromise = computeJs(item.js, item.jsMode, false);
|
||||||
Promise.all([htmlPromise, cssPromise, jsPromise]).then(result => {
|
Promise.all([htmlPromise, cssPromise, jsPromise]).then(async result => {
|
||||||
var html = result[0].code,
|
var html = result[0].code,
|
||||||
css = result[1].code,
|
css = result[1].code,
|
||||||
js = result[2].code;
|
js = result[2].code;
|
||||||
|
|
||||||
var fileContent = getCompleteHtml(html, css, js, item, true);
|
var fileContent = inlineAssets
|
||||||
|
? await inlineAssetsInHtml(getCompleteHtml(html, css, js, item, true))
|
||||||
|
: getCompleteHtml(html, css, js, item, true);
|
||||||
|
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
var fileName = [
|
var fileName = [
|
||||||
@@ -480,6 +485,56 @@ export function saveAsHtml(item) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function inlineAssetsInHtml(html) {
|
||||||
|
const encodeFileToBase64 = async url => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64String = reader.result.split(',')[1];
|
||||||
|
const mimeType = blob.type;
|
||||||
|
resolve(`data:${mimeType};base64,${base64String}`);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const inlineAssets = async htmlContent => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||||
|
|
||||||
|
const processElement = async (element, attr) => {
|
||||||
|
const url = element.getAttribute(attr);
|
||||||
|
if (url && !url.startsWith('data:')) {
|
||||||
|
try {
|
||||||
|
const encodedData = await encodeFileToBase64(url);
|
||||||
|
element.setAttribute(attr, encodedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to inline ${url}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const images = Array.from(doc.querySelectorAll('img'));
|
||||||
|
const audios = Array.from(doc.querySelectorAll('audio'));
|
||||||
|
const videos = Array.from(doc.querySelectorAll('video'));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...images.map(img => processElement(img, 'src')),
|
||||||
|
...audios.map(audio => processElement(audio, 'src')),
|
||||||
|
...videos.map(video => processElement(video, 'src'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
return doc.documentElement.outerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = await inlineAssets(html);
|
||||||
|
// console.log(html, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
export function handleDownloadsPermission() {
|
export function handleDownloadsPermission() {
|
||||||
var d = deferred();
|
var d = deferred();
|
||||||
if (!window.IS_EXTENSION) {
|
if (!window.IS_EXTENSION) {
|
||||||
|
Reference in New Issue
Block a user