circuitpython/supervisor/shared/web_workflow/static/directory.js

368 lines
10 KiB
JavaScript

/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* File: sortable-table.js
*
* Desc: Adds sorting to a HTML data table that implements ARIA Authoring Practices
*/
'use strict';
class SortableTable {
constructor(tableNode) {
this.tableNode = tableNode;
this.columnHeaders = tableNode.querySelectorAll('thead th');
this.sortColumns = [];
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (buttonNode) {
this.sortColumns.push(i);
buttonNode.setAttribute('data-column-index', i);
buttonNode.addEventListener('click', this.handleClick.bind(this));
}
}
this.optionCheckbox = document.querySelector(
'input[type="checkbox"][value="show-unsorted-icon"]'
);
if (this.optionCheckbox) {
this.optionCheckbox.addEventListener(
'change',
this.handleOptionChange.bind(this)
);
if (this.optionCheckbox.checked) {
this.tableNode.classList.add('show-unsorted-icon');
}
}
}
setColumnHeaderSort(columnIndex) {
if (typeof columnIndex === 'string') {
columnIndex = parseInt(columnIndex);
}
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (i === columnIndex) {
var value = ch.getAttribute('aria-sort');
if (value === 'descending') {
ch.setAttribute('aria-sort', 'ascending');
this.sortColumn(
columnIndex,
'ascending',
ch.classList.contains('num')
);
} else {
ch.setAttribute('aria-sort', 'descending');
this.sortColumn(
columnIndex,
'descending',
ch.classList.contains('num')
);
}
} else {
if (ch.hasAttribute('aria-sort') && buttonNode) {
ch.removeAttribute('aria-sort');
}
}
}
}
sortColumn(columnIndex, sortValue, isNumber) {
function compareValues(a, b) {
if (sortValue === 'ascending') {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return a.value - b.value;
} else {
return a.value < b.value ? -1 : 1;
}
}
} else {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return b.value - a.value;
} else {
return a.value > b.value ? -1 : 1;
}
}
}
}
if (typeof isNumber !== 'boolean') {
isNumber = false;
}
var tbodyNode = this.tableNode.querySelector('tbody');
var rowNodes = [];
var dataCells = [];
var rowNode = tbodyNode.firstElementChild;
var index = 0;
while (rowNode) {
rowNodes.push(rowNode);
var rowCells = rowNode.querySelectorAll('th, td');
var dataCell = rowCells[columnIndex];
var data = {};
data.index = index;
data.value = dataCell.textContent.toLowerCase().trim();
if (isNumber) {
data.value = parseFloat(data.value);
}
dataCells.push(data);
rowNode = rowNode.nextElementSibling;
index += 1;
}
dataCells.sort(compareValues);
// remove rows
while (tbodyNode.firstChild) {
tbodyNode.removeChild(tbodyNode.lastChild);
}
// add sorted rows
for (var i = 0; i < dataCells.length; i += 1) {
tbodyNode.appendChild(rowNodes[dataCells[i].index]);
}
}
/* EVENT HANDLERS */
handleClick(event) {
var tgt = event.currentTarget;
this.setColumnHeaderSort(tgt.getAttribute('data-column-index'));
}
handleOptionChange(event) {
var tgt = event.currentTarget;
if (tgt.checked) {
this.tableNode.classList.add('show-unsorted-icon');
} else {
this.tableNode.classList.remove('show-unsorted-icon');
}
}
}
// Initialize sortable table buttons
window.addEventListener('load', function () {
var sortableTables = document.querySelectorAll('table.sortable');
for (var i = 0; i < sortableTables.length; i++) {
new SortableTable(sortableTables[i]);
}
});
let new_directory_name = document.getElementById("name");
let files = document.getElementById("files");
var url_base = window.location;
var current_path;
var editable = undefined;
async function refresh_list() {
current_path = window.location.hash.substr(1);
if (current_path == "") {
current_path = "/";
}
// Do the fetch first because the browser will prompt for credentials.
const response = await fetch(new URL("/fs" + current_path, url_base),
{
headers: {
"Accept": "application/json"
},
credentials: "include"
}
);
const data = await response.json();
var new_children = [];
var title = document.querySelector("title");
title.textContent = current_path;
var path = document.querySelector("#path");
path.textContent = current_path;
var template = document.querySelector('#row');
if (editable === undefined) {
const status = await fetch(new URL("/fs/", url_base),
{
method: "OPTIONS",
credentials: "include"
}
);
editable = status.headers.get("Access-Control-Allow-Methods").includes("DELETE");
new_directory_name.disabled = !editable;
files.disabled = !editable;
if (!editable) {
let usbwarning = document.querySelector("#usbwarning");
usbwarning.style.display = "block";
}
}
if (window.location.path != "/fs/") {
var clone = template.content.cloneNode(true);
var td = clone.querySelectorAll("td");
td[0].textContent = "🗀";
var path = clone.querySelector("a");
let parent = new URL("..", "file://" + current_path);
path.href = "#" + parent.pathname;
path.textContent = "..";
// Remove the delete button
td[4].replaceChildren();
new_children.push(clone);
}
for (const f of data) {
// Clone the new row and insert it into the table
var clone = template.content.cloneNode(true);
var td = clone.querySelectorAll("td");
var icon = "⬇";
var file_path = current_path + f.name;
let api_url = new URL("/fs" + file_path, url_base);
let edit_url = "/edit/#" + file_path;
if (f.directory) {
file_path = "#" + file_path + "/";
api_url += "/";
} else {
file_path = api_url;
}
if (f.directory) {
icon = "🗀";
} else if(f.name.endsWith(".txt") ||
f.name.endsWith(".py") ||
f.name.endsWith(".js") ||
f.name.endsWith(".json")) {
icon = "🖹";
} else if (f.name.endsWith(".html")) {
icon = "🌐";
}
td[0].textContent = icon;
td[1].textContent = f.file_size;
var path = clone.querySelector("a");
path.href = file_path;
path.textContent = f.name;
td[3].textContent = (new Date(f.modified_ns / 1000000)).toLocaleString();
var delete_button = clone.querySelector("button.delete");
delete_button.value = api_url;
delete_button.disabled = !editable;
delete_button.onclick = del;
edit_url = new URL(edit_url, url_base);
let edit_link = clone.querySelector(".edit_link");
edit_link.href = edit_url
new_children.push(clone);
}
var tbody = document.querySelector("tbody");
tbody.replaceChildren(...new_children);
}
async function find_devices() {
const version_response = await fetch("/cp/version.json");
if (version_response.ok) {
url_base = new URL("/", window.location).href;
} else {
// TODO: Remove this when we've settled things. It is only used when this file isn't hosted
// by a CP device.
const response = await fetch("http://circuitpython.local/cp/devices.json");
let url = new URL("/", response.url);
url_base = url.href;
const data = await response.json();
}
refresh_list();
}
async function mkdir(e) {
const response = await fetch(
new URL("/fs" + current_path + new_directory_name.value + "/", url_base),
{
method: "PUT",
headers: {
'X-Timestamp': Date.now()
}
}
);
if (response.ok) {
refresh_list();
new_directory_name.value = "";
mkdir_button.disabled = true;
}
}
async function upload(e) {
for (const file of files.files) {
let file_path = new URL("/fs" + current_path + file.name, url_base);
const response = await fetch(file_path,
{
method: "PUT",
headers: {
'Content-Type': 'application/octet-stream',
'X-Timestamp': file.lastModified
},
body: file
}
)
if (response.ok) {
refresh_list();
files.value = "";
upload_button.disabled = true;
}
}
}
async function del(e) {
let fn = new URL(e.target.value);
var prompt = "Delete " + fn.pathname.substr(3);
if (e.target.value.endsWith("/")) {
prompt += " and all of its contents?";
} else {
prompt += "?";
}
if (confirm(prompt)) {
const response = await fetch(e.target.value,
{
method: "DELETE"
}
)
if (response.ok) {
refresh_list();
}
}
}
find_devices();
let mkdir_button = document.getElementById("mkdir");
mkdir_button.onclick = mkdir;
let upload_button = document.getElementById("upload");
upload_button.onclick = upload;
upload_button.disabled = files.files.length == 0;
files.onchange = () => {
upload_button.disabled = files.files.length == 0;
}
mkdir_button.disabled = new_directory_name.value.length == 0;
new_directory_name.oninput = () => {
mkdir_button.disabled = new_directory_name.value.length == 0;
}
window.onhashchange = refresh_list;