diff --git a/.jshintignore b/.jshintignore
index b7a1c570e..45b4087f5 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -2,6 +2,7 @@ node_modules/
www/bower_components/
www/common/pdfjs/
www/common/tippy/
+www/common/jquery-ui/
server.js
www/common/media-tag.js
@@ -14,6 +15,8 @@ www/pad/wysiwygarea-plugin.js
www/pad/mediatag-plugin.js
www/pad/mediatag-plugin-dialog.js
+www/kanban/jkanban.js
+
www/common/media-tag-nacl.min.js
customize/
diff --git a/customize.dist/loading.js b/customize.dist/loading.js
index c08b82681..2bc0ec420 100644
--- a/customize.dist/loading.js
+++ b/customize.dist/loading.js
@@ -45,8 +45,7 @@ define([], function () {
flex-shrink: 0;
display: flex;
flex-flow: column;
- justify-content: center;
- justify-content: space-evenly;
+ justify-content: space-around;
align-items: center;
}
@media screen and (max-height: 800px) {
diff --git a/customize.dist/pages.js b/customize.dist/pages.js
index 281bdc585..1cd3d5f7c 100644
--- a/customize.dist/pages.js
+++ b/customize.dist/pages.js
@@ -1,10 +1,11 @@
define([
'/api/config',
'/common/hyperscript.js',
+ '/common/common-language.js',
'/customize/messages.js',
'jquery',
'/customize/application_config.js',
-], function (Config, h, Msg, $, AppConfig) {
+], function (Config, h, Language, Msg, $, AppConfig) {
var Pages = {};
var urlArgs = Config.requireConf.urlArgs;
@@ -13,6 +14,26 @@ define([
return e;
};
+ var languageSelector = function () {
+ var options = [];
+ var languages = Msg._languages;
+ var selected = Msg._languageUsed;
+ var keys = Object.keys(languages).sort();
+ keys.forEach(function (l) {
+ var attr = { value: l };
+ if (selected === l) { attr.selected = 'selected'; }
+ options.push(h('option', attr, languages[l]));
+ });
+ var select = h('select', {}, options);
+ $(select).change(function () {
+ Language.setLanguage($(select).val() || '', null, function () {
+ window.location.reload();
+ });
+ });
+ return select;
+ };
+ languageSelector = languageSelector; // jshint
+
var footerCol = function (title, L, literal) {
return h('div.col-6.col-sm-3', [
h('ul.list-unstyled', [
@@ -47,7 +68,8 @@ define([
h('div.row', [
footerCol(null, [
h('div.cp-bio-foot', [
- h('p', Msg.main_footerText)
+ h('p', Msg.main_footerText),
+ //languageSelector()
])
], ''),
footerCol('footer_applications', [
@@ -56,6 +78,7 @@ define([
footLink('/code/', 'main_code'),
footLink('/slide/', 'main_slide'),
footLink('/poll/', 'main_poll'),
+ footLink('/kanban/', 'main_kanban'),
footLink('/whiteboard/', null, Msg.type.whiteboard)
]),
footerCol('footer_aboutUs', [
@@ -72,7 +95,7 @@ define([
])
])
]),
- h('div.cp-version-footer', "CryptPad v2.1.0 (Badger)")
+ h('div.cp-version-footer', "CryptPad v2.2.0 (Coati)")
]);
};
@@ -129,8 +152,8 @@ define([
h('div.container-fluid.cp-about-intro', [
h('div.container', [
h('center', [
- h('h1', Msg.about),
- setHTML(h('p'), 'CryptPad is created inside of the Research Team at XWiki SAS , a small business located in Paris France and Iasi Romania. There are 3 core team members working on CryptPad plus a number of contributors both inside and outside of XWiki SAS.'),
+ h('h1', Msg.about),
+ setHTML(h('p'), Msg.about_intro),
]),
]),
]),
@@ -138,7 +161,7 @@ define([
h('div.row', [
h('div.cp-develop-about.col-12',[
h('div.cp-icon-cent'),
- h('h2.text-center', 'Core Developers')
+ h('h2.text-center', Msg.about_core)
]),
]),
h('div.row.align-items-center', [
@@ -189,7 +212,7 @@ define([
h('div.row', [
h('div.cp-develop-about.col-12.cp-contrib',[
h('div.cp-icon-cent'),
- h('h2.text-center', 'Key Contributors')
+ h('h2.text-center', Msg.about_contributors)
]),
]),
h('div.row.align-items-center', [
@@ -566,6 +589,7 @@ define([
[ 'code', '/code/', Msg.main_codePad, 'fa-file-code-o' ],
[ 'slide', '/slide/', Msg.main_slidePad, 'fa-file-powerpoint-o' ],
[ 'poll', '/poll/', Msg.main_pollPad, 'fa-calendar' ],
+ [ 'kanban', '/kanban/', Msg.main_kanbanPad, 'fa-calendar' ],
[ 'whiteboard', '/whiteboard/', Msg.main_whiteboardPad, 'fa-paint-brush' ],
[ 'recent', '/drive/', Msg.main_localPads, 'fa-hdd-o' ]
].filter(function (x) {
diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less
index 6a0293ae8..b6862c3f4 100644
--- a/customize.dist/src/less2/include/colortheme.less
+++ b/customize.dist/src/less2/include/colortheme.less
@@ -115,6 +115,10 @@
@colortheme_todo-color: #000;
@colortheme_todo-warn: #cd2532;
+@colortheme_kanban-bg: #8C4;
+@colortheme_kanban-color: #000;
+@colortheme_kanban-warn: #e6385d;
+
// Sidebar layout (profile / settings)
@colortheme_sidebar-active: #fff;
@colortheme_sidebar-left-bg: #eee;
@@ -131,6 +135,7 @@
@cryptpad_color_grey: #999999;
@cryptpad_header_col: #1E1F1F;
@cryptpad_text_col: #3F4141;
+@cryptpad_color_light_blue: #00b7d8;
@colortheme_checkmark-back0: @colortheme_form-bg-alt;
@colortheme_checkmark-back0-active: @colortheme_form-border;
diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less
index 57509455e..e2115288a 100644
--- a/customize.dist/src/less2/include/creation.less
+++ b/customize.dist/src/less2/include/creation.less
@@ -103,7 +103,7 @@
flex-flow: column;
align-items: center;
flex: 1 0 auto;
- justify-content: space-evenly;
+ justify-content: space-around;
& > div {
width: 400px;
max-width: 100%;
diff --git a/customize.dist/src/less2/include/help.less b/customize.dist/src/less2/include/help.less
index 90f23119b..0e6e90d87 100644
--- a/customize.dist/src/less2/include/help.less
+++ b/customize.dist/src/less2/include/help.less
@@ -14,7 +14,7 @@
right: 5px;
}
.cp-help-text {
- color: @color;
+ color: contrast(lighten(@bg-color, 15%), #fff, #000); //@color;
margin: 0;
padding: 15px;
a {
diff --git a/customize.dist/src/less2/include/icon-colors.less b/customize.dist/src/less2/include/icon-colors.less
index b787fee1c..283626cfd 100644
--- a/customize.dist/src/less2/include/icon-colors.less
+++ b/customize.dist/src/less2/include/icon-colors.less
@@ -13,6 +13,7 @@
.cp-icon-color-profile { color: @colortheme_settings-bg; }
.cp-icon-color-default { color: @colortheme_default-bg; }
.cp-icon-color-todo { color: @colortheme_todo-bg; }
+ .cp-icon-color-kanban { color: @colortheme_kanban-bg; }
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
.cp-border-color-code { border-color: @colortheme_code-bg !important; }
@@ -26,5 +27,6 @@
.cp-border-color-profile { border-color: @colortheme_settings-bg !important; }
.cp-border-color-default { border-color: @colortheme_default-bg !important; }
.cp-border-color-todo { border-color: @colortheme_todo-bg !important; }
+ .cp-border-color-kanban { border-color: @colortheme_kanban-bg !important; }
}
diff --git a/customize.dist/src/less2/include/infopages.less b/customize.dist/src/less2/include/infopages.less
index 4282b3499..131877a65 100644
--- a/customize.dist/src/less2/include/infopages.less
+++ b/customize.dist/src/less2/include/infopages.less
@@ -170,9 +170,8 @@
.nav-link {
padding: 0.5em 0.7em;
&:hover {
- font-size: 1.05em;
- //transform: scale(1.05);
- };
+ color: @cryptpad_color_light_blue;
+ }
}
.cp-register-btn {
border: 2px solid #4591C4;
diff --git a/customize.dist/src/less2/include/tokenfield.less b/customize.dist/src/less2/include/tokenfield.less
index 6604b4481..5e518f08c 100644
--- a/customize.dist/src/less2/include/tokenfield.less
+++ b/customize.dist/src/less2/include/tokenfield.less
@@ -1,25 +1,31 @@
@import (once) "./tools.less";
.tokenfield_main () {
+ .ui-autocomplete {
+ z-index: 100001; // alertify + 1
+ }
.tokenfield {
.tools_unselectable();
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
height: auto;
min-height: 34px;
padding-bottom: 0px;
background-color: unset;
border: none;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- padding: 0 10px;
+ margin: 0 10px;
+ padding: 0;
+ width: ~"calc(100% - 20px)";
.token {
box-sizing: border-box;
border-radius: 3px;
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
- margin: 10px 5px;
+ margin: 2px 0;
height: 24px;
vertical-align: middle;
cursor: default;
@@ -50,7 +56,7 @@
.close {
font-family: Arial;
display: inline-block;
- line-height: 24px;
+ line-height: 1.49em;
font-size: 1.1em;
margin-left: 5px;
float: none;
@@ -73,6 +79,8 @@
margin: 0 !important; // Override alertify
box-shadow: none;
max-width: 100%;
+ width: 100%;
+ min-width: 100% !important;
&:focus {
border-color: transparent;
outline: 0;
diff --git a/customize.dist/src/less2/include/tools.less b/customize.dist/src/less2/include/tools.less
index 9fd2df5bc..b8c36cbcd 100644
--- a/customize.dist/src/less2/include/tools.less
+++ b/customize.dist/src/less2/include/tools.less
@@ -19,6 +19,7 @@
}
.tools_unselectable () {
+ user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
diff --git a/customize.dist/src/less2/main.less b/customize.dist/src/less2/main.less
index dadbef539..1186465da 100644
--- a/customize.dist/src/less2/main.less
+++ b/customize.dist/src/less2/main.less
@@ -40,4 +40,5 @@ body.cp-app-profile { @import "../../../profile/app-profile.less"; }
body.cp-app-settings { @import "../../../settings/app-settings.less"; }
body.cp-app-debug { @import "../../../debug/app-debug.less"; }
body.cp-app-worker { @import "../../../worker/app-worker.less"; }
+body.cp-app-kanban { @import "../../../kanban/app-kanban.less"; }
diff --git a/customize.dist/src/less2/pages/page-index.less b/customize.dist/src/less2/pages/page-index.less
index 55671f913..b0a6d124e 100644
--- a/customize.dist/src/less2/pages/page-index.less
+++ b/customize.dist/src/less2/pages/page-index.less
@@ -80,6 +80,7 @@ body {
}
.nav-link {
&:hover {
+ color: inherit;
transform: scale(1.05);
};
}
@@ -154,6 +155,7 @@ h4 {
.cp-callout-code .fa { background-color: @colortheme_code-bg; }
.cp-callout-slide .fa { background-color: @colortheme_slide-bg; }
.cp-callout-poll .fa { background-color: @colortheme_poll-bg; }
+.cp-callout-kanban .fa { background-color: @colortheme_kanban-bg; }
.cp-callout-whiteboard .fa { background-color: @colortheme_whiteboard-bg; }
.cp-callout-recent .fa { background-color: @colortheme_drive-bg; }
.cp-hidden { display: none !important; }
diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js
index cf7ddcf8c..b11bc2e17 100644
--- a/customize.dist/translations/messages.fr.js
+++ b/customize.dist/translations/messages.fr.js
@@ -2,12 +2,12 @@ define(function () {
var out = {};
out.main_title = "CryptPad : Éditeur collaboratif en temps réel, zero knowledge";
- out.main_slogan = "L'unité est la force, la collaboration est la clé";
out.type = {};
out.type.pad = 'Texte';
out.type.code = 'Code';
out.type.poll = 'Sondage';
+ out.type.kanban = 'Kanban';
out.type.slide = 'Présentation';
out.type.drive = 'CryptDrive';
out.type.whiteboard = "Tableau Blanc";
@@ -21,6 +21,7 @@ define(function () {
out.button_newpoll = 'Nouveau sondage';
out.button_newslide = 'Nouvelle présentation';
out.button_newwhiteboard = 'Nouveau tableau blanc';
+ out.button_newkanban = 'Nouveau kanban';
out.updated_0_common_connectionLost = "Connexion au serveur perdue Vous êtes désormais en mode lecture seule jusqu'au retour de la connexion.";
out.common_connectionLost = out.updated_0_common_connectionLost;
@@ -246,6 +247,17 @@ define(function () {
out.pad_mediatagWidth = "Largeur (px)";
out.pad_mediatagHeight = "Hauteur (px)";
+ // Kanban
+ out.kanban_newBoard = "Nouveau tableau";
+ out.kanban_item = "Élément {0}"; // Item number for initial content
+ out.kanban_todo = "À faire";
+ out.kanban_done = "Terminé";
+ out.kanban_working = "En cours";
+ out.kanban_deleteBoard = "Êtes-vous sûr de vouloir supprimer ce tableau ?";
+ out.kanban_addBoard = "Ajouter un tableau";
+ out.kanban_removeItem = "Supprimer cet élément";
+ out.kanban_removeItemConfirm = "Êtes-vous sûr de vouloir supprimer cet élément ?";
+
// Polls
out.poll_title = "Sélecteur de date Zero Knowledge";
@@ -367,6 +379,7 @@ define(function () {
out.fm_searchName = "Recherche";
out.fm_recentPadsName = "Pads récents";
out.fm_ownedPadsName = "Pads en votre possession";
+ out.fm_tagsName = "Mots-clés";
out.fm_searchPlaceholder = "Rechercher...";
out.fm_newButton = "Nouveau";
out.fm_newButtonTitle = "Créer un nouveau pad ou un dossier, importer un fichier dans le dossier courant";
@@ -429,6 +442,8 @@ define(function () {
out.fm_padIsOwned = "Vous êtes le propriétaire de ce pad";
out.fm_padIsOwnedOther = "Ce pad est la propriété d'un autre utilisateur";
out.fm_deletedPads = "Ces pads n'existent plus sur le serveur, ils ont été supprimés de votre CryptDrive: {0}";
+ out.fm_tags_name = "Mot-clé";
+ out.fm_tags_used = "Nombre d'utilisations";
// File - Context menu
out.fc_newfolder = "Nouveau dossier";
out.fc_rename = "Renommer";
@@ -633,38 +648,24 @@ define(function () {
// index.html
//about.html
- out.main_p2 = 'Ce projet utilise l\'éditeur visuel (WYSIWYG) CKEditor , l\'éditeur de code source CodeMirror , et le moteur temps-réel ChainPad .';
- out.main_howitworks_p1 = 'CryptPad utilise une variante de l\'algorithme d\'Operational transformation qui est capable de trouver un consensus distribué en utilisant une chaîne de bloc Nakamoto , un outil popularisé par le Bitcoin . De cette manière, l\'algorithme évite la nécessité d\'utiliser un serveur central pour résoudre les conflits d\'édition de l\'Operational Transformation, et sans ce besoin de résolution des conflits le serveur peut rester ignorant du contenu qui est édité dans le pad.';
+ out.about_intro = 'CryptPad est développé au sein de l\'équipe Recherche d\'XWiki SAS , une petite entreprise située à Paris en France et à Iasi en Roumanie. Il y a 3 développeurs principaux qui travaillent sur CryptPad, ainsi que quelques contributeurs à la fois dans et en dehors d\'XWiki SAS';
+ out.about_core = 'Développeurs principaux';
+ out.about_contributors = 'Contributeurs clés';
+
//contact.html
- out.main_about_p2 = 'Si vous avez des questions ou commentaires, vous pouvez nous tweeter , ouvrir une issue sur GitHub , venir dire bonjour sur notre salle Matrix ou IRC (#cryptpad sur irc.freenode.net), ou bien encore nous envoyer un email .';
out.main_about_p22 = 'Tweetez nous';
out.main_about_p23 = 'Ouvrez un ticket (GitHub)';
out.main_about_p24 = 'Dites Bonjour (Matrix)';
out.main_about_p25 = 'Envoyez-nous un email';
out.main_about_p26 = 'Si vous avez une question ou des remarques, n\'hésitez pas à nous contacter !';
-
out.main_info = "
Collaborez avec confiance Développez vos idées en groupe avec des documents partagés; la technologie Zero Knowledge sécurise vos données.";
out.main_catch_phrase = "Le Cloud Zero Knowledge";
- out.main_howitworks = 'Comment ça fonctionne';
- out.main_zeroKnowledge = 'Zero Knowledge';
- out.main_zeroKnowledge_p = "Vous n'avez pas besoin de croire que nous n'allons pas regarder vos pads. Avec la technologie Zero Knowledge de CryptPad, nous ne pouvons pas le faire. Apprenez-en plus sur notre manière de protéger vos données .";
- out.main_writeItDown = 'Prenez-en note';
- out.main_writeItDown_p = "Les plus grands projets naissent des plus petites idées. Prenez note de vos moments d'inspiration et de vos idées inattendues car vous ne savez pas lesquels seront des découvertes capitales.";
- out.main_share = 'Partagez le lien, partagez le pad';
- out.main_share_p = "Faites croître vos idées à plusieurs : réalisez des réunions efficaces, collaborez sur vos listes de tâches et réalisez des présentations rapides avec tous vos amis sur tous vos appareils.";
- out.main_organize = 'Soyez organisé';
- out.main_organize_p = "Avec CryptDrive, vous pouvez garder vos vues sur ce qui est important. Les dossiers vous permettent de garder la trace de vos projets et d'avoir une vision globale du travail effectué.";
- out.tryIt = 'Essayez-le !';
out.main_richText = 'Éditeur de texte';
- out.main_richText_p = 'Éditez des documents texte collaborativement avec notre application CkEditor temps-réel et Zero Knowledge.';
out.main_code = 'Éditeur de code';
- out.main_code_p = 'Modifiez votre code collaborativement grâce à notre application CodeMirror temps-réel et Zero Knowledge.';
out.main_slide = 'Présentations';
- out.main_slide_p = 'Créez vos présentations en syntaxe Markdown collaborativement de manière sécurisée et affichez les dans votre navigateur.';
out.main_poll = 'Sondages';
- out.main_poll_p = 'Planifiez vos réunions ou évènements, ou votez pour la meilleure solution concernant votre problème.';
out.main_drive = 'CryptDrive';
out.main_richTextPad = 'Pad de Texte Riche';
@@ -719,7 +720,7 @@ define(function () {
out.policy_whatwetell = 'Ce que nous dévoilons à d\'autres à propos de vous';
out.policy_whatwetell_p1 = 'Nous ne fournissons aucune information que nous récoltons ou que vous nous fournissez à des tierces parties à moins d\'y être contraints par la loi.';
out.policy_links = 'Liens vers d\'autres sites';
- out.policy_links_p1 = 'Ce site contient des liens vers d\'autres sites, certains étant produits par d\'autres organisations. Nous ne sommes responsables des pratiques de confidentialité ou du contenu d\'aucun site externe. De manière générale, les liens vers des sites externes sont lancés dans une nouvelle fenêtre (ou onglet) du navigateur, pour rendre clair le fait que vous quittez CryptpPad.fr.';
+ out.policy_links_p1 = 'Ce site contient des liens vers d\'autres sites, certains étant produits par d\'autres organisations. Nous ne sommes responsables des pratiques de confidentialité ou du contenu d\'aucun site externe. De manière générale, les liens vers des sites externes sont lancés dans une nouvelle fenêtre (ou onglet) du navigateur, pour rendre clair le fait que vous quittez CryptPad.fr.';
out.policy_ads = 'Publicité';
out.policy_ads_p1 = 'Nous n\'affichons pas de publicité en ligne, bien que nous puissions afficher des liens vers les sites des organisations qui financent nos recherches.';
out.policy_choices = 'Vos choix';
@@ -1001,6 +1002,11 @@ define(function () {
embed: 'Intégrez des images de votre disque ou de votre CryptDrive et exporter le contenu en tant que PNG sur votre disque ou votre CryptDrive '
};
+ out.help.kanban = {
+ add: 'Ajoutez un tableau en utilisant le bouton dans le coin supérieur-droit',
+ task: 'Déplacez les éléments en les faisant glisser d\'un tableau à l\'autre',
+ color: 'Modifiez les couleurs en cliquant sur les parties colorées à côté du titre de chaque tableau'
+ };
out.initialState = [
'',
diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js
index 18e295a4c..225965b42 100644
--- a/customize.dist/translations/messages.js
+++ b/customize.dist/translations/messages.js
@@ -2,12 +2,12 @@ define(function () {
var out = {};
out.main_title = "CryptPad: Zero Knowledge, Collaborative Real Time Editing";
- out.main_slogan = "Unity is Strength - Collaboration is Key"; // TODO remove?
out.type = {};
out.type.pad = 'Rich text';
out.type.code = 'Code';
out.type.poll = 'Poll';
+ out.type.kanban = 'Kanban';
out.type.slide = 'Presentation';
out.type.drive = 'CryptDrive';
out.type.whiteboard = 'Whiteboard';
@@ -21,6 +21,7 @@ define(function () {
out.button_newpoll = 'New Poll';
out.button_newslide = 'New Presentation';
out.button_newwhiteboard = 'New Whiteboard';
+ out.button_newkanban = 'New Kanban';
// NOTE: Remove updated_0_ if we need an updated_1_
out.updated_0_common_connectionLost = "Server Connection Lost You're now in read-only mode until the connection is back.";
@@ -248,6 +249,17 @@ define(function () {
out.pad_mediatagWidth = "Width (px)";
out.pad_mediatagHeight = "Height (px)";
+ // Kanban
+ out.kanban_newBoard = "New board";
+ out.kanban_item = "Item {0}"; // Item number for initial content
+ out.kanban_todo = "To Do";
+ out.kanban_done = "Done";
+ out.kanban_working = "Working";
+ out.kanban_deleteBoard = "Are you sure you want to delete this board?";
+ out.kanban_addBoard = "Add a board";
+ out.kanban_removeItem = "Remove this item";
+ out.kanban_removeItemConfirm = "Are you sure you want to delete this item?";
+
// Polls
out.poll_title = "Zero Knowledge Date Picker";
@@ -368,6 +380,7 @@ define(function () {
out.fm_searchName = "Search";
out.fm_recentPadsName = "Recent pads";
out.fm_ownedPadsName = "Owned";
+ out.fm_tagsName = "Tags";
out.fm_searchPlaceholder = "Search...";
out.fm_newButton = "New";
out.fm_newButtonTitle = "Create a new pad or folder, import a file in the current folder";
@@ -430,6 +443,8 @@ define(function () {
out.fm_padIsOwned = "You are the owner of this pad";
out.fm_padIsOwnedOther = "This pad is owned by another user";
out.fm_deletedPads = "These pads no longer exist on the server, they've been removed from your CryptDrive: {0}";
+ out.fm_tags_name = "Tag name";
+ out.fm_tags_used = "Number of uses";
// File - Context menu
out.fc_newfolder = "New folder";
out.fc_rename = "Rename";
@@ -638,11 +653,11 @@ define(function () {
//about.html
- out.main_p2 = 'This project uses the CKEditor Visual Editor, CodeMirror , and the ChainPad realtime engine.';
- out.main_howitworks_p1 = 'CryptPad uses a variant of the Operational transformation algorithm which is able to find distributed consensus using a Nakamoto Blockchain , a construct popularized by Bitcoin . This way the algorithm can avoid the need for a central server to resolve Operational Transform Edit Conflicts and without the need for resolving conflicts, the server can be kept unaware of the content which is being edited on the pad.';
+ out.about_intro = 'CryptPad is created inside of the Research Team at XWiki SAS , a small business located in Paris France and Iasi Romania. There are 3 core team members working on CryptPad plus a number of contributors both inside and outside of XWiki SAS.';
+ out.about_core = 'Core Developers';
+ out.about_contributors = 'Key Contributors';
// contact.html
- out.main_about_p2 = 'If you have any questions or comments, feel free to reach out! You can tweet us , open an issue on GitHub . Come say hi on our Matrix channel or IRC (#cryptpad on irc.freenode.net), or send us an email .';
out.main_about_p22 = 'Tweet us';
out.main_about_p23 = 'open an issue on GitHub';
out.main_about_p24 = 'say Hello (Matrix)';
@@ -652,25 +667,10 @@ define(function () {
out.main_info = "
Collaborate in Confidence Grow your ideas together with shared documents while Zero Knowledge technology secures your privacy; even from us .";
out.main_catch_phrase = "The Zero Knowledge Cloud";
- out.main_howitworks = 'How It Works';
- out.main_zeroKnowledge = 'Zero Knowledge';
- out.main_zeroKnowledge_p = "You don't have to trust that we won't look at your pads, with CryptPad's revolutionary Zero Knowledge Technology we can't . Learn more about how we protect your Privacy and Security .";
- out.main_writeItDown = 'Write it down';
-
- out.main_writeItDown_p = "The greatest projects come from the smallest ideas. Take down the moments of inspiration and unexpected ideas because you never know which one might be a breakthrough.";
- out.main_share = 'Share the link, share the pad';
- out.main_share_p = "Grow your ideas together: conduct efficient meetings, collaborate on TODO lists and make quick presentations with all your friends and all your devices.";
- out.main_organize = 'Get organized';
- out.main_organize_p = "With CryptPad Drive, you can keep your sights on what's important. Folders allow you to keep track of your projects and have a global vision of where things are going.";
- out.tryIt = 'Try it out!';
out.main_richText = 'Rich Text editor';
- out.main_richText_p = 'Edit rich text pads collaboratively with our realtime Zero Knowledge CkEditor application.';
out.main_code = 'Code editor';
- out.main_code_p = 'Edit code from your software collaboratively with our realtime Zero Knowledge CodeMirror application.';
out.main_slide = 'Slide editor';
- out.main_slide_p = 'Create your presentations using the Markdown syntax, and display them in your browser.';
out.main_poll = 'Polls';
- out.main_poll_p = 'Plan your meeting or your event, or vote for the best solution regarding your problem.';
out.main_drive = 'CryptDrive';
out.main_richTextPad = 'Rich Text Pad';
@@ -1045,6 +1045,11 @@ define(function () {
embed: 'Embed images from your disk or your CryptDrive and export them as PNG to your disk or your CryptDrive '
};
+ out.help.kanban = {
+ add: 'Add new boards using the button in the top-right corner',
+ task: 'Move items by dragging and dropping them from one board to another',
+ color: 'Change the colors by clicking on the colored part next to the board titles',
+ };
out.initialState = [
'',
diff --git a/package.json b/package.json
index e0ea88e95..7ad861062 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
- "version": "2.1.1",
+ "version": "2.2.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"chainpad-server": "^2.0.0",
diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index f6760f897..0ab300721 100644
--- a/www/common/application_config_internal.js
+++ b/www/common/application_config_internal.js
@@ -9,7 +9,7 @@ define(function() {
/* Select the buttons displayed on the main page to create new collaborative sessions
* Existing types : pad, code, poll, slide
*/
- config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'whiteboard', 'file', 'todo', 'contacts'];
+ config.availablePadTypes = ['drive', 'pad', 'code', 'slide', 'poll', 'kanban', 'whiteboard', 'file', 'todo', 'contacts'];
config.registeredOnlyTypes = ['file', 'contacts'];
/* Cryptpad apps use a common API to display notifications to users
@@ -81,6 +81,7 @@ define(function() {
whiteboard: 'fa-paint-brush',
todo: 'fa-tasks',
contacts: 'fa-users',
+ kanban: 'fa-list-alt',
};
// Ability to create owned pads and expiring pads through a new pad creation screen.
diff --git a/www/common/common-hash.js b/www/common/common-hash.js
index 5ba9ec5b3..3aaa7719b 100644
--- a/www/common/common-hash.js
+++ b/www/common/common-hash.js
@@ -117,12 +117,12 @@ Version 1
var hashArr = fixDuplicateSlashes(hash).split('/');
if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) {
parsed.type = 'pad';
+ parsed.getHash = function () { return hash; };
if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Version 0
// Old hash
parsed.channel = hash.slice(0, 32);
parsed.key = hash.slice(32, 56);
parsed.version = 0;
- parsed.getHash = function () { return hash; };
return parsed;
}
var options;
diff --git a/www/common/common-interface.js b/www/common/common-interface.js
index 7f318dfae..0592d859a 100644
--- a/www/common/common-interface.js
+++ b/www/common/common-interface.js
@@ -12,8 +12,10 @@ define([
'/customize/loading.js',
'/common/test.js',
+ '/common/jquery-ui/jquery-ui.min.js',
'/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js',
'css!/common/tippy/tippy.css',
+ 'css!/common/jquery-ui/jquery-ui.min.css'
], function ($, Messages, Util, Hash, Notifier, AppConfig,
Alertify, Tippy, Pages, h, Loading, Test) {
var UI = {};
@@ -183,11 +185,17 @@ define([
]);
};
- UI.tokenField = function (target) {
+ UI.tokenField = function (target, autocomplete) {
var t = {
element: target || h('input'),
};
- var $t = t.tokenfield = $(t.element).tokenfield();
+ var $t = t.tokenfield = $(t.element).tokenfield({
+ autocomplete: {
+ source: autocomplete,
+ delay: 100
+ },
+ showAutocompleteOnFocus: false
+ });
t.getTokens = function (ignorePending) {
var tokens = $t.tokenfield('getTokens').map(function (token) {
@@ -210,10 +218,17 @@ define([
t.preventDuplicates = function (cb) {
$t.on('tokenfield:createtoken', function (ev) {
+ // Close the suggest list when a token is added because we're going to wipe the input
+ var $input = $t.closest('.tokenfield').find('.token-input');
+ $input.autocomplete('close');
+
var val;
ev.attrs.value = ev.attrs.value.toLowerCase();
if (t.getTokens(true).some(function (t) {
- if (t === ev.attrs.value) { return ((val = t)); }
+ if (t === ev.attrs.value) {
+ ev.preventDefault();
+ return ((val = t));
+ }
})) {
ev.preventDefault();
if (typeof(cb) === 'function') { cb(val); }
@@ -241,7 +256,7 @@ define([
return t;
};
- dialog.tagPrompt = function (tags, cb) {
+ dialog.tagPrompt = function (tags, existing, cb) {
var input = dialog.textInput();
var tagger = dialog.frame([
@@ -255,7 +270,7 @@ define([
dialog.nav(),
]);
- var field = UI.tokenField(input).preventDuplicates(function (val) {
+ var field = UI.tokenField(input, existing).preventDuplicates(function (val) {
UI.warn(Messages._getKey('tags_duplicate', [val]));
});
@@ -396,7 +411,7 @@ define([
stopListening(listener);
cb();
});
- listener = listenForKeys(close, close, ok);
+ listener = listenForKeys(close, close);
var $ok = $(ok).click(close);
document.body.appendChild(frame);
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 2e4dfacbc..18f38aae0 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -23,20 +23,27 @@ define([
}
UIElements.updateTags = function (common, href) {
- var sframeChan = common.getSframeChannel();
- sframeChan.query('Q_TAGS_GET', href || null, function (err, res) {
- if (err || res.error) {
- if (res.error === 'NO_ENTRY') {
- UI.alert(Messages.tags_noentry);
+ var existing, tags;
+ NThen(function(waitFor) {
+ common.getSframeChannel().query("Q_GET_ALL_TAGS", null, waitFor(function(err, res) {
+ if (err || res.error) { return void console.error(err || res.error); }
+ existing = Object.keys(res.tags).sort();
+ }));
+ }).nThen(function (waitFor) {
+ common.getPadAttribute('tags', waitFor(function (err, res) {
+ if (err) {
+ if (err === 'NO_ENTRY') {
+ UI.alert(Messages.tags_noentry);
+ }
+ waitFor.abort();
+ return void console.error(err);
}
- return void console.error(err || res.error);
- }
- UI.dialog.tagPrompt(res.data, function (tags) {
- if (!Array.isArray(tags)) { return; }
- sframeChan.event('EV_TAGS_SET', {
- tags: tags,
- href: href,
- });
+ tags = res || [];
+ }), href);
+ }).nThen(function () {
+ UI.dialog.tagPrompt(tags, existing, function (newTags) {
+ if (!Array.isArray(newTags)) { return; }
+ common.setPadAttribute('tags', newTags, null, href);
});
});
};
@@ -303,7 +310,7 @@ define([
var embed = initValue ? val.embed : Util.isChecked($(link).find('#cp-share-embed'));
var present = initValue ? val.present : Util.isChecked($(link).find('#cp-share-present'));
- var hash = (edit && hashes.editHash) ? hashes.editHash : hashes.viewHash;
+ var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash;
var href = origin + pathname + '#' + hash;
var parsed = Hash.parsePadUrl(href);
return origin + parsed.getUrl({embed: embed, present: present});
@@ -387,8 +394,11 @@ define([
val = val || {};
if (val.edit === false) {
$(link).find('#cp-share-editable-false').prop('checked', true);
+ $(link).find('#cp-share-editable-true').prop('checked', false);
+ } else {
+ $(link).find('#cp-share-editable-true').prop('checked', true);
+ $(link).find('#cp-share-editable-false').prop('checked', false);
}
- else { $(link).find('#cp-share-editable-true').prop('checked', true); }
if (val.embed) { $(link).find('#cp-share-embed').prop('checked', true); }
if (val.present) { $(link).find('#cp-share-present').prop('checked', true); }
$(link).find('#cp-share-link-preview').val(getLinkValue(val));
diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js
index 4f8f69a1b..5faffc5c9 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -578,7 +578,7 @@ define([
}
var parsed = Hash.parsePadUrl(window.location.href);
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
- if (parsed.type === 'file') { secret.channel = Util.base64ToHex(secret.channel); }
+ if (parsed.type === 'file' && typeof(parsed.channel) === 'string') { secret.channel = Util.base64ToHex(secret.channel); }
hashes = Hash.getHashes(secret);
if (secret.version === 0) {
@@ -594,6 +594,7 @@ define([
postMessage("GET_STRONGER_HASH", {
href: window.location.href,
+ channel: secret.channel,
password: secret.password
}, function (hash) {
if (hash) { hashes.editHash = hash; }
diff --git a/www/common/jquery-ui/jquery-ui.min.css b/www/common/jquery-ui/jquery-ui.min.css
new file mode 100644
index 000000000..1ff50449e
--- /dev/null
+++ b/www/common/jquery-ui/jquery-ui.min.css
@@ -0,0 +1,7 @@
+/*! jQuery UI - v1.12.1 - 2018-05-15
+* http://jqueryui.com
+* Includes: core.css, autocomplete.css, menu.css, theme.css
+* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666}
\ No newline at end of file
diff --git a/www/common/jquery-ui/jquery-ui.min.js b/www/common/jquery-ui/jquery-ui.min.js
new file mode 100644
index 000000000..d17a46a44
--- /dev/null
+++ b/www/common/jquery-ui/jquery-ui.min.js
@@ -0,0 +1,6 @@
+/*! jQuery UI - v1.12.1 - 2018-05-15
+* http://jqueryui.com
+* Includes: widget.js, position.js, keycode.js, unique-id.js, widgets/autocomplete.js, widgets/menu.js
+* Copyright jQuery Foundation and other contributors; Licensed MIT */
+
+(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){t.ui=t.ui||{},t.ui.version="1.12.1";var e=0,i=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},l=e.split(".")[0];e=e.split(".")[1];var h=l+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][h.toLowerCase()]=function(e){return!!t.data(e,h)},t[l]=t[l]||{},n=t[l][e],o=t[l][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:l,widgetName:e,widgetFullName:h}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var s,n,o=i.call(arguments,1),a=0,r=o.length;r>a;a++)for(s in o[a])n=o[a][s],o[a].hasOwnProperty(s)&&void 0!==n&&(e[s]=t.isPlainObject(n)?t.isPlainObject(e[s])?t.widget.extend({},e[s],n):t.widget.extend({},n):n);return e},t.widget.bridge=function(e,s){var n=s.prototype.widgetFullName||e;t.fn[e]=function(o){var a="string"==typeof o,r=i.call(arguments,1),l=this;return a?this.length||"instance"!==o?this.each(function(){var i,s=t.data(this,n);return"instance"===o?(l=s,!1):s?t.isFunction(s[o])&&"_"!==o.charAt(0)?(i=s[o].apply(s,r),i!==s&&void 0!==i?(l=i&&i.jquery?l.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+o+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+o+"'")}):l=void 0:(r.length&&(o=t.widget.extend.apply(null,[o].concat(r))),this.each(function(){var e=t.data(this,n);e?(e.option(o||{}),e._init&&e._init()):t.data(this,n,new s(o,this))})),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(i,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=e++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),i),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var l=s.match(/^([\w:-]*)\s*(.*)$/),h=l[1]+o.eventNamespace,c=l[2];c?n.on(h,c,r):i.on(h,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,l=/top|center|bottom/,h=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width
i?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};h>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),l.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-r-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-r-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,(i>0||u>a(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.ui.safeActiveElement=function(t){var e;try{e=t.activeElement}catch(i){e=t.body}return e||(e=t.body),e.nodeName||(e=t.body),e},t.widget("ui.menu",{version:"1.12.1",defaultElement:"",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",o=!1,n=e.keyCode>=96&&105>=e.keyCode?""+(e.keyCode-96):String.fromCharCode(e.keyCode),clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,l=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=l.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=l.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n;this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("").appendTo(this._appendTo()).menu({role:null}).hide().menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(e){e.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,this.element[0]!==t.ui.safeActiveElement(this.document[0])&&this.element.trigger("focus")})},menufocus:function(e,i){var s,n;return this.isNewMenu&&(this.isNewMenu=!1,e.originalEvent&&/^mouse/.test(e.originalEvent.type))?(this.menu.blur(),this.document.one("mousemove",function(){t(e.target).trigger(e.originalEvent)}),void 0):(n=i.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",e,{item:n})&&e.originalEvent&&/^key/.test(e.originalEvent.type)&&this._value(n.value),s=i.item.attr("aria-label")||n.value,s&&t.trim(s).length&&(this.liveRegion.children().hide(),t("").text(s).appendTo(this.liveRegion)),void 0)},menuselect:function(e,i){var s=i.item.data("ui-autocomplete-item"),n=this.previous;this.element[0]!==t.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=n,this._delay(function(){this.previous=n,this.selectedItem=s})),!1!==this._trigger("select",e,{item:s})&&this._value(s.value),this.term=this._value(),this.close(e),this.selectedItem=s}}),this.liveRegion=t("
",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(e){var i=this.menu.element[0];return e.target===this.element[0]||e.target===i||t.contains(i,e.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var e=this.options.appendTo;return e&&(e=e.jquery||e.nodeType?t(e):this.document.find(e).eq(0)),e&&e[0]||(e=this.element.closest(".ui-front, dialog")),e.length||(e=this.document[0].body),e},_initSource:function(){var e,i,s=this;t.isArray(this.options.source)?(e=this.options.source,this.source=function(i,s){s(t.ui.autocomplete.filter(e,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(e,n){s.xhr&&s.xhr.abort(),s.xhr=t.ajax({url:i,data:e,dataType:"json",success:function(t){n(t)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(t){clearTimeout(this.searching),this.searching=this._delay(function(){var e=this.term===this._value(),i=this.menu.element.is(":visible"),s=t.altKey||t.ctrlKey||t.metaKey||t.shiftKey;(!e||e&&!i&&!s)&&(this.selectedItem=null,this.search(null,t))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length
").append(t("").text(i.label)).appendTo(e)},_move:function(t,e){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur(),void 0):(this.menu[t](e),void 0):(this.search(null,e),void 0)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),t.extend(t.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(e,i){var s=RegExp(t.ui.autocomplete.escapeRegex(i),"i");return t.grep(e,function(t){return s.test(t.label||t.value||t)})}}),t.widget("ui.autocomplete",t.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(t>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var i;this._superApply(arguments),this.options.disabled||this.cancelSearch||(i=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.children().hide(),t("
").text(i).appendTo(this.liveRegion))}}),t.ui.autocomplete});
\ No newline at end of file
diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js
index 6b1a5b4a5..e02a527ae 100644
--- a/www/common/outer/async-store.js
+++ b/www/common/outer/async-store.js
@@ -636,18 +636,7 @@ define([
// Tags
Store.listAllTags = function (data, cb) {
- var all = [];
- var files = Util.find(store.proxy, ['drive', 'filesData']);
-
- if (typeof(files) !== 'object') { return cb({error: 'invalid_drive'}); }
- Object.keys(files).forEach(function (k) {
- var file = files[k];
- if (!Array.isArray(file.tags)) { return; }
- file.tags.forEach(function (tag) {
- if (all.indexOf(tag) === -1) { all.push(tag); }
- });
- });
- cb(all);
+ cb(store.userObject.getTagsList());
};
// Templates
diff --git a/www/common/outer/chainpad-netflux-worker.js b/www/common/outer/chainpad-netflux-worker.js
index bc1be6e09..be0030ceb 100644
--- a/www/common/outer/chainpad-netflux-worker.js
+++ b/www/common/outer/chainpad-netflux-worker.js
@@ -68,7 +68,11 @@ define([], function () {
// shim between chainpad and netflux
var msgIn = function (peerId, msg) {
- return msg.replace(/^cp\|([A-Za-z0-9+\/=]+\|)?/, '');
+ // NOTE: Hash version 0 contains a 32 characters nonce followed by a pipe
+ // at the beginning of each message on the server.
+ // We have to make sure our regex ignores this nonce using {0,20} (our IDs
+ // should only be 8 characters long)
+ return msg.replace(/^cp\|([A-Za-z0-9+\/=]{0,20}\|)?/, '');
};
var msgOut = function (msg) {
diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js
index be718562c..b24092673 100644
--- a/www/common/outer/userObject.js
+++ b/www/common/outer/userObject.js
@@ -588,6 +588,7 @@ define([
if (!el.title) { el.title = Hash.getDefaultName(parsed); }
// Fix channel
if (!el.channel) {
+ try {
if (parsed.hashData && parsed.hashData.type === "file") {
// PASSWORD_FILES
el.channel = Util.base64ToHex(parsed.hashData.channel);
@@ -596,6 +597,9 @@ define([
el.channel = secret.channel;
}
console.log('Adding missing channel in filesData ', el.channel);
+ } catch (e) {
+ console.error(e);
+ }
}
if ((loggedIn || config.testMode) && rootFiles.indexOf(id) === -1) {
diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js
index 0fb4a7e39..fcebc354f 100644
--- a/www/common/sframe-common-outer.js
+++ b/www/common/sframe-common-outer.js
@@ -393,6 +393,7 @@ define([
// If we have a stronger hash, use it for pad attributes
href = window.location.pathname + '#' + hashes.editHash;
}
+ if (data.href) { href = data.href; }
Cryptpad.getPadAttribute(data.key, function (e, data) {
cb({
error: e,
@@ -406,6 +407,7 @@ define([
// If we have a stronger hash, use it for pad attributes
href = window.location.pathname + '#' + hashes.editHash;
}
+ if (data.href) { href = data.href; }
Cryptpad.setPadAttribute(data.key, data.value, function (e) {
cb({error:e});
}, href);
@@ -573,19 +575,6 @@ define([
}
});
- sframeChan.on('Q_TAGS_GET', function (data, cb) {
- Cryptpad.getPadTags(data, function (err, data) {
- cb({
- error: err,
- data: data
- });
- });
- });
-
- sframeChan.on('EV_TAGS_SET', function (data) {
- Cryptpad.resetTags(data.href, data.tags);
- });
-
sframeChan.on('Q_PIN_GET_USAGE', function (data, cb) {
Cryptpad.isOverPinLimit(function (err, overLimit, data) {
cb({
@@ -606,6 +595,15 @@ define([
Cryptpad.removeOwnedChannel(channel, cb);
});
+ sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) {
+ Cryptpad.listAllTags(function (err, tags) {
+ cb({
+ error: err,
+ tags: tags
+ });
+ });
+ });
+
if (cfg.addRpc) {
cfg.addRpc(sframeChan, Cryptpad, Utils);
}
diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js
index dca14afb9..4fc5a6ac6 100644
--- a/www/common/sframe-common.js
+++ b/www/common/sframe-common.js
@@ -235,17 +235,20 @@ define([
});
};
- funcs.getPadAttribute = function (key, cb) {
+ // href is optional here: if not provided, we use the href of the current tab
+ funcs.getPadAttribute = function (key, cb, href) {
ctx.sframeChan.query('Q_GET_PAD_ATTRIBUTE', {
- key: key
+ key: key,
+ href: href
}, function (err, res) {
cb (err || res.error, res.data);
});
};
- funcs.setPadAttribute = function (key, value, cb) {
+ funcs.setPadAttribute = function (key, value, cb, href) {
cb = cb || $.noop;
ctx.sframeChan.query('Q_SET_PAD_ATTRIBUTE', {
key: key,
+ href: href,
value: value
}, cb);
};
diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js
index a0e9b82c2..fef8b1027 100644
--- a/www/common/sframe-protocol.js
+++ b/www/common/sframe-protocol.js
@@ -165,10 +165,6 @@ define({
// Put one entry in the parent sessionStorage
'Q_SESSIONSTORAGE_PUT': true,
- // Set and get the tags using the tag prompt button
- 'Q_TAGS_GET': true,
- 'EV_TAGS_SET': true,
-
// Merge the anonymous drive (FS_hash) into the current logged in user's drive, to keep the pads
// in the drive at registration.
'Q_MERGE_ANON_DRIVE': true,
@@ -237,4 +233,7 @@ define({
// Loading events to display in the loading screen
'EV_LOADING_INFO': true,
+
+ // Get all existing tags
+ 'Q_GET_ALL_TAGS': true,
});
diff --git a/www/common/userObject.js b/www/common/userObject.js
index 6192c4236..b05798946 100644
--- a/www/common/userObject.js
+++ b/www/common/userObject.js
@@ -627,6 +627,21 @@ define([
if (typeof cb === "function") { cb(); }
};
+ // Tags
+ exp.getTagsList = function () {
+ var tags = {};
+ var data;
+ var pushTag = function (tag) {
+ tags[tag] = tags[tag] ? ++tags[tag] : 1;
+ };
+ for (var id in files[FILES_DATA]) {
+ data = files[FILES_DATA][id];
+ if (!data.tags || !Array.isArray(data.tags)) { continue; }
+ data.tags.forEach(pushTag);
+ }
+ return tags;
+ };
+
return exp;
};
return module;
diff --git a/www/drive/app-drive.less b/www/drive/app-drive.less
index 0348cf89e..8c1ea1fa4 100644
--- a/www/drive/app-drive.less
+++ b/www/drive/app-drive.less
@@ -462,6 +462,8 @@ span {
padding-right: 15px;
}
.cp-app-drive-search-opendir {
+ display: flex;
+ justify-content: space-between;
a {
cursor: pointer;
color: #41b7d8;
@@ -495,6 +497,19 @@ span {
}
}
}
+ &.cp-app-drive-tags-list {
+ width: 100%;
+ table {
+ margin: 10px 50px;
+ width: ~"calc(100% - 100px)";
+ table-layout: fixed;
+ td, th {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
}
}
.cp-app-drive-element {
diff --git a/www/drive/inner.js b/www/drive/inner.js
index 5975d1e26..b668429cd 100644
--- a/www/drive/inner.js
+++ b/www/drive/inner.js
@@ -51,20 +51,65 @@ define([
var E_OVER_LIMIT = 'E_OVER_LIMIT';
- var SEARCH = "search";
- var SEARCH_NAME = Messages.fm_searchName;
var ROOT = "root";
var ROOT_NAME = Messages.fm_rootName;
+ var SEARCH = "search";
+ var SEARCH_NAME = Messages.fm_searchName;
+ var TRASH = "trash";
+ var TRASH_NAME = Messages.fm_trashName;
var FILES_DATA = Constants.storageKey;
var FILES_DATA_NAME = Messages.fm_filesDataName;
var TEMPLATE = "template";
var TEMPLATE_NAME = Messages.fm_templateName;
- var TRASH = "trash";
- var TRASH_NAME = Messages.fm_trashName;
var RECENT = "recent";
var RECENT_NAME = Messages.fm_recentPadsName;
var OWNED = "owned";
var OWNED_NAME = Messages.fm_ownedPadsName;
+ var TAGS = "tags";
+ var TAGS_NAME = Messages.fm_tagsName;
+
+ // Icons
+ var faFolder = 'fa-folder';
+ var faFolderOpen = 'fa-folder-open';
+ var faReadOnly = 'fa-eye';
+ var faRename = 'fa-pencil';
+ var faTrash = 'fa-trash';
+ var faDelete = 'fa-eraser';
+ var faProperties = 'fa-database';
+ var faTags = 'fa-hashtag';
+ var faEmpty = 'fa-trash-o';
+ var faRestore = 'fa-repeat';
+ var faShowParent = 'fa-location-arrow';
+ var $folderIcon = $('
', {
+ "class": faFolder + " fa cp-app-drive-icon-folder cp-app-drive-content-icon"
+ });
+ //var $folderIcon = $(' ', {src: "/customize/images/icons/folder.svg", "class": "folder icon"});
+ var $folderEmptyIcon = $folderIcon.clone();
+ var $folderOpenedIcon = $('', {"class": faFolderOpen + " fa cp-app-drive-icon-folder"});
+ //var $folderOpenedIcon = $(' ', {src: "/customize/images/icons/folderOpen.svg", "class": "folder icon"});
+ var $folderOpenedEmptyIcon = $folderOpenedIcon.clone();
+ //var $upIcon = $('', {"class": "fa fa-arrow-circle-up"});
+ var $unsortedIcon = $('', {"class": "fa fa-files-o"});
+ var $templateIcon = $('', {"class": "fa fa-cubes"});
+ var $recentIcon = $('', {"class": "fa fa-clock-o"});
+ var $trashIcon = $('', {"class": "fa " + faTrash});
+ var $trashEmptyIcon = $('', {"class": "fa fa-trash-o"});
+ //var $collapseIcon = $('', {"class": "fa fa-minus-square-o cp-app-drive-icon-expcol"});
+ var $expandIcon = $('', {"class": "fa fa-plus-square-o cp-app-drive-icon-expcol"});
+ var $emptyTrashIcon = $('', {"class": "fa fa-ban"});
+ var $listIcon = $('', {"class": "fa fa-list"});
+ var $gridIcon = $('', {"class": "fa fa-th-large"});
+ var $sortAscIcon = $('', {"class": "fa fa-angle-up sortasc"});
+ var $sortDescIcon = $('', {"class": "fa fa-angle-down sortdesc"});
+ var $closeIcon = $('', {"class": "fa fa-window-close"});
+ //var $backupIcon = $('', {"class": "fa fa-life-ring"});
+ var $searchIcon = $('', {"class": "fa fa-search cp-app-drive-tree-search-con"});
+ var $addIcon = $('', {"class": "fa fa-plus"});
+ var $renamedIcon = $('', {"class": "fa fa-flag"});
+ var $readonlyIcon = $('', {"class": "fa " + faReadOnly});
+ var $ownedIcon = $('', {"class": "fa fa-id-card-o"});
+ var $ownerIcon = $('', {"class": "fa fa-id-card"});
+ var $tagsIcon = $('', {"class": "fa " + faTags});
var LS_LAST = "app-drive-lastOpened";
var LS_OPENED = "app-drive-openedFolders";
@@ -157,48 +202,6 @@ define([
}
};
- // Icons
- var faFolder = 'fa-folder';
- var faFolderOpen = 'fa-folder-open';
- var faReadOnly = 'fa-eye';
- var faRename = 'fa-pencil';
- var faTrash = 'fa-trash';
- var faDelete = 'fa-eraser';
- var faProperties = 'fa-database';
- var faTags = 'fa-hashtag';
- var faEmpty = 'fa-trash-o';
- var faRestore = 'fa-repeat';
- var faShowParent = 'fa-location-arrow';
- var $folderIcon = $('', {
- "class": faFolder + " fa cp-app-drive-icon-folder cp-app-drive-content-icon"
- });
- //var $folderIcon = $(' ', {src: "/customize/images/icons/folder.svg", "class": "folder icon"});
- var $folderEmptyIcon = $folderIcon.clone();
- var $folderOpenedIcon = $('', {"class": faFolderOpen + " fa cp-app-drive-icon-folder"});
- //var $folderOpenedIcon = $(' ', {src: "/customize/images/icons/folderOpen.svg", "class": "folder icon"});
- var $folderOpenedEmptyIcon = $folderOpenedIcon.clone();
- //var $upIcon = $('', {"class": "fa fa-arrow-circle-up"});
- var $unsortedIcon = $('', {"class": "fa fa-files-o"});
- var $templateIcon = $('', {"class": "fa fa-cubes"});
- var $recentIcon = $('', {"class": "fa fa-clock-o"});
- var $trashIcon = $('', {"class": "fa " + faTrash});
- var $trashEmptyIcon = $('', {"class": "fa fa-trash-o"});
- //var $collapseIcon = $('', {"class": "fa fa-minus-square-o cp-app-drive-icon-expcol"});
- var $expandIcon = $('', {"class": "fa fa-plus-square-o cp-app-drive-icon-expcol"});
- var $emptyTrashIcon = $('', {"class": "fa fa-ban"});
- var $listIcon = $('', {"class": "fa fa-list"});
- var $gridIcon = $('', {"class": "fa fa-th-large"});
- var $sortAscIcon = $('', {"class": "fa fa-angle-up sortasc"});
- var $sortDescIcon = $('', {"class": "fa fa-angle-down sortdesc"});
- var $closeIcon = $('', {"class": "fa fa-window-close"});
- //var $backupIcon = $('', {"class": "fa fa-life-ring"});
- var $searchIcon = $('', {"class": "fa fa-search cp-app-drive-tree-search-con"});
- var $addIcon = $('', {"class": "fa fa-plus"});
- var $renamedIcon = $('', {"class": "fa fa-flag"});
- var $readonlyIcon = $('', {"class": "fa " + faReadOnly});
- var $ownedIcon = $('', {"class": "fa fa-id-card-o"});
- var $ownerIcon = $('', {"class": "fa fa-id-card"});
-
var history = {
isHistoryMode: false,
};
@@ -360,10 +363,16 @@ define([
// Categories dislayed in the menu
// _WORKGROUP_ : do not display unsorted
var displayedCategories = [ROOT, TRASH, SEARCH, RECENT];
+
+ // PCS enabled: display owned pads
if (AppConfig.displayCreationScreen) { displayedCategories.push(OWNED); }
+ // Templates enabled: display template category
if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); }
+ // Tags used: display Tags category
+ if (Object.keys(filesOp.getTagsList()).length) { displayedCategories.push(TAGS); }
+
if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; }
- var virtualCategories = [SEARCH, RECENT, OWNED];
+ var virtualCategories = [SEARCH, RECENT, OWNED, TAGS];
if (!APP.loggedIn) {
displayedCategories = [FILES_DATA];
@@ -1444,6 +1453,7 @@ define([
case SEARCH: pName = SEARCH_NAME; break;
case RECENT: pName = RECENT_NAME; break;
case OWNED: pName = OWNED_NAME; break;
+ case TAGS: pName = TAGS_NAME; break;
default: pName = name;
}
return pName;
@@ -1512,6 +1522,8 @@ define([
case OWNED:
msg = Messages.fm_info_owned;
break;
+ case TAGS:
+ break;
default:
msg = undefined;
}
@@ -2136,6 +2148,13 @@ define([
}
var $openDir = $(' ', {'class': 'cp-app-drive-search-opendir'}).append($a);
+ $('').text(Messages.fc_prop).click(function () {
+ APP.getProperties(r.id, function (e, $prop) {
+ if (e) { return void logError(e); }
+ UI.alert($prop[0], undefined, true);
+ });
+ }).appendTo($openDir);
+
// rows 1-3
$('').append($icon).append($title).append($typeName).append($type).appendTo($table);
$(' ').append($path).append($atimeName).append($atime).appendTo($table);
@@ -2226,6 +2245,35 @@ define([
});
};
+ // Tags category
+ var displayTags = function ($container) {
+ var list = filesOp.getTagsList();
+ if (Object.keys(list).length === 0) { return; }
+ var sortedTags = Object.keys(list);
+ sortedTags.sort(function (a, b) {
+ return list[b] - list[a];
+ });
+ var lines = [
+ h('tr', [
+ h('th', Messages.fm_tags_name),
+ h('th', Messages.fm_tags_used)
+ ])
+ ];
+ sortedTags.forEach(function (tag) {
+ var tagLink = h('a', { href: '#' }, '#' + tag);
+ $(tagLink).click(function () {
+ if (displayedCategories.indexOf(SEARCH) !== -1) {
+ APP.Search.$input.val('#' + tag).keyup();
+ }
+ });
+ lines.push(h('tr', [
+ h('td', tagLink),
+ h('td.cp-app-drive-tags-used', list[tag])
+ ]));
+ });
+ $(h('li.cp-app-drive-tags-list', h('table', lines))).appendTo($container);
+ };
+
// Display the selected directory into the content part (rightside)
// NOTE: Elements in the trash are not using the same storage structure as the others
// _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage
@@ -2255,10 +2303,9 @@ define([
var isTrashRoot = filesOp.comparePath(path, [TRASH]);
var isTemplate = filesOp.comparePath(path, [TEMPLATE]);
var isAllFiles = filesOp.comparePath(path, [FILES_DATA]);
- var isSearch = path[0] === SEARCH;
- var isRecent = path[0] === RECENT;
- var isOwned = path[0] === OWNED;
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
+ var isSearch = path[0] === SEARCH;
+ var isTags = path[0] === TAGS;
var root = isVirtual ? undefined : filesOp.find(path);
if (!isVirtual && typeof(root) === "undefined") {
@@ -2292,7 +2339,7 @@ define([
var $dirContent = $('', {id: FOLDER_CONTENT_ID});
$dirContent.data('path', path);
- if (!isSearch) {
+ if (!isSearch && !isTags) {
var mode = getViewMode();
if (mode) {
$dirContent.addClass(getViewModeClass());
@@ -2354,10 +2401,12 @@ define([
displayTrashRoot($list, $folderHeader, $fileHeader);
} else if (isSearch) {
displaySearch($list, path[1]);
- } else if (isRecent) {
+ } else if (path[0] === RECENT) {
displayRecent($list);
- } else if (isOwned) {
+ } else if (path[0] === OWNED) {
displayOwned($list);
+ } else if (isTags) {
+ displayTags($list);
} else {
$dirContent.contextmenu(openContextMenu('content'));
if (filesOp.hasSubfolder(root)) { $list.append($folderHeader); }
@@ -2499,25 +2548,6 @@ define([
});
};
- var createTemplate = function ($container, path) {
- var $icon = $templateIcon.clone();
- var isOpened = filesOp.comparePath(path, currentPath);
- var $element = createTreeElement(TEMPLATE_NAME, $icon, [TEMPLATE], false, true, false, isOpened);
- $element.addClass('cp-app-drive-tree-root');
- var $list = $('
', { 'class': 'cp-app-drive-tree-category' }).append($element);
- $container.append($list);
- };
-
- var createAllFiles = function ($container, path) {
- var $icon = $unsortedIcon.clone();
- var isOpened = filesOp.comparePath(path, currentPath);
- var $allfilesElement = createTreeElement(FILES_DATA_NAME, $icon, [FILES_DATA], false, false, false, isOpened);
- $allfilesElement.addClass('root');
- var $allfilesList = $('', { 'class': 'cp-app-drive-tree-category' })
- .append($allfilesElement);
- $container.append($allfilesList);
- };
-
var createTrash = function ($container, path) {
var $icon = filesOp.isFolderEmpty(files[TRASH]) ? $trashEmptyIcon.clone() : $trashIcon.clone();
var isOpened = filesOp.comparePath(path, currentPath);
@@ -2530,29 +2560,11 @@ define([
$container.append($trashList);
};
- var createRecent = function ($container, path) {
- var $icon = $recentIcon.clone();
- var isOpened = filesOp.comparePath(path, currentPath);
- var $element = createTreeElement(RECENT_NAME, $icon, [RECENT], false, false, false, isOpened);
- $element.addClass('root');
- var $list = $('', { 'class': 'cp-app-drive-tree-category' }).append($element);
- $container.append($list);
- };
-
- var createOwned = function ($container, path) {
- var $icon = $ownedIcon.clone(); // TODO
- var isOpened = filesOp.comparePath(path, currentPath);
- var $element = createTreeElement(OWNED_NAME, $icon, [OWNED], false, false, false, isOpened);
- $element.addClass('root');
- var $list = $('', { 'class': 'cp-app-drive-tree-category' }).append($element);
- $container.append($list);
- };
-
var search = APP.Search = {};
var createSearch = function ($container) {
var isInSearch = currentPath[0] === SEARCH;
var $div = $('', {'id': 'cp-app-drive-tree-search', 'class': 'cp-unselectable'});
- var $input = $('
', {
+ var $input = APP.Search.$input = $('
', {
id: 'cp-app-drive-tree-search-input',
type: 'text',
draggable: false,
@@ -2602,6 +2614,38 @@ define([
$container.append($div);
};
+ var categories = {};
+ categories[FILES_DATA] = {
+ name: FILES_DATA_NAME,
+ $icon: $unsortedIcon
+ };
+ categories[TEMPLATE] = {
+ name: TEMPLATE_NAME,
+ droppable: true,
+ $icon: $templateIcon
+ };
+ categories[RECENT] = {
+ name: RECENT_NAME,
+ $icon: $recentIcon
+ };
+ categories[OWNED] = {
+ name: OWNED_NAME,
+ $icon: $ownedIcon
+ };
+ categories[TAGS] = {
+ name: TAGS_NAME,
+ $icon: $tagsIcon
+ };
+ var createCategory = function ($container, cat) {
+ var options = categories[cat];
+ var $icon = options.$icon.clone();
+ var isOpened = filesOp.comparePath([cat], currentPath);
+ var $element = createTreeElement(options.name, $icon, [cat], options.draggable, options.droppable, false, isOpened);
+ $element.addClass('cp-app-drive-tree-root');
+ var $list = $('
', { 'class': 'cp-app-drive-tree-category' }).append($element);
+ $container.append($list);
+ };
+
APP.resetTree = function () {
var $categories = $tree.find('.cp-app-drive-tree-categories-container');
var s = $categories.scrollTop() || 0;
@@ -2610,11 +2654,12 @@ define([
if (displayedCategories.indexOf(SEARCH) !== -1) { createSearch($tree); }
var $div = $('', {'class': 'cp-app-drive-tree-categories-container'})
.appendTo($tree);
- if (displayedCategories.indexOf(RECENT) !== -1) { createRecent($div, [RECENT]); }
- if (displayedCategories.indexOf(OWNED) !== -1) { createOwned($div, [OWNED]); }
+ if (displayedCategories.indexOf(TAGS) !== -1) { createCategory($div, TAGS); }
+ if (displayedCategories.indexOf(RECENT) !== -1) { createCategory($div, RECENT); }
+ if (displayedCategories.indexOf(OWNED) !== -1) { createCategory($div, OWNED); }
if (displayedCategories.indexOf(ROOT) !== -1) { createTree($div, [ROOT]); }
- if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($div, [TEMPLATE]); }
- if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($div, [FILES_DATA]); }
+ if (displayedCategories.indexOf(TEMPLATE) !== -1) { createCategory($div, TEMPLATE); }
+ if (displayedCategories.indexOf(FILES_DATA) !== -1) { createCategory($div, FILES_DATA); }
if (displayedCategories.indexOf(TRASH) !== -1) { createTrash($div, [TRASH]); }
$tree.append(APP.$limit);
@@ -2668,7 +2713,7 @@ define([
}
});
- var getProperties = function (el, cb) {
+ var getProperties = APP.getProperties = function (el, cb) {
if (!filesOp.isFile(el)) {
return void cb('NOT_FILE');
}
diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less
new file mode 100644
index 000000000..03b9267e5
--- /dev/null
+++ b/www/kanban/app-kanban.less
@@ -0,0 +1,173 @@
+@import (once) "../../customize/src/less2/include/browser.less";
+@import (once) "../../customize/src/less2/include/framework.less";
+@import (once) "../../customize/src/less2/include/tools.less";
+
+.framework_main( @bg-color: @colortheme_kanban-bg,
+@warn-color: @colortheme_kanban-warn,
+@color: @colortheme_kanban-color);
+
+// body
+&.cp-app-kanban {
+ display: flex;
+ flex-flow: column;
+ max-height: 100%;
+ min-height: auto;
+
+ #cp-app-kanban-container {
+ flex: 1;
+ display: flex;
+ flex-flow: column;
+ }
+ #cp-app-kanban-editor {
+ flex: 1;
+ display: flex;
+ flex-flow: row;
+ height: 100%;
+ overflow: hidden;
+ }
+ #cp-app-kanban-content {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-flow: column;
+ .kanban-container-outer {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ min-height: -webkit-min-content;
+ min-height: min-content;
+ .kanban-container {
+ flex: 1;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-around;
+ }
+ }
+
+ .kanban-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .kanban-board {
+ header {
+ display: flex;
+ align-items: center;
+ .kanban-title-board {
+ flex: 1;
+ margin-right: 10px;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ #kanban-edit {
+ color: black;
+ font-weight: bold;
+ }
+ }
+ }
+
+ #kanban-edit {
+ width: 100%;
+ background: transparent;
+ border: 1px solid rgba(0,0,0,0.3);
+ }
+
+ @button-size: 50px;
+ #kanban-addboard {
+ margin: 30px;
+ border: 1px solid;
+ width: @button-size;
+ height: @button-size;
+ line-height: @button-size;
+ text-align: center;
+ background: @colortheme_kanban-bg;
+ font-weight: bold;
+ align-self: flex-start;
+ font-size: 50px;
+ cursor: pointer;
+ .tools_unselectable();
+ }
+
+ .kanban-remove-item {
+ padding: 0 0.5em;
+ visibility: hidden;
+ }
+ .kanban-item:hover {
+ .kanban-remove-item {
+ visibility: visible;
+ }
+ }
+
+ .kanban-additem {
+ float: right;
+ background: #EEE;
+ padding: 5px .5rem 4px;
+ line-height: 1;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ font-weight: bold;
+ font-size: 1.2em;
+ &:hover {
+ background: transparent;
+ }
+ }
+
+ .kanban-header-yellow {
+ background: #FC3;
+ }
+
+ .kanban-header-orange {
+ background: #F91;
+ }
+
+ .kanban-header-blue {
+ background: #0AC;
+ }
+
+ .kanban-header-red {
+ background: #E43;
+ }
+
+ .kanban-header-green {
+ background: #8C4;
+ }
+
+ .kanban-header-purple {
+ background: #c851ff;
+ }
+
+ .kanban-header-cyan {
+ background: #00ffff;
+ }
+
+ .kanban-header-lightgreen {
+ background: #c3ff5b;
+ }
+
+ .kanban-header-lightblue {
+ background: #adeeff;
+ }
+
+ @media (max-width: @browser_media-medium-screen) {
+ #cp-app-kanban-container {
+ flex: 1;
+ max-width: 100%;
+ resize: none;
+ }
+ }
+
+ &.cp-app-readonly {
+ .kanban-item, .kanban-title-board {
+ cursor: default !important;
+ .tools_unselectable();
+ }
+ .kanban-title-button, #kanban-addboard, .kanban-remove-item, .kanban-additem {
+ display: none !important;
+ }
+ }
+ }
+
+}
diff --git a/www/kanban/index.html b/www/kanban/index.html
new file mode 100644
index 000000000..e1f312ebd
--- /dev/null
+++ b/www/kanban/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
CryptPad
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/kanban/inner.html b/www/kanban/inner.html
new file mode 100644
index 000000000..0cc1ecdb2
--- /dev/null
+++ b/www/kanban/inner.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/kanban/inner.js b/www/kanban/inner.js
new file mode 100644
index 000000000..be2906e87
--- /dev/null
+++ b/www/kanban/inner.js
@@ -0,0 +1,399 @@
+define([
+ 'jquery',
+ 'json.sortify',
+ '/bower_components/nthen/index.js',
+ '/common/sframe-common.js',
+ '/common/sframe-app-framework.js',
+ '/common/common-util.js',
+ '/common/common-hash.js',
+ '/common/common-interface.js',
+ '/common/modes.js',
+ '/customize/messages.js',
+ '/kanban/jkanban.js',
+ 'css!/kanban/jkanban.css',
+], function (
+ $,
+ Sortify,
+ nThen,
+ SFCommon,
+ Framework,
+ Util,
+ Hash,
+ UI,
+ Modes,
+ Messages)
+{
+
+ var verbose = function (x) { console.log(x); };
+ verbose = function () {}; // comment out to enable verbose logging
+
+ var COLORS = ['yellow', 'green', 'orange', 'blue', 'red', 'purple', 'cyan', 'lightgreen', 'lightblue'];
+
+ var addRemoveItemButton = function (framework, kanban) {
+ if (!kanban) { return; }
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ var $container = $(kanban.element);
+ $container.find('.kanban-remove-item').remove();
+ $container.find('.kanban-board .kanban-item').each(function (i, el) {
+ var pos = kanban.findElementPosition(el);
+ var board = kanban.options.boards.find(function (b) {
+ return b.id === $(el.parentNode.parentNode).attr('data-id');
+ });
+ $('
', {
+ 'class': 'kanban-remove-item btn btn-default',
+ title: Messages.kanban_removeItem
+ }).click(function (e) {
+ e.stopPropagation();
+ UI.confirm(Messages.kanban_removeItemConfirm, function (yes) {
+ if (!yes) { return; }
+ board.item.splice(pos, 1);
+ $(el).remove();
+ kanban.onChange();
+ });
+ }).text('❌').appendTo($(el));
+ });
+ };
+
+ // Kanban code
+ var initKanban = function (framework, boards) {
+ var defaultBoards = [{
+ "id": "todo",
+ "title": Messages.kanban_todo,
+ "color": "blue",
+ "item": [{
+ "title": Messages._getKey('kanban_item', [1])
+ }, {
+ "title": Messages._getKey('kanban_item', [2])
+ }]
+ }, {
+ "id": "working",
+ "title": Messages.kanban_working,
+ "color": "orange",
+ "item": [{
+ "title": Messages._getKey('kanban_item', [3])
+ }, {
+ "title": Messages._getKey('kanban_item', [4])
+ }]
+ }, {
+ "id": "done",
+ "title": Messages.kanban_done,
+ "color": "green",
+ "item": [{
+ "title": Messages._getKey('kanban_item', [5])
+ }, {
+ "title": Messages._getKey('kanban_item', [6])
+ }]
+ }];
+
+ if (!boards) {
+ verbose("Initializing with default boards content");
+ boards = defaultBoards;
+ } else {
+ verbose("Initializing with boards content " + boards);
+ }
+
+ // Remove any existing elements
+ $(".kanban-container-outer").remove();
+
+ var getInput = function () {
+ return $(' ', {
+ 'type': 'text',
+ 'id': 'kanban-edit',
+ 'size': '30'
+ });
+ };
+
+ var kanban = new window.jKanban({
+ element: '#cp-app-kanban-content',
+ gutter: '15px',
+ widthBoard: '300px',
+ buttonContent: '❌',
+ colors: COLORS,
+ readOnly: framework.isReadOnly(),
+ onChange: function () {
+ verbose("Board object has changed");
+ framework.localChange();
+ if (kanban) {
+ addRemoveItemButton(framework, kanban);
+ }
+ },
+ click: function (el) {
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ if (kanban.inEditMode) {
+ verbose("An edit is already active");
+ return;
+ }
+ kanban.inEditMode = true;
+ $(el).find('button').remove();
+ var name = $(el).text();
+ $(el).html('');
+ var $input = getInput().val(name).appendTo(el).focus();
+ $input[0].select();
+ var save = function () {
+ // Store the value
+ var name = $input.val();
+ // Remove the input
+ $(el).text(name);
+ // Save the value for the correct board
+ var board = $(el.parentNode.parentNode).attr("data-id");
+ var pos = kanban.findElementPosition(el);
+ kanban.getBoardJSON(board).item[pos].title = name;
+ kanban.onChange();
+ // Unlock edit mode
+ kanban.inEditMode = false;
+ };
+ $input.blur(save);
+ $input.keydown(function (e) {
+ if (e.which === 13) {
+ e.preventDefault();
+ e.stopPropagation();
+ save();
+ return;
+ }
+ if (e.which === 27) {
+ e.preventDefault();
+ e.stopPropagation();
+ $(el).text(name);
+ kanban.inEditMode = false;
+ addRemoveItemButton(framework, kanban);
+ return;
+ }
+ });
+
+ },
+ boardTitleClick: function (el, e) {
+ e.stopPropagation();
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ if (kanban.inEditMode) {
+ verbose("An edit is already active");
+ return;
+ }
+ kanban.inEditMode = true;
+ var name = $(el).text();
+ $(el).html('');
+ var $input = getInput().val(name).appendTo(el).focus();
+ $input[0].select();
+ var save = function () {
+ // Store the value
+ var name = $input.val();
+ // Remove the input
+ $(el).text(name);
+ // Save the value for the correct board
+ var board = $(el.parentNode.parentNode).attr("data-id");
+ kanban.getBoardJSON(board).title = name;
+ kanban.onChange();
+ // Unlock edit mode
+ kanban.inEditMode = false;
+ };
+ $input.blur(save);
+ $input.keydown(function (e) {
+ if (e.which === 13) {
+ e.preventDefault();
+ e.stopPropagation();
+ save();
+ return;
+ }
+ if (e.which === 27) {
+ e.preventDefault();
+ e.stopPropagation();
+ $(el).text(name);
+ kanban.inEditMode = false;
+ return;
+ }
+ });
+ },
+ colorClick: function (el) {
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ verbose("in color click");
+ var board = $(el.parentNode).attr("data-id");
+ var boardJSON = kanban.getBoardJSON(board);
+ var currentColor = boardJSON.color;
+ verbose("Current color " + currentColor);
+ var index = kanban.options.colors.findIndex(function (element) {
+ return (element === currentColor);
+ }) + 1;
+ verbose("Next index " + index);
+ if (index >= kanban.options.colors.length) { index = 0; }
+ var nextColor = kanban.options.colors[index];
+ verbose("Next color " + nextColor);
+ boardJSON.color = nextColor;
+ $(el).removeClass("kanban-header-" + currentColor);
+ $(el).addClass("kanban-header-" + nextColor);
+ kanban.onChange();
+ },
+ buttonClick: function (el, boardId, e) {
+ e.stopPropagation();
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ UI.confirm(Messages.kanban_deleteBoard, function (yes) {
+ if (!yes) { return; }
+ verbose("Delete board");
+ //var boardName = $(el.parentNode.parentNode).attr("data-id");
+ for (var index in kanban.options.boards) {
+ if (kanban.options.boards[index].id === boardId) {
+ break;
+ }
+ index++;
+ }
+ kanban.options.boards.splice(index, 1);
+ kanban.removeBoard(boardId);
+ kanban.onChange();
+ });
+ },
+ addItemClick: function (el) {
+ if (framework.isReadOnly() || framework.isLocked()) { return; }
+ if (kanban.inEditMode) {
+ verbose("An edit is already active");
+ return;
+ }
+ kanban.inEditMode = true;
+ // create a form to enter element
+ var boardId = $(el.parentNode.parentNode).attr("data-id");
+ var $item = $('', {'class': 'kanban-item'});
+ var $input = getInput().val(name).appendTo($item);
+ kanban.addForm(boardId, $item[0]);
+ $input.focus();
+ var save = function () {
+ $item.remove();
+ kanban.inEditMode = false;
+ if (!$input.val()) { return; }
+ kanban.addElement(boardId, {
+ "title": $input.val(),
+ });
+ };
+ $input.blur(save);
+ $input.keydown(function (e) {
+ if (e.which === 13) {
+ e.preventDefault();
+ e.stopPropagation();
+ save();
+ return;
+ }
+ if (e.which === 27) {
+ e.preventDefault();
+ e.stopPropagation();
+ $item.remove();
+ kanban.inEditMode = false;
+ return;
+ }
+ });
+ },
+ addItemButton: true,
+ boards: boards
+ });
+
+ var addBoardDefault = document.getElementById('kanban-addboard');
+ $(addBoardDefault).attr('title', Messages.kanban_addBoard);
+ addBoardDefault.addEventListener('click', function () {
+ if (framework.isReadOnly()) { return; }
+ var counter = 1;
+
+ // Get the new board id
+ var boardExists = function (b) { return b.id === "board" + counter; };
+ while (kanban.options.boards.some(boardExists)) { counter++; }
+
+ kanban.addBoards([{
+ "id": "board" + counter,
+ "title": Messages.kanban_newBoard,
+ "color": COLORS[Math.floor(Math.random()*COLORS.length)], // random color
+ "item": [{
+ "title": Messages._getKey('kanban_item', [1]),
+ }]
+ }]);
+ kanban.onChange();
+ });
+
+ return kanban;
+ };
+
+ var mkHelpMenu = function (framework) {
+ var $toolbarContainer = $('#cp-app-kanban-container');
+ var helpMenu = framework._.sfCommon.createHelpMenu(['kanban']);
+ $toolbarContainer.prepend(helpMenu.menu);
+
+ framework._.toolbar.$drawer.append(helpMenu.button);
+ };
+
+ // Start of the main loop
+ var andThen2 = function (framework) {
+
+ var kanban;
+ var $container = $('#cp-app-kanban-content');
+
+ mkHelpMenu(framework);
+
+ if (framework.isReadOnly()) {
+ $container.addClass('cp-app-readonly');
+ }
+ framework.onEditableChange(function (unlocked) {
+ if (framework.isReadOnly()) { return; }
+ if (!kanban) { return; }
+ if (unlocked) {
+ addRemoveItemButton(framework, kanban);
+ kanban.options.readOnly = false;
+ return void $container.removeClass('cp-app-readonly');
+ }
+ kanban.options.readOnly = true;
+ $container.addClass('cp-app-readonly');
+ });
+
+ framework.onContentUpdate(function (newContent) {
+ // Init if needed
+ if (!kanban) {
+ kanban = initKanban(framework, (newContent || {}).content);
+ addRemoveItemButton(framework, kanban);
+ return;
+ }
+
+ // Need to update the content
+ verbose("Content should be updated to " + newContent);
+ var currentContent = kanban.getBoardsJSON();
+ var remoteContent = newContent.content;
+
+ if (Sortify(currentContent) !== Sortify(remoteContent)) {
+ // reinit kanban (TODO: optimize to diff only)
+ verbose("Content is different.. Applying content");
+ kanban.setBoards(remoteContent);
+ kanban.inEditMode = false;
+ addRemoveItemButton(framework, kanban);
+ }
+ });
+
+ framework.setContentGetter(function () {
+ if (!kanban) {
+ return {
+ content: []
+ };
+ }
+ var content = kanban.getBoardsJSON();
+ verbose("Content current value is " + content);
+ return {
+ content: content
+ };
+ });
+
+ framework.onReady(function () {
+ $("#cp-app-kanban-content").focus();
+ });
+
+ framework.onDefaultContentNeeded(function () {
+ kanban = initKanban(framework);
+ });
+
+ framework.start();
+ };
+
+ var main = function () {
+ // var framework;
+ nThen(function (waitFor) {
+
+ // Framework initialization
+ Framework.create({
+ toolbarContainer: '#cme_toolbox',
+ contentContainer: '#cp-app-kanban-editor',
+ }, waitFor(function (framework) {
+ andThen2(framework);
+ }));
+ });
+ };
+ main();
+});
diff --git a/www/kanban/jkanban.css b/www/kanban/jkanban.css
new file mode 100644
index 000000000..4bbb28128
--- /dev/null
+++ b/www/kanban/jkanban.css
@@ -0,0 +1,100 @@
+.kanban-container * {
+ box-sizing: border-box;
+}
+
+.kanban-board {
+ position: relative;
+ float: left;
+ background: #E2E4E6;
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+ margin: 10px;
+ vertical-align: top;
+ display: flex;
+ flex-flow: column;
+}
+
+.kanban-board.disabled-board {
+ opacity: .3;
+}
+
+.kanban-board.is-moving.gu-mirror {
+ transform: rotate(3deg);
+}
+
+.kanban-board.is-moving.gu-mirror .kanban-drag {
+ overflow: hidden;
+ padding-right: 50px;
+}
+
+.kanban-board header {
+ font-size: 16px;
+ padding: 10px;
+}
+
+.kanban-board header .kanban-title-board {
+ font-weight: 700;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+
+.kanban-board header .kanban-title-button {
+ float: right;
+ line-height: 1;
+ padding: .25rem .5rem;
+}
+
+.kanban-board .kanban-drag {
+ min-height: 200px;
+ padding: 20px;
+ flex: 1;
+}
+
+.kanban-item {
+ background: #fff;
+ padding: 15px;
+ margin-bottom: 20px;
+ transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.kanban-item:hover {
+ cursor: move;
+}
+
+.kanban-item:last-child {
+ margin: 0;
+}
+
+.kanban-item.is-moving.gu-mirror {
+ transform: rotate(3deg);
+ height: auto !important;
+}
+
+/* Dragula CSS */
+
+.gu-mirror {
+ position: fixed !important;
+ margin: 0 !important;
+ z-index: 9999 !important;
+}
+
+.gu-hide {
+ display: none !important;
+}
+
+.gu-unselectable {
+ -webkit-user-select: none !important;
+ -moz-user-select: none !important;
+ -ms-user-select: none !important;
+ user-select: none !important;
+}
+
+.gu-transit {
+ opacity: 0.2 !important;
+ transform: rotate(0deg) !important;
+}
+
+.form-group {
+ text-align: right;
+ margin-button: 5px;
+}
diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js
new file mode 100644
index 000000000..65dc24d2e
--- /dev/null
+++ b/www/kanban/jkanban.js
@@ -0,0 +1,1569 @@
+(function e(t, n, r) {
+ function s(o, u) {
+ if (!n[o]) {
+ if (!t[o]) {
+ var a = typeof require == "function" && require;
+ if (!u && a) return a(o, !0);
+ if (i) return i(o, !0);
+ var f = new Error("Cannot find module '" + o + "'");
+ throw f.code = "MODULE_NOT_FOUND", f
+ }
+ var l = n[o] = {
+ exports: {}
+ };
+ t[o][0].call(l.exports, function (e) {
+ var n = t[o][1][e];
+ return s(n ? n : e)
+ }, l, l.exports, e, t, n, r)
+ }
+ return n[o].exports
+ }
+ var i = typeof require == "function" && require;
+ for (var o = 0; o < r.length; o++) s(r[o]);
+ return s
+})({
+ 1: [function (require, module, exports) {
+ /**
+ * jKanban
+ * Vanilla Javascript plugin for manage kanban boards
+ *
+ * @site: http://www.riccardotartaglia.it/jkanban/
+ * @author: Riccardo Tartaglia
+ */
+
+ //Require dragula
+ var dragula = require('dragula');
+
+ (function () {
+
+ this.jKanban = function () {
+ var self = this;
+ this.element = '';
+ this.container = '';
+ this.boardContainer = [];
+ this.dragula = dragula;
+ this.drake = '';
+ this.drakeBoard = '';
+ this.addItemButton = false;
+ this.buttonContent = '+';
+ defaults = {
+ element: '',
+ gutter: '15px',
+ widthBoard: '250px',
+ responsive: '700',
+ colors: ["yellow", "green", "blue", "red", "orange"],
+ boards: [],
+ dragBoards: true,
+ addItemButton: false,
+ buttonContent: '+',
+ readOnly: false,
+ dragEl: function (el, source) {},
+ dragendEl: function (el) {},
+ dropEl: function (el, target, source, sibling) {},
+ dragcancelEl: function (el, boardId) {},
+ dragBoard: function (el, source) {},
+ dragendBoard: function (el) {},
+ dropBoard: function (el, target, source, sibling) {},
+ click: function (el) {},
+ boardTitleclick: function (el, boardId) {},
+ buttonClick: function (el, boardId) {},
+ colorClick: function (el, boardId) {},
+ addItemClick: function (el, boardId) {},
+ onChange: function () {}
+ };
+
+ if (arguments[0] && typeof arguments[0] === "object") {
+ this.options = __extendDefaults(defaults, arguments[0]);
+ }
+
+ this.init = function () {
+ // set initial boards
+ __setBoard();
+ //set drag with dragula
+ if (window.innerWidth > self.options.responsive) {
+
+ //Init Drag Board
+ self.drakeBoard = self.dragula([self.container], {
+ moves: function (el, source, handle, sibling) {
+ if (self.options.readOnly) { return false; }
+ if (!self.options.dragBoards) return false;
+ return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board'));
+ },
+ accepts: function (el, target, source, sibling) {
+ if (self.options.readOnly) { return false; }
+ return target.classList.contains('kanban-container');
+ },
+ revertOnSpill: true,
+ direction: 'horizontal',
+ })
+ .on('drag', function (el, source) {
+ el.classList.add('is-moving');
+ self.options.dragBoard(el, source);
+ if (typeof (el.dragfn) === 'function')
+ el.dragfn(el, source);
+ })
+ .on('dragend', function (el) {
+ el.classList.remove('is-moving');
+ self.options.dragendBoard(el);
+ if (typeof (el.dragendfn) === 'function')
+ el.dragendfn(el);
+ })
+ .on('drop', function (el, target, source, sibling) {
+ el.classList.remove('is-moving');
+ self.options.dropBoard(el, target, source, sibling);
+ if (typeof (el.dropfn) === 'function')
+ el.dropfn(el, target, source, sibling);
+
+ // TODO: update board object board order
+ console.log("Drop " + $(el).attr("data-id") + " just before " + (sibling ? $(sibling).attr("data-id") : " end "));
+ var index1, index2;
+ self.options.boards.some(function (element, index) {
+ if (element.id === $(el).attr("data-id")) {
+ index1 = index;
+ return true;
+ }
+ });
+ if (sibling) {
+ self.options.boards.some(function (element, index) {
+ if (element.id === $(sibling).attr("data-id")) {
+ index2 = index;
+ return true;
+ }
+ })
+ } else {
+ index2 = self.options.boards.length;
+ }
+ console.log("Switch " + index1 + " and " + index2);
+ if (index1 < index2)
+ index2 = index2 - 1;
+ self.options.boards.splice(index2, 0, self.options.boards.splice(index1, 1)[0]);
+ // send event that board has changed
+ self.onChange();
+
+ });
+
+ //Init Drag Item
+ self.drake = self.dragula(self.boardContainer, {
+ moves: function (el, source, handle, sibling) {
+ if (self.options.readOnly) { return false; }
+ return handle.classList.contains('kanban-item');
+ },
+ accepts: function (el, target, source, sibling) {
+ if (self.options.readOnly) { return false; }
+ return true;
+ },
+ revertOnSpill: true
+ })
+ .on('drag', function (el, source) {
+ // we need to calculate the position before starting to drag
+ self.dragItemPos = self.findElementPosition(el);
+
+ el.classList.add('is-moving');
+ var boardJSON = __findBoardJSON(source.parentNode.dataset.id);
+ if (boardJSON.dragTo !== undefined) {
+ self.options.boards.map(function (board) {
+ if (boardJSON.dragTo.indexOf(board.id) === -1 && board.id !== source.parentNode.dataset.id) {
+ self.findBoard(board.id).classList.add('disabled-board');
+ }
+ })
+ }
+
+ self.options.dragEl(el, source);
+ if (el !== null && typeof (el.dragfn) === 'function')
+ el.dragfn(el, source);
+ })
+ .on('dragend', function (el) {
+ console.log("In dragend");
+ self.options.dragendEl(el);
+ if (el !== null && typeof (el.dragendfn) === 'function')
+ el.dragendfn(el);
+ })
+ .on('cancel', function (el, container, source) {
+ console.log("In cancel");
+ // FIXME custom code
+ var boardId = source.parentNode.dataset.id;
+ self.options.dragcancelEl(el, boardId);
+ })
+ .on('drop', function (el, target, source, sibling) {
+ console.log("In drop");
+
+ // TODO: update board object board order
+ var board1;
+ self.options.boards.some(function (element) {
+ if (element.id === $(source.parentNode).attr("data-id")) {
+ return board1 = element;
+ }
+ });
+ var board2;
+ self.options.boards.some(function (element) {
+ if (element.id === $(target.parentNode).attr("data-id")) {
+ return board2 = element;
+ }
+ });
+ var pos1 = self.dragItemPos;
+ var pos2 = (sibling) ? self.findElementPosition(sibling) : (board2.item.length + 1);
+ console.log("Drop element " + pos1 + " before " + pos2);
+
+ // TODO: update board object item order
+
+ var allB = document.querySelectorAll('.kanban-board');
+ if (allB.length > 0 && allB !== undefined) {
+ for (var i = 0; i < allB.length; i++) {
+ allB[i].classList.remove('disabled-board');
+ }
+ }
+ var boardJSON = __findBoardJSON(source.parentNode.dataset.id);
+ if (boardJSON.dragTo !== undefined) {
+ if (boardJSON.dragTo.indexOf(target.parentNode.dataset.id) === -1 && target.parentNode.dataset.id !== source.parentNode.dataset.id) {
+ self.drake.cancel(true)
+ }
+ }
+ if (el !== null) {
+ self.options.dropEl(el, target, source, sibling);
+ el.classList.remove('is-moving');
+ if (typeof (el.dropfn) === 'function')
+ el.dropfn(el, target, source, sibling);
+ }
+
+ var item = board1.item[pos1];
+ // if (board1==board2 && pos2
'
+ headerBoard.appendChild(btn);
+ __onButtonClickHandler(btn, board.id);
+ }
+ //content board
+ var contentBoard = document.createElement('main');
+ contentBoard.classList.add('kanban-drag');
+ //add drag to array for dragula
+ self.boardContainer.push(contentBoard);
+ for (var itemkey in board.item) {
+ //create item
+ var itemKanban = board.item[itemkey];
+ var nodeItem = document.createElement('div');
+ nodeItem.classList.add('kanban-item');
+ nodeItem.dataset.eid = itemKanban.id;
+ nodeItem.innerHTML = itemKanban.title;
+ //add function
+ nodeItem.clickfn = itemKanban.click;
+ nodeItem.dragfn = itemKanban.drag;
+ nodeItem.dragendfn = itemKanban.dragend;
+ nodeItem.dropfn = itemKanban.drop;
+ //add click handler of item
+ __onclickHandler(nodeItem);
+ contentBoard.appendChild(nodeItem);
+ }
+ //footer board
+ var footerBoard = document.createElement('footer');
+ //add button
+ var addBoardItem = document.createElement('button');
+ $(addBoardItem).text("+")
+ $(addBoardItem).addClass("kanban-additem btn btn-default");
+ footerBoard.appendChild(addBoardItem);
+ __onAddItemClickHandler(addBoardItem);
+
+ //board assembly
+ boardNode.appendChild(headerBoard);
+ boardNode.appendChild(contentBoard);
+ boardNode.appendChild(footerBoard);
+ //board add
+ self.container.appendChild(boardNode);
+ }
+
+ // send event that board has changed
+ self.onChange();
+
+ return self;
+ }
+
+ this.setBoards = function (boards) {
+ self.element
+ for (var boardkey in this.options.boards) {
+ var board = this.options.boards[boardkey];
+ this.removeBoard(board.id);
+ }
+ this.options.boards = [];
+ this.addBoards(boards);
+ }
+
+ this.findBoard = function (id) {
+ var el = self.element.querySelector('[data-id="' + id + '"]');
+ return el;
+ }
+
+ this.findElement = function (id) {
+ var el = self.element.querySelector('[data-eid="' + id + '"]');
+ return el;
+ }
+
+ this.findElementPosition = function (el) {
+ // we are looking at the element position in the child array
+ return $(el.parentNode.children).index(el);
+ }
+
+ this.getBoardElements = function (id) {
+ var board = self.element.querySelector('[data-id="' + id + '"] .kanban-drag');
+ return (board.childNodes);
+ }
+
+ this.removeElement = function (el) {
+ if (typeof (el) === 'string')
+ el = self.element.querySelector('[data-eid="' + el + '"]');
+ el.remove();
+
+ // send event that board has changed
+ self.onChange();
+
+ return self;
+ };
+
+ this.removeBoard = function (board) {
+ if (typeof (board) === 'string')
+ board = self.element.querySelector('[data-id="' + board + '"]');
+ if (board) {
+ board.remove();
+
+ // send event that board has changed
+ self.onChange();
+ }
+
+ return self;
+ }
+
+ // board button on click function
+ this.onButtonClick = function (el) {
+
+ }
+
+ this.onChange = function () {
+ self.options.onChange();
+ }
+
+ this.getBoardsJSON = function (id) {
+ return self.options.boards;
+ }
+
+ this.getBoardJSON = function (id) {
+ return __findBoardJSON(id);
+ }
+
+ //PRIVATE FUNCTION
+ function __extendDefaults(source, properties) {
+ var property;
+ for (property in properties) {
+ if (properties.hasOwnProperty(property)) {
+ source[property] = properties[property];
+ }
+ }
+ return source;
+ }
+
+ function __setBoard() {
+ self.element = document.querySelector(self.options.element);
+ //create container
+ var boardContainerOuter = document.createElement('div');
+ boardContainerOuter.classList.add('kanban-container-outer');
+ var boardContainer = document.createElement('div');
+ boardContainer.classList.add('kanban-container');
+ boardContainerOuter.appendChild(boardContainer);
+ var addBoard = document.createElement('div');
+ addBoard.id = 'kanban-addboard';
+ $(addBoard).text("+");
+ boardContainerOuter.appendChild(addBoard);
+
+ self.container = boardContainer;
+ //add boards
+ self.addBoards(self.options.boards);
+ //appends to container
+ self.element.appendChild(boardContainerOuter);
+
+ // send event that board has changed
+ self.onChange();
+ };
+
+ function __onclickHandler(nodeItem, clickfn) {
+ nodeItem.addEventListener('click', function (e) {
+ e.preventDefault;
+ self.options.click(this);
+ if (typeof (this.clickfn) === 'function')
+ this.clickfn(this);
+ });
+ }
+
+ function __onboardTitleClickHandler(nodeItem, clickfn) {
+ nodeItem.addEventListener('click', function (e) {
+ e.preventDefault;
+ self.options.boardTitleClick(this, e);
+ if (typeof (this.clickfn) === 'function')
+ this.clickfn(this);
+ });
+ }
+
+ function __onColorClickHandler(nodeItem, clickfn) {
+ nodeItem.addEventListener('click', function (e) {
+ e.preventDefault;
+ self.options.colorClick(this);
+ if (typeof (this.clickfn) === 'function')
+ this.clickfn(this);
+ });
+ }
+
+ function __onAddItemClickHandler(nodeItem, clickfn) {
+ nodeItem.addEventListener('click', function (e) {
+ e.preventDefault;
+ self.options.addItemClick(this);
+ if (typeof (this.clickfn) === 'function')
+ this.clickfn(this);
+ });
+ }
+
+ function __onButtonClickHandler(nodeItem, boardId) {
+ nodeItem.addEventListener('click', function (e) {
+ e.stopPropagation();
+ e.preventDefault;
+ self.options.buttonClick(this, boardId, e);
+ // if(typeof(this.clickfn) === 'function')
+ // this.clickfn(this);
+ });
+ }
+
+ function __findBoardJSON(id) {
+ var el = []
+ self.options.boards.map(function (board) {
+ if (board.id === id) {
+ return el.push(board)
+ }
+ })
+ return el[0]
+ }
+
+
+ //init plugin
+ this.init();
+ };
+ }());
+
+
+}, {
+ "dragula": 9
+ }],
+ 2: [function (require, module, exports) {
+ module.exports = function atoa(a, n) {
+ return Array.prototype.slice.call(a, n);
+ }
+
+}, {}],
+ 3: [function (require, module, exports) {
+ 'use strict';
+
+ var ticky = require('ticky');
+
+ module.exports = function debounce(fn, args, ctx) {
+ if (!fn) {
+ return;
+ }
+ ticky(function run() {
+ fn.apply(ctx || null, args || []);
+ });
+ };
+
+}, {
+ "ticky": 10
+ }],
+ 4: [function (require, module, exports) {
+ 'use strict';
+
+ var atoa = require('atoa');
+ var debounce = require('./debounce');
+
+ module.exports = function emitter(thing, options) {
+ var opts = options || {};
+ var evt = {};
+ if (thing === undefined) {
+ thing = {};
+ }
+ thing.on = function (type, fn) {
+ if (!evt[type]) {
+ evt[type] = [fn];
+ } else {
+ evt[type].push(fn);
+ }
+ return thing;
+ };
+ thing.once = function (type, fn) {
+ fn._once = true; // thing.off(fn) still works!
+ thing.on(type, fn);
+ return thing;
+ };
+ thing.off = function (type, fn) {
+ var c = arguments.length;
+ if (c === 1) {
+ delete evt[type];
+ } else if (c === 0) {
+ evt = {};
+ } else {
+ var et = evt[type];
+ if (!et) {
+ return thing;
+ }
+ et.splice(et.indexOf(fn), 1);
+ }
+ return thing;
+ };
+ thing.emit = function () {
+ var args = atoa(arguments);
+ return thing.emitterSnapshot(args.shift()).apply(this, args);
+ };
+ thing.emitterSnapshot = function (type) {
+ var et = (evt[type] || []).slice(0);
+ return function () {
+ var args = atoa(arguments);
+ var ctx = this || thing;
+ if (type === 'error' && opts.throws !== false && !et.length) {
+ throw args.length === 1 ? args[0] : args;
+ }
+ et.forEach(function emitter(listen) {
+ if (opts.async) {
+ debounce(listen, args, ctx);
+ } else {
+ listen.apply(ctx, args);
+ }
+ if (listen._once) {
+ thing.off(type, listen);
+ }
+ });
+ return thing;
+ };
+ };
+ return thing;
+ };
+
+}, {
+ "./debounce": 3,
+ "atoa": 2
+ }],
+ 5: [function (require, module, exports) {
+ (function (global) {
+ 'use strict';
+
+ var customEvent = require('custom-event');
+ var eventmap = require('./eventmap');
+ var doc = global.document;
+ var addEvent = addEventEasy;
+ var removeEvent = removeEventEasy;
+ var hardCache = [];
+
+ if (!global.addEventListener) {
+ addEvent = addEventHard;
+ removeEvent = removeEventHard;
+ }
+
+ module.exports = {
+ add: addEvent,
+ remove: removeEvent,
+ fabricate: fabricateEvent
+ };
+
+ function addEventEasy(el, type, fn, capturing) {
+ return el.addEventListener(type, fn, capturing);
+ }
+
+ function addEventHard(el, type, fn) {
+ return el.attachEvent('on' + type, wrap(el, type, fn));
+ }
+
+ function removeEventEasy(el, type, fn, capturing) {
+ return el.removeEventListener(type, fn, capturing);
+ }
+
+ function removeEventHard(el, type, fn) {
+ var listener = unwrap(el, type, fn);
+ if (listener) {
+ return el.detachEvent('on' + type, listener);
+ }
+ }
+
+ function fabricateEvent(el, type, model) {
+ var e = eventmap.indexOf(type) === -1 ? makeCustomEvent() : makeClassicEvent();
+ if (el.dispatchEvent) {
+ el.dispatchEvent(e);
+ } else {
+ el.fireEvent('on' + type, e);
+ }
+
+ function makeClassicEvent() {
+ var e;
+ if (doc.createEvent) {
+ e = doc.createEvent('Event');
+ e.initEvent(type, true, true);
+ } else if (doc.createEventObject) {
+ e = doc.createEventObject();
+ }
+ return e;
+ }
+
+ function makeCustomEvent() {
+ return new customEvent(type, {
+ detail: model
+ });
+ }
+ }
+
+ function wrapperFactory(el, type, fn) {
+ return function wrapper(originalEvent) {
+ var e = originalEvent || global.event;
+ e.target = e.target || e.srcElement;
+ e.preventDefault = e.preventDefault || function preventDefault() {
+ e.returnValue = false;
+ };
+ e.stopPropagation = e.stopPropagation || function stopPropagation() {
+ e.cancelBubble = true;
+ };
+ e.which = e.which || e.keyCode;
+ fn.call(el, e);
+ };
+ }
+
+ function wrap(el, type, fn) {
+ var wrapper = unwrap(el, type, fn) || wrapperFactory(el, type, fn);
+ hardCache.push({
+ wrapper: wrapper,
+ element: el,
+ type: type,
+ fn: fn
+ });
+ return wrapper;
+ }
+
+ function unwrap(el, type, fn) {
+ var i = find(el, type, fn);
+ if (i) {
+ var wrapper = hardCache[i].wrapper;
+ hardCache.splice(i, 1); // free up a tad of memory
+ return wrapper;
+ }
+ }
+
+ function find(el, type, fn) {
+ var i, item;
+ for (i = 0; i < hardCache.length; i++) {
+ item = hardCache[i];
+ if (item.element === el && item.type === type && item.fn === fn) {
+ return i;
+ }
+ }
+ }
+
+ }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+}, {
+ "./eventmap": 6,
+ "custom-event": 7
+ }],
+ 6: [function (require, module, exports) {
+ (function (global) {
+ 'use strict';
+
+ var eventmap = [];
+ var eventname = '';
+ var ron = /^on/;
+
+ for (eventname in global) {
+ if (ron.test(eventname)) {
+ eventmap.push(eventname.slice(2));
+ }
+ }
+
+ module.exports = eventmap;
+
+ }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+}, {}],
+ 7: [function (require, module, exports) {
+ (function (global) {
+
+ var NativeCustomEvent = global.CustomEvent;
+
+ function useNative() {
+ try {
+ var p = new NativeCustomEvent('cat', {
+ detail: {
+ foo: 'bar'
+ }
+ });
+ return 'cat' === p.type && 'bar' === p.detail.foo;
+ } catch (e) {}
+ return false;
+ }
+
+ /**
+ * Cross-browser `CustomEvent` constructor.
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent.CustomEvent
+ *
+ * @public
+ */
+
+ module.exports = useNative() ? NativeCustomEvent :
+
+ // IE >= 9
+ 'function' === typeof document.createEvent ? function CustomEvent(type, params) {
+ var e = document.createEvent('CustomEvent');
+ if (params) {
+ e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail);
+ } else {
+ e.initCustomEvent(type, false, false, void 0);
+ }
+ return e;
+ } :
+
+ // IE <= 8
+ function CustomEvent(type, params) {
+ var e = document.createEventObject();
+ e.type = type;
+ if (params) {
+ e.bubbles = Boolean(params.bubbles);
+ e.cancelable = Boolean(params.cancelable);
+ e.detail = params.detail;
+ } else {
+ e.bubbles = false;
+ e.cancelable = false;
+ e.detail = void 0;
+ }
+ return e;
+ }
+
+ }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+}, {}],
+ 8: [function (require, module, exports) {
+ 'use strict';
+
+ var cache = {};
+ var start = '(?:^|\\s)';
+ var end = '(?:\\s|$)';
+
+ function lookupClass(className) {
+ var cached = cache[className];
+ if (cached) {
+ cached.lastIndex = 0;
+ } else {
+ cache[className] = cached = new RegExp(start + className + end, 'g');
+ }
+ return cached;
+ }
+
+ function addClass(el, className) {
+ var current = el.className;
+ if (!current.length) {
+ el.className = className;
+ } else if (!lookupClass(className).test(current)) {
+ el.className += ' ' + className;
+ }
+ }
+
+ function rmClass(el, className) {
+ el.className = el.className.replace(lookupClass(className), ' ').trim();
+ }
+
+ module.exports = {
+ add: addClass,
+ rm: rmClass
+ };
+
+}, {}],
+ 9: [function (require, module, exports) {
+ (function (global) {
+ 'use strict';
+
+ var emitter = require('contra/emitter');
+ var crossvent = require('crossvent');
+ var classes = require('./classes');
+ var doc = document;
+ var documentElement = doc.documentElement;
+
+ function dragula(initialContainers, options) {
+ var len = arguments.length;
+ if (len === 1 && Array.isArray(initialContainers) === false) {
+ options = initialContainers;
+ initialContainers = [];
+ }
+ var _mirror; // mirror image
+ var _source; // source container
+ var _item; // item being dragged
+ var _offsetX; // reference x
+ var _offsetY; // reference y
+ var _moveX; // reference move x
+ var _moveY; // reference move y
+ var _initialSibling; // reference sibling when grabbed
+ var _currentSibling; // reference sibling now
+ var _copy; // item used for copying
+ var _renderTimer; // timer for setTimeout renderMirrorImage
+ var _lastDropTarget = null; // last container item was over
+ var _grabbed; // holds mousedown context until first mousemove
+
+ var o = options || {};
+ if (o.moves === void 0) {
+ o.moves = always;
+ }
+ if (o.accepts === void 0) {
+ o.accepts = always;
+ }
+ if (o.invalid === void 0) {
+ o.invalid = invalidTarget;
+ }
+ if (o.containers === void 0) {
+ o.containers = initialContainers || [];
+ }
+ if (o.isContainer === void 0) {
+ o.isContainer = never;
+ }
+ if (o.copy === void 0) {
+ o.copy = false;
+ }
+ if (o.copySortSource === void 0) {
+ o.copySortSource = false;
+ }
+ if (o.revertOnSpill === void 0) {
+ o.revertOnSpill = false;
+ }
+ if (o.removeOnSpill === void 0) {
+ o.removeOnSpill = false;
+ }
+ if (o.direction === void 0) {
+ o.direction = 'vertical';
+ }
+ if (o.ignoreInputTextSelection === void 0) {
+ o.ignoreInputTextSelection = true;
+ }
+ if (o.mirrorContainer === void 0) {
+ o.mirrorContainer = doc.body;
+ }
+
+ var drake = emitter({
+ containers: o.containers,
+ start: manualStart,
+ end: end,
+ cancel: cancel,
+ remove: remove,
+ destroy: destroy,
+ canMove: canMove,
+ dragging: false
+ });
+
+ if (o.removeOnSpill === true) {
+ drake.on('over', spillOver).on('out', spillOut);
+ }
+
+ events();
+
+ return drake;
+
+ function isContainer(el) {
+ return drake.containers.indexOf(el) !== -1 || o.isContainer(el);
+ }
+
+ function events(remove) {
+ var op = remove ? 'remove' : 'add';
+ touchy(documentElement, op, 'mousedown', grab);
+ touchy(documentElement, op, 'mouseup', release);
+ }
+
+ function eventualMovements(remove) {
+ var op = remove ? 'remove' : 'add';
+ touchy(documentElement, op, 'mousemove', startBecauseMouseMoved);
+ }
+
+ function movements(remove) {
+ var op = remove ? 'remove' : 'add';
+ crossvent[op](documentElement, 'selectstart', preventGrabbed); // IE8
+ crossvent[op](documentElement, 'click', preventGrabbed);
+ }
+
+ function destroy() {
+ events(true);
+ release({});
+ }
+
+ function preventGrabbed(e) {
+ if (_grabbed) {
+ e.preventDefault();
+ }
+ }
+
+ function grab(e) {
+ _moveX = e.clientX;
+ _moveY = e.clientY;
+
+ var ignore = whichMouseButton(e) !== 1 || e.metaKey || e.ctrlKey;
+ if (ignore) {
+ return; // we only care about honest-to-god left clicks and touch events
+ }
+ var item = e.target;
+ var context = canStart(item);
+ if (!context) {
+ return;
+ }
+ _grabbed = context;
+ eventualMovements();
+ if (e.type === 'mousedown') {
+ if (isInput(item)) { // see also: https://github.com/bevacqua/dragula/issues/208
+ item.focus(); // fixes https://github.com/bevacqua/dragula/issues/176
+ } else {
+ e.preventDefault(); // fixes https://github.com/bevacqua/dragula/issues/155
+ }
+ }
+ }
+
+ function startBecauseMouseMoved(e) {
+ if (!_grabbed) {
+ return;
+ }
+ if (whichMouseButton(e) === 0) {
+ release({});
+ return; // when text is selected on an input and then dragged, mouseup doesn't fire. this is our only hope
+ }
+ // truthy check fixes #239, equality fixes #207
+ if (e.clientX !== void 0 && e.clientX === _moveX && e.clientY !== void 0 && e.clientY === _moveY) {
+ return;
+ }
+ if (o.ignoreInputTextSelection) {
+ var clientX = getCoord('clientX', e);
+ var clientY = getCoord('clientY', e);
+ var elementBehindCursor = doc.elementFromPoint(clientX, clientY);
+ if (isInput(elementBehindCursor)) {
+ return;
+ }
+ }
+
+ var grabbed = _grabbed; // call to end() unsets _grabbed
+ eventualMovements(true);
+ movements();
+ end();
+ start(grabbed);
+
+ var offset = getOffset(_item);
+ _offsetX = getCoord('pageX', e) - offset.left;
+ _offsetY = getCoord('pageY', e) - offset.top;
+
+ classes.add(_copy || _item, 'gu-transit');
+ renderMirrorImage();
+ drag(e);
+ }
+
+ function canStart(item) {
+ if (drake.dragging && _mirror) {
+ return;
+ }
+ if (isContainer(item)) {
+ return; // don't drag container itself
+ }
+ var handle = item;
+ while (getParent(item) && isContainer(getParent(item)) === false) {
+ if (o.invalid(item, handle)) {
+ return;
+ }
+ item = getParent(item); // drag target should be a top element
+ if (!item) {
+ return;
+ }
+ }
+ var source = getParent(item);
+ if (!source) {
+ return;
+ }
+ if (o.invalid(item, handle)) {
+ return;
+ }
+
+ var movable = o.moves(item, source, handle, nextEl(item));
+ if (!movable) {
+ return;
+ }
+
+ return {
+ item: item,
+ source: source
+ };
+ }
+
+ function canMove(item) {
+ return !!canStart(item);
+ }
+
+ function manualStart(item) {
+ var context = canStart(item);
+ if (context) {
+ start(context);
+ }
+ }
+
+ function start(context) {
+ if (isCopy(context.item, context.source)) {
+ _copy = context.item.cloneNode(true);
+ drake.emit('cloned', _copy, context.item, 'copy');
+ }
+
+ _source = context.source;
+ _item = context.item;
+ _initialSibling = _currentSibling = nextEl(context.item);
+
+ drake.dragging = true;
+ drake.emit('drag', _item, _source);
+ }
+
+ function invalidTarget() {
+ return false;
+ }
+
+ function end() {
+ if (!drake.dragging) {
+ return;
+ }
+ var item = _copy || _item;
+ drop(item, getParent(item));
+ }
+
+ function ungrab() {
+ _grabbed = false;
+ eventualMovements(true);
+ movements(true);
+ }
+
+ function release(e) {
+ ungrab();
+
+ if (!drake.dragging) {
+ return;
+ }
+ var item = _copy || _item;
+ var clientX = getCoord('clientX', e);
+ var clientY = getCoord('clientY', e);
+ var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
+ var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
+ if (dropTarget && ((_copy && o.copySortSource) || (!_copy || dropTarget !== _source))) {
+ drop(item, dropTarget);
+ } else if (o.removeOnSpill) {
+ remove();
+ } else {
+ cancel();
+ }
+ }
+
+ function drop(item, target) {
+ var parent = getParent(item);
+ if (_copy && o.copySortSource && target === _source) {
+ parent.removeChild(_item);
+ }
+ if (isInitialPlacement(target)) {
+ drake.emit('cancel', item, _source, _source);
+ } else {
+ drake.emit('drop', item, target, _source, _currentSibling);
+ }
+ cleanup();
+ }
+
+ function remove() {
+ if (!drake.dragging) {
+ return;
+ }
+ var item = _copy || _item;
+ var parent = getParent(item);
+ if (parent) {
+ parent.removeChild(item);
+ }
+ drake.emit(_copy ? 'cancel' : 'remove', item, parent, _source);
+ cleanup();
+ }
+
+ function cancel(revert) {
+ if (!drake.dragging) {
+ return;
+ }
+ var reverts = arguments.length > 0 ? revert : o.revertOnSpill;
+ var item = _copy || _item;
+ var parent = getParent(item);
+ var initial = isInitialPlacement(parent);
+ if (initial === false && reverts) {
+ if (_copy) {
+ if (parent) {
+ parent.removeChild(_copy);
+ }
+ } else {
+ _source.insertBefore(item, _initialSibling);
+ }
+ }
+ if (initial || reverts) {
+ drake.emit('cancel', item, _source, _source);
+ } else {
+ drake.emit('drop', item, parent, _source, _currentSibling);
+ }
+ cleanup();
+ }
+
+ function cleanup() {
+ var item = _copy || _item;
+ ungrab();
+ removeMirrorImage();
+ if (item) {
+ classes.rm(item, 'gu-transit');
+ }
+ if (_renderTimer) {
+ clearTimeout(_renderTimer);
+ }
+ drake.dragging = false;
+ if (_lastDropTarget) {
+ drake.emit('out', item, _lastDropTarget, _source);
+ }
+ drake.emit('dragend', item);
+ _source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null;
+ }
+
+ function isInitialPlacement(target, s) {
+ var sibling;
+ if (s !== void 0) {
+ sibling = s;
+ } else if (_mirror) {
+ sibling = _currentSibling;
+ } else {
+ sibling = nextEl(_copy || _item);
+ }
+ return target === _source && sibling === _initialSibling;
+ }
+
+ function findDropTarget(elementBehindCursor, clientX, clientY) {
+ var target = elementBehindCursor;
+ while (target && !accepted()) {
+ target = getParent(target);
+ }
+ return target;
+
+ function accepted() {
+ var droppable = isContainer(target);
+ if (droppable === false) {
+ return false;
+ }
+
+ var immediate = getImmediateChild(target, elementBehindCursor);
+ var reference = getReference(target, immediate, clientX, clientY);
+ var initial = isInitialPlacement(target, reference);
+ if (initial) {
+ return true; // should always be able to drop it right back where it was
+ }
+ return o.accepts(_item, target, _source, reference);
+ }
+ }
+
+ function drag(e) {
+ if (!_mirror) {
+ return;
+ }
+ e.preventDefault();
+
+ var clientX = getCoord('clientX', e);
+ var clientY = getCoord('clientY', e);
+ var x = clientX - _offsetX;
+ var y = clientY - _offsetY;
+
+ _mirror.style.left = x + 'px';
+ _mirror.style.top = y + 'px';
+
+ var item = _copy || _item;
+ var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
+ var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
+ var changed = dropTarget !== null && dropTarget !== _lastDropTarget;
+ if (changed || dropTarget === null) {
+ out();
+ _lastDropTarget = dropTarget;
+ over();
+ }
+ var parent = getParent(item);
+ if (dropTarget === _source && _copy && !o.copySortSource) {
+ if (parent) {
+ parent.removeChild(item);
+ }
+ return;
+ }
+ var reference;
+ var immediate = getImmediateChild(dropTarget, elementBehindCursor);
+ if (immediate !== null) {
+ reference = getReference(dropTarget, immediate, clientX, clientY);
+ } else if (o.revertOnSpill === true && !_copy) {
+ reference = _initialSibling;
+ dropTarget = _source;
+ } else {
+ if (_copy && parent) {
+ parent.removeChild(item);
+ }
+ return;
+ }
+ if (
+ (reference === null && changed) ||
+ reference !== item &&
+ reference !== nextEl(item)
+ ) {
+ _currentSibling = reference;
+ dropTarget.insertBefore(item, reference);
+ drake.emit('shadow', item, dropTarget, _source);
+ }
+
+ function moved(type) {
+ drake.emit(type, item, _lastDropTarget, _source);
+ }
+
+ function over() {
+ if (changed) {
+ moved('over');
+ }
+ }
+
+ function out() {
+ if (_lastDropTarget) {
+ moved('out');
+ }
+ }
+ }
+
+ function spillOver(el) {
+ classes.rm(el, 'gu-hide');
+ }
+
+ function spillOut(el) {
+ if (drake.dragging) {
+ classes.add(el, 'gu-hide');
+ }
+ }
+
+ function renderMirrorImage() {
+ if (_mirror) {
+ return;
+ }
+ var rect = _item.getBoundingClientRect();
+ _mirror = _item.cloneNode(true);
+ _mirror.style.width = getRectWidth(rect) + 'px';
+ _mirror.style.height = getRectHeight(rect) + 'px';
+ classes.rm(_mirror, 'gu-transit');
+ classes.add(_mirror, 'gu-mirror');
+ o.mirrorContainer.appendChild(_mirror);
+ touchy(documentElement, 'add', 'mousemove', drag);
+ classes.add(o.mirrorContainer, 'gu-unselectable');
+ drake.emit('cloned', _mirror, _item, 'mirror');
+ }
+
+ function removeMirrorImage() {
+ if (_mirror) {
+ classes.rm(o.mirrorContainer, 'gu-unselectable');
+ touchy(documentElement, 'remove', 'mousemove', drag);
+ getParent(_mirror).removeChild(_mirror);
+ _mirror = null;
+ }
+ }
+
+ function getImmediateChild(dropTarget, target) {
+ var immediate = target;
+ while (immediate !== dropTarget && getParent(immediate) !== dropTarget) {
+ immediate = getParent(immediate);
+ }
+ if (immediate === documentElement) {
+ return null;
+ }
+ return immediate;
+ }
+
+ function getReference(dropTarget, target, x, y) {
+ var horizontal = o.direction === 'horizontal';
+ var reference = target !== dropTarget ? inside() : outside();
+ return reference;
+
+ function outside() { // slower, but able to figure out any position
+ var len = dropTarget.children.length;
+ var i;
+ var el;
+ var rect;
+ for (i = 0; i < len; i++) {
+ el = dropTarget.children[i];
+ rect = el.getBoundingClientRect();
+ if (horizontal && (rect.left + rect.width / 2) > x) {
+ return el;
+ }
+ if (!horizontal && (rect.top + rect.height / 2) > y) {
+ return el;
+ }
+ }
+ return null;
+ }
+
+ function inside() { // faster, but only available if dropped inside a child element
+ var rect = target.getBoundingClientRect();
+ if (horizontal) {
+ return resolve(x > rect.left + getRectWidth(rect) / 2);
+ }
+ return resolve(y > rect.top + getRectHeight(rect) / 2);
+ }
+
+ function resolve(after) {
+ return after ? nextEl(target) : target;
+ }
+ }
+
+ function isCopy(item, container) {
+ return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container);
+ }
+ }
+
+ function touchy(el, op, type, fn) {
+ var touch = {
+ mouseup: 'touchend',
+ mousedown: 'touchstart',
+ mousemove: 'touchmove'
+ };
+ var pointers = {
+ mouseup: 'pointerup',
+ mousedown: 'pointerdown',
+ mousemove: 'pointermove'
+ };
+ var microsoft = {
+ mouseup: 'MSPointerUp',
+ mousedown: 'MSPointerDown',
+ mousemove: 'MSPointerMove'
+ };
+ if (global.navigator.pointerEnabled) {
+ crossvent[op](el, pointers[type], fn);
+ } else if (global.navigator.msPointerEnabled) {
+ crossvent[op](el, microsoft[type], fn);
+ } else {
+ crossvent[op](el, touch[type], fn);
+ crossvent[op](el, type, fn);
+ }
+ }
+
+ function whichMouseButton(e) {
+ if (e.touches !== void 0) {
+ return e.touches.length;
+ }
+ if (e.which !== void 0 && e.which !== 0) {
+ return e.which;
+ } // see https://github.com/bevacqua/dragula/issues/261
+ if (e.buttons !== void 0) {
+ return e.buttons;
+ }
+ var button = e.button;
+ if (button !== void 0) { // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575
+ return button & 1 ? 1 : button & 2 ? 3 : (button & 4 ? 2 : 0);
+ }
+ }
+
+ function getOffset(el) {
+ var rect = el.getBoundingClientRect();
+ return {
+ left: rect.left + getScroll('scrollLeft', 'pageXOffset'),
+ top: rect.top + getScroll('scrollTop', 'pageYOffset')
+ };
+ }
+
+ function getScroll(scrollProp, offsetProp) {
+ if (typeof global[offsetProp] !== 'undefined') {
+ return global[offsetProp];
+ }
+ if (documentElement.clientHeight) {
+ return documentElement[scrollProp];
+ }
+ return doc.body[scrollProp];
+ }
+
+ function getElementBehindPoint(point, x, y) {
+ var p = point || {};
+ var state = p.className;
+ var el;
+ p.className += ' gu-hide';
+ el = doc.elementFromPoint(x, y);
+ p.className = state;
+ return el;
+ }
+
+ function never() {
+ return false;
+ }
+
+ function always() {
+ return true;
+ }
+
+ function getRectWidth(rect) {
+ return rect.width || (rect.right - rect.left);
+ }
+
+ function getRectHeight(rect) {
+ return rect.height || (rect.bottom - rect.top);
+ }
+
+ function getParent(el) {
+ return el.parentNode === doc ? null : el.parentNode;
+ }
+
+ function isInput(el) {
+ return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el);
+ }
+
+ function isEditable(el) {
+ if (!el) {
+ return false;
+ } // no parents were editable
+ if (el.contentEditable === 'false') {
+ return false;
+ } // stop the lookup
+ if (el.contentEditable === 'true') {
+ return true;
+ } // found a contentEditable element in the chain
+ return isEditable(getParent(el)); // contentEditable is set to 'inherit'
+ }
+
+ function nextEl(el) {
+ return el.nextElementSibling || manually();
+
+ function manually() {
+ var sibling = el;
+ do {
+ sibling = sibling.nextSibling;
+ } while (sibling && sibling.nodeType !== 1);
+ return sibling;
+ }
+ }
+
+ function getEventHost(e) {
+ // on touchend event, we have to use `e.changedTouches`
+ // see http://stackoverflow.com/questions/7192563/touchend-event-properties
+ // see https://github.com/bevacqua/dragula/issues/34
+ if (e.targetTouches && e.targetTouches.length) {
+ return e.targetTouches[0];
+ }
+ if (e.changedTouches && e.changedTouches.length) {
+ return e.changedTouches[0];
+ }
+ return e;
+ }
+
+ function getCoord(coord, e) {
+ var host = getEventHost(e);
+ var missMap = {
+ pageX: 'clientX', // IE8
+ pageY: 'clientY' // IE8
+ };
+ if (coord in missMap && !(coord in host) && missMap[coord] in host) {
+ coord = missMap[coord];
+ }
+ return host[coord];
+ }
+
+ module.exports = dragula;
+
+ }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+}, {
+ "./classes": 8,
+ "contra/emitter": 4,
+ "crossvent": 5
+ }],
+ 10: [function (require, module, exports) {
+ var si = typeof setImmediate === 'function',
+ tick;
+ if (si) {
+ tick = function (fn) {
+ setImmediate(fn);
+ };
+ } else {
+ tick = function (fn) {
+ setTimeout(fn, 0);
+ };
+ }
+
+ module.exports = tick;
+}, {}]
+}, {}, [1]);
diff --git a/www/mediatag/index.html b/www/mediatag/index.html
new file mode 100644
index 000000000..45e88aba5
--- /dev/null
+++ b/www/mediatag/index.html
@@ -0,0 +1,22 @@
+
+
+ Test media-tag
+
+
+
+
+ Media-tag:
+
+
+
+
diff --git a/www/mediatag/main.js b/www/mediatag/main.js
new file mode 100644
index 000000000..d2f1691ae
--- /dev/null
+++ b/www/mediatag/main.js
@@ -0,0 +1,58 @@
+require([
+ 'jquery',
+ '/mediatag/media-tag.js',
+ '/bower_components/tweetnacl/nacl-fast.min.js'
+], function ($, MediaTag) {
+ console.log(MediaTag);
+ console.log($('media-tag'));
+ if (typeof MediaTag === "function") {
+ MediaTag.PdfPlugin.viewer = '/common/pdfjs/web/viewer.html';
+
+ var config = {
+ allowed: ['download'],
+ download: {
+ text: 'Download'
+ }
+ };
+ MediaTag($('media-tag'), config)
+ .on('progress', function (data) {
+ console.log(data.progress);
+ })
+ .on('complete', function (data) {
+ console.log(data);
+ })
+ .on('error', function (data) {
+ console.error(data);
+ });
+ MediaTag($('media-tag')[1])
+ .on('progress', function (data) {
+ console.log(data.progress);
+ })
+ .on('complete', function (data) {
+ console.log(data);
+ })
+ .on('error', function (data) {
+ console.error(data);
+ });
+ MediaTag($('media-tag')[2])
+ .on('progress', function (data) {
+ console.log(data.progress);
+ })
+ .on('complete', function (data) {
+ console.log(data);
+ })
+ .on('error', function (data) {
+ console.error(data);
+ });
+ MediaTag($('media-tag')[3])
+ .on('progress', function (data) {
+ console.log(data.progress);
+ })
+ .on('complete', function (data) {
+ console.log(data);
+ })
+ .on('error', function (data) {
+ console.error(data);
+ });
+ }
+});
diff --git a/www/mediatag/media-tag.js b/www/mediatag/media-tag.js
new file mode 100644
index 000000000..f240e0941
--- /dev/null
+++ b/www/mediatag/media-tag.js
@@ -0,0 +1,442 @@
+(function(name, definition) {
+ if (typeof module !== 'undefined') { module.exports = definition(); }
+ else if (typeof define === 'function' && typeof define.amd === 'object') { define(definition); }
+ else { this[name] = definition(); }
+}('MediaTag', function() {
+ var cache;
+ var PARANOIA = true;
+ var cypherChunkLength = 131088;
+
+ // Save a blob on the file system
+ var saveFile = function (blob, url, fileName) {
+ if (window.navigator && window.navigator.msSaveOrOpenBlob) {
+ window.navigator.msSaveOrOpenBlob(blob, fileName);
+ } else {
+ // We want to be able to download the file with a name, so we need an "a" tag with
+ // a download attribute
+ var a = document.createElement("a");
+ a.href = url;
+ a.download = fileName;
+ // It's not in the DOM, so we can't use a.click();
+ var event = new MouseEvent("click");
+ a.dispatchEvent(event);
+ }
+ };
+
+ // Default config, can be overriden per media-tag call
+ var config = {
+ allowed: [
+ 'image/png',
+ 'image/jpeg',
+ 'image/jpg',
+ 'image/gif',
+ 'audio/mp3',
+ 'audio/ogg',
+ 'audio/wav',
+ 'audio/webm',
+ 'video/mp4',
+ 'video/ogg',
+ 'video/webm',
+ 'application/pdf',
+ //'application/dash+xml', // FIXME?
+ 'download'
+ ],
+ pdf: {},
+ download: {
+ text: "Download"
+ },
+ Plugins: {
+ image: function (metadata, url, content, cfg, cb) {
+ var img = document.createElement('img');
+ img.setAttribute('src', url);
+ cb(void 0, img);
+ },
+ video: function (metadata, url, content, cfg, cb) {
+ var video = document.createElement('video');
+ video.setAttribute('src', url);
+ video.setAttribute('controls', true);
+ cb(void 0, video);
+ },
+ audio: function (metadata, url, content, cfg, cb) {
+ var audio = document.createElement('audio');
+ audio.setAttribute('src', url);
+ audio.setAttribute('controls', true);
+ cb(void 0, audio);
+ },
+ pdf: function (metadata, url, content, cfg, cb) {
+ var iframe = document.createElement('iframe');
+ if (cfg.pdf.viewer) { // PDFJS
+ var viewerUrl = cfg.pdf.viewer + '?file=' + url;
+ iframe.src = viewerUrl + '#' + window.encodeURIComponent(metadata.name);
+ return void cb (void 0, iframe);
+ }
+ iframe.src = url + '#' + window.encodeURIComponent(metadata.name);
+ return void cb (void 0, iframe);
+ },
+ download: function (metadata, url, content, cfg, cb) {
+ var btn = document.createElement('button');
+ btn.innerHTML = cfg.download.text;
+ btn.addEventListener('click', function () {
+ saveFile(content, url, metadata.name);
+ });
+ cb(void 0, btn);
+ }
+ }
+ };
+
+
+ // Download a blob from href
+ var download = function (src, cb) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', src, true);
+ xhr.responseType = 'arraybuffer';
+
+ xhr.onload = function () {
+ // Error?
+ if (/^4/.test('' + this.status)) { return void cb("XHR_ERROR " + this.status); }
+
+ var arrayBuffer = xhr.response;
+ if (arrayBuffer) { cb(null, new Uint8Array(arrayBuffer)); }
+ };
+
+ xhr.send(null);
+ };
+
+ // Decryption tools
+ var Decrypt = {
+ // Create a nonce
+ createNonce: function () {
+ if (!Array.prototype.fill) {
+ // IE support
+ var arr = [];
+ for (var i = 0; i < 24; i++) { arr[i] = 0; }
+ return new Uint8Array(arr);
+ }
+ return new Uint8Array(new Array(24).fill(0));
+ },
+
+ // Increment a nonce
+ // FIXME: remove throw?
+ increment: function (N) {
+ var l = N.length;
+ while (l-- > 1) {
+ if (PARANOIA) {
+ if (typeof(N[l]) !== 'number') {
+ throw new Error('E_UNSAFE_TYPE');
+ }
+ if (N[l] > 255) {
+ throw new Error('E_OUT_OF_BOUNDS');
+ }
+ }
+ /* .jshint probably suspects this is unsafe because we lack types
+ but as long as this is only used on nonces, it should be safe */
+ if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
+ N[l] = 0;
+
+ // you don't need to worry about this running out.
+ // you'd need a REAAAALLY big file
+ if (l === 0) {
+ throw new Error('E_NONCE_TOO_LARGE');
+ }
+ }
+ },
+
+ decodePrefix: function (A) {
+ return (A[0] << 8) | A[1];
+ },
+ joinChunks: function (chunks) {
+ return new Blob(chunks);
+ },
+
+ // Convert a Uint8Array into Array.
+ slice: function (u8) {
+ return Array.prototype.slice.call(u8);
+ },
+
+ // Gets the random key string.
+ getRandomKeyStr: function () {
+ var Nacl = window.nacl;
+ var rdm = Nacl.randomBytes(18);
+ return Nacl.util.encodeBase64(rdm);
+ },
+
+ // Gets the key from the key string.
+ getKeyFromStr: function (str) {
+ return window.nacl.util.decodeBase64(str);
+ }
+ };
+
+ // Decrypts a Uint8Array with the given key.
+ var decrypt = function (u8, strKey, done, progressCb) {
+ var Nacl = window.nacl;
+
+ var progress = function (offset) {
+ progressCb((offset / u8.length) * 100);
+ };
+
+ var key = Decrypt.getKeyFromStr(strKey);
+ var nonce = Decrypt.createNonce();
+ var i = 0;
+ var prefix = u8.subarray(0, 2);
+ var metadataLength = Decrypt.decodePrefix(prefix);
+
+ var res = { metadata: undefined };
+
+ // Get metadata
+ var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
+ var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
+
+ Decrypt.increment(nonce);
+
+ try { res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk)); }
+ catch (e) { return void done('E_METADATA_DECRYPTION'); }
+
+ if (!res.metadata) { return void done('NO_METADATA'); }
+
+ var takeChunk = function (cb) {
+ setTimeout(function () {
+ var start = i * cypherChunkLength + 2 + metadataLength;
+ var end = start + cypherChunkLength;
+ i++;
+
+ // Get the chunk
+ var box = new Uint8Array(u8.subarray(start, end));
+
+ // Decrypt the chunk
+ var plaintext = Nacl.secretbox.open(box, nonce, key);
+ Decrypt.increment(nonce);
+
+ if (!plaintext) { return void cb('DECRYPTION_FAILURE'); }
+
+ progress(Math.min(end, u8.length));
+
+ cb(void 0, plaintext);
+ });
+ };
+
+ var chunks = [];
+
+ // decrypt file contents
+ var again = function () {
+ takeChunk(function (e, plaintext) {
+ if (e) { return setTimeout(function () { done(e); }); }
+
+ if (plaintext) {
+ if (i * cypherChunkLength < u8.length) { // not done
+ chunks.push(plaintext);
+ return again();
+ }
+
+ chunks.push(plaintext);
+ res.content = Decrypt.joinChunks(chunks);
+ return void done(void 0, res);
+ }
+ done('UNEXPECTED_ENDING');
+ });
+ };
+ again();
+ };
+
+ // Get type
+ var getType = function (mediaObject, metadata, cfg) {
+ var mime = metadata.type;
+ var s = metadata.type.split('/');
+ var type = s[0];
+ var extension = s[1];
+
+ mediaObject.name = metadata.name;
+ if (mime && cfg.allowed.indexOf(mime) !== -1) {
+ mediaObject.type = type;
+ mediaObject.extension = extension;
+ mediaObject.mime = mime;
+ return type;
+ } else if (cfg.allowed.indexOf('download') !== -1) {
+ mediaObject.type = type;
+ mediaObject.extension = extension;
+ mediaObject.mime = mime;
+ return 'download';
+ } else {
+ return;
+ }
+ };
+
+ // Copy attributes
+ var copyAttributes = function (origin, dest) {
+ Object.keys(origin.attributes).forEach(function (i) {
+ if (!/^data-attr/.test(origin.attributes[i].name)) { return; }
+ var name = origin.attributes[i].name.slice(10);
+ var value = origin.attributes[i].value;
+ dest.setAttribute(name, value);
+ });
+ };
+
+ // Process
+ var process = function (mediaObject, decrypted, cfg, cb) {
+ var metadata = decrypted.metadata;
+ var blob = decrypted.content;
+
+ var mediaType = getType(mediaObject, metadata, cfg);
+
+ if (mediaType === 'application') {
+ mediaType = mediaObject.extension;
+ }
+
+ if (!mediaType || !cfg.Plugins[mediaType]) {
+ return void cb('NO_PLUGIN_FOUND');
+ }
+
+ // Get blob URL
+ var url = decrypted.url;
+ if (!url) {
+ url = decrypted.url = window.URL.createObjectURL(new Blob([blob], {
+ type: metadata.type
+ }));
+ }
+
+ cfg.Plugins[mediaType](metadata, url, blob, cfg, function (err, el) {
+ if (err || !el) { return void cb(err || 'ERR_MEDIATAG_DISPLAY'); }
+ copyAttributes(mediaObject.tag, el);
+ mediaObject.tag.innerHTML = '';
+ mediaObject.tag.appendChild(el);
+ cb();
+ });
+ };
+
+ var addMissingConfig = function (base, target) {
+ Object.keys(target).forEach(function (k) {
+ if (!target[k]) { return; }
+ // Target is an object, fix it recursively
+ if (typeof target[k] === "object" && !Array.isArray(target[k])) {
+ // Sub-object
+ if (base[k] && (typeof base[k] !== "object" || Array.isArray(base[k]))) { return; }
+ else if (base[k]) { addMissingConfig(base[k], target[k]); }
+ else {
+ base[k] = {};
+ addMissingConfig(base[k], target[k]);
+ }
+ }
+ // Target is array or immutable, copy the value if it's missing
+ if (!base[k]) {
+ base[k] = Array.isArray(target[k]) ? JSON.parse(JSON.stringify(target[k]))
+ : target[k];
+ }
+ });
+ };
+
+ // Initialize a media-tag
+ var init = function (el, cfg) {
+ cfg = cfg || {};
+
+ addMissingConfig(cfg, config);
+
+ // Add support for old mediatag library
+ if (!cfg.pdf.viewer && init.PdfPlugin && init.PdfPlugin.viewer) {
+ cfg.pdf.viewer = init.PdfPlugin.viewer;
+ }
+
+ // Handle jQuery elements
+ if (typeof(el) === "object" && el.jQuery) { el = el[0]; }
+
+ // Abort smoothly if the element is not a media-tag
+ if (el.nodeName !== "MEDIA-TAG") {
+ console.error("Not a media-tag!");
+ return {
+ on: function () { return this; }
+ };
+ }
+
+ var handlers = cfg.handlers || {
+ 'progress': [],
+ 'complete': [],
+ 'error': []
+ };
+
+ var mediaObject = el._mediaObject = {
+ handlers: handlers,
+ tag: el
+ };
+
+ var emit = function (ev, data) {
+ // Check if the event name is valid
+ if (Object.keys(handlers).indexOf(ev) === -1) {
+ return void console.error("Invalid mediatag event");
+ }
+
+ // Call the handlers
+ handlers[ev].forEach(function (h) {
+ // Make sure a bad handler won't break the media-tag script
+ try {
+ h(data);
+ } catch (err) {
+ console.error(err);
+ }
+ });
+ };
+
+ mediaObject.on = function (ev, handler) {
+ // Check if the event name is valid
+ if (Object.keys(handlers).indexOf(ev) === -1) {
+ console.error("Invalid mediatag event");
+ return mediaObject;
+ }
+ // Check if the handler is valid
+ if (typeof (handler) !== "function") {
+ console.error("Handler is not a function!");
+ return mediaObject;
+ }
+ // Add the handler
+ handlers[ev].push(handler);
+ return mediaObject;
+ };
+
+ var src = el.getAttribute('src');
+ var strKey = el.getAttribute('data-crypto-key');
+ if (/^cryptpad:/.test(strKey)) {
+ strKey = strKey.slice(9);
+ }
+ var uid = [src, strKey].join('');
+
+ // End media-tag rendering: display the tag and emit the event
+ var end = function (decrypted) {
+ process(mediaObject, decrypted, cfg, function (err) {
+ if (err) { return void emit('error', err); }
+ emit('complete', decrypted);
+ });
+ };
+
+ // If we have the blob in our cache, don't download & decrypt it again, just display
+ if (cache[uid]) {
+ end(cache[uid]);
+ return mediaObject;
+ }
+
+ // Download the encrypted blob
+ download(src, function (err, u8Encrypted) {
+ if (err) {
+ return void emit('error', err);
+ }
+ // Decrypt the blob
+ decrypt(u8Encrypted, strKey, function (errDecryption, u8Decrypted) {
+ if (errDecryption) {
+ return void emit('error', errDecryption);
+ }
+ // Cache and display the decrypted blob
+ cache[uid] = u8Decrypted;
+ end(u8Decrypted);
+ }, function (progress) {
+ emit('progress', {
+ progress: progress
+ });
+ });
+ });
+
+ return mediaObject;
+ };
+
+ // Add the cache as a property of MediaTag
+ cache = init.__Cryptpad_Cache = {};
+
+ init.PdfPlugin = {};
+
+ return init;
+}));
diff --git a/www/poll/app-poll.less b/www/poll/app-poll.less
index 1b56b99bd..0c594395b 100644
--- a/www/poll/app-poll.less
+++ b/www/poll/app-poll.less
@@ -18,6 +18,7 @@
@poll-th-user-bg: darken(@poll-th-bg, 10%);
@poll-editing: lighten(@poll-th-bg, 10%);
@poll-winner: darken(@poll-th-bg, 15%);
+@poll-highlighted: lighten(@poll-th-bg, 15%);
@poll-td-bg: @poll-th-bg;
@poll-td-fg: @poll-th-fg;
@@ -514,6 +515,13 @@ div.cp-app-poll-realtime {
}
}
}
+ tr:not(:last-child) {
+ &:hover {
+ td:first-child {
+ background-color: @poll-highlighted;
+ }
+ }
+ }
}
.cp-app-poll-table-edit {
//color: @poll-cover-color;