You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
375 lines
11 KiB
375 lines
11 KiB
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="utf8" /> |
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|
<title>Card Sort</title> |
|
<style> |
|
/* Reset */ |
|
body, html { |
|
margin: 0; |
|
padding: 0; |
|
font-family: sans-serif; |
|
} |
|
* { |
|
box-sizing: border-box; |
|
} |
|
/* Layout */ |
|
.main-layout { |
|
display: grid; |
|
grid-template-columns: auto 1fr; |
|
grid-template-rows: minmax(60px, min-content) 1fr; |
|
gap: 10px 10px; |
|
grid-template-areas: |
|
"top top" |
|
"left right"; |
|
margin: 10px; |
|
} |
|
.toolbar { |
|
grid-area: top; |
|
} |
|
.uncategorized-cards { |
|
position: relative; |
|
grid-area: left; |
|
} |
|
.categories { |
|
position: relative; |
|
grid-area: right; |
|
} |
|
/* Muuri functional */ |
|
.grid { |
|
position: relative; |
|
} |
|
.item { |
|
display: block; |
|
position: absolute; |
|
} |
|
.item.muuri-item-dragging { |
|
z-index: 3; |
|
} |
|
.item.muuri-item-releasing { |
|
z-index: 2; |
|
} |
|
.item.muuri-item-hidden { |
|
z-index: 0; |
|
} |
|
.item-content { |
|
position: relative; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
/* Toolbar */ |
|
.toolbar { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr 1fr; |
|
grid-template-rows: 1fr; |
|
gap: 10px 10px; |
|
align-items: center; |
|
justify-items: center; |
|
} |
|
.toolbar button { |
|
font-size: 24px; |
|
cursor: pointer; |
|
} |
|
/* Uncategorized cards */ |
|
.uncategorized-cards { |
|
/* padding: 5px; */ |
|
border-radius: 5px; |
|
/* box-shadow: 0 2px 5px rgba(0, 0, 0, .5); */ |
|
width: 200px; |
|
max-width: calc(50vw - 35px); |
|
} |
|
.uncategorized-cards .grid { |
|
min-height: 100%; |
|
} |
|
.uncategorized-cards .item { |
|
width: 100%; |
|
} |
|
.card { |
|
margin: 5px; |
|
padding: 5px 7px; |
|
border-radius: 3px; |
|
font-size: 18px; |
|
background: #ee6; |
|
box-shadow: 0 2px 2px rgba(172, 121, 0, 0.58); |
|
cursor: grab; |
|
} |
|
.muuri-item-placeholder .card { |
|
pointer-events: none; |
|
background: transparent; |
|
border: 2px dashed #bbb; |
|
color: #999; |
|
box-shadow: none; |
|
border-radius: 7px; |
|
} |
|
/* Categories */ |
|
.category { |
|
margin: 5px 15px 30px 5px; |
|
padding: calc(5px + 5px); |
|
background: #fff; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, .1); |
|
} |
|
.category .title { |
|
width: 100%; |
|
max-width: 200px; |
|
padding: 7px 5px; |
|
margin-bottom: 10px; |
|
border: 0; |
|
border-bottom: 1px solid #bbf; |
|
font-size: 20px; |
|
color: inherit; |
|
} |
|
.category-cards { |
|
} |
|
.category-cards .grid { |
|
min-height: 100px; |
|
} |
|
.category-cards .item { |
|
width: 100%; |
|
} |
|
.category-controls { |
|
margin-top: 5px; |
|
} |
|
.category-drag-handle { |
|
width: 100%; |
|
height: 16px; |
|
background: radial-gradient(#ddd 35%, transparent 36%) 0 0; |
|
background-color: #fff; |
|
background-size: 8px 8px; |
|
cursor: grab; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="main-layout"> |
|
<div class="toolbar"> |
|
<button class="btn-create-category">Create a new category</button> |
|
<button class="btn-share-result">Share result</button> |
|
<button class="btn-export-result">Export result</button> |
|
</div> |
|
<div class="uncategorized-cards"> |
|
<div class="grid"></div> |
|
</div> |
|
<div class="categories"> |
|
<div class="grid"></div> |
|
</div> |
|
</div> |
|
<template class="card-template"> |
|
<div class="item"> |
|
<div class="item-content"> |
|
<div class="card"></div> |
|
</div> |
|
</div> |
|
</template> |
|
<template class="category-template"> |
|
<div class="item"> |
|
<div class="item-content"> |
|
<div class="category"> |
|
<input class="title" placeholder="Type a name" /> |
|
<div class="category-cards"> |
|
<div class="grid"></div> |
|
</div> |
|
<div class="category-controls"> |
|
<div class="category-drag-handle"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
<script src="muuri-0.9.5.min.js"></script> |
|
<script src="pako-2.0.4.min.js"></script> |
|
<script src="common-scripts.js"></script> |
|
<script> |
|
let data = {}; |
|
const settings = { |
|
allowCategoryEditing: true, |
|
}; |
|
|
|
/* Card sorting */ |
|
const buttonCreateCategory = document.querySelector('.btn-create-category'); |
|
const buttonShareResult = document.querySelector('.btn-share-result'); |
|
const buttonExportResult = document.querySelector('.btn-export-result'); |
|
const cardTemplate = document.querySelector('.card-template'); |
|
const categoryTemplate = document.querySelector('.category-template'); |
|
const uncategorizedCardsGrid = new Muuri('.uncategorized-cards .grid', { |
|
dragEnabled: true, |
|
dragSort: () => cardGrids, |
|
dragPlaceholder: { |
|
enabled: true, |
|
createElement: (item) => item.getElement().cloneNode(true), |
|
}, |
|
dragAutoScroll: { |
|
targets: [window], |
|
sortDuringScroll: false, |
|
}, |
|
}); |
|
const categoriesGrid = new Muuri('.categories .grid', { |
|
dragEnabled: true, |
|
dragSort: true, |
|
dragHandle: '.category-drag-handle', |
|
}); |
|
const cardGrids = [uncategorizedCardsGrid]; |
|
|
|
const debouncedUpdateData = debounce(updateData, 500); |
|
initialize(); |
|
|
|
async function initialize() { |
|
if ( ! window.location.hash) { |
|
window.location.href = 'index.html'; |
|
return; |
|
} |
|
data = await loadFromString(window.location.hash.split('#').pop()); |
|
const cardTexts = data[uncategorizedKey]; |
|
Object.assign(settings, loadSettings(data)); |
|
if (settings.allowCategoryEditing === false) { |
|
categoryTemplate.content.querySelector('.title').disabled = true; |
|
} |
|
buttonCreateCategory.addEventListener('click', () => createCategory()); |
|
for (const text of cardTexts) { |
|
createCard(text); |
|
} |
|
const categories = Object.keys(data).filter(categoryName => !reservedKeys.includes(categoryName)); |
|
if (categories.length > 0) { |
|
for (categoryName of categories) { |
|
createCategory(categoryName); |
|
} |
|
} |
|
else { |
|
createCategory(); |
|
createCategory(); |
|
createCategory(); |
|
} |
|
uncategorizedCardsGrid.on('dragEnd', function handleDragEnd(data) { |
|
debouncedUpdateData(); |
|
categoriesGrid.refreshItems().layout(); |
|
}); |
|
buttonShareResult.addEventListener('click', shareResult); |
|
buttonExportResult.addEventListener('click', exportResult); |
|
} |
|
|
|
function createCard(text, categoryGrid) { |
|
const targetGrid = categoryGrid || uncategorizedCardsGrid; |
|
const itemElement = document.importNode(cardTemplate.content.children[0], true); |
|
itemElement.querySelector('.card').textContent = text; |
|
targetGrid.show( |
|
targetGrid.add([itemElement], { |
|
active: false, |
|
}), |
|
{ |
|
onFinish: () => { |
|
categoriesGrid.refreshItems().layout(); |
|
}, |
|
}, |
|
); |
|
} |
|
|
|
function createCategory(name, cards) { |
|
const itemElement = document.importNode(categoryTemplate.content.children[0], true); |
|
const titleInput = itemElement.querySelector('.title'); |
|
titleInput.addEventListener('input', function handleInput() { |
|
debouncedUpdateData(); |
|
}); |
|
if (name) { |
|
titleInput.value = name; |
|
} |
|
categoriesGrid.show( |
|
categoriesGrid.add([itemElement], { |
|
active: false |
|
}), |
|
{ |
|
onFinish: () => { |
|
const categoryGrid = new Muuri(itemElement.querySelector('.grid'), { |
|
dragEnabled: true, |
|
dragSort: () => cardGrids, |
|
dragPlaceholder: { |
|
enabled: true, |
|
createElement: (item) => item.getElement().cloneNode(true), |
|
}, |
|
dragAutoScroll: { |
|
targets: [window], |
|
sortDuringScroll: false, |
|
}, |
|
}); |
|
categoryGrid.on('dragEnd', function handleDragEnd(data) { |
|
debouncedUpdateData(); |
|
categoriesGrid.refreshItems().layout(); |
|
}); |
|
cardGrids.push(categoryGrid); |
|
const cardTexts = data[name] || []; |
|
for (const text of cardTexts) { |
|
createCard(text, categoryGrid); |
|
} |
|
}, |
|
} |
|
); |
|
} |
|
|
|
function updateData() { |
|
data = {}; |
|
for (const categoryElement of document.querySelectorAll('.category')) { |
|
const categoryName = categoryElement.querySelector('.title').value; |
|
data[categoryName] = []; |
|
for (const cardElement of categoryElement.querySelectorAll('.card')) { |
|
data[categoryName].push(cardElement.textContent); |
|
} |
|
} |
|
data[uncategorizedKey] = []; |
|
for (const cardElement of document.querySelectorAll('.uncategorized-cards .card')) { |
|
data[uncategorizedKey].push(cardElement.textContent); |
|
} |
|
data[settingsFlagsKey] = saveSettings(settings); |
|
} |
|
|
|
async function shareResult() { |
|
updateData(); |
|
const dataString = await saveToString(data); |
|
const baseUrl = window.location.href.replace(regexRemoveBasenameFromUrl, '/'); |
|
const url = baseUrl + 'card-sort.html#' + dataString; |
|
if (navigator.share) { |
|
navigator.share({ |
|
title: '', |
|
text: '', |
|
url: url, |
|
}); |
|
} |
|
else if (navigator.clipboard) { |
|
navigator.clipboard.writeText(url); |
|
const originalText = buttonShareResult.textContent; |
|
buttonShareResult.textContent = 'Link copied to clipboard'; |
|
clearTimeout(buttonShareResult._textTimeout); |
|
buttonShareResult._textTimeout = setTimeout(() => { |
|
buttonShareResult.textContent = originalText; |
|
}, 5000); |
|
} |
|
else { |
|
alert('Your browser does not support sharing. Please copy from the address bar instead.'); |
|
window.location.href = url; |
|
} |
|
} |
|
|
|
function exportResult() { |
|
updateData(); |
|
const cleanedData = {}; |
|
for ([key, value] of Object.entries(data)) { |
|
if (reservedKeys.includes(key)) { |
|
continue; |
|
} |
|
cleanedData[key] = value; |
|
} |
|
const serializedData = JSON.stringify(cleanedData, null, 2); |
|
if (navigator.clipboard) { |
|
navigator.clipboard.writeText(serializedData); |
|
const originalText = buttonExportResult.textContent; |
|
buttonExportResult.textContent = 'Data copied to clipboard'; |
|
clearTimeout(buttonExportResult._textTimeout); |
|
buttonExportResult._textTimeout = setTimeout(() => { |
|
buttonExportResult.textContent = originalText; |
|
}, 5000); |
|
} |
|
else { |
|
prompt('Your browser does not support clipboard. You may try copying the content from below.', serializedData); |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |