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.
 
 

384 lines
12 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Card Sorting</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: flex;
padding: 5px;
align-items: center;
justify-content: space-around;
}
.toolbar button {
font-size: 24px;
cursor: pointer;
margin: 5px;
}
/* Uncategorized cards */
.uncategorized-cards {
border-radius: 5px;
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 .grid {
min-height: 100px;
}
.category-cards .item {
width: 100%;
}
.category-controls {
margin-bottom: 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">
<div class="category-controls">
<div class="category-drag-handle"></div>
</div>
<input class="title" placeholder="Type a name" />
<div class="category-cards">
<div class="grid"></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 = Object.assign({}, settingsDefaults);
/* 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) {
if (window.location.search) {
data = loadFromQueryParameters();
}
else {
window.location.href = 'index.html';
return;
}
}
else {
data = await loadFromString(window.location.hash.split('#').pop());
}
const cardTexts = data[uncategorizedKey];
Object.assign(settings, loadSettings(data));
if (settings.allowCategoryEditing === false) {
buttonCreateCategory.style.display = 'none';
categoryTemplate.content.querySelector('.title').disabled = true;
categoryTemplate.content.querySelector('.title').removeAttribute('placeholder');
}
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;
}
else if (settings.allowCategoryEditing === false) {
titleInput.style.opacity = '0';
titleInput.style.height = '0';
titleInput.style.margin = '0';
titleInput.style.padding = '0';
}
categoriesGrid.show(
categoriesGrid.add([itemElement], {
active: false,
index: !!name ? -1 : 0,
}),
{
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>