/* Offline Tierlist Maker Copyright (C) 2022 silverweed Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. */ 'use strict'; const MAX_NAME_LEN = 200; const DEFAULT_TIERS = ['S', 'A', 'B', 'C', 'D', 'E', 'F']; const TIER_COLORS = [ '#ff6666', '#f0a731', '#f4d95b', '#66ff66', '#58c8f4', '#5b76f4', '#f45bed' ]; let unique_id = 0; let unsaved_changes = false; const LAYOUT_HORIZONTAL = 0; const LAYOUT_VERTICAL = 1; let cur_layout = LAYOUT_HORIZONTAL; // Contains [[header, input, label]] let all_headers = []; let headers_orig_min_width; // DOM elems let untiered_images; let tierlist_div; let dragged_image; function reset_row(row) { row.querySelectorAll('span.item').forEach((item) => { for (let i = 0; i < item.children.length; ++i) { let img = item.children[i]; item.removeChild(img); untiered_images.appendChild(img); } item.parentNode.removeChild(item); }); } function hard_reset_list() { tierlist_div.innerHTML = ''; untiered_images.innerHTML = ''; } function soft_reset_list() { tierlist_div.querySelectorAll('.row').forEach(reset_row); unsaved_changes = true; } window.addEventListener('load', () => { untiered_images = document.querySelector('.images'); tierlist_div = document.querySelector('.tierlist'); for (let i = 0; i < DEFAULT_TIERS.length; ++i) { add_row(i, DEFAULT_TIERS[i]); } recompute_header_colors(); headers_orig_min_width = all_headers[0][0].clientWidth; make_accept_drop(document.querySelector('.images')); bind_title_events(); document.getElementById('load-img-input').addEventListener('input', (evt) => { let images = document.querySelector('.images'); for (let file of evt.target.files) { let reader = new FileReader(); reader.addEventListener('load', (load_evt) => { let img = create_img_with_src(load_evt.target.result); images.appendChild(img); unsaved_changes = true; }); reader.readAsDataURL(file); } }); document.onpaste = (evt) => { let clip_data = evt.clipboardData || evt.originalEvent.clipboardData; let items = clip_data.items; let images = document.querySelector('.images'); for (let item of items) { if (item.kind === 'file') { let blob = item.getAsFile(); let reader = new FileReader(); reader.onload = (load_evt) => { let img = create_img_with_src(load_evt.target.result); images.appendChild(img); unsaved_changes = true; }; reader.readAsDataURL(blob); } } }; document.getElementById('reset-list-input').addEventListener('click', () => { if (confirm('Reset Tierlist? (this will place all images back in the pool)')) { soft_reset_list(); } }); document.getElementById('export-input').addEventListener('click', () => { let name = prompt('Please give a name to this tierlist'); if (name) { save_tierlist(`${name}.json`); } }); document.getElementById('import-input').addEventListener('input', (evt) => { if (!evt.target.files) { return; } let file = evt.target.files[0]; let reader = new FileReader(); reader.addEventListener('load', (load_evt) => { let raw = load_evt.target.result; let parsed = JSON.parse(raw); if (!parsed) { alert("Failed to parse data"); return; } hard_reset_list(); load_tierlist(parsed); }); reader.readAsText(file); }); bind_trash_events(); bind_toggle_layout_events(); window.addEventListener('beforeunload', (evt) => { if (!unsaved_changes) return null; var msg = "You have unsaved changes. Leave anyway?"; (evt || window.event).returnValue = msg; return msg; }); void try_load_tierlist_json(); const modal = document.getElementById('image-modal'); const modalImg = document.getElementById('modal-img'); const modalDesc = document.getElementById('modal-description'); const modalSave = document.getElementById('modal-save'); const modalClose = document.querySelector('.modal .close'); let currentModalImage = null; function showModal(img) { currentModalImage = img; modal.style.display = 'flex'; modalImg.src = img.src; modalDesc.value = img.dataset.description || ''; } modalClose.addEventListener('click', () => { modal.style.display = 'none'; }); modalSave.addEventListener('click', () => { if (currentModalImage) { currentModalImage.dataset.description = modalDesc.value; } modal.style.display = 'none'; }); window.addEventListener('click', (evt) => { if (evt.target == modal) { modal.style.display = 'none'; } }); tierlist_div.addEventListener('click', (evt) => { if (evt.target.tagName.toUpperCase() === 'IMG') { showModal(evt.target); } }); }); function create_img_with_src(src) { let img = document.createElement('img'); img.src = src; img.style.userSelect = 'none'; img.classList.add('draggable'); img.draggable = true; img.addEventListener("dragstart", (evt) => { evt.dataTransfer.setData("text/plain", null); dragged_image = evt.target; dragged_image.classList.add("dragged"); }); img.addEventListener("dragend", (evt) => { if (dragged_image) { dragged_image.classList.remove("dragged"); } dragged_image = null; }); return img; } function save(filename, text) { unsaved_changes = false; var el = document.createElement('a'); el.setAttribute('href', 'data:text/html;charset=utf-8,' + encodeURIComponent(text)); el.setAttribute('download', filename); el.style.display = 'none'; document.body.appendChild(el); el.click(); document.body.removeChild(el); } function save_tierlist(filename) { let serialized_tierlist = { title: document.querySelector('.title-label').innerText, rows: [], }; tierlist_div.querySelectorAll('.row').forEach((row, i) => { serialized_tierlist.rows.push({ name: row.querySelector('.header label').innerText.substr(0, MAX_NAME_LEN) }); serialized_tierlist.rows[i].imgs = []; row.querySelectorAll('img').forEach((img) => { serialized_tierlist.rows[i].imgs.push({ src: img.src, description: img.dataset.description || '' }); }); }); let untiered_imgs = document.querySelectorAll('.images img'); if (untiered_imgs.length > 0) { serialized_tierlist.untiered = []; untiered_imgs.forEach((img) => { serialized_tierlist.untiered.push({ src: img.src, description: img.dataset.description || '' }); }); } save(filename, JSON.stringify(serialized_tierlist)); } function load_tierlist(serialized_tierlist) { document.querySelector('.title-label').innerText = serialized_tierlist.title; for (let idx in serialized_tierlist.rows) { let ser_row = serialized_tierlist.rows[idx]; let elem = add_row(idx, ser_row.name); for (let img_obj of ser_row.imgs ?? []) { let img = create_img_with_src(img_obj.src); img.dataset.description = img_obj.description || ''; let td = document.createElement('span'); td.classList.add('item'); td.appendChild(img); let items_container = elem.querySelector('.items'); items_container.appendChild(td); } elem.querySelector('label').innerText = ser_row.name; } recompute_header_colors(); if (serialized_tierlist.untiered) { let images = document.querySelector('.images'); for (let img_obj of serialized_tierlist.untiered) { let img = create_img_with_src(img_obj.src); img.dataset.description = img_obj.description || ''; images.appendChild(img); } } resize_headers(); unsaved_changes = false; } function end_drag(evt) { if (dragged_image) { dragged_image.classList.remove("dragged"); } dragged_image = null; } window.addEventListener('mouseup', end_drag); window.addEventListener('dragend', end_drag); function make_accept_drop(elem) { elem.classList.add('droppable'); elem.addEventListener('dragenter', (evt) => { evt.preventDefault(); evt.target.classList.add('drag-entered'); }); elem.addEventListener('dragleave', (evt) => { evt.target.classList.remove('drag-entered'); }); elem.addEventListener('dragover', (evt) => { evt.preventDefault(); }); elem.addEventListener('drop', (evt) => { evt.preventDefault(); evt.target.classList.remove('drag-entered'); if (!dragged_image) { return; } let dragged_image_parent = dragged_image.parentNode; if (dragged_image_parent.tagName.toUpperCase() === 'SPAN' && dragged_image_parent.classList.contains('item')) { let containing_tr = dragged_image_parent.parentNode; containing_tr.removeChild(dragged_image_parent); } else { dragged_image_parent.removeChild(dragged_image); } let td = document.createElement('span'); td.classList.add('item'); td.appendChild(dragged_image); let items_container = elem.querySelector('.items'); if (!items_container) { items_container = elem; } items_container.appendChild(td); dragged_image.draggable = true; dragged_image.classList.remove("dragged"); dragged_image.addEventListener("dragstart", (e) => { e.dataTransfer.setData("text/plain", null); dragged_image.classList.add("dragged"); }); unsaved_changes = true; }); } function enable_edit_on_click(container, input, label) { function change_label(evt) { input.style.display = 'none'; label.innerText = input.value; label.style.display = 'inline'; unsaved_changes = true; } input.addEventListener('change', change_label); input.addEventListener('focusout', change_label); container.addEventListener('click', (evt) => { label.style.display = 'none'; input.value = label.innerText.substr(0, MAX_NAME_LEN); input.style.display = 'inline'; input.select(); }); } function bind_title_events() { let title_label = document.querySelector('.title-label'); let title_input = document.getElementById('title-input'); let title = document.querySelector('.title'); enable_edit_on_click(title, title_input, title_label); } function create_label_input(row, row_idx, row_name) { let input = document.createElement('input'); input.id = `input-tier-${unique_id++}`; input.type = 'text'; input.addEventListener('change', resize_headers); let label = document.createElement('label'); label.htmlFor = input.id; label.innerText = row_name; let header = row.querySelector('.header'); all_headers.splice(row_idx, 0, [header, input, label]); header.appendChild(label); header.appendChild(input); enable_edit_on_click(header, input, label); } function resize_headers() { let max_width = headers_orig_min_width; for (let [other_header, _i, label] of all_headers) { max_width = Math.max(max_width, label.clientWidth); } for (let [other_header, _i2, _l2] of all_headers) { other_header.style.minWidth = `${max_width}px`; } } function add_row(index, name) { let div = document.createElement('div'); let header = document.createElement('span'); let items = document.createElement('span'); div.classList.add('row'); header.classList.add('header'); items.classList.add('items'); div.appendChild(header); div.appendChild(items); let row_buttons = document.createElement('div'); row_buttons.classList.add('row-buttons'); let btn_plus_up = document.createElement('input'); btn_plus_up.type = "button"; btn_plus_up.value = '+'; btn_plus_up.title = "Add row above"; btn_plus_up.addEventListener('click', (evt) => { let parent_div = evt.target.parentNode.parentNode; let rows = Array.from(tierlist_div.children); let idx = rows.indexOf(parent_div); console.assert(idx >= 0); add_row(idx, ''); recompute_header_colors(); }); let btn_rm = document.createElement('input'); btn_rm.type = "button"; btn_rm.value = '-'; btn_rm.title = "Remove row"; btn_rm.addEventListener('click', (evt) => { let rows = Array.from(tierlist_div.querySelectorAll('.row')); if (rows.length < 2) return; let parent_div = evt.target.parentNode.parentNode; let idx = rows.indexOf(parent_div); console.assert(idx >= 0); if (rows[idx].querySelectorAll('img').length === 0 || confirm(`Remove tier ${rows[idx].querySelector('.header label').innerText}? (This will move back all its content to the untiered pool)`)) { rm_row(idx); } recompute_header_colors(); }); let btn_plus_down = document.createElement('input'); btn_plus_down.type = "button"; btn_plus_down.value = '+'; btn_plus_down.title = "Add row below"; btn_plus_down.addEventListener('click', (evt) => { let parent_div = evt.target.parentNode.parentNode; let rows = Array.from(tierlist_div.children); let idx = rows.indexOf(parent_div); console.assert(idx >= 0); add_row(idx + 1, name); recompute_header_colors(); }); row_buttons.appendChild(btn_plus_up); row_buttons.appendChild(btn_rm); row_buttons.appendChild(btn_plus_down); div.appendChild(row_buttons); let rows = tierlist_div.children; if (index === rows.length) { tierlist_div.appendChild(div); } else { let nxt_child = rows[index]; tierlist_div.insertBefore(div, nxt_child); } make_accept_drop(div); create_label_input(div, index, name); return div; } function rm_row(idx) { let row = tierlist_div.children[idx]; reset_row(row); tierlist_div.removeChild(row); } function recompute_header_colors() { tierlist_div.querySelectorAll('.row').forEach((row, row_idx) => { let color = TIER_COLORS[row_idx % TIER_COLORS.length]; row.querySelector('.header').style.backgroundColor = color; }); } function bind_trash_events() { let trash = document.getElementById('trash'); trash.classList.add('droppable'); trash.addEventListener('dragenter', (evt) => { evt.preventDefault(); evt.target.src = '/static/img/trash-2.svg'; }); trash.addEventListener('dragexit', (evt) => { evt.preventDefault(); evt.target.src = '/static/img/trash.svg'; }); trash.addEventListener('dragover', (evt) => { evt.preventDefault(); }); trash.addEventListener('drop', (evt) => { evt.preventDefault(); evt.target.src = '/static/img/trash.svg'; if (dragged_image) { let dragged_image_parent = dragged_image.parentNode; if (dragged_image_parent.tagName.toUpperCase() === 'SPAN' && dragged_image_parent.classList.contains('item')) { let containing_tr = dragged_image_parent.parentNode; containing_tr.removeChild(dragged_image_parent); } dragged_image.remove(); } }); } function bind_toggle_layout_events() { let toggle = document.getElementById('toggle-layout'); toggle.addEventListener('click', () => { set_layout((cur_layout + 1) % 2); }); } function set_layout(layout) { let main = document.getElementsByClassName("main-content")[0]; if (layout === LAYOUT_VERTICAL) { main.classList.add("vertical"); } else { main.classList.remove("vertical"); } cur_layout = layout; } function is_url(str) { try { new URL(str); return true; } catch (e) { return false; } } async function try_load_tierlist_json() { const load_from_url = new URLSearchParams(window.location.search).get('url'); if (load_from_url !== null && is_url(load_from_url)) { try { let result = await fetch(load_from_url); result = await result.json(); hard_reset_list(); load_tierlist(result); } catch (e) { console.error(e); } } }