aboutsummaryrefslogtreecommitdiffstats
path: root/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'index.js')
-rw-r--r--index.js659
1 files changed, 659 insertions, 0 deletions
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..366a0b5
--- /dev/null
+++ b/index.js
@@ -0,0 +1,659 @@
+/*
+ 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 the file. This will export your tiers as a JSON file so you can continue working on it later');
+ if (name) {
+ save_tierlist(`${name}.json`);
+ }
+ });
+
+ document.getElementById('export-input-html').addEventListener('click', () => {
+ let name = prompt('This will export your tiers as a single interactive HTML file. Please give a name to the file');
+ if (name) {
+ save_tierlist_with_template(`${name}.html`);
+ }
+ });
+
+ 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 modalTitle = document.getElementById('modal-title');
+ 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;
+ document.body.style.overflow = 'hidden';
+ modal.style.display = 'flex';
+ modalImg.src = img.src;
+ modalTitle.value = img.dataset.title || '';
+ modalDesc.value = img.dataset.description || '';
+ }
+
+ modalClose.addEventListener('click', () => {
+ modal.style.display = 'none';
+ document.body.style.overflow = 'hidden';
+ });
+
+ modalSave.addEventListener('click', () => {
+ if (currentModalImage) {
+ currentModalImage.dataset.title = modalTitle.value;
+ currentModalImage.dataset.description = modalDesc.value;
+ }
+ modal.style.display = 'none';
+ document.body.style.overflow = 'hidden';
+ });
+
+ window.addEventListener('click', (evt) => {
+ if (evt.target == modal) {
+ modal.style.display = 'none';
+ document.body.style.overflow = 'hidden';
+ }
+ });
+
+ 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("mouseenter", (evt) => {
+ const title = evt.target.dataset.title;
+ if (title) {
+ evt.target.title = title;
+ }
+ });
+
+ 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,
+ title: img.dataset.title || '',
+ 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,
+ title: img.dataset.title || '',
+ description: img.dataset.description || ''
+ });
+ });
+ }
+
+ save(filename, JSON.stringify(serialized_tierlist));
+}
+async function save_tierlist_with_template(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').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,
+ title: img.dataset.title || '',
+ 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,
+ title: img.dataset.title || '',
+ description: img.dataset.description || ''
+ });
+ });
+ }
+
+ try {
+ // Fetch resources
+ let [templateResponse, jsResponse, cssResponse] = await Promise.all([
+ fetch('/tiers'),
+ fetch('/tiers.js'),
+ fetch('/tiers.css')
+ ]);
+
+ let [templateHTML, scriptContent, styleContent] = await Promise.all([
+ templateResponse.text(),
+ jsResponse.text(),
+ cssResponse.text()
+ ]);
+
+ // Inject the EMBEDDED_JSON inside a script tag
+ let jsonScript = `<script>
+ const EMBEDDED_JSON = ${JSON.stringify(serialized_tierlist, null, 4)};
+ window.addEventListener('load', () => {
+ hard_reset_list();
+ load_tierlist(EMBEDDED_JSON);
+ });
+ </script>`;
+
+ let inlineScript = `<script>\n${scriptContent}\n</script>`;
+ let inlineCSS = `<style>\n${styleContent}\n</style>`;
+
+ templateHTML = templateHTML.replace("</head>", inlineCSS + "\n</head>");
+ let updatedHTML = templateHTML.replace("</body>", jsonScript + "\n" + inlineScript + "\n</body>");
+ save(filename, updatedHTML);
+ } catch (e) {
+ console.error("Error fetching resources:", e);
+ }
+}
+
+
+
+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.title = img_obj.title || '';
+ 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.title = img_obj.title || '';
+ 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 = '/img/trash-2.svg';
+ });
+ trash.addEventListener('dragexit', (evt) => {
+ evt.preventDefault();
+ evt.target.src = '/img/trash.svg';
+ });
+ trash.addEventListener('dragover', (evt) => {
+ evt.preventDefault();
+ });
+ trash.addEventListener('drop', (evt) => {
+ evt.preventDefault();
+ evt.target.src = '/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); }
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage