來做一個html生成器

一些和AI相关的讨论
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

當前代碼:

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejwoom HTML Editor</title>
    <style>
        body {font-family:system-ui,sans-serif;margin:0;padding:0;background:#f5f5f5;}
        .ejwoomtab {display:flex;background:#333;color:white;}
        .ejwoomtab span {padding:12px 20px;cursor:pointer;}
        .ejwoomtab span.active {background:#007bff;}
        
        .ejwoomedit, .ejwoomresult {
            padding:20px;background:white;min-height:100vh;display:none;
        }
        .ejwoomedit.active, .ejwoomresult.active {display:block;}
        
        .ejwoomcontrol {
            margin-bottom:15px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;
        }
        .ejwoomcontrol span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;
        }
        .ejwoomcontrol span:hover {background:#ddd;}
        
        .ejwoomtitle {
            width:100%;padding:10px;font-size:18px;border:1px solid #ccc;
            border-radius:4px;box-sizing:border-box;margin-bottom:15px;
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;margin-bottom:15px;
        }
        .ejwoombuttons span {
            padding:3px 7px;background:#007bff;color:white;border:none;
            border-radius:4px;cursor:pointer;font-size:14px;
        }
        .ejwoombuttons span:hover {background:#0056b3;}
        
        .ejwoomcontent {
            width:100%;height:70vh;padding:12px;font-size:16px;
            font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
            border:1px solid #ccc;border-radius:4px;box-sizing:border-box;
            resize:vertical;
        }
        
        .ejwoomattach {
            width:36px;height:36px;background:#28a745;color:white;border:none;
            border-radius:8px;font-size:20px;cursor:pointer;
            display:flex;align-items:center;justify-content:center;margin-top:10px;
        }
        
        .ejwoomattached {
            display:flex;align-items:center;gap:8px;
            background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
            padding:8px;margin:8px 0;font-size:14px;cursor:pointer;
        }
        .wmattachtype {
            background:#e3f2fd;line-height:24px;border-radius:4px;
            font-weight:bold;color:#1976d2;padding:0 7px;text-align:center;
        }
        .wmattachname {
            flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap;background:#f5f5f5;padding:0 8px;
            border-radius:4px;color:#333;margin-right:8px;
        }
        .wmattachsize {
            background:#e8f5e8;color:#2e7d32;font-weight:500;
            padding:2px 6px;border-radius:4px;margin-right:8px;font-size:12px;
        }
        .wmattachdelete {
            background:#ffebee;color:#d32f2f;width:24px;height:24px;
            border-radius:50%;line-height:22px!important;font-size:18px;
            text-align:center;cursor:pointer;
        }
        
        .ejwoomdownload {margin-bottom:20px;}
        .ejwoomdownload a {
            padding:10px 16px;background:#28a745;color:white;
            text-decoration:none;border-radius:6px;display:inline-block;
        }
        .ejwoomdownloadsize {
            margin-left:15px;color:#666;font-size:14px;
        }
        
        .ejwoompreview {
            border:1px solid #ddd;padding:20px;background:white;
            border-radius:6px;min-height:70vh;overflow:auto;
        }

        /* Custom Button Styles */
        .ejwoomaddbutton {
            padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;font-weight:bold;
        }
        .ejwoomaddbutton:hover {background:#5a6268;}
        .ejwoomcustombutton {
            padding:3px 7px;background:#17a2b8;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;
        }
        .ejwoomcustombutton:hover {background:#138496;}

        /* Modal Window */
        .ejwoomwindow {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoomwindow.active {display:flex;}
        .ejwoomwindow-content {
            background:white;padding:28px 32px;border-radius:12px;
            width:90%;max-width:540px;max-height:90vh;overflow-y:auto;
            box-shadow:0 20px 40px rgba(0,0,0,0.22);
            display:flex;flex-direction:column;gap:18px;
            font-family:system-ui,sans-serif;
        }
        .ejwoom-btn-selector {
            display:flex;flex-wrap:wrap;gap:10px;
            border-bottom:1px solid #eee;padding-bottom:15px;
        }
        .ejwoomselbutton {
            padding:6px 14px;background:#e9ecef;border-radius:6px;
            cursor:pointer;user-select:none;transition:all 0.2s;font-weight:500;
        }
        .ejwoomselbutton:hover {background:#dee2e6;}
        .ejwoomselbutton.selected {
            background:#007bff;color:white;
        }
        .ejwoomselbutton.selected:hover {background:#0056b3;}
        .ejwoom-input-row {
            display:flex;align-items:center;gap:10px;
        }
        .ejwoom-input-row label {flex-basis:100px;flex-shrink:0;}
        .ejwoom-input-row input[type="text"] {
            flex-grow:1;padding:8px;border:1px solid #ccc;border-radius:4px;
        }
        .ejwoom-modal-controls {
            display:flex;gap:10px;justify-content:flex-end;
            padding-top:10px;border-top:1px solid #eee;
        }
        .ejwoom-modal-controls button {
            border:none;padding:8px 16px;border-radius:4px;
            cursor:pointer;color:white;font-weight:500;
        }
        .wmconfirm {background:#007bff;}
        .wmconfirm:hover {background:#0056b3;}
        .wmdelete {background:#dc3545;}
        .wmdelete:hover {background:#c82333;}
        .wmcancel {background:#6c757d;}
        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}
/* Read File Tab */
        .ejwoomreadfile {
            padding: 20px;
            background: white;
            min-height: 100vh;
        }
        .ejwoomreadarea {
            border: 3px dashed #28a745;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            font-size: 1.2em;
            color: #555;
            cursor: pointer;
            transition: all 0.3s;
        }
        .ejwoomreadarea.selected {
            border-color: #fd7e14; /* Orange */
            color: #333;
            font-weight: bold;
        }
        .ejwoomreadreset {
            margin: -10px 0 15px 0;
            color: #007bff;
            cursor: pointer;
            text-decoration: underline;
            font-size: 14px;
        }
        .ejwoomreadoptions {
            display: none; /* Hidden by default */
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            gap: 10px;
            flex-wrap: wrap;
        }
        .ejwoomreadoptions a {
            padding: 10px 16px;
            background: #007bff; /* Blue */
            color: white;
            text-decoration: none;
            border-radius: 6px;
            display: inline-block;
            cursor: pointer;
        }
        .ejwoomreadoptions a:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview &amp; Download</span>
    <span class="ejwoomreadfiletab">Read File</span>
</div>

<div class="ejwoomedit active">
    <div class="ejwoomcontrol">
        <span class="ejwoomundo">Undo</span>
        <span class="ejwoomredo">Redo</span>
    </div>
    <input type="text" class="ejwoomtitle" placeholder="Enter title...">
    <div class="ejwoombuttons">
        <span>p</span>
        <span>b</span><span>u</span><span>s</span><span>i</span>
        <span>ul</span><span>ol</span><span>li</span>
        <span>h1</span><span>h2</span><span>h3</span><span>h4</span><span>h5</span><span>h6</span>
        <span class="ejwoomaddbutton">+</span>
    </div>
    <textarea class="ejwoomcontent" placeholder="Enter HTML content..."></textarea>
    <div id="attachments"></div>
    <button class="ejwoomattach" title="Attach file">+</button>
</div>

<div class="ejwoomresult"></div>

<input type="file" id="attachfile" style="display:none;">
<input type="file" id="readhtmlfile" style="display:none;" accept=".html,.htm">

<script>
document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
    const resultTab = document.querySelector('.ejwoomresulttab');
    const readfileTab = document.querySelector('.ejwoomreadfiletab'); // New
    const editDiv = document.querySelector('.ejwoomedit');
    const resultDiv = document.querySelector('.ejwoomresult');
    // .ejwoomreadfile is created dynamically

    const titleInput = document.querySelector('.ejwoomtitle');
    const textarea = document.querySelector('.ejwoomcontent');
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const readHtmlInput = document.getElementById('readhtmlfile'); // New
    const attachmentsContainer = document.getElementById('attachments');
    const buttonsContainer = document.querySelector('.ejwoombuttons');
    const addButton = document.querySelector('.ejwoomaddbutton');

    const attachedFiles = new Map();
    const imageExts = new Set(['png','jpg','jpeg','gif','svg','webp','bmp','ico']);
    
    let loadedHtmlContent = null; // New: To store read file content

    // === Helpers ===
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    function formatBytes(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }

    // New: Convert Data URL back to File object
    function dataURLtoFile(dataurl, filename) {
        try {
            let arr = dataurl.split(','),
                mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), 
                n = bstr.length, 
                u8arr = new Uint8Array(n);
            while(n--){
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, {type:mime});
        } catch (e) {
            console.error("Error converting data URL to file:", e);
            return null;
        }
    }

    // === Attachment System ===
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const ext = getExt(file.name);
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        typeSpan.textContent = (imageExts.has(ext) ? 'img' : 'file') + '/' + ext;

        const nameSpan = document.createElement('span');
        nameSpan.className = 'wmattachname';
        nameSpan.textContent = file.name;
        nameSpan.title = file.name;

        const sizeSpan = document.createElement('span');
        sizeSpan.className = 'wmattachsize';
        sizeSpan.textContent = formatBytes(file.size);

        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
            e.stopPropagation();

            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');

            row.remove();
            attachedFiles.delete(file.name);
        };
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };

        row.onclick = (e) => {
            if (e.target === deleteSpan) return;
            insertRef();
        };

        row.append(typeSpan, nameSpan, sizeSpan, deleteSpan);
        return row;
    }

    attachBtn.onclick = () => attachInput.click();
    attachInput.onchange = e => {
        for (let file of e.target.files) {
            if (attachedFiles.has(file.name)) {
                alert(`File "${file.name}" is already attached.`);
                continue;
            }
            attachmentsContainer.appendChild(createAttachmentRow(file));
            attachedFiles.set(file.name, file);
        }
        attachInput.value = '';
    };

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }

    // Default buttons
    document.querySelectorAll('.ejwoombuttons span:not(.ejwoomaddbutton):not(.ejwoomcustombutton)').forEach(span => {
        span.onclick = () => wrapSelection(`<${span.textContent}>`, `</${span.textContent}>`);
    });

    // New: Refactored function to apply click logic to custom buttons
    function applyCustomButtonLogic(targetSpan) {
        targetSpan.onclick = () => {
            const beforeRaw = targetSpan.dataset.before ? atob(targetSpan.dataset.before) : '';
            const afterRaw = targetSpan.dataset.after ? atob(targetSpan.dataset.after) : '';

            if (beforeRaw === '' && afterRaw === '') {
                // If both before and after are empty, use <name>selected text</name>
                const name = targetSpan.textContent.trim();
                wrapSelection(`<${name}>`, `</${name}>`);
            } else {
                // Otherwise use custom before/after (even if one is empty)
                wrapSelection(beforeRaw, afterRaw);
            }
        };
    }

    // === Custom Button Editor (Fixed!) ===
    addButton.onclick = function () {
        const modal = document.createElement('div');
        modal.className = 'ejwoomwindow active';
        modal.innerHTML = `
            <div class="ejwoomwindow-content">
                <div class="ejwoom-btn-selector">
                    <span class="ejwoomselbutton selected" data-value="new">new</span>
                </div>
                <div class="ejwoom-input-row"><label>Name</label><input type="text" placeholder="e.g. blockquote"></div>
                <div class="ejwoom-input-row"><label>Before HTML</label><input type="text" placeholder="&lt;div class=&quot;tip&quot;&gt;"></div>
                <div class="ejwoom-input-row"><label>After HTML</label><input type="text" placeholder="&lt;/div&gt;"></div>
                <div class="ejwoom-modal-controls">
                    <button class="wmconfirm">Confirm</button>
                    <button class="wmdelete" disabled>Delete</button>
                    <button class="wmcancel">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(modal);

        const selector = modal.querySelector('.ejwoom-btn-selector');
        const nameInp = modal.querySelectorAll('input')[0];
        const beforeInp = modal.querySelectorAll('input')[1];
        const afterInp = modal.querySelectorAll('input')[2];
        const confirmBtn = modal.querySelector('.wmconfirm');
        const deleteBtn = modal.querySelector('.wmdelete');
        const cancelBtn = modal.querySelector('.wmcancel');

        // Load existing custom buttons into selector
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => {
            const sel = document.createElement('span');
            sel.className = 'ejwoomselbutton';
            sel.textContent = btn.textContent;
            sel.dataset.value = btn.textContent;
            sel._target = btn;
            selector.appendChild(sel);
        });

        function updateForm() {
            const selected = selector.querySelector('.selected');
            if (!selected || selected.dataset.value === 'new') {
                nameInp.value = beforeInp.value = afterInp.value = '';
                deleteBtn.disabled = true;
            } else {
                const target = selected._target;
                nameInp.value = target.textContent;
                beforeInp.value = target.dataset.before ? atob(target.dataset.before) : '';
                afterInp.value = target.dataset.after ? atob(target.dataset.after) : '';
                deleteBtn.disabled = false;
            }
        }

        selector.onclick = e => {
            const btn = e.target.closest('.ejwoomselbutton');
            if (!btn) return;
            selector.querySelectorAll('.ejwoomselbutton').forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            updateForm();
        };

        deleteBtn.onclick = () => {
            const selected = selector.querySelector('.selected');
            if (selected && selected.dataset.value !== 'new') {
                selected._target.remove();
                selected.remove();
                selector.querySelector('[data-value="new"]').classList.add('selected');
                updateForm();
            }
        };

        confirmBtn.onclick = () => {
            const name = nameInp.value.trim();
            if (!name) return alert('Button name is required');

            const selected = selector.querySelector('.selected');
            let targetSpan;

            if (selected.dataset.value === 'new') {
                targetSpan = document.createElement('span');
                targetSpan.className = 'ejwoomcustombutton';
                buttonsContainer.insertBefore(targetSpan, addButton);

                const newSel = document.createElement('span');
                newSel.className = 'ejwoomselbutton';
                newSel.textContent = name;
                newSel.dataset.value = name;
                newSel._target = targetSpan;
                selector.appendChild(newSel);
            } else {
                targetSpan = selected._target;
            }

            targetSpan.textContent = name;
            if (beforeInp.value) targetSpan.dataset.before = btoa(beforeInp.value);
            else delete targetSpan.dataset.before;
            if (afterInp.value) targetSpan.dataset.after = btoa(afterInp.value);
            else delete targetSpan.dataset.after;

            // Apply click behavior
            applyCustomButtonLogic(targetSpan);
            modal.remove();
        };

        cancelBtn.onclick = () => modal.remove();
        updateForm();
    };

    // === Preview & Download (Fixed filename + [img:] [file:]) ===
    async function buildResult() {
        // Requirement 2: Collect all custom buttons to inject as <meta> in the final HTML
        const customButtons = Array.from(buttonsContainer.querySelectorAll('.ejwoomcustombutton'));
        let metaTagHTML = '';
        if (customButtons.length > 0) {
            const allButtonsHTML = customButtons.map(btn => btn.outerHTML.trim()).join('\n');
            try {
                const buttonsBase64 = btoa(allButtonsHTML);
                metaTagHTML = `<meta name="ejwoombuttons" content="${buttonsBase64}">`;
            } catch (e) {
                console.error("Error encoding buttons:", e);
            }
        }

        resultDiv.innerHTML = `
            <div class="ejwoomdownload">
                <a id="downloadlink" download="">Download HTML</a>
                <span class="ejwoomdownloadsize"></span>
            </div>
            <div class="ejwoompreview"></div>
        `;


        const downloadLink = resultDiv.querySelector('#downloadlink');
        const sizeSpan = resultDiv.querySelector('.ejwoomdownloadsize');
        const previewDiv = resultDiv.querySelector('.ejwoompreview');

        let title = (titleInput.value || 'Untitled').trim();
        let processedContent = textarea.value.trim();
        const replacements = new Map();

        for (const [name, file] of attachedFiles) {
            const base64 = await fileToBase64(file);
            const ext = getExt(name);
            const isImage = imageExts.has(ext);

            let html = '';
            if (isImage) {
                let dims = '';
                if (ext === 'svg') {
                    try {
                        const svg = atob(base64.split(',')[1]);
                        const w = svg.match(/width=["'](\d+)["']/i);
                        const h = svg.match(/height=["'](\d+)["']/i);
                        if (w) dims += ` width="${w[1]}"`;
                        if (h) dims += ` height="${h[1]}"`;
                    } catch (e) {}
                }
                html = `<img src="${base64}" alt="${name}" style="max-width:100%;height:auto;"${dims}>`;
            } else {
                html = `<a href="${base64}" download="${name}">Download ${name}</a>`;
            }
            replacements.set(`[img:${name}]`, html);
            replacements.set(`[file:${name}]`, html);
        }

        for (const [key, val] of replacements) {
            processedContent = processedContent.replace(
                new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), val
            );
        }

        previewDiv.innerHTML = processedContent;

        const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
</head>
<body>
${processedContent}
</body>
</html>`;

        const blob = new Blob([fullHTML], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = title + '.html';
        downloadLink.textContent = 'Download ' + title + '.html';
        sizeSpan.textContent = formatBytes(blob.size);
    }
    
    // === NEW: Read File Functions ===
    
    // 6.6: Parse HTML, extract attachments, and replace with placeholders
    function rebuildAttachmentsFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        
        // Clear existing attachments
        attachmentsContainer.innerHTML = '';
        attachedFiles.clear();
        
        // Find all data-based images and links
        tempDiv.querySelectorAll('img[src^="data:"], a[href^="data:"][download]').forEach(el => {
            const dataUrl = el.tagName === 'IMG' ? el.getAttribute('src') : el.getAttribute('href');
            const filename = el.tagName === 'IMG' ? el.getAttribute('alt') : el.getAttribute('download');
            
            if (!filename) {
                console.warn('Found data URL element without alt/download name. Skipping.', el);
                return;
            }
            
            if (attachedFiles.has(filename)) {
                 // Already processed a file with this name, just replace
                 const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                 el.parentNode.replaceChild(document.createTextNode(placeholder), el);
                 return;
            }
            
            const file = dataURLtoFile(dataUrl, filename);
            if (file) {
                // Add to attachment systems
                attachedFiles.set(file.name, file);
                attachmentsContainer.appendChild(createAttachmentRow(file));
                
                // Replace element with placeholder
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
            }
        });
        
        // Return the HTML string with placeholders
        return tempDiv.innerHTML;
    }

    // 6.5: Load buttons from a parsed HTML document
    function parseAndLoadButtons(htmlDoc) {
        // Clear existing custom buttons
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => btn.remove());
        
        // Requirement 2: Read from meta tag
        const meta = htmlDoc.head.querySelector('meta[name="ejwoombuttons"]');
        if (meta && meta.content) {
            try {
                const buttonsHTML = atob(meta.content);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = buttonsHTML;
                
                Array.from(tempDiv.children).forEach(node => {
                    if (node.tagName === 'SPAN' && node.classList.contains('ejwoomcustombutton')) {
                        // node is newly created by innerHTML, just append it
                        buttonsContainer.insertBefore(node, addButton);
                        applyCustomButtonLogic(node); // Re-apply click logic
                    }
                });
            } catch (e) {
                console.error("Error parsing custom buttons from meta tag:", e);
            }
        }
    }
    
    // 6.4: Get body HTML
    function parseAndLoadHTML(htmlDoc) {
        const body = htmlDoc.body;
        if (!body) return '';
        
        // Template removal is no longer needed as it's in the head.
        
        // Also try to find title
        const titleTag = htmlDoc.head.querySelector('title');
        if (titleTag && titleTag.textContent) {
            titleInput.value = titleTag.textContent;
        }

        return body.innerHTML.trim(); // Use innerHTML to skip the <body> tag itself
    }
    
    // Switch to Edit Tab helper
    function switchToEditTab() {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        editTab.classList.add('active');
        editDiv.classList.add('active');
        resultDiv.classList.remove('active');
        resultDiv.innerHTML = '';
        
        const readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
    }
    
    // === Tab Switching ===
    resultTab.onclick = () => {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        resultTab.classList.add('active');
        editDiv.classList.remove('active');
        resultDiv.classList.add('active');
        
        const readDiv = document.querySelector('.ejwoomreadfile'); // New
        if (readDiv) readDiv.remove(); // New
        
        buildResult();
    };

    editTab.onclick = () => {
        switchToEditTab(); // Use helper
    };
    
    // New: Read File Tab click
    readfileTab.onclick = () => {
        // Set active tab
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        readfileTab.classList.add('active');
        
        // Hide other main divs
        editDiv.classList.remove('active');
        resultDiv.classList.remove('active');
        
        // Clean up old read div if it exists
        let readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
        
        // Create new read file UI
        readDiv = document.createElement('div');
        readDiv.className = 'ejwoomreadfile';
        readDiv.innerHTML = `
            <div class="ejwoomreadarea">Open the HTML file that you saved before.</div>
            <div class="ejwoomreadoptions">
                <a class="ejwoomreadall">Read All0</a>
                <a class="ejwoomreadhtml">Read HTML Only</a>
                <a class="ejwoomreadbutton">Read Buttons Only</a>
            </div>
        `;
        document.body.appendChild(readDiv);
        
        const readArea = readDiv.querySelector('.ejwoomreadarea');
        const readOptions = readDiv.querySelector('.ejwoomreadoptions');
        
        // Reset function
        function resetReadFileUI() {
            readArea.innerHTML = 'Open the HTML file that you saved before.';
            readArea.classList.remove('selected');
            readOptions.style.display = 'none';
            const resetBtn = readDiv.querySelector('.ejwoomreadreset');
            if (resetBtn) resetBtn.remove();
            readHtmlInput.value = ''; // Clear file input
            loadedHtmlContent = null;
        }
        
        // Read area click
        readArea.onclick = () => readHtmlInput.click();
        
        // Read file input change
        readHtmlInput.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (ev) => {
                loadedHtmlContent = ev.target.result;
                
                // Update UI
                readArea.textContent = file.name;
                readArea.classList.add('selected');
                readOptions.style.display = 'flex';
                
                // Add reset button if it doesn't exist
                if (!readDiv.querySelector('.ejwoomreadreset')) {
                    const resetBtn = document.createElement('div');
                    resetBtn.className = 'ejwoomreadreset';
                    resetBtn.textContent = 'Reset file selection.';
                    resetBtn.onclick = resetReadFileUI;
                    readDiv.insertBefore(resetBtn, readArea);
                }
            };
            reader.onerror = () => {
                alert('Error reading file.');
                resetReadFileUI();
            };
            reader.readAsText(file);
        };
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            
            switchToEditTab();
        };
    };
});
</script>
</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

當前代碼(已改好第四點):

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejwoom HTML Editor</title>
    <style>
        body {font-family:system-ui,sans-serif;margin:0;padding:0;background:#f5f5f5;}
        .ejwoomtab {display:flex;background:#333;color:white;}
        .ejwoomtab span {padding:12px 20px;cursor:pointer;}
        .ejwoomtab span.active {background:#007bff;}
        
        .ejwoomedit, .ejwoomresult {
            padding:20px;background:white;min-height:100vh;display:none;
        }
        .ejwoomedit.active, .ejwoomresult.active {display:block;}
        
        .ejwoomcontrol {
            margin-bottom:15px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;
        }
        .ejwoomcontrol span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;
        }
        .ejwoomcontrol span:hover {background:#ddd;}
        
        .ejwoomtitle {
            width:100%;padding:10px;font-size:18px;border:1px solid #ccc;
            border-radius:4px;box-sizing:border-box;margin-bottom:15px;
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;margin-bottom:15px;
        }
        .ejwoombuttons span {
            padding:3px 7px;background:#007bff;color:white;border:none;
            border-radius:4px;cursor:pointer;font-size:14px;
        }
        .ejwoombuttons span:hover {background:#0056b3;}
        
        .ejwoomcontent {
            width:100%;height:70vh;padding:12px;font-size:16px;
            font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
            border:1px solid #ccc;border-radius:4px;box-sizing:border-box;
            resize:vertical;
        }
        
        .ejwoomattach {
            width:36px;height:36px;background:#28a745;color:white;border:none;
            border-radius:8px;font-size:20px;cursor:pointer;
            display:flex;align-items:center;justify-content:center;margin-top:10px;
        }
        
        .ejwoomattached {
            display:flex;align-items:center;gap:8px;
            background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
            padding:8px;margin:8px 0;font-size:14px;cursor:pointer;
        }
        .wmattachtype {
            background:#e3f2fd;line-height:24px;border-radius:4px;
            font-weight:bold;color:#1976d2;padding:0 7px;text-align:center;
        }
        .wmattachname {
            flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap;background:#f5f5f5;padding:0 8px;
            border-radius:4px;color:#333;margin-right:8px;
        }
        .wmattachsize {
            background:#e8f5e8;color:#2e7d32;font-weight:500;
            padding:2px 6px;border-radius:4px;margin-right:8px;font-size:12px;
        }
        .wmattachdelete {
            background:#ffebee;color:#d32f2f;width:24px;height:24px;
            border-radius:50%;line-height:22px!important;font-size:18px;
            text-align:center;cursor:pointer;
        }
        
        .ejwoomdownload {margin-bottom:20px;}
        .ejwoomdownload a {
            padding:10px 16px;background:#28a745;color:white;
            text-decoration:none;border-radius:6px;display:inline-block;
        }
        .ejwoomdownloadsize {
            margin-left:15px;color:#666;font-size:14px;
        }
        
        .ejwoompreview {
            border:1px solid #ddd;padding:20px;background:white;
            border-radius:6px;min-height:70vh;overflow:auto;
        }

        /* Custom Button Styles */
        .ejwoomaddbutton {
            padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;font-weight:bold;
        }
        .ejwoomaddbutton:hover {background:#5a6268;}
        .ejwoomcustombutton {
            padding:3px 7px;background:#17a2b8;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;
        }
        .ejwoomcustombutton:hover {background:#138496;}

        /* Modal Window */
        .ejwoomwindow {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoomwindow.active {display:flex;}
        .ejwoomwindow-content {
            background:white;padding:28px 32px;border-radius:12px;
            width:90%;max-width:540px;max-height:90vh;overflow-y:auto;
            box-shadow:0 20px 40px rgba(0,0,0,0.22);
            display:flex;flex-direction:column;gap:18px;
            font-family:system-ui,sans-serif;
        }
        .ejwoom-btn-selector {
            display:flex;flex-wrap:wrap;gap:10px;
            border-bottom:1px solid #eee;padding-bottom:15px;
        }
        .ejwoomselbutton {
            padding:6px 14px;background:#e9ecef;border-radius:6px;
            cursor:pointer;user-select:none;transition:all 0.2s;font-weight:500;
        }
        .ejwoomselbutton:hover {background:#dee2e6;}
        .ejwoomselbutton.selected {
            background:#007bff;color:white;
        }
        .ejwoomselbutton.selected:hover {background:#0056b3;}
        .ejwoom-input-row {
            display:flex;align-items:center;gap:10px;
        }
        .ejwoom-input-row label {flex-basis:100px;flex-shrink:0;}
        .ejwoom-input-row input[type="text"] {
            flex-grow:1;padding:8px;border:1px solid #ccc;border-radius:4px;
        }
        .ejwoom-modal-controls {
            display:flex;gap:10px;justify-content:flex-end;
            padding-top:10px;border-top:1px solid #eee;
        }
        .ejwoom-modal-controls button {
            border:none;padding:8px 16px;border-radius:4px;
            cursor:pointer;color:white;font-weight:500;
        }
        .wmconfirm {background:#007bff;}
        .wmconfirm:hover {background:#0056b3;}
        .wmdelete {background:#dc3545;}
        .wmdelete:hover {background:#c82333;}
        .wmcancel {background:#6c757d;}
        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}
/* Read File Tab */
        .ejwoomreadfile {
            padding: 20px;
            background: white;
            min-height: 100vh;
        }
        .ejwoomreadarea {
            border: 3px dashed #28a745;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            font-size: 1.2em;
            color: #555;
            cursor: pointer;
            transition: all 0.3s;
        }
        .ejwoomreadarea.selected {
            border-color: #fd7e14; /* Orange */
            color: #333;
            font-weight: bold;
        }
        .ejwoomreadreset {
            margin: -10px 0 15px 0;
            color: #007bff;
            cursor: pointer;
            text-decoration: underline;
            font-size: 14px;
        }
        .ejwoomreadoptions {
            display: none; /* Hidden by default */
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            gap: 10px;
            flex-wrap: wrap;
        }
        .ejwoomreadoptions a {
            padding: 10px 16px;
            background: #007bff; /* Blue */
            color: white;
            text-decoration: none;
            border-radius: 6px;
            display: inline-block;
            cursor: pointer;
        }
        .ejwoomreadoptions a:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview &amp; Download</span>
    <span class="ejwoomreadfiletab">Read File</span>
</div>

<div class="ejwoomedit active">
    <div class="ejwoomcontrol">
        <span class="ejwoomundo">Undo</span>
        <span class="ejwoomredo">Redo</span>
    </div>
    <input type="text" class="ejwoomtitle" placeholder="Enter title...">
    <div class="ejwoombuttons">
        <span>p</span>
        <span>b</span><span>u</span><span>s</span><span>i</span>
        <span>ul</span><span>ol</span><span>li</span>
        <span>h1</span><span>h2</span><span>h3</span><span>h4</span><span>h5</span><span>h6</span>
        <span class="ejwoomaddbutton">+</span>
    </div>
    <textarea class="ejwoomcontent" placeholder="Enter HTML content..."></textarea>
    <div id="attachments"></div>
    <button class="ejwoomattach" title="Attach file">+</button>
</div>

<div class="ejwoomresult"></div>

<input type="file" id="attachfile" style="display:none;">
<input type="file" id="readhtmlfile" style="display:none;" accept=".html,.htm">

<script>
document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
    const resultTab = document.querySelector('.ejwoomresulttab');
    const readfileTab = document.querySelector('.ejwoomreadfiletab'); // New
    const editDiv = document.querySelector('.ejwoomedit');
    const resultDiv = document.querySelector('.ejwoomresult');
    // .ejwoomreadfile is created dynamically

    const titleInput = document.querySelector('.ejwoomtitle');
    const textarea = document.querySelector('.ejwoomcontent');
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const readHtmlInput = document.getElementById('readhtmlfile'); // New
    const attachmentsContainer = document.getElementById('attachments');
    const buttonsContainer = document.querySelector('.ejwoombuttons');
    const addButton = document.querySelector('.ejwoomaddbutton');

    const attachedFiles = new Map();
    const imageExts = new Set(['png','jpg','jpeg','gif','svg','webp','bmp','ico']);
    
    let loadedHtmlContent = null; // New: To store read file content

    // === Helpers ===
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    function formatBytes(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }

    // New: Convert Data URL back to File object
    function dataURLtoFile(dataurl, filename) {
        try {
            let arr = dataurl.split(','),
                mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), 
                n = bstr.length, 
                u8arr = new Uint8Array(n);
            while(n--){
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, {type:mime});
        } catch (e) {
            console.error("Error converting data URL to file:", e);
            return null;
        }
    }

    // === Attachment System ===
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const ext = getExt(file.name);
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        typeSpan.textContent = (imageExts.has(ext) ? 'img' : 'file') + '/' + ext;

        const nameSpan = document.createElement('span');
        nameSpan.className = 'wmattachname';
        nameSpan.textContent = file.name;
        nameSpan.title = file.name;

        const sizeSpan = document.createElement('span');
        sizeSpan.className = 'wmattachsize';
        sizeSpan.textContent = formatBytes(file.size);

        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
            e.stopPropagation();

            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');

            row.remove();
            attachedFiles.delete(file.name);
        };
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };

        row.onclick = (e) => {
            if (e.target === deleteSpan) return;
            insertRef();
        };

        row.append(typeSpan, nameSpan, sizeSpan, deleteSpan);
        return row;
    }

    attachBtn.onclick = () => attachInput.click();

    attachInput.onchange = e => {
        for (let file of e.target.files) {
            if (attachedFiles.has(file.name)) {
                alert(`File "${file.name}" is already attached.`);
                continue;
            }
    
            // Add to UI
            attachmentsContainer.appendChild(createAttachmentRow(file));
            attachedFiles.set(file.name, file);
    
            // Auto-insert placeholder into textarea at cursor position
            const ext = getExt(file.name);
            const isImage = imageExts.has(ext);
            const placeholder = `[${isImage ? 'img' : 'file'}:${file.name}]`;
    
            const start = textarea.selectionStart;
            textarea.value = textarea.value.slice(0, start) + placeholder + textarea.value.slice(start);
            textarea.focus();
            textarea.setSelectionRange(start + placeholder.length, start + placeholder.length);
        }
        attachInput.value = '';
    };

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }

    // Default buttons
    document.querySelectorAll('.ejwoombuttons span:not(.ejwoomaddbutton):not(.ejwoomcustombutton)').forEach(span => {
        span.onclick = () => wrapSelection(`<${span.textContent}>`, `</${span.textContent}>`);
    });

    // New: Refactored function to apply click logic to custom buttons
    function applyCustomButtonLogic(targetSpan) {
        targetSpan.onclick = () => {
            const beforeRaw = targetSpan.dataset.before ? atob(targetSpan.dataset.before) : '';
            const afterRaw = targetSpan.dataset.after ? atob(targetSpan.dataset.after) : '';

            if (beforeRaw === '' && afterRaw === '') {
                // If both before and after are empty, use <name>selected text</name>
                const name = targetSpan.textContent.trim();
                wrapSelection(`<${name}>`, `</${name}>`);
            } else {
                // Otherwise use custom before/after (even if one is empty)
                wrapSelection(beforeRaw, afterRaw);
            }
        };
    }

    // === Custom Button Editor (Fixed!) ===
    addButton.onclick = function () {
        const modal = document.createElement('div');
        modal.className = 'ejwoomwindow active';
        modal.innerHTML = `
            <div class="ejwoomwindow-content">
                <div class="ejwoom-btn-selector">
                    <span class="ejwoomselbutton selected" data-value="new">new</span>
                </div>
                <div class="ejwoom-input-row"><label>Name</label><input type="text" placeholder="e.g. blockquote"></div>
                <div class="ejwoom-input-row"><label>Before HTML</label><input type="text" placeholder="&lt;div class=&quot;tip&quot;&gt;"></div>
                <div class="ejwoom-input-row"><label>After HTML</label><input type="text" placeholder="&lt;/div&gt;"></div>
                <div class="ejwoom-modal-controls">
                    <button class="wmconfirm">Confirm</button>
                    <button class="wmdelete" disabled>Delete</button>
                    <button class="wmcancel">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(modal);

        const selector = modal.querySelector('.ejwoom-btn-selector');
        const nameInp = modal.querySelectorAll('input')[0];
        const beforeInp = modal.querySelectorAll('input')[1];
        const afterInp = modal.querySelectorAll('input')[2];
        const confirmBtn = modal.querySelector('.wmconfirm');
        const deleteBtn = modal.querySelector('.wmdelete');
        const cancelBtn = modal.querySelector('.wmcancel');

        // Load existing custom buttons into selector
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => {
            const sel = document.createElement('span');
            sel.className = 'ejwoomselbutton';
            sel.textContent = btn.textContent;
            sel.dataset.value = btn.textContent;
            sel._target = btn;
            selector.appendChild(sel);
        });

        function updateForm() {
            const selected = selector.querySelector('.selected');
            if (!selected || selected.dataset.value === 'new') {
                nameInp.value = beforeInp.value = afterInp.value = '';
                deleteBtn.disabled = true;
            } else {
                const target = selected._target;
                nameInp.value = target.textContent;
                beforeInp.value = target.dataset.before ? atob(target.dataset.before) : '';
                afterInp.value = target.dataset.after ? atob(target.dataset.after) : '';
                deleteBtn.disabled = false;
            }
        }

        selector.onclick = e => {
            const btn = e.target.closest('.ejwoomselbutton');
            if (!btn) return;
            selector.querySelectorAll('.ejwoomselbutton').forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            updateForm();
        };

        deleteBtn.onclick = () => {
            const selected = selector.querySelector('.selected');
            if (selected && selected.dataset.value !== 'new') {
                selected._target.remove();
                selected.remove();
                selector.querySelector('[data-value="new"]').classList.add('selected');
                updateForm();
            }
        };

        confirmBtn.onclick = () => {
            const name = nameInp.value.trim();
            if (!name) return alert('Button name is required');

            const selected = selector.querySelector('.selected');
            let targetSpan;

            if (selected.dataset.value === 'new') {
                targetSpan = document.createElement('span');
                targetSpan.className = 'ejwoomcustombutton';
                buttonsContainer.insertBefore(targetSpan, addButton);

                const newSel = document.createElement('span');
                newSel.className = 'ejwoomselbutton';
                newSel.textContent = name;
                newSel.dataset.value = name;
                newSel._target = targetSpan;
                selector.appendChild(newSel);
            } else {
                targetSpan = selected._target;
            }

            targetSpan.textContent = name;
            if (beforeInp.value) targetSpan.dataset.before = btoa(beforeInp.value);
            else delete targetSpan.dataset.before;
            if (afterInp.value) targetSpan.dataset.after = btoa(afterInp.value);
            else delete targetSpan.dataset.after;

            // Apply click behavior
            applyCustomButtonLogic(targetSpan);
            modal.remove();
        };

        cancelBtn.onclick = () => modal.remove();
        updateForm();
    };

    // === Preview & Download (Fixed filename + [img:] [file:]) ===
    async function buildResult() {
        // Requirement 2: Collect all custom buttons to inject as <meta> in the final HTML
        const customButtons = Array.from(buttonsContainer.querySelectorAll('.ejwoomcustombutton'));
        let metaTagHTML = '';
        if (customButtons.length > 0) {
            const allButtonsHTML = customButtons.map(btn => btn.outerHTML.trim()).join('\n');
            try {
                const buttonsBase64 = btoa(allButtonsHTML);
                metaTagHTML = `<meta name="ejwoombuttons" content="${buttonsBase64}">`;
            } catch (e) {
                console.error("Error encoding buttons:", e);
            }
        }

        resultDiv.innerHTML = `
            <div class="ejwoomdownload">
                <a id="downloadlink" download="">Download HTML</a>
                <span class="ejwoomdownloadsize"></span>
            </div>
            <div class="ejwoompreview"></div>
        `;


        const downloadLink = resultDiv.querySelector('#downloadlink');
        const sizeSpan = resultDiv.querySelector('.ejwoomdownloadsize');
        const previewDiv = resultDiv.querySelector('.ejwoompreview');

        let title = (titleInput.value || 'Untitled').trim();
        let processedContent = textarea.value.trim();
        const replacements = new Map();

        for (const [name, file] of attachedFiles) {
            const base64 = await fileToBase64(file);
            const ext = getExt(name);
            const isImage = imageExts.has(ext);

            let html = '';
            if (isImage) {
                let dims = '';
                if (ext === 'svg') {
                    try {
                        const svg = atob(base64.split(',')[1]);
                        const w = svg.match(/width=["'](\d+)["']/i);
                        const h = svg.match(/height=["'](\d+)["']/i);
                        if (w) dims += ` width="${w[1]}"`;
                        if (h) dims += ` height="${h[1]}"`;
                    } catch (e) {}
                }
                // Get current index in attachedFiles Map (which preserves insertion order)
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<img src="${base64}" alt="${name}" attachsort="${index}" style="max-width:100%;height:auto;"${dims}>`;
            } else {
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<a href="${base64}" download="${name}" attachsort="${index}">Download ${name}</a>`;
            }

            replacements.set(`[img:${name}]`, html);
            replacements.set(`[file:${name}]`, html);
        }

        for (const [key, val] of replacements) {
            processedContent = processedContent.replace(
                new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), val
            );
        }

        previewDiv.innerHTML = processedContent;

        const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
</head>
<body>
${processedContent}
</body>
</html>`;

        const blob = new Blob([fullHTML], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = title + '.html';
        downloadLink.textContent = 'Download ' + title + '.html';
        sizeSpan.textContent = formatBytes(blob.size);
    }
    
    // === NEW: Read File Functions ===
    
    // 6.6: Parse HTML, extract attachments, and replace with placeholders
    function rebuildAttachmentsFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        
        // Clear current attachments
        attachmentsContainer.innerHTML = '';
        attachedFiles.clear();
        
        const attachmentData = []; // To store file + sort order
    
        tempDiv.querySelectorAll('img[src^="data:"], a[href^="data:"][download]').forEach(el => {
            const dataUrl = el.tagName === 'IMG' ? el.getAttribute('src') : el.getAttribute('href');
            const filename = el.tagName === 'IMG' ? el.getAttribute('alt') : el.getAttribute('download');
            const sortOrder = el.hasAttribute('attachsort') ? parseInt(el.getAttribute('attachsort')) : -1;
            
            if (!filename) {
                console.warn('Found data URL element without alt/download name. Skipping.', el);
                return;
            }
            
            if (attachedFiles.has(filename)) {
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
                return;
            }
            
            const file = dataURLtoFile(dataUrl, filename);
            if (file) {
                attachedFiles.set(file.name, file);
                attachmentData.push({ file, sortOrder });
                
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
            }
        });
        
        // Sort by attachsort (preserving original order if missing)
        attachmentData.sort((a, b) => {
            if (a.sortOrder === -1) return 1;
            if (b.sortOrder === -1) return -1;
            return a.sortOrder - b.sortOrder;
        });
        
        // Append rows in correct order
        attachmentData.forEach(item => {
            attachmentsContainer.appendChild(createAttachmentRow(item.file));
        });
        
        return tempDiv.innerHTML;
    }

    // 6.5: Load buttons from a parsed HTML document
    function parseAndLoadButtons(htmlDoc) {
        // Clear existing custom buttons
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => btn.remove());
        
        // Requirement 2: Read from meta tag
        const meta = htmlDoc.head.querySelector('meta[name="ejwoombuttons"]');
        if (meta && meta.content) {
            try {
                const buttonsHTML = atob(meta.content);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = buttonsHTML;
                
                Array.from(tempDiv.children).forEach(node => {
                    if (node.tagName === 'SPAN' && node.classList.contains('ejwoomcustombutton')) {
                        // node is newly created by innerHTML, just append it
                        buttonsContainer.insertBefore(node, addButton);
                        applyCustomButtonLogic(node); // Re-apply click logic
                    }
                });
            } catch (e) {
                console.error("Error parsing custom buttons from meta tag:", e);
            }
        }
    }
    
    // 6.4: Get body HTML
    function parseAndLoadHTML(htmlDoc) {
        const body = htmlDoc.body;
        if (!body) return '';
        
        // Template removal is no longer needed as it's in the head.
        
        // Also try to find title
        const titleTag = htmlDoc.head.querySelector('title');
        if (titleTag && titleTag.textContent) {
            titleInput.value = titleTag.textContent;
        }

        return body.innerHTML.trim(); // Use innerHTML to skip the <body> tag itself
    }
    
    // Switch to Edit Tab helper
    function switchToEditTab() {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        editTab.classList.add('active');
        editDiv.classList.add('active');
        resultDiv.classList.remove('active');
        resultDiv.innerHTML = '';
        
        const readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
    }
    
    // === Tab Switching ===
    resultTab.onclick = () => {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        resultTab.classList.add('active');
        editDiv.classList.remove('active');
        resultDiv.classList.add('active');
        
        const readDiv = document.querySelector('.ejwoomreadfile'); // New
        if (readDiv) readDiv.remove(); // New
        
        buildResult();
    };

    editTab.onclick = () => {
        switchToEditTab(); // Use helper
    };
    
    // New: Read File Tab click
    readfileTab.onclick = () => {
        // Set active tab
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        readfileTab.classList.add('active');
        
        // Hide other main divs
        editDiv.classList.remove('active');
        resultDiv.classList.remove('active');
        
        // Clean up old read div if it exists
        let readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
        
        // Create new read file UI
        readDiv = document.createElement('div');
        readDiv.className = 'ejwoomreadfile';
        readDiv.innerHTML = `
            <div class="ejwoomreadarea">Open the HTML file that you saved before.</div>
            <div class="ejwoomreadoptions">
                <a class="ejwoomreadall">Read All</a>
                <a class="ejwoomreadhtml">Read HTML Only</a>
                <a class="ejwoomreadbutton">Read Buttons Only</a>
            </div>
        `;
        document.body.appendChild(readDiv);
        
        const readArea = readDiv.querySelector('.ejwoomreadarea');
        const readOptions = readDiv.querySelector('.ejwoomreadoptions');
        
        // Reset function
        function resetReadFileUI() {
            readArea.innerHTML = 'Open the HTML file that you saved before.';
            readArea.classList.remove('selected');
            readOptions.style.display = 'none';
            const resetBtn = readDiv.querySelector('.ejwoomreadreset');
            if (resetBtn) resetBtn.remove();
            readHtmlInput.value = ''; // Clear file input
            loadedHtmlContent = null;
        }
        
        // Read area click
        readArea.onclick = () => readHtmlInput.click();
        
        // Read file input change
        readHtmlInput.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (ev) => {
                loadedHtmlContent = ev.target.result;
                
                // Update UI
                readArea.textContent = file.name;
                readArea.classList.add('selected');
                readOptions.style.display = 'flex';
                
                // Add reset button if it doesn't exist
                if (!readDiv.querySelector('.ejwoomreadreset')) {
                    const resetBtn = document.createElement('div');
                    resetBtn.className = 'ejwoomreadreset';
                    resetBtn.textContent = 'Reset file selection.';
                    resetBtn.onclick = resetReadFileUI;
                    readDiv.insertBefore(resetBtn, readArea);
                }
            };
            reader.onerror = () => {
                alert('Error reading file.');
                resetReadFileUI();
            };
            reader.readAsText(file);
        };
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            
            switchToEditTab();
        };
    };
});
</script>
</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

現在還差button-new的直插功能
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

修改程式:

一,更改ejwoomcontrol內的元素的css,使ejwoomrecord和ejwoombuttons同處一行(垂直居中對齊),ejwoomrecord按原始寬度佔據左邊,右邊剩餘位置留給ejwoombuttons。

二,增加undo和redo的點擊功能。

當ejwoomcontent發生變化時,js將record它的值,之後undo將可用。

ejwoomcontent發生變化有這幾種情況:ejwoomcontent.oninput,ejwoombuttons點擊插入文本,ejwoomattached點擊插入文本。

當readfile點擊時,如果只更新button,則不影響record,但是如果更新all或html,則record重置。

當undo或redo可用時為綠色,不可用時為灰色(即當前的顏色)。

只需要給出所修改的地方。所有代碼全部使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

代码: 全选

 Gemini
Conversation with Gemini

修改程式:


一,更改ejwoomcontrol內的元素的css,使ejwoomrecord和ejwoombuttons同處一行(垂直居中對齊),ejwoomrecord按原始寬度佔據左邊,右邊剩餘位置留給ejwoombuttons。


二,增加undo和redo的點擊功能。


當ejwoomcontent發生變化時,js將record它的值,之後undo將可用。


ejwoomcontent發生變化有這幾種情況:ejwoomcontent.oninput,ejwoombuttons點擊插入文本,ejwoomattached點擊插入文本。


當readfile點擊時,如果只更新button,則不影響record,但是如果更新all或html,則record重置。


當undo或redo可用時為綠色,不可用時為灰色(即當前的顏色)。


只需要給出所修改的地方。所有代碼全部使用英文。

Here are the modifications to the CSS and JavaScript based on your request.
🎨 CSS Modifications

I have added styles for .ejwoomcontrol to enable flex layout, modified .ejwoomrecord and .ejwoombuttons for correct alignment and growth, and added enabled / default (disabled) states for the undo/redo buttons.
CSS

        .ejwoomrecord {
            /* margin-bottom:15px; */ /* Removed */
            display:flex;gap:10px;flex-wrap:wrap;align-items:center;
            flex-shrink: 0; /* Prevent shrinking */
        }
        .ejwoomrecord span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;font-size:14px;
            color: #888; /* Default disabled text color */
            cursor: not-allowed;
            user-select: none;
        }
        /* No hover effect when disabled */
        .ejwoomrecord span:hover {background:#eee;} 
        
        /* New enabled state */
        .ejwoomrecord span.enabled {
            background: #28a745; /* Green */
            color: white;
            cursor: pointer;
        }
        .ejwoomrecord span.enabled:hover {
            background: #218838; /* Darker green on hover */
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;
            /* margin-bottom:15px; */ /* Removed */
            flex-grow: 1; /* Take remaining space */
        }
        
        .ejwoomcontent {

(...later in the CSS...)
CSS

        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}

/* New style for the control container */
        .ejwoomcontrol {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-bottom: 15px;
        }
/* Read File Tab */
        .ejwoomreadfile {

⚙️ JavaScript Modifications

I have added new variables and functions for the undo/redo history and integrated them into the existing functions that modify the textarea content.
JavaScript

document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
// ... (rest of const declarations) ...
    const addButton = document.querySelector('.ejwoomaddbutton');
    const undoBtn = document.querySelector('.ejwoomundo'); // New
    const redoBtn = document.querySelector('.ejwoomredo'); // New

    const attachedFiles = new Map();
// ... (rest of const declarations) ...
    
    let loadedHtmlContent = null; // New: To store read file content

    // === New: Undo/Redo System ===
    let history = [];
    let redoStack = [];
    let currentText = textarea.value;
    const MAX_HISTORY = 50;
    let isUndoingOrRedoing = false; // Flag to prevent recursion

    function updateUndoRedoButtons() {
        if (history.length > 0) {
            undoBtn.classList.add('enabled');
        } else {
            undoBtn.classList.remove('enabled');
        }
        
        if (redoStack.length > 0) {
            redoBtn.classList.add('enabled');
        } else {
            redoBtn.classList.remove('enabled');
        }
    }

    function recordHistory(newValue) {
        if (isUndoingOrRedoing) return; // Don't record undo/redo actions
        if (newValue === currentText) return; // No change

        history.push(currentText);
        redoStack = []; // Clear redo stack on new action
        currentText = newValue;

        if (history.length > MAX_HISTORY) {
            history.shift(); // Remove oldest entry
        }
        updateUndoRedoButtons();
    }

    function doUndo() {
        if (history.length === 0) return;
        isUndoingOrRedoing = true;
        
        const previousText = history.pop();
        redoStack.push(currentText);
        currentText = previousText;
        textarea.value = currentText;
        
        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function doRedo() {
        if (redoStack.length === 0) return;
        isUndoingOrRedoing = true;

        const nextText = redoStack.pop();
        history.push(currentText);
        currentText = nextText;
        textarea.value = currentText;

        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function resetHistory() {
        history = [];
        redoStack = [];
        currentText = textarea.value; // Reset baseline to current
        updateUndoRedoButtons();
    }

    // === Helpers ===
// ... (rest of helper functions) ...

    // === Attachment System ===
// ... (inside createAttachmentRow) ...
        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
// ...
            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');
            recordHistory(textarea.value); // New

            row.remove();
            attachedFiles.delete(file.name);
// ...
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            recordHistory(textarea.value); // New
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };
// ... (rest of createAttachmentRow) ...

    attachBtn.onclick = () => attachInput.click();

    attachInput.onchange = e => {
// ... (inside loop) ...
    
            // Auto-insert placeholder into textarea at cursor position
            const ext = getExt(file.name);
// ...
            const placeholder = `[${isImage ? 'img' : 'file'}:${file.name}]`;
    
            const start = textarea.selectionStart;
            textarea.value = textarea.value.slice(0, start) + placeholder + textarea.value.slice(start);
            recordHistory(textarea.value); // New
            textarea.focus();
            textarea.setSelectionRange(start + placeholder.length, start + placeholder.length);
        }
// ...

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        
        recordHistory(textarea.value); // New
        
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }
// ... (rest of script, inside readfileTab.onclick) ...
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory(); // New
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory(); // New
            
            switchToEditTab();
        };
// ... (rest of script) ...
    };

    // === New: Init Undo/Redo and Textarea Input ===
    updateUndoRedoButtons();
    undoBtn.onclick = doUndo;
    redoBtn.onclick = doRedo;
    textarea.oninput = () => {
        recordHistory(textarea.value);
    };
});
</script>
</body>
</html>


Gemini can make mistakes, so double-check it
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

grok4.1

代码: 全选

Here are **only the modified/added parts** you need to apply to the original code (all in English):

```css
/* 1. CSS modifications – replace the original .ejwoomcontrol block */
.ejwoomcontrol {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 15px;
    flex-wrap: wrap;
    gap: 10px;
}

.ejwoomrecord {
    display: flex;
    gap: 10px;
    flex-shrink: 0;           /* keep original width on the left */
}

.ejwoombuttons {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    flex-grow: 1;             /* take all remaining space on the right */
    justify-content: flex-end; /* optional: align buttons to the right */
}

/* Undo/Redo button color when available */
.ejwoomundo.available,
.ejwoomredo.available {
    background: #28a745 !important;
    color: white !important;
}
```

```javascript
// 2. Add these variables near the top (after other const declarations)
let history = [];
let historyIndex = -1;
const MAX_HISTORY = 100;

// Function to save current state
function saveHistory() {
    const current = textarea.value;
    if (historyIndex >= 0 && current === history[historyIndex]) return;

    // Remove future states if we are in the middle of history
    history = history.slice(0, historyIndex + 1);
    
    history.push(current);
    if (history.length > MAX_HISTORY) history.shift();
    
    historyIndex = history.length - 1;
    updateUndoRedoButtons();
}

// Update Undo/Redo button appearance
function updateUndoRedoButtons() {
    const undoBtn = document.querySelector('.ejwoomundo');
    const redoBtn = document.querySelector('.ejwoomredo');
    
    undoBtn.classList.toggle('available', historyIndex > 0);
    redoBtn.classList.toggle('available', historyIndex < history.length - 1);
}

// Undo function
function performUndo() {
    if (historyIndex <= 0) return;
    historyIndex--;
    textarea.value = history[historyIndex];
    updateUndoRedoButtons();
}

// Redo function
function performRedo() {
    if (historyIndex >= history.length - 1) return;
    historyIndex++;
    textarea.value = history[historyIndex];
    updateUndoRedoButtons();
}

// 3. Add click handlers for Undo/Redo (inside DOMContentLoaded)
document.querySelector('.ejwoomundo').onclick = performUndo;
document.querySelector('.ejwoomredo').onclick = performRedo;

// 4. Track changes – add these event listeners

// When typing manually
textarea.oninput = () => {
    saveHistory();
};

// Save history after any button insertion (including custom buttons)
function wrapSelection(before, after = '') {
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const selected = textarea.value.substring(start, end);
    textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
    textarea.focus();
    textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    
    saveHistory();           // save after insertion
}

// Also save when clicking attached file to insert reference
// (modify the insertRef function inside createAttachmentRow)
const insertRef = () => {
    const start = textarea.selectionStart;
    const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
    textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
    textarea.focus();
    textarea.setSelectionRange(start + ref.length, start + ref.length);
    
    saveHistory();           // save after insertion
};

// 5. Reset history when loading full HTML or HTML-only from "Read File"
readDiv.querySelector('.ejwoomreadall').onclick = () => {
    if (!loadedHtmlContent) return alert('No file loaded.');
    const parser = new DOMParser();
    const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
    
    parseAndLoadButtons(doc);
    let bodyHtml = parseAndLoadHTML(doc);
    textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
    
    // Reset history
    history = [textarea.value];
    historyIndex = 0;
    updateUndoRedoButtons();
    
    switchToEditTab();
};

readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
    if (!loadedHtmlContent) return alert('No file loaded.');
    const parser = new DOMParser();
    const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
    
    let bodyHtml = parseAndLoadHTML(doc);
    textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
    
    // Reset history
    history = [textarea.value];
    historyIndex = 0;
    updateUndoRedoButtons();
    
    switchToEditTab();
};

// 6. Initialize history with empty state
textarea.value = '';          // ensure it's empty at start
saveHistory();                // saves the initial empty state
```

**Summary of changes:**
- Layout fixed with flexbox (left: undo/redo, right: buttons)
- Full Undo/Redo system with history tracking
- History saved on typing, button clicks, and attachment insertion
- Buttons turn green when available
- History correctly reset when loading full/HTML from file

Just replace/add the above code blocks in the original file. No other changes needed.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

當前代碼:

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejwoom HTML Editor</title>
    <style>
        body {font-family:system-ui,sans-serif;margin:0;padding:0;background:#f5f5f5;}
        .ejwoomtab {display:flex;background:#333;color:white;}
        .ejwoomtab span {padding:12px 20px;cursor:pointer;}
        .ejwoomtab span.active {background:#007bff;}
        
        .ejwoomedit, .ejwoomresult {
            padding:20px;background:white;min-height:100vh;display:none;
        }
        .ejwoomedit.active, .ejwoomresult.active {display:block;}
        
        .ejwoomtitle {
            width:100%;padding:10px;font-size:18px;border:1px solid #ccc;
            border-radius:4px;box-sizing:border-box;margin-bottom:15px;
        }
        .ejwoomrecord {
            display:flex;gap:10px;flex-wrap:wrap;align-items:center;
            flex-shrink: 0; /* Prevent shrinking */
        }
        .ejwoomrecord span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;font-size:14px;
            color: #888; /* Default disabled text color */
            cursor: not-allowed;
            user-select: none;
        }
        /* No hover effect when disabled */
        .ejwoomrecord span:hover {background:#eee;} 
        
        /* New enabled state */
        .ejwoomrecord span.enabled {
            background: #28a745; /* Green */
            color: white;
            cursor: pointer;
        }
        .ejwoomrecord span.enabled:hover {
            background: #218838; /* Darker green on hover */
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;
            flex-grow: 1; /* Take remaining space */
            user-select: none;
        }
        
        .ejwoombuttons span {
            padding:3px 7px;background:#007bff;color:white;border:none;
            border-radius:4px;cursor:pointer;font-size:14px;
        }
        .ejwoombuttons span:hover {background:#0056b3;}
        
        .ejwoomcontent {
            width:100%;height:70vh;padding:12px;font-size:16px;
            font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
            border:1px solid #ccc;border-radius:4px;box-sizing:border-box;
            resize:vertical;
        }
        
        .ejwoomattach {
            width:36px;height:36px;background:#28a745;color:white;border:none;
            border-radius:8px;font-size:20px;cursor:pointer;
            display:flex;align-items:center;justify-content:center;margin-top:10px;
        }
        
        .ejwoomattached {
            display:flex;align-items:center;gap:8px;
            background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
            padding:8px;margin:8px 0;font-size:14px;cursor:pointer;
        }
        .wmattachtype {
            background:#e3f2fd;line-height:24px;border-radius:4px;
            font-weight:bold;color:#1976d2;padding:0 7px;text-align:center;
        }
        .wmattachname {
            flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap;background:#f5f5f5;padding:0 8px;
            border-radius:4px;color:#333;margin-right:8px;
        }
        .wmattachsize {
            background:#e8f5e8;color:#2e7d32;font-weight:500;
            padding:2px 6px;border-radius:4px;margin-right:8px;font-size:12px;
        }
        .wmattachdelete {
            background:#ffebee;color:#d32f2f;width:24px;height:24px;
            border-radius:50%;line-height:22px!important;font-size:18px;
            text-align:center;cursor:pointer;
        }
        
        .ejwoomdownload {margin-bottom:20px;}
        .ejwoomdownload a {
            padding:10px 16px;background:#28a745;color:white;
            text-decoration:none;border-radius:6px;display:inline-block;
        }
        .ejwoomdownloadsize {
            margin-left:15px;color:#666;font-size:14px;
        }
        
        .ejwoompreview {
            border:1px solid #ddd;padding:20px;background:white;
            border-radius:6px;min-height:70vh;overflow:auto;
        }

        /* Custom Button Styles */
        .ejwoomaddbutton {
            padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;font-weight:bold;
        }
        .ejwoomaddbutton:hover {background:#5a6268;}
        .ejwoomcustombutton {
            padding:3px 7px;background:#17a2b8;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;
        }
        .ejwoomcustombutton:hover {background:#138496;}

        /* Modal Window */
        .ejwoomwindow {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoomwindow.active {display:flex;}
        .ejwoomwindow-content {
            background:white;padding:28px 32px;border-radius:12px;
            width:90%;max-width:540px;max-height:90vh;overflow-y:auto;
            box-shadow:0 20px 40px rgba(0,0,0,0.22);
            display:flex;flex-direction:column;gap:18px;
            font-family:system-ui,sans-serif;
        }
        .ejwoom-btn-selector {
            display:flex;flex-wrap:wrap;gap:10px;
            border-bottom:1px solid #eee;padding-bottom:15px;
        }
        .ejwoomselbutton {
            padding:6px 14px;background:#e9ecef;border-radius:6px;
            cursor:pointer;user-select:none;transition:all 0.2s;font-weight:500;
        }
        .ejwoomselbutton:hover {background:#dee2e6;}
        .ejwoomselbutton.selected {
            background:#007bff;color:white;
        }
        .ejwoomselbutton.selected:hover {background:#0056b3;}
        .ejwoom-input-row {
            display:flex;align-items:center;gap:10px;
        }
        .ejwoom-input-row label {flex-basis:100px;flex-shrink:0;}
        .ejwoom-input-row input[type="text"] {
            flex-grow:1;padding:8px;border:1px solid #ccc;border-radius:4px;
        }
        .ejwoom-modal-controls {
            display:flex;gap:10px;justify-content:flex-end;
            padding-top:10px;border-top:1px solid #eee;
        }
        .ejwoom-modal-controls button {
            border:none;padding:8px 16px;border-radius:4px;
            cursor:pointer;color:white;font-weight:500;
        }
        .wmconfirm {background:#007bff;}
        .wmconfirm:hover {background:#0056b3;}
        .wmdelete {background:#dc3545;}
        .wmdelete:hover {background:#c82333;}
        .wmcancel {background:#6c757d;}
        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}

        .ejwoomcontrol {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-bottom: 15px;
        }

/* Read File Tab */
        .ejwoomreadfile {
            padding: 20px;
            background: white;
            min-height: 100vh;
        }
        .ejwoomreadarea {
            border: 3px dashed #28a745;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            font-size: 1.2em;
            color: #555;
            cursor: pointer;
            transition: all 0.3s;
        }
        .ejwoomreadarea.selected {
            border-color: #fd7e14; /* Orange */
            color: #333;
            font-weight: bold;
        }
        .ejwoomreadreset {
            margin: -10px 0 15px 0;
            color: #007bff;
            cursor: pointer;
            text-decoration: underline;
            font-size: 14px;
        }
        .ejwoomreadoptions {
            display: none; /* Hidden by default */
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            gap: 10px;
            flex-wrap: wrap;
        }
        .ejwoomreadoptions a {
            padding: 10px 16px;
            background: #007bff; /* Blue */
            color: white;
            text-decoration: none;
            border-radius: 6px;
            display: inline-block;
            cursor: pointer;
        }
        .ejwoomreadoptions a:hover {
            background: #0056b3;
        }

        @media (max-width: 720px) {
            .ejwoomrecord {
                flex-direction: column;
                gap: 5px; /* Adjust vertical gap */
            }
        }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview &amp; Download</span>
    <span class="ejwoomreadfiletab">Read File</span>
</div>

<div class="ejwoomedit active">
    <input type="text" class="ejwoomtitle" placeholder="Enter title...">
    <div class="ejwoomcontrol">
        <div class="ejwoomrecord">
            <span class="ejwoomundo">Undo</span>
            <span class="ejwoomredo">Redo</span>
        </div>
        <div class="ejwoombuttons">
            <span>p</span>
            <span>b</span><span>u</span><span>s</span><span>i</span>
            <span>ul</span><span>ol</span><span>li</span>
            <span>h1</span><span>h2</span><span>h3</span><span>h4</span><span>h5</span><span>h6</span>
            <span class="ejwoomaddbutton">+</span>
        </div>
    </div>
    <textarea class="ejwoomcontent" placeholder="Enter HTML content..."></textarea>
    <div id="attachments"></div>
    <button class="ejwoomattach" title="Attach file">+</button>
</div>

<div class="ejwoomresult"></div>

<input type="file" id="attachfile" style="display:none;">
<input type="file" id="readhtmlfile" style="display:none;" accept=".html,.htm">

<script>
document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
    const resultTab = document.querySelector('.ejwoomresulttab');
    const readfileTab = document.querySelector('.ejwoomreadfiletab'); // New
    const editDiv = document.querySelector('.ejwoomedit');
    const resultDiv = document.querySelector('.ejwoomresult');
    // .ejwoomreadfile is created dynamically

    const titleInput = document.querySelector('.ejwoomtitle');
    const textarea = document.querySelector('.ejwoomcontent');
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const readHtmlInput = document.getElementById('readhtmlfile'); // New
    const attachmentsContainer = document.getElementById('attachments');
    const buttonsContainer = document.querySelector('.ejwoombuttons');
    const addButton = document.querySelector('.ejwoomaddbutton');
    const undoBtn = document.querySelector('.ejwoomundo');
    const redoBtn = document.querySelector('.ejwoomredo');

    const attachedFiles = new Map();
    const imageExts = new Set(['png','jpg','jpeg','gif','svg','webp','bmp','ico']);
    
    let loadedHtmlContent = null; // New: To store read file content

// === New: Undo/Redo System ===
    let history = [];
    let redoStack = [];
    let currentText = textarea.value;
    const MAX_HISTORY = 50;
    let isUndoingOrRedoing = false; // Flag to prevent recursion

    function updateUndoRedoButtons() {
        if (history.length > 0) {
            undoBtn.classList.add('enabled');
        } else {
            undoBtn.classList.remove('enabled');
        }
        
        if (redoStack.length > 0) {
            redoBtn.classList.add('enabled');
        } else {
            redoBtn.classList.remove('enabled');
        }
    }

    function recordHistory(newValue) {
        if (isUndoingOrRedoing) return; // Don't record undo/redo actions
        if (newValue === currentText) return; // No change

        history.push(currentText);
        redoStack = []; // Clear redo stack on new action
        currentText = newValue;

        if (history.length > MAX_HISTORY) {
            history.shift(); // Remove oldest entry
        }
        updateUndoRedoButtons();
    }

    function doUndo() {
        if (history.length === 0) return;
        isUndoingOrRedoing = true;
        
        const previousText = history.pop();
        redoStack.push(currentText);
        currentText = previousText;
        textarea.value = currentText;
        
        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function doRedo() {
        if (redoStack.length === 0) return;
        isUndoingOrRedoing = true;

        const nextText = redoStack.pop();
        history.push(currentText);
        currentText = nextText;
        textarea.value = currentText;

        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function resetHistory() {
        history = [];
        redoStack = [];
        currentText = textarea.value; // Reset baseline to current
        updateUndoRedoButtons();
    }

    // === Helpers ===
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    function formatBytes(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }

    // New: Convert Data URL back to File object
    function dataURLtoFile(dataurl, filename) {
        try {
            let arr = dataurl.split(','),
                mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), 
                n = bstr.length, 
                u8arr = new Uint8Array(n);
            while(n--){
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, {type:mime});
        } catch (e) {
            console.error("Error converting data URL to file:", e);
            return null;
        }
    }

    // === Attachment System ===
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const ext = getExt(file.name);
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        typeSpan.textContent = (imageExts.has(ext) ? 'img' : 'file') + '/' + ext;

        const nameSpan = document.createElement('span');
        nameSpan.className = 'wmattachname';
        nameSpan.textContent = file.name;
        nameSpan.title = file.name;

        const sizeSpan = document.createElement('span');
        sizeSpan.className = 'wmattachsize';
        sizeSpan.textContent = formatBytes(file.size);

        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
            e.stopPropagation();

            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');
            recordHistory(textarea.value);

            row.remove();
            attachedFiles.delete(file.name);
        };
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };

        row.onclick = (e) => {
            if (e.target === deleteSpan) return;
            insertRef();
        };

        row.append(typeSpan, nameSpan, sizeSpan, deleteSpan);
        return row;
    }

    attachBtn.onclick = () => attachInput.click();

    attachInput.onchange = e => {
        for (let file of e.target.files) {
            if (attachedFiles.has(file.name)) {
                alert(`File "${file.name}" is already attached.`);
                continue;
            }
    
            // Add to UI
            attachmentsContainer.appendChild(createAttachmentRow(file));
            attachedFiles.set(file.name, file);
    
            // Auto-insert placeholder into textarea at cursor position
            const ext = getExt(file.name);
            const isImage = imageExts.has(ext);
            const placeholder = `[${isImage ? 'img' : 'file'}:${file.name}]`;
    
            const start = textarea.selectionStart;
            textarea.value = textarea.value.slice(0, start) + placeholder + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + placeholder.length, start + placeholder.length);
        }
        attachInput.value = '';
    };

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        recordHistory(textarea.value);
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }

    // Default buttons
    document.querySelectorAll('.ejwoombuttons span:not(.ejwoomaddbutton):not(.ejwoomcustombutton)').forEach(span => {
        span.onclick = () => wrapSelection(`<${span.textContent}>`, `</${span.textContent}>`);
    });

    // New: Refactored function to apply click logic to custom buttons
    function applyCustomButtonLogic(targetSpan) {
        targetSpan.onclick = () => {
            const beforeRaw = targetSpan.dataset.before ? atob(targetSpan.dataset.before) : '';
            const afterRaw = targetSpan.dataset.after ? atob(targetSpan.dataset.after) : '';

            if (beforeRaw === '' && afterRaw === '') {
                // If both before and after are empty, use <name>selected text</name>
                const name = targetSpan.textContent.trim();
                wrapSelection(`<${name}>`, `</${name}>`);
            } else {
                // Otherwise use custom before/after (even if one is empty)
                wrapSelection(beforeRaw, afterRaw);
            }
        };
    }

    // === Custom Button Editor (Fixed!) ===
    addButton.onclick = function () {
        const modal = document.createElement('div');
        modal.className = 'ejwoomwindow active';
        modal.innerHTML = `
            <div class="ejwoomwindow-content">
                <div class="ejwoom-btn-selector">
                    <span class="ejwoomselbutton selected" data-value="new">new</span>
                </div>
                <div class="ejwoom-input-row"><label>Name</label><input type="text" placeholder="e.g. blockquote"></div>
                <div class="ejwoom-input-row"><label>Before HTML</label><input type="text" placeholder="&lt;div class=&quot;tip&quot;&gt;"></div>
                <div class="ejwoom-input-row"><label>After HTML</label><input type="text" placeholder="&lt;/div&gt;"></div>
                <div class="ejwoom-modal-controls">
                    <button class="wmconfirm">Confirm</button>
                    <button class="wmdelete wmopbutton">Delete</button>
                    <button class="wmcancel">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(modal);

        const selector = modal.querySelector('.ejwoom-btn-selector');
        const nameInp = modal.querySelectorAll('input')[0];
        const beforeInp = modal.querySelectorAll('input')[1];
        const afterInp = modal.querySelectorAll('input')[2];
        const confirmBtn = modal.querySelector('.wmconfirm');
        const funcBtn = modal.querySelector('.wmopbutton');
        const cancelBtn = modal.querySelector('.wmcancel');

        // Load existing custom buttons into selector
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => {
            const sel = document.createElement('span');
            sel.className = 'ejwoomselbutton';
            sel.textContent = btn.textContent;
            sel.dataset.value = btn.textContent;
            sel._target = btn;
            selector.appendChild(sel);
        });

        function updateForm() {
            const selected = selector.querySelector('.selected');
            if (!selected || selected.dataset.value === 'new') {
                nameInp.value = beforeInp.value = afterInp.value = '';
                funcBtn.textContent = 'Insert';
                funcBtn.classList.remove('wmdelete'); // Remove delete style
                funcBtn.classList.add('wmconfirm');  // Use confirm style for insert
                funcBtn.disabled = false;
            } else {
                const target = selected._target;
                nameInp.value = target.textContent;
                beforeInp.value = target.dataset.before ? atob(target.dataset.before) : '';
                afterInp.value = target.dataset.after ? atob(target.dataset.after) : '';
                funcBtn.textContent = 'Delete';
                funcBtn.classList.add('wmdelete'); // Restore delete style
                funcBtn.classList.remove('wmconfirm'); // Remove confirm style
                funcBtn.disabled = false; // Enabled for existing buttons
            }
        }

        selector.onclick = e => {
            const btn = e.target.closest('.ejwoomselbutton');
            if (!btn) return;
            selector.querySelectorAll('.ejwoomselbutton').forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            updateForm();
        };

        // New logic for funcBtn (Delete/Insert)
        funcBtn.onclick = () => {
            const selected = selector.querySelector('.selected');

            if (selected.dataset.value === 'new') {
                // 'Insert' functionality
                const name = nameInp.value.trim();
                const beforeRaw = beforeInp.value;
                const afterRaw = afterInp.value;

                if (beforeRaw || afterRaw) {
                    // Use custom before/after
                    wrapSelection(beforeRaw, afterRaw);
                } else if (name) {
                    // Use <name>selected text</name>
                    wrapSelection(`<${name}>`, `</${name}>`);
                } else {
                    return alert('Enter a Name or Before/After HTML to insert.');
                }
                modal.remove(); // Close window after insertion
            } else {
                // 'Delete' functionality for existing button
                if (confirm(`Are you sure you want to delete the button "${selected.dataset.value}"?`)) {
                    selected._target.remove();
                    selected.remove();
                    selector.querySelector('[data-value="new"]').classList.add('selected');
                    updateForm();
                }
            }
        };

        confirmBtn.onclick = () => {
            const name = nameInp.value.trim();
            if (!name) return alert('Button name is required');

            const selected = selector.querySelector('.selected');
            let targetSpan;

            if (selected.dataset.value === 'new') {
                targetSpan = document.createElement('span');
                targetSpan.className = 'ejwoomcustombutton';
                buttonsContainer.insertBefore(targetSpan, addButton);

                const newSel = document.createElement('span');
                newSel.className = 'ejwoomselbutton';
                newSel.textContent = name;
                newSel.dataset.value = name;
                newSel._target = targetSpan;
                selector.appendChild(newSel);
            } else {
                targetSpan = selected._target;
            }

            targetSpan.textContent = name;
            if (beforeInp.value) targetSpan.dataset.before = btoa(beforeInp.value);
            else delete targetSpan.dataset.before;
            if (afterInp.value) targetSpan.dataset.after = btoa(afterInp.value);
            else delete targetSpan.dataset.after;

            // Apply click behavior
            applyCustomButtonLogic(targetSpan);
            modal.remove();
        };

        cancelBtn.onclick = () => modal.remove();
        updateForm();
    };

    // === Preview & Download (Fixed filename + [img:] [file:]) ===
    async function buildResult() {
        // Requirement 2: Collect all custom buttons to inject as <meta> in the final HTML
        const customButtons = Array.from(buttonsContainer.querySelectorAll('.ejwoomcustombutton'));
        let metaTagHTML = '';
        if (customButtons.length > 0) {
            const allButtonsHTML = customButtons.map(btn => btn.outerHTML.trim()).join('\n');
            try {
                const buttonsBase64 = btoa(allButtonsHTML);
                metaTagHTML = `<meta name="ejwoombuttons" content="${buttonsBase64}">`;
            } catch (e) {
                console.error("Error encoding buttons:", e);
            }
        }

        resultDiv.innerHTML = `
            <div class="ejwoomdownload">
                <a id="downloadlink" download="">Download HTML</a>
                <span class="ejwoomdownloadsize"></span>
            </div>
            <div class="ejwoompreview"></div>
        `;


        const downloadLink = resultDiv.querySelector('#downloadlink');
        const sizeSpan = resultDiv.querySelector('.ejwoomdownloadsize');
        const previewDiv = resultDiv.querySelector('.ejwoompreview');

        let title = (titleInput.value || 'Untitled').trim();
        let processedContent = textarea.value.trim();
        const replacements = new Map();

        for (const [name, file] of attachedFiles) {
            const base64 = await fileToBase64(file);
            const ext = getExt(name);
            const isImage = imageExts.has(ext);

            let html = '';
            if (isImage) {
                let dims = '';
                if (ext === 'svg') {
                    try {
                        const svg = atob(base64.split(',')[1]);
                        const w = svg.match(/width=["'](\d+)["']/i);
                        const h = svg.match(/height=["'](\d+)["']/i);
                        if (w) dims += ` width="${w[1]}"`;
                        if (h) dims += ` height="${h[1]}"`;
                    } catch (e) {}
                }
                // Get current index in attachedFiles Map (which preserves insertion order)
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<img src="${base64}" alt="${name}" attachsort="${index}" style="max-width:100%;height:auto;"${dims}>`;
            } else {
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<a href="${base64}" download="${name}" attachsort="${index}">Download ${name}</a>`;
            }

            replacements.set(`[img:${name}]`, html);
            replacements.set(`[file:${name}]`, html);
        }

        for (const [key, val] of replacements) {
            processedContent = processedContent.replace(
                new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), val
            );
        }

        previewDiv.innerHTML = processedContent;

        const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
</head>
<body>
${processedContent}
</body>
</html>`;

        const blob = new Blob([fullHTML], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = title + '.html';
        downloadLink.textContent = 'Download ' + title + '.html';
        sizeSpan.textContent = formatBytes(blob.size);
    }
    
    // === NEW: Read File Functions ===
    
    // 6.6: Parse HTML, extract attachments, and replace with placeholders
    function rebuildAttachmentsFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        
        // Clear current attachments
        attachmentsContainer.innerHTML = '';
        attachedFiles.clear();
        
        const attachmentData = []; // To store file + sort order
    
        tempDiv.querySelectorAll('img[src^="data:"], a[href^="data:"][download]').forEach(el => {
            const dataUrl = el.tagName === 'IMG' ? el.getAttribute('src') : el.getAttribute('href');
            const filename = el.tagName === 'IMG' ? el.getAttribute('alt') : el.getAttribute('download');
            const sortOrder = el.hasAttribute('attachsort') ? parseInt(el.getAttribute('attachsort')) : -1;
            
            if (!filename) {
                console.warn('Found data URL element without alt/download name. Skipping.', el);
                return;
            }
            
            if (attachedFiles.has(filename)) {
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
                return;
            }
            
            const file = dataURLtoFile(dataUrl, filename);
            if (file) {
                attachedFiles.set(file.name, file);
                attachmentData.push({ file, sortOrder });
                
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
            }
        });
        
        // Sort by attachsort (preserving original order if missing)
        attachmentData.sort((a, b) => {
            if (a.sortOrder === -1) return 1;
            if (b.sortOrder === -1) return -1;
            return a.sortOrder - b.sortOrder;
        });
        
        // Append rows in correct order
        attachmentData.forEach(item => {
            attachmentsContainer.appendChild(createAttachmentRow(item.file));
        });
        
        return tempDiv.innerHTML;
    }

    // 6.5: Load buttons from a parsed HTML document
    function parseAndLoadButtons(htmlDoc) {
        // Clear existing custom buttons
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => btn.remove());
        
        // Requirement 2: Read from meta tag
        const meta = htmlDoc.head.querySelector('meta[name="ejwoombuttons"]');
        if (meta && meta.content) {
            try {
                const buttonsHTML = atob(meta.content);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = buttonsHTML;
                
                Array.from(tempDiv.children).forEach(node => {
                    if (node.tagName === 'SPAN' && node.classList.contains('ejwoomcustombutton')) {
                        // node is newly created by innerHTML, just append it
                        buttonsContainer.insertBefore(node, addButton);
                        applyCustomButtonLogic(node); // Re-apply click logic
                    }
                });
            } catch (e) {
                console.error("Error parsing custom buttons from meta tag:", e);
            }
        }
    }
    
    // 6.4: Get body HTML
    function parseAndLoadHTML(htmlDoc) {
        const body = htmlDoc.body;
        if (!body) return '';
        
        // Template removal is no longer needed as it's in the head.
        
        // Also try to find title
        const titleTag = htmlDoc.head.querySelector('title');
        if (titleTag && titleTag.textContent) {
            titleInput.value = titleTag.textContent;
        }

        return body.innerHTML.trim(); // Use innerHTML to skip the <body> tag itself
    }
    
    // Switch to Edit Tab helper
    function switchToEditTab() {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        editTab.classList.add('active');
        editDiv.classList.add('active');
        resultDiv.classList.remove('active');
        resultDiv.innerHTML = '';
        
        const readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
    }
    
    // === Tab Switching ===
    resultTab.onclick = () => {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        resultTab.classList.add('active');
        editDiv.classList.remove('active');
        resultDiv.classList.add('active');
        
        const readDiv = document.querySelector('.ejwoomreadfile'); // New
        if (readDiv) readDiv.remove(); // New
        
        buildResult();
    };

    editTab.onclick = () => {
        switchToEditTab(); // Use helper
    };
    
    // New: Read File Tab click
    readfileTab.onclick = () => {
        // Set active tab
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        readfileTab.classList.add('active');
        
        // Hide other main divs
        editDiv.classList.remove('active');
        resultDiv.classList.remove('active');
        
        // Clean up old read div if it exists
        let readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
        
        // Create new read file UI
        readDiv = document.createElement('div');
        readDiv.className = 'ejwoomreadfile';
        readDiv.innerHTML = `
            <div class="ejwoomreadarea">Open the HTML file that you saved before.</div>
            <div class="ejwoomreadoptions">
                <a class="ejwoomreadall">Read All</a>
                <a class="ejwoomreadhtml">Read HTML Only</a>
                <a class="ejwoomreadbutton">Read Buttons Only</a>
            </div>
        `;
        document.body.appendChild(readDiv);
        
        const readArea = readDiv.querySelector('.ejwoomreadarea');
        const readOptions = readDiv.querySelector('.ejwoomreadoptions');
        
        // Reset function
        function resetReadFileUI() {
            readArea.innerHTML = 'Open the HTML file that you saved before.';
            readArea.classList.remove('selected');
            readOptions.style.display = 'none';
            const resetBtn = readDiv.querySelector('.ejwoomreadreset');
            if (resetBtn) resetBtn.remove();
            readHtmlInput.value = ''; // Clear file input
            loadedHtmlContent = null;
        }
        
        // Read area click
        readArea.onclick = () => readHtmlInput.click();
        
        // Read file input change
        readHtmlInput.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (ev) => {
                loadedHtmlContent = ev.target.result;
                
                // Update UI
                readArea.textContent = file.name;
                readArea.classList.add('selected');
                readOptions.style.display = 'flex';
                
                // Add reset button if it doesn't exist
                if (!readDiv.querySelector('.ejwoomreadreset')) {
                    const resetBtn = document.createElement('div');
                    resetBtn.className = 'ejwoomreadreset';
                    resetBtn.textContent = 'Reset file selection.';
                    resetBtn.onclick = resetReadFileUI;
                    readDiv.insertBefore(resetBtn, readArea);
                }
            };
            reader.onerror = () => {
                alert('Error reading file.');
                resetReadFileUI();
            };
            reader.readAsText(file);
        };
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            
            switchToEditTab();
        };
    };

    // === New: Init Undo/Redo and Textarea Input ===
    updateUndoRedoButtons();
    undoBtn.onclick = doUndo;
    redoBtn.onclick = doRedo;
    textarea.oninput = () => {
        recordHistory(textarea.value);
    };

});
</script>
</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

基本製作完成
Ejwoom HTML Editor(1).png
Ejwoom HTML Editor(1).png (312.06 KiB) 查看 51 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

調整textarea以使其佔滿剩餘高度。

代码: 全选

/* 步驟 1: 讓 ejwoomedit 容器成為 Flex 容器,並確保其高度能延伸 */
.ejwoomedit {
    /* 根據您提供的程式碼片段,您已經設定了 min-height: 100vh;
       這裡需要設定 display: flex; 和 flex-direction: column; 
       來啟用垂直 Flex 佈局
    */
    display: flex;
    flex-direction: column; /* 垂直堆疊其子元素 */
    
    /* 確保高度可以填滿 (如果您希望它是填滿整個視窗) */
    /* 如果您的 Tab 容器已經設定了固定的父高度,這裡可以用 height: 100%; 
       否則,100vh 會填滿視窗,但要考慮 Tab 標籤的高度
    */
    height: calc(100vh - 44px); /* 假設 Tab 標籤的高度是 44px */
    padding: 20px;
    background: white;
    /* min-height: 100vh; - 應刪除或調整為與 height 配合 */
    display: none;
}
.ejwoomedit.active { display: flex; } /* 保持顯示,但用 flex 代替 block */


/* 步驟 2: 處理 textarea 以外的固定元素 */
.ejwoomtitle, .ejwoomrecord {
    /* 確保這些元素不會被壓縮或拉伸,保持其內容所需的高度 */
    flex-shrink: 0; 
    margin-bottom: 15px; /* 保持原有的間距 */
}


/* 步驟 3: 讓 textarea 佔滿所有剩餘空間 (這是關鍵) */
#ejwoom-textarea {
    flex-grow: 1; /* 讓它佔用所有剩餘的垂直空間 */
    width: 100%;  /* 讓它佔用所有可用的水平寬度 */
    
    /* 確保邊框和內距不會導致溢出 */
    box-sizing: border-box; 
    
    /* 避免 Flexbox 的最小高度問題 */
    min-height: 0; 
}

/* 確保其他 tab (ejwoomresult) 的 display 屬性不變 */
.ejwoomresult {
    padding:20px;background:white;min-height:100vh;display:none; /* 保持原狀 */
}
.ejwoomresult.active {display:block;} /* 保持原狀 */
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

ejwoom74.html

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejwoom HTML Editor</title>
    <style>
        body {font-family:system-ui,sans-serif;margin:0;padding:0;background:#f5f5f5;}
        .ejwoomtab {display:flex;background:#333;color:white;height:44px;}
        .ejwoomtab span {padding:0 20px;cursor:pointer;line-height: 44px;}
        .ejwoomtab span.active {background:#007bff;}
        
        .ejwoomedit, .ejwoomresult {
            padding:20px;background:white;display:none;
        }
        .ejwoomeditinner {
            display: flex;
            flex-direction: column;
            height: calc(100vh - 44px - 46px - 20px);
            box-sizing:border-box;
        }
        .ejwoomedit.active,.ejwoomresult.active {display:block;}
        
        .ejwoomtitle {
            width:100%;padding:10px;font-size:18px;border:1px solid #ccc;
            border-radius:4px;box-sizing:border-box;margin-bottom:15px;
        }
        .ejwoomtitle, .ejwoomcontrol {
            flex-shrink: 0; 
            margin-bottom: 15px;
        }
        .ejwoomrecord {
            display:flex;gap:10px;flex-wrap:wrap;align-items:center;
            flex-shrink: 0; /* Prevent shrinking */
        }
        .ejwoomrecord span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;font-size:14px;
            color: #888; /* Default disabled text color */
            cursor: not-allowed;
            user-select: none;
        }
        /* No hover effect when disabled */
        .ejwoomrecord span:hover {background:#eee;} 
        
        /* New enabled state */
        .ejwoomrecord span.enabled {
            background: #28a745; /* Green */
            color: white;
            cursor: pointer;
        }
        .ejwoomrecord span.enabled:hover {
            background: #218838; /* Darker green on hover */
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;
            flex-grow: 1; /* Take remaining space */
            user-select: none;
        }
        
        .ejwoombuttons span {
            padding:3px 7px;background:#007bff;color:white;border:none;
            border-radius:4px;cursor:pointer;font-size:14px;
        }
        .ejwoombuttons span:hover {background:#0056b3;}
        .ejwoomcontent {
            flex-grow: 1;
            width: 100%;
            padding:12px;font-size:16px;
            font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
            border:1px solid #ccc;border-radius:4px;
            box-sizing:border-box;
            resize:vertical;
        }
        
        .ejwoomattach {
            width:36px;height:36px;background:#28a745;color:white;border:none;
            border-radius:8px;font-size:20px;cursor:pointer;
            display:flex;align-items:center;justify-content:center;margin-top:10px;
        }
        
        .ejwoomattached {
            display:flex;align-items:center;gap:8px;
            background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
            padding:8px;margin:8px 0;font-size:14px;cursor:pointer;
        }
        .wmattachtype {
            background:#e3f2fd;line-height:24px;border-radius:4px;
            font-weight:bold;color:#1976d2;padding:0 7px;text-align:center;
        }
        .wmattachname {
            flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap;background:#f5f5f5;padding:0 8px;
            border-radius:4px;color:#333;margin-right:8px;
        }
        .wmattachsize {
            background:#e8f5e8;color:#2e7d32;font-weight:500;
            padding:2px 6px;border-radius:4px;margin-right:8px;font-size:12px;
        }
        .wmattachdelete {
            background:#ffebee;color:#d32f2f;width:24px;height:24px;
            border-radius:50%;line-height:22px!important;font-size:18px;
            text-align:center;cursor:pointer;
        }
        
        .ejwoomdownload {margin-bottom:20px;}
        .ejwoomdownload a {
            padding:10px 16px;background:#28a745;color:white;
            text-decoration:none;border-radius:6px;display:inline-block;
        }
        .ejwoomdownloadsize {
            margin-left:15px;color:#666;font-size:14px;
        }
        
        .ejwoompreview {
            border:1px solid #ddd;padding:20px;background:white;
            border-radius:6px;min-height:70vh;overflow:auto;
        }

        /* Custom Button Styles */
        .ejwoomaddbutton {
            padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;font-weight:bold;
        }
        .ejwoomaddbutton:hover {background:#5a6268;}
        .ejwoomcustombutton {
            padding:3px 7px;background:#17a2b8;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;
        }
        .ejwoomcustombutton:hover {background:#138496;}

        /* Modal Window */
        .ejwoomwindow {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoomwindow.active {display:flex;}
        .ejwoomwindow-content {
            background:white;padding:28px 32px;border-radius:12px;
            width:90%;max-width:540px;max-height:90vh;overflow-y:auto;
            box-shadow:0 20px 40px rgba(0,0,0,0.22);
            display:flex;flex-direction:column;gap:18px;
            font-family:system-ui,sans-serif;
        }
        .ejwoom-btn-selector {
            display:flex;flex-wrap:wrap;gap:10px;
            border-bottom:1px solid #eee;padding-bottom:15px;
        }
        .ejwoomselbutton {
            padding:6px 14px;background:#e9ecef;border-radius:6px;
            cursor:pointer;user-select:none;transition:all 0.2s;font-weight:500;
        }
        .ejwoomselbutton:hover {background:#dee2e6;}
        .ejwoomselbutton.selected {
            background:#007bff;color:white;
        }
        .ejwoomselbutton.selected:hover {background:#0056b3;}
        .ejwoom-input-row {
            display:flex;align-items:center;gap:10px;
        }
        .ejwoom-input-row label {flex-basis:100px;flex-shrink:0;}
        .ejwoom-input-row input[type="text"] {
            flex-grow:1;padding:8px;border:1px solid #ccc;border-radius:4px;
        }
        .ejwoom-modal-controls {
            display:flex;gap:10px;justify-content:flex-end;
            padding-top:10px;border-top:1px solid #eee;
        }
        .ejwoom-modal-controls button {
            border:none;padding:8px 16px;border-radius:4px;
            cursor:pointer;color:white;font-weight:500;
        }
        .wmconfirm {background:#007bff;}
        .wmconfirm:hover {background:#0056b3;}
        .wmdelete {background:#dc3545;}
        .wmdelete:hover {background:#c82333;}
        .wmcancel {background:#6c757d;}
        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}

        .ejwoomcontrol {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-bottom: 15px;
        }

/* Read File Tab */
        .ejwoomreadfile {
            padding: 20px;
            background: white;
            min-height: 100vh;
        }
        .ejwoomreadarea {
            border: 3px dashed #28a745;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            font-size: 1.2em;
            color: #555;
            cursor: pointer;
            transition: all 0.3s;
        }
        .ejwoomreadarea.selected {
            border-color: #fd7e14; /* Orange */
            color: #333;
            font-weight: bold;
        }
        .ejwoomreadreset {
            margin: -10px 0 15px 0;
            color: #007bff;
            cursor: pointer;
            text-decoration: underline;
            font-size: 14px;
        }
        .ejwoomreadoptions {
            display: none; /* Hidden by default */
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            gap: 10px;
            flex-wrap: wrap;
        }
        .ejwoomreadoptions a {
            padding: 10px 16px;
            background: #007bff; /* Blue */
            color: white;
            text-decoration: none;
            border-radius: 6px;
            display: inline-block;
            cursor: pointer;
        }
        .ejwoomreadoptions a:hover {
            background: #0056b3;
        }

        @media (max-width: 600px) {
            .ejwoomrecord {
                flex-direction: column;
                gap: 5px; /* Adjust vertical gap */
            }
        }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview &amp; Download</span>
    <span class="ejwoomreadfiletab">Read File</span>
</div>

<div class="ejwoomedit active">
    <div class="ejwoomeditinner">
        <input type="text" class="ejwoomtitle" placeholder="Enter title...">
        <div class="ejwoomcontrol">
            <div class="ejwoomrecord">
                <span class="ejwoomundo">Undo</span>
                <span class="ejwoomredo">Redo</span>
            </div>
            <div class="ejwoombuttons">
                <span>p</span>
                <span>b</span><span>u</span><span>s</span><span>i</span>
                <span>ul</span><span>ol</span><span>li</span>
                <span>h1</span><span>h2</span><span>h3</span><span>h4</span><span>h5</span><span>h6</span>
                <span class="ejwoomaddbutton">+</span>
            </div>
        </div>
        <textarea class="ejwoomcontent" placeholder="Enter HTML content..."></textarea>
    </div>
    <button class="ejwoomattach" title="Attach file">+</button>
    <div id="attachments"></div>
</div>

<div class="ejwoomresult"></div>

<input type="file" id="attachfile" style="display:none;">
<input type="file" id="readhtmlfile" style="display:none;" accept=".html,.htm">

<script>
document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
    const resultTab = document.querySelector('.ejwoomresulttab');
    const readfileTab = document.querySelector('.ejwoomreadfiletab'); // New
    const editDiv = document.querySelector('.ejwoomedit');
    const resultDiv = document.querySelector('.ejwoomresult');
    // .ejwoomreadfile is created dynamically

    const titleInput = document.querySelector('.ejwoomtitle');
    const textarea = document.querySelector('.ejwoomcontent');
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const readHtmlInput = document.getElementById('readhtmlfile'); // New
    const attachmentsContainer = document.getElementById('attachments');
    const buttonsContainer = document.querySelector('.ejwoombuttons');
    const addButton = document.querySelector('.ejwoomaddbutton');
    const undoBtn = document.querySelector('.ejwoomundo');
    const redoBtn = document.querySelector('.ejwoomredo');

    const attachedFiles = new Map();
    const imageExts = new Set(['png','jpg','jpeg','gif','svg','webp','bmp','ico']);
    
    let loadedHtmlContent = null; // New: To store read file content

// === New: Undo/Redo System ===
    let history = [];
    let redoStack = [];
    let currentText = textarea.value;
    const MAX_HISTORY = 50;
    let isUndoingOrRedoing = false; // Flag to prevent recursion

    function updateUndoRedoButtons() {
        if (history.length > 0) {
            undoBtn.classList.add('enabled');
        } else {
            undoBtn.classList.remove('enabled');
        }
        
        if (redoStack.length > 0) {
            redoBtn.classList.add('enabled');
        } else {
            redoBtn.classList.remove('enabled');
        }
    }

    function recordHistory(newValue) {
        if (isUndoingOrRedoing) return; // Don't record undo/redo actions
        if (newValue === currentText) return; // No change

        history.push(currentText);
        redoStack = []; // Clear redo stack on new action
        currentText = newValue;

        if (history.length > MAX_HISTORY) {
            history.shift(); // Remove oldest entry
        }
        updateUndoRedoButtons();
    }

    function doUndo() {
        if (history.length === 0) return;
        isUndoingOrRedoing = true;
        
        const previousText = history.pop();
        redoStack.push(currentText);
        currentText = previousText;
        textarea.value = currentText;
        
        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function doRedo() {
        if (redoStack.length === 0) return;
        isUndoingOrRedoing = true;

        const nextText = redoStack.pop();
        history.push(currentText);
        currentText = nextText;
        textarea.value = currentText;

        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function resetHistory() {
        history = [];
        redoStack = [];
        currentText = textarea.value; // Reset baseline to current
        updateUndoRedoButtons();
    }

    // === Helpers ===
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    function formatBytes(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }

    // New: Convert Data URL back to File object
    function dataURLtoFile(dataurl, filename) {
        try {
            let arr = dataurl.split(','),
                mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), 
                n = bstr.length, 
                u8arr = new Uint8Array(n);
            while(n--){
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, {type:mime});
        } catch (e) {
            console.error("Error converting data URL to file:", e);
            return null;
        }
    }

    // === Attachment System ===
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const ext = getExt(file.name);
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        typeSpan.textContent = (imageExts.has(ext) ? 'img' : 'file') + '/' + ext;

        const nameSpan = document.createElement('span');
        nameSpan.className = 'wmattachname';
        nameSpan.textContent = file.name;
        nameSpan.title = file.name;

        const sizeSpan = document.createElement('span');
        sizeSpan.className = 'wmattachsize';
        sizeSpan.textContent = formatBytes(file.size);

        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
            e.stopPropagation();

            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');
            recordHistory(textarea.value);

            row.remove();
            attachedFiles.delete(file.name);
        };
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };

        row.onclick = (e) => {
            if (e.target === deleteSpan) return;
            insertRef();
        };

        row.append(typeSpan, nameSpan, sizeSpan, deleteSpan);
        return row;
    }

    attachBtn.onclick = () => attachInput.click();

    attachInput.onchange = e => {
        for (let file of e.target.files) {
            if (attachedFiles.has(file.name)) {
                alert(`File "${file.name}" is already attached.`);
                continue;
            }
    
            // Add to UI
            attachmentsContainer.appendChild(createAttachmentRow(file));
            attachedFiles.set(file.name, file);
    
            // Auto-insert placeholder into textarea at cursor position
            const ext = getExt(file.name);
            const isImage = imageExts.has(ext);
            const placeholder = `[${isImage ? 'img' : 'file'}:${file.name}]`;
    
            const start = textarea.selectionStart;
            textarea.value = textarea.value.slice(0, start) + placeholder + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + placeholder.length, start + placeholder.length);
        }
        attachInput.value = '';
    };

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        recordHistory(textarea.value);
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }

    // Default buttons
    document.querySelectorAll('.ejwoombuttons span:not(.ejwoomaddbutton):not(.ejwoomcustombutton)').forEach(span => {
        span.onclick = () => wrapSelection(`<${span.textContent}>`, `</${span.textContent}>`);
    });

    // New: Refactored function to apply click logic to custom buttons
    function applyCustomButtonLogic(targetSpan) {
        targetSpan.onclick = () => {
            const beforeRaw = targetSpan.dataset.before ? atob(targetSpan.dataset.before) : '';
            const afterRaw = targetSpan.dataset.after ? atob(targetSpan.dataset.after) : '';

            if (beforeRaw === '' && afterRaw === '') {
                // If both before and after are empty, use <name>selected text</name>
                const name = targetSpan.textContent.trim();
                wrapSelection(`<${name}>`, `</${name}>`);
            } else {
                // Otherwise use custom before/after (even if one is empty)
                wrapSelection(beforeRaw, afterRaw);
            }
        };
    }

    // === Custom Button Editor (Fixed!) ===
    addButton.onclick = function () {
        const modal = document.createElement('div');
        modal.className = 'ejwoomwindow active';
        modal.innerHTML = `
            <div class="ejwoomwindow-content">
                <div class="ejwoom-btn-selector">
                    <span class="ejwoomselbutton selected" data-value="new">new</span>
                </div>
                <div class="ejwoom-input-row"><label>Name</label><input type="text" placeholder="e.g. blockquote"></div>
                <div class="ejwoom-input-row"><label>Before HTML</label><input type="text" placeholder="&lt;div class=&quot;tip&quot;&gt;"></div>
                <div class="ejwoom-input-row"><label>After HTML</label><input type="text" placeholder="&lt;/div&gt;"></div>
                <div class="ejwoom-modal-controls">
                    <button class="wmconfirm">Confirm</button>
                    <button class="wmdelete wmopbutton">Delete</button>
                    <button class="wmcancel">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(modal);

        const selector = modal.querySelector('.ejwoom-btn-selector');
        const nameInp = modal.querySelectorAll('input')[0];
        const beforeInp = modal.querySelectorAll('input')[1];
        const afterInp = modal.querySelectorAll('input')[2];
        const confirmBtn = modal.querySelector('.wmconfirm');
        const funcBtn = modal.querySelector('.wmopbutton');
        const cancelBtn = modal.querySelector('.wmcancel');

        // Load existing custom buttons into selector
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => {
            const sel = document.createElement('span');
            sel.className = 'ejwoomselbutton';
            sel.textContent = btn.textContent;
            sel.dataset.value = btn.textContent;
            sel._target = btn;
            selector.appendChild(sel);
        });

        function updateForm() {
            const selected = selector.querySelector('.selected');
            if (!selected || selected.dataset.value === 'new') {
                nameInp.value = beforeInp.value = afterInp.value = '';
                funcBtn.textContent = 'Insert';
                funcBtn.classList.remove('wmdelete'); // Remove delete style
                funcBtn.classList.add('wmconfirm');  // Use confirm style for insert
                funcBtn.disabled = false;
            } else {
                const target = selected._target;
                nameInp.value = target.textContent;
                beforeInp.value = target.dataset.before ? atob(target.dataset.before) : '';
                afterInp.value = target.dataset.after ? atob(target.dataset.after) : '';
                funcBtn.textContent = 'Delete';
                funcBtn.classList.add('wmdelete'); // Restore delete style
                funcBtn.classList.remove('wmconfirm'); // Remove confirm style
                funcBtn.disabled = false; // Enabled for existing buttons
            }
        }

        selector.onclick = e => {
            const btn = e.target.closest('.ejwoomselbutton');
            if (!btn) return;
            selector.querySelectorAll('.ejwoomselbutton').forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            updateForm();
        };

        // New logic for funcBtn (Delete/Insert)
        funcBtn.onclick = () => {
            const selected = selector.querySelector('.selected');

            if (selected.dataset.value === 'new') {
                // 'Insert' functionality
                const name = nameInp.value.trim();
                const beforeRaw = beforeInp.value;
                const afterRaw = afterInp.value;

                if (beforeRaw || afterRaw) {
                    // Use custom before/after
                    wrapSelection(beforeRaw, afterRaw);
                } else if (name) {
                    // Use <name>selected text</name>
                    wrapSelection(`<${name}>`, `</${name}>`);
                } else {
                    return alert('Enter a Name or Before/After HTML to insert.');
                }
                modal.remove(); // Close window after insertion
            } else {
                // 'Delete' functionality for existing button
                if (confirm(`Are you sure you want to delete the button "${selected.dataset.value}"?`)) {
                    selected._target.remove();
                    selected.remove();
                    selector.querySelector('[data-value="new"]').classList.add('selected');
                    updateForm();
                }
            }
        };

        confirmBtn.onclick = () => {
            const name = nameInp.value.trim();
            if (!name) return alert('Button name is required');

            const selected = selector.querySelector('.selected');
            let targetSpan;

            if (selected.dataset.value === 'new') {
                targetSpan = document.createElement('span');
                targetSpan.className = 'ejwoomcustombutton';
                buttonsContainer.insertBefore(targetSpan, addButton);

                const newSel = document.createElement('span');
                newSel.className = 'ejwoomselbutton';
                newSel.textContent = name;
                newSel.dataset.value = name;
                newSel._target = targetSpan;
                selector.appendChild(newSel);
            } else {
                targetSpan = selected._target;
            }

            targetSpan.textContent = name;
            if (beforeInp.value) targetSpan.dataset.before = btoa(beforeInp.value);
            else delete targetSpan.dataset.before;
            if (afterInp.value) targetSpan.dataset.after = btoa(afterInp.value);
            else delete targetSpan.dataset.after;

            // Apply click behavior
            applyCustomButtonLogic(targetSpan);
            modal.remove();
        };

        cancelBtn.onclick = () => modal.remove();
        updateForm();
    };

    // === Preview & Download (Fixed filename + [img:] [file:]) ===
    async function buildResult() {
        // Requirement 2: Collect all custom buttons to inject as <meta> in the final HTML
        const customButtons = Array.from(buttonsContainer.querySelectorAll('.ejwoomcustombutton'));
        let metaTagHTML = '';
        if (customButtons.length > 0) {
            const allButtonsHTML = customButtons.map(btn => btn.outerHTML.trim()).join('\n');
            try {
                const buttonsBase64 = btoa(allButtonsHTML);
                metaTagHTML = `<meta name="ejwoombuttons" content="${buttonsBase64}">`;
            } catch (e) {
                console.error("Error encoding buttons:", e);
            }
        }

        resultDiv.innerHTML = `
            <div class="ejwoomdownload">
                <a id="downloadlink" download="">Download HTML</a>
                <span class="ejwoomdownloadsize"></span>
            </div>
            <div class="ejwoompreview"></div>
        `;


        const downloadLink = resultDiv.querySelector('#downloadlink');
        const sizeSpan = resultDiv.querySelector('.ejwoomdownloadsize');
        const previewDiv = resultDiv.querySelector('.ejwoompreview');

        let title = (titleInput.value || 'Untitled').trim();
        let processedContent = textarea.value.trim();
        const replacements = new Map();

        for (const [name, file] of attachedFiles) {
            const base64 = await fileToBase64(file);
            const ext = getExt(name);
            const isImage = imageExts.has(ext);

            let html = '';
            if (isImage) {
                let dims = '';
                if (ext === 'svg') {
                    try {
                        const svg = atob(base64.split(',')[1]);
                        const w = svg.match(/width=["'](\d+)["']/i);
                        const h = svg.match(/height=["'](\d+)["']/i);
                        if (w) dims += ` width="${w[1]}"`;
                        if (h) dims += ` height="${h[1]}"`;
                    } catch (e) {}
                }
                // Get current index in attachedFiles Map (which preserves insertion order)
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<img src="${base64}" alt="${name}" attachsort="${index}" style="max-width:100%;height:auto;"${dims}>`;
            } else {
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<a href="${base64}" download="${name}" attachsort="${index}">Download ${name}</a>`;
            }

            replacements.set(`[img:${name}]`, html);
            replacements.set(`[file:${name}]`, html);
        }

        for (const [key, val] of replacements) {
            processedContent = processedContent.replace(
                new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), val
            );
        }

        previewDiv.innerHTML = processedContent;

        const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
</head>
<body>
${processedContent}
</body>
</html>`;

        const blob = new Blob([fullHTML], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = title + '.html';
        downloadLink.textContent = 'Download ' + title + '.html';
        sizeSpan.textContent = formatBytes(blob.size);
    }
    
    // === NEW: Read File Functions ===
    
    // 6.6: Parse HTML, extract attachments, and replace with placeholders
    function rebuildAttachmentsFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        
        // Clear current attachments
        attachmentsContainer.innerHTML = '';
        attachedFiles.clear();
        
        const attachmentData = []; // To store file + sort order
    
        tempDiv.querySelectorAll('img[src^="data:"], a[href^="data:"][download]').forEach(el => {
            const dataUrl = el.tagName === 'IMG' ? el.getAttribute('src') : el.getAttribute('href');
            const filename = el.tagName === 'IMG' ? el.getAttribute('alt') : el.getAttribute('download');
            const sortOrder = el.hasAttribute('attachsort') ? parseInt(el.getAttribute('attachsort')) : -1;
            
            if (!filename) {
                console.warn('Found data URL element without alt/download name. Skipping.', el);
                return;
            }
            
            if (attachedFiles.has(filename)) {
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
                return;
            }
            
            const file = dataURLtoFile(dataUrl, filename);
            if (file) {
                attachedFiles.set(file.name, file);
                attachmentData.push({ file, sortOrder });
                
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
            }
        });
        
        // Sort by attachsort (preserving original order if missing)
        attachmentData.sort((a, b) => {
            if (a.sortOrder === -1) return 1;
            if (b.sortOrder === -1) return -1;
            return a.sortOrder - b.sortOrder;
        });
        
        // Append rows in correct order
        attachmentData.forEach(item => {
            attachmentsContainer.appendChild(createAttachmentRow(item.file));
        });
        
        return tempDiv.innerHTML;
    }

    // 6.5: Load buttons from a parsed HTML document
    function parseAndLoadButtons(htmlDoc) {
        // Clear existing custom buttons
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => btn.remove());
        
        // Requirement 2: Read from meta tag
        const meta = htmlDoc.head.querySelector('meta[name="ejwoombuttons"]');
        if (meta && meta.content) {
            try {
                const buttonsHTML = atob(meta.content);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = buttonsHTML;
                
                Array.from(tempDiv.children).forEach(node => {
                    if (node.tagName === 'SPAN' && node.classList.contains('ejwoomcustombutton')) {
                        // node is newly created by innerHTML, just append it
                        buttonsContainer.insertBefore(node, addButton);
                        applyCustomButtonLogic(node); // Re-apply click logic
                    }
                });
            } catch (e) {
                console.error("Error parsing custom buttons from meta tag:", e);
            }
        }
    }
    
    // 6.4: Get body HTML
    function parseAndLoadHTML(htmlDoc) {
        const body = htmlDoc.body;
        if (!body) return '';
        
        // Template removal is no longer needed as it's in the head.
        
        // Also try to find title
        const titleTag = htmlDoc.head.querySelector('title');
        if (titleTag && titleTag.textContent) {
            titleInput.value = titleTag.textContent;
        }

        return body.innerHTML.trim(); // Use innerHTML to skip the <body> tag itself
    }
    
    // Switch to Edit Tab helper
    function switchToEditTab() {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        editTab.classList.add('active');
        editDiv.classList.add('active');
        resultDiv.classList.remove('active');
        resultDiv.innerHTML = '';
        
        const readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
    }
    
    // === Tab Switching ===
    resultTab.onclick = () => {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        resultTab.classList.add('active');
        editDiv.classList.remove('active');
        resultDiv.classList.add('active');
        
        const readDiv = document.querySelector('.ejwoomreadfile'); // New
        if (readDiv) readDiv.remove(); // New
        
        buildResult();
    };

    editTab.onclick = () => {
        switchToEditTab(); // Use helper
    };
    
    // New: Read File Tab click
    readfileTab.onclick = () => {
        // Set active tab
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        readfileTab.classList.add('active');
        
        // Hide other main divs
        editDiv.classList.remove('active');
        resultDiv.classList.remove('active');
        
        // Clean up old read div if it exists
        let readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
        
        // Create new read file UI
        readDiv = document.createElement('div');
        readDiv.className = 'ejwoomreadfile';
        readDiv.innerHTML = `
            <div class="ejwoomreadarea">Open the HTML file that you saved before.</div>
            <div class="ejwoomreadoptions">
                <a class="ejwoomreadall">Read All</a>
                <a class="ejwoomreadhtml">Read HTML Only</a>
                <a class="ejwoomreadbutton">Read Buttons Only</a>
            </div>
        `;
        document.body.appendChild(readDiv);
        
        const readArea = readDiv.querySelector('.ejwoomreadarea');
        const readOptions = readDiv.querySelector('.ejwoomreadoptions');
        
        // Reset function
        function resetReadFileUI() {
            readArea.innerHTML = 'Open the HTML file that you saved before.';
            readArea.classList.remove('selected');
            readOptions.style.display = 'none';
            const resetBtn = readDiv.querySelector('.ejwoomreadreset');
            if (resetBtn) resetBtn.remove();
            readHtmlInput.value = ''; // Clear file input
            loadedHtmlContent = null;
        }
        
        // Read area click
        readArea.onclick = () => readHtmlInput.click();
        
        // Read file input change
        readHtmlInput.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (ev) => {
                loadedHtmlContent = ev.target.result;
                
                // Update UI
                readArea.textContent = file.name;
                readArea.classList.add('selected');
                readOptions.style.display = 'flex';
                
                // Add reset button if it doesn't exist
                if (!readDiv.querySelector('.ejwoomreadreset')) {
                    const resetBtn = document.createElement('div');
                    resetBtn.className = 'ejwoomreadreset';
                    resetBtn.textContent = 'Reset file selection.';
                    resetBtn.onclick = resetReadFileUI;
                    readDiv.insertBefore(resetBtn, readArea);
                }
            };
            reader.onerror = () => {
                alert('Error reading file.');
                resetReadFileUI();
            };
            reader.readAsText(file);
        };
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            
            switchToEditTab();
        };
    };

    // === New: Init Undo/Redo and Textarea Input ===
    updateUndoRedoButtons();
    undoBtn.onclick = doUndo;
    redoBtn.onclick = doRedo;
    textarea.oninput = () => {
        recordHistory(textarea.value);
    };

});
</script>
</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

修改程式:

當生成result下載鏈接時,增加meta[name=ejwoom],在readfile時,如果不存在meta[name=ejwoom],則不讀取並彈出消息。

只需給出修改的地方。所有代碼都要使用英文。


chatgpt

代码: 全选

以下只給 **「需要修改/新增的地方」**,全部用英文,並保持你的原程式其它部分不變。

---

# ✅ **1. 在產生下載結果時加入 `<meta name="ejwoom">`**

找到 **buildResult()** 裡組合 fullHTML 的地方,
在 `<head>` 內加入一行:

```js
${metaTagHTML}
<meta name="ejwoom" content="yes">
```

修改後如下(只顯示需改動處):

```js
const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
    <meta name="ejwoom" content="yes">   <!-- NEW -->
</head>
<body>
${processedContent}
</body>
</html>`;
```

---

# ✅ **2. 在 Read File 時檢查 meta[name=ejwoom]**

在 three read options:

* `.ejwoomreadall`
* `.ejwoomreadhtml`
* `.ejwoomreadbutton`

每個讀取前 **插入檢查 meta 的程式**。

在三個 handler 的最前面加入以下程式碼:

```js
const metaCheck = doc.querySelector('meta[name="ejwoom"]');
if (!metaCheck) {
    alert("This file is not created by Ejwoom. Cannot read.");
    return;
}
```

---

### **(1) read all**

```js
readDiv.querySelector('.ejwoomreadall').onclick = () => {
    if (!loadedHtmlContent) return alert('No file loaded.');
    const parser = new DOMParser();
    const doc = parser.parseFromString(loadedHtmlContent, 'text/html');

    const metaCheck = doc.querySelector('meta[name="ejwoom"]');  // NEW
    if (!metaCheck) {
        alert("This file is not created by Ejwoom. Cannot read.");
        return;
    }

    parseAndLoadButtons(doc);
    let bodyHtml = parseAndLoadHTML(doc);
    textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
    resetHistory();
    switchToEditTab();
};
```

---

### **(2) read html only**

```js
readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
    if (!loadedHtmlContent) return alert('No file loaded.');
    const parser = new DOMParser();
    const doc = parser.parseFromString(loadedHtmlContent, 'text/html');

    const metaCheck = doc.querySelector('meta[name="ejwoom"]');  // NEW
    if (!metaCheck) {
        alert("This file is not created by Ejwoom. Cannot read.");
        return;
    }

    let bodyHtml = parseAndLoadHTML(doc);
    textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
    resetHistory();
    switchToEditTab();
};
```

---

### **(3) read buttons only**

```js
readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
    if (!loadedHtmlContent) return alert('No file loaded.');
    const parser = new DOMParser();
    const doc = parser.parseFromString(loadedHtmlContent, 'text/html');

    const metaCheck = doc.querySelector('meta[name="ejwoom"]');  // NEW
    if (!metaCheck) {
        alert("This file is not created by Ejwoom. Cannot read.");
        return;
    }

    parseAndLoadButtons(doc);
    switchToEditTab();
};
```

---

# 完成!

如需我幫你把完整 HTML 直接整合後再次輸出,也可以告訴我。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4188
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 165 次
被圈友点赞: 195 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ejwoom HTML Editor</title>
    <style>
        body {font-family:system-ui,sans-serif;margin:0;padding:0;background:#f5f5f5;}
        .ejwoomtab {display:flex;background:#333;color:white;height:44px;}
        .ejwoomtab span {padding:0 20px;cursor:pointer;line-height: 44px;}
        .ejwoomtab span.active {background:#007bff;}
        
        .ejwoomedit, .ejwoomresult {
            padding:20px;background:white;display:none;
        }
        .ejwoomeditinner {
            display: flex;
            flex-direction: column;
            height: calc(100vh - 44px - 46px - 20px);
            box-sizing:border-box;
        }
        .ejwoomedit.active,.ejwoomresult.active {display:block;}
        
        .ejwoomtitle {
            width:100%;padding:10px;font-size:18px;border:1px solid #ccc;
            border-radius:4px;box-sizing:border-box;margin-bottom:15px;
        }
        .ejwoomtitle, .ejwoomcontrol {
            flex-shrink: 0; 
            margin-bottom: 15px;
        }
        .ejwoomrecord {
            display:flex;gap:10px;flex-wrap:wrap;align-items:center;
            flex-shrink: 0; /* Prevent shrinking */
        }
        .ejwoomrecord span {
            padding:6px 12px;background:#eee;border-radius:4px;cursor:pointer;font-size:14px;
            color: #888; /* Default disabled text color */
            cursor: not-allowed;
            user-select: none;
        }
        /* No hover effect when disabled */
        .ejwoomrecord span:hover {background:#eee;} 
        
        /* New enabled state */
        .ejwoomrecord span.enabled {
            background: #28a745; /* Green */
            color: white;
            cursor: pointer;
        }
        .ejwoomrecord span.enabled:hover {
            background: #218838; /* Darker green on hover */
        }
        
        .ejwoombuttons {
            display:flex;gap:8px;flex-wrap:wrap;
            flex-grow: 1; /* Take remaining space */
            user-select: none;
        }
        
        .ejwoombuttons span {
            padding:3px 7px;background:#007bff;color:white;border:none;
            border-radius:4px;cursor:pointer;font-size:14px;
        }
        .ejwoombuttons span:hover {background:#0056b3;}
        .ejwoomcontent {
            flex-grow: 1;
            width: 100%;
            padding:12px;font-size:16px;
            font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
            border:1px solid #ccc;border-radius:4px;
            box-sizing:border-box;
            resize:vertical;
        }
        
        .ejwoomattach {
            width:36px;height:36px;background:#28a745;color:white;border:none;
            border-radius:8px;font-size:20px;cursor:pointer;
            display:flex;align-items:center;justify-content:center;margin-top:10px;
        }
        
        .ejwoomattached {
            display:flex;align-items:center;gap:8px;
            background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
            padding:8px;margin:8px 0;font-size:14px;cursor:pointer;
        }
        .wmattachtype {
            background:#e3f2fd;line-height:24px;border-radius:4px;
            font-weight:bold;color:#1976d2;padding:0 7px;text-align:center;
        }
        .wmattachname {
            flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
            white-space:nowrap;background:#f5f5f5;padding:0 8px;
            border-radius:4px;color:#333;margin-right:8px;
        }
        .wmattachsize {
            background:#e8f5e8;color:#2e7d32;font-weight:500;
            padding:2px 6px;border-radius:4px;margin-right:8px;font-size:12px;
        }
        .wmattachdelete {
            background:#ffebee;color:#d32f2f;width:24px;height:24px;
            border-radius:50%;line-height:22px!important;font-size:18px;
            text-align:center;cursor:pointer;
        }
        
        .ejwoomdownload {margin-bottom:20px;}
        .ejwoomdownload a {
            padding:10px 16px;background:#28a745;color:white;
            text-decoration:none;border-radius:6px;display:inline-block;
        }
        .ejwoomdownloadsize {
            margin-left:15px;color:#666;font-size:14px;
        }
        
        .ejwoompreview {
            border:1px solid #ddd;padding:20px;background:white;
            border-radius:6px;min-height:70vh;overflow:auto;
        }

        /* Custom Button Styles */
        .ejwoomaddbutton {
            padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;font-weight:bold;
        }
        .ejwoomaddbutton:hover {background:#5a6268;}
        .ejwoomcustombutton {
            padding:3px 7px;background:#17a2b8;color:white;border-radius:4px;
            cursor:pointer;font-size:14px;
        }
        .ejwoomcustombutton:hover {background:#138496;}

        /* Modal Window */
        .ejwoomwindow {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoomwindow.active {display:flex;}
        .ejwoomwindow-content {
            background:white;padding:28px 32px;border-radius:12px;
            width:90%;max-width:540px;max-height:90vh;overflow-y:auto;
            box-shadow:0 20px 40px rgba(0,0,0,0.22);
            display:flex;flex-direction:column;gap:18px;
            font-family:system-ui,sans-serif;
        }
        .ejwoom-btn-selector {
            display:flex;flex-wrap:wrap;gap:10px;
            border-bottom:1px solid #eee;padding-bottom:15px;
        }
        .ejwoomselbutton {
            padding:6px 14px;background:#e9ecef;border-radius:6px;
            cursor:pointer;user-select:none;transition:all 0.2s;font-weight:500;
        }
        .ejwoomselbutton:hover {background:#dee2e6;}
        .ejwoomselbutton.selected {
            background:#007bff;color:white;
        }
        .ejwoomselbutton.selected:hover {background:#0056b3;}
        .ejwoom-input-row {
            display:flex;align-items:center;gap:10px;
        }
        .ejwoom-input-row label {flex-basis:100px;flex-shrink:0;}
        .ejwoom-input-row input[type="text"] {
            flex-grow:1;padding:8px;border:1px solid #ccc;border-radius:4px;
        }
        .ejwoom-modal-controls {
            display:flex;gap:10px;justify-content:flex-end;
            padding-top:10px;border-top:1px solid #eee;
        }
        .ejwoom-modal-controls button {
            border:none;padding:8px 16px;border-radius:4px;
            cursor:pointer;color:white;font-weight:500;
        }
        .wmconfirm {background:#007bff;}
        .wmconfirm:hover {background:#0056b3;}
        .wmdelete {background:#dc3545;}
        .wmdelete:hover {background:#c82333;}
        .wmcancel {background:#6c757d;}
        .wmcancel:hover {background:#5a6268;}
        .wmdelete:disabled {opacity:0.5;cursor:not-allowed;background:#ccc;}

        .ejwoomcontrol {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-bottom: 15px;
        }

/* Read File Tab */
        .ejwoomreadfile {
            padding: 20px;
            background: white;
            min-height: 100vh;
        }
        .ejwoomreadarea {
            border: 3px dashed #28a745;
            border-radius: 8px;
            padding: 20px;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            font-size: 1.2em;
            color: #555;
            cursor: pointer;
            transition: all 0.3s;
        }
        .ejwoomreadarea.selected {
            border-color: #fd7e14; /* Orange */
            color: #333;
            font-weight: bold;
        }
        .ejwoomreadreset {
            margin: -10px 0 15px 0;
            color: #007bff;
            cursor: pointer;
            text-decoration: underline;
            font-size: 14px;
        }
        .ejwoomreadoptions {
            display: none; /* Hidden by default */
            margin-top: 20px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            gap: 10px;
            flex-wrap: wrap;
        }
        .ejwoomreadoptions a {
            padding: 10px 16px;
            background: #007bff; /* Blue */
            color: white;
            text-decoration: none;
            border-radius: 6px;
            display: inline-block;
            cursor: pointer;
        }
        .ejwoomreadoptions a:hover {
            background: #0056b3;
        }

        @media (max-width: 600px) {
            .ejwoomrecord {
                flex-direction: column;
                gap: 5px; /* Adjust vertical gap */
            }
        }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview &amp; Download</span>
    <span class="ejwoomreadfiletab">Read File</span>
</div>

<div class="ejwoomedit active">
    <div class="ejwoomeditinner">
        <input type="text" class="ejwoomtitle" placeholder="Enter title...">
        <div class="ejwoomcontrol">
            <div class="ejwoomrecord">
                <span class="ejwoomundo">Undo</span>
                <span class="ejwoomredo">Redo</span>
            </div>
            <div class="ejwoombuttons">
                <span>p</span>
                <span>b</span><span>u</span><span>s</span><span>i</span>
                <span>ul</span><span>ol</span><span>li</span>
                <span>h1</span><span>h2</span><span>h3</span><span>h4</span><span>h5</span><span>h6</span>
                <span class="ejwoomaddbutton">+</span>
            </div>
        </div>
        <textarea class="ejwoomcontent" placeholder="Enter HTML content..."></textarea>
    </div>
    <button class="ejwoomattach" title="Attach file">+</button>
    <div id="attachments"></div>
</div>

<div class="ejwoomresult"></div>

<input type="file" id="attachfile" style="display:none;">
<input type="file" id="readhtmlfile" style="display:none;" accept=".html,.htm">

<script>
document.addEventListener('DOMContentLoaded', function () {
    const editTab = document.querySelector('.ejwoomedittab');
    const resultTab = document.querySelector('.ejwoomresulttab');
    const readfileTab = document.querySelector('.ejwoomreadfiletab'); // New
    const editDiv = document.querySelector('.ejwoomedit');
    const resultDiv = document.querySelector('.ejwoomresult');
    // .ejwoomreadfile is created dynamically

    const titleInput = document.querySelector('.ejwoomtitle');
    const textarea = document.querySelector('.ejwoomcontent');
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const readHtmlInput = document.getElementById('readhtmlfile'); // New
    const attachmentsContainer = document.getElementById('attachments');
    const buttonsContainer = document.querySelector('.ejwoombuttons');
    const addButton = document.querySelector('.ejwoomaddbutton');
    const undoBtn = document.querySelector('.ejwoomundo');
    const redoBtn = document.querySelector('.ejwoomredo');

    const attachedFiles = new Map();
    const imageExts = new Set(['png','jpg','jpeg','gif','svg','webp','bmp','ico']);
    
    let loadedHtmlContent = null; // New: To store read file content

// === New: Undo/Redo System ===
    let history = [];
    let redoStack = [];
    let currentText = textarea.value;
    const MAX_HISTORY = 50;
    let isUndoingOrRedoing = false; // Flag to prevent recursion

    function updateUndoRedoButtons() {
        if (history.length > 0) {
            undoBtn.classList.add('enabled');
        } else {
            undoBtn.classList.remove('enabled');
        }
        
        if (redoStack.length > 0) {
            redoBtn.classList.add('enabled');
        } else {
            redoBtn.classList.remove('enabled');
        }
    }

    function recordHistory(newValue) {
        if (isUndoingOrRedoing) return; // Don't record undo/redo actions
        if (newValue === currentText) return; // No change

        history.push(currentText);
        redoStack = []; // Clear redo stack on new action
        currentText = newValue;

        if (history.length > MAX_HISTORY) {
            history.shift(); // Remove oldest entry
        }
        updateUndoRedoButtons();
    }

    function doUndo() {
        if (history.length === 0) return;
        isUndoingOrRedoing = true;
        
        const previousText = history.pop();
        redoStack.push(currentText);
        currentText = previousText;
        textarea.value = currentText;
        
        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function doRedo() {
        if (redoStack.length === 0) return;
        isUndoingOrRedoing = true;

        const nextText = redoStack.pop();
        history.push(currentText);
        currentText = nextText;
        textarea.value = currentText;

        updateUndoRedoButtons();
        isUndoingOrRedoing = false;
    }

    function resetHistory() {
        history = [];
        redoStack = [];
        currentText = textarea.value; // Reset baseline to current
        updateUndoRedoButtons();
    }

    // === Helpers ===
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    function formatBytes(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    function fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }

    // New: Convert Data URL back to File object
    function dataURLtoFile(dataurl, filename) {
        try {
            let arr = dataurl.split(','),
                mime = arr[0].match(/:(.*?);/)[1],
                bstr = atob(arr[1]), 
                n = bstr.length, 
                u8arr = new Uint8Array(n);
            while(n--){
                u8arr[n] = bstr.charCodeAt(n);
            }
            return new File([u8arr], filename, {type:mime});
        } catch (e) {
            console.error("Error converting data URL to file:", e);
            return null;
        }
    }

    // === Attachment System ===
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const ext = getExt(file.name);
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        typeSpan.textContent = (imageExts.has(ext) ? 'img' : 'file') + '/' + ext;

        const nameSpan = document.createElement('span');
        nameSpan.className = 'wmattachname';
        nameSpan.textContent = file.name;
        nameSpan.title = file.name;

        const sizeSpan = document.createElement('span');
        sizeSpan.className = 'wmattachsize';
        sizeSpan.textContent = formatBytes(file.size);

        const deleteSpan = document.createElement('span');
        deleteSpan.className = 'wmattachdelete';
        deleteSpan.textContent = '×';
        deleteSpan.onclick = (e) => {
            e.stopPropagation();

            // Remove all references of this file from the editor content
            const fileName = file.name;
            const esc = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const regex = new RegExp(`\\[img:${esc}\\]|\\[file:${esc}\\]`, 'g');
            textarea.value = textarea.value.replace(regex, '');
            recordHistory(textarea.value);

            row.remove();
            attachedFiles.delete(file.name);
        };
        const insertRef = () => {
            const start = textarea.selectionStart;
            const ref = imageExts.has(ext) ? `[img:${file.name}]` : `[file:${file.name}]`;
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + ref.length, start + ref.length);
        };

        row.onclick = (e) => {
            if (e.target === deleteSpan) return;
            insertRef();
        };

        row.append(typeSpan, nameSpan, sizeSpan, deleteSpan);
        return row;
    }

    attachBtn.onclick = () => attachInput.click();

    attachInput.onchange = e => {
        for (let file of e.target.files) {
            if (attachedFiles.has(file.name)) {
                alert(`File "${file.name}" is already attached.`);
                continue;
            }
    
            // Add to UI
            attachmentsContainer.appendChild(createAttachmentRow(file));
            attachedFiles.set(file.name, file);
    
            // Auto-insert placeholder into textarea at cursor position
            const ext = getExt(file.name);
            const isImage = imageExts.has(ext);
            const placeholder = `[${isImage ? 'img' : 'file'}:${file.name}]`;
    
            const start = textarea.selectionStart;
            textarea.value = textarea.value.slice(0, start) + placeholder + textarea.value.slice(start);
            recordHistory(textarea.value);
            textarea.focus();
            textarea.setSelectionRange(start + placeholder.length, start + placeholder.length);
        }
        attachInput.value = '';
    };

    // === Button Wrapping ===
    function wrapSelection(before, after = '') {
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const selected = textarea.value.substring(start, end);
        textarea.value = textarea.value.substring(0, start) + before + selected + after + textarea.value.substring(end);
        recordHistory(textarea.value);
        textarea.focus();
        textarea.setSelectionRange(start + before.length, start + before.length + selected.length);
    }

    // Default buttons
    document.querySelectorAll('.ejwoombuttons span:not(.ejwoomaddbutton):not(.ejwoomcustombutton)').forEach(span => {
        span.onclick = () => wrapSelection(`<${span.textContent}>`, `</${span.textContent}>`);
    });

    // New: Refactored function to apply click logic to custom buttons
    function applyCustomButtonLogic(targetSpan) {
        targetSpan.onclick = () => {
            const beforeRaw = targetSpan.dataset.before ? atob(targetSpan.dataset.before) : '';
            const afterRaw = targetSpan.dataset.after ? atob(targetSpan.dataset.after) : '';

            if (beforeRaw === '' && afterRaw === '') {
                // If both before and after are empty, use <name>selected text</name>
                const name = targetSpan.textContent.trim();
                wrapSelection(`<${name}>`, `</${name}>`);
            } else {
                // Otherwise use custom before/after (even if one is empty)
                wrapSelection(beforeRaw, afterRaw);
            }
        };
    }

    // === Custom Button Editor (Fixed!) ===
    addButton.onclick = function () {
        const modal = document.createElement('div');
        modal.className = 'ejwoomwindow active';
        modal.innerHTML = `
            <div class="ejwoomwindow-content">
                <div class="ejwoom-btn-selector">
                    <span class="ejwoomselbutton selected" data-value="new">new</span>
                </div>
                <div class="ejwoom-input-row"><label>Name</label><input type="text" placeholder="e.g. blockquote"></div>
                <div class="ejwoom-input-row"><label>Before HTML</label><input type="text" placeholder="&lt;div class=&quot;tip&quot;&gt;"></div>
                <div class="ejwoom-input-row"><label>After HTML</label><input type="text" placeholder="&lt;/div&gt;"></div>
                <div class="ejwoom-modal-controls">
                    <button class="wmconfirm">Confirm</button>
                    <button class="wmdelete wmopbutton">Delete</button>
                    <button class="wmcancel">Cancel</button>
                </div>
            </div>`;
        document.body.appendChild(modal);

        const selector = modal.querySelector('.ejwoom-btn-selector');
        const nameInp = modal.querySelectorAll('input')[0];
        const beforeInp = modal.querySelectorAll('input')[1];
        const afterInp = modal.querySelectorAll('input')[2];
        const confirmBtn = modal.querySelector('.wmconfirm');
        const funcBtn = modal.querySelector('.wmopbutton');
        const cancelBtn = modal.querySelector('.wmcancel');

        // Load existing custom buttons into selector
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => {
            const sel = document.createElement('span');
            sel.className = 'ejwoomselbutton';
            sel.textContent = btn.textContent;
            sel.dataset.value = btn.textContent;
            sel._target = btn;
            selector.appendChild(sel);
        });

        function updateForm() {
            const selected = selector.querySelector('.selected');
            if (!selected || selected.dataset.value === 'new') {
                nameInp.value = beforeInp.value = afterInp.value = '';
                funcBtn.textContent = 'Insert';
                funcBtn.classList.remove('wmdelete'); // Remove delete style
                funcBtn.classList.add('wmconfirm');  // Use confirm style for insert
                funcBtn.disabled = false;
            } else {
                const target = selected._target;
                nameInp.value = target.textContent;
                beforeInp.value = target.dataset.before ? atob(target.dataset.before) : '';
                afterInp.value = target.dataset.after ? atob(target.dataset.after) : '';
                funcBtn.textContent = 'Delete';
                funcBtn.classList.add('wmdelete'); // Restore delete style
                funcBtn.classList.remove('wmconfirm'); // Remove confirm style
                funcBtn.disabled = false; // Enabled for existing buttons
            }
        }

        selector.onclick = e => {
            const btn = e.target.closest('.ejwoomselbutton');
            if (!btn) return;
            selector.querySelectorAll('.ejwoomselbutton').forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            updateForm();
        };

        // New logic for funcBtn (Delete/Insert)
        funcBtn.onclick = () => {
            const selected = selector.querySelector('.selected');

            if (selected.dataset.value === 'new') {
                // 'Insert' functionality
                const name = nameInp.value.trim();
                const beforeRaw = beforeInp.value;
                const afterRaw = afterInp.value;

                if (beforeRaw || afterRaw) {
                    // Use custom before/after
                    wrapSelection(beforeRaw, afterRaw);
                } else if (name) {
                    // Use <name>selected text</name>
                    wrapSelection(`<${name}>`, `</${name}>`);
                } else {
                    return alert('Enter a Name or Before/After HTML to insert.');
                }
                modal.remove(); // Close window after insertion
            } else {
                // 'Delete' functionality for existing button
                if (confirm(`Are you sure you want to delete the button "${selected.dataset.value}"?`)) {
                    selected._target.remove();
                    selected.remove();
                    selector.querySelector('[data-value="new"]').classList.add('selected');
                    updateForm();
                }
            }
        };

        confirmBtn.onclick = () => {
            const name = nameInp.value.trim();
            if (!name) return alert('Button name is required');

            const selected = selector.querySelector('.selected');
            let targetSpan;

            if (selected.dataset.value === 'new') {
                targetSpan = document.createElement('span');
                targetSpan.className = 'ejwoomcustombutton';
                buttonsContainer.insertBefore(targetSpan, addButton);

                const newSel = document.createElement('span');
                newSel.className = 'ejwoomselbutton';
                newSel.textContent = name;
                newSel.dataset.value = name;
                newSel._target = targetSpan;
                selector.appendChild(newSel);
            } else {
                targetSpan = selected._target;
            }

            targetSpan.textContent = name;
            if (beforeInp.value) targetSpan.dataset.before = btoa(beforeInp.value);
            else delete targetSpan.dataset.before;
            if (afterInp.value) targetSpan.dataset.after = btoa(afterInp.value);
            else delete targetSpan.dataset.after;

            // Apply click behavior
            applyCustomButtonLogic(targetSpan);
            modal.remove();
        };

        cancelBtn.onclick = () => modal.remove();
        updateForm();
    };

    // === Preview & Download (Fixed filename + [img:] [file:]) ===
    async function buildResult() {
        // Requirement 2: Collect all custom buttons to inject as <meta> in the final HTML
        const customButtons = Array.from(buttonsContainer.querySelectorAll('.ejwoomcustombutton'));
        let metaTagHTML = '<meta name="ejwoom">\n';
        if (customButtons.length > 0) {
            const allButtonsHTML = customButtons.map(btn => btn.outerHTML.trim()).join('\n');
            try {
                const buttonsBase64 = btoa(allButtonsHTML);
                metaTagHTML += `<meta name="ejwoombuttons" content="${buttonsBase64}">`;
            } catch (e) {
                console.error("Error encoding buttons:", e);
            }
        }

        resultDiv.innerHTML = `
            <div class="ejwoomdownload">
                <a id="downloadlink" download="">Download HTML</a>
                <span class="ejwoomdownloadsize"></span>
            </div>
            <div class="ejwoompreview"></div>
        `;


        const downloadLink = resultDiv.querySelector('#downloadlink');
        const sizeSpan = resultDiv.querySelector('.ejwoomdownloadsize');
        const previewDiv = resultDiv.querySelector('.ejwoompreview');

        let title = (titleInput.value || 'Untitled').trim();
        let processedContent = textarea.value.trim();
        const replacements = new Map();

        for (const [name, file] of attachedFiles) {
            const base64 = await fileToBase64(file);
            const ext = getExt(name);
            const isImage = imageExts.has(ext);

            let html = '';
            if (isImage) {
                let dims = '';
                if (ext === 'svg') {
                    try {
                        const svg = atob(base64.split(',')[1]);
                        const w = svg.match(/width=["'](\d+)["']/i);
                        const h = svg.match(/height=["'](\d+)["']/i);
                        if (w) dims += ` width="${w[1]}"`;
                        if (h) dims += ` height="${h[1]}"`;
                    } catch (e) {}
                }
                // Get current index in attachedFiles Map (which preserves insertion order)
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<img src="${base64}" alt="${name}" attachsort="${index}" style="max-width:100%;height:auto;"${dims}>`;
            } else {
                const index = Array.from(attachedFiles.keys()).indexOf(name);
                html = `<a href="${base64}" download="${name}" attachsort="${index}">Download ${name}</a>`;
            }

            replacements.set(`[img:${name}]`, html);
            replacements.set(`[file:${name}]`, html);
        }

        for (const [key, val] of replacements) {
            processedContent = processedContent.replace(
                new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), val
            );
        }

        previewDiv.innerHTML = processedContent;

        const fullHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width:device-width, initial-scale=1.0">
    <title>${title}</title>
    ${metaTagHTML}
</head>
<body>
${processedContent}
</body>
</html>`;

        const blob = new Blob([fullHTML], { type: 'text/html' });
        const url = URL.createObjectURL(blob);
        downloadLink.href = url;
        downloadLink.download = title + '.html';
        downloadLink.textContent = 'Download ' + title + '.html';
        sizeSpan.textContent = formatBytes(blob.size);
    }
    
    // === NEW: Read File Functions ===
    
    // 6.6: Parse HTML, extract attachments, and replace with placeholders
    function rebuildAttachmentsFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        
        // Clear current attachments
        attachmentsContainer.innerHTML = '';
        attachedFiles.clear();
        
        const attachmentData = []; // To store file + sort order
    
        tempDiv.querySelectorAll('img[src^="data:"], a[href^="data:"][download]').forEach(el => {
            const dataUrl = el.tagName === 'IMG' ? el.getAttribute('src') : el.getAttribute('href');
            const filename = el.tagName === 'IMG' ? el.getAttribute('alt') : el.getAttribute('download');
            const sortOrder = el.hasAttribute('attachsort') ? parseInt(el.getAttribute('attachsort')) : -1;
            
            if (!filename) {
                console.warn('Found data URL element without alt/download name. Skipping.', el);
                return;
            }
            
            if (attachedFiles.has(filename)) {
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
                return;
            }
            
            const file = dataURLtoFile(dataUrl, filename);
            if (file) {
                attachedFiles.set(file.name, file);
                attachmentData.push({ file, sortOrder });
                
                const placeholder = el.tagName === 'IMG' ? `[img:${filename}]` : `[file:${filename}]`;
                el.parentNode.replaceChild(document.createTextNode(placeholder), el);
            }
        });
        
        // Sort by attachsort (preserving original order if missing)
        attachmentData.sort((a, b) => {
            if (a.sortOrder === -1) return 1;
            if (b.sortOrder === -1) return -1;
            return a.sortOrder - b.sortOrder;
        });
        
        // Append rows in correct order
        attachmentData.forEach(item => {
            attachmentsContainer.appendChild(createAttachmentRow(item.file));
        });
        
        return tempDiv.innerHTML;
    }

    // 6.5: Load buttons from a parsed HTML document
    function parseAndLoadButtons(htmlDoc) {
        // Clear existing custom buttons
        buttonsContainer.querySelectorAll('.ejwoomcustombutton').forEach(btn => btn.remove());
        
        // Requirement 2: Read from meta tag
        const meta = htmlDoc.head.querySelector('meta[name="ejwoombuttons"]');
        if (meta && meta.content) {
            try {
                const buttonsHTML = atob(meta.content);
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = buttonsHTML;
                
                Array.from(tempDiv.children).forEach(node => {
                    if (node.tagName === 'SPAN' && node.classList.contains('ejwoomcustombutton')) {
                        // node is newly created by innerHTML, just append it
                        buttonsContainer.insertBefore(node, addButton);
                        applyCustomButtonLogic(node); // Re-apply click logic
                    }
                });
            } catch (e) {
                console.error("Error parsing custom buttons from meta tag:", e);
            }
        }
    }
    
    // 6.4: Get body HTML
    function parseAndLoadHTML(htmlDoc) {
        const body = htmlDoc.body;
        if (!body) return '';
        
        // Template removal is no longer needed as it's in the head.
        
        // Also try to find title
        const titleTag = htmlDoc.head.querySelector('title');
        if (titleTag && titleTag.textContent) {
            titleInput.value = titleTag.textContent;
        }

        return body.innerHTML.trim(); // Use innerHTML to skip the <body> tag itself
    }
    
    // Switch to Edit Tab helper
    function switchToEditTab() {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        editTab.classList.add('active');
        editDiv.classList.add('active');
        resultDiv.classList.remove('active');
        resultDiv.innerHTML = '';
        
        const readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
    }
    
    // === Tab Switching ===
    resultTab.onclick = () => {
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        resultTab.classList.add('active');
        editDiv.classList.remove('active');
        resultDiv.classList.add('active');
        
        const readDiv = document.querySelector('.ejwoomreadfile'); // New
        if (readDiv) readDiv.remove(); // New
        
        buildResult();
    };

    editTab.onclick = () => {
        switchToEditTab(); // Use helper
    };
    
    // New: Read File Tab click
    readfileTab.onclick = () => {
        // Set active tab
        document.querySelectorAll('.ejwoomtab span').forEach(s => s.classList.remove('active'));
        readfileTab.classList.add('active');
        
        // Hide other main divs
        editDiv.classList.remove('active');
        resultDiv.classList.remove('active');
        
        // Clean up old read div if it exists
        let readDiv = document.querySelector('.ejwoomreadfile');
        if (readDiv) readDiv.remove();
        
        // Create new read file UI
        readDiv = document.createElement('div');
        readDiv.className = 'ejwoomreadfile';
        readDiv.innerHTML = `
            <div class="ejwoomreadarea">Open the HTML file that you saved before.</div>
            <div class="ejwoomreadoptions">
                <a class="ejwoomreadall">Read All</a>
                <a class="ejwoomreadhtml">Read HTML Only</a>
                <a class="ejwoomreadbutton">Read Buttons Only</a>
            </div>
        `;
        document.body.appendChild(readDiv);
        
        const readArea = readDiv.querySelector('.ejwoomreadarea');
        const readOptions = readDiv.querySelector('.ejwoomreadoptions');
        
        // Reset function
        function resetReadFileUI() {
            readArea.innerHTML = 'Open the HTML file that you saved before.';
            readArea.classList.remove('selected');
            readOptions.style.display = 'none';
            const resetBtn = readDiv.querySelector('.ejwoomreadreset');
            if (resetBtn) resetBtn.remove();
            readHtmlInput.value = ''; // Clear file input
            loadedHtmlContent = null;
        }
        
        // Read area click
        readArea.onclick = () => readHtmlInput.click();
        
        // Read file input change
        readHtmlInput.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            
            const reader = new FileReader();
            reader.onload = (ev) => {
                loadedHtmlContent = ev.target.result;

                // Chech if created by ejwoom
                if (!loadedHtmlContent) return alert('No file loaded.');
                const parser = new DOMParser();
                const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
                const metaCheck = doc.querySelector('meta[name="ejwoom"]');
                if (!metaCheck) {
                    alert("This file is not created by Ejwoom. Cannot read.");
                    resetReadFileUI();
                    return;
                }
                
                // Update UI
                readArea.textContent = file.name;
                readArea.classList.add('selected');
                readOptions.style.display = 'flex';
                
                // Add reset button if it doesn't exist
                if (!readDiv.querySelector('.ejwoomreadreset')) {
                    const resetBtn = document.createElement('div');
                    resetBtn.className = 'ejwoomreadreset';
                    resetBtn.textContent = 'Reset file selection.';
                    resetBtn.onclick = resetReadFileUI;
                    readDiv.insertBefore(resetBtn, readArea);
                }
            };
            reader.onerror = () => {
                alert('Error reading file.');
                resetReadFileUI();
            };
            reader.readAsText(file);
        };
        
        // Option button clicks
        readDiv.querySelector('.ejwoomreadall').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadhtml').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            let bodyHtml = parseAndLoadHTML(doc);
            textarea.value = rebuildAttachmentsFromHTML(bodyHtml).trim();
            resetHistory();
            
            switchToEditTab();
        };
        
        readDiv.querySelector('.ejwoomreadbutton').onclick = () => {
            if (!loadedHtmlContent) return alert('No file loaded.');
            const parser = new DOMParser();
            const doc = parser.parseFromString(loadedHtmlContent, 'text/html');
            
            parseAndLoadButtons(doc);
            
            switchToEditTab();
        };
    };

    // === New: Init Undo/Redo and Textarea Input ===
    updateUndoRedoButtons();
    undoBtn.onclick = doUndo;
    redoBtn.onclick = doRedo;
    textarea.oninput = () => {
        recordHistory(textarea.value);
    };

});
</script>
</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: Ahrefs [Bot] 和 10 访客