nullboard/nullboard.html

5186 lines
103 KiB
HTML
Raw Normal View History

<!doctype html>
2019-05-28 12:09:54 +02:00
<html>
<head>
<!--
2019-05-28 12:09:54 +02:00
+-------------------------------------------------------------+
2019-05-28 12:42:04 +02:00
| |
| Nullboard, a minimalist kanban board |
| https://nullboard.io |
| |
+-------------------------------------------------------------+
2019-05-28 12:09:54 +02:00
LICENSE
The 2-clause BSD license with the Commons Clause condition.
IN SHORT
You can use, change and distribute the software free of charge
provided that you do not sell it or make money off it in
certain less direct ways as specified below. When distributed,
the software must include a complete copy of this license.
IN FULL
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant of
rights under the License will not include, and the License does
not grant to you, the right to Sell the Software.
For purposes of the foregoing, "Sell" means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required
by the License must also include this Commons Clause License
Condition notice.
Software: Nullboard
License: The 2-clause BSD License
Licensor: Alexander Pankratov / swapped.ch
-->
<meta charset="utf-8">
2023-11-01 22:37:05 -05:00
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nullboard</title>
2021-12-10 17:53:00 +01:00
<link rel="icon" href="extras/favicon-16.png"/>
<style>
/***/
@font-face {
font-family: 'f-barlow';
font-weight: 400;
font-style: normal;
src: url('extras/Barlow-Regular.woff') format('woff');
}
2019-05-28 12:09:54 +02:00
@font-face {
font-family: 'f-barlow';
font-weight: 500;
font-style: normal;
src: url('extras/Barlow-Medium.woff') format('woff');
}
2019-06-10 13:19:05 +02:00
/***/
@font-face {
font-family: 'f-ibm-plex';
font-weight: 400;
font-style: normal;
src: url('extras/IBMPlexSans-Regular.woff') format('woff');
}
@font-face {
font-family: 'f-ibm-plex';
font-weight: 500;
font-style: normal;
src: url('extras/IBMPlexSans-Medium.woff') format('woff');
}
/***/
@font-face {
font-family: 'f-open-sans';
font-weight: 400;
font-style: normal;
src: url('extras/OpenSans-Regular.woff') format('woff');
}
@font-face {
font-family: 'f-open-sans';
font-weight: 500;
font-style: normal;
src: url('extras/OpenSans-600.woff') format('woff');
}
/***/
@font-face {
font-family: 'f-segoe-ui';
font-weight: 400;
font-style: normal;
src: local('Segoe UI');
}
@font-face {
font-family: 'f-segoe-ui';
font-weight: 500;
font-style: normal;
src: local('Segoe UI Semibold');
}
/***/
@font-face {
font-family: 'f-maven-pro';
font-weight: 400;
font-style: normal;
src: url('extras/MavenPro-Regular.woff') format('woff');
}
@font-face {
font-family: 'f-maven-pro';
font-weight: 500;
font-style: normal;
src: url('extras/MavenPro-500.woff') format('woff');
}
/***/
html {
--ff: f-barlow, sans-serif;
2021-04-07 13:56:10 +02:00
--fs: 11; /* font-size */
--lh: calc( var(--fs) * 1.25 ); /* line-height */
--fs-head: 13.6; /* font-size of board heading */
--fs-nops: 9; /* font-size of the note ops */
--lw: calc(20 * var(--fs) + 30); /* .list width */
}
html, body, input, textarea {
font-family: var(--ff);
font-size: calc( var(--fs) * 1px );
line-height: calc( var(--lh) * 1px );
}
/***/
html.f-ibm-plex {
--ff: f-ibm-plex, sans-serif;
--fs: 11;
--fs-head: 12.5;
--lw: calc(20 * var(--fs) + 40); /* .list width */
}
/***/
html.f-open-sans {
--ff: f-open-sans, sans-serif;
--fs: 11;
--fs-head: 12.5;
--lw: calc(20 * var(--fs) + 50); /* .list width */
}
/***/
html.f-segoe-ui {
--ff: f-segoe-ui, sans-serif;
--fs: 11;
--fs-head: 13;
--fs-nops: 8;
2021-04-06 13:21:48 +02:00
--lw: calc(22 * var(--fs) + 40); /* .list width */
}
2021-04-07 13:56:10 +02:00
/***/
html.f-maven-pro {
--ff: f-maven-pro, sans-serif;
--fs: 12;
--fs-head: 13;
--lw: calc(20 * var(--fs) + 47); /* .list width */
}
/***/
html, body, h1, textarea, input {
padding: 0;
margin: 0;
}
body {
background: #fff;
background: #f8f9fb;
}
/***/
2021-03-31 18:27:33 +02:00
@keyframes shake {
33% { margin-left: -5px; }
66% { margin-left: 5px; }
100% { margin-left: 0px; }
}
a {
text-decoration: none;
transition: color 200ms;
}
a, a:active, a:focus, textarea, input {
outline: none;
}
tt {
display: none;
}
/***/
2021-04-06 19:32:46 +02:00
.ding {
animation-name: shake;
animation-duration: 200ms;
}
.no-user-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
2022-08-31 13:33:56 -05:00
/***/
.clearfix:after,
.board:after,
.lists:after,
.notes:after,
.head:after,
.menu:after {
content: "";
display: table;
clear: both;
}
/***/
body.dragging {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/***/
.board {
2021-04-06 19:54:47 +02:00
min-width: calc( var(--lw) * 1px );
width: -moz-max-content; /* firefox */
width: -webkit-max-content; /* chrome */
width: intrinsic; /* safari */
margin: 0 auto;
padding: 20px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.crowded .board {
margin-top: 28px;
}
.board u {
text-decoration: none;
}
.board u:before {
content: '\00D7';
position: relative;
top: 2px;
font-size: calc(16rem / 11);
line-height: calc(10 / 17);
font-weight: 400;
}
/***/
.board .head {
padding: 5px 0;
margin: 1px 5px 0;
position: relative;
}
.board .head .text,
.board .head .edit {
font-weight: 500;
font-size: calc( var(--fs-head) / 11 * 1rem );
line-height: calc( 20 / var(--fs-head) );
padding: 0 5px 2px;
border: none;
}
.board .head .text {
min-height: 20px;
white-space: pre;
overflow: hidden;
}
.board .head .edit {
display: none;
}
.board .head .edit::placeholder {
font-weight: 400;
font-size: calc(10rem / 11);
line-height: calc(22 / 10);
text-transform: uppercase;
color: #1489db;
2020-02-19 14:25:11 +01:00
opacity: 0.8;
}
.board .head.editing .text {
display: none;
}
.board .head.editing .edit {
display: block;
outline: 1px solid #8eaedd;
}
.board .menu {
position: absolute;
top: 0;
right: 0;
height: 20px;
padding: 5px 6px 7px 30px;
background: linear-gradient(to right, #EAEDF000, #EAEDF0 10px);
line-height: calc(20 / 11);
}
.board .menu a,
.board .ops a {
color: #000000A0;
transition: color 200ms;
}
.board .menu a {
padding-left: 10px;
}
.board .menu a:hover,
.board .ops a:hover {
color: #000;
}
.board .menu a.warn:hover,
.board .ops a.warn:hover {
color: #c40;
}
.board .menu .undo-board,
.board .menu .redo-board {
display: none;
}
.board .head.editing .menu {
display: none;
}
.board > .head {
background: #EAEDF0;
padding: 5px;
margin: 0 0 10px;
border-radius: 2px;
position: relative;
}
.board > .head .menu {
margin-right: 5px;
}
/***/
.board .lists-scroller {
height: auto;
margin: -1px 0 10px;
overflow-x: auto;
overflow-y: hidden;
display: none;
}
.lists-scroller div {
height: 1px;
}
.board .lists {
white-space: nowrap;
overflow: auto;
scrollbar-width: none;
}
.board .list {
display: inline-block;
vertical-align: top;
width: calc( var(--lw) * 1px );
margin: 0 5px 10px;
background: linear-gradient(#EAEDF0 30px, #DDE1E5 90px);
border-radius: 2px;
}
.board .list::-webkit-scrollbar {
display: none;
}
.board .list:first-child {
margin-left: 0;
}
.board .list:last-child {
margin-right: 0;
}
.board .list .notes {
padding: 0 5px;
}
/***/
.board .head .menu .teaser {
position: absolute;
right: 3px;
top: 5px;
padding: 0 3px;
}
.board .head .menu .bulk {
display: none;
opacity: 0;
z-index: 1;
}
.board .head .menu:hover .bulk {
display: block;
opacity: 1;
}
.board .head .menu:hover .teaser {
display: none;
}
/***/
.board .list .menu .mov-list-r.half {
padding-left: 0;
}
.board .list .menu .full {
display: none;
}
.board .list:first-child .menu .half,
.board .list:last-child .menu .half {
display: none;
}
.board .list:first-child .menu .mov-list-r.full,
.board .list:last-child .menu .mov-list-l.full {
display: inline-block;
}
.board .list:first-child:last-child .menu .half,
.board .list:first-child:last-child .menu .full {
display: none;
}
/***/
.board .note {
background: #fff;
margin-top: 5px;
box-shadow: 0 1px 2px #bbb, 0 0 1px #ddd;
position: relative;
}
.board .note.dragging,
.board .note.dragging.raw {
background: #CED4DA;
box-shadow: 0 +1px 0 #0001 inset,
0 -1px 0 #0001 inset,
+1px 0 0 #0001 inset,
-1px 0 0 #0001 inset;
}
.board .note.dragging * {
opacity: 0 !important;
}
/***/
.board .note:last-child {
margin-bottom: 5px;
}
.board .note {
padding-bottom: 6px;
}
.board .note .text,
.board .note .edit {
padding: 5px 10px 0;
margin-right: 15px;
min-height: 100%;
}
.board .note .text {
white-space: pre-wrap;
overflow-wrap: anywhere;
min-height: calc( var(--lh) * 1px );
}
2020-02-20 01:13:00 +01:00
/***/
.board .head .text a,
.board .note .text a {
2020-02-20 01:13:00 +01:00
color: inherit;
cursor: default;
transition: none;
}
@keyframes whoomp {
0% { color: inherit; }
30% { color: #888; }
100% { color: inherit; }
}
.board .head .text a:hover,
.board .note .text a:hover {
2020-02-20 01:13:00 +01:00
animation-name: whoomp;
animation-duration: 700ms;
}
.reveal .board .head .text a,
.reveal .board .note .text a {
2020-02-20 01:13:00 +01:00
color: #1489db;
cursor: pointer;
}
.reveal .board .head .text a:hover,
.reveal .board .note .text a:hover {
2020-02-20 01:13:00 +01:00
animation-name: none;
}
/***/
.board .note .edit {
display: none;
border: none;
}
.board .note.editing {
box-shadow: none;
outline: 1px solid #8eaedd;
}
.board .note.editing .text {
display: none;
}
.board .note.editing .edit {
display: block;
resize: none;
}
/***/
.board .note .ops {
position: absolute;
top: 0;
right: 0;
opacity: 0;
transition: opacity 400ms;
cursor: default;
font-size: calc(9rem / 11);
}
.board .note.editing .ops {
display: none;
}
.board .note:hover .ops {
opacity: 1;
}
.board .note .ops .teaser {
display: block;
margin-top: 2px;
margin-right: 1px;
padding-right: 3px;
}
.board .note .ops .teaser:before {
content: '\2261';
}
.board .note .ops .bulk {
display: none;
background: #fff;
border-left: none;
padding: 1px 0 2px 5px;
font-size: calc( var(--fs-nops) * 1px );
font-weight: 500;
border-left: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.board .note .ops .bulk a {
padding-right: 4px;
}
.board .note .ops:hover .bulk {
display: block;
}
.board .note .ops:hover .teaser {
display: none;
}
/***/
.board .note.raw {
background: transparent;
box-shadow: none;
font-weight: 500;
}
.board .note.raw.editing {
background: #fff;
}
.board .note.raw .text {
}
/***/
.board .note.collapsed {
padding-bottom: 6px;
}
.board .note.collapsed .text,
.note-dragster.collapsed .text {
max-height: calc( var(--lh) * 1px );
overflow: hidden;
padding-bottom: 0;
}
.board .note.collapsed .ops {
opacity: 1;
}
.board .note.collapsed .ops .teaser {
padding: 1px 3px 0 1px;
}
.board .note.collapsed .ops .teaser:before {
content: '_';
top: 1px;
}
.board .note.collapsed:hover .ops .teaser:before {
content: '\2261';
}
/***/
2021-04-02 01:59:34 +02:00
.note-dragster {
z-index: 2;
position: absolute;
opacity: 0;
background: #fff;
white-space: pre-wrap;
cursor: move;
padding: 5px 25px 6px 10px;
box-shadow: 0 +1px 0 #ACB4BC inset,
0 -1px 0 #ACB4BC inset,
+1px 0 0 #ACB4BC inset,
-1px 0 0 #ACB4BC inset,
0px 1px 3px #00000030;
}
2021-04-02 01:59:34 +02:00
.note-dragster a {
2020-02-20 01:13:00 +01:00
color: #000;
}
/***/
.logo {
position: absolute;
top: 9px;
left: 9px;
line-height: calc(19 / 11);
padding: 6px 12px;
z-index: 3;
opacity: 0.6;
background: #f8f9fb;
}
.crowded .logo:hover {
background: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.logo:hover {
opacity: 1.0;
}
.logo .alert {
display: none;
font-style: normal;
margin-left: 5px;
color: #d20;
}
.logo .bulk {
overflow: hidden;
height: 0;
opacity: 0;
transition: opacity 400ms;
padding-top: 1px;
}
.logo:hover .bulk {
height: auto;
opacity: 1;
}
.logo a.site,
.logo .bulk a {
color: #000;
}
.logo .bulk a:hover {
color: #1489db;
}
.logo .bulk a {
display: block;
}
.logo .bulk a:before {
display: inline-block;
content: '-';
width: 11px;
}
/***/
.logo.updated {
opacity: 1;
}
.logo.updated .alert {
display: inline-block;
}
.logo.updated .bulk .view-changes {
color: #d20;
}
/***/
.config {
position: absolute;
top: 10px;
right: 21px;
line-height: calc(19 / 11);
z-index: 3;
background: #f8f9fb;
}
2021-04-08 13:24:35 +02:00
.crowded .config:hover,
.crowded.adjusting .config {
background: #fff;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.config a {
display: block;
color: #000;
transition: color 200ms;
}
.config a:hover {
color: #1489db;
}
.config .teaser {
padding: 5px;
color: #999;
2021-04-14 13:54:03 +02:00
position: relative;
}
2021-04-14 17:00:49 +02:00
.config .teaser i {
/* backups off */
font-style: normal;
}
2021-04-14 13:54:03 +02:00
.config .teaser u {
2021-04-14 17:00:49 +02:00
/* backups on */
2021-04-14 13:54:03 +02:00
display: none;
text-decoration: none;
}
.config .bulk {
margin-right: 20px;
padding: 5px 0 0 22px;
transition: opacity 400ms;
}
.config .bulk .boards {
display: none;
padding: 6px 2px;
margin: 6px -2px;
border-top: 1px solid #00000028;
border-bottom: 1px solid #00000028;
}
.config .bulk .boards a.load-board {
display: block;
padding-left: 5px;
margin-left: -5px;
transition: none;
}
.config .bulk .boards a.load-board.active {
color: #1489db;
}
.config .bulk .boards a.load-board.active:before {
content: '\2022 ';
display: inline-block;
width: 13px;
margin-left: -13px;
}
.config .bulk .boards a.load-board.active:hover {
2021-04-02 01:59:34 +02:00
color: #000;
}
.config .bulk .boards a.load-board.dragging {
2021-04-02 01:59:34 +02:00
background: #DDE1E5;
border-radius: 2px;
color: transparent;
transition: none;
box-shadow: 0 0 1px #0002 inset;
2021-04-02 01:59:34 +02:00
}
.config .bulk .boards a.load-board.dragging.active:before {
content: '';
}
2021-04-02 01:59:34 +02:00
.load-dragster {
z-index: 10;
position: absolute;
opacity: 0;
line-height: calc(19 / 11);
2021-04-02 01:59:34 +02:00
padding-left: 5px;
background: #fff;
color: #000;
2021-04-02 01:59:34 +02:00
border-radius: 2px;
cursor: move;
outline: 1px solid #ACB4BC;
box-shadow: 0px 1px 3px #00000030;
}
2020-02-19 14:25:11 +01:00
.config .bulk a i,
.config .bulk a b {
font-style: normal;
font-weight: inherit;
}
.config .bulk a i {
display: none;
}
2021-04-14 13:54:03 +02:00
.config .bulk .break {
padding: 6px 2px 0;
margin: 6px -2px 0;
border-top: 1px solid #00000028;
}
/***/
.config .bulk input.imp-board-select {
position: absolute;
width: 1px;
height: 1px;
visibility: hidden;
}
2021-04-14 13:54:03 +02:00
/***/
2021-04-14 17:00:49 +02:00
.config.backups-on .teaser i {
display: none;
}
2021-04-14 13:54:03 +02:00
.config.backups-on .teaser u {
display: inline-block;
font-size: calc(8rem / 11);
color: #555;
}
2021-04-14 17:00:49 +02:00
.config.backups-on.backing-up .teaser u {
2021-04-14 13:54:03 +02:00
opacity: 0.4;
}
.config.backups-on .bulk .auto-backup:before {
content: '\2713 ';
display: inline-block;
width: 15px;
margin-left: -15px;
}
/***/
2021-04-14 17:00:49 +02:00
.config.backups-on.backup-err .teaser i {
display: inline-block;
2021-04-14 13:54:03 +02:00
}
.config.backups-on.backup-err .teaser u {
display: inline-block;
position: absolute;
width: 4px;
height: 4px;
border-radius: 10px;
top: 13px;
right: -4px;
background: #d20;
2021-04-14 17:00:49 +02:00
color: transparent;
2021-04-14 13:54:03 +02:00
}
.config.backups-on.backup-err .bulk .auto-backup {
color: #d20;
}
.config.backups-on.backup-err .bulk .auto-backup:before {
content: '! ';
color: #d20;
width: 13px;
margin-left: -13px;
2020-02-19 14:25:11 +01:00
}
/***/
2021-04-06 16:51:20 +02:00
.config .bulk .section .title {
text-align: center;
width: 0;
display: inline-block;
white-space: pre;
2021-04-06 16:51:20 +02:00
transition: width 250ms, padding 250ms;
}
2021-04-06 20:21:08 +02:00
.config .bulk .section .title u {
text-decoration: none;
2021-04-06 16:51:20 +02:00
transition: opacity 100ms;
}
2021-04-06 16:51:20 +02:00
.config .bulk .section .details {
height: 0;
opacity: 0;
transition: opacity 250ms;
padding-right: 8px;
margin-right: -8px;
}
2021-04-06 13:21:48 +02:00
/***/
2021-04-06 16:51:20 +02:00
.config .bulk .section.open {
padding-bottom: 8px;
border-bottom: 1px solid #00000028;
}
2021-04-06 19:54:47 +02:00
.crowded .config .bulk .section.open {
border-bottom: none;
}
2021-04-06 16:51:20 +02:00
.config .bulk .section.open .title {
2021-04-06 13:21:48 +02:00
width: 100%;
font-weight: 500;
2021-04-06 16:51:20 +02:00
padding-left: 5px;
transition: width 250ms, padding 250ms;
2021-04-06 13:21:48 +02:00
}
2021-04-06 20:21:08 +02:00
.config .bulk .section.open .title u {
2021-04-06 16:51:20 +02:00
opacity: 0;
transition: opacity 100ms;
2021-04-06 13:21:48 +02:00
}
2021-04-06 16:51:20 +02:00
.config .bulk .section.open .details {
height: 100%;
2021-04-06 13:21:48 +02:00
opacity: 1;
transition: opacity 250ms;
}
/***/
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs table {
width: 10px; /* mobile safari */
padding: 0;
margin: 0;
border-collapse: collapse;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs td {
margin: 0;
padding: 0 0 0 5px;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs td.name {
width: 100%;
white-space: pre;
padding-left: 0;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs tr td.val {
text-align: center;
}
2021-04-06 18:37:26 +02:00
.fs-set .config .bulk .ui-prefs .f-prefs tr.ui-fs td.val,
2021-04-06 19:32:46 +02:00
.lh-set .config .bulk .ui-prefs .f-prefs tr.ui-lh td.val,
.lw-set .config .bulk .ui-prefs .f-prefs tr.ui-lw td.val {
2021-04-06 13:21:48 +02:00
color: #1489db;
cursor: pointer;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs a {
display: inline-block;
padding: 0 2px;
transition: padding 250ms;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs a.less:hover {
padding: 0 4px 0 0;
transition: padding 250ms;
}
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .f-prefs a.more:hover {
padding: 0 0 0 4px;
transition: padding 250ms;
}
.config .bulk .ui-prefs .f-prefs a.more:hover {
padding: 0 0 0 4px;
transition: padding 250ms;
}
/***/
2021-04-06 16:51:20 +02:00
.config .bulk .ui-prefs .switch-font.active:before {
content: '\2022 ';
display: inline-block;
width: 13px;
margin-left: -13px;
}
/***/
.config .bulk {
display: none;
opacity: 0;
}
2021-04-08 13:24:35 +02:00
.config:hover .teaser,
.adjusting .config .teaser {
display: none;
}
2021-04-08 13:24:35 +02:00
.config:hover .bulk,
.adjusting .config .bulk {
display: block;
opacity: 1;
}
2021-04-07 18:51:52 +02:00
/***/
.adjusting * {
cursor: row-resize !important;
}
/***/
.overlay {
position: fixed;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
color: #000;
2021-04-18 13:55:59 +02:00
display: none; /* flex */
justify-content: center;
}
2021-04-18 13:55:59 +02:00
.overlay > div {
font-size: calc(12rem / 11);
line-height: calc(16 / 12);
2021-04-18 13:55:59 +02:00
background: #fff;
align-self: start;
margin-top: 100px;
border-radius: 1px;
box-shadow: 0 2px 6px #000;
}
/***/
.overlay > div.license {
white-space: pre-wrap;
padding: 20px 25px 22px;
overflow-y: auto;
width: calc( (var(--fs) * 36 + 6) * 1px ); /* 402px, 476px */
}
2021-04-18 13:55:59 +02:00
.overlay > div.license a {
color: #1489db;
}
2021-04-18 13:55:59 +02:00
.overlay > div.license span {
display: inline-block;
padding-right: 8px;
}
/***/
2021-04-18 13:55:59 +02:00
.overlay > div.about {
text-align: center;
2021-04-18 13:55:59 +02:00
padding: 50px 50px;
2021-08-04 12:44:59 +02:00
position: relative;
}
2021-04-18 13:55:59 +02:00
.overlay > div.about h1 {
font-size: calc(15rem / 11);
font-weight: 500;
margin-bottom: 10px;
}
2021-04-18 13:55:59 +02:00
.overlay > div.about a {
display: block;
color: #1489db;
}
2021-04-18 13:55:59 +02:00
.overlay > div.about div {
position: absolute;
bottom: -30px;
2021-08-04 12:44:59 +02:00
left: 0;
width: 100%;
color: #fff9;
}
2021-04-18 13:55:59 +02:00
.overlay > div.about div span {
display: inline-block;
text-align: left;
padding-right: 10px;
}
/***/
2021-04-18 13:55:59 +02:00
.overlay .backup-conf {
padding: 20px 40px;
min-width: 300px;
2021-04-19 17:52:26 +02:00
position: relative;
}
.overlay .backup-conf .close {
position: absolute;
top: 0px;
right: 12px;
font-size: calc(22rem / 11);
color: #000;
opacity: 0.3;
transition: opacity 300ms;
}
.overlay .backup-conf .close:hover {
opacity: 1;
transition: opacity 300ms;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf h3 {
font-size: calc(15rem / 11);
font-weight: 500;
text-align: center;
margin: 20px 0 0 0;
}
.overlay .backup-conf .what {
display: block;
text-align: center;
border-bottom: 1px solid #888;
margin: 0 -10px 20px;
padding: 0 10px 30px;
}
.overlay .backup-conf .what a {
color: #1489db;
}
.overlay .backup-conf .opt {
margin-top: 5px;
}
.overlay .backup-conf .opt:before {
--col: #333;
content: ' ';
display: inline-block;
margin-top: -1px;
display: inline-block;
content: ' ';
width: calc(9rem / 11);
height: calc(9rem / 11);
margin-right: 8px;
position: relative;
top: 1px;
background: var(--col);
border: 1px solid var(--col);
border-radius: 1px;
box-shadow: 0 2px 0 #fff inset, 0 -2px 0 #fff inset, 2px 0 0 #fff inset, -2px 0 0 #fff inset;
}
.overlay .backup-conf .opt:hover:before {
--col: #1489db;
}
.overlay .backup-conf .off .opt:before {
background: #fff;
}
.overlay .backup-conf .opt:hover {
color: #1489db;
}
.overlay .backup-conf .opt {
cursor: pointer;
}
.overlay .backup-conf .etc {
2021-04-19 17:52:26 +02:00
margin-top: 3px;
2021-04-18 13:55:59 +02:00
margin-bottom: 10px;
2021-04-18 22:36:39 +02:00
margin-left: calc( 9rem / 22 );
2021-04-19 17:52:26 +02:00
padding: 2px 0 0 14px;
2021-04-18 22:36:39 +02:00
border-left: 1px solid #888;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf .off .etc {
display: none;
}
.overlay .backup-conf .etc span {
display: inline-block;
min-width: calc( 3px + var(--fs) * 7px );
margin-right: calc( var(--fs) * 1px );
}
.overlay .backup-conf .etc input {
display: inline-block;
width: 170px;
padding: 0 5px;
line-height: calc(20 / 11);
margin-bottom: 3px;
2021-04-18 22:36:39 +02:00
border: 1px solid #ccc;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf .etc input:focus {
2021-04-18 22:36:39 +02:00
border-bottom: 1px solid #888;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf .etc input::placeholder {
font-weight: 400;
font-size: calc(10rem / 11);
line-height: calc(20 / 10);
text-transform: uppercase;
color: #000;
opacity: 0.4;
}
2021-04-18 22:36:39 +02:00
.overlay .backup-conf .etc .status {
2021-04-18 13:55:59 +02:00
display: none;
margin: 0;
}
2021-04-18 22:36:39 +02:00
.overlay .backup-conf .etc .status input {
color: #222;
background: #0000000E;
border: 1px solid transparent;
}
.overlay .backup-conf .etc .status.error input {
color: #d20;
background: #dd220018;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf .save {
display: block;
text-align: center;
margin: 20px -10px 5px;
padding: 20px 10px 0;
border-top: 1px solid #888;
word-spacing: 30px;
2021-04-19 17:52:26 +02:00
position: relative;
2021-04-18 13:55:59 +02:00
}
.overlay .backup-conf .save a {
color: #1489db;
}
2021-04-19 17:52:26 +02:00
.overlay .backup-conf.off .save .check {
color: #aaa;
pointer-events: none;
cursor: default;
}
2021-04-18 13:55:59 +02:00
/***/
@media print {
.logo, .config,
.board .head .teaser,
.list .head .teaser {
visibility: hidden;
display: none;
}
.board .note {
box-shadow: none;
text-shadow: none !important;
outline: 1px solid #888;
}
.list {
outline: 1px solid #555;
}
.board .note .raw {
outline: none;
text-shadow: none;
}
.board .head .text a,
.board .note .text a {
color: #333;
text-shadow: none;
text-decoration: underline;
}
}
/*
* Dark mode
*/
.theme-dark body {
background: #09090A;
color: #d6d6d6;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .head {
background: #17171a;
}
/***/
2021-03-31 18:27:33 +02:00
.theme-dark .board .menu {
background: linear-gradient(to right, #17171a00, #17171a 10px);
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .menu a {
color: #aaa;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .menu a:hover {
color: #fc4;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .menu a.warn:hover,
.theme-dark .board .ops a.warn:hover {
color: #fc5555 !important;
text-shadow: 0 0 1px #fc555544;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .head {
color: #eee;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .head .edit::placeholder {
color: #bf9d21;
}
.theme-dark .board > .head {
box-shadow: 0 0 2px #fff1 inset;
}
/***/
2021-03-31 18:27:33 +02:00
.theme-dark .board .list {
background: #17171a;
box-shadow: 0 0 2px #fff1 inset;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note {
background: linear-gradient(#242426, #202022);
box-shadow: 0 1px 3px #000a;
text-shadow: 0 0 4px #0008;
}
.theme-dark .reveal .board .head .text a,
.theme-dark .reveal .board .note .text a {
2020-02-20 01:13:00 +01:00
color: #fc0;
}
@keyframes whoomp-dark {
0% { color: inherit; }
30% { color: #fff; }
100% { color: inherit; }
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .head .text a:hover,
.theme-dark .board .note .text a:hover {
2020-02-20 01:13:00 +01:00
animation-name: whoomp-dark;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note.raw {
background: transparent;
box-shadow: none;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note.raw .text {
color: #eee;
text-shadow: 0 0 4px #0008;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note .ops .teaser {
color: #ccc;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note .ops .bulk {
background: #202022;
border-left: 1px solid #111;
border-bottom: 1px solid #111;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note.raw .ops .bulk {
border: none;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note .ops .bulk a {
color: #ccc;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note .ops .bulk a:hover {
color: #fc2;
}
2021-04-02 01:59:34 +02:00
/***/
2021-03-31 18:27:33 +02:00
.theme-dark .board .note.dragging,
.theme-dark .board .note.dragging.raw {
background: #09090A;
outline: 1px solid #fff2;
}
2021-04-02 01:59:34 +02:00
.theme-dark .note-dragster {
box-shadow: 0px 1px 4px #000;
background: linear-gradient(#242426, #202022);
outline: 1px solid #fc28;
}
2021-04-02 01:59:34 +02:00
.theme-dark .note-dragster a {
2020-02-20 01:13:00 +01:00
color: #d6d6d6;
}
/***/
.theme-dark .wrap textarea,
.theme-dark .wrap input {
background: #111315;
color: #eee;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .head.editing .edit {
outline: 1px solid #bc9908;
}
2021-03-31 18:27:33 +02:00
.theme-dark .board .note.editing {
background: #111315;
outline: 1px solid #bc9908;
}
/***/
.theme-dark .logo a {
color: #ccc;
}
.theme-dark .logo,
.theme-dark.crowded .logo:hover {
background: #09090A !important;
}
.theme-dark .logo a:hover {
color: #fc2;
}
.theme-dark .logo > a:hover {
color: #fff;
}
/***/
.theme-dark .config,
.theme-dark.crowded .config:hover {
background: #09090A !important;
}
2021-03-31 18:27:33 +02:00
.theme-dark .config a {
color: #ddd;
}
2021-03-31 18:27:33 +02:00
.theme-dark .config a:hover {
color: #fc2;
}
.theme-dark .config .bulk .boards {
2020-02-19 14:25:11 +01:00
border-top: 1px solid #fff2;
border-bottom: 1px solid #fff2;
}
.theme-dark .config .bulk .boards a.load-board.active {
color: #fc2;
}
.theme-dark .config .bulk .boards a.load-board.active:hover {
2021-04-02 01:59:34 +02:00
color: #ddd;
}
.theme-dark .config .bulk .break {
2020-02-19 14:25:11 +01:00
border-top: 1px solid #fff2;
}
.theme-dark.fs-set .config .bulk .ui-prefs .f-prefs tr.ui-fs td.val,
.theme-dark.lh-set .config .bulk .ui-prefs .f-prefs tr.ui-lh td.val,
.theme-dark.lw-set .config .bulk .ui-prefs .f-prefs tr.ui-lw td.val {
2021-04-06 16:51:20 +02:00
color: #fc2;
}
2021-03-31 18:27:33 +02:00
.theme-dark .config a.switch-theme i {
2020-02-19 14:25:11 +01:00
display: inline;
}
2021-03-31 18:27:33 +02:00
.theme-dark .config a.switch-theme b {
2020-02-19 14:25:11 +01:00
display: none;
}
2021-04-02 01:59:34 +02:00
/***/
.theme-dark .config .bulk .boards .load-board.dragging,
.theme-dark .config .bulk .boards .load-board.active.dragging {
background: #fff2;
color: transparent;
2021-04-02 01:59:34 +02:00
}
.theme-dark .load-dragster {
2021-04-07 18:51:52 +02:00
background: #09090A;
2021-04-02 01:59:34 +02:00
box-shadow: 0px 2px 4px #000;
outline: 1px solid #fc2a;
color: #ddd;
}
</style>
</head>
2019-05-28 12:09:54 +02:00
<body>
<div class="logo">
<a class=site href=https://nullboard.io>Nullboard</a>
<i class=alert></i>
<div class=bulk>
<a href=# class=view-about>About</a>
<a href=# class=view-license>License</a>
<a href=https://nullboard.io/changes target=_blank class=view-changes>Changes</a>
<a href=https://nullboard.io/github target=_blank>Github</a>
<a href=https://nullboard.io/twitter target=_blank>Twitter</a>
</div>
</div>
2019-05-28 12:09:54 +02:00
2021-04-06 16:51:20 +02:00
<div class='config no-user-select'>
2021-04-14 17:00:49 +02:00
<a href=# class=teaser><i>&equiv;</i><u>&#x2714;</u></a>
<div class=bulk>
<a href=# class=add-board>Add new board...</a>
<div class=boards draggable=false>
2019-05-28 12:09:54 +02:00
<!-- here'll be boards -->
</div>
2021-03-31 20:20:49 +02:00
<a href=# class=exp-board>Export current board...</a>
2021-03-31 20:49:29 +02:00
<a href=# class=imp-board>Import boards...</a>
<input class=imp-board-select type="file" accept=".nbx">
2021-04-14 13:54:03 +02:00
<a href=# class=auto-backup>Auto-backup...</a>
2021-04-06 16:51:20 +02:00
<div class="section ui-prefs break">
2021-04-06 20:21:08 +02:00
<a href=# class=title>UI preferences<u>...</u></a>
2021-04-06 16:51:20 +02:00
<div class=details>
<a href="#" class="switch-font active break" font="barlow">Use Barlow</a>
2021-04-06 13:21:48 +02:00
<a href="#" class="switch-font" font="ibm-plex">Use IBM Plex Sans</a>
<a href="#" class="switch-font" font="open-sans">Use Open Sans</a>
<a href="#" class="switch-font" font="segoe-ui">Use Segoe UI</a>
<a href="#" class="switch-font" font="maven-pro">Use Maven Pro</a>
2021-04-06 16:51:20 +02:00
<div class='f-prefs break'>
2021-04-06 13:21:48 +02:00
<table>
2021-04-06 19:32:46 +02:00
<tr class='ui-fs'><td class=name>Font size:</td> <td><a href=# class=less>&lt;</a></td><td class=val></td><td><a href=# class=more>&gt;</a></td></tr>
<tr class='ui-lh'><td class=name>Line height:</td><td><a href=# class=less>&lt;</a></td><td class=val></td><td><a href=# class=more>&gt;</a></td></tr>
<tr class='ui-lw'><td class=name>List width:</td> <td><a href=# class=less>&lt;</a></td><td class=val></td><td><a href=# class=more>&gt;</a></td></tr>
</table>
</div>
<a href="#" class="switch-theme break">Use <i>light</i><b>dark</b> theme</a>
</div>
</div>
2019-05-28 12:09:54 +02:00
</div>
</div>
</div>
2019-05-28 12:09:54 +02:00
<div class=wrap>
2019-05-28 12:09:54 +02:00
</div>
<div class=overlay>
2019-05-28 12:09:54 +02:00
</div>
<tt>
<!-- templates -->
<div class=board>
<div class=head>
<div class=text></div>
<input class=edit spellcheck=false placeholder='Name of the board'>
<div class=menu>
<a href=# class=teaser>&equiv;</a>
<div class=bulk>
<a href=# class='del-board warn'><u></u> Board</a>
<a href=# class='undo-board'>Undo</a>
<a href=# class='redo-board'>Redo</a>
<a href=# class='add-list'>+ List</a>
2019-05-28 12:09:54 +02:00
</div>
</div>
</div>
<div class=lists-scroller><div></div></div>
<div class=lists>
2019-05-28 12:09:54 +02:00
</div>
</div>
<div class=list>
<div class=head>
<div class=text></div>
<input class=edit spellcheck=false placeholder='Name of the list'>
<div class=menu>
<a href=# class=teaser>&equiv;</a>
<div class=bulk>
<a href=# class='del-list warn'><u></u> List</a>
<a href=# class='mov-list-l full'>&lt; Move</a>
<a href=# class='mov-list-l half'>&lt; Mo</a><a href=# class='mov-list-r half'>ve &gt;</a>
<a href=# class='mov-list-r full'>Move &gt;</a>
<a href=# class='add-note'>+ Note</a>
2019-05-28 12:09:54 +02:00
</div>
</div>
</div>
<div class=notes>
2019-05-28 12:09:54 +02:00
</div>
</div>
<div class=note>
<div class=ops>
<a href=# class=teaser></a>
<div class=bulk>
<a href=# class='del-note warn'>X</a>
<a href=# class='raw-note'>R</a>
<a href=# class='collapse'>_</a>
2019-05-28 12:09:54 +02:00
</div>
</div>
<div class=text> </div>
<textarea class=edit spellcheck=false></textarea>
2019-05-28 12:09:54 +02:00
</div>
<a href=# class=load-board></a>
2019-05-28 12:09:54 +02:00
<textarea class=encoder></textarea>
2019-05-28 12:09:54 +02:00
<div class=about>
2019-05-28 12:09:54 +02:00
<h1>Nullboard</h1>
Minimalist locally-stored kanban board
<a href=https://nullboard.io target=_blank>https://nullboard.io</a>
2019-05-28 12:09:54 +02:00
<div></div>
</div>
<div class=license>
2019-05-28 12:09:54 +02:00
</div>
2021-04-18 13:55:59 +02:00
<div class=backup-conf>
2021-04-19 17:52:26 +02:00
<a href=# class=close>&times;</a>
2021-04-18 13:55:59 +02:00
<h3>Auto-backup</h3>
<div class=what>
<a href=https://nullboard.io/backups target=_blank>What is this?</a>
</div>
<div class=loc>
<div class=opt>Local backup</div>
<div class=etc>
<span>Access token</span> <input class=auth placeholder="required"><br>
2021-04-18 22:36:39 +02:00
<p class=status><span>Status</span> <input disabled></p>
2021-04-18 13:55:59 +02:00
</div>
</div>
<div class=rem>
<div class=opt>Remote backup</div>
<div class=etc>
<span>URL</span> <input class=base placeholder="API entry point"><br>
<span>Access token</span> <input class=auth placeholder="required"><br>
2021-04-18 22:36:39 +02:00
<p class=status><span>Status</span> <input disabled></p>
2021-04-18 13:55:59 +02:00
</div>
</div>
2021-04-19 17:52:26 +02:00
<div class=save><a href=# class=check>Check...</a> <a href=# class=ok>Save</a></div>
2021-04-18 13:55:59 +02:00
</div>
2019-05-28 12:09:54 +02:00
</tt>
</body>
2021-11-24 12:24:07 +02:00
<script src="extras/jquery-3.6.0.min.js"></script>
<script>window.jQuery || document.write('<script src="https://code.jquery.com/jquery-3.6.0.min.js">\x3C/script>')</script>
2021-03-31 18:27:33 +02:00
2021-04-07 18:51:52 +02:00
<script type="text/javascript">
Number.prototype.clamp = function(min, max)
{
return Math.min(Math.max(this, min), max);
};
2022-08-31 14:28:49 -05:00
/*
2022-08-10 17:18:21 +02:00
* add a blank line to push 'Prevent this page from opening ...'
* tack-on from the actual message we are trying to display
*/
var confirm_org = window.confirm;
var alert_org = window.alert;
window.confirm = function(msg) { return confirm_org(msg + "\n "); }
window.alert = function(msg) { return alert_org (msg + "\n "); }
2021-04-07 18:51:52 +02:00
</script>
2019-05-28 12:09:54 +02:00
<script type="text/javascript">
2021-03-31 18:27:33 +02:00
function AppConfig()
{
2021-04-18 13:55:59 +02:00
this.verLast = null; // last used codeVersion
this.verSeen = null; // latest codeVersion they saw the changelog for
2021-04-18 13:55:59 +02:00
this.maxUndo = 50; // board revisions to keep
this.fontName = null; // font-family
this.fontSize = null; // font-size
this.lineHeight = null; // line-height
this.listWidth = null; // list-width
this.theme = null; // default or 'dark'
this.fileLinks = false; // mark up `foo` as <a href=file:///foo>...</a>
2021-04-18 13:55:59 +02:00
this.board = null; // active board
2021-04-25 14:37:57 +02:00
this.backups =
{
agents : [ ], // [ { type, id, enabled, conf } ];
nextId : 1
};
2021-04-25 16:04:11 +02:00
2021-04-25 20:33:33 +02:00
this.backupStatus = { }; // agentId => [ 'conf' ]
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
const default_backup_agents =
[
{ base: 'http://127.0.0.1:10001', auth: '' }, // local agent
{ base: '', auth: '' } // remote agent
];
//
2021-03-31 18:27:33 +02:00
function BoardMeta()
{
2021-03-31 18:27:33 +02:00
this.title = '';
2021-04-13 19:05:14 +02:00
this.current = 1; // revision
this.ui_spot = 0; // 0 = not set
this.history = [ ]; // revision IDs
2021-04-25 20:33:33 +02:00
this.backupStatus = { }; // agentId => [ what's backed up ]
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
class Storage
{
2021-03-31 22:12:53 +02:00
constructor()
{
this.type = '?';
2021-04-01 19:12:43 +02:00
this.conf = new AppConfig();
2021-03-31 22:12:53 +02:00
this.boardIndex = new Map();
2021-04-13 19:05:14 +02:00
2021-04-25 14:37:57 +02:00
this.backups =
{
status : '', // '', 'ok', 'busy', 'failed'
agents : [ ], // BackupStorage instances
};
2021-03-31 22:12:53 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
open()
{
2021-03-31 18:27:33 +02:00
return this.openInner();
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
wipe()
{
return this.wipeInner();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
getConfig()
{
2021-03-31 18:27:33 +02:00
return this.conf;
2019-05-28 12:09:54 +02:00
}
2021-04-25 14:37:57 +02:00
setVerLast()
{
2021-04-25 14:37:57 +02:00
if (this.conf.verLast == NB.codeVersion)
return true;
this.conf.verLast = NB.codeVersion;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
}
setVerSeen(ver)
{
this.conf.verSeen = ver || NB.codeVersion;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
}
2021-03-31 18:27:33 +02:00
setActiveBoard(board_id)
{
2021-04-25 14:37:57 +02:00
console.log('setActiveBoard [' + this.conf.board + '] -> [' + board_id + ']');
2021-03-31 18:27:33 +02:00
var meta = board_id ? this.boardIndex.get(board_id) : true;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
throw `Invalid board_id in setActiveBoard(... ${board_id})`;
2019-05-28 12:09:54 +02:00
2021-04-13 19:05:14 +02:00
if (this.conf.board == board_id)
return true;
2021-03-31 18:27:33 +02:00
this.conf.board = board_id;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
2021-03-31 18:27:33 +02:00
}
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
setTheme(theme)
{
if (this.conf.theme == theme) return;
2021-03-31 18:27:33 +02:00
this.conf.theme = theme;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
2021-03-31 18:27:33 +02:00
}
2020-02-20 01:13:00 +01:00
setFontName(fname)
2021-03-31 18:27:33 +02:00
{
if (this.conf.fontName == fname) return;
2021-04-06 19:09:14 +02:00
this.conf.fontName = fname;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
2021-03-31 18:27:33 +02:00
}
2020-02-20 01:13:00 +01:00
setFontSize(fs)
{
if (this.conf.fontSize == fs) return;
2021-04-06 19:09:14 +02:00
this.conf.fontSize = fs;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
}
2021-04-06 16:51:20 +02:00
setLineHeight(lh)
{
if (this.conf.lineHeight == lh) return;
2021-04-06 19:09:14 +02:00
this.conf.lineHeight = lh;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
2021-04-06 16:51:20 +02:00
}
2021-04-06 19:32:46 +02:00
setListWidth(lw)
{
if (this.conf.listWidth == lw) return;
2021-04-06 19:32:46 +02:00
this.conf.listWidth = lw;
2021-04-13 19:05:14 +02:00
return this.saveConfig();
}
saveConfig()
{
2021-04-14 13:54:03 +02:00
this.backupConfig();
2021-04-06 19:32:46 +02:00
return this.setJson('config', this.conf);
}
2021-03-31 18:27:33 +02:00
//
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
getBoardIndex()
{
return this.boardIndex;
}
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
saveBoard(board)
{
/*
* 1. assign new revision (next unused)
* 2. trim all in-between revisions bypassed by undos if any
* 3. cap history as per config
*/
var meta = this.boardIndex.get(board.id);
var ok_data, ok_meta;
2019-05-28 12:09:54 +02:00
delete board.history; // remove temporarily
2021-03-31 18:27:33 +02:00
if (! meta)
{
board.revision = 1;
ok_data = this.setJson('board.' + board.id + '.' + board.revision, board);
2021-03-31 18:27:33 +02:00
meta = new BoardMeta();
meta.title = board.title || '(Untitled board)';
meta.current = board.revision;
meta.history = [ board.revision ];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.boardIndex.set(board.id, meta);
}
else
{
var rev_old = board.revision;
var rev_new = meta.history[0] + 1;
2019-05-28 12:09:54 +02:00
board.revision = rev_new;
2019-05-28 12:09:54 +02:00
ok_data = this.setJson('board.' + board.id + '.' + board.revision, board);
2019-05-28 12:09:54 +02:00
meta.title = board.title || '(Untitled board)';
meta.current = board.revision;
2019-05-28 12:09:54 +02:00
// trim revisions skipped over with undo and cap the revision count
2019-05-28 12:09:54 +02:00
var rebuild = [ board.revision ];
2019-05-28 12:09:54 +02:00
for (var rev of meta.history)
2021-03-31 18:27:33 +02:00
{
2021-04-06 19:09:14 +02:00
if ( (rev_old < rev && rev < rev_new) || (rebuild.length >= this.conf.maxUndo) )
{
this.delItem('board.' + board.id + '.' + rev);
console.log( `Deleted revision ${rev} of ${board.id} (${board.title})` );
}
else
{
rebuild.push(rev);
}
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
meta.history = rebuild;
}
2019-05-28 12:09:54 +02:00
2021-04-13 19:05:14 +02:00
/*
* save meta
*/
ok_meta = this.setJson('board.' + board.id + '.meta', meta) &&
this.setJson('board.' + board.id, meta.current); // for older versions
2019-05-28 12:09:54 +02:00
2021-04-13 19:05:14 +02:00
/*
* run backups
*/
if (ok_meta && ok_data)
2021-04-14 13:54:03 +02:00
this.backupBoard(board.id, board, meta)
2021-04-13 19:05:14 +02:00
2021-03-31 18:27:33 +02:00
board.history = meta.history; // restore
console.log( `Saved revision ${board.revision} of ${board.id} (${board.title}), ok = ${ok_data} | ${ok_meta}` );
return ok_data && ok_meta;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
loadBoard(board_id, revision)
{
2021-03-31 18:27:33 +02:00
var meta = this.boardIndex.get(board_id);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
throw `Invalid board_id in loadBoard(${board_id}, ${revision})`;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (revision == null)
revision = meta.current;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta.history.includes(revision))
throw `Invalid revision in loadBoard(${board_id}, ${revision})`;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var board = this.getJson('board.' + board_id + '.' + revision);
if (! board)
return false;
2021-03-30 10:18:01 +02:00
if (board.format != NB.blobVersion)
2021-03-31 18:27:33 +02:00
{
console.log('Board ' + board_id + '/' + revision + ' format is unsupported');
console.log('Have [' + board.format + '], need [' + NB.blobVersion);
2021-03-31 18:27:33 +02:00
return false;
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
if (board.revision != revision)
{
console.log('Board ' + board_id + '/' + revision + ' revision is wrong');
console.log('Have [' + board.revision + ']');
return false;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
board.history = meta.history;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
console.log( `Loaded revision ${board.revision} of ${board.id} (${board.title})` );
return Object.assign(new Board(), board);
2019-05-28 12:09:54 +02:00
}
2021-03-31 20:35:46 +02:00
nukeBoard(board_id)
2021-03-31 18:27:33 +02:00
{
var meta = this.boardIndex.get(board_id);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
2022-08-31 14:26:35 -05:00
throw `Invalid board_id in nukeBoard(${board_id})`;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var title = meta.title + '';
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
for (var rev of meta.history)
this.delItem('board.' + board_id + '.' + rev);
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
this.delItem('board.' + board_id + '.meta');
this.boardIndex.delete(board_id);
2021-03-30 10:18:01 +02:00
2021-04-25 14:37:57 +02:00
this.backups.agents.forEach(function(store){
2021-04-13 19:05:14 +02:00
store.nukeBoard(board_id);
});
2021-03-31 20:35:46 +02:00
console.log( `Deleted board ${board_id} (${title})` );
2021-03-31 18:27:33 +02:00
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
getBoardHistory(board_id)
{
var meta = this.boardIndex.get(board_id);
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
throw `Invalid board_id in getBoardHistory(${board_id})`;
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
return meta.history;
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
setBoardRevision(board_id, revision)
{
var meta = this.boardIndex.get(board_id);
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
throw `Invalid board_id in setBoardRevision(${board_id}, ${revision})`;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta.history.includes(revision))
throw `Invalid revision in setBoardRevision(${board_id}, ${revision})`;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (meta.current == revision) // wth
return true;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
meta.current = revision;
this.backupBoard(board_id, null, meta);
2021-04-14 13:54:03 +02:00
return this.setJson('board.' + board_id + '.meta', meta) &&
this.setJson('board.' + board_id, revision); // for older versions
2019-05-28 12:09:54 +02:00
}
2021-04-02 01:59:34 +02:00
setBoardUiSpot(board_id, ui_spot)
{
var meta = this.boardIndex.get(board_id);
if (! meta)
2022-08-31 14:26:35 -05:00
throw `Invalid board_id in setBoardUiSpot(${board_id}, ${ui_spot})`;
2021-04-02 01:59:34 +02:00
meta.ui_spot = ui_spot;
2021-04-25 16:04:11 +02:00
this.backupBoard(board_id, null, meta);
2021-04-02 01:59:34 +02:00
return this.setJson('board.' + board_id + '.meta', meta);
}
2021-03-31 18:27:33 +02:00
/*
* private
*/
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
getItem(name) { throw 'implement-me'; }
setItem(name) { throw 'implement-me'; }
delItem(name) { throw 'implement-me'; }
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
openInner() { throw 'implement-me'; }
wipeInner() { throw 'implement-me'; }
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
getJson(name)
{
var foo = this.getItem(name);
if (! foo) return false;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
try { foo = JSON.parse(foo); } catch (x) { return false; }
return foo;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
setJson(name, val)
{
if (! this.setItem(name, JSON.stringify(val)))
{
console.log("setJson(" + name + ") failed");
return false;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
return true;
}
2021-04-13 19:05:14 +02:00
2021-04-19 11:49:23 +02:00
/*
* config
*/
fixupConfig(newInstall)
{
var conf = this.conf;
var simp = (new SimpleBackup).type;
if (conf.board && ! this.boardIndex.has(conf.board))
conf.board = null;
if (! conf && ! newInstall) // pre-20210410 upgrade
{
conf.verLast = 20210327;
conf.verSeen = 20200220; // 20200429;
}
2021-04-25 14:37:57 +02:00
var agents = conf.backups.agents;
if (agents.length != 2 ||
agents[0].type != simp || agents[0].conf.base != 'http://127.0.0.1:10001' ||
agents[1].type != simp)
2021-04-19 11:49:23 +02:00
{
const def = default_backup_agents;
2021-04-25 14:37:57 +02:00
console.log('Unexpected backup config, will re-initialize.', agents);
conf.backups.agents = [];
2021-04-19 11:49:23 +02:00
2021-04-25 14:37:57 +02:00
conf.backups.agents.push({
// localhost
type : simp,
id : simp + '-' + (conf.backups.nextId++),
enabled : def[0].base && def[0].auth,
conf : def[0],
2021-04-19 11:49:23 +02:00
})
2021-04-25 14:37:57 +02:00
conf.backups.agents.push({
// remote
type : simp,
id : simp + '-' + (conf.backups.nextId++),
enabled : def[1].base && def[1].auth,
conf : def[1],
2021-04-19 11:49:23 +02:00
})
2021-04-25 16:30:17 +02:00
this.saveConfig();
2021-04-19 11:49:23 +02:00
}
}
2021-04-14 13:54:03 +02:00
/*
* backups
*/
2021-04-25 14:37:57 +02:00
initBackups(onBackupStatus)
2021-04-13 19:05:14 +02:00
{
var self = this;
2021-04-14 13:54:03 +02:00
var pending = 0;
var success = true;
2021-04-14 14:37:25 +02:00
var store_id = 1;
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
self.backups.agents = [];
2021-04-14 17:00:49 +02:00
2021-04-25 14:37:57 +02:00
onBackupStatus(null);
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
this.conf.backups.agents.forEach(function(b){
2021-04-18 13:55:59 +02:00
var T = NB.backupTypes.get(b.type);
2021-04-14 13:54:03 +02:00
if (! T)
{
2021-04-18 13:55:59 +02:00
console.log( `Unknown backup type "${b.type}" - skipped` );
2021-04-14 13:54:03 +02:00
return;
}
2021-04-18 13:55:59 +02:00
if (! b.enabled)
return;
2021-04-25 14:37:57 +02:00
var agent = new T(b.id, b.conf, onBackupStatus);
self.backups.agents.push(agent);
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
console.log( `Added backup agent - type '${agent.type}', id '${agent.id}'` );
2021-04-18 13:55:59 +02:00
2021-04-25 14:37:57 +02:00
agent.checkStatus(null); // will need just onBackupStatus() callbacks
2021-04-14 13:54:03 +02:00
});
}
backupBoard(board_id, board, meta)
{
var self = this;
2021-04-25 20:33:33 +02:00
var was = meta.backupStatus || {};
2021-04-25 16:04:11 +02:00
2021-04-25 20:33:33 +02:00
meta.backupStatus = {};
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
if (! this.backups.agents.length)
2021-04-14 13:54:03 +02:00
{
2021-04-25 20:33:33 +02:00
if (was['data'] || was['meta'])
2021-04-25 16:04:11 +02:00
self.setJson('board.' + board_id + '.meta', meta);
2021-04-14 13:54:03 +02:00
return;
}
2021-04-25 14:37:57 +02:00
console.log( `Backing up ${board_id}...` );
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
this.backups.agents.forEach(function(agent){
2021-04-14 13:54:03 +02:00
2021-04-25 20:33:33 +02:00
var fields = was[agent.id] || {};
2021-04-25 16:04:11 +02:00
if (board) delete fields.data;
if (meta) delete fields.meta;
2021-04-25 20:33:33 +02:00
meta.backupStatus[agent.id] = fields;
2021-04-25 16:04:11 +02:00
2021-04-25 16:37:15 +02:00
agent.saveBoard(board_id, board, meta, function(ok){
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
var what = 'Backup of ' + board_id + (board ? '' : ' (meta)');
2021-04-25 20:33:33 +02:00
console.log( `${what} to '${agent.id}' -> ${ok ? 'ok' : 'failed'}` );
2021-04-25 14:37:57 +02:00
2021-04-25 16:37:15 +02:00
if (ok)
2021-04-25 16:04:11 +02:00
{
if (board) fields.data = + new Date();
if (meta) fields.meta = + new Date();
2021-04-25 20:33:33 +02:00
meta.backupStatus[agent.id] = fields;
2021-04-25 16:04:11 +02:00
}
2021-04-14 13:54:03 +02:00
self.setJson('board.' + board_id + '.meta', meta);
});
});
}
backupConfig()
{
var self = this;
2021-04-25 20:33:33 +02:00
var was = self.conf.backupStatus || {};
2021-04-25 16:04:11 +02:00
2021-04-25 20:33:33 +02:00
self.conf.backupStatus = {};
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
if (! this.backups.agents.length)
2021-04-25 16:04:11 +02:00
{
2021-04-25 20:33:33 +02:00
if (was['conf'])
2021-04-25 16:04:11 +02:00
this.setJson('config', this.conf);
2021-04-14 13:54:03 +02:00
return;
2021-04-25 16:04:11 +02:00
}
2021-04-14 13:54:03 +02:00
2021-04-25 14:37:57 +02:00
this.backups.agents.forEach(function(agent){
2021-04-25 16:04:11 +02:00
var fields = { };
2021-04-25 20:33:33 +02:00
self.conf.backupStatus[agent.id] = fields;
2021-04-25 16:04:11 +02:00
2021-04-25 16:37:15 +02:00
agent.saveConfig(self.conf, function(ok){
2021-04-25 16:04:11 +02:00
2021-04-25 16:37:15 +02:00
if (ok)
2021-04-25 16:04:11 +02:00
{
fields.conf = + new Date()
2021-04-25 20:33:33 +02:00
self.conf.backupStatus[agent.id] = fields;
2021-04-25 16:04:11 +02:00
}
2021-04-25 16:30:17 +02:00
self.setJson('config', self.conf);
2021-04-25 16:04:11 +02:00
});
2021-04-13 19:05:14 +02:00
});
}
2021-03-31 18:27:33 +02:00
};
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
class Storage_Local extends Storage
{
2021-03-31 22:12:53 +02:00
constructor()
{
super();
this.type = 'LocalStorage';
}
2021-03-31 18:27:33 +02:00
getItem(name)
{
2021-03-31 18:27:33 +02:00
return localStorage.getItem('nullboard.' + name);
}
2021-03-31 18:27:33 +02:00
setItem(name, val)
{
2021-03-31 18:27:33 +02:00
localStorage.setItem('nullboard.' + name, val);
return true;
}
2021-03-31 18:27:33 +02:00
delItem(name)
{
2021-03-31 18:27:33 +02:00
localStorage.removeItem('nullboard.' + name);
return true;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
openInner()
{
var conf = this.getJson('config');
2021-04-10 14:32:14 +02:00
var newInstall = true;
2019-05-28 12:09:54 +02:00
2021-04-25 16:04:11 +02:00
// if (conf && (conf.format != NB.confVersion))
// {
// if (! confirm('Preferences are stored in an unsupported format. Reset them?'))
// return false;
//
// conf = null;
// }
if (conf)
{
2021-04-13 19:05:14 +02:00
this.conf = Object.assign(new AppConfig(), conf);
2021-03-31 18:27:33 +02:00
}
else
{
this.conf.theme = this.getItem('theme');
2019-05-28 12:09:54 +02:00
if (this.getItem('fsize') == 'z1')
{
2021-04-06 19:09:14 +02:00
this.conf.fontSize = 13;
this.conf.lineHeight = 17;
}
2021-03-31 18:27:33 +02:00
if (! this.setJson('config', this.conf))
{
this.conf = null;
return false;
}
this.conf.board = this.getItem('last_board');
2021-03-31 18:27:33 +02:00
}
2021-03-31 18:27:33 +02:00
this.boardIndex = new Map();
// new format
2021-03-31 18:27:33 +02:00
for (var i=0; i<localStorage.length; i++)
{
var k = localStorage.key(i);
var m = k.match(/^nullboard\.board\.(\d+).meta$/);
2019-05-28 12:09:54 +02:00
if (! m)
continue;
var board_id = parseInt(m[1]);
var meta = this.getJson('board.' + board_id + '.meta');
if (! meta.hasOwnProperty('history'))
2021-03-31 18:27:33 +02:00
{
console.log( `Invalid meta for board ${board_id}` );
continue;
}
2019-05-28 12:09:54 +02:00
for (var rev of meta.history)
if (! this.getJson('board.' + board_id + '.' + rev))
2021-03-31 18:27:33 +02:00
{
console.log( `Invalid revision ${rev} in history of ${board_id}` );
meta = this.rebuildMeta(board_id);
break;
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta)
continue;
2019-05-28 12:09:54 +02:00
2021-04-25 14:37:57 +02:00
delete meta.backingUp; // run-time var
delete meta.needsBackup; // ditto
2021-03-31 18:27:33 +02:00
meta = Object.assign(new BoardMeta(), meta);
this.boardIndex.set(board_id, meta);
}
// old format
for (var i=0; i<localStorage.length; i++)
{
var k = localStorage.key(i);
var m = k.match(/^nullboard\.board\.(\d+)$/);
if (! m)
continue;
2021-04-10 14:32:14 +02:00
newInstall = false;
var board_id = parseInt(m[1]);
if (this.boardIndex.has(board_id))
continue;
var meta = this.rebuildMeta(board_id);
if (! meta)
continue;
meta = Object.assign(new BoardMeta(), meta);
this.boardIndex.set(board_id, meta);
2021-03-31 18:27:33 +02:00
}
2021-04-19 11:49:23 +02:00
this.fixupConfig(newInstall);
2021-04-10 14:32:14 +02:00
2021-03-31 18:27:33 +02:00
this.type = 'LocalStorage';
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
return true;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
wipeInner()
{
for (var i=0; i<localStorage.length; )
{
var k = localStorage.key(i);
var m = k.match(/^nullboard\./);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (m) localStorage.removeItem(k);
else i++;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.conf = new AppConfig();
this.boardIndex = new Map();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
/*
* private
*/
rebuildMeta(board_id)
{
var meta = new BoardMeta();
2019-05-28 12:09:54 +02:00
console.log( `Rebuilding meta for ${board_id} ...` );
2021-03-31 18:27:33 +02:00
// get current revision
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
meta.current = this.getItem('board.' + board_id); // may be null
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
// load history
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var re = new RegExp('^nullboard\.board\.' + board_id + '\.(\\d+)$');
var revs = new Array();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
for (var i=0; i<localStorage.length; i++)
{
var m = localStorage.key(i).match(re);
if (m) revs.push( parseInt(m[1]) );
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! revs.length)
{
console.log('* No revisions found');
this.delItem('board.' + board_id);
2021-03-31 18:27:33 +02:00
return false;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
revs.sort(function(a,b){ return b-a; });
meta.history = revs;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
// validate current revision
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! meta.history.includes(meta.current))
meta.current = meta.history[meta.history.length-1];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
// get board title
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var board = this.getJson('board.' + board_id + '.' + meta.current)
meta.title = (board.title || '(untitled board)');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.setJson('board.' + board_id + '.meta', meta);
return meta;
2019-05-28 12:09:54 +02:00
}
}
2021-04-13 19:05:14 +02:00
/*
*
*/
class BackupStorage
{
2021-04-25 14:37:57 +02:00
constructor(id, conf, onStatusChange)
2021-04-13 19:05:14 +02:00
{
2021-04-25 14:37:57 +02:00
this.type = '?';
this.id = id;
this.conf = conf;
this.status = '';
this.lastOp = '';
this.lastXhr = { op: '', text: '', code: 0 };
this.onStatusChange = onStatusChange;
this.queue = [];
2021-04-13 19:05:14 +02:00
}
checkStatus(cb) { return false; }
saveConfig(conf, cb) { throw 'implement-me'; }
saveBoard (id, data, meta, cb) { throw 'implement-me'; }
nukeBoard (id, cb) { throw 'implement-me'; }
}
class SimpleBackup extends BackupStorage
{
2021-04-25 14:37:57 +02:00
constructor(id, conf, onStatusChange)
2021-04-13 19:05:14 +02:00
{
2021-04-25 14:37:57 +02:00
super(id, null, onStatusChange);
2021-04-18 13:55:59 +02:00
2021-04-14 14:37:25 +02:00
this.type = 'simp';
2021-04-18 13:55:59 +02:00
this.conf = { base: '', auth: '' }
2021-04-13 19:05:14 +02:00
this.conf = Object.assign(this.conf, conf);
}
checkStatus(cb)
{
2021-04-25 14:37:57 +02:00
this.queue.push({
what : 'checkStatus',
cb : cb,
args :
{
2021-04-25 14:37:57 +02:00
url: this.conf.base + '/config',
type: 'put',
headers: { 'X-Access-Token': this.conf.auth },
data:
{
self: document.location.href,
// conf: -- without the data --
},
dataType: 'json'
}
});
this.runQueue();
2021-04-13 19:05:14 +02:00
}
saveConfig(conf, cb)
{
2021-04-25 14:37:57 +02:00
this.queue.push({
what : 'saveConfig',
cb : cb,
args :
{
2021-04-25 14:37:57 +02:00
url: this.conf.base + '/config',
type: 'put',
headers: { 'X-Access-Token': this.conf.auth },
data:
{
self: document.location.href,
conf: JSON.stringify(conf)
},
dataType: 'json'
}
});
this.runQueue();
2021-04-13 19:05:14 +02:00
}
saveBoard(id, data, meta, cb)
{
2021-04-25 14:37:57 +02:00
this.queue.push({
what : 'saveBoard',
cb : cb,
args :
2021-04-13 19:05:14 +02:00
{
2021-04-25 14:37:57 +02:00
url: this.conf.base + '/board/' + id,
type: 'put',
headers: { 'X-Access-Token': this.conf.auth },
data:
{
self: document.location.href,
data: data ? JSON.stringify(data) : null,
meta: meta ? JSON.stringify(meta) : null
},
dataType: 'json'
}
});
this.runQueue();
2021-04-13 19:05:14 +02:00
}
nukeBoard(id, cb)
{
2021-04-25 14:37:57 +02:00
this.queue.push({
what : 'saveBoard',
cb : cb,
args :
{
url: this.conf.base + '/board/' + id,
type: 'delete',
headers: { 'X-Access-Token': this.conf.auth },
}
});
2021-04-13 19:05:14 +02:00
2021-04-25 14:37:57 +02:00
this.runQueue();
2021-04-19 13:09:04 +02:00
}
/*
* private
*/
2021-04-25 14:37:57 +02:00
runQueue()
2021-04-19 13:09:04 +02:00
{
2021-04-25 14:37:57 +02:00
var self = this;
if (! this.queue.length)
return;
if (this.status == 'busy')
return;
var req = this.queue.shift();
this.setStatus('busy', req.what);
$.ajax(req.args)
.done(function(d, s, x) { self.onRequestDone(req, true, x); })
.fail(function(x, s, e) { self.onRequestDone(req, false, x); })
}
onRequestDone(req, ok, xhr)
{
console.log( `Backup agent '${this.id}', ${this.lastOp}() -> ${ok ? 'ok' : 'failed'}` );
var code = xhr.status;
var text = xhr.responseText || (code ? `Response code ${code}` : 'Offline or CORS-blocked');
this.lastXhr = { text: text, code: code };
2021-04-25 16:37:15 +02:00
if (req.cb) req.cb.call(this, ok);
2021-04-25 14:37:57 +02:00
2021-04-25 16:37:15 +02:00
if (! this.queue.length)
{
this.setStatus(ok ? 'ready' : 'error', this.lastOp);
return;
}
2021-04-25 14:37:57 +02:00
2021-04-25 16:37:15 +02:00
this.status = 'pre-busy';
2021-04-25 14:37:57 +02:00
this.runQueue();
}
setStatus(status, op)
{
if (status == 'busy' && this.status == 'busy')
throw `Backup agent ${this.id} is already busy!`;
2021-04-25 20:33:33 +02:00
console.log( `Backup agent '${this.id}' status: '${this.status}' -> '${status}'` );
2021-04-25 14:37:57 +02:00
this.status = status;
this.lastOp = op;
this.onStatusChange(this);
2021-04-13 19:05:14 +02:00
}
}
2021-03-31 18:27:33 +02:00
</script>
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
<script type="text/javascript">
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function Note(text)
{
this.text = text;
this.raw = false;
this.min = false;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
function List(title)
{
2021-03-31 18:27:33 +02:00
this.title = title;
this.notes = [ ];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.addNote = function(text)
{
var x = new Note(text);
this.notes.push(x);
return x;
}
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
function Board(title)
{
this.format = NB.blobVersion;
2021-03-31 18:27:33 +02:00
this.id = +new Date();
this.revision = 0;
this.title = title || '';
this.lists = [ ];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.addList = function(title)
{
2021-03-31 18:27:33 +02:00
var x = new List(title);
this.lists.push(x);
return x;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
</script>
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
<script type="text/javascript">
2019-05-28 12:09:54 +02:00
2021-04-01 19:12:43 +02:00
function Drag2()
{
2021-04-01 19:12:43 +02:00
// config
this.listSel = null;
this.itemSel = null;
2021-04-02 01:59:34 +02:00
this.dragster = null;
2021-04-01 19:12:43 +02:00
this.onDragging = function(started) { }
this.swapAnimMs = 200;
// state
this.item = null;
2021-03-31 18:27:33 +02:00
this.priming = null;
2021-04-01 19:12:43 +02:00
this.primeXY = { x: 0, y: 0 };
2021-03-31 18:27:33 +02:00
this.$drag = null;
2021-04-01 19:12:43 +02:00
this.mouseEv = null;
2021-03-31 18:27:33 +02:00
this.delta = { x: 0, y: 0 };
2021-04-01 19:12:43 +02:00
this.inSwap = 0;
2019-05-28 12:09:54 +02:00
2021-04-01 19:12:43 +02:00
// api
2021-03-31 18:27:33 +02:00
this.prime = function(item, ev)
{
var self = this;
2021-04-01 19:12:43 +02:00
2021-03-31 18:27:33 +02:00
this.item = item;
this.priming = setTimeout(function(){ self.onPrimed.call(self); }, ev.altKey ? 1 : 500);
2021-04-01 19:12:43 +02:00
this.primeXY = { x: ev.clientX, y: ev.clientY };
this.mouseEv = ev;
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.cancelPriming = function()
{
2021-04-01 19:12:43 +02:00
if (! this.item || ! this.priming)
return;
clearTimeout(this.priming);
this.priming = null;
this.item = null;
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
this.end = function()
2021-03-30 10:18:01 +02:00
{
2021-03-31 18:27:33 +02:00
this.cancelPriming();
this.stopDragging();
2021-03-30 10:18:01 +02:00
}
2021-03-31 18:27:33 +02:00
this.isActive = function()
{
return this.item && (this.priming == null);
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
this.onPrimed = function()
{
clearTimeout(this.priming);
this.priming = null;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
removeTextSelection();
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
var $item = $(this.item);
$item.addClass('dragging');
2021-03-30 10:18:01 +02:00
2021-04-02 01:59:34 +02:00
$('body').append('<div class=' + this.dragster + '></div>');
var $drag = $('body .' + this.dragster).last();
2021-03-30 10:18:01 +02:00
$drag.outerWidth ( $item.outerWidth() );
$drag.outerHeight( $item.outerHeight() );
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
this.$drag = $drag;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (this.onDragging)
this.onDragging.call(this, true); // started
2021-03-31 18:27:33 +02:00
var $win = $(window);
var scroll_x = $win.scrollLeft();
var scroll_y = $win.scrollTop();
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
var pos = $item.offset();
this.delta.x = pos.left - this.mouseEv.clientX - scroll_x;
this.delta.y = pos.top - this.mouseEv.clientY - scroll_y;
2021-04-02 01:59:34 +02:00
2021-03-31 18:27:33 +02:00
this.adjustDrag();
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
$drag.css({ opacity: 1 });
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
$('body').addClass('dragging');
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
this.adjustDrag = function()
2021-03-30 10:18:01 +02:00
{
2021-03-31 18:27:33 +02:00
if (! this.$drag)
return;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
var drag = this;
var $drag = this.$drag;
2021-03-31 18:27:33 +02:00
var $win = $(window);
var scroll_x = $win.scrollLeft();
var scroll_y = $win.scrollTop();
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
var drag_x = drag.mouseEv.clientX + drag.delta.x + scroll_x;
var drag_y = drag.mouseEv.clientY + drag.delta.y + scroll_y;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
$drag.offset({ left: drag_x, top: drag_y });
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (drag.inSwap)
2021-03-31 18:27:33 +02:00
return;
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
/*
* see if a swap is in order
*/
2021-04-01 19:12:43 +02:00
var pos = $drag.offset();
var x = pos.left + $drag.width()/2 - $win.scrollLeft();
var y = pos.top + $drag.height()/2 - $win.scrollTop();
2021-03-30 10:18:01 +02:00
var targetList = null;
var targetItem = null; // if over some item
var before = false; // if should go before targetItem
var $target;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
$(this.listSel).each(function(){
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
var list = this;
2021-04-01 19:12:43 +02:00
var rcList = list.getBoundingClientRect();
var yTop, itemTop = null;
var yBottom, itemBottom = null;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (x <= rcList.left || rcList.right <= x)
2021-03-31 18:27:33 +02:00
return;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
$(list).find(drag.itemSel).each(function(){
var rcItem = this.getBoundingClientRect();
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (! itemTop || rcItem.top < yTop)
{
itemTop = this;
yTop = rcItem.top;
}
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (! itemBottom || yBottom < rcItem.bottom)
2021-03-31 18:27:33 +02:00
{
2021-04-01 19:12:43 +02:00
itemBottom = this;
yBottom = rcItem.bottom;
2021-03-31 18:27:33 +02:00
}
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (y <= rcItem.top || rcItem.bottom <= y)
2021-03-31 18:27:33 +02:00
return;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (this == drag.item)
2021-03-31 18:27:33 +02:00
return;
2021-03-30 10:18:01 +02:00
targetList = list;
targetItem = this;
2021-04-01 19:12:43 +02:00
before = (y < (rcItem.top + rcItem.bottom)/2);
2021-03-31 18:27:33 +02:00
});
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (y < rcList.top)
2021-03-31 18:27:33 +02:00
{
targetList = list;
targetItem = itemTop;
2021-04-01 19:12:43 +02:00
before = true;
2021-03-31 18:27:33 +02:00
}
2021-04-01 19:12:43 +02:00
else
if (y >= rcList.bottom)
{
targetList = list;
targetItem = itemBottom;
2021-04-01 19:12:43 +02:00
before = false;
}
2021-03-31 18:27:33 +02:00
});
2021-03-30 10:18:01 +02:00
if (! targetList)
2021-03-31 18:27:33 +02:00
return;
2021-03-30 10:18:01 +02:00
if (targetItem)
{
if (targetItem == drag.item)
return;
2021-03-30 10:18:01 +02:00
$target = $(targetItem);
if (! before && $target.next()[0] == drag.item ||
before && $target.prev()[0] == drag.item)
return;
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
/*
* swap 'em
*/
2021-04-01 19:12:43 +02:00
var have = drag.item;
var $have = $(have);
2021-03-31 18:27:33 +02:00
var $want = $have.clone();
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
$want.css({ display: 'none' });
2021-03-30 10:18:01 +02:00
if (targetItem)
2021-04-01 19:12:43 +02:00
{
if (before)
{
$want.insertBefore($target);
$want = $target.prev();
}
else
{
$want.insertAfter($target);
$want = $target.next();
}
2021-04-01 19:12:43 +02:00
}
else
2021-03-31 18:27:33 +02:00
{
var $list = $(targetList);
$want = $list.append($want).find(drag.itemSel)
2021-04-01 19:12:43 +02:00
}
drag.item = $want[0];
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (! drag.swapAnimMs)
{
$have.remove();
$want.show();
return;
}
/*
* see if it's a same-list move
*/
if (targetList == have.parentNode)
2021-04-01 19:12:43 +02:00
{
var delta = $have.offset().top - $target.offset().top;
var d_bulk = 0;
var d_have = 0;
var $bulk = $();
if (delta < 0) // item is moving down
2021-03-31 18:27:33 +02:00
{
2021-04-01 19:12:43 +02:00
for (var $i = $have.next(); $i.length && $i[0] != $want[0]; $i = $i.next())
$bulk = $bulk.add($i);
2021-03-31 18:27:33 +02:00
}
else
{
2021-04-01 19:12:43 +02:00
for (var $i = $want.next(); $i.length && $i[0] != $have[0]; $i = $i.next())
$bulk = $bulk.add($i);
2021-03-31 18:27:33 +02:00
}
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
d_bulk = $have.outerHeight(true);
d_have = $bulk.last().offset().top + $bulk.last().outerHeight(true) - $bulk.first().offset().top;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
if (delta < 0) d_bulk = -d_bulk;
else d_have = -d_have;
2021-03-31 18:27:33 +02:00
2021-04-01 19:12:43 +02:00
$have.parent().css({ position: 'relative' });
$have.css({ position: 'relative', 'z-index': 0 });
$bulk.css({ position: 'relative', 'z-index': 1 });
2021-03-31 18:27:33 +02:00
2021-04-01 19:12:43 +02:00
drag.inSwap = 1 + $bulk.length;
2021-03-31 18:27:33 +02:00
2021-04-01 19:12:43 +02:00
$have.animate({ top: d_have }, drag.swapAnimMs, function(){ if (! --drag.inSwap) swapCleanUp(); });
$bulk.animate({ top: d_bulk }, drag.swapAnimMs, function(){ if (! --drag.inSwap) swapCleanUp(); });
2021-03-31 18:27:33 +02:00
2021-04-01 19:12:43 +02:00
function swapCleanUp()
{
$have.parent().css({ position: '' });
$have.remove();
2021-04-01 19:12:43 +02:00
$want.show();
$bulk.css({ position: '', 'z-index': '', top: '' });
drag.adjustDrag();
}
2021-04-01 19:12:43 +02:00
}
else
{
drag.inSwap = 1;
$want.slideDown(drag.swapAnimMs);
$have.slideUp(drag.swapAnimMs, function() {
$have.remove();
drag.inSwap = 0;
drag.adjustDrag();
});
}
2021-03-31 18:27:33 +02:00
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
this.onMouseMove = function(ev)
2021-03-30 10:18:01 +02:00
{
2021-04-01 19:12:43 +02:00
this.mouseEv = ev;
2021-03-31 18:27:33 +02:00
if (! this.item)
2021-03-30 10:18:01 +02:00
return;
2021-03-31 18:27:33 +02:00
if (this.priming)
{
2021-04-01 19:12:43 +02:00
var x = ev.clientX - this.primeXY.x;
var y = ev.clientY - this.primeXY.y;
2021-03-31 18:27:33 +02:00
if (x*x + y*y > 5*5)
this.onPrimed();
}
else
{
this.adjustDrag();
2021-03-30 10:18:01 +02:00
}
}
2021-03-31 18:27:33 +02:00
this.stopDragging = function()
2021-03-30 10:18:01 +02:00
{
2021-04-01 19:12:43 +02:00
var $item = $(this.item);
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
$item.removeClass('dragging');
2021-03-31 18:27:33 +02:00
$('body').removeClass('dragging');
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
if (this.$drag)
{
this.$drag.remove();
this.$drag = null;
2021-03-30 10:18:01 +02:00
2021-04-01 19:12:43 +02:00
removeTextSelection();
2021-03-31 18:27:33 +02:00
2021-04-01 19:12:43 +02:00
if (this.onDragging)
this.onDragging.call(this, false); // stopped
2021-03-31 18:27:33 +02:00
}
this.item = null;
}
2021-03-30 10:18:01 +02:00
}
2021-03-31 18:27:33 +02:00
</script>
2021-04-07 18:51:52 +02:00
<script type="text/javascript">
function VarAdjust()
{
// state
this.onChange = null;
this.onFinish = null;
2021-04-07 18:51:52 +02:00
this.startY = 0;
this.used = false;
// api
this.start = function(ev, onChange, onFinish)
2021-04-07 18:51:52 +02:00
{
if (! onChange)
return;
this.onChange = onChange;
this.onFinish = onFinish;
2021-04-07 18:51:52 +02:00
this.startY = ev.clientY;
this.used = false;
var self = this;
setTimeout(function(){
2021-04-08 13:24:35 +02:00
if (! self.onChange)
return;
$('body').addClass('adjusting');
self.used = true;
}, 250);
2021-04-07 18:51:52 +02:00
}
this.onMouseMove = function(ev)
{
if (! this.onChange)
return;
$('body').addClass('adjusting');
self.used = true;
this.onChange(ev.clientY - this.startY);
}
this.end = function()
{
if (! this.onChange)
return;
$('body').removeClass('adjusting');
this.onChange = null;
if (this.onFinish) this.onFinish();
2021-04-07 18:51:52 +02:00
}
}
</script>
2021-03-31 18:27:33 +02:00
<script type="text/javascript">
/*
* poor man's error handling -- $fixme
*/
var easyMartina = false;
2021-03-31 18:27:33 +02:00
window.onerror = function(message, file, line, col, e){
var cb1;
if (! easyMartina) alert("Error occurred: " + e.message);
2021-03-31 18:27:33 +02:00
return false;
};
window.addEventListener("error", function(e) {
var cb2;
if (! easyMartina) alert("Error occurred: " + e.error.message);
2021-03-31 18:27:33 +02:00
return false;
});
/*
2021-03-31 20:35:46 +02:00
* notes / lists / boards
2019-05-28 12:09:54 +02:00
*/
2021-03-31 18:27:33 +02:00
function addNote($list, $after, $before)
{
2021-03-31 18:27:33 +02:00
var $note = $('tt .note').clone();
var $notes = $list.find('.notes');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$note.find('.text').html('');
$note.addClass('brand-new');
2019-05-28 12:09:54 +02:00
if ($before && $before.length)
{
2021-03-31 18:27:33 +02:00
$before.before($note);
$note = $before.prev();
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
else
if ($after && $after.length)
{
2021-03-31 18:27:33 +02:00
$after.after($note);
$note = $after.next();
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
else
{
2021-03-31 18:27:33 +02:00
$notes.append($note);
$note = $notes.find('.note').last();
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
$note.find('.text').click();
}
2021-03-30 10:31:28 +02:00
2021-03-31 18:27:33 +02:00
function deleteNote($note)
{
$note
.animate({ opacity: 0 }, 'fast')
.slideUp('fast')
.queue(function(){
$note.remove();
saveBoard();
});
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
function noteLocation($item)
{
var loc = 0;
for (var $p = $item.closest('.note'); $p.length; $p = $p.prev(), loc += 1);
for (var $p = $item.closest('.list'); $p.length; $p = $p.prev(), loc += 10000);
return loc;
}
2021-03-31 18:27:33 +02:00
//
function addList()
{
var $board = $('.wrap .board');
var $lists = $board.find('.lists');
var $list = $('tt .list').clone();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$list.find('.text').html('');
$list.find('.head').addClass('brand-new');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$lists.append($list);
$board.find('.lists .list .head .text').last().click();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var lists = $lists[0];
lists.scrollLeft = Math.max(0, lists.scrollWidth - lists.clientWidth);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
setupListScrolling();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function deleteList($list)
{
var empty = true;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$list.find('.note .text').each(function(){
empty &= ($(this).html().length == 0);
});
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! empty && ! confirm("Delete this list and all its notes?"))
return;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$list
.animate({ opacity: 0 })
.queue(function(){
$list.remove();
saveBoard();
});
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
setupListScrolling();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function moveList($list, left)
{
var $a = $list;
var $b = left ? $a.prev() : $a.next();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var $menu_a = $a.find('> .head .menu .bulk');
var $menu_b = $b.find('> .head .menu .bulk');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var pos_a = $a.offset().left;
var pos_b = $b.offset().left;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$a.css({ position: 'relative' });
$b.css({ position: 'relative' });
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$menu_a.hide();
$menu_b.hide();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$a.animate({ left: (pos_b - pos_a) + 'px' }, 'fast');
$b.animate({ left: (pos_a - pos_b) + 'px' }, 'fast', function(){
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (left) $list.prev().before($list);
else $list.before($list.next());
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$a.css({ position: '', left: '' });
$b.css({ position: '', left: '' });
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$menu_a.css({ display: '' });
$menu_b.css({ display: '' });
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
saveBoard();
});
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
function openBoard(board_id)
{
closeBoard(true);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
NB.board = NB.storage.loadBoard(board_id, null);
NB.storage.setActiveBoard(board_id);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
showBoard(true);
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function reopenBoard(revision)
{
var board_id = NB.board.id;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var via_menu = $('.wrap .board > .head .menu .bulk').is(':visible');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
NB.storage.setBoardRevision(board_id, revision);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
openBoard(board_id);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (via_menu)
{
var $menu = $('.wrap .board > .head .menu');
var $teaser = $menu.find('.teaser');
var $bulk = $menu.find('.bulk');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$teaser.hide().delay(100).queue(function(){ $(this).css('display', '').dequeue(); });
$bulk.show().delay(100).queue(function(){ $(this).css('display', '').dequeue(); });
}
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function closeBoard(quick)
{
2021-04-25 14:37:57 +02:00
if (! NB.board)
return;
2021-03-31 18:27:33 +02:00
var $board = $('.wrap .board');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (quick)
$board.remove();
else
$board
.animate({ opacity: 0 }, 'fast')
.queue(function(){ $board.remove(); });
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
NB.board = null;
NB.storage.setActiveBoard(null);
2019-05-28 12:09:54 +02:00
2021-04-13 19:05:14 +02:00
// updateUndoRedo();
2021-03-31 18:27:33 +02:00
updateBoardIndex();
2021-04-13 19:05:14 +02:00
updatePageTitle();
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
function addBoard()
{
closeBoard(true);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
NB.board = new Board();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
showBoard(true);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap .board .head').addClass('brand-new');
$('.wrap .board .head .text').click();
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
function saveBoard()
{
var $board = $('.wrap .board');
var board = Object.assign(new Board(), NB.board); // id, revision & title
2021-03-31 20:35:46 +02:00
board.lists = [];
$board.find('.list').each(function(){
var $list = $(this);
var l = board.addList( getText($list.find('.head .text')) );
$list.find('.note').each(function(){
var $note = $(this)
var n = l.addNote( getText($note.find('.text')) );
n.raw = $note.hasClass('raw');
n.min = $note.hasClass('collapsed');
});
});
NB.storage.saveBoard(board);
NB.board = board;
updateUndoRedo();
updateBoardIndex();
}
2021-03-31 18:27:33 +02:00
function deleteBoard()
{
var $list = $('.wrap .board .list');
2021-04-13 19:05:14 +02:00
var board_id = NB.board.id;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if ($list.length && ! confirm("PERMANENTLY delete this board, all its lists and their notes?"))
return;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
closeBoard();
2021-04-13 19:05:14 +02:00
NB.storage.nukeBoard(board_id);
updateBoardIndex();
2021-03-31 18:27:33 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
function undoBoard()
{
if (! NB.board)
return false;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var hist = NB.storage.getBoardHistory(NB.board.id);
var have = NB.board.revision;
var want = 0;
for (var i=0; i<hist.length-1 && ! want; i++)
if (have == hist[i])
want = hist[i+1];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! want)
{
2021-03-31 18:27:33 +02:00
console.log('Undo - failed');
return false;
}
2021-03-30 10:31:28 +02:00
2021-03-31 18:27:33 +02:00
console.log('Undo -> ' + want);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
reopenBoard(want);
return true;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function redoBoard()
{
if (! NB.board)
return false;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var hist = NB.storage.getBoardHistory(NB.board.id);
var have = NB.board.revision;
var want = 0;
2021-03-30 10:31:28 +02:00
2021-03-31 18:27:33 +02:00
for (var i=1; i<hist.length && ! want; i++)
if (have == hist[i])
want = hist[i-1];
if (! want)
{
console.log('Redo - failed');
return false;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
console.log('Redo -> ' + want);
reopenBoard(want);
return true;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
//
function showBoard(quick)
{
var board = NB.board;
2021-03-30 10:37:15 +02:00
2021-03-31 18:27:33 +02:00
var $wrap = $('.wrap');
var $bdiv = $('tt .board');
var $ldiv = $('tt .list');
var $ndiv = $('tt .note');
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
var $b = $bdiv.clone();
var $b_lists = $b.find('.lists');
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
$b[0].board_id = board.id;
setText( $b.find('.head .text'), board.title );
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
board.lists.forEach(function(list){
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var $l = $ldiv.clone();
var $l_notes = $l.find('.notes');
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
setText( $l.find('.head .text'), list.title );
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
list.notes.forEach(function(n){
var $n = $ndiv.clone();
setText( $n.find('.text'), n.text );
if (n.raw) $n.addClass('raw');
if (n.min) $n.addClass('collapsed');
$l_notes.append($n);
});
$b_lists.append($l);
});
if (quick)
$wrap.html('').append($b);
else
2021-03-31 18:27:33 +02:00
$wrap.html('')
.css({ opacity: 0 })
.append($b)
.animate({ opacity: 1 });
updatePageTitle();
updateUndoRedo();
updateBoardIndex();
setupListScrolling();
}
2021-03-31 20:35:46 +02:00
/*
* demo board
*/
function createDemoBoard()
{
var blob =
'{"format":20190412,"id":1555071015420,"revision":581,"title":"Welcome to Nullboard","lists":[{"title":"The Use' +
'r Manual","notes":[{"text":"This is a note.\\nA column of notes is a list.\\nA set of lists is a board.","raw"' +
':false,"min":false},{"text":"All data is saved locally.\\nThe whole thing works completely offline.","raw":fal' +
'se,"min":false},{"text":"Last 50 board revisions are retained.","raw":false,"min":false},{"text":"Ctrl-Z is Un' +
'do - goes one revision back.\\nCtrl-Y is Redo - goes one revision forward.","raw":false,"min":false},{"tex' +
't":"Caveats","raw":true,"min":false},{"text":"Desktop-oriented.\\nMobile support is basically untested.","raw"' +
':false,"min":false},{"text":"Works in Firefox, Chrome is supported.\\nShould work in Safari, may work in Edge.' +
'","raw":false,"min":false},{"text":"Still very much in beta. Caveat emptor.","raw":false,"min":false},{"text":' +
'"Issues and suggestions","raw":true,"min":false},{"text":"Post them on Github.\\nSee \\"Nullboard\\" at the to' +
'p left for the link.","raw":false,"min":false}]},{"title":"Things to try","notes":[{"text":"\u2022 Click on ' +
'a note to edit.","raw":false,"min":false},{"text":"\u2022 Click outside of it when done editing.\\n\u2022 ' +
'Alternatively, use Shift-Enter.","raw":false,"min":false},{"text":"\u2022 To discard changes press Escape.",' +
'"raw":false,"min":false},{"text":"\u2022 Try Ctrl-Enter, see what it does.\\n\u2022 Try Ctrl-Shift-Enter t' +
'oo.","raw":false,"min":false},{"text":"\u2022 Hover over a note to show its \u2261 menu.\\n\u2022 Hover ' +
'over \u2261 to reveal the options.","raw":false,"min":false},{"text":"\u2022 X deletes the note.\\n\u2022' +
' R changes how a note looks.\\n\u2022 _ collapses the note.","raw":false,"min":false},{"text":"This is a ' +
'raw note.","raw":true,"min":false},{"text":"This is a collapsed note. Only its first line is visible. Useful f' +
'or keeping lists compact.","raw":false,"min":true}, {"text":"Links","raw":true,"min":false}, {"text":"Links pu' +
'lse on hover and can be opened via the right-click menu - https://nullboard.io","raw":false,"min":false}, {"tex' +
't":"Pressing CapsLock or Control highlights all links and makes them left-clickable.","raw":false,"min":false}]},{"title"' +
2021-03-31 20:35:46 +02:00
':"More things to try","notes":[{"text":"\u2022 Drag notes around to rearrange.\\n\u2022 Works between the ' +
'lists too.","raw":false,"min":false},{"text":"\u2022 Click on a list name to edit.\\n\u2022 Enter to save,' +
' Esc to cancel.","raw":false,"min":false},{"text":"\u2022 Try adding a new list.\\n\u2022 Try deleting one' +
'. This _can_ be undone.","raw":false,"min":false},{"text":"\u2022 Same for the board name.","raw":false,"m' +
'in":false},{"text":"Boards","raw":true,"min":false},{"text":"\u2022 Check out \u2261 at the top right.",' +
'"raw":false,"min":false},{"text":"\u2022 Try adding a new board.\\n\u2022 Try switching between the boards' +
'.","raw":false,"min":false},{"text":"\u2022 Try deleting a board. Unlike deleting a\\n list this _canno' +
't_ be undone.","raw":false,"min":false},{"text":"\u2022 Export the board (save to a file, as json)\\n' +
'\u2022 Import the board (load from a save)","raw":false,"min":false}]}]}';
var demo = JSON.parse(blob);
if (! demo)
return false;
demo.id = +new Date();
demo.revision = 0;
NB.storage.saveBoard(demo);
NB.storage.setActiveBoard(demo.id);
return Object.assign(new Board(), demo);
}
/*
* board export / import
*/
function exportBoard()
{
var blob, file;
if (! NB.board)
{
var index = NB.storage.getBoardIndex();
var all = [];
boards.forEach(function(meta, board_id){
all.push( NB.storage.loadBoard(board_id, null) );
})
blob = JSON.stringify(all);
file = `Nullboard.nbx`;
}
else
{
var board = NB.board;
blob = JSON.stringify(board);
file = `Nullboard-${board.id}-${board.title}.nbx`;
}
blob = encodeURIComponent(blob);
blob = "data:application/octet-stream," + blob;
return { blob: blob, file: file };
}
function checkImport(foo)
{
var props = [ 'format', 'id', 'revision', 'title', 'lists' ];
for (var i=0; i<props.length; i++)
if (! foo.hasOwnProperty(props[i]))
return "Required board properties are missing.";
if (! foo.id || ! foo.revision || ! Array.isArray(foo.lists))
return "Required board properties are empty.";
if (foo.format != NB.blobVersion)
2022-08-31 14:28:49 -05:00
return `Unsupported blob format "${foo.format}", expecting "${NB.blobVersion}".`;
2021-03-31 20:35:46 +02:00
return null;
}
function importBoard(blob)
{
var data;
try
{
data = JSON.parse(blob);
}
catch (x)
{
alert('File is not in a valid JSON format.');
return false;
}
if (! Array.isArray(data))
data = [ data ];
var index = NB.storage.getBoardIndex();
var msg, one, all = '';
for (var i=0; i<data.length; i++)
{
var board = data[i];
var whoops = checkImport(board);
if (whoops)
{
alert(whoops);
return false;
}
var title = board.title || '(untitled board)';
one = `"${title}", ID ${board.id}, revision ${board.revision}`;
all += ` ID ${board.id}, revision ${board.revision} - "${title}" \n`;
}
if (data.length == 1) msg = `Import a board called ${one} ?`;
else msg = `About to import the following boards:\n\n${all}\nProceed?`;
if (! confirm(msg))
return false;
var to_open = '';
2021-03-31 20:35:46 +02:00
for (var i=0; i<data.length; i++)
{
var board = data[i];
var check_title = true;
// check ID
2021-03-31 20:35:46 +02:00
if (index.has(board.id))
{
var which = (data.length == 1) ? "with the same ID" : board.id;
if (confirm(`Board ${which} already exists. Overwrite it?`) &&
confirm(`OVERWRITE for sure?`))
{
console.log(`Import: ${board.id} (${board.title} - will overwrite existing one`);
check_title = false;
}
else
if (confirm(`Import the board under a new ID?`))
{
var new_id = +new Date();
console.log(`Import: ${board.id} (${board.title} - will import as ${new_id}`);
board.id = new_id;
}
else
{
console.log(`Import: ${board.id} (${board.title} - ID conflict, will not import`);
continue;
}
}
if (check_title)
{
var retitle = false;
index.forEach( have => { retitle |= (have.title == board.title) } );
if (retitle) board.title += ' (imported)';
2021-03-31 20:35:46 +02:00
}
// ok, do the deed
2021-03-31 20:35:46 +02:00
board.revision--; // save will ++ it back
if (! NB.storage.saveBoard(board)) // this updates 'index'
{
alert(`Failed to save board ${board.id}. Import failed.`);
return false;
}
if (! to_open) to_open = data[0].id;
2021-03-31 20:35:46 +02:00
}
if (to_open) openBoard(to_open);
2021-03-31 20:35:46 +02:00
}
2021-04-13 19:05:14 +02:00
/*
*
*/
2021-04-25 14:37:57 +02:00
function findBackupAgent(which)
2021-04-18 13:55:59 +02:00
{
2021-04-25 14:37:57 +02:00
var a = null;
2021-04-18 13:55:59 +02:00
2021-04-25 14:37:57 +02:00
NB.storage.backups.agents.forEach(function(agent){
if (agent.type == which.type &&
agent.conf.auth == which.conf.auth &&
agent.conf.base == which.conf.base)
2021-04-18 13:55:59 +02:00
{
2021-04-25 14:37:57 +02:00
a = agent;
2021-04-18 13:55:59 +02:00
}
});
2021-04-25 14:37:57 +02:00
return a;
2021-04-18 13:55:59 +02:00
}
2021-04-19 17:52:26 +02:00
function setBackupConfigUi($div, backupConf)
2021-04-19 11:49:23 +02:00
{
if (! backupConf.enabled)
{
$div.addClass('off');
return;
}
var $status = $div.find('.status');
2021-04-25 14:37:57 +02:00
var b = findBackupAgent(backupConf);
2021-04-19 11:49:23 +02:00
var text = 'OK';
2021-04-25 16:30:17 +02:00
if (b && b.status == 'error')
2021-04-19 11:49:23 +02:00
{
2021-04-25 16:30:17 +02:00
text = b.lastXhr.text;
2021-04-19 11:49:23 +02:00
$status.addClass('error');
}
$status.find('input').val(text);
$status.css({ display: 'block' });
}
2021-04-19 17:52:26 +02:00
function getBackupConfigUi()
{
var conf = NB.storage.getConfig();
2021-04-25 14:37:57 +02:00
var loc = conf.backups.agents[0];
var rem = conf.backups.agents[1];
2021-04-19 17:52:26 +02:00
var $div = $('.overlay .backup-conf');
var $loc = $div.find('.loc');
var $rem = $div.find('.rem');
2021-04-25 14:37:57 +02:00
var ret =
{
loc: jsonClone(loc),
2021-04-19 17:52:26 +02:00
rem: jsonClone(rem)
};
ret.loc.enabled = ! $loc.hasClass('off');
ret.loc.conf.auth = $loc.find('.auth').val();
ret.rem.enabled = ! $rem.hasClass('off');
ret.rem.conf.base = $rem.find('.base').val();
ret.rem.conf.auth = $rem.find('.auth').val();
//
if (ret.loc.enabled && ! ret.loc.conf.auth)
{
shakeControl($loc.find('.auth'));
return null;
}
if (ret.rem.enabled && ! ret.rem.conf.base)
{
shakeControl($rem.find('.base'));
return null;
}
if (ret.rem.enabled && ! ret.rem.conf.auth)
{
shakeControl($rem.find('.auth'));
return null;
}
return ret;
}
2021-04-19 11:49:23 +02:00
function checkBackupConfig(backupConf, $div, onDone)
{
var $status = $div.find('.status');
var $text = $status.find('input');
$text.val('Checking...');
$status.removeClass('error').slideDown();
2021-04-19 13:09:04 +02:00
$div.delay(850).queue(function(){
var T = NB.backupTypes.get(backupConf.type);
2021-04-25 14:37:57 +02:00
var foo = new T(backupConf.id, backupConf.conf, function(){});
2021-04-19 13:09:04 +02:00
2021-04-25 16:37:15 +02:00
foo.checkStatus(function(ok){
2021-04-25 14:37:57 +02:00
2021-04-25 16:37:15 +02:00
if (ok)
2021-04-19 13:09:04 +02:00
{
$text.val('OK');
}
else
{
2021-04-25 14:37:57 +02:00
$text.val(foo.lastXhr.text);
2021-04-19 13:09:04 +02:00
$status.addClass('error');
}
onDone();
});
2021-04-19 11:49:23 +02:00
$(this).dequeue();
});
}
2021-04-18 13:55:59 +02:00
function configBackups()
2021-04-13 19:05:14 +02:00
{
var conf = NB.storage.getConfig();
2021-04-25 14:37:57 +02:00
if (conf.backups.agents.length != 2)
throw 'Invalid conf.backups.agents[]'; // as per fixupConfig()
2021-04-19 11:49:23 +02:00
//
2021-04-18 13:55:59 +02:00
var $div = $('tt .backup-conf').clone();
2021-04-19 17:52:26 +02:00
var div = $div[0];
2021-04-18 13:55:59 +02:00
var $loc = $div.find('.loc');
var $rem = $div.find('.rem');
var typ = (new SimpleBackup).type;
2021-04-25 14:37:57 +02:00
var loc = conf.backups.agents[0];
var rem = conf.backups.agents[1];
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
div.checking = 0;
2021-04-19 11:49:23 +02:00
//
2021-04-18 13:55:59 +02:00
$loc.find('.auth').val( loc.conf.auth );
$rem.find('.auth').val( rem.conf.auth );
$rem.find('.base').val( rem.conf.base );
2021-04-19 17:52:26 +02:00
setBackupConfigUi($loc, loc);
setBackupConfigUi($rem, rem);
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (! loc.enabled && ! rem.enabled)
$div.addClass('off');
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
//
$div.find('.opt').click(function(){
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
var $opt = $(this).parent();
2021-04-19 13:09:04 +02:00
2021-04-19 17:52:26 +02:00
if ($opt.hasClass('off'))
{
$opt.find('.etc')
.css({ opacity: 0 })
.slideDown('fast')
.animate({ opacity: 1 }, 'fast')
.queue(function(){
$opt.removeClass('off');
$div.removeClass('off');
$(this).css('opacity', '').dequeue();
})
2021-04-18 22:36:39 +02:00
2021-04-19 17:52:26 +02:00
$opt.find('input').first()
.delay(800)
.queue(function(){ $(this).focus().dequeue(); });
}
else
{
$opt.find('.etc')
.animate({ opacity: 0 }, 'fast')
.slideUp('fast')
.queue(function(){
$opt.addClass('off');
if ($loc.hasClass('off') && $rem.hasClass('off'))
$div.addClass('off');
$(this).css({ opacity: '' }).dequeue();
})
}
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
return false;
});
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
$div.find('.check').click(function(){
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (div.checking)
return false;
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
var foo = getBackupConfigUi();
if (! foo)
return false;
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (foo.loc.enabled)
2021-04-18 13:55:59 +02:00
{
2021-04-19 17:52:26 +02:00
div.checking++;
checkBackupConfig(foo.loc, $loc, function(){ div.checking--; });
2021-04-19 11:49:23 +02:00
}
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (foo.rem.enabled)
2021-04-19 11:49:23 +02:00
{
2021-04-19 17:52:26 +02:00
div.checking++;
checkBackupConfig(foo.rem, $rem, function(){ div.checking--; });
2021-04-18 13:55:59 +02:00
}
2021-04-19 17:52:26 +02:00
return false;
});
$div.find('.ok').click(function(){
var foo = getBackupConfigUi();
if (! foo)
return false;
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (foo.loc.enabled && ! loc.enabled)
2021-04-25 14:37:57 +02:00
foo.loc.id = typ + '-' + (conf.backups.nextId++);
2021-04-18 13:55:59 +02:00
2021-04-19 17:52:26 +02:00
if (foo.rem.enabled && ! rem.enabled)
2021-04-25 14:37:57 +02:00
foo.rem.id = typ + '-' + (conf.backups.nextId++);
2021-04-18 13:55:59 +02:00
2021-04-25 14:37:57 +02:00
conf.backups.agents[0] = foo.loc;
conf.backups.agents[1] = foo.rem;
2021-04-18 13:55:59 +02:00
2021-04-25 14:37:57 +02:00
NB.storage.initBackups(onBackupStatusChange);
2021-04-18 13:55:59 +02:00
NB.storage.saveConfig();
hideOverlay();
});
2021-04-19 17:52:26 +02:00
$div.find('a.close').click(function(){
2021-04-19 11:49:23 +02:00
hideOverlay();
});
2021-04-18 13:55:59 +02:00
showOverlay($div);
2021-04-14 13:54:03 +02:00
}
2021-04-25 14:37:57 +02:00
function onBackupStatusChange(agent)
2021-04-14 13:54:03 +02:00
{
2021-04-25 14:37:57 +02:00
var agents = NB.storage.backups.agents;
2021-04-14 13:54:03 +02:00
var $config = $('.config');
var $status = $('.config .teaser u')
2021-04-25 14:37:57 +02:00
// if (agent) console.log( `onBackupStatusChange: ${agent.id}, status ${agent.status}, op ${agent.lastOp}, xhr '${agent.lastXhr.text}' / ${agent.lastXhr.code}` );
// else console.log( `onBackupStatusChange: <generic>` );
if (! agents.length)
2021-04-14 13:54:03 +02:00
{
2021-04-14 17:00:49 +02:00
$config.removeClass('backups-on backup-err backing-up');
2021-04-14 13:54:03 +02:00
return;
}
$config.addClass('backups-on');
2021-04-25 14:37:57 +02:00
var busy = 0;
var error = 0;
var ready = 0;
2021-04-25 14:37:57 +02:00
agents.forEach(function(agent){
if (agent.status == 'busy') busy++; else
if (agent.status == 'error') error++; else
if (agent.status == 'ready') ready++; else
throw `Unknown status [${agent.status}] on backup agent ${agent.id}`;
});
if (error > 0) $config.addClass('backup-err').removeClass('backing-up'); else
if (busy > 0) $config.addClass('backing-up').removeClass('backup-err'); else
$config.removeClass('backing-up backup-err');
// process all pending backups if needed
2021-04-25 14:37:57 +02:00
if (! error && ! busy)
runPendingBackups();
}
2021-04-25 20:33:33 +02:00
function needsBackingUp(backupStatus, fields, agentIds)
{
var stale = false;
agentIds.forEach(function(id){
var obj = backupStatus[id];
if (obj) fields.forEach(function(f){ stale = !obj[f]; });
else stale = true;
});
return stale;
}
2021-04-25 14:37:57 +02:00
function runPendingBackups()
{
2021-04-25 20:33:33 +02:00
console.log('Checking for pending backups...');
var conf = NB.storage.getConfig();
var agentIds = [];
NB.storage.backups.agents.forEach(function(agent){
agentIds.push(agent.id);
});
if (needsBackingUp(conf.backupStatus, [ 'conf' ], agentIds))
{
console.log(" Backing up app config...");
NB.storage.backupConfig();
}
var boards = NB.storage.getBoardIndex();
boards.forEach(function(meta, id){
if (! needsBackingUp(meta.backupStatus, [ 'data', 'meta' ], agentIds))
return;
console.log(` Backing up board ${id}...`);
var board = NB.storage.loadBoard(id);
if (! board)
return;
NB.storage.backupBoard(id, board, meta)
});
2021-04-13 19:05:14 +02:00
}
2021-04-02 01:59:34 +02:00
/*
*
*/
function saveBoardOrder()
{
var $index = $('.config .load-board');
var spot = 1;
$index.each(function(){
var id = parseInt( $(this).attr('board_id') );
NB.storage.setBoardUiSpot(id, spot++);
});
}
2021-03-31 18:27:33 +02:00
/*
*
*/
function updatePageTitle()
{
var title = 'Nullboard';
if (NB.board)
{
2021-03-31 18:27:33 +02:00
title = NB.board.title;
title = 'NB - ' + (title || '(untitled board)');
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
document.title = title;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function updateUndoRedo()
{
var $undo = $('.board .menu .undo-board');
var $redo = $('.board .menu .redo-board');
var undo = false;
var redo = false;
if (NB.board && NB.board.revision)
{
2021-03-31 18:27:33 +02:00
var history = NB.storage.getBoardHistory(NB.board.id);
var rev = NB.board.revision;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
undo = (rev != history[history.length-1]);
redo = (rev != history[0]);
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (undo) $undo.show(); else $undo.hide();
if (redo) $redo.show(); else $redo.hide();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function updateBoardIndex()
{
var $index = $('.config .boards');
var $export = $('.config .exp-board');
2021-04-14 13:54:03 +02:00
var $backup = $('.config .auto-backup');
2021-03-31 18:27:33 +02:00
var $entry = $('tt .load-board');
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
var $board = $('.wrap .board');
var id_now = NB.board && NB.board.id;
var empty = true;
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
$index.html('');
$index.hide();
2020-02-20 01:13:00 +01:00
2021-03-31 18:27:33 +02:00
var boards = NB.storage.getBoardIndex();
2021-04-02 01:59:34 +02:00
var index = [];
2019-05-28 12:09:54 +02:00
2021-04-02 01:59:34 +02:00
boards.forEach(function(meta, id){ index.push({ id: id, meta: meta }); });
index.sort(function(a, b){ return b.meta.ui_spot && a.meta.ui_spot > b.meta.ui_spot; });
index.forEach(function(entry){
2021-03-31 18:27:33 +02:00
var $e = $entry.clone();
2021-04-02 01:59:34 +02:00
$e.attr('board_id', entry.id);
$e.html(entry.meta.title);
2019-05-28 12:09:54 +02:00
2021-04-02 01:59:34 +02:00
if (entry.id == id_now)
2021-03-31 18:27:33 +02:00
$e.addClass('active');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$index.append($e);
empty = false;
});
2021-03-31 20:20:49 +02:00
if (! empty)
{
if (id_now) $export.html('Export this board...').show();
else $export.html('Export all boards...').show();
2021-04-14 13:54:03 +02:00
$backup.show();
2021-03-31 20:20:49 +02:00
}
else
{
$export.hide();
2021-04-14 13:54:03 +02:00
$backup.hide();
2021-03-31 20:20:49 +02:00
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! empty) $index.show();
}
2019-05-28 12:09:54 +02:00
function setWhatsNew()
{
var conf = NB.storage.getConfig();
2021-04-10 14:32:14 +02:00
if (conf.verSeen && conf.verSeen < NB.codeVersion)
{
$('.logo').addClass('updated');
$('.logo .alert').html("(updated)");
}
var $link = $('.logo .view-changes');
var link = $link.attr('href') + '/?have=' + NB.codeVersion;
if (conf.verSeen) link += '&seen=' + conf.verSeen;
if (conf.verLast) link += '&last=' + conf.verLast;
$link.attr('href', link);
}
2021-03-31 18:27:33 +02:00
/*
2021-03-31 20:35:46 +02:00
* generic utils
*/
2021-04-19 11:49:23 +02:00
function jsonMatch(a, b)
{
return JSON.stringify(a) == JSON.stringify(b);
}
function jsonClone(x)
{
return JSON.parse(JSON.stringify(x));
}
2021-03-31 20:35:46 +02:00
function htmlEncode(raw)
{
return $('tt .encoder').text(raw).html();
}
function setText($note, text)
{
$note.attr('_text', text);
text = htmlEncode(text);
var hmmm = /\b(https?:\/\/[^\s]+)/mg;
2021-03-31 20:35:46 +02:00
text = text.replace(hmmm, function(url){
return '<a href="' + url + '" target=_blank>' + url + '</a>';
});
if ( NB.peek('fileLinks') )
{
var xmmm = /`(.*?)`/mg;
text = text.replace(xmmm, function(full, text){
link = 'file:///' + text.replace('\\', '/');
return '`<a href="' + link + '" target=_blank>' + text + '</a>`';
});
}
$note.html(text); // ? text : ' ');
2021-03-31 20:35:46 +02:00
}
function getText($note)
{
return $note.attr('_text');
}
2021-04-01 19:12:43 +02:00
function removeTextSelection()
{
if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); }
}
2021-04-18 13:55:59 +02:00
function shakeControl($x)
{
$x
.css({ position: 'relative' })
2021-04-18 22:36:39 +02:00
.focus()
2021-04-18 13:55:59 +02:00
.animate({ left: '+4px' }, 60)
.animate({ left: '-3px' }, 60)
.animate({ left: '+2px' }, 60)
.animate({ left: '0px' }, 60)
.queue(function(){
2021-04-18 22:36:39 +02:00
$x.css({ position: '', left: '' }).dequeue();
2021-04-18 13:55:59 +02:00
});
}
2021-03-31 20:35:46 +02:00
/*
* inline editing
2021-03-31 18:27:33 +02:00
*/
function startEditing($text, ev)
{
var $note = $text.parent();
var $edit = $note.find('.edit');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$note[0]._collapsed = $note.hasClass('collapsed');
$note.removeClass('collapsed');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$edit.val( getText($text) );
$edit.width( $text.width() );
2021-03-31 18:27:33 +02:00
$edit.height( $text.height() );
$note.addClass('editing');
2021-03-31 18:27:33 +02:00
$edit.focus();
}
2021-03-31 18:27:33 +02:00
function stopEditing($edit, via_escape, via_xclick)
{
var $item = $edit.parent();
if (! $item.hasClass('editing'))
return;
2021-03-31 18:27:33 +02:00
$item.removeClass('editing');
if ($item[0]._collapsed)
$item.addClass('collapsed')
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
var $text = $item.find('.text');
var text_now = $edit.val().trimRight();
var text_was = getText( $text );
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
var brand_new = $item.hasClass('brand-new');
$item.removeClass('brand-new');
2019-05-28 12:09:54 +02:00
if (via_escape)
{
if (brand_new)
$item.closest('.note, .list, .board').remove();
2021-03-31 18:27:33 +02:00
return;
2019-05-28 12:09:54 +02:00
}
2021-03-31 18:27:33 +02:00
if (via_xclick && brand_new && !text_now.length)
{
$item.closest('.note, .list, .board').remove();
return;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (text_now != text_was || brand_new)
{
setText( $text, text_now );
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if ($item.parent().hasClass('board'))
NB.board.title = text_now;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
updatePageTitle();
saveBoard();
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
if (brand_new && $item.hasClass('list'))
addNote($item);
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
function handleTab(ev)
{
var $this = $(this);
var $note = $this.closest('.note');
var $sibl = ev.shiftKey ? $note.prev() : $note.next();
if ($sibl.length)
{
stopEditing($this, false, false);
$sibl.find('.text').click();
}
}
//
function setRevealState(ev)
{
var raw = ev.originalEvent;
var do_reveal = raw.getModifierState && (raw.getModifierState( 'CapsLock' ) || raw.getModifierState( 'Control' ));
2021-03-31 20:35:46 +02:00
if (do_reveal) $('body').addClass('reveal');
else $('body').removeClass('reveal');
2021-03-31 20:35:46 +02:00
}
2019-05-28 12:09:54 +02:00
//
2021-03-31 18:27:33 +02:00
function showDing()
{
$('body')
.addClass('ding')
.delay(250)
.queue(function(){ $(this).removeClass('ding').dequeue(); });
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
/*
* overlay
*/
2021-03-31 18:27:33 +02:00
function showOverlay($div)
{
$('.overlay')
.html('')
.append($div)
2021-04-18 13:55:59 +02:00
.css({ opacity: 0, display: 'flex' })
2021-03-31 18:27:33 +02:00
.animate({ opacity: 1 });
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function hideOverlay()
{
$('.overlay').animate({ opacity: 0 }, function(){
$(this).hide();
});
}
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
function haveOverlay()
{
return $('.overlay').css('display') != 'none';
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
/*
* license popup
*/
2021-03-31 18:27:33 +02:00
function formatLicense()
{
var text = document.head.childNodes[1].nodeValue;
var pos = text.search('LICENSE');
var qos = text.search('Software:');
var bulk;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
bulk = text.substr(pos, qos-pos);
bulk = bulk.replace(/([^\n])\n\t/g, '$1 ');
bulk = bulk.replace(/\n\n\t/g, '\n\n');
bulk = bulk.replace(/([A-Z ]{7,})/g, '<u>$1</u>');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
var c1 = [];
var c2 = [];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
text.substr(qos).trim().split('\n').forEach(function(line){
line = line.split(':');
c1.push( line[0].trim() + ':' );
c2.push( line[1].trim() );
});
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
bulk += '<span>' + c1.join('<br>') + '</span>';
bulk += '<span>' + c2.join('<br>') + '</span>';
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
//
var links =
[
{ text: '2-clause BSD license', href: 'https://opensource.org/licenses/BSD-2-Clause/' },
{ text: 'Commons Clause', href: 'https://commonsclause.com/' }
];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
links.forEach(function(l){
bulk = bulk.replace(l.text, '<a href="' + l.href + '" target=_blank>' + l.text + '</a>');
});
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
return bulk.trim();
}
2019-05-28 12:09:54 +02:00
2021-03-31 20:35:46 +02:00
/*
* adjust this and that
*/
2021-03-31 18:27:33 +02:00
function adjustLayout()
{
var $body = $('body');
var $board = $('.board');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (! $board.length)
return;
2019-05-28 12:09:54 +02:00
2021-04-06 19:54:47 +02:00
var list_w = getListWidth();
2021-03-31 18:27:33 +02:00
var lists = $board.find('.list').length;
2021-04-06 19:54:47 +02:00
var lists_w = (lists < 2) ? list_w : (list_w + 10) * lists - 10;
2021-03-31 18:27:33 +02:00
var body_w = $body.width();
if (lists_w + 190 <= body_w)
{
$board.css('max-width', '');
$body.removeClass('crowded');
}
else
{
2021-04-06 19:54:47 +02:00
var max = Math.floor( (body_w - 40) / (list_w + 10) );
max = (max < 2) ? list_w : (list_w + 10) * max - 10;
2021-03-31 18:27:33 +02:00
$board.css('max-width', max + 'px');
$body.addClass('crowded');
}
}
2019-05-28 12:09:54 +02:00
//
2021-03-31 18:27:33 +02:00
function adjustListScroller()
{
var $board = $('.board');
if (! $board.length)
return;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var $lists = $('.board .lists');
var $scroller = $('.board .lists-scroller');
var $inner = $scroller.find('div');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
var max = $board.width();
var want = $lists[0].scrollWidth;
2021-04-07 18:51:52 +02:00
var have = $inner.outerWidth();
2019-05-28 12:09:54 +02:00
2021-04-08 13:24:35 +02:00
if (want <= max+5)
2021-03-31 18:27:33 +02:00
{
$scroller.hide();
return;
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$scroller.show();
if (want == have)
return;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$inner.width(want);
cloneScrollPos($lists, $scroller);
}
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
function cloneScrollPos($src, $dst)
{
var src = $src[0];
var dst = $dst[0];
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (src._busyScrolling)
{
src._busyScrolling--;
return;
}
2021-03-31 18:27:33 +02:00
dst._busyScrolling++;
dst.scrollLeft = src.scrollLeft;
}
2020-02-19 14:25:11 +01:00
2021-03-31 18:27:33 +02:00
function setupListScrolling()
{
var $lists = $('.board .lists');
var $scroller = $('.board .lists-scroller');
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
adjustListScroller();
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$lists[0]._busyScrolling = 0;
$scroller[0]._busyScrolling = 0;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$scroller.on('scroll', function(){ cloneScrollPos($scroller, $lists); });
$lists .on('scroll', function(){ cloneScrollPos($lists, $scroller); });
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
adjustLayout();
}
2019-05-28 12:09:54 +02:00
/*
2021-04-06 13:21:48 +02:00
* dragsters
*/
2021-04-06 13:21:48 +02:00
function initDragAndDrop()
{
NB.noteDrag = new Drag2();
NB.noteDrag.listSel = '.board .list .notes';
2021-04-06 13:21:48 +02:00
NB.noteDrag.itemSel = '.note';
NB.noteDrag.dragster = 'note-dragster';
NB.noteDrag.onDragging = function(started)
{
var drag = this;
var $note = $(drag.item);
if (started)
{
var $drag = drag.$drag;
if ($note.hasClass('collapsed'))
$drag.addClass('collapsed');
$drag.html('<div class=text></div>');
$drag.find('.text').html( $note.find('.text').html() );
drag.org_loc = noteLocation($note);
if ($note.hasClass('collapsed'))
drag.$drag.addClass('collapsed');
}
else
{
2021-04-13 19:05:14 +02:00
if (this.org_loc != noteLocation($note))
2021-04-06 13:21:48 +02:00
saveBoard();
}
}
NB.loadDrag = new Drag2();
NB.loadDrag.listSel = '.config .boards';
NB.loadDrag.itemSel = 'a.load-board';
NB.loadDrag.dragster = 'load-dragster';
NB.loadDrag.onDragging = function(started)
{
var drag = this;
if (started)
{
var $drag = drag.$drag;
$('.config .teaser').css({ display: 'none' });
$('.config .bulk').css({ display: 'block', opacity: 1 });
$drag.html( $(this.item).html() );
}
else
{
$('.config .teaser').css({ display: '' });
$('.config .bulk')
.show()
.delay(250)
.queue(function(){ $(this).css({ display: '', opacity: '' }).dequeue(); });
saveBoardOrder();
}
}
}
/*
* fonts
*/
function initFonts()
{
2021-04-06 19:09:14 +02:00
var toGo = 0;
var loaded = [];
var failed = [];
2021-04-06 13:21:48 +02:00
NB.font = null; // current font
//
2021-04-06 19:09:14 +02:00
function isUsable(f)
{
return ! failed.includes(f) && loaded.includes(f);
}
2021-04-06 13:21:48 +02:00
function onFontsLoaded()
{
var conf = NB.storage.getConfig();
2021-04-06 19:09:14 +02:00
$('.config .switch-font').each(function(){
if (! isUsable($(this).attr('font')))
$(this).remove();
});
if (conf.fontName && ! isUsable(conf.fontName))
2021-04-06 13:21:48 +02:00
{
NB.storage.setFontName(null);
2021-04-06 13:21:48 +02:00
selectFont(null);
}
2021-04-06 19:09:14 +02:00
selectFont(conf.fontName || 'barlow');
2021-04-06 13:21:48 +02:00
2021-04-06 19:09:14 +02:00
if (conf.fontSize)
setFontSize(conf.fontSize);
2021-04-06 19:09:14 +02:00
if (conf.lineHeight)
setLineHeight(conf.lineHeight);
2022-11-10 12:50:04 -05:00
if (conf.listWidth)
setListWidth(conf.listWidth);
2021-04-07 18:51:52 +02:00
updateVarsAndLayout();
2021-04-06 13:21:48 +02:00
}
function onFontLoaded(f, ok)
{
var m = f.family.match(/["']?f-([^"']*)/);
var f_name = m ? m[1] : ''; /* ios safari will set 'family' to 'weight' on failure ! */
2021-04-06 13:21:48 +02:00
if (! ok)
{
2021-04-06 19:09:14 +02:00
console.log( `! Failed to load ${f.family} ${f.weight}` );
failed.push(f_name);
2021-04-06 13:21:48 +02:00
}
else
{
2021-04-06 19:09:14 +02:00
loaded.push(f_name);
2021-04-06 13:21:48 +02:00
}
2021-04-06 19:09:14 +02:00
if (! --toGo)
2021-04-06 13:21:48 +02:00
onFontsLoaded();
}
document.fonts.forEach(function(f){
2021-04-06 19:09:14 +02:00
2021-04-06 13:21:48 +02:00
if (f.status == 'loaded')
return;
console.log( `Loading ${f.family} ${f.weight} ...` );
2021-04-06 19:09:14 +02:00
toGo++;
f.load()
.then(function(){ onFontLoaded(f, true); })
.catch(function(){ onFontLoaded(f, false); });
2021-04-06 13:21:48 +02:00
});
}
function selectFont(font)
{
2021-04-06 18:37:26 +02:00
var $html = $('html');
$html.removeClass('f-' + NB.font).addClass('f-' + font);
2021-04-06 13:21:48 +02:00
NB.font = font;
var $list = $('.config .switch-font');
$list.removeClass('active');
$list.filter('[font="' + font + '"]').addClass('active');
2021-04-06 13:21:48 +02:00
2021-04-07 18:51:52 +02:00
updateVarsAndLayout();
2021-04-06 13:21:48 +02:00
}
//
2021-04-07 13:56:10 +02:00
function getVar(name)
{
var v = $('html').css(name);
var m = v.match(/^\s*calc\((.*)\)$/);
if (m) v = eval(m[1]);
return parseFloat( v );
}
2021-04-06 13:21:48 +02:00
function getFontSize()
{
2021-04-07 13:56:10 +02:00
return getVar('--fs');
}
2021-04-06 13:21:48 +02:00
function getLineHeight()
{
2021-04-07 13:56:10 +02:00
return getVar('--lh');;
}
2021-04-06 19:32:46 +02:00
function getListWidth()
{
2021-04-07 13:56:10 +02:00
return parseInt( getVar('--lw') );
2021-04-06 19:32:46 +02:00
}
2021-04-06 13:21:48 +02:00
//
function updateFontSize()
{
var val = getFontSize();
2021-04-06 18:37:26 +02:00
$('.config .f-prefs .ui-fs .val').html( val.toFixed(1) );
return val;
2021-04-06 13:21:48 +02:00
}
2021-04-01 19:12:43 +02:00
2021-04-06 13:21:48 +02:00
function updateLineHeight()
2021-04-01 19:12:43 +02:00
{
2021-04-06 13:21:48 +02:00
var val = getLineHeight();
2021-04-06 18:37:26 +02:00
$('.config .f-prefs .ui-lh .val').html( val.toFixed(1) );
return val;
2021-04-06 13:21:48 +02:00
}
2021-04-01 19:12:43 +02:00
2021-04-06 19:32:46 +02:00
function updateListWidth()
{
var val = getListWidth();
$('.config .f-prefs .ui-lw .val').html( val.toFixed(0) );
return val;
}
2021-04-07 18:51:52 +02:00
function updateVarsAndLayout()
2021-04-07 13:56:10 +02:00
{
updateFontSize();
updateLineHeight();
updateListWidth();
2021-04-07 18:51:52 +02:00
adjustLayout();
2021-04-07 13:56:10 +02:00
}
2021-04-06 13:21:48 +02:00
//
function setFontSize(fs)
{
2021-04-07 18:51:52 +02:00
fs = fs.clamp(9, 24);
$('html').css('--fs', fs + '').addClass('fs-set');
updateVarsAndLayout();
if (getLineHeight() < fs)
setLineHeight(fs);
2021-04-06 13:21:48 +02:00
return getFontSize();
2021-04-01 19:12:43 +02:00
}
2021-04-06 13:21:48 +02:00
function setLineHeight(lh)
2021-04-02 01:59:34 +02:00
{
2021-04-07 18:51:52 +02:00
var fs = getFontSize();
2021-04-07 13:56:10 +02:00
lh = parseInt(10*lh) / 10.; // trim to a single decimal digit
2021-04-07 18:51:52 +02:00
lh = lh.clamp(fs, 3*fs);
$('html').css('--lh', lh + '').addClass('lh-set');
updateVarsAndLayout();
2021-04-07 13:56:10 +02:00
return getLineHeight();
2021-04-06 13:21:48 +02:00
}
2021-04-06 19:32:46 +02:00
function setListWidth(lw)
{
2021-04-07 18:51:52 +02:00
lw = lw.clamp(200, 400);
$('html').css('--lw', lw + '').addClass('lw-set');
updateVarsAndLayout();
2021-04-06 19:32:46 +02:00
return getListWidth();
}
2021-04-06 13:21:48 +02:00
//
function resetFontSize()
{
2021-04-06 18:37:26 +02:00
$('html').css('--fs', '').removeClass('fs-set');
2021-04-07 18:51:52 +02:00
updateVarsAndLayout();
return updateFontSize();
2021-04-06 13:21:48 +02:00
}
function resetLineHeight()
{
2021-04-06 18:37:26 +02:00
$('html').css('--lh', '').removeClass('lh-set');
2021-04-07 18:51:52 +02:00
updateVarsAndLayout();
return updateLineHeight();
2021-04-02 01:59:34 +02:00
}
2021-04-06 19:32:46 +02:00
function resetListWidth()
{
$('html').css('--lw', '').removeClass('lw-set');
2021-04-07 18:51:52 +02:00
updateVarsAndLayout();
2021-04-06 19:32:46 +02:00
return updateListWidth();
}
//
function saveUiPrefs()
{
var $html = $('html');
NB.storage.setFontSize ( $html.hasClass('fs-set') ? getFontSize() : null );
NB.storage.setLineHeight ( $html.hasClass('lh-set') ? getLineHeight() : null );
NB.storage.setListWidth ( $html.hasClass('lw-set') ? getListWidth() : null );
}
2021-03-31 18:27:33 +02:00
/*
2021-03-31 20:35:46 +02:00
* event handlers
2021-03-31 18:27:33 +02:00
*/
$(window).on('blur', function(){
$('body').removeClass('reveal');
});
2020-02-19 14:25:11 +01:00
2021-03-31 18:27:33 +02:00
$(document).on('keydown', function(ev){
setRevealState(ev);
});
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
$(document).on('keyup', function(ev){
2021-03-30 10:18:01 +02:00
2021-03-31 18:27:33 +02:00
var raw = ev.originalEvent;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
setRevealState(ev);
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (ev.target.nodeName == 'TEXTAREA' ||
ev.target.nodeName == 'INPUT')
return;
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (ev.ctrlKey && (raw.code == 'KeyZ'))
{
var ok = ev.shiftKey ? redoBoard() : undoBoard();
if (! ok)
showDing();
}
else
if (ev.ctrlKey && (raw.code == 'KeyY'))
{
if (! redoBoard())
showDing();
}
});
2021-03-30 10:37:15 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .text', function(ev){
2019-05-28 12:09:54 +02:00
2021-03-31 18:27:33 +02:00
if (this.was_dragged)
{
this.was_dragged = false;
return false;
}
2021-04-02 01:59:34 +02:00
NB.noteDrag.cancelPriming();
2021-03-31 18:27:33 +02:00
startEditing($(this), ev);
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .note .text a', function(ev){
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
if (! $('body').hasClass('reveal'))
return true;
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
ev.stopPropagation();
return true;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('keydown', '.board .edit', function(ev){
2021-03-31 12:35:41 +02:00
var $this = $(this);
var $note = $this.closest('.note');
var $list = $this.closest('.list');
var isNote = $note.length > 0;
var isList = $list.length > 0;
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
// esc
if (ev.keyCode == 27)
2021-03-31 12:35:41 +02:00
{
stopEditing($this, true, false);
2021-03-31 18:27:33 +02:00
return false;
2021-03-31 12:35:41 +02:00
}
2021-03-31 18:27:33 +02:00
// tab
if (ev.keyCode == 9)
2021-03-31 12:35:41 +02:00
{
2021-03-31 18:27:33 +02:00
handleTab.call(this, ev);
return false;
2021-03-31 12:35:41 +02:00
}
2021-03-31 18:27:33 +02:00
// done
if (ev.keyCode == 13 && ev.altKey ||
2021-03-31 18:27:33 +02:00
ev.keyCode == 13 && ev.shiftKey && ! ev.ctrlKey)
2021-03-31 12:35:41 +02:00
{
stopEditing($this, false, false);
2021-03-31 18:27:33 +02:00
return false;
2021-03-31 12:35:41 +02:00
}
2021-03-31 18:27:33 +02:00
// done + (add after / add before)
if (ev.keyCode == 13 && ev.ctrlKey)
2021-03-31 12:35:41 +02:00
{
2021-03-31 18:27:33 +02:00
stopEditing($this, false, false);
2021-03-31 12:35:41 +02:00
if (isNote)
{
if (ev.shiftKey) // ctrl-shift-enter
addNote($list, null, $note);
else
addNote($list, $note);
}
2021-03-31 18:27:33 +02:00
else
if (isList)
{
$note = $list.find('.note').last();
2021-03-31 18:27:33 +02:00
addNote($list, $note);
}
else
{
addList();
}
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
return false;
}
2021-03-31 12:35:41 +02:00
// done on Enter if editing board or list title
if (ev.keyCode == 13 && ! isNote)
{
stopEditing($this, false, false);
return false;
}
2021-03-31 18:27:33 +02:00
// done + collapse
if (isNote && ev.altKey && ev.key == 'ArrowUp')
{
var $item = $this.parent();
2021-03-31 18:27:33 +02:00
$item[0]._collapsed = true;
stopEditing($this, false, false);
2021-03-31 18:27:33 +02:00
return false;
}
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
// done + expand
if (isNote && ev.altKey && ev.key == 'ArrowDown')
{
var $item = $this.parent();
2021-03-31 18:27:33 +02:00
$item[0]._collapsed = false;
stopEditing($this, false, false);
2021-03-31 18:27:33 +02:00
return false;
2021-03-31 12:35:41 +02:00
}
2021-03-31 18:27:33 +02:00
// done + toggle 'raw'
if (isNote && ev.altKey && ev.keyCode == 82)
2021-03-31 12:35:41 +02:00
{
$this.parent().toggleClass('raw');
stopEditing($this, false, false);
2021-03-31 18:27:33 +02:00
return false;
}
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
// ctrl-shift-8
if (isNote && ev.key == '*' && ev.ctrlKey)
{
var have = this.value;
var pos = this.selectionStart;
var want = have.substr(0, pos) + '\u2022 ' + have.substr(this.selectionEnd);
$this.val(want);
2021-03-31 18:27:33 +02:00
this.selectionStart = this.selectionEnd = pos + 2;
return false;
}
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
return true;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('keypress', '.board .edit', function(ev){
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
// tab
if (ev.keyCode == 9)
2021-03-31 12:35:41 +02:00
{
2021-03-31 18:27:33 +02:00
handleTab.call(this, ev);
return false;
}
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('blur', '.board .edit', function(ev){
if (document.activeElement != this)
stopEditing($(this), false, true);
else
; // switch away from the browser window
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('input propertychange', '.board .note .edit', function(){
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
var delta = $(this).outerHeight() - $(this).height();
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$(this).height(10);
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
if (this.scrollHeight > this.clientHeight)
$(this).height(this.scrollHeight - delta);
2021-03-31 18:27:33 +02:00
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.config').on('click', '.add-board', function(){
addBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.config').on('click', '.load-board', function(){
2021-03-31 12:35:41 +02:00
2021-04-02 01:59:34 +02:00
var board_id = parseInt( $(this).attr('board_id') );
NB.loadDrag.cancelPriming();
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
if (NB.board && (NB.board.id == board_id))
closeBoard();
else
2021-04-02 01:59:34 +02:00
openBoard(board_id);
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .del-board', function(){
deleteBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .undo-board', function(){
undoBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .redo-board', function(){
redoBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('click', '.board .add-list', function(){
addList();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .del-list', function(){
deleteList( $(this).closest('.list') );
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .mov-list-l', function(){
moveList( $(this).closest('.list'), true );
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .mov-list-r', function(){
moveList( $(this).closest('.list'), false );
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('click', '.board .add-note', function(){
addNote( $(this).closest('.list') );
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .del-note', function(){
deleteNote( $(this).closest('.note') );
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .raw-note', function(){
$(this).closest('.note').toggleClass('raw');
saveBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.wrap').on('click', '.board .collapse', function(){
$(this).closest('.note').toggleClass('collapsed');
saveBoard();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
$('.wrap').on('mousedown', '.board .note .text', function(ev){
2022-11-10 12:50:04 -05:00
NB.noteDrag.prime(this.parentNode, ev);
2021-04-02 01:59:34 +02:00
});
$('.config').on('mousedown', 'a.load-board', function(ev){
if ($('.config a.load-board').length > 1)
NB.loadDrag.prime(this, ev);
2021-03-31 18:27:33 +02:00
});
2021-03-31 12:35:41 +02:00
2021-04-07 18:51:52 +02:00
//
$('.config').on('mousedown', '.ui-fs .val', function(ev){
var org = getFontSize();
NB.varAdjust.start(ev, function(delta){ setFontSize( org + delta/50. ); }, saveUiPrefs);
2021-04-07 18:51:52 +02:00
});
$('.config').on('mousedown', '.ui-lh .val', function(ev){
var org = getLineHeight();
NB.varAdjust.start(ev, function(delta){ setLineHeight( org + delta/50. ); }, saveUiPrefs);
2021-04-07 18:51:52 +02:00
});
$('.config').on('mousedown', '.ui-lw .val', function(ev){
var org = getListWidth();
NB.varAdjust.start(ev, function(delta){ setListWidth( org + delta/5. ); }, saveUiPrefs);
2021-04-07 18:51:52 +02:00
});
//
2021-03-31 18:27:33 +02:00
$(document).on('mouseup', function(ev){
2021-04-02 01:59:34 +02:00
NB.noteDrag.end();
NB.loadDrag.end();
2021-04-07 18:51:52 +02:00
NB.varAdjust.end();
2021-03-31 18:27:33 +02:00
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$(document).on('mousemove', function(ev){
setRevealState(ev);
2021-04-02 01:59:34 +02:00
NB.noteDrag.onMouseMove(ev);
NB.loadDrag.onMouseMove(ev);
2021-04-07 18:51:52 +02:00
NB.varAdjust.onMouseMove(ev);
2021-03-31 18:27:33 +02:00
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
2021-04-14 13:54:03 +02:00
$('.config .imp-board').on('click', function(ev){
2021-03-31 18:27:33 +02:00
$('.config .imp-board-select').click();
return false;
});
2021-03-31 12:35:41 +02:00
2021-04-14 13:54:03 +02:00
$('.config .imp-board-select').on('change' , function(){
2021-03-31 18:27:33 +02:00
var files = this.files;
var reader = new FileReader();
reader.onload = function(ev){ importBoard(ev.target.result); };
reader.readAsText(files[0]);
return true;
});
2021-03-31 12:35:41 +02:00
2021-04-14 13:54:03 +02:00
$('.config .exp-board').on('click', function(){
2021-03-31 20:20:49 +02:00
var pack = exportBoard();
$(this).attr('href', pack.blob);
$(this).attr('download', pack.file);
2021-03-31 18:27:33 +02:00
return true;
});
2021-03-31 12:35:41 +02:00
2021-04-14 13:54:03 +02:00
$('.config .auto-backup').on('click', function(){
2021-04-18 13:55:59 +02:00
configBackups();
2021-04-14 13:54:03 +02:00
});
2021-04-06 16:51:20 +02:00
//
$('.config .section .title').on('click', function(){
$(this).closest('.section').toggleClass('open');
2021-03-31 18:27:33 +02:00
return false;
});
2021-03-31 12:35:41 +02:00
$('.config').on('click', '.switch-font', function(){
var font = $(this).attr('font');
selectFont(font);
NB.storage.setFontName(font);
2021-03-31 18:27:33 +02:00
return false;
});
2021-03-31 12:35:41 +02:00
2021-04-06 13:21:48 +02:00
//
$('.config .f-prefs .ui-fs .less').on('click', function(){
setFontSize( parseInt(10*getFontSize()) / 10. - 0.5 );
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
2021-04-06 13:21:48 +02:00
$('.config .f-prefs .ui-fs .val').on('click', function(){
2021-04-07 18:51:52 +02:00
if (NB.varAdjust.used) return false;
2021-04-06 13:21:48 +02:00
var fs = resetFontSize();
2021-04-07 18:51:52 +02:00
if (getLineHeight() < fs) setLineHeight(fs);
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
2021-04-06 13:21:48 +02:00
$('.config .f-prefs .ui-fs .more').on('click', function(){
setFontSize( parseInt(10*getFontSize()) / 10. + 0.5 );
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
//
2021-04-06 13:21:48 +02:00
$('.config .f-prefs .ui-lh .less').on('click', function(){
setLineHeight( parseInt(10*getLineHeight()) / 10. - 0.1 );
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
2021-04-06 13:21:48 +02:00
$('.config .f-prefs .ui-lh .val').on('click', function(){
2021-04-07 18:51:52 +02:00
if (NB.varAdjust.used) return false;
2021-04-06 13:21:48 +02:00
var lh = resetLineHeight();
2021-04-07 18:51:52 +02:00
if (lh < getFontSize()) setFontSize(lh);
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
$('.config .f-prefs .ui-lh .more').on('click', function(){
setLineHeight( parseInt(10*getLineHeight()) / 10. + 0.1 );
saveUiPrefs();
2021-04-06 13:21:48 +02:00
return false;
});
2021-04-06 19:32:46 +02:00
//
$('.config .f-prefs .ui-lw .less').on('click', function(){
setListWidth( getListWidth() - 5 );
saveUiPrefs();
2021-04-06 19:32:46 +02:00
return false;
});
$('.config .f-prefs .ui-lw .val').on('click', function(){
2021-04-07 18:51:52 +02:00
if (NB.varAdjust.used) return false;
2021-04-06 19:32:46 +02:00
resetListWidth();
saveUiPrefs();
2021-04-06 19:32:46 +02:00
return false;
});
$('.config .f-prefs .ui-lw .more').on('click', function(){
setListWidth( getListWidth() + 5 );
saveUiPrefs();
2021-04-06 19:32:46 +02:00
return false;
});
2021-04-06 16:51:20 +02:00
//
2021-04-14 17:00:49 +02:00
$('.config .switch-theme').on('click', function() {
2021-04-06 16:51:20 +02:00
var $html = $('html');
$html.toggleClass('theme-dark');
NB.storage.setTheme($html.hasClass('theme-dark') ? 'dark' : '');
return false;
});
2021-03-31 18:27:33 +02:00
//
$('.overlay').click(function(ev){
if (ev.originalEvent.target != this)
return true;
hideOverlay();
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$(window).keydown(function(ev){
if (haveOverlay() && ev.keyCode == 27)
hideOverlay();
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.view-about').click(function(){
var $div = $('tt .about').clone();
2021-03-31 20:49:29 +02:00
$div.find('div').html(`Version ${NB.codeVersion}`);
2021-03-31 18:27:33 +02:00
showOverlay($div);
return false;
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
$('.view-license').click(function(){
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
var $div = $('tt .license').clone();
$div.html(formatLicense());
showOverlay($div);
return false;
});
2021-03-31 12:35:41 +02:00
$('.view-changes').click(function(){
if (! $('.logo').hasClass('updated'))
return;
NB.storage.setVerSeen();
$('.logo').removeClass('updated');
});
2021-04-18 13:55:59 +02:00
/***/
2021-03-31 18:27:33 +02:00
$(window).resize(adjustLayout);
2021-03-31 12:35:41 +02:00
$('body').on('dragstart', function(){ return false; });
2021-03-31 18:27:33 +02:00
/*
* the init()
*/
2021-04-06 13:21:48 +02:00
var NB =
{
codeVersion: 20231105,
2021-04-07 18:51:52 +02:00
blobVersion: 20190412, // board blob format in Storage
2021-04-06 13:21:48 +02:00
board: null,
storage: null,
peek: function(name)
{
return this.storage.getConfig()[name];
},
poke: function(name, val)
{
var conf = this.storage.getConfig();
conf[name] = val;
return this.storage.saveConfig();
}
2021-04-06 13:21:48 +02:00
};
NB.storage = new Storage_Local();
2021-03-31 18:27:33 +02:00
if (! NB.storage.open())
{
easyMartina = true;
throw new Error();
2021-03-31 18:27:33 +02:00
}
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
var boards = NB.storage.getBoardIndex();
2021-03-31 12:35:41 +02:00
2021-03-31 20:35:46 +02:00
boards.forEach( function(meta, board_id) {
2021-03-31 18:27:33 +02:00
var hist = meta.history.join(', ');
2021-04-25 20:33:33 +02:00
console.log( `Found board ${board_id} - "${meta.title}", revision ${meta.current}, history [${hist}], backup ${JSON.stringify(meta.backupStatus)}` );
2021-03-31 18:27:33 +02:00
});
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
var conf = NB.storage.getConfig();
2021-03-31 12:35:41 +02:00
console.log( `Active: [${conf.board}]` );
console.log( `Theme: [${conf.theme}]` );
console.log( `Font: [${conf.fontName}], size [${conf.fontSize || '-'}], line-height [${conf.lineHeight || '-'}]` );
console.log( `FileLinks: [${conf.fileLinks}]` );
console.log( 'Backups: ', conf.backups);
2021-04-14 13:54:03 +02:00
/*
* backups
*/
NB.backupTypes = new Map();
2021-04-14 14:37:25 +02:00
NB.backupTypes.set( (new SimpleBackup).type, SimpleBackup );
2021-04-13 19:05:14 +02:00
2021-04-25 14:37:57 +02:00
NB.storage.initBackups(onBackupStatusChange);
2021-04-14 13:54:03 +02:00
/*
* the ui
*/
2021-04-06 13:21:48 +02:00
initFonts();
2021-04-06 13:21:48 +02:00
initDragAndDrop();
2021-04-07 18:51:52 +02:00
NB.varAdjust = new VarAdjust()
//
2021-03-31 18:27:33 +02:00
if (conf.theme)
$('html').addClass('theme-' + conf.theme);
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
if (conf.board)
openBoard(conf.board);
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
adjustLayout();
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
updateBoardIndex();
2021-03-31 12:35:41 +02:00
setWhatsNew();
NB.storage.setVerLast();
2021-03-31 20:35:46 +02:00
//
2021-03-31 18:27:33 +02:00
if (! NB.board && ! $('.config .load-board').length)
NB.board = createDemoBoard();
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
if (NB.board)
showBoard(true);
2021-03-31 12:35:41 +02:00
2021-03-31 18:27:33 +02:00
//
setInterval(adjustListScroller, 100);
setupListScrolling();
2021-03-31 12:35:41 +02:00
2019-05-28 12:09:54 +02:00
</script>
</html>