mirror of
https://github.com/lus/pasty.git
synced 2023-08-10 21:13:09 +03:00
Improve the frontend (API v2 functionalities) (#18)
* Fix line number height issue * Fix notification container position * Remove line wrapping * Switch to the new API * Rework JS & implement paste editing * Implement paste reports * Document the report webhook
This commit is contained in:
parent
149abf77f1
commit
70c4392390
3
API.md
3
API.md
@ -173,9 +173,12 @@ POST /api/v2/pastes/{paste_id}/report
|
|||||||
**Response:**
|
**Response:**
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
|
"success": true, // Whether or not the report was received successfully (this is returned by the report webhook to allow custom errors)
|
||||||
"message": "message" // An optional message to display to the reporting user
|
"message": "message" // An optional message to display to the reporting user
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
* The endpoint is only available if the report system is enabled. Otherwise it will return a `404 Not Found` error.
|
* The endpoint is only available if the report system is enabled. Otherwise it will return a `404 Not Found` error.
|
||||||
|
* The request for this endpoint is the exact same that will reach the webhook.
|
||||||
|
* The response from this endpoint is the exact same that pasty expects from the webhook.
|
@ -17,6 +17,7 @@ type ReportRequest struct {
|
|||||||
|
|
||||||
// ReportResponse represents a report response received from the report webhook
|
// ReportResponse represents a report response received from the report webhook
|
||||||
type ReportResponse struct {
|
type ReportResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,28 @@ html, body {
|
|||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #444444;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -33,7 +55,15 @@ html, body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#spinner {
|
#spinner-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 130px;
|
||||||
|
right: 20px;
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-container .spinner {
|
||||||
-webkit-animation: .75s linear infinite spinner;
|
-webkit-animation: .75s linear infinite spinner;
|
||||||
animation: .75s linear infinite spinner;
|
animation: .75s linear infinite spinner;
|
||||||
-webkit-animation-play-state: inherit;
|
-webkit-animation-play-state: inherit;
|
||||||
@ -41,16 +71,32 @@ html, body {
|
|||||||
border: solid 5px #ffffff;
|
border: solid 5px #ffffff;
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 50px;
|
height: 100%;
|
||||||
width: 50px;
|
width: 100%;
|
||||||
position: fixed;
|
|
||||||
top: 130px;
|
|
||||||
right: 20px;
|
|
||||||
-webkit-transform: translate3d(-50%, -50%, 0);
|
-webkit-transform: translate3d(-50%, -50%, 0);
|
||||||
transform: translate3d(-50%, -50%, 0);
|
transform: translate3d(-50%, -50%, 0);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#btn_report {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 60px;
|
||||||
|
right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_report svg {
|
||||||
|
-webkit-transition: all 250ms;
|
||||||
|
transition: all 250ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_report:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_report:hover svg {
|
||||||
|
stroke: #2daa57;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -132,44 +178,57 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container #content {
|
.container #content {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: calc(100vw - 50px);
|
width: calc(100vw - 50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #content #code {
|
.container #content #code {
|
||||||
word-break: break-all;
|
white-space: pre;
|
||||||
white-space: pre-wrap;
|
line-height: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #content #input {
|
.container #content #input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
resize: none;
|
resize: none;
|
||||||
font-size: 16px;
|
font: inherit;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #notifications {
|
.container #notifications {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 30px;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #notifications div {
|
.container #notifications div {
|
||||||
width: 100vw;
|
border-radius: 10px;
|
||||||
padding: 10px 20px;
|
width: 500px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #notifications div.error {
|
.container #notifications div.error {
|
||||||
background-color: #ff3d3d;
|
background-color: #ff4d4d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container #notifications div.success {
|
.container #notifications div.success {
|
||||||
background-color: #389b38;
|
background-color: #389b38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container #notifications div:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -236,6 +295,15 @@ html, body {
|
|||||||
.navigation .meta #version {
|
.navigation .meta #version {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.container #notifications {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container #notifications div {
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100vw;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
#footer #flex {
|
#footer #flex {
|
||||||
margin: 0 0 0 25px;
|
margin: 0 0 0 25px;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -7,6 +7,29 @@ html, body {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: 'Source Code Pro', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #444444;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -31,7 +54,13 @@ html, body {
|
|||||||
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#spinner {
|
#spinner-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 130px;
|
||||||
|
right: 20px;
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
& .spinner {
|
||||||
-webkit-animation: .75s linear infinite spinner;
|
-webkit-animation: .75s linear infinite spinner;
|
||||||
animation: .75s linear infinite spinner;
|
animation: .75s linear infinite spinner;
|
||||||
-webkit-animation-play-state: inherit;
|
-webkit-animation-play-state: inherit;
|
||||||
@ -39,15 +68,28 @@ html, body {
|
|||||||
border: solid 5px #ffffff;
|
border: solid 5px #ffffff;
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
height: 50px;
|
height: 100%;
|
||||||
width: 50px;
|
width: 100%;
|
||||||
position: fixed;
|
|
||||||
top: 130px;
|
|
||||||
right: 20px;
|
|
||||||
-webkit-transform: translate3d(-50%, -50%, 0);
|
-webkit-transform: translate3d(-50%, -50%, 0);
|
||||||
transform: translate3d(-50%, -50%, 0);
|
transform: translate3d(-50%, -50%, 0);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_report {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 60px;
|
||||||
|
right: 30px;
|
||||||
|
& svg {
|
||||||
|
transition: all 250ms;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
& svg {
|
||||||
|
stroke: #2daa57;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -106,35 +148,46 @@ html, body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
& #content {
|
& #content {
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: calc(100vw - 50px);
|
width: calc(100vw - 50px);
|
||||||
& #code {
|
& #code {
|
||||||
word-break: break-all;
|
white-space: pre;
|
||||||
white-space: pre-wrap;
|
line-height: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
& #input {
|
& #input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
resize: none;
|
resize: none;
|
||||||
font-size: 16px;
|
font: inherit;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& #notifications {
|
& #notifications {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 30px;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px;
|
||||||
& div {
|
& div {
|
||||||
width: 100vw;
|
border-radius: 10px;
|
||||||
padding: 10px 20px;
|
width: 500px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px 30px;
|
||||||
&.error {
|
&.error {
|
||||||
background-color: #ff3d3d;
|
background-color: #ff4d4d;
|
||||||
}
|
}
|
||||||
&.success {
|
&.success {
|
||||||
background-color: #389b38;
|
background-color: #389b38;
|
||||||
}
|
}
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,6 +244,15 @@ html, body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container #notifications {
|
||||||
|
padding: 0;
|
||||||
|
& div {
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
& #flex {
|
& #flex {
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
// apiBase defines the base URL of the API
|
|
||||||
const apiBase = location.protocol + "//" + location.host + "/api/v1";
|
|
||||||
|
|
||||||
// getAPIInformation returns the API information
|
|
||||||
export async function getAPIInformation() {
|
|
||||||
return fetch(apiBase + "/info");
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPaste retrieves a paste
|
|
||||||
export async function getPaste(id) {
|
|
||||||
return fetch(apiBase + "/pastes/" + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// createPaste creates a new paste
|
|
||||||
export async function createPaste(content) {
|
|
||||||
return await fetch(apiBase + "/pastes", {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
content
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// deletePaste deletes a paste
|
|
||||||
export async function deletePaste(id, deletionToken) {
|
|
||||||
return await fetch(apiBase + "/pastes/" + id, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
deletionToken
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
5
web/assets/js/app.js
Normal file
5
web/assets/js/app.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as Spinner from "./modules/spinner.js";
|
||||||
|
import * as State from "./modules/state.js";
|
||||||
|
|
||||||
|
// Initialize the application state
|
||||||
|
Spinner.surround(State.initialize);
|
@ -1,113 +0,0 @@
|
|||||||
// Import the used modules
|
|
||||||
import * as api from "./api.js";
|
|
||||||
import * as buttons from "./buttons.js";
|
|
||||||
import * as spinner from "./spinner.js";
|
|
||||||
import * as notifications from "./notifications.js";
|
|
||||||
|
|
||||||
// Set up the buttons
|
|
||||||
buttons.setupButtons();
|
|
||||||
buttons.setupKeybinds();
|
|
||||||
|
|
||||||
// Define element handles
|
|
||||||
const versionElement = document.getElementById("version");
|
|
||||||
const lineNOsElement = document.getElementById("linenos");
|
|
||||||
const codeElement = document.getElementById("code");
|
|
||||||
const inputElement = document.getElementById("input");
|
|
||||||
|
|
||||||
// Load the API information
|
|
||||||
async function loadAPIInformation() {
|
|
||||||
const response = await api.getAPIInformation();
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.text();
|
|
||||||
notifications.error("Failed fetching the API information: <b>" + data + "</b>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
versionElement.innerText = data.version;
|
|
||||||
}
|
|
||||||
loadAPIInformation();
|
|
||||||
|
|
||||||
// Try to load a paste if one exists
|
|
||||||
export let PASTE_ID;
|
|
||||||
let CODE;
|
|
||||||
async function loadPaste() {
|
|
||||||
if (location.pathname !== "/") {
|
|
||||||
// Define the paste ID and language
|
|
||||||
const split = location.pathname.replace("/", "").split(".");
|
|
||||||
const pasteID = split[0];
|
|
||||||
const language = split[1];
|
|
||||||
|
|
||||||
// Retrieve the paste from the API and redirect the user to the main page if it could not be found
|
|
||||||
const response = await api.getPaste(pasteID);
|
|
||||||
if (!response.ok) {
|
|
||||||
location.replace(location.protocol + "//" + location.host);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CODE = (await response.json()).content;
|
|
||||||
|
|
||||||
// Adjust the button states
|
|
||||||
document.getElementById("btn_save").setAttribute("disabled", true);
|
|
||||||
document.getElementById("btn_delete").removeAttribute("disabled");
|
|
||||||
document.getElementById("btn_copy").removeAttribute("disabled");
|
|
||||||
|
|
||||||
// Set the paste content to the DOM
|
|
||||||
codeElement.innerHTML = language
|
|
||||||
? hljs.highlight(language, CODE).value
|
|
||||||
: hljs.highlightAuto(CODE).value;
|
|
||||||
|
|
||||||
// Display the line numbers
|
|
||||||
renderLineNumbers();
|
|
||||||
window.addEventListener("resize", renderLineNumbers);
|
|
||||||
|
|
||||||
// Set the PASTE_ID variable
|
|
||||||
PASTE_ID = pasteID;
|
|
||||||
} else {
|
|
||||||
inputElement.classList.remove("hidden");
|
|
||||||
inputElement.focus();
|
|
||||||
window.addEventListener("keydown", function (event) {
|
|
||||||
if (event.keyCode != 9) return;
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
insertTextAtCursor(inputElement, " ");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spinner.surround(loadPaste);
|
|
||||||
|
|
||||||
function renderLineNumbers() {
|
|
||||||
lineNOsElement.innerHTML = CODE.split(/\n/).map((line, index) => {
|
|
||||||
let lineWidth = getTextWidth(line, "16px Source Code Pro");
|
|
||||||
let linesSpace = Math.ceil(lineWidth / codeElement.offsetWidth);
|
|
||||||
|
|
||||||
let result = `<span>${index+1}</span>`;
|
|
||||||
if (linesSpace > 1) {
|
|
||||||
result += "<span></span>".repeat(linesSpace - 1);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is
|
|
||||||
function insertTextAtCursor(element, text) {
|
|
||||||
let value = element.value, endIndex, range, doc = element.ownerDocument;
|
|
||||||
if (typeof element.selectionStart == "number"
|
|
||||||
&& typeof element.selectionEnd == "number") {
|
|
||||||
endIndex = element.selectionEnd;
|
|
||||||
element.value = value.slice(0, endIndex) + text + value.slice(endIndex);
|
|
||||||
element.selectionStart = element.selectionEnd = endIndex + text.length;
|
|
||||||
} else if (doc.selection != "undefined" && doc.selection.createRange) {
|
|
||||||
element.focus();
|
|
||||||
range = doc.selection.createRange();
|
|
||||||
range.collapse(false);
|
|
||||||
range.text = text;
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also a kind of skid
|
|
||||||
function getTextWidth(text, font) {
|
|
||||||
let canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
|
|
||||||
let context = canvas.getContext("2d");
|
|
||||||
context.font = font;
|
|
||||||
return context.measureText(text).width;
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
// Import the used modules
|
|
||||||
import * as api from "./api.js";
|
|
||||||
import * as autoload from "./autoload.js";
|
|
||||||
import * as spinner from "./spinner.js";
|
|
||||||
import * as notifications from "./notifications.js";
|
|
||||||
|
|
||||||
// setupKeybinds initializes the keybinds for the buttons
|
|
||||||
export function setupKeybinds() {
|
|
||||||
window.addEventListener("keydown", function (event) {
|
|
||||||
// Return if the CTRL key was not pressed
|
|
||||||
if (!event.ctrlKey) return;
|
|
||||||
|
|
||||||
// Define the DOM element of the pressed button
|
|
||||||
let element = null;
|
|
||||||
switch (event.keyCode) {
|
|
||||||
case 81: {
|
|
||||||
element = document.getElementById("btn_new");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 83: {
|
|
||||||
element = document.getElementById("btn_save");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 88: {
|
|
||||||
element = document.getElementById("btn_delete");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 66: {
|
|
||||||
element = document.getElementById("btn_copy");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the onClick function of the button
|
|
||||||
if (element) {
|
|
||||||
if (element.hasAttribute("disabled")) return;
|
|
||||||
event.preventDefault();
|
|
||||||
element.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupButtons configures the click listeners of the buttons
|
|
||||||
export function setupButtons() {
|
|
||||||
// Define the behavior of the 'new' button
|
|
||||||
document.getElementById("btn_new").addEventListener("click", function () {
|
|
||||||
location.replace(location.protocol + "//" + location.host);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the behavior of the 'save' button
|
|
||||||
document.getElementById("btn_save").addEventListener("click", function () {
|
|
||||||
spinner.surround(async function () {
|
|
||||||
// Return if the text area is empty
|
|
||||||
const input = document.getElementById("input");
|
|
||||||
if (!input.value) return;
|
|
||||||
|
|
||||||
// Create the paste
|
|
||||||
const response = await api.createPaste(input.value);
|
|
||||||
if (!response.ok) {
|
|
||||||
notifications.error("Failed creating the paste: <b>" + await response.text() + "</b>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Give the user the chance to copy the deletion token
|
|
||||||
if (data.deletionToken) {
|
|
||||||
prompt("The deletion token for your paste is:", data.deletionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect the user to the paste page
|
|
||||||
let address = location.protocol + "//" + location.host + "/" + data.id;
|
|
||||||
if (data.suggestedSyntaxType) address += "." + data.suggestedSyntaxType;
|
|
||||||
location.replace(address);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the behavior of the 'delete' button
|
|
||||||
document.getElementById("btn_delete").addEventListener("click", function () {
|
|
||||||
spinner.surround(async function () {
|
|
||||||
// Ask the user for the deletion token
|
|
||||||
const deletionToken = prompt("Deletion Token:");
|
|
||||||
if (!deletionToken) return;
|
|
||||||
|
|
||||||
// Delete the paste
|
|
||||||
const response = await api.deletePaste(autoload.PASTE_ID, deletionToken);
|
|
||||||
const data = await response.text();
|
|
||||||
if (!response.ok) {
|
|
||||||
notifications.error("Failed deleting the paste: <b>" + data + "</b>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect the user to the main page
|
|
||||||
location.replace(location.protocol + "//" + location.host);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the behavior of the 'copy' button
|
|
||||||
document.getElementById("btn_copy").addEventListener("click", function () {
|
|
||||||
spinner.surround(async function () {
|
|
||||||
// Ask for the clipboard permissions
|
|
||||||
askClipboardPermissions();
|
|
||||||
|
|
||||||
// Copy the code
|
|
||||||
await navigator.clipboard.writeText(document.getElementById("code").innerText);
|
|
||||||
notifications.success("Copied the code!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// askClipboardPermissions asks the user for the clipboard permissions
|
|
||||||
async function askClipboardPermissions() {
|
|
||||||
try {
|
|
||||||
const state = await navigator.permissions.query({
|
|
||||||
name: "clipboard-write"
|
|
||||||
});
|
|
||||||
return state === "granted";
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
12
web/assets/js/modules/animation.js
Normal file
12
web/assets/js/modules/animation.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Properly animates an element
|
||||||
|
export function animate(element, animation, duration, after) {
|
||||||
|
element.style.setProperty("--animate-duration", duration);
|
||||||
|
element.classList.add("animate__animated", animation);
|
||||||
|
element.addEventListener("animationend", () => {
|
||||||
|
element.style.removeProperty("--animate-duration");
|
||||||
|
element.classList.remove("animate__animated", animation);
|
||||||
|
if (after) {
|
||||||
|
after();
|
||||||
|
}
|
||||||
|
}, {once: true});
|
||||||
|
}
|
57
web/assets/js/modules/api.js
Normal file
57
web/assets/js/modules/api.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2";
|
||||||
|
|
||||||
|
export async function getAPIInformation() {
|
||||||
|
return fetch(API_BASE_URL + "/info");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPaste(pasteID) {
|
||||||
|
return fetch(API_BASE_URL + "/pastes/" + pasteID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPaste(content, metadata) {
|
||||||
|
return fetch(API_BASE_URL + "/pastes", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editPaste(pasteID, modificationToken, content, metadata) {
|
||||||
|
return fetch(API_BASE_URL + "/pastes/" + pasteID, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": "Bearer " + modificationToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePaste(pasteID, modificationToken) {
|
||||||
|
return fetch(API_BASE_URL + "/pastes/" + pasteID, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + modificationToken,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reportPaste(pasteID, reason) {
|
||||||
|
return fetch(API_BASE_URL + "/pastes/" + pasteID + "/report", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
reason
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
25
web/assets/js/modules/notifications.js
Normal file
25
web/assets/js/modules/notifications.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as Animation from "./animation.js";
|
||||||
|
|
||||||
|
const ELEMENT = document.getElementById("notifications");
|
||||||
|
|
||||||
|
// Shows a success notification
|
||||||
|
export function success(message) {
|
||||||
|
create("success", message, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows an error notification
|
||||||
|
export function error(message) {
|
||||||
|
create("error", message, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new custom notification
|
||||||
|
function create(type, message, duration) {
|
||||||
|
const node = document.createElement("div");
|
||||||
|
node.classList.add(type);
|
||||||
|
Animation.animate(node, "animate__fadeInUp", "0.2s");
|
||||||
|
node.innerHTML = message;
|
||||||
|
|
||||||
|
ELEMENT.childNodes.forEach(child => Animation.animate(child, "animate__slideInUp", "0.2s"));
|
||||||
|
ELEMENT.appendChild(node);
|
||||||
|
setTimeout(() => Animation.animate(node, "animate__fadeOutUp", "0.2s", () => ELEMENT.removeChild(node)), duration);
|
||||||
|
}
|
21
web/assets/js/modules/spinner.js
Normal file
21
web/assets/js/modules/spinner.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as Animation from "./animation.js";
|
||||||
|
|
||||||
|
const ELEMENT = document.getElementById("spinner-container");
|
||||||
|
|
||||||
|
// SHows the spinner
|
||||||
|
export function show() {
|
||||||
|
ELEMENT.classList.remove("hidden");
|
||||||
|
Animation.animate(ELEMENT, "animate__zoomIn", "0.2s");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hides the spinner
|
||||||
|
export function hide() {
|
||||||
|
Animation.animate(ELEMENT, "animate__zoomOut", "0.2s", () => ELEMENT.classList.add("hidden"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surrounds an async action with a spinner
|
||||||
|
export async function surround(innerFunction) {
|
||||||
|
show();
|
||||||
|
await innerFunction();
|
||||||
|
hide();
|
||||||
|
}
|
353
web/assets/js/modules/state.js
Normal file
353
web/assets/js/modules/state.js
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
import * as API from "./api.js";
|
||||||
|
import * as Notifications from "./notifications.js";
|
||||||
|
import * as Spinner from "./spinner.js";
|
||||||
|
import * as Animation from "./animation.js";
|
||||||
|
|
||||||
|
const CODE_ELEMENT = document.getElementById("code");
|
||||||
|
const LINE_NUMBERS_ELEMENT = document.getElementById("linenos");
|
||||||
|
const INPUT_ELEMENT = document.getElementById("input");
|
||||||
|
|
||||||
|
const BUTTONS_DEFAULT_ELEMENT = document.getElementById("buttons_default");
|
||||||
|
const BUTTON_NEW_ELEMENT = document.getElementById("btn_new");
|
||||||
|
const BUTTON_SAVE_ELEMENT = document.getElementById("btn_save");
|
||||||
|
const BUTTON_EDIT_ELEMENT = document.getElementById("btn_edit");
|
||||||
|
const BUTTON_DELETE_ELEMENT = document.getElementById("btn_delete");
|
||||||
|
const BUTTON_COPY_ELEMENT = document.getElementById("btn_copy");
|
||||||
|
|
||||||
|
const BUTTON_REPORT_ELEMENT = document.getElementById("btn_report");
|
||||||
|
|
||||||
|
const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit");
|
||||||
|
const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel");
|
||||||
|
const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply");
|
||||||
|
|
||||||
|
let PASTE_ID;
|
||||||
|
let LANGUAGE;
|
||||||
|
let CODE;
|
||||||
|
|
||||||
|
let EDIT_MODE = false;
|
||||||
|
|
||||||
|
let API_INFORMATION = {
|
||||||
|
version: "error",
|
||||||
|
modificationTokens: false,
|
||||||
|
reports: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initializes the state system
|
||||||
|
export async function initialize() {
|
||||||
|
loadAPIInformation();
|
||||||
|
|
||||||
|
setupButtonFunctionality();
|
||||||
|
setupKeybinds();
|
||||||
|
|
||||||
|
if (location.pathname !== "/") {
|
||||||
|
// Extract the paste data (ID and language)
|
||||||
|
const split = location.pathname.replace("/", "").split(".");
|
||||||
|
const pasteID = split[0];
|
||||||
|
const language = split[1];
|
||||||
|
|
||||||
|
// Try to retrieve the paste data from the API
|
||||||
|
const response = await API.getPaste(pasteID);
|
||||||
|
if (!response.ok) {
|
||||||
|
Notifications.error("Could not load paste: <b>" + await response.text() + "</b>");
|
||||||
|
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the persistent paste data
|
||||||
|
PASTE_ID = pasteID;
|
||||||
|
LANGUAGE = language;
|
||||||
|
CODE = (await response.json()).content;
|
||||||
|
|
||||||
|
// Fill the code block with the just received data
|
||||||
|
updateCode();
|
||||||
|
} else {
|
||||||
|
// Give the user the opportunity to paste his code
|
||||||
|
INPUT_ELEMENT.classList.remove("hidden");
|
||||||
|
INPUT_ELEMENT.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the state of the buttons to match the current state
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the API information
|
||||||
|
async function loadAPIInformation() {
|
||||||
|
// try to retrieve the API information
|
||||||
|
const response = await API.getAPIInformation();
|
||||||
|
if (response.ok) {
|
||||||
|
API_INFORMATION = await response.json();
|
||||||
|
} else {
|
||||||
|
Notifications.error("Failed loading API information: <b>" + await response.text() + "</b>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the API version
|
||||||
|
document.getElementById("version").innerText = API_INFORMATION.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the current persistent code to the code block, highlights it and updates the line numbers
|
||||||
|
function updateCode() {
|
||||||
|
CODE_ELEMENT.innerHTML = LANGUAGE
|
||||||
|
? hljs.highlight(LANGUAGE, CODE).value
|
||||||
|
: hljs.highlightAuto(CODE).value;
|
||||||
|
|
||||||
|
LINE_NUMBERS_ELEMENT.innerHTML = CODE.split(/\n/).map((_, index) => `<span>${index + 1}</span>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the button state according to the current state
|
||||||
|
function updateButtonState() {
|
||||||
|
if (PASTE_ID) {
|
||||||
|
BUTTON_SAVE_ELEMENT.setAttribute("disabled", true);
|
||||||
|
BUTTON_EDIT_ELEMENT.removeAttribute("disabled");
|
||||||
|
BUTTON_DELETE_ELEMENT.removeAttribute("disabled");
|
||||||
|
BUTTON_COPY_ELEMENT.removeAttribute("disabled");
|
||||||
|
|
||||||
|
if (API_INFORMATION.reports) {
|
||||||
|
BUTTON_REPORT_ELEMENT.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BUTTON_SAVE_ELEMENT.removeAttribute("disabled");
|
||||||
|
BUTTON_EDIT_ELEMENT.setAttribute("disabled", true);
|
||||||
|
BUTTON_DELETE_ELEMENT.setAttribute("disabled", true);
|
||||||
|
BUTTON_COPY_ELEMENT.setAttribute("disabled", true);
|
||||||
|
|
||||||
|
if (API_INFORMATION.reports) {
|
||||||
|
BUTTON_REPORT_ELEMENT.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles the edit mode
|
||||||
|
function toggleEditMode() {
|
||||||
|
if (EDIT_MODE) {
|
||||||
|
EDIT_MODE = false;
|
||||||
|
INPUT_ELEMENT.classList.add("hidden");
|
||||||
|
CODE_ELEMENT.classList.remove("hidden");
|
||||||
|
Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeOutDown", "0.3s", () => {
|
||||||
|
BUTTONS_EDIT_ELEMENT.classList.add("hidden");
|
||||||
|
BUTTONS_DEFAULT_ELEMENT.classList.remove("hidden");
|
||||||
|
Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeInDown", "0.3s");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
EDIT_MODE = true;
|
||||||
|
CODE_ELEMENT.classList.add("hidden");
|
||||||
|
INPUT_ELEMENT.classList.remove("hidden");
|
||||||
|
INPUT_ELEMENT.value = CODE;
|
||||||
|
INPUT_ELEMENT.focus();
|
||||||
|
Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeOutUp", "0.3s", () => {
|
||||||
|
BUTTONS_DEFAULT_ELEMENT.classList.add("hidden");
|
||||||
|
BUTTONS_EDIT_ELEMENT.classList.remove("hidden");
|
||||||
|
Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeInUp", "0.3s");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the keybinds for the buttons
|
||||||
|
function setupKeybinds() {
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
// All keybinds in the default button set include the CTRL key
|
||||||
|
if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the DOM element of the button to trigger
|
||||||
|
let element;
|
||||||
|
if (EDIT_MODE) {
|
||||||
|
switch (event.code) {
|
||||||
|
case "Escape": {
|
||||||
|
element = BUTTON_EDIT_CANCEL_ELEMENT;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "KeyS": {
|
||||||
|
element = BUTTON_EDIT_APPLY_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (event.code) {
|
||||||
|
case "KeyQ": {
|
||||||
|
element = BUTTON_NEW_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "KeyS": {
|
||||||
|
element = BUTTON_SAVE_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "KeyO": {
|
||||||
|
element = BUTTON_EDIT_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "KeyX": {
|
||||||
|
element = BUTTON_DELETE_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "KeyB": {
|
||||||
|
element = BUTTON_COPY_ELEMENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the found button
|
||||||
|
if (element) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (element.hasAttribute("disabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additionally fix the behaviour of the Tab key
|
||||||
|
window.addEventListener("keydown", (event) => {
|
||||||
|
if (event.code != "Tab") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
insertTextAtCursor(inputElement, " ");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets up the different button functionalities
|
||||||
|
function setupButtonFunctionality() {
|
||||||
|
BUTTON_NEW_ELEMENT.addEventListener("click", () => location.replace(location.protocol + "//" + location.host));
|
||||||
|
|
||||||
|
BUTTON_SAVE_ELEMENT.addEventListener("click", () => {
|
||||||
|
Spinner.surround(async () => {
|
||||||
|
// Only proceed if the input is not empty
|
||||||
|
if (!INPUT_ELEMENT.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create the paste
|
||||||
|
const response = await API.createPaste(INPUT_ELEMENT.value);
|
||||||
|
if (!response.ok) {
|
||||||
|
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Display the modification token if provided
|
||||||
|
if (data.modificationToken) {
|
||||||
|
prompt("The modification token for your paste is:", data.modificationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect the user to his newly created paste
|
||||||
|
location.replace(location.protocol + "//" + location.host + "/" + data.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
BUTTON_EDIT_ELEMENT.addEventListener("click", toggleEditMode);
|
||||||
|
|
||||||
|
BUTTON_DELETE_ELEMENT.addEventListener("click", () => {
|
||||||
|
Spinner.surround(async () => {
|
||||||
|
// Ask for the modification token
|
||||||
|
const modificationToken = prompt("Modification token:");
|
||||||
|
if (!modificationToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to delete the paste
|
||||||
|
const response = await API.deletePaste(PASTE_ID, modificationToken);
|
||||||
|
if (!response.ok) {
|
||||||
|
Notifications.error("Error while deleting paste: <b>" + await response.text() + "</b>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect the user to the start page
|
||||||
|
location.replace(location.protocol + "//" + location.host);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
BUTTON_COPY_ELEMENT.addEventListener("click", async () => {
|
||||||
|
// Ask for clipboard permissions
|
||||||
|
if (!(await askForClipboardPermission())) {
|
||||||
|
Notifications.error("Clipboard permission denied.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the current code
|
||||||
|
await navigator.clipboard.writeText(CODE);
|
||||||
|
Notifications.success("Successfully copied the code.");
|
||||||
|
});
|
||||||
|
|
||||||
|
BUTTON_EDIT_CANCEL_ELEMENT.addEventListener("click", toggleEditMode);
|
||||||
|
|
||||||
|
BUTTON_EDIT_APPLY_ELEMENT.addEventListener("click", async () => {
|
||||||
|
// Only proceed if the input is not empty
|
||||||
|
if (!INPUT_ELEMENT.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for the modification token
|
||||||
|
const modificationToken = prompt("Modification token:");
|
||||||
|
if (!modificationToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to edit the paste
|
||||||
|
const response = await API.editPaste(PASTE_ID, modificationToken, INPUT_ELEMENT.value);
|
||||||
|
if (!response.ok) {
|
||||||
|
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the code and leave the edit mode
|
||||||
|
CODE = INPUT_ELEMENT.value;
|
||||||
|
updateCode();
|
||||||
|
toggleEditMode();
|
||||||
|
Notifications.success("Successfully edited paste.");
|
||||||
|
});
|
||||||
|
|
||||||
|
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
|
||||||
|
// Ask the user for a reason
|
||||||
|
const reason = prompt("Reason:");
|
||||||
|
if (!reason) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to report the paste
|
||||||
|
const response = await API.reportPaste(PASTE_ID, reason);
|
||||||
|
if (!response.ok) {
|
||||||
|
Notifications.error("Error while reporting paste: <b>" + await response.text() + "</b>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the response message
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
Notifications.error("Error while reporting paste: <b>" + data.message + "</b>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Notifications.success(data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asks for clipboard write permission
|
||||||
|
async function askForClipboardPermission() {
|
||||||
|
try {
|
||||||
|
const state = await navigator.permissions.query({
|
||||||
|
name: "clipboard-write"
|
||||||
|
});
|
||||||
|
return state.state === "granted" || state.state === "prompt";
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is
|
||||||
|
function insertTextAtCursor(element, text) {
|
||||||
|
let value = element.value, endIndex, range, doc = element.ownerDocument;
|
||||||
|
if (typeof element.selectionStart == "number"
|
||||||
|
&& typeof element.selectionEnd == "number") {
|
||||||
|
endIndex = element.selectionEnd;
|
||||||
|
element.value = value.slice(0, endIndex) + text + value.slice(endIndex);
|
||||||
|
element.selectionStart = element.selectionEnd = endIndex + text.length;
|
||||||
|
} else if (doc.selection != "undefined" && doc.selection.createRange) {
|
||||||
|
element.focus();
|
||||||
|
range = doc.selection.createRange();
|
||||||
|
range.collapse(false);
|
||||||
|
range.text = text;
|
||||||
|
range.select();
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
// element holds the notification containers DOM element
|
|
||||||
const element = document.getElementById("notifications");
|
|
||||||
|
|
||||||
// error shows an error notifications
|
|
||||||
export function error(message) {
|
|
||||||
create("error", message, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// success shows a success notifications
|
|
||||||
export function success(message) {
|
|
||||||
create("success", message, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create creates a new notification
|
|
||||||
function create(type, message, duration) {
|
|
||||||
const node = document.createElement("div");
|
|
||||||
node.classList.add(type);
|
|
||||||
node.innerHTML = message;
|
|
||||||
|
|
||||||
element.appendChild(node);
|
|
||||||
setTimeout(() => element.removeChild(node), duration);
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
// element holds the spinners DOM element
|
|
||||||
const element = document.getElementById("spinner");
|
|
||||||
|
|
||||||
// show shows the spinner
|
|
||||||
export function show() {
|
|
||||||
element.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide hides the spinner
|
|
||||||
export function hide() {
|
|
||||||
element.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// surround surrounds an action with a spinner
|
|
||||||
export async function surround(action) {
|
|
||||||
show();
|
|
||||||
await action();
|
|
||||||
hide();
|
|
||||||
}
|
|
@ -7,12 +7,24 @@
|
|||||||
<title>pasty</title>
|
<title>pasty</title>
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/solarized-dark.min.css">
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.2/styles/solarized-dark.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="spinner" class="hidden"></div>
|
<div id="spinner-container" class="hidden"><div class="spinner"></div></div>
|
||||||
|
<div id="btn_report" class="hidden" title="Report paste">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-flag" width="40"
|
||||||
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<line x1="5" y1="5" x2="5" y2="21" />
|
||||||
|
<line x1="19" y1="5" x2="19" y2="14" />
|
||||||
|
<path d="M5 5a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
|
||||||
|
<path d="M5 14a5 5 0 0 1 7 0a5 5 0 0 0 7 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<div class="buttons">
|
<div class="buttons" id="buttons_default">
|
||||||
<button class="button" id="btn_new" title="Create new paste (Ctrl + Q)">
|
<button class="button" id="btn_new" title="Create new paste (Ctrl + Q)">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-plus" width="40"
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-plus" width="40"
|
||||||
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
@ -33,6 +45,16 @@
|
|||||||
<polyline points="14 4 14 8 8 8 8 4" />
|
<polyline points="14 4 14 8 8 8 8 4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="button" id="btn_edit" title="Edit paste (Ctrl + O)" disabled>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-edit" width="40"
|
||||||
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M9 7h-3a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-3" />
|
||||||
|
<path d="M9 15h3l8.5 -8.5a1.5 1.5 0 0 0 -3 -3l-8.5 8.5v3" />
|
||||||
|
<line x1="16" y1="5" x2="19" y2="8" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="button" id="btn_delete" title="Delete paste (Ctrl + X)" disabled>
|
<button class="button" id="btn_delete" title="Delete paste (Ctrl + X)" disabled>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="40"
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="40"
|
||||||
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
@ -55,6 +77,24 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons hidden" id="buttons_edit">
|
||||||
|
<button class="button" id="btn_edit_cancel" title="Cancel (Escape)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-back-up" width="40"
|
||||||
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M9 13l-4 -4l4 -4m-4 4h11a4 4 0 0 1 0 8h-1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="button" id="btn_edit_apply" title="Apply changes (Ctrl + S)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="40"
|
||||||
|
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M5 12l5 5l10 -10" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="notifications"></div>
|
<div id="notifications"></div>
|
||||||
@ -78,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.min.js"></script>
|
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.min.js"></script>
|
||||||
<script src="assets/js/autoload.js" type="module"></script>
|
<script src="assets/js/app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user