TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| 第152行: | 第152行: | ||
} | } | ||
.exporting .tier-dropzone.pool { | |||
border-color: transparent; | |||
background: transparent; | |||
} | } | ||
</style> | </style> | ||
| 第165行: | 第165行: | ||
} | } | ||
function contrastColor(hex) { | function contrastColor(hex) { | ||
hex = hex.replace('#',''); | hex = (hex || '').replace('#',''); | ||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); | if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); | ||
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return '#fff'; | |||
var r = parseInt(hex.substr(0,2), 16); | var r = parseInt(hex.substr(0,2), 16); | ||
var g = parseInt(hex.substr(2,2), 16); | var g = parseInt(hex.substr(2,2), 16); | ||
| 第184行: | 第185行: | ||
} | } | ||
}, { offset: Number.NEGATIVE_INFINITY, element: null }).element; | }, { offset: Number.NEGATIVE_INFINITY, element: null }).element; | ||
} | |||
function ensureCrossOrigin(img) { | |||
try { | |||
if (!img) return; | |||
if (!img.crossOrigin) img.crossOrigin = 'anonymous'; | |||
if (!img.referrerPolicy) img.referrerPolicy = 'no-referrer'; | |||
} catch(e) {} | |||
} | } | ||
function makeDraggable(avatar) { | function makeDraggable(avatar) { | ||
if (!avatar || avatar._draggableInit) return; | |||
avatar._draggableInit = true; | |||
avatar.setAttribute('draggable', 'true'); | avatar.setAttribute('draggable', 'true'); | ||
avatar.classList.add('draggable-avatar'); | avatar.classList.add('draggable-avatar'); | ||
avatar.querySelectorAll('img').forEach(ensureCrossOrigin); | |||
avatar.addEventListener('dragstart', function(e) { | avatar.addEventListener('dragstart', function(e) { | ||
avatar.classList.add('dragging'); | avatar.classList.add('dragging'); | ||
avatar._prevParent = avatar.parentNode; | |||
e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || ''); | e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || ''); | ||
e.dataTransfer.effectAllowed = 'move'; | e.dataTransfer.effectAllowed = 'move'; | ||
| 第195行: | 第208行: | ||
avatar.addEventListener('dragend', function() { | avatar.addEventListener('dragend', function() { | ||
avatar.classList.remove('dragging'); | avatar.classList.remove('dragging'); | ||
cleanupEmptyWrapper(avatar._prevParent); | |||
avatar._prevParent = null; | |||
}); | }); | ||
} | } | ||
function initAvatars(root) { | function initAvatars(root) { | ||
if (!root) return; | |||
const avatars = root.querySelectorAll('.avatar-frame'); | const avatars = root.querySelectorAll('.avatar-frame'); | ||
avatars.forEach(function(av) { | avatars.forEach(function(av) { | ||
| 第205行: | 第221行: | ||
} | } | ||
makeDraggable(av); | makeDraggable(av); | ||
}); | |||
} | |||
function cleanupEmptyWrapper(node) { | |||
if (!node || node.nodeType !== 1) return; | |||
if (node.classList && node.classList.contains('tier-dropzone')) return; | |||
const hasElementChild = node.querySelector('.avatar-frame'); | |||
const onlyWhitespace = !node.textContent || node.textContent.trim().length === 0; | |||
if (!hasElementChild && node.childElementCount === 0 && onlyWhitespace) { | |||
node.parentNode && node.parentNode.removeChild(node); | |||
} | |||
} | |||
function cleanupEmptyPlaceholdersIn(container) { | |||
if (!container) return; | |||
Array.from(container.children).forEach(function(child) { | |||
if (child.nodeType !== 1) return; | |||
if (child.classList.contains('avatar-frame')) return; | |||
const hasAvatarInside = !!child.querySelector('.avatar-frame'); | |||
const onlyWhitespace = !child.textContent || child.textContent.trim().length === 0; | |||
if (!hasAvatarInside && child.childElementCount === 0 && onlyWhitespace) { | |||
child.remove(); | |||
} | |||
}); | }); | ||
} | } | ||
function initDropzone(zone) { | function initDropzone(zone) { | ||
if (!zone || zone._dropzoneInit) return; | |||
zone._dropzoneInit = true; | |||
zone.addEventListener('dragover', function(e) { | zone.addEventListener('dragover', function(e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
e.dataTransfer.dropEffect = 'move'; | e.dataTransfer.dropEffect = 'move'; | ||
const afterElement = getDragAfterElement(zone, e.clientY); | |||
const dragging = document.querySelector('.avatar-frame.dragging'); | const dragging = document.querySelector('.avatar-frame.dragging'); | ||
if (!dragging) return; | if (!dragging) return; | ||
if (afterElement == null) { | |||
if (dragging.parentNode !== zone) zone.appendChild(dragging); | |||
} else { | |||
if (afterElement !== dragging) zone.insertBefore(dragging, afterElement); | |||
} | } | ||
}); | }); | ||
zone.addEventListener('drop', function(e) { | zone.addEventListener('drop', function(e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
e.stopPropagation(); | |||
const dragging = document.querySelector('.avatar-frame.dragging'); | const dragging = document.querySelector('.avatar-frame.dragging'); | ||
if (dragging) { | if (dragging) { | ||
if (dragging.parentNode !== zone) zone.appendChild(dragging); | |||
if (!zone. | cleanupEmptyWrapper(dragging._prevParent); | ||
dragging._prevParent = null; | |||
} | |||
if (zone.classList.contains('pool')) { | |||
cleanupEmptyPlaceholdersIn(zone); | |||
} | } | ||
}); | }); | ||
| 第265行: | 第290行: | ||
const color = document.createElement('div'); | const color = document.createElement('div'); | ||
color.className = 'color-toggle'; | color.className = 'color-toggle'; | ||
color.title = '更改颜色'; | |||
const del = document.createElement('div'); | const del = document.createElement('div'); | ||
del.className = 'delete-row'; | del.className = 'delete-row'; | ||
del.textContent = '删除'; | del.textContent = '删除'; | ||
del.title = '删除该行'; | |||
t.appendChild(color); t.appendChild(del); | t.appendChild(color); t.appendChild(del); | ||
return t; | return t; | ||
| 第273行: | 第300行: | ||
th.appendChild(label); | th.appendChild(label); | ||
th.appendChild(toolsWrap); | th.appendChild(toolsWrap); | ||
const initialBg = th.getAttribute('data-initial-bg') || ' | const initialBg = th.getAttribute('data-initial-bg') || '#555'; | ||
th.style.background = initialBg; | th.style.background = initialBg; | ||
th.style.color = | th.style.color = contrastColor(initialBg); | ||
attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | ||
const delBtn = toolsWrap.querySelector('.delete-row'); | const delBtn = toolsWrap.querySelector('.delete-row'); | ||
| 第283行: | 第309行: | ||
const dropzone = tr.querySelector('.tier-dropzone'); | const dropzone = tr.querySelector('.tier-dropzone'); | ||
const pool = document.getElementById('character-pool'); | const pool = document.getElementById('character-pool'); | ||
Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) { | Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) { | ||
pool.appendChild(av); | pool.appendChild(av); | ||
| 第291行: | 第316行: | ||
} | } | ||
function attachColorPalette(toggle, th) { | function attachColorPalette(toggle, th) { | ||
if (!toggle) return; | |||
const palette = document.createElement('div'); | const palette = document.createElement('div'); | ||
palette.className = 'color-palette'; | palette.className = 'color-palette'; | ||
| 第302行: | 第328行: | ||
sw.className = 'color-swatch'; | sw.className = 'color-swatch'; | ||
sw.style.background = c; | sw.style.background = c; | ||
sw.addEventListener('click', function( | sw.addEventListener('click', function() { | ||
th.style.background = c; | th.style.background = c; | ||
th.style.color = contrastColor(c); | th.style.color = contrastColor(c); | ||
| 第336行: | 第362行: | ||
} | } | ||
function addNewRow() { | function addNewRow() { | ||
const tbody = document.querySelector('#tierlist-table tbody'); | const tbody = document.querySelector('#tierlist-table tbody') || document.querySelector('#tierlist-table'); | ||
const tr = document.createElement('tr'); | const tr = document.createElement('tr'); | ||
tr.className = 'tier-row'; | tr.className = 'tier-row'; | ||
| 第342行: | 第368行: | ||
th.className = 'tier-head'; | th.className = 'tier-head'; | ||
th.setAttribute('data-initial-bg', '#8888ff'); | th.setAttribute('data-initial-bg', '#8888ff'); | ||
th.textContent = '新行'; | th.textContent = '新行'; | ||
const td = document.createElement('td'); | const td = document.createElement('td'); | ||
| 第356行: | 第381行: | ||
buildEditableHead(th); | buildEditableHead(th); | ||
} | } | ||
function ensureHtml2Canvas(cb) { | function ensureHtml2Canvas(cb) { | ||
if (window.html2canvas) { cb(); return; } | if (window.html2canvas) { cb(); return; } | ||
| 第379行: | 第391行: | ||
document.body.appendChild(s); | document.body.appendChild(s); | ||
} | } | ||
function downloadCanvas(canvas) { | |||
function done(blobOrUrl) { | |||
try { | |||
const link = document.createElement('a'); | |||
const isBlob = blobOrUrl instanceof Blob; | |||
const url = isBlob ? (URL.createObjectURL(blobOrUrl)) : blobOrUrl; | |||
link.href = url; | |||
link.download = 'tierlist.png'; | |||
document.body.appendChild(link); | |||
link.click(); | |||
document.body.removeChild(link); | |||
if (isBlob) setTimeout(() => URL.revokeObjectURL(url), 0); | |||
} catch (e) { | |||
const dataUrl = canvas.toDataURL('image/png'); | |||
window.open(dataUrl, '_blank'); | |||
} | |||
} | |||
if (canvas.toBlob) { | |||
canvas.toBlob(function(blob) { | |||
if (blob) done(blob); | |||
else done(canvas.toDataURL('image/png')); | |||
}, 'image/png'); | |||
} else { | |||
done(canvas.toDataURL('image/png')); | |||
} | |||
} | |||
function prepareImagesForExport(container) { | |||
if (!container) return; | |||
container.querySelectorAll('img').forEach(ensureCrossOrigin); | |||
} | |||
function savePNG() { | function savePNG() { | ||
const | const maker = document.getElementById('tierlist-maker'); | ||
const table = document.getElementById('tierlist-table'); | const table = document.getElementById('tierlist-table'); | ||
if (!table) { | |||
alert('未找到要导出的表格。'); | |||
return; | |||
} | |||
maker.classList.add('exporting'); | |||
prepareImagesForExport(table); | |||
ensureHtml2Canvas(function() { | ensureHtml2Canvas(function() { | ||
html2canvas(table, { | |||
backgroundColor: null, | |||
scale: 2, | |||
useCORS: true, | |||
allowTaint: false, | |||
imageTimeout: 15000 | |||
}).then(function(canvas) { | |||
downloadCanvas(canvas); | |||
}).catch(function(err) { | |||
console.error('html2canvas 失败', err); | |||
alert('保存PNG失败:' + (err && err.message ? err.message : '未知错误')); | |||
}).finally(function() { | |||
maker.classList.remove('exporting'); | |||
}); | |||
}); | |||
} | |||
function resetAll() { | |||
const pool = document.getElementById('character-pool'); | |||
if (!pool) return; | |||
const rows = document.querySelectorAll('#tierlist-table .tier-dropzone'); | |||
rows.forEach(function(zone) { | |||
if (zone.classList.contains('pool')) return; | |||
Array.from(zone.querySelectorAll('.avatar-frame')).forEach(function(av) { | |||
pool.appendChild(av); | |||
}); | |||
cleanupEmptyPlaceholdersIn(zone); | |||
}); | }); | ||
cleanupEmptyPlaceholdersIn(pool); | |||
} | } | ||
ready(function() { | ready(function() { | ||
document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | ||
document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | ||
initAvatars(document.getElementById('character-pool')); | initAvatars(document.getElementById('character-pool')); | ||
document.getElementById('add-row').addEventListener('click', addNewRow); | var addBtn = document.getElementById('add-row'); | ||
document.getElementById('save-png').addEventListener('click', savePNG); | if (addBtn) addBtn.addEventListener('click', addNewRow); | ||
document.getElementById('reset-btn').addEventListener('click', | var saveBtn = document.getElementById('save-png'); | ||
if (saveBtn) saveBtn.addEventListener('click', savePNG); | |||
(function insertResetButton() { | |||
const controls = document.querySelector('#tierlist-maker .tierlist-controls'); | |||
if (!controls || document.getElementById('reset-roles')) return; | |||
const resetBtn = document.createElement('div'); | |||
resetBtn.id = 'reset-roles'; | |||
resetBtn.className = 'btn'; | |||
resetBtn.textContent = '重置'; | |||
controls.insertBefore(resetBtn, controls.querySelector('#save-png') || null); | |||
resetBtn.addEventListener('click', resetAll); | |||
})(); | |||
const pool = document.getElementById('character-pool'); | const pool = document.getElementById('character-pool'); | ||
const obs = new MutationObserver(function(muts) { | if (pool) { | ||
const obs = new MutationObserver(function(muts) { | |||
muts.forEach(function(m) { | |||
if (m.addedNodes && m.addedNodes.length) { | |||
m.addedNodes.forEach(function(n) { | |||
if (n.nodeType === 1) { | |||
if (n.classList.contains('avatar-frame')) { | |||
} | makeDraggable(n); | ||
} | } | ||
} | n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable); | ||
// 去除初始即为空的无用占位 | |||
if (pool.contains(n)) cleanupEmptyPlaceholdersIn(pool); | |||
} | |||
}); | |||
} | |||
}); | |||
}); | }); | ||
obs.observe(pool, { childList: true, subtree: true }); | |||
} | |||
}); | }); | ||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||