initial commit
This commit is contained in:
commit
0961d0e47d
15 changed files with 564 additions and 0 deletions
44
static/form.html
Normal file
44
static/form.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>in?</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="form.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="../">in?</a></h1>
|
||||
<form id="form">
|
||||
<label for="id">ID</label>
|
||||
<input id="id" type="text" placeholder="loading...">
|
||||
|
||||
<label for="is_in">Is in</label>
|
||||
<select id="is_in">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
|
||||
<label for="coords_bl">Bottom left coordinates</label>
|
||||
<input id="coords_bl" type="text">
|
||||
|
||||
<label for="coords_tr">Top right coordinates</label>
|
||||
<input id="coords_tr" type="text">
|
||||
|
||||
<label for="type">Type</label>
|
||||
<input id="type" type="text">
|
||||
|
||||
<label for="name">Name / Label</label>
|
||||
<input id="name" type="text">
|
||||
|
||||
<label for="content">Content</label>
|
||||
<textarea id="content"></textarea>
|
||||
|
||||
<label for="note">Note</label>
|
||||
<textarea id="note"></textarea>
|
||||
|
||||
<label for="hidden">Hide by default</label>
|
||||
<input type="checkbox" id="hidden">
|
||||
</form>
|
||||
|
||||
<button id="delete" class="btn red" autocomplete="off">Delete</button>
|
||||
<button id="save" class="btn green" autocomplete="off">Save</button>
|
||||
</body>
|
||||
</html>
|
103
static/form.js
Normal file
103
static/form.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
let items = {};
|
||||
const id = new URLSearchParams(window.location.search).get('id');
|
||||
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === 'interactive') {
|
||||
fetch('/api/items').then(r => r.json()).then((data) => {
|
||||
items = data;
|
||||
fillForm();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fillForm() {
|
||||
const item = items[id];
|
||||
|
||||
const is_in = document.getElementById('is_in');
|
||||
|
||||
for (const [_, item] of Object.entries(items)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = `${item.id} ${item.name && `(${item.name})` || ''}`
|
||||
is_in.appendChild(option);
|
||||
};
|
||||
|
||||
document.getElementById('id').placeholder = '';
|
||||
|
||||
if (id && item) {
|
||||
document.getElementById('id').value = item.id;
|
||||
document.getElementById('is_in').value = item.is_in;
|
||||
document.getElementById('coords_bl').value = item.coords_bl;
|
||||
document.getElementById('coords_tr').value = item.coords_tr;
|
||||
document.getElementById('type').value = item.type;
|
||||
document.getElementById('name').value = item.name;
|
||||
document.getElementById('content').value = item.content;
|
||||
document.getElementById('note').value = item.note;
|
||||
document.getElementById('hidden').checked = item.hidden;
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('save');
|
||||
const delBtn = document.getElementById('delete');
|
||||
|
||||
saveBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
};
|
||||
|
||||
delBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
del();
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
item = {
|
||||
id: document.getElementById('id').value || null,
|
||||
is_in: document.getElementById('is_in').value || null,
|
||||
coords_bl: document.getElementById('coords_bl').value || null,
|
||||
coords_tr: document.getElementById('coords_tr').value || null,
|
||||
type: document.getElementById('type').value || null,
|
||||
name: document.getElementById('name').value || null,
|
||||
content: document.getElementById('content').value || null,
|
||||
note: document.getElementById('note').value || null,
|
||||
hidden: document.getElementById('hidden').checked,
|
||||
}
|
||||
|
||||
if (items[item.id]) {
|
||||
if (!id || (id && id != item.id)) {
|
||||
alert(`ID ${item.id} exists. Please chose a different ID or edit the existing ${item.id} item!`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const original_id = id || item.id;
|
||||
|
||||
fetch(`/api/items/${original_id}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(item),
|
||||
})
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
window.location.href = '../';
|
||||
} else {
|
||||
r.text().then(t => alert(`Error:\n${t}`));
|
||||
}
|
||||
}).catch(error => {
|
||||
alert(`Error:\n${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
function del() {
|
||||
if (id && confirm('Are you sure?')) {
|
||||
fetch(`/api/items/${id}`, {method: 'DELETE'}).then(r => {
|
||||
if (r.ok) {
|
||||
window.location.href = '../';
|
||||
} else {
|
||||
r.text().then(t => alert(`Error:\n${t}`));
|
||||
}
|
||||
}).catch(error => {
|
||||
alert(`Error:\n${error}`);
|
||||
});
|
||||
}
|
||||
}
|
30
static/index.html
Normal file
30
static/index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>in?</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>in?</h1>
|
||||
<input type="text" id="search" value="" autocomplete="off" placeholder="RegEx search" tabindex="1" autofocus>
|
||||
<label>Show hidden items</label> <input type="checkbox" id="showhidden">
|
||||
<div id="results">
|
||||
<span id="loading">loading…</span>
|
||||
<template id="item">
|
||||
<div id="" class="result" tabindex="0">
|
||||
<h2>
|
||||
<span class="id"></span>
|
||||
<span class="name"></span>
|
||||
<a title="edit">⚙</a>
|
||||
<span class="loc"></span>
|
||||
</h2>
|
||||
<div class="type"></div>
|
||||
<div class="note"></div>
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<a href="form.html" class="btn green">+</a>
|
||||
</body>
|
||||
</html>
|
92
static/index.js
Normal file
92
static/index.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
let items = {};
|
||||
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === 'interactive') {
|
||||
document.getElementById('search').oninput = search;
|
||||
const checkbox = document.getElementById('showhidden')
|
||||
checkbox.oninput = showhidden;
|
||||
fetch('/api/items').then((response) => response.json()).then((data) => {
|
||||
items = data;
|
||||
renderItems();
|
||||
showhidden({target: checkbox});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderItems() {
|
||||
const container = document.getElementById('results');
|
||||
const template = document.getElementById('item');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
for (const [id, item] of Object.entries(items)) {
|
||||
const clone = template.content.cloneNode(true);
|
||||
const [loc, longloc] = getLocString(items, item);
|
||||
clone.querySelector(".loc").textContent = loc;
|
||||
clone.querySelector(".loc").title = longloc;
|
||||
clone.querySelector(".type").textContent = item.type;
|
||||
clone.querySelector(".id").textContent = item.id;
|
||||
clone.querySelector(".name").textContent = item.name;
|
||||
clone.querySelector(".content").textContent = item.content;
|
||||
clone.querySelector(".note").textContent = item.note;
|
||||
clone.querySelector(".result").id = id;
|
||||
clone.querySelector("a").href = `form.html?id=${id}`;
|
||||
if (item.hidden) {
|
||||
clone.querySelector(".result").classList.add('hidden')
|
||||
};
|
||||
container.appendChild(clone);
|
||||
};
|
||||
loading.remove();
|
||||
}
|
||||
|
||||
function getLocString(items, item) {
|
||||
if (item.is_in == item.id) {
|
||||
return ['in ↻', `${item.id} is in itself`];
|
||||
}
|
||||
let ancestors = [];
|
||||
let next = item;
|
||||
while (next) {
|
||||
ancestors.unshift(next);
|
||||
next = items[next.is_in];
|
||||
}
|
||||
ancestors.pop();
|
||||
const loc = ancestors.map(i => i.id).join(' ➜ ') || '⬚';
|
||||
let longloc = ancestors.map(i => `${i.type || ''} ${i.id} ${i.name && `(${i.name})` || ''}`).join(' ➜ ') || 'universe';
|
||||
return [loc, `${item.id} is in ${longloc}`];
|
||||
}
|
||||
|
||||
function search(e) {
|
||||
const searchAttrs = ['id', 'name', 'type', 'note', 'content'];
|
||||
const query = e.target.value;
|
||||
const regex = new RegExp(query, 'i')
|
||||
|
||||
for (const elem of document.getElementsByClassName('result')) {
|
||||
const item = items[elem.id];
|
||||
let found = false;
|
||||
if (query) {
|
||||
for (const a in searchAttrs) {
|
||||
const attr = item[searchAttrs[a]];
|
||||
if (attr && attr.match(regex)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
elem.classList.remove('filtered');
|
||||
} else {
|
||||
elem.classList.add('filtered');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showhidden(e){
|
||||
const container = document.getElementById('results');
|
||||
if (e.target.checked) {
|
||||
results.classList.add('showhidden');
|
||||
} else {
|
||||
results.classList.remove('showhidden');
|
||||
}
|
||||
}
|
136
static/style.css
Normal file
136
static/style.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
padding-bottom: 1em;
|
||||
background: #fefefe;
|
||||
}
|
||||
|
||||
h1, #search {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
text-decoration: none;
|
||||
color: initial;
|
||||
}
|
||||
|
||||
#results {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.result {
|
||||
background: #eee;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.result a {
|
||||
text-decoration: none;
|
||||
color: #03d;
|
||||
}
|
||||
|
||||
.result.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#results.showhidden .result.hidden {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.result.filtered {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loc {
|
||||
float: right;
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.note:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note {
|
||||
background: #aef;
|
||||
padding: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
padding: 0.5em;
|
||||
text-decoration: initial;
|
||||
color: initial;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn.green {
|
||||
background: #0a0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.red {
|
||||
background: #a00;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
margin: 0 0 1em 0;
|
||||
padding: 0.3em;
|
||||
font-family: monospace;
|
||||
font-size: 1.4em;
|
||||
background: initial;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
width: initial;
|
||||
display: initial;
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 8em;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue