來做一個html生成器

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

來做一個html生成器

帖子 ejsoon »

代码: 全选

做一個html+css+js:

一,html包含:

div.ejwoomtab>(
span.ejwoomedittab+
span.ejwoomresulttab
)

div.ejwoomedit>(
div.ejwoomcontrol+
input.ejwoomtitle[type=text]+
div.ejwoombuttons+
textarea.ejwoomcontent+
button.ejwoomattach{+}
)

div.ejwoomresult>(
div.ejwoomdownload
div.ejwoompreview
)

二,css:

.ejwoomtitle寬為100%,注意設box-sizing使其不出現横向滾動。

.ejwoomcontent高70vh,font-size為16px。

.ejwoomattach為一個寬和高都為36px的圖標,綠底,圓角。

三,當點擊ejwoomattach時,它將讀取一個本地文件。(不允許上傳文件名相同的文件。)

讀到之後在.ejwoomattach的上方生成一個div.ejwoomattached。其內容為

div.ejwoomattached>(span.wmattachtype+span.wmattachname+span.wmattachsize+span.wmattachdelete)

如果這個文件是png、jpg、svg、gif等圖片文件時,wmattachtype的內容將是img;如果這個文件是zip、7z、txt等其它文件時,wmattachtype的內容將是file。
wmattachname內容是文件名。
wmattachsize是文件大小(byte)。
當div.ejwoomattached被點擊時,textarea.ejwoomcontent的光標處將增加[wmattachname]。
當點擊span.wmattachdelete時,這個div.ejwoomattached將被刪除。

div.ejwoomattached將只有一行,span.wmattachname佔據剩餘寬度,如果文件名過長,將被省略成「...」,但需要顯示擴展名。

span.wmattachdelete的內容是一個乘號。

span.wmattachtype、span.wmattachname、span.wmattachsize、span.wmattachdelete的背景色都不相同。

四,textarea.ejwoomcontent的默認內容為:

<div style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all;">
<h1></h1>



</div>

五,ejwoombuttons內含:加粗、h1到h6、下劃線、刪除線、斜體、ul,ol,li、dl,dt,dd、details跟summary、fieldset跟legend。

當點擊時,將直接在textarea.ejwoomcontent的光標處將增加相應的html文本。

六,起初ejwoomedittab默認選中,div.ejwoomedit顯示,div.ejwoomresult不存在。

當點擊span.ejwoomresulttab時,它被選中,div.ejwoomedit隱藏,div.ejwoomresult將被構建。

ejwoomresult構建時:

div.ejwoomdownload將出現a[download],下載文件名為「ejwoomtitle的內容.html」,還有span.ejwoomdownloadsize(byte),下載內容將是textarea.ejwoomcontent的內容包進一個html的body中,如果有<style>,它將被移至head,head中有<title>,它的值為ejwoomtitle的內容。head中還要有meta-chatset-utf8。要有html的文件頭。

div.ejwoompreview將把textarea.ejwoomcontent的內容直接顯示。

如果textarea.ejwoomcontent的內容包含[img文件名],則它將被轉成img[src=base64],並定義寬和高。注意svg的寬高獲取跟png、jpg不同。

如果textarea.ejwoomcontent的內容包含[file文件名],則它將被轉成a[download=base64],點擊可下載文件。

當點擊回ejwoomedittab時,div.ejwoomresult將被銷毀,div.ejwoomedit重新顯示。

七,div.ejwoomcontrol包含span.ejwoomundo、span.ejwoomredo、span.ejwoomreadfile、span.ejwoomstorage。

當undo和redo點擊時,將對div.ejwoomedit內的所有操作進行撤銷和重做。

當點擊span.ejwoomreadfile時,將讀取之前下載的html,將能把input.ejwoomtitle[type=text]、textarea.ejwoomcontent、div.ejwoomattached生成恢復。

當點擊span.ejwoomstorage時,將把html的記錄在localStorage中。如果localStorage有數據,將在span.ejwoomstorage右方出現span.ejwoomgetstorage,點擊後會出現全屏彈窗,可點選之前保存的html的文件名及保存時間yyyy-mm-dd hh:ii:ss以及大小byte。每項的最左方還要有刪除按鈕。彈窗的右上角有關閉按鈕。


八,回答和代碼全部使用英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4100
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 163 次
被圈友点赞: 193 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

grok4.1

代码: 全选

<!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 button {
            padding: 8px 12px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .ejwoombuttons button: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;
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            padding: 8px;
            margin: 8px 0;
            font-size: 14px;
            cursor: pointer;
        }
        .wmattachtype { width: 24px; text-align: center; margin-right: 8px; }
        .wmattachname {
            flex: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            margin-right: 8px;
        }
        .wmattachsize { background: #e9ecef; padding: 2px 6px; border-radius: 4px; margin-right: 8px; font-size: 12px; }
        .wmattachdelete {
            background: #dc3545;
            color: white;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            text-align: center;
            line-height: 18px;
            cursor: pointer;
            font-weight: bold;
        }

        .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;
        }

        /* Storage modal */
        .ejwoommodal {
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.7);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 999;
        }
        .ejwoommodal.active { display: flex; }
        .ejwoommodal-content {
            background: white;
            width: 90%;
            max-width: 700px;
            max-height: 80vh;
            border-radius: 8px;
            overflow: hidden;
            position: relative;
        }
        .ejwoommodal-header {
            padding: 15px 20px;
            background: #333;
            color: white;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .ejwoomclose { cursor: pointer; font-size: 24px; }
        .ejwoomlist { padding: 0; max-height: 60vh; overflow-y: auto; }
        .ejwoomitem {
            padding: 12px 20px;
            border-bottom: 1px solid #eee;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .ejwoomitem:hover { background: #f8f9fa; }
        .ejwoomdeleteitem { color: red; cursor: pointer; }
    </style>
</head>
<body>

<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview & Download</span>
</div>

<div class="ejwoomedit active">
    <div class="ejwoomcontrol">
        <span class="ejwoomundo">Undo</span>
        <span class="ejwoomredo">Redo</span>
        <span class="ejwoomreadfile">Load HTML</span>
        <span class="ejwoomstorage">Save to Browser <span id="storagecount"></span></span>
        <span class="ejwoomgetstorage" style="display:none; color:#007bff; cursor:pointer;">(View Saved)</span>
    </div>

    <input type="text" class="ejwoomtitle" placeholder="Enter title...">

    <div class="ejwoombuttons">
        <button data-tag="b">Bold</button>
        <button data-tag="h1">H1</button>
        <button data-tag="h2">H2</button>
        <button data-tag="h3">H3</button>
        <button data-tag="h4">H4</button>
        <button data-tag="h5">H5</button>
        <button data-tag="h6">H6</button>
        <button data-tag="u">Underline</button>
        <button data-tag="s">Strikethrough</button>
        <button data-tag="i">Italic</button>
        <button data-tag="ul">UL</button>
        <button data-tag="ol">OL</button>
        <button data-tag="li">LI</button>
        <button data-tag="dl">DL</button>
        <button data-tag="dt">DT</button>
        <button data-tag="dd">DD</button>
        <button data-tag="details">Details</button>
        <button data-tag="summary">Summary</button>
        <button data-tag="fieldset">Fieldset</button>
        <button data-tag="legend">Legend</button>
    </div>

    <div id="attachments"></div>
    <button class="ejwoomattach" title="Attach file">+</button>

    <textarea class="ejwoomcontent">&lt;div style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all;"&gt;
&lt;h1&gt;&lt;/h1&gt;



&lt;/div&gt;</textarea>
</div>

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

<!-- Hidden file inputs -->
<input type="file" id="attachfile" style="display:none;" multiple accept="*">
<input type="file" id="loadhtml" style="display:none;" accept=".html,.htm">

<!-- Storage Modal -->
<div class="ejwoommodal" id="storagemodal">
    <div class="ejwoommodal-content">
        <div class="ejwoommodal-header">
            <h3>Saved Documents</h3>
            <span class="ejwoomclose">&times;</span>
        </div>
        <div class="ejwoomlist" id="savedlist"></div>
    </div>
</div>

<script>
const editTab = document.querySelector('.ejwoomedittab');
const resultTab = document.querySelector('.ejwoomresulttab');
const editPanel = document.querySelector('.ejwoomedit');
const resultPanel = document.querySelector('.ejwoomresult');
const titleInput = document.querySelector('.ejwoomtitle');
const contentTextarea = document.querySelector('.ejwoomcontent');
const attachBtn = document.querySelector('.ejwoomattach');
const attachmentsDiv = document.getElementById('attachments');

let attachments = []; // {name, type, size, dataURL, file}
let history = [];
let historyIndex = -1;

const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp'];
const tagMap = {
    'b': '<b></b>',
    'i': '<i></i>',
    'u': '<u></u>',
    's': '<s></s>',
    'h1': '<h1></h1>',
    'h2': '<h2></h2>',
    'h3': '<h3></h3>',
    'h4': '<h4></h4>',
    'h5': '<h5></h5>',
    'h6': '<h6></h6>',
    'ul': '<ul>\n  <li></li>\n</ul>',
    'ol': '<ol>\n  <li></li>\n</ol>',
    'li': '<li></li>',
    'dl': '<dl>\n  <dt></dt>\n  <dd></dd>\n</dl>',
    'dt': '<dt></dt>',
    'dd': '<dd></dd>',
    'details': '<details>\n  <summary>Click to expand</summary>\n\n</details>',
    'summary': '<summary></summary>',
    'fieldset': '<fieldset>\n  <legend></legend>\n</fieldset>',
    'legend': '<legend></legend>'
};

// Save to history
function saveHistory() {
    history = history.slice(0, historyIndex + 1);
    history.push({
        title: titleInput.value,
        content: contentTextarea.value,
        attachments: attachments.map(a => ({name: a.name, type: a.type, size: a.size, dataURL: a.dataURL}))
    });
    historyIndex++;
}

// Insert at cursor
function insertAtCursor(text, focusAfter = true) {
    const start = contentTextarea.selectionStart;
    const end = contentTextarea.selectionEnd;
    const value = contentTextarea.value;
    contentTextarea.value = value.slice(0, start) + text + value.slice(end);
    if (focusAfter) {
        contentTextarea.focus();
        contentTextarea.setSelectionRange(start + text.length, start + text.length);
    }
    saveHistory();
}

// Formatting buttons
document.querySelectorAll('.ejwoombuttons button').forEach(btn => {
    btn.addEventListener('click', () => {
        const tag = btn.getAttribute('data-tag');
        insertAtCursor(tagMap[tag]);
    });
});

// Attach file
attachBtn.addEventListener('click', () => document.getElementById('attachfile').click());

document.getElementById('attachfile').addEventListener('change', e => {
    const files = e.target.files;
    for (let file of files) {
        if (attachments.some(a => a.name === file.name)) {
            alert(`File "${file.name}" already attached.`);
            continue;
        }
        const reader = new FileReader();
        reader.onload = function(ev) {
            const ext = file.name.split('.').pop().toLowerCase();
            const isImage = imageExts.includes(ext);
            const type = isImage ? 'img' : 'file';
            attachments.push({
                name: file.name,
                type,
                size: file.size,
                dataURL: ev.target.result,
                file
            });
            renderAttachments();
            saveHistory();
        };
        reader.readAsDataURL(file);
    }
    e.target.value = '';
});

function renderAttachments() {
    attachmentsDiv.innerHTML = '';
    attachments.forEach((att, i) => {
        const div = document.createElement('div');
        div.className = 'ejwoomattached';
        div.innerHTML = `
            <span class="wmattachtype">${att.type === 'img' ? '🖼' : '📄'}</span>
            <span class="wmattachname" title="${att.name}">${att.name}</span>
            <span class="wmattachsize">${formatBytes(att.size)}</span>
            <span class="wmattachdelete">×</span>
        `;
        div.addEventListener('click', e => {
            if (e.target.classList.contains('wmattachdelete')) {
                attachments.splice(i, 1);
                renderAttachments();
                saveHistory();
            } else {
                insertAtCursor(`[${att.name}]`);
            }
        });
        attachmentsDiv.appendChild(div);
    });
}

function formatBytes(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
    return (bytes/1048576).toFixed(1) + ' MB';
}

// Undo / Redo
document.querySelector('.ejwoomundo').addEventListener('click', () => {
    if (historyIndex > 0) {
        historyIndex--;
        restoreState(history[historyIndex]);
    }
});
document.querySelector('.ejwoomredo').addEventListener('click', () => {
    if (historyIndex < history.length - 1) {
        historyIndex++;
        restoreState(history[historyIndex]);
    }
});

function restoreState(state) {
    titleInput.value = state.title || '';
    contentTextarea.value = state.content || '';
    attachments = state.attachments ? state.attachments.map(a => ({
        ...a,
        file: null
    })) : [];
    renderAttachments();
}

// Save to localStorage
document.querySelector('.ejwoomstorage').addEventListener('click', () => {
    const key = 'ejwoom_' + Date.now();
    const data = {
        title: titleInput.value,
        content: contentTextarea.value,
        attachments: attachments,
        timestamp: new Date().toISOString(),
        size: new Blob([JSON.stringify({titleInput.value, contentTextarea.value, attachments})]).size
    };
    localStorage.setItem(key, JSON.stringify(data));
    updateStorageCount();
    alert('Saved to browser storage!');
});

// Load from saved
document.querySelector('.ejwoomgetstorage').addEventListener('click', () => {
    document.getElementById('storagemodal').classList.add('active');
    renderSavedList();
});

document.querySelector('.ejwoomclose').addEventListener('click', () => {
    document.getElementById('storagemodal').classList.remove('active');
});

function updateStorageCount() {
    let count = 0;
    for (let i = 0; i < localStorage.length; i++) {
        if (localStorage.key(i).startsWith('ejwoom_')) count++;
    }
    const indicator = document.getElementById('storagecount');
    if (count > 0) {
        indicator.textContent = `(${count})`;
        document.querySelector('.ejwoomgetstorage').style.display = 'inline';
    } else {
        indicator.textContent = '';
        document.querySelector('.ejwoomgetstorage').style.display = 'none';
    }
}

function renderSavedList() {
    const list = document.getElementById('savedlist');
    list.innerHTML = '';
    const keys = [];
    for (let i = 0; i < localStorage.length; i++) {
        const k = localStorage.key(i);
        if (k.startsWith('ejwoom_')) keys.push(k);
    }
    keys.sort((a,b) => b.localeCompare(a));
    keys.forEach(key => {
        const data = JSON.parse(localStorage.getItem(key));
        const div = document.createElement('div');
        div.className = 'ejwoomitem';
        div.innerHTML = `
            <span class="ejwoomdeleteitem" title="Delete">×</span>
            <strong>${data.title || 'Untitled'}</strong>
            <span style="flex:1; text-align:right; color:#666; font-size:13px;">
                ${new Date(data.timestamp).toLocaleString()} — ${formatBytes(data.size)}
            </span>
        `;
        div.querySelector('.ejwoomdeleteitem').addEventListener('click', e => {
            e.stopPropagation();
            if (confirm('Delete this saved document?')) {
                localStorage.removeItem(key);
                renderSavedList();
                updateStorageCount();
            }
        });
        div.addEventListener('click', () => {
            restoreState({
                title: data.title,
                content: data.content,
                attachments: data.attachments
            });
            document.getElementById('storagemodal').classList.remove('active');
        });
        list.appendChild(div);
    });
}

// Load HTML file
document.querySelector('.ejwoomreadfile').addEventListener('click', () => {
    document.getElementById('loadhtml').click();
});
document.getElementById('loadhtml').addEventListener('change', e => {
    const file = e.target.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = function(ev) {
        const html = ev.target.result;
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        titleInput.value = doc.querySelector('title')?.textContent || '';
        let bodyHTML = doc.body.innerHTML;
        // Extract style if exists
        const style = doc.querySelector('style')?.outerHTML || '';
        if (style) {
            bodyHTML = bodyHTML.replace(style, '');
        }
        contentTextarea.value = bodyHTML;
        // Try to extract attachments from base64
        attachments = [];
        renderAttachments();
        saveHistory();
        alert('HTML loaded successfully!');
    };
    reader.readAsText(file);
    e.target.value = '';
});

// Tab switching
editTab.addEventListener('click', () => {
    editTab.classList.add('active');
    resultTab.classList.remove('active');
    editPanel.classList.add('active');
    resultPanel.classList.remove('active');
    resultPanel.innerHTML = '';
});

resultTab.addEventListener('click', () => {
    editTab.classList.remove('active');
    resultTab.classList.add('active');
    editPanel.classList.remove('active');
    resultPanel.classList.add('active');
    buildResult();
});

function buildResult() {
    let content = contentTextarea.value;
    let downloadHTML = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${titleInput.value || 'Untitled'}</title>
    <style>
        body { font-family: system-ui, sans-serif; line-height: 1.6; padding: 20px; }
        pre, code { font-family: 'Courier New', monospace; }
        img { max-width: 100%; height: auto; border-radius: ; }
    </style>
</head>
<body>
${content}
</body>
</html>`;

    // Replace [filename] with actual files
    attachments.forEach(att => {
        const placeholder = `[${att.name}]`;
        if (att.type === 'img') {
            const imgTag = `<img src="${att.dataURL}" alt="${att.name}" style="max-width:100%;height:auto;">`;
            downloadHTML = downloadHTML.replace(new RegExp(escapeRegExp(placeholder), 'g'), imgTag);
            content = content.replace(new RegExp(escapeRegExp(placeholder), 'g'), imgTag);
        } else {
            const aTag = `<a href="${att.dataURL}" download="${att.name}">📎 Download: ${att.name} (${formatBytes(att.size)})</a>`;
            downloadHTML = downloadHTML.replace(new RegExp(escapeRegExp(placeholder), 'g'), aTag);
            content = content.replace(new RegExp(escapeRegExp(placeholder), 'g'), aTag);
        }
    });

    const blob = new Blob([downloadHTML], {type: 'text/html'});
    const url = URL.createObjectURL(blob);
    const size = blob.size;

    resultPanel.innerHTML = `
        <div class="ejwoomdownload">
            <a href="${url}" download="${(titleInput.value || 'document').replace(/[^a-z0-9]/gi, '_')}.html">📥 Download HTML</a>
            <span class="ejwoomdownloadsize">${formatBytes(size)}</span>
        </div>
        <div class="ejwoompreview">${content}</div>
    `;
}

function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

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

Re: 來做一個html生成器

帖子 ejsoon »

gemini

代码: 全选

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>EJWOOM Editor</title>
    <style>
        /* CSS Reset and Basic Setup */
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: Arial, sans-serif;
            padding: 10px;
            background-color: #f4f4f4;
        }

        /* Tab Styles */
        .ejwoomtab {
            border-bottom: 2px solid #ccc;
            margin-bottom: 10px;
        }

        .ejwoomtab span {
            display: inline-block;
            padding: 10px 15px;
            cursor: pointer;
            background-color: #eee;
            border: 1px solid #ccc;
            border-bottom: none;
            border-radius: 5px 5px 0 0;
            position: relative;
            bottom: -1px;
        }

        .ejwoomtab span.selected {
            background-color: #fff;
            border-bottom: 2px solid #fff;
            font-weight: bold;
        }

        /* Edit Area Styles */
        .ejwoomedit {
            display: block;
        }

        /* Control Bar */
        .ejwoomcontrol {
            background-color: #f0f0f0;
            padding: 5px;
            border: 1px solid #ddd;
            border-radius: 5px;
            margin-bottom: 5px;
        }

        .ejwoomcontrol span {
            display: inline-block;
            padding: 5px 8px;
            cursor: pointer;
            background-color: #e0e0e0;
            border: 1px solid #ccc;
            border-radius: 3px;
            margin-right: 5px;
            font-size: 14px;
        }
        .ejwoomcontrol span:hover {
            background-color: #d0d0d0;
        }

        /* Requirement 2: .ejwoomtitle */
        .ejwoomtitle {
            width: 100%;
            box-sizing: border-box; /* As requested */
            padding: 10px;
            font-size: 1.2em;
            margin-bottom: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        /* Editor Buttons */
        .ejwoombuttons {
            background-color: #f0f0f0;
            padding: 5px;
            border: 1px solid #ddd;
            border-radius: 5px;
            margin-bottom: 5px;
            display: flex;
            flex-wrap: wrap;
            gap: 5px;
        }

        .ejwoombuttons button {
            padding: 5px 8px;
            cursor: pointer;
            background-color: #e0e0e0;
            border: 1px solid #ccc;
            border-radius: 3px;
            font-family: monospace;
            font-size: 14px;
        }
        .ejwoombuttons button:hover {
            background-color: #d0d0d0;
        }

        /* Requirement 2: .ejwoomcontent */
        .ejwoomcontent {
            width: 100%;
            height: 70vh;
            font-size: 16px;
            font-family: 'Courier New', Courier, monospace;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            margin-bottom: 10px;
            resize: vertical;
        }

        /* Requirement 2: .ejwoomattach */
        .ejwoomattach {
            width: 36px;
            height: 36px;
            background-color: green;
            border-radius: 5px; /* "rounded corners" */
            color: white;
            font-size: 24px;
            font-weight: bold;
            line-height: 36px;
            text-align: center;
            cursor: pointer;
            border: none;
        }

        /* Attachment Styles */
        .ejwoomattached {
            display: flex;
            flex-direction: row;
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
            margin-bottom: 5px;
            overflow: hidden; /* Ensures single line */
            cursor: pointer;
            background-color: #f9f9f9;
        }
        .ejwoomattached:hover {
            background-color: #f0f0f0;
        }

        .ejwoomattached span {
            padding: 8px;
            white-space: nowrap;
        }

        .wmattachtype {
            background-color: #dbeafe; /* Light Blue */
            font-weight: bold;
            flex-shrink: 0;
        }

        .wmattachname {
            background-color: #fef3c7; /* Light Yellow */
            flex-grow: 1; /* Takes remaining space */
            overflow: hidden;
            text-overflow: ellipsis; /* ... as requested */
        }

        .wmattachsize {
            background-color: #dcfce7; /* Light Green */
            flex-shrink: 0;
            font-size: 0.9em;
            color: #333;
        }

        .wmattachdelete {
            background-color: #fee2e2; /* Light Red */
            flex-shrink: 0;
            font-weight: bold;
            cursor: pointer;
        }
        .wmattachdelete:hover {
            background-color: #fca5a5; /* Darker Red */
        }

        /* Result Area */
        .ejwoomresult {
            display: none; /* Hidden by default */
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #fff;
            border-radius: 5px;
        }

        .ejwoomdownload {
            padding: 10px;
            background-color: #f5f5f5;
            border: 1px solid #ddd;
            border-radius: 5px;
            margin-bottom: 15px;
        }
        .ejwoomdownload a {
            padding: 8px 12px;
            background-color: #28a745;
            color: white;
            text-decoration: none;
            border-radius: 4px;
            margin-right: 10px;
        }
        .ejwoomdownload a:hover {
            background-color: #218838;
        }
        .ejwoomdownloadsize {
            font-size: 0.9em;
            color: #555;
        }

        .ejwoompreview {
            border: 1px dashed #ccc;
            padding: 15px;
            background-color: #fff;
            min-height: 100px;
        }
        
        /* Fix for user's default content style */
        .ejwoompreview > div[style] {
            font-family: 'Courier New', Courier, monospace;
        }

        /* LocalStorage Modal */
        #storageModal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0, 0, 0, 0.5);
        }
        #storageModalContent {
            background-color: #fefefe;
            margin: 10% auto;
            padding: 20px;
            border: 1px solid #888;
            width: 80%;
            max-width: 800px;
            border-radius: 8px;
            position: relative;
        }
        #storageModalClose {
            color: #aaa;
            position: absolute;
            top: 10px;
            right: 20px;
            font-size: 28px;
            font-weight: bold;
            cursor: pointer;
        }
        #storageModalClose:hover {
            color: #000;
        }
        #storageList {
            list-style: none;
            padding: 0;
            margin-top: 20px;
            max-height: 60vh;
            overflow-y: auto;
        }
        #storageList li {
            display: flex;
            border: 1px solid #eee;
            margin-bottom: 5px;
            align-items: center;
        }
        #storageList li > span {
            padding: 10px;
            border-left: 1px solid #eee;
        }
        .storage-delete {
            background-color: #fee2e2;
            padding: 10px;
            cursor: pointer;
            font-weight: bold;
            border: none;
        }
        .storage-delete:hover {
            background-color: #fca5a5;
        }
        .storage-item {
            flex-grow: 1;
            cursor: pointer;
            padding: 10px;
        }
        .storage-item:hover {
            background-color: #f0f0f0;
        }
        .storage-name {
            font-weight: bold;
            flex-basis: 40%;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .storage-time {
            flex-basis: 35%;
            font-size: 0.9em;
            color: #555;
        }
        .storage-size {
            flex-basis: 20%;
            font-size: 0.9em;
            color: #555;
            text-align: right;
        }

    </style>
</head>
<body>

    <div class="ejwoomtab">
        <span class="ejwoomedittab selected">Edit</span>
        <span class="ejwoomresulttab">Result</span>
    </div>

    <div class="ejwoomedit">
        <div class="ejwoomcontrol">
            <span class="ejwoomundo">Undo</span>
            <span class="ejwoomredo">Redo</span>
            <span class="ejwoomreadfile">Read HTML File</span>
            <span class="ejwoomstorage">Save to Storage</span>
            </div>

        <input class="ejwoomtitle" type="text" placeholder="Enter title here...">

        <div class="ejwoombuttons">
            <button data-tag="b">&lt;b&gt;</button>
            <button data-tag="h1">&lt;h1&gt;</button>
            <button data-tag="h2">&lt;h2&gt;</button>
            <button data-tag="h3">&lt;h3&gt;</button>
            <button data-tag="h4">&lt;h4&gt;</button>
            <button data-tag="h5">&lt;h5&gt;</button>
            <button data-tag="h6">&lt;h6&gt;</button>
            <button data-tag="u">&lt;u&gt;</button>
            <button data-tag="s">&lt;s&gt;</button>
            <button data-tag="i">&lt;i&gt;</button>
            <button data-snippet="<ul>\n  <li></li>\n</ul>">&lt;ul&gt;</button>
            <button data-snippet="<ol>\n  <li></li>\n</ol>">&lt;ol&gt;</button>
            <button data-snippet="<dl>\n  <dt></dt>\n  <dd></dd>\n</dl>">&lt;dl&gt;</button>
            <button data-snippet="<details>\n  <summary></summary>\n  \n</details>">&lt;details&gt;</button>
            <button data-snippet="<fieldset>\n  <legend></legend>\n  \n</fieldset>">&lt;fieldset&gt;</button>
        </div>

        <textarea class="ejwoomcontent"></textarea>
        
        <div class="ejwoom-attach-container">
            <button class="ejwoomattach">+</button>
        </div>
    </div>

    <input type="file" id="fileInput" style="display: none;" />
    <input type="file" id="htmlFileInput" style="display: none;" accept=".html, .htm" />

    <div id="storageModal">
        <div id="storageModalContent">
            <span id="storageModalClose">&times;</span>
            <h2>Saved Items</h2>
            <ul id="storageList">
                </ul>
        </div>
    </div>


    <script>
        document.addEventListener('DOMContentLoaded', () => {
            
            // --- DOM Element References ---
            const editTab = document.querySelector('.ejwoomedittab');
            const resultTab = document.querySelector('.ejwoomresulttab');
            const editPanel = document.querySelector('.ejwoomedit');
            const tabContainer = document.querySelector('.ejwoomtab');
            const titleInput = document.querySelector('.ejwoomtitle');
            const contentArea = document.querySelector('.ejwoomcontent');
            const attachButton = document.querySelector('.ejwoomattach');
            const attachContainer = document.querySelector('.ejwoom-attach-container');
            const fileInput = document.getElementById('fileInput');
            const htmlFileInput = document.getElementById('htmlFileInput');
            const buttonContainer = document.querySelector('.ejwoombuttons');
            
            // Control Bar
            const controlBar = document.querySelector('.ejwoomcontrol');
            const undoBtn = document.querySelector('.ejwoomundo');
            const redoBtn = document.querySelector('.ejwoomredo');
            const readFileBtn = document.querySelector('.ejwoomreadfile');
            const storageBtn = document.querySelector('.ejwoomstorage');
            
            // Storage Modal
            const storageModal = document.getElementById('storageModal');
            const storageModalClose = document.getElementById('storageModalClose');
            const storageList = document.getElementById('storageList');

            // --- State Variables ---
            /**
             * @type {Map<string, {name: string, type: string, size: number, base64: string, fileType: 'img' | 'file'}>}
             */
            let attachedFiles = new Map();
            let historyStack = [];
            let redoStack = [];
            let currentHistoryIndex = -1; // Points to the current state in historyStack
            let isUndoingOrRedoing = false; // Flag to prevent saving state during undo/redo


            // --- Requirement 4: Default Textarea Content ---
            const defaultContent = `<div style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all;">
<h1></h1>



</div>`;
            contentArea.value = defaultContent;

            
            // --- Requirement 7: History (Undo/Redo) ---
            
            /** Saves the current editor state to the history stack */
            function saveState() {
                if (isUndoingOrRedoing) return; // Don't save state while loading a state

                const state = {
                    title: titleInput.value,
                    content: contentArea.value,
                    files: new Map(attachedFiles) // Clone the map
                };

                // Clear redo stack because we are starting a new timeline
                redoStack = [];

                // If we've undone, truncate the history
                if (currentHistoryIndex < historyStack.length - 1) {
                    historyStack = historyStack.slice(0, currentHistoryIndex + 1);
                }

                historyStack.push(state);
                currentHistoryIndex = historyStack.length - 1;
                updateUndoRedoButtons();
            }

            /** Loads a specific state into the editor */
            function loadState(state) {
                isUndoingOrRedoing = true; // Set flag
                try {
                    titleInput.value = state.title;
                    contentArea.value = state.content;
                    attachedFiles = new Map(state.files); // Restore files map
                    
                    // Re-render attachment elements
                    // Clear existing elements first
                    document.querySelectorAll('.ejwoomattached').forEach(el => el.remove());
                    // Add new ones
                    attachedFiles.forEach(fileData => {
                        createAttachmentElement(fileData);
                    });
                } finally {
                    isUndoingOrRedoing = false; // Unset flag
                }
                updateUndoRedoButtons();
            }
            
            function updateUndoRedoButtons() {
                undoBtn.style.opacity = currentHistoryIndex > 0 ? '1' : '0.5';
                redoBtn.style.opacity = redoStack.length > 0 ? '1' : '0.5';
            }

            undoBtn.addEventListener('click', () => {
                if (currentHistoryIndex > 0) {
                    // Push current state to redo stack
                    redoStack.push(historyStack.pop());
                    currentHistoryIndex--;
                    loadState(historyStack[currentHistoryIndex]);
                }
            });

            redoBtn.addEventListener('click', () => {
                if (redoStack.length > 0) {
                    const stateToRestore = redoStack.pop();
                    historyStack.push(stateToRestore);
                    currentHistoryIndex++;
                    loadState(stateToRestore);
                }
            });
            
            // Debounce function
            function debounce(func, delay) {
                let timeout;
                return function(...args) {
                    clearTimeout(timeout);
                    timeout = setTimeout(() => func.apply(this, args), delay);
                };
            }
            
            // Save initial state
            saveState();
            
            // Add event listeners to save state on change
            const debouncedSaveState = debounce(saveState, 500);
            titleInput.addEventListener('input', debouncedSaveState);
            contentArea.addEventListener('input', debouncedSaveState);


            // --- Requirement 5: Editor Buttons ---
            buttonContainer.addEventListener('click', (e) => {
                if (e.target.tagName !== 'BUTTON') return;

                const button = e.target;
                const tag = button.dataset.tag;
                const snippet = button.dataset.snippet;
                const start = contentArea.selectionStart;
                const end = contentArea.selectionEnd;
                const selectedText = contentArea.value.substring(start, end);
                let replacement = '';

                if (snippet) {
                    replacement = snippet;
                } else if (tag) {
                    replacement = `<${tag}>${selectedText}</${tag}>`;
                }

                // Insert the text
                contentArea.value = contentArea.value.substring(0, start) + replacement + contentArea.value.substring(end);
                
                // Adjust cursor position
                if (selectedText) {
                    contentArea.focus();
                    contentArea.setSelectionRange(start + replacement.length, start + replacement.length);
                } else if (snippet) {
                     // Try to place cursor in a good spot
                    const cursorOffset = replacement.indexOf('</') -1; // e.g., before </li>
                    const finalPos = start + (cursorOffset > 0 ? cursorOffset : replacement.length);
                    contentArea.focus();
                    contentArea.setSelectionRange(finalPos, finalPos);
                } else {
                    const cursorOffset = tag.length + 2; // Place cursor inside the tags: <tag>|</tag>
                    contentArea.focus();
                    contentArea.setSelectionRange(start + cursorOffset, start + cursorOffset);
                }
                
                saveState(); // Save state immediately after button click
            });

            
            // --- Requirement 3: File Attachment ---
            attachButton.addEventListener('click', () => {
                fileInput.click();
            });

            fileInput.addEventListener('change', (e) => {
                const file = e.target.files[0];
                if (!file) return;

                // Check for duplicate filenames
                if (attachedFiles.has(file.name)) {
                    alert('A file with this name has already been attached.');
                    fileInput.value = ''; // Reset input
                    return;
                }

                const reader = new FileReader();
                reader.onload = (readEvent) => {
                    const base64 = readEvent.target.result;
                    const fileType = getFileType(file.type, file.name);

                    const fileData = {
                        name: file.name,
                        type: file.type,
                        size: file.size,
                        base64: base64,
                        fileType: fileType // 'img' or 'file'
                    };

                    attachedFiles.set(file.name, fileData);
                    createAttachmentElement(fileData);
                    saveState(); // Save state after attaching file
                };
                reader.readAsDataURL(file);

                fileInput.value = ''; // Reset input to allow attaching the same file after deletion
            });

            function getFileType(mimeType, fileName) {
                const imgTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml'];
                const imgExts = ['.png', '.jpg', '.jpeg', '.gif', '.svg'];

                if (imgTypes.includes(mimeType)) {
                    return 'img';
                }
                
                const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
                if (imgExts.includes(ext)) {
                    return 'img';
                }
                
                // All other types
                return 'file';
            }

            function createAttachmentElement(fileData) {
                const div = document.createElement('div');
                div.className = 'ejwoomattached';
                div.dataset.filename = fileData.name;

                const typeSpan = document.createElement('span');
                typeSpan.className = 'wmattachtype';
                typeSpan.textContent = fileData.fileType;

                const nameSpan = document.createElement('span');
                nameSpan.className = 'wmattachname';
                nameSpan.textContent = fileData.name;
                nameSpan.title = fileData.name; // Show full name on hover

                const sizeSpan = document.createElement('span');
                sizeSpan.className = 'wmattachsize';
                sizeSpan.textContent = `${fileData.size} bytes`;

                const deleteSpan = document.createElement('span');
                deleteSpan.className = 'wmattachdelete';
                deleteSpan.textContent = '×'; // Multiplication sign

                div.append(typeSpan, nameSpan, sizeSpan, deleteSpan);

                // Insert the new element *before* the attach button
                attachContainer.insertBefore(div, attachButton);

                // Add event listeners
                
                // Click on delete span
                deleteSpan.addEventListener('click', (e) => {
                    e.stopPropagation(); // Prevent div click handler from firing
                    attachedFiles.delete(fileData.name);
                    div.remove();
                    saveState(); // Save state after deleting file
                });

                // Click on the whole div
                div.addEventListener('click', () => {
                    const textToInsert = `[${fileData.name}]`;
                    const start = contentArea.selectionStart;
                    const end = contentArea.selectionEnd;
                    
                    contentArea.value = contentArea.value.substring(0, start) + textToInsert + contentArea.value.substring(end);
                    
                    contentArea.focus();
                    contentArea.setSelectionRange(start + textToInsert.length, start + textToInsert.length);
                    saveState(); // Save state
                });
            }


            // --- Requirement 6: Tab Switching ---
            editTab.addEventListener('click', () => {
                showEditTab();
            });

            resultTab.addEventListener('click', () => {
                showResultTab();
            });

            function showEditTab() {
                editTab.classList.add('selected');
                resultTab.classList.remove('selected');
                
                editPanel.style.display = 'block';
                
                // Destroy result panel
                const resultPanel = document.querySelector('.ejwoomresult');
                if (resultPanel) {
                    resultPanel.remove();
                }
            }

            function showResultTab() {
                resultTab.classList.add('selected');
                editTab.classList.remove('selected');
                
                editPanel.style.display = 'none';
                
                buildResultPanel();
            }

            function buildResultPanel() {
                // Destroy existing one if it exists (shouldn't, but safe)
                document.querySelector('.ejwoomresult')?.remove();

                // Create main container
                const resultPanel = document.createElement('div');
                resultPanel.className = 'ejwoomresult';
                resultPanel.style.display = 'block';

                // Create download and preview areas
                const downloadDiv = document.createElement('div');
                downloadDiv.className = 'ejwoomdownload';

                const previewDiv = document.createElement('div');
                previewDiv.className = 'ejwoompreview';

                resultPanel.append(downloadDiv, previewDiv);
                
                // Insert after the tab container
                tabContainer.insertAdjacentElement('afterend', resultPanel);

                // --- Process Content for Preview and Download ---
                const rawContent = contentArea.value;
                const processedContent = processContentForPreview(rawContent);

                // Populate preview
                previewDiv.innerHTML = processedContent;

                // Build and populate download section
                buildDownloadLink(downloadDiv, rawContent, processedContent);
            }
            
            /** Processes content, replacing [filename] tags */
            function processContentForPreview(content) {
                // Regex to find [filename]
                return content.replace(/\[([^\]]+)\]/g, (match, filename) => {
                    if (attachedFiles.has(filename)) {
                        const fileData = attachedFiles.get(filename);
                        
                        if (fileData.fileType === 'img') {
                            // Special handling for SVG to get dimensions
                            if (fileData.type === 'image/svg+xml') {
                                // Decode base64 to string
                                const svgString = atob(fileData.base64.split(',')[1]);
                                let width = 'auto';
                                let height = 'auto';
                                
                                // Try to find width/height attributes
                                const widthMatch = svgString.match(/width="([^"]+)"/);
                                const heightMatch = svgString.match(/height="([^"]+)"/);
                                if (widthMatch) width = widthMatch[1];
                                if (heightMatch) height = heightMatch[1];
                                
                                // If not found, try viewBox
                                if (width === 'auto' || height === 'auto') {
                                    const viewBoxMatch = svgString.match(/viewBox="([\d\s\.]+)"/);
                                    if (viewBoxMatch) {
                                        const parts = viewBoxMatch[1].split(' ');
                                        if (parts.length === 4) {
                                            width = parts[2] + 'px';
                                            height = parts[3] + 'px';
                                        }
                                    }
                                }
                                // Fallback: just use 100% width
                                if (width === 'auto') width = '100%';

                                return `<img src="${fileData.base64}" alt="${filename}" style="width: ${width}; height: ${height}; max-width: 100%;">`;
                            } else {
                                // For raster images (png, jpg), we can't get dimensions
                                // without loading them, which is slow and complex here.
                                // We will just set a max-width.
                                return `<img src="${fileData.base64}" alt="${filename}" style="max-width: 100%; height: auto;">`;
                            }
                        } else {
                            // 'file' type
                            return `<a href="${fileData.base64}" download="${filename}">${filename}</a>`;
                        }
                    }
                    return match; // Return original match if file not found
                });
            }

            /** Builds the downloadable HTML file */
            function buildDownloadLink(container, rawContent, processedContent) {
                const title = titleInput.value || 'Untitled';
                let styleTags = '';
                
                // Regex to find and remove <style> tags from raw content
                const styleRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gmi;
                const contentWithoutStyles = rawContent.replace(styleRegex, (match) => {
                    styleTags += match + '\n'; // Grab the style tag
                    return ''; // Remove it from the body
                });

                // Re-process content *without* styles for the body
                const bodyContent = processContentForPreview(contentWithoutStyles);
                
                // --- Modification for Requirement 7 (ReadFile) ---
                // Embed editor state into the HTML for restoration
                const editorState = {
                    title: titleInput.value,
                    content: contentArea.value,
                    files: Array.from(attachedFiles.values()) // Convert Map to Array for JSON
                };
                
                const stateScript = `<script id="ejwoom-data" type="application/json">${JSON.stringify(editorState)}</script>`;

                const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>${title}</title>
    ${stateScript}
    ${styleTags}
</head>
<body>
    ${bodyContent}
</body>
</html>`;

                const blob = new Blob([htmlContent], { type: 'text/html' });
                const url = URL.createObjectURL(blob);
                const fileSize = blob.size;
                const fileName = `${title}.html`;

                const a = document.createElement('a');
                a.href = url;
                a.download = fileName;
                a.textContent = `Download ${fileName}`;

                const sizeSpan = document.createElement('span');
                sizeSpan.className = 'ejwoomdownloadsize';
                sizeSpan.textContent = `(${fileSize} bytes)`;

                container.innerHTML = ''; // Clear container
                container.append(a, sizeSpan);
            }


            // --- Requirement 7: Control Bar ---

            // Read File
            readFileBtn.addEventListener('click', () => {
                htmlFileInput.click();
            });

            htmlFileInput.addEventListener('change', (e) => {
                const file = e.target.files[0];
                if (!file) return;

                const reader = new FileReader();
                reader.onload = (readEvent) => {
                    try {
                        const htmlText = readEvent.target.result;
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(htmlText, 'text/html');
                        
                        const stateScript = doc.getElementById('ejwoom-data');
                        if (!stateScript) {
                            alert('This HTML file does not contain readable ejwoom data.');
                            return;
                        }

                        const state = JSON.parse(stateScript.textContent);
                        
                        // Convert files array back to Map
                        const filesMap = new Map();
                        state.files.forEach(fileData => filesMap.set(fileData.name, fileData));
                        
                        const newState = {
                            title: state.title,
                            content: state.content,
                            files: filesMap
                        };

                        // Load the state and save it to history
                        loadState(newState);
                        saveState(); // This becomes the new "current" state
                        
                    } catch (err) {
                        console.error('Error reading file:', err);
                        alert('Failed to read or parse the HTML file.');
                    } finally {
                        htmlFileInput.value = ''; // Reset input
                    }
                };
                reader.readAsText(file);
            });


            // LocalStorage
            const STORAGE_KEY = 'ejwoomStorage';

            storageBtn.addEventListener('click', () => {
                try {
                    const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                    
                    const state = {
                        title: titleInput.value,
                        content: contentArea.value,
                        files: Array.from(attachedFiles.values())
                    };
                    
                    const stateString = JSON.stringify(state);
                    const size = new Blob([stateString]).size;
                    
                    const entry = {
                        id: Date.now(),
                        name: titleInput.value || 'Untitled',
                        time: new Date().toISOString(),
                        size: size,
                        state: state
                    };
                    
                    storage.push(entry);
                    localStorage.setItem(STORAGE_KEY, JSON.stringify(storage));
                    alert('Saved to local storage.');
                    checkStorageButton();

                } catch (err) {
                    console.error('Error saving to local storage:', err);
                    alert('Failed to save. Storage might be full.');
                }
            });

            function checkStorageButton() {
                const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                let getBtn = document.querySelector('.ejwoomgetstorage');

                if (storage.length > 0) {
                    if (!getBtn) {
                        getBtn = document.createElement('span');
                        getBtn.className = 'ejwoomgetstorage';
                        getBtn.textContent = 'Get from Storage';
                        getBtn.style.backgroundColor = '#cce5ff'; // Light blue
                        controlBar.appendChild(getBtn);
                        
                        getBtn.addEventListener('click', showStorageModal);
                    }
                } else {
                    getBtn?.remove();
                }
            }
            
            function showStorageModal() {
                populateStorageList();
                storageModal.style.display = 'block';
            }
            
            function hideStorageModal() {
                storageModal.style.display = 'none';
            }
            
            function populateStorageList() {
                const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                storageList.innerHTML = ''; // Clear list
                
                if (storage.length === 0) {
                    storageList.innerHTML = '<li>No items saved.</li>';
                    return;
                }
                
                storage.reverse().forEach(entry => { // Show newest first
                    const li = document.createElement('li');
                    
                    const deleteBtn = document.createElement('button');
                    deleteBtn.className = 'storage-delete';
                    deleteBtn.textContent = '×';
                    deleteBtn.dataset.id = entry.id;
                    deleteBtn.onclick = () => deleteStorageItem(entry.id);
                    
                    const itemDiv = document.createElement('div');
                    itemDiv.className = 'storage-item';
                    itemDiv.dataset.id = entry.id;
                    itemDiv.onclick = () => loadStorageItem(entry.id);
                    
                    const nameSpan = document.createElement('span');
                    nameSpan.className = 'storage-name';
                    nameSpan.textContent = entry.name;
                    
                    const timeSpan = document.createElement('span');
                    timeSpan.className = 'storage-time';
                    // Format yyyy-mm-dd hh:ii:ss
                    timeSpan.textContent = entry.time.replace('T', ' ').substring(0, 19);
                    
                    const sizeSpan = document.createElement('span');
                    sizeSpan.className = 'storage-size';
                    sizeSpan.textContent = `${entry.size} bytes`;
                    
                    itemDiv.append(nameSpan, timeSpan, sizeSpan);
                    li.append(deleteBtn, itemDiv);
                    storageList.appendChild(li);
                });
            }
            
            function deleteStorageItem(id) {
                let storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                storage = storage.filter(item => item.id !== id);
                localStorage.setItem(STORAGE_KEY, JSON.stringify(storage));
                populateStorageList(); // Refresh list
                checkStorageButton(); // Hide "Get" button if empty
            }
            
            function loadStorageItem(id) {
                const storage = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                const entry = storage.find(item => item.id === id);
                
                if (entry) {
                    // Convert files array back to Map
                    const filesMap = new Map();
                    entry.state.files.forEach(fileData => filesMap.set(fileData.name, fileData));
                    
                    const newState = {
                        title: entry.state.title,
                        content: entry.state.content,
                        files: filesMap
                    };

                    loadState(newState);
                    saveState(); // Save this loaded state as the new history point
                    hideStorageModal();
                }
            }

            storageModalClose.addEventListener('click', hideStorageModal);
            
            // Initial check on load
            checkStorageButton();

        });
    </script>

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

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;
        }
        
        /* Storage modal */
        .ejwoommodal {
            position:fixed;inset:0;background:rgba(0,0,0,0.7);
            display:none;align-items:center;justify-content:center;z-index:999;
        }
        .ejwoommodal.active {display:flex;}
        .ejwoommodal-content {
            background:white;width:90%;max-width:700px;max-height:80vh;
            border-radius:8px;overflow:hidden;position:relative;
        }
        .ejwoommodal-header {
            padding:15px 20px;background:#333;color:white;
            display:flex;justify-content:space-between;align-items:center;
        }
        .ejwoomclose {cursor:pointer;font-size:24px;}
        .ejwoomlist {padding:0;max-height:60vh;overflow-y:auto;}
        .ejwoomitem {
            padding:12px 20px;border-bottom:1px solid #eee;
            display:flex;align-items:center;gap:10px;
        }
        .ejwoomitem:hover {background:#f8f9fa;}
        .ejwoomdeleteitem {color:red;cursor:pointer;}
    </style>
</head>
<body>
<div class="ejwoomtab">
    <span class="ejwoomedittab active">Edit</span>
    <span class="ejwoomresulttab">Preview & Download</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 data-tag="h1">H1</span>
        <span data-tag="h2">H2</span>
        <span data-tag="h3">H3</span>
        <span data-tag="h4">H4</span>
        <span data-tag="h5">H5</span>
        <span data-tag="h6">H6</span>
        <span data-tag="bold">B</span>
        <span data-tag="underline">U</span>
        <span data-tag="strikethrough">S</span>
        <span data-tag="italic">i</span>
        <span data-tag="ul">UL</span>
        <span data-tag="ol">OL</span>
        <span data-tag="li">Li</span>
        <span data-tag="dl">DL</span>
        <span data-tag="dt">DT</span>
        <span data-tag="dd">DD</span>
    </div>
    <textarea class="ejwoomcontent">&lt;div style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all;"&gt;
&lt;h1&gt;&lt;/h1&gt;
&lt;/div&gt;</textarea>
    <div id="attachments"></div>
    <button class="ejwoomattach" title="Attach file">+</button>
</div>
<div class="ejwoomresult"></div>
<!-- Hidden file inputs -->
<input type="file" id="attachfile" style="display:none;" multiple accept="*">
<input type="file" id="loadhtml" style="display:none;" accept=".html,.htm">
<!-- Storage Modal -->
<div class="ejwoommodal" id="storagemodal">
    <div class="ejwoommodal-content">
        <div class="ejwoommodal-header">
            <h3>Saved Documents</h3>
            <span class="ejwoomclose">&times;</span>
        </div>
        <div class="ejwoomlist" id="savedlist"></div>
    </div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
    const attachBtn = document.querySelector('.ejwoomattach');
    const attachInput = document.getElementById('attachfile');
    const attachmentsContainer = document.getElementById('attachments');
    const textarea = document.querySelector('.ejwoomcontent');
    // Store currently attached files (to check duplicates and for deletion)
    const attachedFiles = new Map(); // key: file.name → { file, element }
    // Image extensions that should show "img" icon
    const imageExts = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico']);
    // Helper: get file extension (lowercase)
    function getExt(name) {
        const i = name.lastIndexOf('.');
        return i === -1 ? '' : name.slice(i + 1).toLowerCase();
    }
    // Helper: format bytes
    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';
    }
    // Create one attachment row
    function createAttachmentRow(file) {
        const row = document.createElement('div');
        row.className = 'ejwoomattached';
        const typeSpan = document.createElement('span');
        typeSpan.className = 'wmattachtype';
        const ext = getExt(file.name);
        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 = '×';
        // Click on type / name / size → insert [filename] at cursor
        const insertReference = function () {
            const start = textarea.selectionStart;
            const end = textarea.selectionEnd;
            const ref = '[' + file.name + ']';
            textarea.value = textarea.value.slice(0, start) + ref + textarea.value.slice(end);
            textarea.focus();
            textarea.selectionStart = textarea.selectionEnd = start + ref.length;
        };
        typeSpan.addEventListener('click', insertReference);
        nameSpan.addEventListener('click', insertReference);
        sizeSpan.addEventListener('click', insertReference);
        // Delete button
        deleteSpan.addEventListener('click', function () {
            // Remove from DOM
            row.remove();
            // Remove from Map
            attachedFiles.delete(file.name);
            // Remove all occurrences of [filename] from textarea
            const regex = new RegExp('\\[' + file.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\]', 'g');
            textarea.value = textarea.value.replace(regex, '');
        });
        // Append all parts
        row.appendChild(typeSpan);
        row.appendChild(nameSpan);
        row.appendChild(sizeSpan);
        row.appendChild(deleteSpan);
        return row;
    }
    // Click on + button → trigger hidden file input
    attachBtn.addEventListener('click', function () {
        attachInput.click();
    });
    // When files are selected
    attachInput.addEventListener('change', function (e) {
        if (!e.target.files) return;
        for (let file of e.target.files) {
            // 1. Check for duplicate name
            if (attachedFiles.has(file.name)) {
                alert('File "' + file.name + '" is already attached. Duplicate names are not allowed.');
                continue; // skip this file
            }
            // Create visual row
            const row = createAttachmentRow(file);
            // Insert before the + button
            attachmentsContainer.appendChild(row);
            // Store reference
            attachedFiles.set(file.name, { file: file, element: row });
        }
        // Reset input so the same file can be selected again if needed
        attachInput.value = '';
    });
});
</script>
</body>
</html>
grok4.1

代码: 全选

Here are **only the parts you need to modify/add** (all in English, using `createElement`/`appendChild`, with proper escaping of `<` and `>`):

### 1. HTML changes (inside `.ejwoombuttons`)

Replace the existing button list with this updated version (added `data-html-before` / `data-html-after` and the final + button):

```html
<div class="ejwoombuttons">
    <span data-tag="h1" data-html-before="&lt;h1&gt;" data-html-after="&lt;/h1&gt;">H1</span>
    <span data-tag="h2" data-html-before="&lt;h2&gt;" data-html-after="&lt;/h2&gt;">H2</span>
    <span data-tag="h3" data-html-before="&lt;h3&gt;" data-html-after="&lt;/h3&gt;">H3</span>
    <span data-tag="h4" data-html-before="&lt;h4&gt;" data-html-after="&lt;/h4&gt;">H4</span>
    <span data-tag="h5" data-html-before="&lt;h5&gt;" data-html-after="&lt;/h5&gt;">H5</span>
    <span data-tag="h6" data-html-before="&lt;h6&gt;" data-html-after="&lt;/h6&gt;">H6</span>
    <span data-tag="bold" data-html-before="&lt;strong&gt;" data-html-after="&lt;/strong&gt;">B</span>
    <span data-tag="underline" data-html-before="&lt;u&gt;" data-html-after="&lt;/u&gt;">U</span>
    <span data-tag="strikethrough" data-html-before="&lt;s&gt;" data-html-after="&lt;/s&gt;">S</span>
    <span data-tag="italic" data-html-before="&lt;em&gt;" data-html-after="&lt;/em&gt;">i</span>
    <span data-tag="ul" data-html-before="&lt;ul&gt;&lt;li&gt;" data-html-after="&lt;/li&gt;&lt;/ul&gt;">UL</span>
    <span data-tag="ol" data-html-before="&lt;ol&gt;&lt;li&gt;" data-html-after="&lt;/li&gt;&lt;/ol&gt;">OL</span>
    <span data-tag="li" data-html-before="&lt;li&gt;" data-html-after="&lt;/li&gt;">Li</span>
    <span data-tag="dl" data-html-before="&lt;dl&gt;&lt;dt&gt;" data-html-after="&lt;/dt&gt;&lt;dd&gt;&lt;/dd&gt;&lt;/dl&gt;">DL</span>
    <span data-tag="dt" data-html-before="&lt;dt&gt;" data-html-after="&lt;/dt&gt;">DT</span>
    <span data-tag="dd" data-html-before="&lt;dd&gt;" data-html-after="&lt;/dd&gt;">DD</span>
    <!-- Custom buttons will be inserted here -->
    <span class="ejwoomcustombutton"></span>
    <span class="ejwoomaddbutton" title="Add custom tag">+</span>
</div>
```

### 2. New modal HTML (add anywhere after the existing modal, e.g. before closing </body>)

```html
<!-- Custom tag creation window -->
<div class="ejwoommodal" id="customtagwindow">
    <div class="ejwoommodal-content">
        <div class="ejwoommodal-header">
            <h3>Add Custom Tag</h3>
            <span class="ejwoomclose" data-target="customtagwindow">×</span>
        </div>
        <div style="padding:20px;">
            <input type="text" id="customname" placeholder="Button text (e.g. Quote)" style="width:100%;padding:10px;margin-bottom:10px;box-sizing:border-box;">
            <input type="text" id="customtag" placeholder="Tag name (for reference)" style="width:100%;padding:10px;margin-bottom:10px;box-sizing:border-box;">
            <input type="text" id="custombefore" placeholder="HTML before (e.g. &lt;blockquote&gt;)" style="width:100%;padding:10px;margin-bottom:10px;box-sizing:border-box;">
            <input type="text" id="customafter" placeholder="HTML after (e.g. &lt;/blockquote&gt;)" style="width:100%;padding:10px;margin-bottom:15px;box-sizing:border-box;">
            <button id="customconfirm" style="padding:10px 20px;background:#007bff;color:white;border:none;border-radius:4px;cursor:pointer;">Confirm</button>
            <button id="customcancel" style="padding:10px 20px;background:#6c757d;color:white;border:none;border-radius:4px;cursor:pointer;margin-left:10px;">Cancel</button>
        </div>
    </div>
</div>
```

### 3. JavaScript to add (place inside the existing `<script>` tag, after all current code)

```javascript
// Reusable modal open/close function
function toggleModal(id, show) {
    const modal = document.getElementById(id);
    if (modal) modal.classList.toggle('active', show);
}

// Insert HTML at cursor or around selection
function insertHtmlAtCursor(before, after) {
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const selected = textarea.value.slice(start, end);
    const insertText = before + selected + after;
    
    textarea.value = textarea.value.slice(0, start) + insertText + textarea.value.slice(end);
    textarea.focus();
    textarea.selectionStart = start + before.length;
    textarea.selectionEnd = start + before.length + selected.length;
}

// Button click handler for predefined and custom tags
function handleTagButtonClick(e) {
    const btn = e.target;
    const before = btn.dataset.htmlBefore || '';
    const after = btn.dataset.htmlAfter || '';
    if (before || after) {
        insertHtmlAtCursor(before, after);
    }
}

// Add custom tag button
function addCustomTagButton(name, tagName, before, after) {
    const container = document.querySelector('.ejwoombuttons');
    const marker = document.querySelector('.ejwoomcustombutton');
    
    const btn = document.createElement('span');
    btn.textContent = name;
    btn.dataset.tag = tagName;
    btn.dataset.htmlBefore = before;
    btn.dataset.htmlAfter = after;
    btn.style.cssText = 'padding:3px 7px;background:#007bff;color:white;border-radius:4px;cursor:pointer;font-size:14px;margin-right:4px;';
    btn.addEventListener('click', handleTagButtonClick);
    
    container.insertBefore(btn, marker);
}

// Event delegation for all tag buttons (existing + future custom ones)
document.querySelector('.ejwoombuttons').addEventListener('click', function(e) {
    if (e.target.matches('span[data-html-before]')) {
        handleTagButtonClick(e);
    }
    if (e.target.classList.contains('ejwoomaddbutton')) {
        toggleModal('customtagwindow', true);
    }
});

// Confirm custom tag
document.getElementById('customconfirm').addEventListener('click', function() {
    const name = document.getElementById('customname').value.trim();
    const tag = document.getElementById('customtag').value.trim();
    const before = document.getElementById('custombefore').value.trim();
    const after = document.getElementById('customafter').value.trim();
    
    if (!name || !before || !after) {
        alert('Button text, before HTML and after HTML are required.');
        return;
    }
    
    addCustomTagButton(name, tag || name.toLowerCase(), before, after);
    
    // Clear and close
    document.getElementById('customname').value = '';
    document.getElementById('customtag').value = '';
    document.getElementById('custombefore').value = '';
    document.getElementById('customafter').value = '';
    toggleModal('customtagwindow', false);
});

// Cancel or close button
document.getElementById('customcancel').addEventListener('click', () => toggleModal('customtagwindow', false));
document.querySelector('#customtagwindow .ejwoomclose').addEventListener('click', () => toggleModal('customtagwindow', false));

// Close modal when clicking outside
document.getElementById('customtagwindow').addEventListener('click', function(e) {
    if (e.target === this) toggleModal('customtagwindow', false);
});
```

These are **all the modifications** required. After adding them, users will be able to:
- Click any button (including newly created ones) → insert corresponding HTML around selection.
- Click the “+” button → open a dialog → create a new reusable custom tag button.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4100
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 163 次
被圈友点赞: 193 次
联系:

Re: 來做一個html生成器

帖子 ejsoon »

為button增加功能,並可以新增、修改和刪除自定義button。

代码: 全选

增加一段js,同時也要更改相應的html和css:

一,在ejwoombuttons內的span增加data-html-before和data-html-after。

before都是<tag>,after都是</tag>。

把「<」和「>」都轉成&lt;和&gt;以使其能寫入dadaset中。

二,當ejwoombuttons內的span被點擊時,textarea class="ejwoomcontent"的光標處或所選中的文字前方增加data-html-before的內容,後面增加data-html-after的內容。

三,在ejwoombuttons內的最後新增一個span.ejwoomaddbutton{+}。

3.1,當點擊它時,將彈出一個對話框div.ejwoomwindow。

3.2,div.ejwoomwindow的第一行是「new、name1、name2、name3…」,它們是單選且必選,默認選中new。

3.3,第二行到第五行是需要填的四個值:name、tag-name、before html、after html。每行都是label+input。如果第一行選的是new,則input清空。如果第一行選中了已加入的,則填上之前的值。

3.4,第六行是button.wmconfirm{Confirm},button.wmdelete{Delete},button.wmcancel{Cancel}。當第一行選中是new時,button.wmdelete為半透明並disable,選中其它時則恢復。

3.5,當button.wmdelete被點擊時,被選中的ejwoombutton將被刪除,之後new被選中,button.wmdelete再又變為半透明並disable。

3.6,當button.wmconfirm{Confirm}被點擊時,窗口關閉,新的span將出現在span.ejwoomcustombutton的前面,其class="ejwoomcustombutton"。

3.7,當button.wmcancel{Cancel}被點擊時,窗口關閉,不作任何更改。

3.8,div.ejwoomwindow在js中要打包成一個獨立的方法,以使之能夠復用。

四,所有的元素增加都要用createElement和appendChild方法。

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

Re: 來做一個html生成器

帖子 ejsoon »

grok4.1對上面的回答:

代码: 全选

Here are **only the parts you need to add / modify** (all in English, using `createElement` + `appendChild`, with proper `<` → `&lt;` and `>` → `&gt;` escaping).

```html
<!-- 1. Add this HTML right after the existing .ejwoombuttons content (before its closing </div>) -->
<span class="ejwoomaddbutton" title="Add custom tag">+</span>

<!-- Add the custom button placeholder (still inside .ejwoombuttons, after the + button) -->
<span class="ejwoomcustombutton" style="display:none;"></span>

<!-- 2. Add the dialog window (place it anywhere after </body> or inside <body>) -->
<div class="ejwoomwindow" id="ejwoomwindow" style="display:none;">
    <div class="ejwoomwindow-content">
        <div class="ejwoomwindow-header">Custom HTML Tag</div>
        
        <!-- Radio group -->
        <div class="ejwoomradio-line">
            <label><input type="radio" name="ejwoompreset" value="new" checked> new</label>
        </div>
        
        <!-- Inputs -->
        <div class="ejwoominput-line">
            <label>Name (button text):</label>
            <input type="text" class="ejwoom-name">
        </div>
        <div class="ejwoominput-line">
            <label>Tag name:</label>
            <input type="text" class="ejwoom-tag">
        </div>
        <div class="ejwoominput-line">
            <label>Before HTML:</label>
            <input type="text" class="ejwoom-before">
        </div>
        <div class="ejwoominput-line">
            <label>After HTML:</label>
            <input type="text" class="ejwoom-after">
        </div>
        
        <!-- Buttons -->
        <div class="ejwoomwindow-buttons">
            <button class="wmconfirm">Confirm</button>
            <button class="wmdelete" disabled style="opacity:0.5;">Delete</button>
            <button class="wmcancel">Cancel</button>
        </div>
    </div>
</div>
```

```css
/* 3. Add to <style> */
.ejwoomaddbutton {
    padding:3px 7px;background:#28a745;color:white;border-radius:4px;
    cursor:pointer;font-size:14px;font-weight:bold;
}
.ejwoomaddbutton:hover {background:#218838;}

.ejwoomcustombutton span {
    padding:3px 7px;background:#6c757d;color:white;border-radius:4px;
    cursor:pointer;font-size:14px;margin-right:4px;
}
.ejwoomcustombutton span:hover {background:#5a6268;}

/* Dialog styles */
.ejwoomwindow {
    position:fixed;inset:0;background:rgba(0,0,0,0.7);
    display:flex;align-items:center;justify-content:center;z-index:1000;
}
.ejwoomwindow-content {
    background:white;width:90%;max-width:460px;border-radius:8px;padding:20px;
    box-shadow:0 4px 20px rgba(0,0,0,0.3);
}
.ejwoomwindow-header {
    font-size:18px;font-weight:bold;margin-bottom:15px;color:#333;
}
.ejwoomradio-line, .ejwoominput-line {
    margin:10px 0;display:flex;align-items:center;gap:10px;
}
.ejwoominput-line label {width:130px;flex-shrink:0;}
.ejwoominput-line input {flex:1;padding:6px;border:1px solid #ccc;border-radius:4px;}
.ejwoomwindow-buttons {
    margin-top:20px;display:flex;gap:10px;justify-content:flex-end;
}
.ejwoomwindow-buttons button {
    padding:8px 16px;border:none;border-radius:4px;cursor:pointer;
}
.wmconfirm {background:#007bff;color:white;}
.wmconfirm:hover {background:#0056b3;}
.wmdelete {background:#dc3545;color:white;}
.wmdelete:hover {background:#c82333;}
.wmcancel {background:#6c757d;color:white;}
.wmcancel:hover {background:#5a6268;}
```

```javascript
/* 4. Add this JavaScript (place it inside the existing <script> after all current code) */

// ---------- Custom Button Manager ----------
const customContainer = document.querySelector('.ejwoomcustombutton');
const textarea = document.querySelector('.ejwoomcontent');
const windowEl = document.getElementById('ejwoomwindow');

// Storage for custom buttons (name → data)
const customButtons = new Map(); // key: displayName

// Escape < and > for dataset
function esc(str) {
    return str.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

// Create a button span from data
function createButtonSpan(data) {
    const span = document.createElement('span');
    span.textContent = data.display;
    span.dataset.tag = data.tag || '';
    span.dataset.htmlBefore = esc(data.before);
    span.dataset.htmlAfter = esc(data.after);
    span.className = 'ejwoomcustombutton-item';
    span.style.cssText = 'padding:3px 7px;background:#6c757d;color:white;border-radius:4px;cursor:pointer;font-size:14px;margin-right:4px;';
    span.title = `Insert <${data.tag}>…</${data.tag}>`;
    return span;
}

// Insert HTML at cursor/selection
function insertAtCursor(before, after) {
    const start = textarea.selectionStart;
    const end = textarea.selectionEnd;
    const selected = textarea.value.slice(start, end);
    const insertText = before + selected + after;
    textarea.value = textarea.value.slice(0, start) + insertText + textarea.value.slice(end);
    textarea.focus();
    textarea.selectionStart = start + before.length;
    textarea.selectionEnd = start + before.length + selected.length;
}

// Open the custom tag window (reusable)
function openCustomWindow(editName = null) {
    const radiosContainer = windowEl.querySelector('.ejwoomradio-line');
    const nameInput = windowEl.querySelector('.ejwoom-name');
    const tagInput = windowEl.querySelector('.ejwoom-tag');
    const beforeInput = windowEl.querySelector('.ejwoom-before');
    const afterInput = windowEl.querySelector('.ejwoom-after');
    const deleteBtn = windowEl.querySelector('.wmdelete');

    // Clear previous radios except "new"
    radiosContainer.querySelectorAll('label:not(:first-child)').forEach(el => el.remove());

    // Rebuild radio list
    const newLabel = radiosContainer.querySelector('label');
    newLabel.querySelector('input').checked = !editName;

    customButtons.forEach((data, name) => {
        const label = document.createElement('label');
        const radio = document.createElement('input');
        radio.type = 'radio';
        radio.name = 'ejwoompreset';
        radio.value = name;
        if (name === editName) radio.checked = true;
        label.appendChild(radio);
        label.appendChild(document.createTextNode(' ' + name));
        radiosContainer.appendChild(label);
    });

    // Fill inputs
    if (editName && customButtons.has(editName)) {
        const d = customButtons.get(editName);
        nameInput.value = d.display;
        tagInput.value = d.tag;
        beforeInput.value = d.before;
        afterInput.value = d.after;
        deleteBtn.disabled = false;
        deleteBtn.style.opacity = '1';
    } else {
        nameInput.value = '';
        tagInput.value = '';
        beforeInput.value = '';
        afterInput.value = '';
        deleteBtn.disabled = true;
        deleteBtn.style.opacity = '0.5';
    }

    windowEl.style.display = 'flex';
}

// Close window
function closeCustomWindow() {
    windowEl.style.display = 'none';
}

// ---- Event: existing fixed buttons (add data attributes) ----
document.querySelectorAll('.ejwoombuttons > span[data-tag]').forEach(span => {
    const tag = span.dataset.tag;
    if (!tag) return;
    const before = '&lt;' + tag + '&gt;';
    const after = '&lt;/' + tag + '&gt;';
    span.dataset.htmlBefore = before;
    span.dataset.htmlAfter = after;
});

// ---- Click handler for all buttons (fixed + custom) ----
document.querySelector('.ejwoombuttons').addEventListener('click', function (e) {
    const btn = e.target.closest('span[data-html-before]');
    if (!btn) return;
    const before = btn.dataset.htmlBefore.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
    const after = btn.dataset.htmlAfter.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
    insertAtCursor(before, after);
});

// ---- Add custom button (+) ----
document.querySelector('.ejwoomaddbutton').addEventListener('click', () => openCustomWindow());

// ---- Dialog Confirm ----
windowEl.querySelector('.wmconfirm').addEventListener('click', () => {
    const selectedRadio = windowEl.querySelector('input[name="ejwoompreset"]:checked').value;
    const display = windowEl.querySelector('.ejwoom-name').value.trim();
    const tag = windowEl.querySelector('.ejwoom-tag').value.trim();
    const before = windowEl.querySelector('.ejwoom-before').value;
    const after = windowEl.querySelector('.ejwoom-after').value;

    if (!display || !tag) {
        alert('Name and Tag are required.');
        return;
    }

    const data = { display, tag, before, after };
    const span = createButtonSpan(data);

    if (selectedRadio === 'new') {
        // Add new
        customButtons.set(display, data);
        customContainer.parentNode.insertBefore(span, customContainer);
    } else {
        // Update existing
        const oldSpan = customContainer.parentNode.querySelector(`span[title*="Insert <${customButtons.get(selectedRadio).tag}>"]`);
        if (oldSpan) oldSpan.replaceWith(span);
        customButtons.set(display, data);
    }

    closeCustomWindow();
});

// ---- Dialog Delete ----
windowEl.querySelector('.wmdelete').addEventListener('click', () => {
    const selected = windowEl.querySelector('input[name="ejwoompreset"]:checked').value;
    if (selected === 'new') return;

    if (confirm(`Delete custom button "${selected}"?`)) {
        const oldSpan = customContainer.parentNode.querySelector(`span[title*="Insert <${customButtons.get(selected).tag}>"]`);
        if (oldSpan) oldSpan.remove();
        customButtons.delete(selected);
        openCustomWindow(); // refresh with "new" selected
    }
});

// ---- Dialog Cancel ----
windowEl.querySelector('.wmcancel').addEventListener('click', closeCustomWindow);

// ---- Radio change → fill fields or clear ----
windowEl.addEventListener('change', e => {
    if (e.target.name !== 'ejwoompreset') return;
    const val = e.target.value;
    openCustomWindow(val === 'new' ? null : val);
});
```

These additions fully implement all requested features using only `createElement`/`appendChild`, proper HTML escaping, and a reusable dialog. Existing buttons (H1, B, etc.) will also work with the new insertion logic.
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 1 访客