by-onlineeditor/demo.html
hanshiyang 27c85de1c4 feat(demo): 添加书签功能支持保存和跳转页面
添加书签侧边栏组件,支持以下功能:
1. 自动生成书签名称并保存当前可见页码
2. 书签列表展示、编辑和删除功能
3. 点击书签跳转到指定页面
4. 使用localStorage持久化存储书签数据
2025-11-28 16:53:44 +08:00

241 lines
9.8 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canvas Editor Iframe Demo</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 0; padding: 16px; }
.panel { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.panel input[type="text"] { width: 420px; padding: 6px 8px; font-size: 12px; }
.panel button { padding: 6px 12px; font-size: 12px; cursor: pointer; }
.layout { display: flex; gap: 12px; align-items: flex-start; }
.sidebar { width: 280px; border: 1px solid #e2e6ed; border-radius: 8px; padding: 12px; background: #fafafa; }
.sidebar h4 { margin: 0 0 8px; font-weight: 600; font-size: 14px; }
.bookmark-form { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.bookmark-form input { flex: 1; min-width: 0; padding: 6px 8px; font-size: 12px; }
.bookmark-form input[type="number"] { width: 80px; }
.bookmark-form button { padding: 6px 12px; font-size: 12px; cursor: pointer; }
.bookmark-list { list-style: none; padding: 0; margin: 0; }
.bookmark-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #eee; }
.bookmark-item span { font-size: 12px; color: #111827; }
.bookmark-actions { display: flex; gap: 6px; }
.bookmark-actions button { padding: 4px 8px; font-size: 12px; cursor: pointer; }
.editorWrap { flex: 1; }
iframe { width: 100%; height: 80vh; border: 1px solid #e2e6ed; }
small { color: #6b7280; }
</style>
</head>
<body>
<h3>父页面通过地址拼接加载子页面filePath + mode=edit</h3>
<div class="panel">
<input id="urlInput" type="text" placeholder="输入同源文件URL" />
<label style="display:flex;align-items:center;gap:6px;">
<input id="editModeToggle" type="checkbox" />
<span>编辑模式(勾选则拼接 mode=edit不勾选为只读</span>
</label>
<button id="loadBtn">设置 iframe 地址</button>
<small>示例vocd.html?filePath=URL&mode=edit 或 vocd.html?filePath=URL</small>
</div>
<div class="layout">
<aside class="sidebar">
<h4>书签</h4>
<div class="bookmark-form">
<button id="addBookmarkBtn">添加书签</button>
<small>自动命名为书签N+1页码取当前可见页</small>
</div>
<ul id="bookmarkList" class="bookmark-list"></ul>
</aside>
<div class="editorWrap">
<iframe id="editorFrame" src="./vocd.html" referrerpolicy="no-referrer"></iframe>
</div>
</div>
<script>
const editorFrame = document.getElementById('editorFrame')
const loadBtn = document.getElementById('loadBtn')
const urlInput = document.getElementById('urlInput')
const editModeToggle = document.getElementById('editModeToggle')
const addBookmarkBtn = document.getElementById('addBookmarkBtn')
const bookmarkList = document.getElementById('bookmarkList')
function buildSrc(filePath, isEdit) {
const base = './vocd.html'
return `${base}?filePath=${encodeURIComponent(filePath)}` + (isEdit ? '&mode=edit' : '')
}
// 页面加载后,自动将 iframe 指向 vocd.html 并使用同源 /test.ocd
document.addEventListener('DOMContentLoaded', () => {
const defaultUrl = '/test.ocd' // 来自 public同源可直接访问
urlInput.value = defaultUrl
const base = './vocd.html'
// 默认不勾选:只读模式(不拼接 mode
editModeToggle.checked = false
const src = buildSrc(defaultUrl, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 自动设置 iframe src:', src)
renderBookmarks()
})
// 切换编辑模式时,刷新 iframe 地址,保留当前 filePath
editModeToggle.addEventListener('change', () => {
const typedUrl = urlInput.value.trim()
const currentAbs = editorFrame.src || './vocd.html'
const currentUrl = new URL(currentAbs, window.location.href)
const currentFilePath = currentUrl.searchParams.get('filePath') || (typedUrl || '/test.ocd')
const src = buildSrc(currentFilePath, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 编辑模式切换,刷新 iframe src:', src)
renderBookmarks()
})
loadBtn.onclick = () => {
const typedUrl = urlInput.value.trim()
const base = './vocd.html'
const targetUrl = typedUrl || '/test.ocd'
const src = buildSrc(targetUrl, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 设置 iframe src:', src)
renderBookmarks()
}
function getCurrentFilePath() {
const currentAbs = editorFrame.src || './vocd.html'
const currentUrl = new URL(currentAbs, window.location.href)
return currentUrl.searchParams.get('filePath') || '/test.ocd'
}
function getBookmarkKey() {
const fp = getCurrentFilePath()
return `bookmarks:${fp}`
}
function readBookmarks() {
try {
const raw = localStorage.getItem(getBookmarkKey())
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
function writeBookmarks(list) {
localStorage.setItem(getBookmarkKey(), JSON.stringify(list))
}
function getNextBookmarkName() {
const list = readBookmarks()
let maxN = 0
list.forEach(b => {
const m = /^书签(\d+)$/.exec(String(b.name || ''))
if (m) {
const n = parseInt(m[1], 10)
if (Number.isFinite(n)) maxN = Math.max(maxN, n)
}
})
return `书签${maxN + 1}`
}
function renderBookmarks() {
const list = readBookmarks()
bookmarkList.innerHTML = ''
list.forEach(item => {
const li = document.createElement('li')
li.className = 'bookmark-item'
const label = document.createElement('span')
label.textContent = `${item.name}(第${item.page}页)`
const actions = document.createElement('div')
actions.className = 'bookmark-actions'
const jumpBtn = document.createElement('button')
jumpBtn.textContent = '跳转'
jumpBtn.onclick = () => navigateToPage(item.page)
const editBtn = document.createElement('button')
editBtn.textContent = '编辑'
editBtn.onclick = () => {
const newName = window.prompt('书签名称', item.name) || item.name
const newPageStr = window.prompt('页码(正整数)', String(item.page)) || String(item.page)
const newPage = Math.max(1, parseInt(newPageStr, 10) || item.page)
const list2 = readBookmarks().map(b => (b.id === item.id ? { ...b, name: newName, page: newPage } : b))
writeBookmarks(list2)
renderBookmarks()
}
const delBtn = document.createElement('button')
delBtn.textContent = '删除'
delBtn.onclick = () => {
const list2 = readBookmarks().filter(b => b.id !== item.id)
writeBookmarks(list2)
renderBookmarks()
}
actions.appendChild(jumpBtn)
actions.appendChild(editBtn)
actions.appendChild(delBtn)
li.appendChild(label)
li.appendChild(actions)
bookmarkList.appendChild(li)
})
}
function addBookmark(name, page) {
const list = readBookmarks()
const id = Date.now().toString(36) + Math.random().toString(36).slice(2)
const item = { id, name, page }
writeBookmarks([...list, item])
renderBookmarks()
}
addBookmarkBtn.onclick = () => {
const name = getNextBookmarkName()
const page = getFirstVisiblePageNo()
addBookmark(name, page)
}
function getFirstVisiblePageNo() {
const doc = editorFrame.contentDocument || editorFrame.contentWindow?.document
if (!doc) return 1
const iframeRect = editorFrame.getBoundingClientRect()
const canvases = Array.from(doc.querySelectorAll('canvas[data-index]'))
if (!canvases.length) return 1
const visible = canvases
.map(c => {
const rect = c.getBoundingClientRect()
const overlapY = Math.min(rect.bottom, iframeRect.bottom) - Math.max(rect.top, iframeRect.top)
return { c, rect, overlapY }
})
.filter(x => x.overlapY > 0)
.sort((a, b) => a.rect.top - b.rect.top)
let chosen = visible.length ? visible[0].c : null
if (!chosen) {
chosen = canvases.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
}
const idxStr = chosen?.getAttribute('data-index') || '0'
const idx = parseInt(idxStr, 10)
return Number.isFinite(idx) ? idx + 1 : 1
}
function navigateToPage(page) {
const doc = editorFrame.contentDocument || editorFrame.contentWindow?.document
if (!doc) return
const target0 = doc.querySelector(`canvas[data-index="${page - 1}"]`)
const target1 = doc.querySelector(`canvas[data-index="${page}"]`)
const canvas = target0 || target1
if (!canvas) {
alert(`未找到第${page}`)
return
}
try {
canvas.scrollIntoView({ behavior: 'smooth', block: 'center' })
const evt = new MouseEvent('mousedown', { bubbles: true })
canvas.dispatchEvent(evt)
} catch (e) {
console.warn('跳转页失败', e)
}
}
editorFrame.addEventListener('load', () => {
renderBookmarks()
})
</script>
</body>
</html>