summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDzial Techniczny WMW Projekt s.c <techniczna@wmwprojekt.pl>2019-12-10 11:34:53 +0100
committerDzial Techniczny WMW Projekt s.c <techniczna@wmwprojekt.pl>2019-12-10 11:34:53 +0100
commitb8fa0246803dab40cf57d40b45984c53046f2d55 (patch)
treedc92b167c7542137c385614a1d558e57669a4339
parent2a43146236fd8fb16f84398d85720ad84aa0a0b1 (diff)
Plugins directory and local modifications
-rw-r--r--plugins/.gitignore2
-rw-r--r--plugins/Bigboard/Asset/js/BoardDragAndDrop.js124
-rw-r--r--plugins/Bigboard/Asset/js/BoardPolling.js43
-rw-r--r--plugins/Bigboard/Controller/Bigboard.php103
-rw-r--r--plugins/Bigboard/Controller/BoardAjaxController.php145
-rw-r--r--plugins/Bigboard/Plugin.php70
-rw-r--r--plugins/Bigboard/README.md10
-rw-r--r--plugins/Bigboard/Template/Bigboard.php15
-rw-r--r--plugins/Bigboard/Template/board/dropdown.php32
-rw-r--r--plugins/Bigboard/Template/board/show.php5
-rw-r--r--plugins/Bigboard/Template/board/table_container.php58
-rw-r--r--plugins/Bigboard/Template/board/table_tasks.php38
-rw-r--r--plugins/Bigboard/Template/board/task_private.php66
-rw-r--r--plugins/Bigboard/Template/board/view.php20
-rw-r--r--plugins/Bigboard/UserSession.php29
-rw-r--r--plugins/Boardcustomizer/Controller/SettingsController.php62
-rw-r--r--plugins/Boardcustomizer/Locale/de_DE/translations.php21
-rw-r--r--plugins/Boardcustomizer/Makefile6
-rw-r--r--plugins/Boardcustomizer/Plugin.php44
-rw-r--r--plugins/Boardcustomizer/README.md51
-rw-r--r--plugins/Boardcustomizer/Template/layout/head.php173
-rw-r--r--plugins/Boardcustomizer/Template/project/dropdown.php3
-rw-r--r--plugins/Boardcustomizer/Template/user/settings.php29
-rw-r--r--plugins/Boardcustomizer/Template/user/sidebar.php3
-rw-r--r--plugins/Calendar/.github/issue_template.md21
-rw-r--r--plugins/Calendar/.gitignore1
-rw-r--r--plugins/Calendar/.travis.yml34
-rw-r--r--plugins/Calendar/Assets/calendar.js81
-rw-r--r--plugins/Calendar/Assets/fullcalendar.min.css5
-rw-r--r--plugins/Calendar/Assets/fullcalendar.min.js10
-rw-r--r--plugins/Calendar/Assets/locale-all.js5
-rw-r--r--plugins/Calendar/Assets/moment.min.js551
-rw-r--r--plugins/Calendar/Controller/CalendarController.php137
-rw-r--r--plugins/Calendar/Controller/ConfigController.php31
-rw-r--r--plugins/Calendar/Formatter/ProjectApiFormatter.php22
-rw-r--r--plugins/Calendar/Formatter/TaskCalendarFormatter.php87
-rw-r--r--plugins/Calendar/Helper/CalendarHelper.php31
-rw-r--r--plugins/Calendar/LICENSE21
-rw-r--r--plugins/Calendar/Locale/bs_BA/translations.php13
-rw-r--r--plugins/Calendar/Locale/cs_CZ/translations.php13
-rw-r--r--plugins/Calendar/Locale/da_DK/translations.php13
-rw-r--r--plugins/Calendar/Locale/de_DE/translations.php13
-rw-r--r--plugins/Calendar/Locale/el_GR/translations.php13
-rw-r--r--plugins/Calendar/Locale/es_ES/translations.php13
-rw-r--r--plugins/Calendar/Locale/fi_FI/translations.php13
-rw-r--r--plugins/Calendar/Locale/fr_FR/translations.php14
-rw-r--r--plugins/Calendar/Locale/hr_HR/translations.php13
-rw-r--r--plugins/Calendar/Locale/hu_HU/translations.php13
-rw-r--r--plugins/Calendar/Locale/id_ID/translations.php13
-rw-r--r--plugins/Calendar/Locale/it_IT/translations.php13
-rw-r--r--plugins/Calendar/Locale/ja_JP/translations.php15
-rw-r--r--plugins/Calendar/Locale/ko_KR/translations.php13
-rw-r--r--plugins/Calendar/Locale/my_MY/translations.php13
-rw-r--r--plugins/Calendar/Locale/nb_NO/translations.php13
-rw-r--r--plugins/Calendar/Locale/nl_NL/translations.php13
-rw-r--r--plugins/Calendar/Locale/pl_PL/translations.php13
-rw-r--r--plugins/Calendar/Locale/pt_BR/translations.php13
-rw-r--r--plugins/Calendar/Locale/pt_PT/translations.php13
-rw-r--r--plugins/Calendar/Locale/ru_RU/translations.php13
-rw-r--r--plugins/Calendar/Locale/sr_Latn_RS/translations.php13
-rw-r--r--plugins/Calendar/Locale/sv_SE/translations.php13
-rw-r--r--plugins/Calendar/Locale/th_TH/translations.php13
-rw-r--r--plugins/Calendar/Locale/tr_TR/translations.php13
-rw-r--r--plugins/Calendar/Locale/zh_CN/translations.php13
-rw-r--r--plugins/Calendar/Makefile5
-rw-r--r--plugins/Calendar/Plugin.php71
-rw-r--r--plugins/Calendar/README.md27
-rw-r--r--plugins/Calendar/Template/calendar/project.php6
-rw-r--r--plugins/Calendar/Template/calendar/user.php4
-rw-r--r--plugins/Calendar/Template/config/calendar.php31
-rw-r--r--plugins/Calendar/Template/config/sidebar.php3
-rw-r--r--plugins/Calendar/Template/dashboard/menu.php3
-rw-r--r--plugins/Calendar/Template/project/dropdown.php3
-rw-r--r--plugins/Calendar/Template/project_header/views.php3
-rw-r--r--plugins/Calendar/Test/PluginTest.php20
-rw-r--r--plugins/Customizer/Assets/css/README.md39
-rw-r--r--plugins/Customizer/Assets/css/customizer.css316
-rw-r--r--plugins/Customizer/Assets/css/theme.css1
-rw-r--r--plugins/Customizer/Assets/css/themes/Blueboard.css265
-rw-r--r--plugins/Customizer/Assets/css/themes/Breathe.css327
-rw-r--r--plugins/Customizer/Assets/css/themes/Clemson.css341
-rw-r--r--plugins/Customizer/Assets/css/themes/Galaxy.css2713
-rw-r--r--plugins/Customizer/Assets/css/themes/Github.css3260
-rw-r--r--plugins/Customizer/Assets/css/themes/KindaDark.css685
-rw-r--r--plugins/Customizer/Assets/css/themes/Material.css1427
-rw-r--r--plugins/Customizer/Assets/css/userthemes/niebieski.css1
-rw-r--r--plugins/Customizer/Assets/img/logo-gen.pngbin0 -> 7850 bytes
-rw-r--r--plugins/Customizer/Assets/js/customizer.js248
-rw-r--r--plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.css167
-rw-r--r--plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.js492
-rw-r--r--plugins/Customizer/Controller/CustomizerConfigController.php388
-rw-r--r--plugins/Customizer/Controller/CustomizerFileController.php313
-rw-r--r--plugins/Customizer/Helper/DynamicAvatar.php59
-rw-r--r--plugins/Customizer/Helper/ThemeHelper.php64
-rw-r--r--plugins/Customizer/LICENSE21
-rw-r--r--plugins/Customizer/Locale/it_IT/translations.php49
-rw-r--r--plugins/Customizer/Model/CustomizerFileModel.php320
-rw-r--r--plugins/Customizer/Plugin.php173
-rw-r--r--plugins/Customizer/README.md142
-rw-r--r--plugins/Customizer/Schema/Mysql.php23
-rw-r--r--plugins/Customizer/Schema/Postgres.php25
-rw-r--r--plugins/Customizer/Schema/Sqlite.php25
-rw-r--r--plugins/Customizer/Template/board/task_avatar.php26
-rw-r--r--plugins/Customizer/Template/config/sidebar.php3
-rw-r--r--plugins/Customizer/Template/config/themecreator.php96
-rw-r--r--plugins/Customizer/Template/file/remove.php15
-rw-r--r--plugins/Customizer/Template/file/show.php435
-rw-r--r--plugins/Customizer/Template/file/upload_flavicon.php22
-rw-r--r--plugins/Customizer/Template/file/upload_loginlogo.php22
-rw-r--r--plugins/Customizer/Template/file/upload_logo.php22
-rw-r--r--plugins/Customizer/Template/header/title.php24
-rw-r--r--plugins/Customizer/Template/header/title_older_kb.php26
-rw-r--r--plugins/Customizer/Template/header/user_dropdown.php45
-rw-r--r--plugins/Customizer/Template/layout/index.php47
-rw-r--r--plugins/Customizer/Template/layout/layout.php85
-rw-r--r--plugins/Customizer/Template/layout/login_no_custom.php21
-rw-r--r--plugins/Customizer/Template/layout/login_with_custom.php128
-rw-r--r--plugins/Customizer/Template/layout/note.php7
-rw-r--r--plugins/Customizer/Template/layout/preview_style.php77
-rw-r--r--plugins/Customizer/Template/user_mod/show.php58
-rw-r--r--plugins/Customizer/_config.yml3
-rw-r--r--plugins/Customizer/composer.json5
-rw-r--r--plugins/Customizer/composer.lock164
-rw-r--r--plugins/Customizer/vendor/autoload.php7
-rw-r--r--plugins/Customizer/vendor/bin/minifycss1
-rw-r--r--plugins/Customizer/vendor/bin/minifyjs1
-rw-r--r--plugins/Customizer/vendor/composer/ClassLoader.php445
-rw-r--r--plugins/Customizer/vendor/composer/LICENSE21
-rw-r--r--plugins/Customizer/vendor/composer/autoload_classmap.php9
-rw-r--r--plugins/Customizer/vendor/composer/autoload_namespaces.php9
-rw-r--r--plugins/Customizer/vendor/composer/autoload_psr4.php12
-rw-r--r--plugins/Customizer/vendor/composer/autoload_real.php52
-rw-r--r--plugins/Customizer/vendor/composer/autoload_static.php44
-rw-r--r--plugins/Customizer/vendor/composer/installed.json154
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/.gitignore2
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/LICENSE21
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/README.md102
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/composer.json21
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/demo/demo.php41
-rw-r--r--plugins/Customizer/vendor/luizbills/css-generator/src/Generator.php102
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/CONTRIBUTING.md59
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/Dockerfile13
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/LICENSE18
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/bin/minifycss45
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/bin/minifyjs45
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/composer.json38
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_after.txt7
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_before.txt26
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_reserved.txt63
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators.txt46
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_after.txt43
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_before.txt43
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/docker-compose.yml31
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/CSS.php736
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/Exception.php20
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/BasicException.php23
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/FileImportException.php21
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/IOException.php21
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/JS.php598
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/minify/src/Minify.php459
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/path-converter/LICENSE18
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/path-converter/composer.json28
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/path-converter/src/Converter.php196
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/path-converter/src/ConverterInterface.php24
-rw-r--r--plugins/Customizer/vendor/matthiasmullie/path-converter/src/NoConverter.php23
-rw-r--r--plugins/Group_assign/.travis.yml30
-rw-r--r--plugins/Group_assign/Action/AssignGroup.php95
-rw-r--r--plugins/Group_assign/Action/EmailGroup.php80
-rw-r--r--plugins/Group_assign/Action/EmailGroupDue.php116
-rw-r--r--plugins/Group_assign/Action/EmailOtherAssignees.php80
-rw-r--r--plugins/Group_assign/Action/EmailOtherAssigneesDue.php116
-rw-r--r--plugins/Group_assign/Assets/css/group_assign.css17
-rw-r--r--plugins/Group_assign/Assets/js/group_assign.js5
-rw-r--r--plugins/Group_assign/Controller/GroupAssignTaskCreationController.php175
-rw-r--r--plugins/Group_assign/Controller/GroupAssignTaskModificationController.php208
-rw-r--r--plugins/Group_assign/Filter/TaskAllAssigneeFilter.php122
-rw-r--r--plugins/Group_assign/Helper/NewTaskHelper.php389
-rw-r--r--plugins/Group_assign/Helper/SmallAvatarHelperExtend.php35
-rw-r--r--plugins/Group_assign/LICENSE21
-rw-r--r--plugins/Group_assign/Locale/de_DE/translations.php13
-rw-r--r--plugins/Group_assign/Locale/pt_BR/translations.php97
-rw-r--r--plugins/Group_assign/Model/GroupAssignCalendarModel.php89
-rw-r--r--plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php175
-rw-r--r--plugins/Group_assign/Model/GroupColorExtension.php28
-rw-r--r--plugins/Group_assign/Model/MultiselectMemberModel.php169
-rw-r--r--plugins/Group_assign/Model/MultiselectModel.php143
-rw-r--r--plugins/Group_assign/Model/NewMetaMagikSubquery.php28
-rw-r--r--plugins/Group_assign/Model/NewTaskFinderModel.php491
-rw-r--r--plugins/Group_assign/Model/NewUserNotificationFilterModel.php218
-rw-r--r--plugins/Group_assign/Model/OldMetaMagikSubquery.php28
-rw-r--r--plugins/Group_assign/Model/OldTaskFinderModel.php489
-rw-r--r--plugins/Group_assign/Model/TaskProjectDuplicationModel.php94
-rw-r--r--plugins/Group_assign/Model/TaskProjectMoveModel.php96
-rw-r--r--plugins/Group_assign/Model/TaskRecurrenceModel.php154
-rw-r--r--plugins/Group_assign/Plugin.php200
-rw-r--r--plugins/Group_assign/README.md95
-rw-r--r--plugins/Group_assign/Schema/Mysql.php34
-rw-r--r--plugins/Group_assign/Schema/Postgres.php34
-rw-r--r--plugins/Group_assign/Schema/Sqlite.php34
-rw-r--r--plugins/Group_assign/Template/action_creation/params.php73
-rw-r--r--plugins/Group_assign/Template/board/filter.php12
-rw-r--r--plugins/Group_assign/Template/board/group.php7
-rw-r--r--plugins/Group_assign/Template/board/multi.php5
-rw-r--r--plugins/Group_assign/Template/config/toggle.php4
-rw-r--r--plugins/Group_assign/Template/header/user_dropdown.php46
-rw-r--r--plugins/Group_assign/Template/task/changes.php92
-rw-r--r--plugins/Group_assign/Template/task/details.php10
-rw-r--r--plugins/Group_assign/Template/task/multi.php6
-rw-r--r--plugins/Group_assign/Template/task_creation/show.php50
-rw-r--r--plugins/Group_assign/Template/task_modification/show.php43
-rw-r--r--plugins/Group_assign/Test/Helper/NewTaskHelperTest.php34
-rw-r--r--plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php184
-rw-r--r--plugins/Group_assign/Test/PluginTest.php19
-rw-r--r--plugins/Group_assign/_config.yml3
-rw-r--r--plugins/KanboardSearchPlugin/Controller/AdvancedSearchController.php54
-rw-r--r--plugins/KanboardSearchPlugin/Filter/AdvancedSearchFilter.php198
-rw-r--r--plugins/KanboardSearchPlugin/LICENSE21
-rw-r--r--plugins/KanboardSearchPlugin/Locale/de_DE/translations.php13
-rw-r--r--plugins/KanboardSearchPlugin/Locale/ru_RU/translations.php13
-rw-r--r--plugins/KanboardSearchPlugin/Plugin.php62
-rw-r--r--plugins/KanboardSearchPlugin/README.md6
-rw-r--r--plugins/KanboardSearchPlugin/Template/config/advanced-search-filter.php30
-rw-r--r--plugins/KanboardSearchPlugin/Template/config/sidebar.php4
-rw-r--r--plugins/KanboardSearchPlugin/docker-compose.yml27
m---------plugins/Moon0
225 files changed, 24821 insertions, 2 deletions
diff --git a/plugins/.gitignore b/plugins/.gitignore
deleted file mode 100644
index 120f485d..00000000
--- a/plugins/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!/.gitignore
diff --git a/plugins/Bigboard/Asset/js/BoardDragAndDrop.js b/plugins/Bigboard/Asset/js/BoardDragAndDrop.js
new file mode 100644
index 00000000..b6b7b8c3
--- /dev/null
+++ b/plugins/Bigboard/Asset/js/BoardDragAndDrop.js
@@ -0,0 +1,124 @@
+Kanboard.BoardDragAndDrop = function(app) {
+ this.app = app;
+ this.savingInProgress = false;
+};
+
+Kanboard.BoardDragAndDrop.prototype.execute = function() {
+ if (this.app.hasId("board")) {
+ this.executeListeners();
+ this.dragAndDrop();
+ }
+};
+
+Kanboard.BoardDragAndDrop.prototype.dragAndDrop = function() {
+ var self = this;
+ var dropzone = $(".board-task-list");
+
+ // Run for every Board List, connecting the Items within the same project id
+ dropzone.each(function() {
+ // Set dropzone height to the height of the table cell
+ $(this).css("min-height", $(this).parent().height());
+
+ var project_id = $(this).closest("table[id=board]").attr("data-project-id");
+
+ var params = {
+ forcePlaceholderSize: true,
+ tolerance: "pointer",
+ connectWith: ".sortable-column[data-project-id=" + project_id + "]",
+ placeholder: "draggable-placeholder",
+ items: ".draggable-item[data-project-id=" + project_id + "]",
+ stop: function(event, ui) {
+ var task = ui.item;
+ var taskId = task.attr('data-task-id');
+ var taskPosition = task.attr('data-position');
+ var taskColumnId = task.attr('data-column-id');
+ var taskSwimlaneId = task.attr('data-swimlane-id');
+
+ var newColumnId = task.parent().attr("data-column-id");
+ var newSwimlaneId = task.parent().attr('data-swimlane-id');
+ var newPosition = task.index() + 1;
+
+ var boardId = task.closest("table").attr("data-project-id");
+ var saveURL = task.closest("table").attr("data-save-url");
+
+ task.removeClass("draggable-item-selected");
+
+ if (newColumnId != taskColumnId || newSwimlaneId != taskSwimlaneId || newPosition != taskPosition) {
+ self.changeTaskState(taskId);
+ self.save(saveURL, boardId, taskId, taskColumnId, newColumnId, newPosition, newSwimlaneId);
+ }
+ },
+ start: function(event, ui) {
+ ui.item.addClass("draggable-item-selected");
+ ui.placeholder.height(ui.item.height());
+ }
+ };
+
+ if (isMobile.any) {
+ $(".task-board-sort-handle").css("display", "inline");
+ params.handle = ".task-board-sort-handle";
+ }
+
+ $(this).sortable(params);
+ });
+};
+
+Kanboard.BoardDragAndDrop.prototype.changeTaskState = function(taskId) {
+ var task = $("div[data-task-id=" + taskId + "]");
+ task.addClass('task-board-saving-state');
+ task.find('.task-board-saving-icon').show();
+};
+
+Kanboard.BoardDragAndDrop.prototype.save = function(saveURL, boardId, taskId, srcColumnId, dstColumnId, position, swimlaneId) {
+ var self = this;
+ self.app.showLoadingIcon();
+ self.savingInProgress = true;
+
+ $.ajax({
+ cache: false,
+ url: saveURL,
+ contentType: "application/json",
+ type: "POST",
+ processData: false,
+ data: JSON.stringify({
+ "task_id": taskId,
+ "src_column_id": srcColumnId,
+ "dst_column_id": dstColumnId,
+ "swimlane_id": swimlaneId,
+ "position": position
+ }),
+ success: function(data) {
+ self.refresh(boardId,data);
+ self.savingInProgress = false;
+ },
+ error: function() {
+ self.app.hideLoadingIcon();
+ self.savingInProgress = false;
+ },
+ statusCode: {
+ 403: function(data) {
+ window.alert(data.responseJSON.message);
+ document.location.reload(true);
+ }
+ }
+ });
+};
+
+Kanboard.BoardDragAndDrop.prototype.refresh = function(boardId, data) {
+
+ $("div[id=board-container][data-project-id=" + boardId + "]").replaceWith(data);
+
+ this.app.hideLoadingIcon();
+ this.executeListeners();
+ this.dragAndDrop();
+};
+
+Kanboard.BoardDragAndDrop.prototype.executeListeners = function() {
+ for (var className in this.app.controllers) {
+ var controller = this.app.get(className);
+
+ if (typeof controller.onBoardRendered === "function") {
+ controller.onBoardRendered();
+ }
+ }
+};
diff --git a/plugins/Bigboard/Asset/js/BoardPolling.js b/plugins/Bigboard/Asset/js/BoardPolling.js
new file mode 100644
index 00000000..6d5499fd
--- /dev/null
+++ b/plugins/Bigboard/Asset/js/BoardPolling.js
@@ -0,0 +1,43 @@
+Kanboard.BoardPolling = function(app) {
+ this.app = app;
+};
+
+Kanboard.BoardPolling.prototype.execute = function() {
+ if (this.app.hasId("board")) {
+ var interval = parseInt($("#board").attr("data-check-interval"));
+
+ if (interval > 0) {
+ window.setInterval(this.check.bind(this), interval * 1000);
+ }
+ }
+};
+
+Kanboard.BoardPolling.prototype.check = function() {
+ if (KB.utils.isVisible() && !this.app.get("BoardDragAndDrop").savingInProgress) {
+ var self = this;
+ if (!$("#app-loading-icon").length) this.app.showLoadingIcon();
+ var pollsinprogress=0;
+
+ $("table.board-project").each(function() {
+ var boardId = $(this).attr("data-project-id")
+ var url = $(this).attr("data-check-url");
+ pollsinprogress++;
+ $.ajax({
+ cache: false,
+ url: url,
+ statusCode: {
+ 200: function(data) {
+ pollsinprogress--;
+ if (pollsinprogress <= 0) self.app.hideLoadingIcon();
+ self.app.get("BoardDragAndDrop").refresh(boardId, data);
+ },
+ 304: function () {
+ pollsinprogress--;
+ if (pollsinprogress <= 0) self.app.hideLoadingIcon();
+ }
+ }
+ });
+ });
+ if (pollsinprogress <= 0) self.app.hideLoadingIcon();
+ }
+};
diff --git a/plugins/Bigboard/Controller/Bigboard.php b/plugins/Bigboard/Controller/Bigboard.php
new file mode 100644
index 00000000..80573d07
--- /dev/null
+++ b/plugins/Bigboard/Controller/Bigboard.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Kanboard\Plugin\Bigboard\Controller;
+
+use Kanboard\Controller\BaseController;
+use Kanboard\Formatter\BoardFormatter;
+use Kanboard\Model\UserMetadataModel;
+
+/**
+ * Bigboard Controller.
+ *
+ * @author Thomas Stinner
+ */
+ class Bigboard extends BaseController
+ {
+ /**
+ * Display a Board which contains multiple projects.
+ */
+ public function index()
+ {
+ if ($this->userSession->isAdmin()) {
+ $project_ids = $this->projectModel->getAllIds();
+ } else {
+ $project_ids = $this->projectPermissionModel->getActiveProjectIds($this->userSession->getId());
+ }
+
+ $nb_projects = count($project_ids);
+ // Draw a header First
+ $this->response->html($this->helper->layout->app('bigboard:board/show', array(
+ 'title' => t('Bigboard').' ('.$nb_projects.')',
+ 'board_selector' => false,
+ )));
+
+ echo $this->template->render('bigboard:board/dropdown', array(
+ 'bigboarddisplaymode' => $this->userSession->isBigboardCollapsed(),
+ ));
+
+ $this->showProjects($project_ids);
+
+ }
+
+ /**
+ * Show projects.
+ *
+ * @param $project_ids list of project ids to show
+ *
+ * @return bool
+ */
+ private function showProjects($project_ids)
+ {
+ print "<div id='bigboard'>";
+
+ foreach ($project_ids as $project_id) {
+ $project = $this->projectModel->getByIdWithOwner($project_id);
+ $search = $this->helper->projectHeader->getSearchQuery($project);
+
+ $this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_BOARD_COLLAPSED.$project_id, $this->userSession->isBigboardCollapsed());
+
+ echo $this->template->render('bigboard:board/view', array(
+ 'no_layout' => true,
+ 'board_selector' => false,
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
+ 'board_private_refresh_interval' => $this->configModel->get('board_private_refresh_interval'),
+ 'board_highlight_period' => $this->configModel->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($search)
+ ->format(BoardFormatter::getInstance($this->container)->withProjectId($project['id'])),
+ ));
+ }
+
+ print "</div>";
+
+ }
+
+ public function collapseAll()
+ {
+ $this->changeDisplayMode(true);
+ }
+
+ public function expandAll()
+ {
+ $this->changeDisplayMode(false);
+ }
+
+ private function changeDisplayMode($mode)
+ {
+ session_set('bigboardCollapsed', $mode);
+
+ if ($this->userSession->isAdmin()) {
+ $project_ids = $this->projectModel->getAllIds();
+ } else {
+ $project_ids = $this->projectPermissionModel->getActiveProjectIds(session_get('user')['id']);
+ }
+
+ if ($this->request->isAjax()) {
+ $this->showProjects($project_ids);
+ } else {
+ $this->response->redirect($this->helper->url->to('Bigboard', 'index', array('plugin' => 'Bigboard')));
+ }
+ }
+ }
diff --git a/plugins/Bigboard/Controller/BoardAjaxController.php b/plugins/Bigboard/Controller/BoardAjaxController.php
new file mode 100644
index 00000000..57832fd5
--- /dev/null
+++ b/plugins/Bigboard/Controller/BoardAjaxController.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Kanboard\Plugin\Bigboard\Controller;
+
+use Kanboard\Controller\BaseController;
+use Kanboard\Core\Controller\AccessForbiddenException;
+use Kanboard\Model\UserMetadataModel;
+
+/**
+ * Class BoardAjaxController
+ *
+ * @package Kanboard\Controller
+ * @author Fredric Guillot
+ */
+class BoardAjaxController extends BaseController
+{
+ /**
+ * Save new task positions (Ajax request made by the drag and drop)
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ if (! $project_id || ! $this->request->isAjax()) {
+ throw new AccessForbiddenException();
+ }
+
+ $values = $this->request->getJson();
+
+ if (! $this->helper->projectRole->canMoveTask($project_id, $values['src_column_id'], $values['dst_column_id'])) {
+ throw new AccessForbiddenException(e("You don't have the permission to move this task"));
+ }
+
+ $result =$this->taskPositionModel->movePosition(
+ $project_id,
+ $values['task_id'],
+ $values['dst_column_id'],
+ $values['position'],
+ $values['swimlane_id']
+ );
+
+ if (! $result) {
+ $this->response->status(400);
+ } else {
+ $this->response->html($this->renderBoard($project_id), 201);
+ }
+ }
+
+ /**
+ * Check if the board have been changed
+ *
+ * @access public
+ */
+ public function check()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $timestamp = $this->request->getIntegerParam('timestamp');
+
+ if (! $project_id || ! $this->request->isAjax()) {
+ throw new AccessForbiddenException();
+ } elseif (! $this->projectModel->isModifiedSince($project_id, $timestamp)) {
+ $this->response->status(304);
+ } else {
+ $this->response->html($this->renderBoard($project_id));
+ }
+ }
+
+ /**
+ * Reload the board with new filters
+ *
+ * @access public
+ */
+ public function reload()
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+
+ if (! $project_id || ! $this->request->isAjax()) {
+ throw new AccessForbiddenException();
+ }
+
+ $values = $this->request->getJson();
+ $this->userSession->setFilters($project_id, empty($values['search']) ? '' : $values['search']);
+
+ $this->response->html($this->renderBoard($project_id));
+ }
+
+ /**
+ * Enable collapsed mode
+ *
+ * @access public
+ */
+ public function collapse()
+ {
+ $this->changeDisplayMode(1);
+ }
+
+ /**
+ * Enable expanded mode
+ *
+ * @access public
+ */
+ public function expand()
+ {
+ $this->changeDisplayMode(0);
+ }
+
+ /**
+ * Change display mode
+ *
+ * @access private
+ * @param int $mode
+ */
+ private function changeDisplayMode($mode)
+ {
+ $project_id = $this->request->getIntegerParam('project_id');
+ $this->userMetadataCacheDecorator->set(UserMetadataModel::KEY_BOARD_COLLAPSED.$project_id, $mode);
+
+ if ($this->request->isAjax()) {
+ $this->response->html($this->renderBoard($project_id));
+ } else {
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project_id)));
+ }
+ }
+
+ /**
+ * Render board
+ *
+ * @access protected
+ * @param integer $project_id
+ * @return string
+ */
+ protected function renderBoard($project_id)
+ {
+ return $this->template->render('bigboard:board/table_container', array(
+ 'project' => $this->projectModel->getById($project_id),
+ 'board_private_refresh_interval' => $this->configModel->get('board_private_refresh_interval'),
+ 'board_highlight_period' => $this->configModel->get('board_highlight_period'),
+ 'swimlanes' => $this->taskLexer
+ ->build($this->userSession->getFilters($project_id))
+ ->format($this->boardFormatter->withProjectId($project_id))
+ ));
+ }
+}
diff --git a/plugins/Bigboard/Plugin.php b/plugins/Bigboard/Plugin.php
new file mode 100644
index 00000000..aa43c48a
--- /dev/null
+++ b/plugins/Bigboard/Plugin.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Kanboard\Plugin\Bigboard;
+
+use DateTime;
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Core\Security\Role;
+
+class Plugin extends Base
+{
+ public function initialize()
+ {
+ $this->template->hook->attach('template:project-list:menu:before', 'bigboard:Bigboard');
+ $this->template->setTemplateOverride('board/table_container','bigboard:board/table_container');
+ $this->template->setTemplateOverride('board/table_tasks','bigboard:board/table_tasks');
+ $this->template->setTemplateOverride('board/table_private','bigboard:board/table_private');
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Bigboard/Asset/js/BoardDragAndDrop.js'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Bigboard/Asset/js/BoardPolling.js'));
+ }
+
+ public function getClasses()
+ {
+ return array(
+ 'Plugin\Bigboard' => array(
+ 'UserSession'
+ ),
+ 'Plugin\Bigboard\Controller' => array(
+ 'Bigboard',
+ 'BoardAjaxController'
+ )
+ );
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ }
+
+ public function getPluginName()
+ {
+ return 'Bigboard';
+ }
+
+ public function getPluginDescription()
+ {
+ return t('Kanboard that displays multiple projects');
+ }
+
+ public function getPluginAuthor()
+ {
+ return 'Thomas Stinner';
+ }
+
+ public function getPluginVersion()
+ {
+ return '1.0.5';
+ }
+
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/stinnux/kanboard-bigboard';
+ }
+
+ public function getCompatibleVersion()
+ {
+ return '>=1.2.4';
+ }
+
+}
diff --git a/plugins/Bigboard/README.md b/plugins/Bigboard/README.md
new file mode 100644
index 00000000..aca27a38
--- /dev/null
+++ b/plugins/Bigboard/README.md
@@ -0,0 +1,10 @@
+kanboard-bigboard
+=================
+
+A Kanboard that can display multiple projects.
+
+Go To Project-Management->BigBoard. Here you will see all projects you have access to in one page.
+
+All functionality is still available, so you can drag-and-drop tasks, add tasks etc.
+
+Comments Welcome.
diff --git a/plugins/Bigboard/Template/Bigboard.php b/plugins/Bigboard/Template/Bigboard.php
new file mode 100644
index 00000000..6767da64
--- /dev/null
+++ b/plugins/Bigboard/Template/Bigboard.php
@@ -0,0 +1,15 @@
+<?php
+ $routerController = $this->app->getRouterController();
+ $routerPlugin = $this->app->getPluginName();
+
+ $active = $routerController == 'Bigboard' && $routerPlugin == 'Bigboard';
+?>
+<li class="<?= $active ? 'active' : '' ?>">
+ <i class="fa fa-th fa-fw"></i>
+ <?= $this->url->link(
+ 'Bigboard',
+ 'Bigboard',
+ 'index',
+ ['plugin' => 'Bigboard', ]
+ ) ?>
+</li>
diff --git a/plugins/Bigboard/Template/board/dropdown.php b/plugins/Bigboard/Template/board/dropdown.php
new file mode 100644
index 00000000..de64917d
--- /dev/null
+++ b/plugins/Bigboard/Template/board/dropdown.php
@@ -0,0 +1,32 @@
+<div class="project-header">
+ <div class="dropdown-component">
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu action-menu"><?= t('Menu') ?> <i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li>
+ <span class="filter-display-mode" <?= $bigboarddisplaymode ? '' : 'style="display: none;"' ?>>
+ <i class="fa fa-expand fa-fw"></i>
+ <?= $this->url->link(t('Expand tasks'), 'Bigboard', 'expandAll', array('plugin' => 'Bigboard'), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
+ </span>
+ <span class="filter-display-mode" <?= $bigboarddisplaymode ? 'style="display: none;"' : '' ?>>
+ <i class="fa fa-compress fa-fw"></i>
+ <?= $this->url->link(t('Collapse tasks'), 'Bigboard', 'collapseAll', array('plugin' => 'Bigboard'), false, 'board-display-mode', t('Keyboard shortcut: "%s"', 's')) ?>
+ </span>
+ </li>
+ <li>
+ <span class="filter-compact">
+ <i class="fa fa-th fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Compact view') ?></a>
+ </span>
+ <span class="filter-wide" style="display: none">
+ <i class="fa fa-arrows-h fa-fw"></i> <a href="#" class="filter-toggle-scrolling" title="<?= t('Keyboard shortcut: "%s"', 'c') ?>"><?= t('Horizontal scrolling') ?></a>
+ </span>
+ </li>
+
+ <li>
+ <i class="fa fa-folder fa-fw" aria-hidden="true"></i>
+ <?= $this->url->link(t('Manage projects'), 'ProjectListController', 'show') ?>
+ </li>
+ </ul>
+ </div>
+ </div>
+</div>
diff --git a/plugins/Bigboard/Template/board/show.php b/plugins/Bigboard/Template/board/show.php
new file mode 100644
index 00000000..dcc10e71
--- /dev/null
+++ b/plugins/Bigboard/Template/board/show.php
@@ -0,0 +1,5 @@
+<?
+
+// just an empty file by intent
+
+?>
diff --git a/plugins/Bigboard/Template/board/table_container.php b/plugins/Bigboard/Template/board/table_container.php
new file mode 100644
index 00000000..e7fc6116
--- /dev/null
+++ b/plugins/Bigboard/Template/board/table_container.php
@@ -0,0 +1,58 @@
+<div id="board-container" data-project-id='<?= $project['id'] ?>'>
+ <?php if (empty($swimlanes) || empty($swimlanes[0]['nb_columns'])): ?>
+ <p class="alert alert-error"><?= t('There is no column or swimlane activated in your project!') ?></p>
+ <?php else: ?>
+
+ <?php if (isset($not_editable)): ?>
+ <table id="board" class="board-project-<?= $project['id'] ?>">
+ <?php else: ?>
+ <table id="board"
+ class="board-project"
+ data-project-id="<?= $project['id'] ?>"
+ data-check-interval="<?= $board_private_refresh_interval ?>"
+ data-save-url="<?= $this->url->href('BoardAjaxController', 'save', array('plugin' => "Bigboard", 'project_id' => $project['id'])) ?>"
+ data-reload-url="<?= $this->url->href('BoardAjaxController', 'reload', array('plugin' => "Bigboard", 'project_id' => $project['id'])) ?>"
+ data-check-url="<?= $this->url->href('BoardAjaxController', 'check', array('plugin' => "Bigboard", 'project_id' => $project['id'], 'timestamp' => time())) ?>"
+ data-task-creation-url="<?= $this->url->href('TaskCreationController', 'show', array('plugin' => "Bigboard", 'project_id' => $project['id'])) ?>"
+ >
+ <?php endif ?>
+
+ <?php foreach ($swimlanes as $index => $swimlane): ?>
+ <?php if (! ($swimlane['nb_tasks'] === 0 && isset($not_editable))): ?>
+
+ <!-- Note: Do not show swimlane row on the top otherwise we can't collapse columns -->
+ <?php if ($index > 0 && $swimlane['nb_swimlanes'] > 1): ?>
+ <?= $this->render('board/table_swimlane', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'not_editable' => isset($not_editable),
+ )) ?>
+ <?php endif ?>
+
+ <?= $this->render('board/table_column', array(
+ 'swimlane' => $swimlane,
+ 'not_editable' => isset($not_editable),
+ )) ?>
+
+ <?php if ($index === 0 && $swimlane['nb_swimlanes'] > 1): ?>
+ <?= $this->render('board/table_swimlane', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'not_editable' => isset($not_editable),
+ )) ?>
+ <?php endif ?>
+
+ <?= $this->render('bigboard:board/table_tasks', array(
+ 'project' => $project,
+ 'swimlane' => $swimlane,
+ 'not_editable' => isset($not_editable),
+ 'board_highlight_period' => $board_highlight_period,
+ )) ?>
+
+ <?php endif ?>
+ <?php endforeach ?>
+
+ </table>
+
+ <?php endif ?>
+</div>
diff --git a/plugins/Bigboard/Template/board/table_tasks.php b/plugins/Bigboard/Template/board/table_tasks.php
new file mode 100644
index 00000000..3d280489
--- /dev/null
+++ b/plugins/Bigboard/Template/board/table_tasks.php
@@ -0,0 +1,38 @@
+<!-- task row -->
+<tr class="board-swimlane board-swimlane-tasks-<?= $swimlane['id'] ?>">
+ <?php foreach ($swimlane['columns'] as $column): ?>
+ <td class="
+ board-column-<?= $column['id'] ?>
+ <?= $column['task_limit'] > 0 && $column['nb_tasks'] > $column['task_limit'] ? 'board-task-list-limit' : '' ?>
+ "
+ >
+
+ <!-- tasks list -->
+ <div
+ class="board-task-list board-column-expanded <?= $this->projectRole->isSortableColumn($column['project_id'], $column['id']) ? 'sortable-column' : '' ?>"
+ data-project-id="<?= $project['id'] ?>"
+ data-column-id="<?= $column['id'] ?>"
+ data-swimlane-id="<?= $swimlane['id'] ?>"
+ data-task-limit="<?= $column['task_limit'] ?>">
+
+ <?php foreach ($column['tasks'] as $task): ?>
+ <?= $this->render($not_editable ? 'board/task_public' : 'bigboard:board/task_private', array(
+ 'project' => $project,
+ 'task' => $task,
+ 'board_highlight_period' => $board_highlight_period,
+ 'not_editable' => $not_editable,
+ )) ?>
+ <?php endforeach ?>
+ </div>
+
+ <!-- column in collapsed mode (rotated text) -->
+ <div class="board-column-collapsed">
+ <div class="board-rotation-wrapper">
+ <div class="board-column-title board-rotation board-toggle-column-view" data-column-id="<?= $column['id'] ?>" title="<?= t('Show this column') ?>">
+ <i class="fa fa-plus-square tooltip" title="<?= $this->text->e($column['title']) ?>"></i> <?= $this->text->e($column['title']) ?>
+ </div>
+ </div>
+ </div>
+ </td>
+ <?php endforeach ?>
+</tr>
diff --git a/plugins/Bigboard/Template/board/task_private.php b/plugins/Bigboard/Template/board/task_private.php
new file mode 100644
index 00000000..d97e0e2c
--- /dev/null
+++ b/plugins/Bigboard/Template/board/task_private.php
@@ -0,0 +1,66 @@
+<div class="
+ task-board
+ <?= $task['is_draggable'] ? 'draggable-item ' : '' ?>
+ <?= $task['is_active'] == 1 ? 'task-board-status-open '.($task['date_modification'] > (time() - $board_highlight_period) ? 'task-board-recent' : '') : 'task-board-status-closed' ?>
+ color-<?= $task['color_id'] ?>"
+ data-project-id="<?= $task['project_id'] ?>"
+ data-task-id="<?= $task['id'] ?>"
+ data-column-id="<?= $task['column_id'] ?>"
+ data-swimlane-id="<?= $task['swimlane_id'] ?>"
+ data-position="<?= $task['position'] ?>"
+ data-owner-id="<?= $task['owner_id'] ?>"
+ data-category-id="<?= $task['category_id'] ?>"
+ data-due-date="<?= $task['date_due'] ?>"
+ data-task-url="<?= $this->url->href('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+
+ <div class="task-board-sort-handle" style="display: none;"><i class="fa fa-arrows-alt"></i></div>
+
+ <?php if ($this->board->isCollapsed($task['project_id'])): ?>
+ <div class="task-board-collapsed">
+ <div class="task-board-saving-icon" style="display: none;"><i class="fa fa-spinner fa-pulse"></i></div>
+ <?php if ($this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
+
+ <?php if (! empty($task['assignee_username'])): ?>
+ <span title="<?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?>">
+ <?= $this->text->e($this->user->getInitials($task['assignee_name'] ?: $task['assignee_username'])) ?>
+ </span> -
+ <?php endif ?>
+ <?= $this->url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id']), false, 'tooltip', $this->text->e($task['title'])) ?>
+ </div>
+ <?php else: ?>
+ <div class="task-board-expanded">
+ <div class="task-board-saving-icon" style="display: none;"><i class="fa fa-spinner fa-pulse fa-2x"></i></div>
+ <div class="task-board-header">
+ <?php if ($this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?>
+ <?= $this->render('task/dropdown', array('task' => $task)) ?>
+ <?php else: ?>
+ <strong><?= '#'.$task['id'] ?></strong>
+ <?php endif ?>
+
+ <?php if (! empty($task['owner_id'])): ?>
+ <span class="task-board-assignee">
+ <?= $this->text->e($task['assignee_name'] ?: $task['assignee_username']) ?>
+ </span>
+ <?php endif ?>
+
+ <?= $this->render('board/task_avatar', array('task' => $task)) ?>
+ </div>
+
+ <?= $this->hook->render('template:board:private:task:before-title', array('task' => $task)) ?>
+ <div class="task-board-title">
+ <?= $this->url->link($this->text->e($task['title']), 'TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>
+ </div>
+ <?= $this->hook->render('template:board:private:task:after-title', array('task' => $task)) ?>
+
+ <?= $this->render('board/task_footer', array(
+ 'task' => $task,
+ 'not_editable' => $not_editable,
+ 'project' => $project,
+ )) ?>
+ </div>
+ <?php endif ?>
+</div>
diff --git a/plugins/Bigboard/Template/board/view.php b/plugins/Bigboard/Template/board/view.php
new file mode 100644
index 00000000..7987e7c2
--- /dev/null
+++ b/plugins/Bigboard/Template/board/view.php
@@ -0,0 +1,20 @@
+<section id="main">
+
+ <span class='header'><h1><?= $this->text->e($project['name']) ?>
+
+ <?php if (! empty($project['description'])): ?>
+ <?= $this->app->tooltipMarkdown($project['description']) ?>
+ <?php endif ?>
+
+ </span>
+ </h1></span>
+
+
+ <?= $this->render('bigboard:board/table_container', array(
+ 'project' => $project,
+ 'swimlanes' => $swimlanes,
+ 'board_private_refresh_interval' => $board_private_refresh_interval,
+ 'board_highlight_period' => $board_highlight_period,
+ )) ?>
+
+</section>
diff --git a/plugins/Bigboard/UserSession.php b/plugins/Bigboard/UserSession.php
new file mode 100644
index 00000000..8bdea501
--- /dev/null
+++ b/plugins/Bigboard/UserSession.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Kanboard\Plugin\Bigboard;
+
+class UserSession extends \Kanboard\Core\User\UserSession
+{
+ /**
+ * is the Bigboard collapsed or expanded
+ *
+ * @access public
+ * @return boolean
+ */
+ public function isBigboardCollapsed()
+ {
+ return session_is_true('bigboardCollapsed');
+ }
+
+ /**
+ * Set Bigboard display mode
+ *
+ * @access public
+ * @param boolean $is_collapsed
+ */
+ public function setBigboardDisplayMode($is_collapsed)
+ {
+ session_set('bigboardCollapsed', true);
+ }
+
+}
diff --git a/plugins/Boardcustomizer/Controller/SettingsController.php b/plugins/Boardcustomizer/Controller/SettingsController.php
new file mode 100644
index 00000000..e98ffbde
--- /dev/null
+++ b/plugins/Boardcustomizer/Controller/SettingsController.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Kanboard\Plugin\BoardCustomizer\Controller;
+
+use Kanboard\Controller\BaseController;
+
+class SettingsController extends BaseController
+{
+
+ public function showSettings()
+ {
+ $user = $this->getUser();
+
+ $options = [
+ t('Board: only show first column header') => 'boardcustomizer_onlyfirstcolumnheaders',
+ t('Board: top selection without scollbar') => 'boardcustomizer_topnavhiddenscrollbar',
+ t('Card: material design') => 'boardcustomizer_materialcard',
+ t('Card: white background') => 'boardcustomizer_whitebackground',
+ t('Card: hide owner name') => 'boardcustomizer_hideownername',
+ t('Card: hide category') => 'boardcustomizer_hidecategory',
+ t('Card: hide tags') => 'boardcustomizer_hidetags',
+ t('Card: hide all footer icons') => 'boardcustomizer_hidefooter',
+ t('Card: hide task priority') => 'boardcustomizer_hidetaskpriority',
+ t('Card: hide task age') => 'boardcustomizer_hidetaskage',
+ t('Card: hide reference') => 'boardcustomizer_hidereference',
+ t('Card: hide score') => 'boardcustomizer_hidescore',
+ t('Card: hide time estimated') => 'boardcustomizer_hidetimeestimated',
+ t('Card: hide task date') => 'boardcustomizer_hidetaskdate'
+ ];
+
+ // additional options is other plugin is installed
+ $pluginFGroupAssign = PLUGINS_DIR . DIRECTORY_SEPARATOR . basename('Group_assign');
+ if (file_exists($pluginFGroupAssign)) {
+ $plugin_groupassign = [
+ t('Card: hide group labels') => 'boardcustomizer_groupassign_hidecardlabels'
+ ];
+ $options = array_merge($options, $plugin_groupassign);
+ }
+
+ $this->response->html($this->helper->layout->user('boardcustomizer:user/settings', [
+ 'title' => t('My display settings'),
+ 'user' => $user,
+ 'options' => $options,
+ ]));
+ }
+
+ public function disable()
+ {
+ $user = $this->getUser();
+ $key = $this->request->getStringParam('key');
+ $this->userMetadataModel->remove($user['id'], $key);
+ return $this->response->redirect($this->helper->url->to('SettingsController', 'showSettings', ['plugin' => 'boardcustomizer']), true);
+ }
+
+ public function enable()
+ {
+ $user = $this->getUser();
+ $key = $this->request->getStringParam('key');
+ $this->userMetadataModel->save($user['id'], [$key => true]);
+ return $this->response->redirect($this->helper->url->to('SettingsController', 'showSettings', ['plugin' => 'boardcustomizer']), true);
+ }
+}
diff --git a/plugins/Boardcustomizer/Locale/de_DE/translations.php b/plugins/Boardcustomizer/Locale/de_DE/translations.php
new file mode 100644
index 00000000..91587405
--- /dev/null
+++ b/plugins/Boardcustomizer/Locale/de_DE/translations.php
@@ -0,0 +1,21 @@
+<?php
+
+return array(
+ 'My display settings' => 'Meine Anzeigeeinstellungen',
+ 'Board: only show first column header' => 'Pinnwand: nur erste Spaltenüberschrift anzeigen',
+ 'Board: top selection without scollbar' => 'Pinnwand: Pinnwandauswahl in Kopfzeile ohne Scrollbar',
+ 'Card: material design' => 'Karte: Materialdesign',
+ 'Card: hide task priority' => 'Karte: Aufgabenpriorität ausblenden',
+ 'Card: hide task age' => 'Karte: Alter der Aufgabe ausblenden',
+ 'Card: white background' => 'Karte: weißer Hintergrund',
+ 'Card: hide group labels' => 'Karte: Gruppenbeschriftungen ausblenden',
+ 'Customize board and card style' => 'Anpassen von Pinnwand- und Kartenstilen',
+ 'Card: hide owner name' => 'Karte: Name Zuständiger ausblenden',
+ 'Card: hide category' => 'Karte: Kategorie ausblenden',
+ 'Card: hide tags' => 'Karte: Schlagworte ausblenden',
+ 'Card: hide all footer icons' => 'Karte: alle Icons ausblenden',
+ 'Card: hide reference' => 'Karte: Referenz ausblenden',
+ 'Card: hide score' => 'Karte: Komplexität ausblenden',
+ 'Card: hide time estimated' => 'Karte: geschätzte Zeit ausblenden',
+ 'Card: hide task date' => 'Karte: Datum ausblenden',
+);
diff --git a/plugins/Boardcustomizer/Makefile b/plugins/Boardcustomizer/Makefile
new file mode 100644
index 00000000..65801bdd
--- /dev/null
+++ b/plugins/Boardcustomizer/Makefile
@@ -0,0 +1,6 @@
+plugin=Boardcustomizer
+version=1.0.1
+
+all:
+ @ echo "Build archive for plugin ${plugin} version=${version}"
+ @ git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip \ No newline at end of file
diff --git a/plugins/Boardcustomizer/Plugin.php b/plugins/Boardcustomizer/Plugin.php
new file mode 100644
index 00000000..0cf8f990
--- /dev/null
+++ b/plugins/Boardcustomizer/Plugin.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Kanboard\Plugin\BoardCustomizer;
+
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+
+class Plugin extends Base
+{
+ public function initialize()
+ {
+ $this->template->hook->attach('template:layout:head', 'boardcustomizer:layout/head');
+ $this->template->hook->attach('template:project:dropdown', 'boardcustomizer:project/dropdown');
+ $this->template->hook->attach('template:user:sidebar:information', 'boardcustomizer:user/sidebar');
+ }
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__ . '/Locale');
+ }
+ public function getPluginName()
+ {
+ return 'Boardcustomizer';
+ }
+ public function getPluginDescription()
+ {
+ return t('Customize board and card style');
+ }
+ public function getPluginAuthor()
+ {
+ return 'BlueTeck';
+ }
+ public function getPluginVersion()
+ {
+ return '1.0.1';
+ }
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/BlueTeck/kanboard_plugin_boardcustomizer';
+ }
+ public function getCompatibleVersion()
+ {
+ return '>=1.2.10';
+ }
+}
diff --git a/plugins/Boardcustomizer/README.md b/plugins/Boardcustomizer/README.md
new file mode 100644
index 00000000..ede7fec0
--- /dev/null
+++ b/plugins/Boardcustomizer/README.md
@@ -0,0 +1,51 @@
+# Kanboard Plugin BoardCustomizer
+
+Customize board and card style per user.
+
+Plugin for https://github.com/kanboard/kanboard
+
+
+## Options
+
+### Board
+- only show first column header
+- top selection without scollbar
+
+### Card
+- material design
+- hide task priority
+- hide task age
+- white background
+- hide owner name
+- hide category
+- hide tags
+- hide whole footer
+- hide reference
+- hide score
+- hide time estimated
+- hide task date
+- hide group labels (if [Group Assign](https://github.com/creecros/group_assign) Plugin is installed)
+
+Further options and pull requests are welcome.
+
+## Screenshots
+
+![Screenshot 1](https://user-images.githubusercontent.com/1961634/60758678-45135700-a01a-11e9-9c9d-23290daaddb7.png)
+
+![Screenshot 2](https://user-images.githubusercontent.com/1961634/60758686-6f651480-a01a-11e9-8bb0-0afb13ba9a6e.png)
+
+## Author
+
+- [BlueTeck](https://github.com/BlueTeck)
+- License MIT
+
+## Installation
+
+No additional tables are needed, metadata storage of the user is used.
+
+- Decompress the archive in the `plugins` folder
+
+or
+
+- Create a folder **plugins/Boardcustomizer**
+- Copy all files under this directory \ No newline at end of file
diff --git a/plugins/Boardcustomizer/Template/layout/head.php b/plugins/Boardcustomizer/Template/layout/head.php
new file mode 100644
index 00000000..fca861f9
--- /dev/null
+++ b/plugins/Boardcustomizer/Template/layout/head.php
@@ -0,0 +1,173 @@
+<?php
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_materialcard")) {
+ /* change overall card layout and shadow */
+ ?>
+ <style>
+ .board-task-list>div.task-board {
+ font-size: 13px;
+ border-left-width: 2.5px !important;
+ border-radius: 3px;
+ padding-left: 6px;
+ padding-right: 4px;
+ padding-top: 6px;
+ padding-bottom: 6px;
+ margin-bottom: 8px;
+ margin-left: 8px;
+ margin-right: 8px;
+ box-shadow: 0 1px 2px rgba(62, 54, 54, 0.55);
+ border-right: none;
+ border-top: 1px solid #ececec;
+ border-bottom: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_onlyfirstcolumnheaders")) {
+ /* This will hide all column titles on swimlanes except for the first one */
+ ?>
+ <style>
+ tr[class*='board-swimlane-columns']:not(:first-child) {
+ display: none;
+ }
+
+ .board-column-header-task-count {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidetaskage")) {
+ /* hide task age */
+ ?>
+ <style>
+ .task-icon-age {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidetaskpriority")) {
+ /* hide task priority */
+ ?>
+ <style>
+ .task-priority {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_whitebackground")) {
+ /* task white background */
+ ?>
+ <style>
+ .board-task-list>div {
+ background-color: white !important;
+ background: #fff;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_topnavhiddenscrollbar")) {
+ /* better nav bar */
+ ?>
+ <style>
+ #select-dropdown-menu {
+ overflow: auto !important;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_groupassign_hidecardlabels")) {
+ /* hide group assign labels */
+ ?>
+ <style>
+ .assigned-group-label {
+ display: none;
+ }
+
+ .assigned-other-label {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hideownername")) {
+ /* hide owner text */
+ ?>
+ <style>
+ div.task-board-header > .task-board-assignee {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidecategory")) {
+ /* hide category */
+ ?>
+ <style>
+ .task-board-category-container {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidetags")) {
+ /* hide tags */
+ ?>
+ <style>
+ .task-tags {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidefooter")) {
+ /* hide whole footer */
+ ?>
+ <style>
+ .task-board-icons {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidereference")) {
+ /* hide reference */
+ ?>
+ <style>
+ .task-board-reference {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidescore")) {
+ /* hide score */
+ ?>
+ <style>
+ .task-score {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidetimeestimated")) {
+ /* hide time estimated */
+ ?>
+ <style>
+ .task-time-estimated {
+ display: none;
+ }
+ </style>
+<?php
+}
+if ($this->user->userMetadataModel->exists($this->user->getid(), "boardcustomizer_hidetaskdate")) {
+ /* hide task date */
+ ?>
+ <style>
+ .task-date {
+ display: none;
+ }
+ </style>
+<?php
+} \ No newline at end of file
diff --git a/plugins/Boardcustomizer/Template/project/dropdown.php b/plugins/Boardcustomizer/Template/project/dropdown.php
new file mode 100644
index 00000000..0d9f7e6a
--- /dev/null
+++ b/plugins/Boardcustomizer/Template/project/dropdown.php
@@ -0,0 +1,3 @@
+<li>
+ <?= $this->url->icon('cog', t('My display settings'), 'SettingsController', 'showSettings', array('plugin' => 'boardcustomizer')) ?>
+</li> \ No newline at end of file
diff --git a/plugins/Boardcustomizer/Template/user/settings.php b/plugins/Boardcustomizer/Template/user/settings.php
new file mode 100644
index 00000000..9a6e3124
--- /dev/null
+++ b/plugins/Boardcustomizer/Template/user/settings.php
@@ -0,0 +1,29 @@
+<div class="page-header">
+ <h2><?= t('My display settings') ?></h2>
+</div>
+
+
+<?php if (empty($options)) : ?>
+ <p class="alert"><?= t('No options') ?></p>
+<?php else : ?>
+ <table class="table-small table-fixed">
+ <tr>
+ <th class="column-40"><?= t('Option') ?></th>
+ <th class="column-20"><?= t('Status') ?></th>
+ </tr>
+ <?php foreach ($options as $option => $key) : ?>
+ <tr>
+ <td>
+ <?= $option ?>
+ </td>
+ <td>
+ <?php if ($this->user->userMetadataModel->exists($user['id'], $key)) : ?>
+ <?= $this->url->icon('toggle-off', t('Disable'), 'SettingsController', 'disable', array('plugin' => 'boardcustomizer', 'key' => $key), true) ?>
+ <?php else : ?>
+ <?= $this->url->icon('toggle-on', t('Enable'), 'SettingsController', 'enable', array('plugin' => 'boardcustomizer', 'key' => $key), true) ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+<?php endif ?> \ No newline at end of file
diff --git a/plugins/Boardcustomizer/Template/user/sidebar.php b/plugins/Boardcustomizer/Template/user/sidebar.php
new file mode 100644
index 00000000..2d7a1582
--- /dev/null
+++ b/plugins/Boardcustomizer/Template/user/sidebar.php
@@ -0,0 +1,3 @@
+<li <?= $this->app->checkMenuSelection('SettingsController', 'showSettings') ?>>
+ <?= $this->url->link(t('My display settings'), 'SettingsController', 'showSettings', array('plugin' => 'boardcustomizer')) ?>
+</li> \ No newline at end of file
diff --git a/plugins/Calendar/.github/issue_template.md b/plugins/Calendar/.github/issue_template.md
new file mode 100644
index 00000000..85dcb3d3
--- /dev/null
+++ b/plugins/Calendar/.github/issue_template.md
@@ -0,0 +1,21 @@
+**Please, do not create duplicate issues**
+
+
+### Actual behaviour
+
+
+### Expected behaviour
+
+
+### Steps to reproduce
+
+
+### Configuration
+
+- Plugin version:
+- Kanboard version:
+- Database type and version:
+- PHP version:
+- OS:
+- Browser:
+
diff --git a/plugins/Calendar/.gitignore b/plugins/Calendar/.gitignore
new file mode 100644
index 00000000..c4c4ffc6
--- /dev/null
+++ b/plugins/Calendar/.gitignore
@@ -0,0 +1 @@
+*.zip
diff --git a/plugins/Calendar/.travis.yml b/plugins/Calendar/.travis.yml
new file mode 100644
index 00000000..dfe52dbb
--- /dev/null
+++ b/plugins/Calendar/.travis.yml
@@ -0,0 +1,34 @@
+language: php
+sudo: false
+
+php:
+ - 7.1
+ - 7.0
+ - 5.6
+ - 5.5
+ - 5.4
+
+env:
+ global:
+ - PLUGIN=Calendar
+ - KANBOARD_REPO=https://github.com/kanboard/kanboard.git
+ matrix:
+ - DB=sqlite
+ - DB=mysql
+ - DB=postgres
+
+matrix:
+ fast_finish: true
+
+install:
+ - git clone --depth 1 $KANBOARD_REPO
+ - ln -s $TRAVIS_BUILD_DIR kanboard/plugins/$PLUGIN
+
+before_script:
+ - cd kanboard
+ - phpenv config-add tests/php.ini
+ - composer install
+ - ls -la plugins/
+
+script:
+ - phpunit -c tests/units.$DB.xml plugins/$PLUGIN/Test/
diff --git a/plugins/Calendar/Assets/calendar.js b/plugins/Calendar/Assets/calendar.js
new file mode 100644
index 00000000..7c26c095
--- /dev/null
+++ b/plugins/Calendar/Assets/calendar.js
@@ -0,0 +1,81 @@
+KB.component('calendar', function (containerElement, options) {
+ var modeMapping = {
+ month: 'month',
+ week: 'agendaWeek',
+ day: 'agendaDay'
+ };
+
+ this.render = function () {
+ var calendar = $(containerElement);
+ var mode = 'month';
+ if (window.location.hash) { // Check if hash contains mode
+ var hashMode = window.location.hash.substr(1);
+ mode = modeMapping[hashMode] || mode;
+ }
+
+ calendar.fullCalendar({
+ locale: $("body").data("js-lang"),
+ editable: true,
+ eventLimit: true,
+ defaultView: mode,
+ header: {
+ left: 'prev,next today',
+ center: 'title',
+ right: 'month,agendaWeek,agendaDay'
+ },
+ eventDrop: function(event) {
+ $.ajax({
+ cache: false,
+ url: options.saveUrl,
+ contentType: "application/json",
+ type: "POST",
+ processData: false,
+ data: JSON.stringify({
+ "task_id": event.id,
+ "date_due": event.start.format()
+ })
+ });
+ },
+ viewRender: function(view) {
+ // Map view.name back and update location.hash
+ for (var id in modeMapping) {
+ if (modeMapping[id] === view.name) { // Found
+ window.location.hash = id;
+ break;
+ }
+ }
+ var url = options.checkUrl;
+ var params = {
+ "start": calendar.fullCalendar('getView').start.format(),
+ "end": calendar.fullCalendar('getView').end.format()
+ };
+
+ for (var key in params) {
+ url += "&" + key + "=" + params[key];
+ }
+
+ $.getJSON(url, function(events) {
+ calendar.fullCalendar('removeEvents');
+ calendar.fullCalendar('addEventSource', events);
+ calendar.fullCalendar('rerenderEvents');
+ });
+ }
+ });
+ };
+});
+
+KB.on('dom.ready', function () {
+ function goToLink (selector) {
+ if (! KB.modal.isOpen()) {
+ var element = KB.find(selector);
+
+ if (element !== null) {
+ window.location = element.attr('href');
+ }
+ }
+ }
+
+ KB.onKey('v+c', function () {
+ goToLink('a.view-calendar');
+ });
+});
diff --git a/plugins/Calendar/Assets/fullcalendar.min.css b/plugins/Calendar/Assets/fullcalendar.min.css
new file mode 100644
index 00000000..66afcb6e
--- /dev/null
+++ b/plugins/Calendar/Assets/fullcalendar.min.css
@@ -0,0 +1,5 @@
+/*!
+ * FullCalendar v3.3.1 Stylesheet
+ * Docs & License: https://fullcalendar.io/
+ * (c) 2017 Adam Shaw
+ */.fc-icon,body .fc{font-size:1em}.fc-button-group,.fc-icon{display:inline-block}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-icon,.fc-unselectable{-khtml-user-select:none;-webkit-touch-callout:none}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view td.fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed td.fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc-unthemed .fc-disabled-day{background:#d7d7d7;opacity:.3}.ui-widget .fc-disabled-day{background-image:none}.fc-icon{height:1em;line-height:1em;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;white-space:nowrap;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a[data-goto]{cursor:pointer}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;font-weight:400}.fc-event,.fc-event-dot{background-color:#3a87ad}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.fc-toolbar.fc-header-toolbar{margin-bottom:1em}.fc-toolbar.fc-footer-toolbar{margin-top:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:2px}.fc-basic-view th.fc-day-number,.fc-basic-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-basic-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-basic-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-basic-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-basic-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-basic-view .fc-day-top .fc-week-number{min-width:1.5em;text-align:center;background-color:#f2f2f2;color:grey}.fc-basic-view td.fc-week-number>*{display:inline-block;min-width:1.25em}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item:hover td{background-color:#f5f5f5}.fc-list-item-marker,.fc-list-item-time{white-space:nowrap;width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee} \ No newline at end of file
diff --git a/plugins/Calendar/Assets/fullcalendar.min.js b/plugins/Calendar/Assets/fullcalendar.min.js
new file mode 100644
index 00000000..3aa6e88a
--- /dev/null
+++ b/plugins/Calendar/Assets/fullcalendar.min.js
@@ -0,0 +1,10 @@
+/*!
+ * FullCalendar v3.3.1
+ * Docs & License: https://fullcalendar.io/
+ * (c) 2017 Adam Shaw
+ */
+!function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):"object"==typeof exports?module.exports=t(require("jquery"),require("moment")):t(jQuery,moment)}(function(t,e){function n(t){return it(t,Qt)}function i(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function r(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function s(){t("body").addClass("fc-not-allowed")}function o(){t("body").removeClass("fc-not-allowed")}function l(e,n,i){var r=Math.floor(n/e.length),s=Math.floor(n-r*(e.length-1)),o=[],l=[],u=[],c=0;a(e),e.each(function(n,i){var a=n===e.length-1?s:r,d=t(i).outerHeight(!0);d<a?(o.push(i),l.push(d),u.push(t(i).height())):c+=d}),i&&(n-=c,r=Math.floor(n/o.length),s=Math.floor(n-r*(o.length-1))),t(o).each(function(e,n){var i=e===o.length-1?s:r,a=l[e],c=u[e],d=i-(a-c);a<i&&t(n).height(d)})}function a(t){t.height("")}function u(e){var n=0;return e.find("> *").each(function(e,i){var r=t(i).outerWidth();r>n&&(n=r)}),n++,e.width(n),n}function c(t,e){var n,i=t.add(e);return i.css({position:"relative",left:-1}),n=t.outerHeight()-e.outerHeight(),i.css({position:"",left:""}),n}function d(e){var n=e.css("position"),i=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==n&&i.length?i:t(e[0].ownerDocument||document)}function h(t,e){var n=t.offset(),i=n.left-(e?e.left:0),r=n.top-(e?e.top:0);return{left:i,right:i+t.outerWidth(),top:r,bottom:r+t.outerHeight()}}function f(t,e){var n=t.offset(),i=p(t),r=n.left+S(t,"border-left-width")+i.left-(e?e.left:0),s=n.top+S(t,"border-top-width")+i.top-(e?e.top:0);return{left:r,right:r+t[0].clientWidth,top:s,bottom:s+t[0].clientHeight}}function g(t,e){var n=t.offset(),i=n.left+S(t,"border-left-width")+S(t,"padding-left")-(e?e.left:0),r=n.top+S(t,"border-top-width")+S(t,"padding-top")-(e?e.top:0);return{left:i,right:i+t.width(),top:r,bottom:r+t.height()}}function p(t){var e,n=t[0].offsetWidth-t[0].clientWidth,i=t[0].offsetHeight-t[0].clientHeight;return n=v(n),i=v(i),e={left:0,right:0,top:0,bottom:i},m()&&"rtl"==t.css("direction")?e.left=n:e.right=n,e}function v(t){return t=Math.max(0,t),t=Math.round(t)}function m(){return null===Xt&&(Xt=y()),Xt}function y(){var e=t("<div><div/></div>").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),n=e.children(),i=n.offset().left>e.offset().left;return e.remove(),i}function S(t,e){return parseFloat(t.css(e))||0}function w(t){return 1==t.which&&!t.ctrlKey}function b(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageX:t.pageX}function E(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageY:t.pageY}function D(t){return/^touch/.test(t.type)}function T(t){t.addClass("fc-unselectable").on("selectstart",H)}function C(t){t.removeClass("fc-unselectable").off("selectstart",H)}function H(t){t.preventDefault()}function R(t,e){var n={left:Math.max(t.left,e.left),right:Math.min(t.right,e.right),top:Math.max(t.top,e.top),bottom:Math.min(t.bottom,e.bottom)};return n.left<n.right&&n.top<n.bottom&&n}function x(t,e){return{left:Math.min(Math.max(t.left,e.left),e.right),top:Math.min(Math.max(t.top,e.top),e.bottom)}}function I(t){return{left:(t.left+t.right)/2,top:(t.top+t.bottom)/2}}function k(t,e){return{left:t.left-e.left,top:t.top-e.top}}function L(e){var n,i,r=[],s=[];for("string"==typeof e?s=e.split(/\s*,\s*/):"function"==typeof e?s=[e]:t.isArray(e)&&(s=e),n=0;n<s.length;n++)i=s[n],"string"==typeof i?r.push("-"==i.charAt(0)?{field:i.substring(1),order:-1}:{field:i,order:1}):"function"==typeof i&&r.push({func:i});return r}function M(t,e,n){var i,r;for(i=0;i<n.length;i++)if(r=B(t,e,n[i]))return r;return 0}function B(t,e,n){return n.func?n.func(t,e):F(t[n.field],e[n.field])*(n.order||1)}function F(e,n){return e||n?null==n?-1:null==e?1:"string"===t.type(e)||"string"===t.type(n)?String(e).localeCompare(String(n)):e-n:0}function N(t,e){var n,i,r,s,o=t.start,l=t.end,a=e.start,u=e.end;if(l>a&&o<u)return o>=a?(n=o.clone(),r=!0):(n=a.clone(),r=!1),l<=u?(i=l.clone(),s=!0):(i=u.clone(),s=!1),{start:n,end:i,isStart:r,isEnd:s}}function z(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days"),ms:t.time()-n.time()})}function A(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days")})}function G(t,n,i){return e.duration(Math.round(t.diff(n,i,!0)),i)}function O(t,e){var n,i,r;for(n=0;n<Jt.length&&(i=Jt[n],r=P(i,t,e),!(r>=1&&vt(r)));n++);return i}function V(t,e){var n=O(t);return"week"===n&&"object"==typeof e&&e.days&&(n="day"),n}function P(t,n,i){return null!=i?i.diff(n,t,!0):e.isDuration(n)?n.as(t):n.end.diff(n.start,t,!0)}function _(t,e,n){var i;return tt(n)?(e-t)/n:(i=n.asMonths(),Math.abs(i)>=1&&vt(i)?e.diff(t,"months",!0)/i:e.diff(t,"days",!0)/n.asDays())}function Y(t,e){var n,i;return tt(t)||tt(e)?t/e:(n=t.asMonths(),i=e.asMonths(),Math.abs(n)>=1&&vt(n)&&Math.abs(i)>=1&&vt(i)?n/i:t.asDays()/e.asDays())}function W(t,n){var i;return tt(t)?e.duration(t*n):(i=t.asMonths(),Math.abs(i)>=1&&vt(i)?e.duration({months:i*n}):e.duration({days:t.asDays()*n}))}function U(t){return{start:t.start.clone(),end:t.end.clone()}}function j(t,e){return t=U(t),e.start&&(t.start=q(t.start,e)),e.end&&(t.end=K(t.end,e.end)),t}function q(t,e){return t=t.clone(),e.start&&(t=J(t,e.start)),e.end&&t>=e.end&&(t=e.end.clone().subtract(1)),t}function Z(t,e){return(!e.start||t>=e.start)&&(!e.end||t<e.end)}function $(t,e){return(!e.start||t.end>=e.start)&&(!e.end||t.start<e.end)}function Q(t,e){return(!e.start||t.start>=e.start)&&(!e.end||t.end<=e.end)}function X(t,e){return(t.start&&e.start&&t.start.isSame(e.start)||!t.start&&!e.start)&&(t.end&&e.end&&t.end.isSame(e.end)||!t.end&&!e.end)}function K(t,e){return(t.isBefore(e)?t:e).clone()}function J(t,e){return(t.isAfter(e)?t:e).clone()}function tt(t){return Boolean(t.hours()||t.minutes()||t.seconds()||t.milliseconds())}function et(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function nt(t){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(t)}function it(t,e){var n,i,r,s,o,l,a={};if(e)for(n=0;n<e.length;n++){for(i=e[n],r=[],s=t.length-1;s>=0;s--)if(o=t[s][i],"object"==typeof o)r.unshift(o);else if(void 0!==o){a[i]=o;break}r.length&&(a[i]=it(r))}for(n=t.length-1;n>=0;n--){l=t[n];for(i in l)i in a||(a[i]=l[i])}return a}function rt(t){var e=function(){};return e.prototype=t,new e}function st(t,e){for(var n in t)ot(t,n)&&(e[n]=t[n])}function ot(t,e){return te.call(t,e)}function lt(e){return/undefined|null|boolean|number|string/.test(t.type(e))}function at(e,n,i){if(t.isFunction(e)&&(e=[e]),e){var r,s;for(r=0;r<e.length;r++)s=e[r].apply(n,i)||s;return s}}function ut(){for(var t=0;t<arguments.length;t++)if(void 0!==arguments[t])return arguments[t]}function ct(t){return(t+"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/'/g,"&#039;").replace(/"/g,"&quot;").replace(/\n/g,"<br />")}function dt(t){return t.replace(/&.*?;/g,"")}function ht(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+":"+e)}),n.join(";")}function ft(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+'="'+ct(e)+'"')}),n.join(" ")}function gt(t){return t.charAt(0).toUpperCase()+t.slice(1)}function pt(t,e){return t-e}function vt(t){return t%1===0}function mt(t,e){var n=t[e];return function(){return n.apply(t,arguments)}}function yt(t,e,n){var i,r,s,o,l,a=function(){var u=+new Date-o;u<e?i=setTimeout(a,e-u):(i=null,n||(l=t.apply(s,r),s=r=null))};return function(){s=this,r=arguments,o=+new Date;var u=n&&!i;return i||(i=setTimeout(a,e)),u&&(l=t.apply(s,r),s=r=null),l}}function St(n,i,r){var s,o,l,a,u=n[0],c=1==n.length&&"string"==typeof u;return e.isMoment(u)||et(u)||void 0===u?a=e.apply(null,n):(s=!1,o=!1,c?ee.test(u)?(u+="-01",n=[u],s=!0,o=!0):(l=ne.exec(u))&&(s=!l[5],o=!0):t.isArray(u)&&(o=!0),a=i||s?e.utc.apply(e,n):e.apply(null,n),s?(a._ambigTime=!0,a._ambigZone=!0):r&&(o?a._ambigZone=!0:c&&a.utcOffset(u))),a._fullCalendar=!0,a}function wt(){}function bt(t,e){var n;return ot(e,"constructor")&&(n=e.constructor),"function"!=typeof n&&(n=e.constructor=function(){t.apply(this,arguments)}),n.prototype=rt(t.prototype),st(e,n.prototype),st(t,n),n}function Et(t,e){st(e,t.prototype)}function Dt(e){var n=t.Deferred(),i=n.promise();if("function"==typeof e&&e(function(t){Dt.immediate&&(i._value=t),n.resolve(t)},function(){n.reject()}),Dt.immediate){var r=i.then;i.then=function(t,e){var n=i.state();if("resolved"===n){if("function"==typeof t)return Dt.resolve(t(i._value))}else if("rejected"===n&&"function"==typeof e)return e(),i;return r.call(i,t,e)}}return i}function Tt(t){function e(t){return new Dt(function(e){var i=function(){Dt.resolve(t()).then(e).then(function(){n.shift(),n.length&&n[0]()})};n.push(i),1===n.length&&i()})}var n=[];this.add="number"==typeof t?yt(e,t):e,this.addQuickly=e}function Ct(t,e){return!t&&!e||!(!t||!e)&&(t.component===e.component&&Ht(t,e)&&Ht(e,t))}function Ht(t,e){for(var n in t)if(!/^(component|left|right|top|bottom)$/.test(n)&&t[n]!==e[n])return!1;return!0}function Rt(t){return{start:t.start.clone(),end:t.end?t.end.clone():null,allDay:t.allDay}}function xt(t){var e=kt(t);return"background"===e||"inverse-background"===e}function It(t){return"inverse-background"===kt(t)}function kt(t){return ut((t.source||{}).rendering,t.rendering)}function Lt(t){var e,n,i={};for(e=0;e<t.length;e++)n=t[e],(i[n._id]||(i[n._id]=[])).push(n);return i}function Mt(t,e){return t.start-e.start}function Bt(n){var i,r,s,o,l=Zt.dataAttrPrefix;return l&&(l+="-"),i=n.data(l+"event")||null,i&&(i="object"==typeof i?t.extend({},i):{},r=i.start,null==r&&(r=i.time),s=i.duration,o=i.stick,delete i.start,delete i.time,delete i.duration,delete i.stick),null==r&&(r=n.data(l+"start")),null==r&&(r=n.data(l+"time")),null==s&&(s=n.data(l+"duration")),null==o&&(o=n.data(l+"stick")),r=null!=r?e.duration(r):null,s=null!=s?e.duration(s):null,o=Boolean(o),{eventProps:i,startTime:r,duration:s,stick:o}}function Ft(t,e){var n,i;for(n=0;n<e.length;n++)if(i=e[n],i.leftCol<=t.rightCol&&i.rightCol>=t.leftCol)return!0;return!1}function Nt(t,e){return t.leftCol-e.leftCol}function zt(t){var e,n,i,r=[];for(e=0;e<t.length;e++){for(n=t[e],i=0;i<r.length&&Ot(n,r[i]).length;i++);n.level=i,(r[i]||(r[i]=[])).push(n)}return r}function At(t){var e,n,i,r,s;for(e=0;e<t.length;e++)for(n=t[e],i=0;i<n.length;i++)for(r=n[i],r.forwardSegs=[],s=e+1;s<t.length;s++)Ot(r,t[s],r.forwardSegs)}function Gt(t){var e,n,i=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;e<i.length;e++)n=i[e],Gt(n),r=Math.max(r,1+n.forwardPressure);t.forwardPressure=r}}function Ot(t,e,n){n=n||[];for(var i=0;i<e.length;i++)Vt(t,e[i])&&n.push(e[i]);return n}function Vt(t,e){return t.bottom>e.top&&t.top<e.bottom}function Pt(t){this.items=t||[]}function _t(e,n){function i(t){n=t}function r(){var i=n.layout;p=e.options.theme?"ui":"fc",i?(g?g.empty():g=this.el=t("<div class='fc-toolbar "+n.extraClasses+"'/>"),g.append(o("left")).append(o("right")).append(o("center")).append('<div class="fc-clear"/>')):s()}function s(){g&&(g.remove(),g=f.el=null)}function o(i){var r=t('<div class="fc-'+i+'"/>'),s=n.layout[i];return s&&t.each(s.split(" "),function(n){var i,s=t(),o=!0;t.each(this.split(","),function(n,i){var r,l,a,u,c,d,h,f,g,m;"title"==i?(s=s.add(t("<h2>&nbsp;</h2>")),o=!1):((r=(e.options.customButtons||{})[i])?(a=function(t){r.click&&r.click.call(m[0],t)},u="",c=r.text):(l=e.getViewSpec(i))?(a=function(){e.changeView(i)},v.push(i),u=l.buttonTextOverride,c=l.buttonTextDefault):e[i]&&(a=function(){e[i]()},u=(e.overrides.buttonText||{})[i],c=e.options.buttonText[i]),a&&(d=r?r.themeIcon:e.options.themeButtonIcons[i],h=r?r.icon:e.options.buttonIcons[i],f=u?ct(u):d&&e.options.theme?"<span class='ui-icon ui-icon-"+d+"'></span>":h&&!e.options.theme?"<span class='fc-icon fc-icon-"+h+"'></span>":ct(c),g=["fc-"+i+"-button",p+"-button",p+"-state-default"],m=t('<button type="button" class="'+g.join(" ")+'">'+f+"</button>").click(function(t){m.hasClass(p+"-state-disabled")||(a(t),(m.hasClass(p+"-state-active")||m.hasClass(p+"-state-disabled"))&&m.removeClass(p+"-state-hover"))}).mousedown(function(){m.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-down")}).mouseup(function(){m.removeClass(p+"-state-down")}).hover(function(){m.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-hover")},function(){m.removeClass(p+"-state-hover").removeClass(p+"-state-down")}),s=s.add(m)))}),o&&s.first().addClass(p+"-corner-left").end().last().addClass(p+"-corner-right").end(),s.length>1?(i=t("<div/>"),o&&i.addClass("fc-button-group"),i.append(s),r.append(i)):r.append(s)}),r}function l(t){g&&g.find("h2").text(t)}function a(t){g&&g.find(".fc-"+t+"-button").addClass(p+"-state-active")}function u(t){g&&g.find(".fc-"+t+"-button").removeClass(p+"-state-active")}function c(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!0).addClass(p+"-state-disabled")}function d(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!1).removeClass(p+"-state-disabled")}function h(){return v}var f=this;f.setToolbarOptions=i,f.render=r,f.removeElement=s,f.updateTitle=l,f.activateButton=a,f.deactivateButton=u,f.disableButton=c,f.enableButton=d,f.getViewsWithButtons=h,f.el=null;var g,p,v=[]}function Yt(n,i){function r(t){t._locale=N}function s(){O?a()&&(f(),u()):o()}function o(){n.addClass("fc"),n.on("click.fc","a[data-goto]",function(e){var n=t(this),i=n.data("goto"),r=F.moment(i.date),s=i.type,o=P.opt("navLink"+gt(s)+"Click");"function"==typeof o?o(r,e):("string"==typeof o&&(s=o),C(r,s))}),F.bindOption("theme",function(t){V=t?"ui":"fc",n.toggleClass("ui-widget",t),n.toggleClass("fc-unthemed",!t)}),F.bindOptions(["isRTL","locale"],function(t){n.toggleClass("fc-ltr",!t),n.toggleClass("fc-rtl",t)}),O=t("<div class='fc-view-container'/>").prependTo(n);var e=y();z=new Pt(e),A=F.header=e[0],G=F.footer=e[1],b(),E(),u(F.options.defaultView),F.options.handleWindowResize&&(Y=yt(v,F.options.windowResizeDelay),t(window).resize(Y))}function l(){P&&P.removeElement(),z.proxyCall("removeElement"),O.remove(),n.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),n.off(".fc"),Y&&t(window).unbind("resize",Y),pe.unneeded()}function a(){return n.is(":visible")}function u(e,n){j++;var i=P&&e&&P.type!==e;i&&(H(),c()),!P&&e&&(P=F.view=U[e]||(U[e]=F.instantiateView(e)),P.setElement(t("<div class='fc-view fc-"+e+"-view' />").appendTo(O)),z.proxyCall("activateButton",e)),P&&a()&&(n&&P.captureInitialScroll(n),P.setDate(F.currentDate),F.currentDate=P.currentDate,n&&P.releaseScroll()),i&&R(),j--}function c(){z.proxyCall("deactivateButton",P.type),P.removeElement(),P=F.view=null}function d(){j++,H();var t=P.type,e=P.queryScroll();c(),f(),u(t,e),R(),j--}function h(t){if(a())return t&&g(),j++,P.updateSize(!0),j--,!0}function f(){a()&&g()}function g(){var t=F.options.contentHeight,e=F.options.height;_="number"==typeof t?t:"function"==typeof t?t():"number"==typeof e?e-p():"function"==typeof e?e()-p():"parent"===e?n.parent().height()-p():Math.round(O.width()/Math.max(F.options.aspectRatio,.5))}function p(){return z.items.reduce(function(t,e){var n=e.el?e.el.outerHeight(!0):0;return t+n},0)}function v(t){!j&&t.target===window&&P.renderRange&&h(!0)&&P.publiclyTrigger("windowResize",W)}function m(){a()&&F.reportEventChange()}function y(){return[new _t(F,S()),new _t(F,w())]}function S(){return{extraClasses:"fc-header-toolbar",layout:F.options.header}}function w(){return{extraClasses:"fc-footer-toolbar",layout:F.options.footer}}function b(){A.setToolbarOptions(S()),A.render(),A.el&&n.prepend(A.el)}function E(){G.setToolbarOptions(w()),G.render(),G.el&&n.append(G.el)}function D(t,e){P.select(F.buildSelectSpan.apply(F,arguments))}function T(){P&&P.unselect()}function C(t,e){var n;e=e||"day",n=F.getViewSpec(e)||F.getUnitViewSpec(e),F.currentDate=t.clone(),u(n?n.type:null)}function H(){q++||O.css({width:"100%",height:O.height(),overflow:"hidden"})}function R(){--q||O.css({width:"",height:"",overflow:""})}function x(){return F}function I(){return P}function k(t,e){var n;if("string"==typeof t){if(void 0===e)return F.options[t];n={},n[t]=e,L(n)}else"object"==typeof t&&L(t)}function L(t){var e,n=0;M(t);for(e in t)n++;if(1===n){if("height"===e||"contentHeight"===e||"aspectRatio"===e)return void h(!0);if("defaultDate"===e)return;if("businessHours"===e)return void(P&&(P.unrenderBusinessHours(),P.renderBusinessHours()));if("timezone"===e)return F.rezoneArrayEventSources(),void F.refetchEvents()}b(),E(),U={},d()}function M(t){var e;for(e in t)F.dynamicOverrides[e]=t[e];F.viewSpecCache={},F.populateOptionsHash();for(e in t)F.triggerOptionHandlers(e)}function B(t,e){var n=Array.prototype.slice.call(arguments,2);if(e=e||W,this.triggerWith(t,e,n),F.options[t])return F.options[t].apply(e,n)}var F=this;pe.needed(),F.render=s,F.destroy=l,F.rerenderEvents=m,F.select=D,F.unselect=T,F.zoomTo=C,F.getCalendar=x,F.getView=I,F.option=k,F.recordOptionOverrides=M,F.publiclyTrigger=B,F.dynamicOverrides={},F.viewSpecCache={},F.optionHandlers={},F.overrides=t.extend({},i),F.populateOptionsHash();var N;F.bindOptions(["locale","monthNames","monthNamesShort","dayNames","dayNamesShort","firstDay","weekNumberCalculation"],function(t,e,n,i,s,o,l){if("iso"===l&&(l="ISO"),N=rt(Ut(t)),e&&(N._months=e),n&&(N._monthsShort=n),i&&(N._weekdays=i),s&&(N._weekdaysShort=s),null==o&&"ISO"===l&&(o=1),null!=o){var a=rt(N._week);a.dow=o,N._week=a}"ISO"!==l&&"local"!==l&&"function"!=typeof l||(N._fullCalendar_weekCalc=l),F.currentDate&&r(F.currentDate)}),F.defaultAllDayEventDuration=e.duration(F.options.defaultAllDayEventDuration),F.defaultTimedEventDuration=e.duration(F.options.defaultTimedEventDuration),F.moment=function(){var t;return"local"===F.options.timezone?(t=Zt.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===F.options.timezone?Zt.moment.utc.apply(null,arguments):Zt.moment.parseZone.apply(null,arguments),r(t),t},F.localizeMoment=r,F.getIsAmbigTimezone=function(){return"local"!==F.options.timezone&&"UTC"!==F.options.timezone},F.applyTimezone=function(t){if(!t.hasTime())return t.clone();var e,n=F.moment(t.toArray()),i=t.time()-n.time();return i&&(e=n.clone().add(i),t.time()-e.time()===0&&(n=e)),n},F.getNow=function(){var t=F.options.now;return"function"==typeof t&&(t=t()),F.moment(t).stripZone()},F.getEventEnd=function(t){return t.end?t.end.clone():F.getDefaultEventEnd(t.allDay,t.start)},F.getDefaultEventEnd=function(t,e){var n=e.clone();return t?n.stripTime().add(F.defaultAllDayEventDuration):n.add(F.defaultTimedEventDuration),F.getIsAmbigTimezone()&&n.stripZone(),n},F.humanizeDuration=function(t){return t.locale(F.options.locale).humanize()},jt.call(F);var z,A,G,O,V,P,_,Y,W=n[0],U={},j=0;this.initCurrentDate(),F.renderView=u,F.getSuggestedViewHeight=function(){return void 0===_&&f(),_},F.isHeightAuto=function(){return"auto"===F.options.contentHeight||"auto"===F.options.height},F.setToolbarsTitle=function(t){z.proxyCall("updateTitle",t)},F.updateToolbarButtons=function(){var t=F.getNow(),e=P.buildDateProfile(t),n=P.buildPrevDateProfile(F.currentDate),i=P.buildNextDateProfile(F.currentDate);z.proxyCall(e.isValid&&!Z(t,P.currentRange)?"enableButton":"disableButton","today"),z.proxyCall(n.isValid?"enableButton":"disableButton","prev"),z.proxyCall(i.isValid?"enableButton":"disableButton","next")},F.freezeContentHeight=H,F.thawContentHeight=R;var q=0;F.initialize()}function Wt(e){t.each(Re,function(t,n){null==e[t]&&(e[t]=n(e))})}function Ut(t){return e.localeData(t)||e.localeData("en")}function jt(){function n(t,e){return!U.options.lazyFetching||s(t,e)?o(t,e):Dt.resolve(Z)}function i(){Z=r(K),U.trigger("eventsReset",Z)}function r(t){var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],n.start.clone().stripZone()<q&&U.getEventEnd(n).stripZone()>j&&i.push(n);return i}function s(t,e){return!j||t<j||e>q}function o(t,e){return j=t,q=e,l()}function l(){return u(Q,"reset")}function a(t){return u(b(t))}function u(t,e){var n,i;for("reset"===e?K=[]:"add"!==e&&(K=C(K,t)),n=0;n<t.length;n++)i=t[n],"pending"!==i._status&&X++,i._fetchId=(i._fetchId||0)+1,i._status="pending";for(n=0;n<t.length;n++)i=t[n],c(i,i._fetchId);return X?new Dt(function(t){U.one("eventsReceived",t)}):Dt.resolve(Z)}function c(e,n){f(e,function(i){var r,s,o,l=t.isArray(e.events);if(n===e._fetchId&&"rejected"!==e._status){if(e._status="resolved",i)for(r=0;r<i.length;r++)s=i[r],o=l?s:N(s,e),o&&K.push.apply(K,_(o));h()}})}function d(t){var e="pending"===t._status;t._status="rejected",e&&h()}function h(){X--,X||(i(K),U.trigger("eventsReceived",Z))}function f(e,n){var i,r,s=Zt.sourceFetchers;for(i=0;i<s.length;i++){if(r=s[i].call(U,e,j.clone(),q.clone(),U.options.timezone,n),r===!0)return;if("object"==typeof r)return void f(r,n)}var o=e.events;if(o)t.isFunction(o)?(U.pushLoading(),o.call(U,j.clone(),q.clone(),U.options.timezone,function(t){n(t),U.popLoading()})):t.isArray(o)?n(o):n();else{var l=e.url;if(l){var a,u=e.success,c=e.error,d=e.complete;a=t.isFunction(e.data)?e.data():e.data;var h=t.extend({},a||{}),g=ut(e.startParam,U.options.startParam),p=ut(e.endParam,U.options.endParam),v=ut(e.timezoneParam,U.options.timezoneParam);g&&(h[g]=j.format()),p&&(h[p]=q.format()),U.options.timezone&&"local"!=U.options.timezone&&(h[v]=U.options.timezone),U.pushLoading(),t.ajax(t.extend({},xe,e,{data:h,success:function(e){e=e||[];var i=at(u,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){at(c,this,arguments),n()},complete:function(){at(d,this,arguments),U.popLoading()}}))}else n()}}function g(t){var e=p(t);e&&(Q.push(e),u([e],"add"))}function p(e){var n,i,r=Zt.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?n={events:e}:"string"==typeof e?n={url:e}:"object"==typeof e&&(n=t.extend({},e)),n){for(n.className?"string"==typeof n.className&&(n.className=n.className.split(/\s+/)):n.className=[],t.isArray(n.events)&&(n.origArray=n.events,n.events=t.map(n.events,function(t){return N(t,n)})),i=0;i<r.length;i++)r[i].call(U,n);return n}}function v(t){y(E(t))}function m(t){null==t?y(Q,!0):y(b(t))}function y(e,n){var r;for(r=0;r<e.length;r++)d(e[r]);n?(Q=[],K=[]):(Q=t.grep(Q,function(t){for(r=0;r<e.length;r++)if(t===e[r])return!1;return!0}),K=C(K,e)),i()}function S(){return Q.slice(1)}function w(e){return t.grep(Q,function(t){return t.id&&t.id===e})[0]}function b(e){e?t.isArray(e)||(e=[e]):e=[];var n,i=[];for(n=0;n<e.length;n++)i.push.apply(i,E(e[n]));return i}function E(e){var n,i;for(n=0;n<Q.length;n++)if(i=Q[n],i===e)return[i];return i=w(e),i?[i]:t.grep(Q,function(t){return D(e,t)})}function D(t,e){return t&&e&&T(t)==T(e)}function T(t){return("object"==typeof t?t.origArray||t.googleCalendarId||t.url||t.events:null)||t}function C(e,n){return t.grep(e,function(t){for(var e=0;e<n.length;e++)if(t.source===n[e])return!1;return!0})}function H(t){R([t])}function R(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.start=U.moment(n.start),n.end?n.end=U.moment(n.end):n.end=null,Y(n,x(n));i()}function x(e){var n={};return t.each(e,function(t,e){I(t)&&void 0!==e&&lt(e)&&(n[t]=e)}),n}function I(t){return!/^_|^(id|allDay|start|end)$/.test(t)}function k(t,e){return L([t],e)}function L(t,e){var n,r,s,o,l,a=[];for(s=0;s<t.length;s++)if(r=N(t[s])){for(n=_(r),o=0;o<n.length;o++)l=n[o],l.source||(e&&($.events.push(l),l.source=$),K.push(l));a=a.concat(n)}return a.length&&i(),a}function M(e){var n,r;for(null==e?e=function(){return!0}:t.isFunction(e)||(n=e+"",e=function(t){return t._id==n}),K=t.grep(K,e,!0),r=0;r<Q.length;r++)t.isArray(Q[r].events)&&(Q[r].events=t.grep(Q[r].events,e,!0));i()}function B(e){return t.isFunction(e)?t.grep(K,e):null!=e?(e+="",t.grep(K,function(t){return t._id==e})):K}function F(t){t.start=U.moment(t.start),t.end&&(t.end=U.moment(t.end)),qt(t)}function N(n,i){var r,s,o,l={};if(U.options.eventDataTransform&&(n=U.options.eventDataTransform(n)),i&&i.eventDataTransform&&(n=i.eventDataTransform(n)),t.extend(l,n),i&&(l.source=i),l._id=n._id||(void 0===n.id?"_fc"+Ie++:n.id+""),n.className?"string"==typeof n.className?l.className=n.className.split(/\s+/):l.className=n.className:l.className=[],r=n.start||n.date,s=n.end,nt(r)&&(r=e.duration(r)),nt(s)&&(s=e.duration(s)),n.dow||e.isDuration(r)||e.isDuration(s))l.start=r?e.duration(r):null,l.end=s?e.duration(s):null,l._recurring=!0;else{if(r&&(r=U.moment(r),!r.isValid()))return!1;s&&(s=U.moment(s),s.isValid()||(s=null)),o=n.allDay,void 0===o&&(o=ut(i?i.allDayDefault:void 0,U.options.allDayDefault)),O(r,s,o,l)}return U.normalizeEvent(l),l}function O(t,e,n,i){i.start=t,i.end=e,i.allDay=n,V(i),qt(i)}function V(t){P(t),t.end&&!t.end.isAfter(t.start)&&(t.end=null),t.end||(U.options.forceEventDuration?t.end=U.getDefaultEventEnd(t.allDay,t.start):t.end=null)}function P(t){null==t.allDay&&(t.allDay=!(t.start.hasTime()||t.end&&t.end.hasTime())),t.allDay?(t.start.stripTime(),t.end&&t.end.stripTime()):(t.start.hasTime()||(t.start=U.applyTimezone(t.start.time(0))),t.end&&!t.end.hasTime()&&(t.end=U.applyTimezone(t.end.time(0))))}function _(e,n,i){var r,s,o,l,a,u,c,d,h,f=[];if(n=n||j,i=i||q,e)if(e._recurring){if(s=e.dow)for(r={},o=0;o<s.length;o++)r[s[o]]=!0;for(l=n.clone().stripTime();l.isBefore(i);)r&&!r[l.day()]||(a=e.start,u=e.end,c=l.clone(),d=null,a&&(c=c.time(a)),u&&(d=l.clone().time(u)),h=t.extend({},e),O(c,d,!a&&!u,h),f.push(h)),l.add(1,"days")}else f.push(e);return f}function Y(e,n,i){function r(t,e){return i?G(t,e,i):n.allDay?A(t,e):z(t,e)}var s,o,l,a,u,c,d={};return n=n||{},n.start||(n.start=e.start.clone()),void 0===n.end&&(n.end=e.end?e.end.clone():null),null==n.allDay&&(n.allDay=e.allDay),V(n),s={start:e._start.clone(),end:e._end?e._end.clone():U.getDefaultEventEnd(e._allDay,e._start),allDay:n.allDay},V(s),o=null!==e._end&&null===n.end,l=r(n.start,s.start),n.end?(a=r(n.end,s.end),u=a.subtract(l)):u=null,t.each(n,function(t,e){I(t)&&void 0!==e&&(d[t]=e)}),c=W(B(e._id),o,n.allDay,l,u,d),{dateDelta:l,durationDelta:u,undo:c}}function W(e,n,i,r,s,o){var l=U.getIsAmbigTimezone(),a=[];return r&&!r.valueOf()&&(r=null),s&&!s.valueOf()&&(s=null),t.each(e,function(e,u){var c,d;c={start:u.start.clone(),end:u.end?u.end.clone():null,allDay:u.allDay},t.each(o,function(t){c[t]=u[t]}),d={start:u._start,end:u._end,allDay:i},V(d),n?d.end=null:s&&!d.end&&(d.end=U.getDefaultEventEnd(d.allDay,d.start)),r&&(d.start.add(r),d.end&&d.end.add(r)),s&&d.end.add(s),l&&!d.allDay&&(r||s)&&(d.start.stripZone(),d.end&&d.end.stripZone()),t.extend(u,o,d),qt(u),a.push(function(){t.extend(u,c),qt(u)})}),function(){for(var t=0;t<a.length;t++)a[t]()}}var U=this;U.requestEvents=n,U.reportEventChange=i,U.isFetchNeeded=s,U.fetchEvents=o,U.fetchEventSources=u,U.refetchEvents=l,U.refetchEventSources=a,U.getEventSources=S,U.getEventSourceById=w,U.addEventSource=g,U.removeEventSource=v,U.removeEventSources=m,U.updateEvent=H,U.updateEvents=R,U.renderEvent=k,U.renderEvents=L,U.removeEvents=M,U.clientEvents=B,U.mutateEvent=Y,U.normalizeEventDates=V,U.normalizeEventTimes=P;var j,q,Z,$={events:[]},Q=[$],X=0,K=[];t.each((U.options.events?[U.options.events]:[]).concat(U.options.eventSources||[]),function(t,e){var n=p(e);n&&Q.push(n)}),U.getEventCache=function(){return K},U.getPrunedEventCache=function(){return Z},U.rezoneArrayEventSources=function(){var e,n,i;for(e=0;e<Q.length;e++)if(n=Q[e].events,t.isArray(n))for(i=0;i<n.length;i++)F(n[i])},U.buildEventFromInput=N,U.expandEvent=_}function qt(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}var Zt=t.fullCalendar={version:"3.3.1",internalApiVersion:9},$t=Zt.views={};t.fn.fullCalendar=function(e){var n=Array.prototype.slice.call(arguments,1),i=this;return this.each(function(r,s){var o,l=t(s),a=l.data("fullCalendar");"string"==typeof e?a&&t.isFunction(a[e])&&(o=a[e].apply(a,n),r||(i=o),"destroy"===e&&l.removeData("fullCalendar")):a||(a=new De(l,e),l.data("fullCalendar",a),a.render())}),i};var Qt=["header","footer","buttonText","buttonIcons","themeButtonIcons"];Zt.intersectRanges=N,Zt.applyAll=at,Zt.debounce=yt,Zt.isInt=vt,Zt.htmlEscape=ct,Zt.cssToStr=ht,Zt.proxy=mt,Zt.capitaliseFirstLetter=gt,Zt.getOuterRect=h,Zt.getClientRect=f,Zt.getContentRect=g,Zt.getScrollbarWidths=p;var Xt=null;Zt.preventDefault=H,Zt.intersectRects=R,Zt.parseFieldSpecs=L,Zt.compareByFieldSpecs=M,Zt.compareByFieldSpec=B,Zt.flexibleCompare=F,Zt.computeGreatestUnit=O,Zt.divideRangeByDuration=_,Zt.divideDurationByDuration=Y,Zt.multiplyDuration=W,Zt.durationHasTime=tt;var Kt=["sun","mon","tue","wed","thu","fri","sat"],Jt=["year","month","week","day","hour","minute","second","millisecond"];Zt.log=function(){var t=window.console;if(t&&t.log)return t.log.apply(t,arguments)},Zt.warn=function(){var t=window.console;return t&&t.warn?t.warn.apply(t,arguments):Zt.log.apply(Zt,arguments)};var te={}.hasOwnProperty;Zt.createObject=rt;var ee=/^\s*\d{4}-\d\d$/,ne=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/,ie=e.fn,re=t.extend({},ie),se=e.momentProperties;se.push("_fullCalendar"),se.push("_ambigTime"),se.push("_ambigZone"),Zt.moment=function(){return St(arguments)},Zt.moment.utc=function(){var t=St(arguments,!0);return t.hasTime()&&t.utc(),t},Zt.moment.parseZone=function(){return St(arguments,!0,!0)},ie.week=ie.weeks=function(t){var e=this._locale._fullCalendar_weekCalc;return null==t&&"function"==typeof e?e(this):"ISO"===e?re.isoWeek.apply(this,arguments):re.week.apply(this,arguments)},ie.time=function(t){if(!this._fullCalendar)return re.time.apply(this,arguments);if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});this._ambigTime=!1,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var n=0;return e.isDuration(t)&&(n=24*Math.floor(t.asDays())),this.hours(n+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},ie.stripTime=function(){return this._ambigTime||(this.utc(!0),this.set({hours:0,minutes:0,seconds:0,ms:0}),this._ambigTime=!0,this._ambigZone=!0),this},ie.hasTime=function(){return!this._ambigTime},ie.stripZone=function(){var t;return this._ambigZone||(t=this._ambigTime,this.utc(!0),this._ambigTime=t||!1,this._ambigZone=!0),this},ie.hasZone=function(){return!this._ambigZone},ie.local=function(t){return re.local.call(this,this._ambigZone||t),this._ambigTime=!1,this._ambigZone=!1,this},ie.utc=function(t){return re.utc.call(this,t),this._ambigTime=!1,this._ambigZone=!1,this},ie.utcOffset=function(t){return null!=t&&(this._ambigTime=!1,this._ambigZone=!1),re.utcOffset.apply(this,arguments)},ie.format=function(){return this._fullCalendar&&arguments[0]?oe(this,arguments[0]):this._ambigTime?ae(this,"YYYY-MM-DD"):this._ambigZone?ae(this,"YYYY-MM-DD[T]HH:mm:ss"):re.format.apply(this,arguments)},ie.toISOString=function(){return this._ambigTime?ae(this,"YYYY-MM-DD"):this._ambigZone?ae(this,"YYYY-MM-DD[T]HH:mm:ss"):re.toISOString.apply(this,arguments)},function(){function t(t,e){return c(r(e).fakeFormatString,t)}function e(t,e){return re.format.call(t,e)}function n(t,e,n,s,o){var l;return t=Zt.moment.parseZone(t),e=Zt.moment.parseZone(e),l=t.localeData(),n=l.longDateFormat(n)||n,i(r(n),t,e,s||" - ",o)}function i(t,e,n,i,r){var s,o,l,a=t.sameUnits,u=e.clone().stripZone(),c=n.clone().stripZone(),f=d(t.fakeFormatString,e),g=d(t.fakeFormatString,n),p="",v="",m="",y="",S="";for(s=0;s<a.length&&(!a[s]||u.isSame(c,a[s]));s++)p+=f[s];for(o=a.length-1;o>s&&(!a[o]||u.isSame(c,a[o]))&&(o-1!==s||"."!==f[o]);o--)v=f[o]+v;for(l=s;l<=o;l++)m+=f[l],y+=g[l];return(m||y)&&(S=r?y+i+m:m+i+y),h(p+S+v)}function r(t){return w[t]||(w[t]=s(t))}function s(t){var e=o(t);return{fakeFormatString:a(e),sameUnits:u(e)}}function o(t){for(var e,n=[],i=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=i.exec(t);)e[1]?n.push.apply(n,l(e[1])):e[2]?n.push({maybe:o(e[2])}):e[3]?n.push({token:e[3]}):e[5]&&n.push.apply(n,l(e[5]));return n}function l(t){return". "===t?["."," "]:[t]}function a(t){
+var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],"string"==typeof n?i.push("["+n+"]"):n.token?n.token in y?i.push(p+"["+n.token+"]"):i.push(n.token):n.maybe&&i.push(v+a(n.maybe)+v);return i.join(g)}function u(t){var e,n,i,r=[];for(e=0;e<t.length;e++)n=t[e],n.token?(i=S[n.token.charAt(0)],r.push(i?i.unit:"second")):n.maybe?r.push.apply(r,u(n.maybe)):r.push(null);return r}function c(t,e){return h(d(t,e).join(""))}function d(t,n){var i,r,s=[],o=e(n,t),l=o.split(g);for(i=0;i<l.length;i++)r=l[i],r.charAt(0)===p?s.push(y[r.substring(1)](n)):s.push(r);return s}function h(t){return t.replace(m,function(t,e){return e.match(/[1-9]/)?e:""})}function f(t){var e,n,i,r,s=o(t);for(e=0;e<s.length;e++)n=s[e],n.token&&(i=S[n.token.charAt(0)],i&&(!r||i.value>r.value)&&(r=i));return r?r.unit:null}Zt.formatDate=t,Zt.formatRange=n,Zt.oldMomentFormat=e,Zt.queryMostGranularFormatUnit=f;var g="\v",p="",v="",m=new RegExp(v+"([^"+v+"]*)"+v,"g"),y={t:function(t){return e(t,"a").charAt(0)},T:function(t){return e(t,"A").charAt(0)}},S={Y:{value:1,unit:"year"},M:{value:2,unit:"month"},W:{value:3,unit:"week"},w:{value:3,unit:"week"},D:{value:4,unit:"day"},d:{value:4,unit:"day"}},w={}}();var oe=Zt.formatDate,le=Zt.formatRange,ae=Zt.oldMomentFormat;Zt.Class=wt,wt.extend=function(){var t,e,n=arguments.length;for(t=0;t<n;t++)e=arguments[t],t<n-1&&Et(this,e);return bt(this,e||{})},wt.mixin=function(t){Et(this,t)},Zt.Promise=Dt,Dt.immediate=!0,Dt.resolve=function(e){if(e&&"function"==typeof e.resolve)return e.promise();if(e&&"function"==typeof e.then)return e;var n=t.Deferred().resolve(e),i=n.promise();if(Dt.immediate){var r=i.then;i._value=e,i.then=function(t,n){return"function"==typeof t?Dt.resolve(t(e)):r.call(i,t,n)}}return i},Dt.reject=function(){return t.Deferred().reject().promise()},Dt.all=function(e){var n,i,r,s=!1;if(Dt.immediate)for(s=!0,n=[],i=0;i<e.length;i++)if(r=e[i],r&&"function"==typeof r.state&&"resolved"===r.state()&&"_value"in r)n.push(r._value);else{if(r&&"function"==typeof r.then){s=!1;break}n.push(r)}return s?Dt.resolve(n):t.when.apply(t.when,e).then(function(){return t.when(t.makeArray(arguments))})},Zt.TaskQueue=Tt;var ue=Zt.EmitterMixin={on:function(e,n){return t(this).on(e,this._prepareIntercept(n)),this},one:function(e,n){return t(this).one(e,this._prepareIntercept(n)),this},_prepareIntercept:function(e){var n=function(t,n){return e.apply(n.context||this,n.args||[])};return e.guid||(e.guid=t.guid++),n.guid=e.guid,n},off:function(e,n){return t(this).off(e,n),this},trigger:function(e){var n=Array.prototype.slice.call(arguments,1);return t(this).triggerHandler(e,{args:n}),this},triggerWith:function(e,n,i){return t(this).triggerHandler(e,{context:n,args:i}),this}},ce=Zt.ListenerMixin=function(){var e=0,n={listenerId:null,listenTo:function(e,n,i){if("object"==typeof n)for(var r in n)n.hasOwnProperty(r)&&this.listenTo(e,r,n[r]);else"string"==typeof n&&e.on(n+"."+this.getListenerNamespace(),t.proxy(i,this))},stopListeningTo:function(t,e){t.off((e||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){return null==this.listenerId&&(this.listenerId=e++),"_listener"+this.listenerId}};return n}(),de=wt.extend(ce,{isHidden:!0,options:null,el:null,margin:10,constructor:function(t){this.options=t||{}},show:function(){this.isHidden&&(this.el||this.render(),this.el.show(),this.position(),this.isHidden=!1,this.trigger("show"))},hide:function(){this.isHidden||(this.el.hide(),this.isHidden=!0,this.trigger("hide"))},render:function(){var e=this,n=this.options;this.el=t('<div class="fc-popover"/>').addClass(n.className||"").css({top:0,left:0}).append(n.content).appendTo(n.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),n.autoHide&&this.listenTo(t(document),"mousedown",this.documentMousedown)},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),this.stopListeningTo(t(document),"mousedown")},position:function(){var e,n,i,r,s,o=this.options,l=this.el.offsetParent().offset(),a=this.el.outerWidth(),u=this.el.outerHeight(),c=t(window),h=d(this.el);r=o.top||0,s=void 0!==o.left?o.left:void 0!==o.right?o.right-a:0,h.is(window)||h.is(document)?(h=c,e=0,n=0):(i=h.offset(),e=i.top,n=i.left),e+=c.scrollTop(),n+=c.scrollLeft(),o.viewportConstrain!==!1&&(r=Math.min(r,e+h.outerHeight()-u-this.margin),r=Math.max(r,e+this.margin),s=Math.min(s,n+h.outerWidth()-a-this.margin),s=Math.max(s,n+this.margin)),this.el.css({top:r-l.top,left:s-l.left})},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}}),he=Zt.CoordCache=wt.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:!1,isVertical:!1,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(e){this.els=t(e.els),this.isHorizontal=e.isHorizontal,this.isVertical=e.isVertical,this.forcedOffsetParentEl=e.offsetParent?t(e.offsetParent):null},build:function(){var t=this.forcedOffsetParentEl;!t&&this.els.length>0&&(t=this.els.eq(0).offsetParent()),this.origin=t?t.offset():null,this.boundingRect=this.queryBoundingRect(),this.isHorizontal&&this.buildElHorizontals(),this.isVertical&&this.buildElVerticals()},clear:function(){this.origin=null,this.boundingRect=null,this.lefts=null,this.rights=null,this.tops=null,this.bottoms=null},ensureBuilt:function(){this.origin||this.build()},buildElHorizontals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().left,l=s.outerWidth();e.push(o),n.push(o+l)}),this.lefts=e,this.rights=n},buildElVerticals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().top,l=s.outerHeight();e.push(o),n.push(o+l)}),this.tops=e,this.bottoms=n},getHorizontalIndex:function(t){this.ensureBuilt();var e,n=this.lefts,i=this.rights,r=n.length;for(e=0;e<r;e++)if(t>=n[e]&&t<i[e])return e},getVerticalIndex:function(t){this.ensureBuilt();var e,n=this.tops,i=this.bottoms,r=n.length;for(e=0;e<r;e++)if(t>=n[e]&&t<i[e])return e},getLeftOffset:function(t){return this.ensureBuilt(),this.lefts[t]},getLeftPosition:function(t){return this.ensureBuilt(),this.lefts[t]-this.origin.left},getRightOffset:function(t){return this.ensureBuilt(),this.rights[t]},getRightPosition:function(t){return this.ensureBuilt(),this.rights[t]-this.origin.left},getWidth:function(t){return this.ensureBuilt(),this.rights[t]-this.lefts[t]},getTopOffset:function(t){return this.ensureBuilt(),this.tops[t]},getTopPosition:function(t){return this.ensureBuilt(),this.tops[t]-this.origin.top},getBottomOffset:function(t){return this.ensureBuilt(),this.bottoms[t]},getBottomPosition:function(t){return this.ensureBuilt(),this.bottoms[t]-this.origin.top},getHeight:function(t){return this.ensureBuilt(),this.bottoms[t]-this.tops[t]},queryBoundingRect:function(){var t;return this.els.length>0&&(t=d(this.els.eq(0)),!t.is(document))?f(t):null},isPointInBounds:function(t,e){return this.isLeftInBounds(t)&&this.isTopInBounds(e)},isLeftInBounds:function(t){return!this.boundingRect||t>=this.boundingRect.left&&t<this.boundingRect.right},isTopInBounds:function(t){return!this.boundingRect||t>=this.boundingRect.top&&t<this.boundingRect.bottom}}),fe=Zt.DragListener=wt.extend(ce,{options:null,subjectEl:null,originX:null,originY:null,scrollEl:null,isInteracting:!1,isDistanceSurpassed:!1,isDelayEnded:!1,isDragging:!1,isTouch:!1,isGeneric:!1,delay:null,delayTimeoutId:null,minDistance:null,shouldCancelTouchScroll:!0,scrollAlwaysKills:!1,constructor:function(t){this.options=t||{}},startInteraction:function(e,n){if("mousedown"===e.type){if(pe.get().shouldIgnoreMouse())return;if(!w(e))return;e.preventDefault()}this.isInteracting||(n=n||{},this.delay=ut(n.delay,this.options.delay,0),this.minDistance=ut(n.distance,this.options.distance,0),this.subjectEl=this.options.subjectEl,T(t("body")),this.isInteracting=!0,this.isTouch=D(e),this.isGeneric="dragstart"===e.type,this.isDelayEnded=!1,this.isDistanceSurpassed=!1,this.originX=b(e),this.originY=E(e),this.scrollEl=d(t(e.target)),this.bindHandlers(),this.initAutoScroll(),this.handleInteractionStart(e),this.startDelay(e),this.minDistance||this.handleDistanceSurpassed(e))},handleInteractionStart:function(t){this.trigger("interactionStart",t)},endInteraction:function(e,n){this.isInteracting&&(this.endDrag(e),this.delayTimeoutId&&(clearTimeout(this.delayTimeoutId),this.delayTimeoutId=null),this.destroyAutoScroll(),this.unbindHandlers(),this.isInteracting=!1,this.handleInteractionEnd(e,n),C(t("body")))},handleInteractionEnd:function(t,e){this.trigger("interactionEnd",t,e||!1)},bindHandlers:function(){var e=pe.get();this.isGeneric?this.listenTo(t(document),{drag:this.handleMove,dragstop:this.endInteraction}):this.isTouch?this.listenTo(e,{touchmove:this.handleTouchMove,touchend:this.endInteraction,scroll:this.handleTouchScroll}):this.listenTo(e,{mousemove:this.handleMouseMove,mouseup:this.endInteraction}),this.listenTo(e,{selectstart:H,contextmenu:H})},unbindHandlers:function(){this.stopListeningTo(pe.get()),this.stopListeningTo(t(document))},startDrag:function(t,e){this.startInteraction(t,e),this.isDragging||(this.isDragging=!0,this.handleDragStart(t))},handleDragStart:function(t){this.trigger("dragStart",t)},handleMove:function(t){var e,n=b(t)-this.originX,i=E(t)-this.originY,r=this.minDistance;this.isDistanceSurpassed||(e=n*n+i*i,e>=r*r&&this.handleDistanceSurpassed(t)),this.isDragging&&this.handleDrag(n,i,t)},handleDrag:function(t,e,n){this.trigger("drag",t,e,n),this.updateAutoScroll(n)},endDrag:function(t){this.isDragging&&(this.isDragging=!1,this.handleDragEnd(t))},handleDragEnd:function(t){this.trigger("dragEnd",t)},startDelay:function(t){var e=this;this.delay?this.delayTimeoutId=setTimeout(function(){e.handleDelayEnd(t)},this.delay):this.handleDelayEnd(t)},handleDelayEnd:function(t){this.isDelayEnded=!0,this.isDistanceSurpassed&&this.startDrag(t)},handleDistanceSurpassed:function(t){this.isDistanceSurpassed=!0,this.isDelayEnded&&this.startDrag(t)},handleTouchMove:function(t){this.isDragging&&this.shouldCancelTouchScroll&&t.preventDefault(),this.handleMove(t)},handleMouseMove:function(t){this.handleMove(t)},handleTouchScroll:function(t){this.isDragging&&!this.scrollAlwaysKills||this.endInteraction(t,!0)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1)),this["_"+t]&&this["_"+t].apply(this,Array.prototype.slice.call(arguments,1))}});fe.mixin({isAutoScroll:!1,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var t=this.scrollEl;this.isAutoScroll=this.options.scroll&&t&&!t.is(window)&&!t.is(document),this.isAutoScroll&&this.listenTo(t,"scroll",yt(this.handleDebouncedScroll,100))},destroyAutoScroll:function(){this.endAutoScroll(),this.isAutoScroll&&this.stopListeningTo(this.scrollEl,"scroll")},computeScrollBounds:function(){this.isAutoScroll&&(this.scrollBounds=h(this.scrollEl))},updateAutoScroll:function(t){var e,n,i,r,s=this.scrollSensitivity,o=this.scrollBounds,l=0,a=0;o&&(e=(s-(E(t)-o.top))/s,n=(s-(o.bottom-E(t)))/s,i=(s-(b(t)-o.left))/s,r=(s-(o.right-b(t)))/s,e>=0&&e<=1?l=e*this.scrollSpeed*-1:n>=0&&n<=1&&(l=n*this.scrollSpeed),i>=0&&i<=1?a=i*this.scrollSpeed*-1:r>=0&&r<=1&&(a=r*this.scrollSpeed)),this.setScrollVel(l,a)},setScrollVel:function(t,e){this.scrollTopVel=t,this.scrollLeftVel=e,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(mt(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;this.scrollTopVel<0?t.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?t.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.endAutoScroll()},endAutoScroll:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.handleScrollEnd())},handleDebouncedScroll:function(){this.scrollIntervalId||this.handleScrollEnd()},handleScrollEnd:function(){}});var ge=fe.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(t,e){fe.call(this,e),this.component=t},handleInteractionStart:function(t){var e,n,i,r=this.subjectEl;this.component.hitsNeeded(),this.computeScrollBounds(),t?(n={left:b(t),top:E(t)},i=n,r&&(e=h(r),i=x(i,e)),this.origHit=this.queryHit(i.left,i.top),r&&this.options.subjectCenter&&(this.origHit&&(e=R(this.origHit,e)||e),i=I(e)),this.coordAdjust=k(i,n)):(this.origHit=null,this.coordAdjust=null),fe.prototype.handleInteractionStart.apply(this,arguments)},handleDragStart:function(t){var e;fe.prototype.handleDragStart.apply(this,arguments),e=this.queryHit(b(t),E(t)),e&&this.handleHitOver(e)},handleDrag:function(t,e,n){var i;fe.prototype.handleDrag.apply(this,arguments),i=this.queryHit(b(n),E(n)),Ct(i,this.hit)||(this.hit&&this.handleHitOut(),i&&this.handleHitOver(i))},handleDragEnd:function(){this.handleHitDone(),fe.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(t){var e=Ct(t,this.origHit);this.hit=t,this.trigger("hitOver",this.hit,e,this.origHit)},handleHitOut:function(){this.hit&&(this.trigger("hitOut",this.hit),this.handleHitDone(),this.hit=null)},handleHitDone:function(){this.hit&&this.trigger("hitDone",this.hit)},handleInteractionEnd:function(){fe.prototype.handleInteractionEnd.apply(this,arguments),this.origHit=null,this.hit=null,this.component.hitsNotNeeded()},handleScrollEnd:function(){fe.prototype.handleScrollEnd.apply(this,arguments),this.isDragging&&(this.component.releaseHits(),this.component.prepareHits())},queryHit:function(t,e){return this.coordAdjust&&(t+=this.coordAdjust.left,e+=this.coordAdjust.top),this.component.queryHit(t,e)}});Zt.touchMouseIgnoreWait=500;var pe=wt.extend(ce,ue,{isTouching:!1,mouseIgnoreDepth:0,handleScrollProxy:null,bind:function(){var e=this;this.listenTo(t(document),{touchstart:this.handleTouchStart,touchcancel:this.handleTouchCancel,touchend:this.handleTouchEnd,mousedown:this.handleMouseDown,mousemove:this.handleMouseMove,mouseup:this.handleMouseUp,click:this.handleClick,selectstart:this.handleSelectStart,contextmenu:this.handleContextMenu}),window.addEventListener("touchmove",this.handleTouchMoveProxy=function(n){e.handleTouchMove(t.Event(n))},{passive:!1}),window.addEventListener("scroll",this.handleScrollProxy=function(n){e.handleScroll(t.Event(n))},!0)},unbind:function(){this.stopListeningTo(t(document)),window.removeEventListener("touchmove",this.handleTouchMoveProxy),window.removeEventListener("scroll",this.handleScrollProxy,!0)},handleTouchStart:function(t){this.stopTouch(t,!0),this.isTouching=!0,this.trigger("touchstart",t)},handleTouchMove:function(t){this.isTouching&&this.trigger("touchmove",t)},handleTouchCancel:function(t){this.isTouching&&(this.trigger("touchcancel",t),this.stopTouch(t))},handleTouchEnd:function(t){this.stopTouch(t)},handleMouseDown:function(t){this.shouldIgnoreMouse()||this.trigger("mousedown",t)},handleMouseMove:function(t){this.shouldIgnoreMouse()||this.trigger("mousemove",t)},handleMouseUp:function(t){this.shouldIgnoreMouse()||this.trigger("mouseup",t)},handleClick:function(t){this.shouldIgnoreMouse()||this.trigger("click",t)},handleSelectStart:function(t){this.trigger("selectstart",t)},handleContextMenu:function(t){this.trigger("contextmenu",t)},handleScroll:function(t){this.trigger("scroll",t)},stopTouch:function(t,e){this.isTouching&&(this.isTouching=!1,this.trigger("touchend",t),e||this.startTouchMouseIgnore())},startTouchMouseIgnore:function(){var t=this,e=Zt.touchMouseIgnoreWait;e&&(this.mouseIgnoreDepth++,setTimeout(function(){t.mouseIgnoreDepth--},e))},shouldIgnoreMouse:function(){return this.isTouching||Boolean(this.mouseIgnoreDepth)}});!function(){var t=null,e=0;pe.get=function(){return t||(t=new pe,t.bind()),t},pe.needed=function(){pe.get(),e++},pe.unneeded=function(){e--,e||(t.unbind(),t=null)}}();var ve=wt.extend(ce,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(e,n){this.options=n=n||{},this.sourceEl=e,this.parentEl=n.parentEl?t(n.parentEl):e.parent()},start:function(e){this.isFollowing||(this.isFollowing=!0,this.y0=E(e),this.x0=b(e),this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),D(e)?this.listenTo(t(document),"touchmove",this.handleMove):this.listenTo(t(document),"mousemove",this.handleMove))},stop:function(e,n){function i(){r.isAnimating=!1,r.removeElement(),r.top0=r.left0=null,n&&n()}var r=this,s=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,this.stopListeningTo(t(document)),e&&s&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:s,complete:i})):i())},getEl:function(){var t=this.el;return t||(t=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}),t.addClass("fc-unselectable"),t.appendTo(this.parentEl)),t},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(t){this.topDelta=E(t)-this.y0,this.leftDelta=b(t)-this.x0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),me=Zt.Grid=wt.extend(ce,{hasDayInteractions:!0,view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayClickListener:null,daySelectListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(t){this.view=t,this.isRTL=t.opt("isRTL"),this.elsByFill={},this.dayClickListener=this.buildDayClickListener(),this.daySelectListener=this.buildDaySelectListener()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(t){this.start=t.start.clone(),this.end=t.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var t,e,n=this.view;this.eventTimeFormat=n.opt("eventTimeFormat")||n.opt("timeFormat")||this.computeEventTimeFormat(),t=n.opt("displayEventTime"),null==t&&(t=this.computeDisplayEventTime()),e=n.opt("displayEventEnd"),null==e&&(e=this.computeDisplayEventEnd()),this.displayEventTime=t,this.displayEventEnd=e},spanToSegs:function(t){},diffDates:function(t,e){return this.largeUnit?G(t,e,this.largeUnit):z(t,e)},hitsNeededDepth:0,hitsNeeded:function(){this.hitsNeededDepth++||this.prepareHits()},hitsNotNeeded:function(){this.hitsNeededDepth&&!--this.hitsNeededDepth&&this.releaseHits()},prepareHits:function(){},releaseHits:function(){},queryHit:function(t,e){},getSafeHitSpan:function(t){var e=this.getHitSpan(t);return Q(e,this.view.activeRange)?e:null},getHitSpan:function(t){},getHitEl:function(t){},setElement:function(t){this.el=t,this.hasDayInteractions&&(T(t),this.bindDayHandler("touchstart",this.dayTouchStart),this.bindDayHandler("mousedown",this.dayMousedown)),this.bindSegHandlers(),this.bindGlobalHandlers()},bindDayHandler:function(e,n){var i=this;this.el.on(e,function(e){if(!t(e.target).is(i.segSelector+","+i.segSelector+" *,.fc-more,a[data-goto]"))return n.call(i,e)})},removeElement:function(){this.unbindGlobalHandlers(),this.clearDragListeners(),this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo(t(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo(t(document))},dayMousedown:function(t){var e=this.view;pe.get().shouldIgnoreMouse()||(this.dayClickListener.startInteraction(t),e.opt("selectable")&&this.daySelectListener.startInteraction(t,{distance:e.opt("selectMinDistance")}))},dayTouchStart:function(t){var e,n=this.view;n.isSelected||n.selectedEvent||(e=n.opt("selectLongPressDelay"),null==e&&(e=n.opt("longPressDelay")),this.dayClickListener.startInteraction(t),n.opt("selectable")&&this.daySelectListener.startInteraction(t,{delay:e}))},buildDayClickListener:function(){var t,e=this,n=this.view,i=new ge(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=i.origHit},hitOver:function(e,n,i){n||(t=null)},hitOut:function(){t=null},interactionEnd:function(i,r){var s;!r&&t&&(s=e.getSafeHitSpan(t),s&&n.triggerDayClick(s,e.getHitEl(t),i))}});return i.shouldCancelTouchScroll=!1,i.scrollAlwaysKills=!0,i},buildDaySelectListener:function(){var t,e=this,n=this.view,i=new ge(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=null},dragStart:function(){n.unselect()},hitOver:function(n,i,r){var o,l;r&&(o=e.getSafeHitSpan(r),l=e.getSafeHitSpan(n),t=o&&l?e.computeSelection(o,l):null,t?e.renderSelection(t):t===!1&&s())},hitOut:function(){t=null,e.unrenderSelection()},hitDone:function(){o()},interactionEnd:function(e,i){!i&&t&&n.reportSelection(t,e)}});return i},clearDragListeners:function(){this.dayClickListener.endInteraction(),this.daySelectListener.endInteraction(),this.segDragListener&&this.segDragListener.endInteraction(),this.segResizeListener&&this.segResizeListener.endInteraction(),this.externalDragListener&&this.externalDragListener.endInteraction()},renderEventLocationHelper:function(t,e){var n=this.fabricateHelperEvent(t,e);return this.renderHelper(n,e)},fabricateHelperEvent:function(t,e){var n=e?rt(e.event):{};return n.start=t.start.clone(),n.end=t.end?t.end.clone():null,n.allDay=null,this.view.calendar.normalizeEventDates(n),n.className=(n.className||[]).concat("fc-helper"),e||(n.editable=!1),n},renderHelper:function(t,e){},unrenderHelper:function(){},renderSelection:function(t){this.renderHighlight(t)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(t,e){var n=this.computeSelectionSpan(t,e);return!(n&&!this.view.calendar.isSelectionSpanAllowed(n))&&n},computeSelectionSpan:function(t,e){var n=[t.start,t.end,e.start,e.end];return n.sort(pt),{start:n[0].clone(),end:n[3].clone()}},renderHighlight:function(t){this.renderFill("highlight",this.spanToSegs(t))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},renderFill:function(t,e){},unrenderFill:function(t){var e=this.elsByFill[t];e&&(e.remove(),delete this.elsByFill[t])},renderFillSegEls:function(e,n){var i,r=this,s=this[e+"SegEl"],o="",l=[];if(n.length){for(i=0;i<n.length;i++)o+=this.fillSegHtml(e,n[i]);t(o).each(function(e,i){var o=n[e],a=t(i);s&&(a=s.call(r,o,a)),a&&(a=t(a),a.is(r.fillSegTag)&&(o.el=a,l.push(o)))})}return l},fillSegTag:"div",fillSegHtml:function(t,e){var n=this[t+"SegClasses"],i=this[t+"SegCss"],r=n?n.call(this,e):[],s=ht(i?i.call(this,e):{});return"<"+this.fillSegTag+(r.length?' class="'+r.join(" ")+'"':"")+(s?' style="'+s+'"':"")+" />"},getDayClasses:function(t,e){var n,i=this.view,r=[];return Z(t,i.activeRange)?(r.push("fc-"+Kt[t.day()]),1==i.currentRangeAs("months")&&t.month()!=i.currentRange.start.month()&&r.push("fc-other-month"),n=i.calendar.getNow(),t.isSame(n,"day")?(r.push("fc-today"),e!==!0&&r.push(i.highlightStateClass)):t<n?r.push("fc-past"):r.push("fc-future")):r.push("fc-disabled-day"),r}});me.mixin({segSelector:".fc-event-container > *",mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(t){var e,n=[],i=[];for(e=0;e<t.length;e++)(xt(t[e])?n:i).push(t[e]);this.segs=[].concat(this.renderBgEvents(n),this.renderFgEvents(i))},renderBgEvents:function(t){var e=this.eventsToSegs(t);return this.renderBgSegs(e)||e},renderFgEvents:function(t){var e=this.eventsToSegs(t);return this.renderFgSegs(e)||e},unrenderEvents:function(){this.handleSegMouseout(),this.clearDragListeners(),this.unrenderFgSegs(),this.unrenderBgSegs(),this.segs=null},getEventSegs:function(){return this.segs||[]},renderFgSegs:function(t){},unrenderFgSegs:function(){},renderFgSegEls:function(e,n){var i,r=this.view,s="",o=[];if(e.length){for(i=0;i<e.length;i++)s+=this.fgSegHtml(e[i],n);t(s).each(function(n,i){var s=e[n],l=r.resolveEventEl(s.event,t(i));l&&(l.data("fc-seg",s),s.el=l,o.push(s))})}return o},fgSegHtml:function(t,e){},renderBgSegs:function(t){return this.renderFill("bgEvent",t)},unrenderBgSegs:function(){this.unrenderFill("bgEvent")},bgEventSegEl:function(t,e){return this.view.resolveEventEl(t.event,e)},bgEventSegClasses:function(t){var e=t.event,n=e.source||{};return["fc-bgevent"].concat(e.className,n.className||[])},bgEventSegCss:function(t){return{"background-color":this.getSegSkinCss(t)["background-color"]}},businessHoursSegClasses:function(t){return["fc-nonbusiness","fc-bgevent"]},buildBusinessHourSegs:function(t,e){return this.eventsToSegs(this.buildBusinessHourEvents(t,e))},buildBusinessHourEvents:function(e,n){var i,r=this.view.calendar;return null==n&&(n=r.options.businessHours),i=r.computeBusinessHourEvents(e,n),!i.length&&n&&(i=[t.extend({},ke,{start:this.view.activeRange.end,end:this.view.activeRange.end,dow:null})]),i},bindSegHandlers:function(){this.bindSegHandlersToEl(this.el)},bindSegHandlersToEl:function(t){this.bindSegHandlerToEl(t,"touchstart",this.handleSegTouchStart),this.bindSegHandlerToEl(t,"mouseenter",this.handleSegMouseover),this.bindSegHandlerToEl(t,"mouseleave",this.handleSegMouseout),this.bindSegHandlerToEl(t,"mousedown",this.handleSegMousedown),this.bindSegHandlerToEl(t,"click",this.handleSegClick)},bindSegHandlerToEl:function(e,n,i){var r=this;e.on(n,this.segSelector,function(e){var n=t(this).data("fc-seg");if(n&&!r.isDraggingSeg&&!r.isResizingSeg)return i.call(r,n,e)})},handleSegClick:function(t,e){var n=this.view.publiclyTrigger("eventClick",t.el[0],t.event,e);n===!1&&e.preventDefault()},handleSegMouseover:function(t,e){pe.get().shouldIgnoreMouse()||this.mousedOverSeg||(this.mousedOverSeg=t,this.view.isEventResizable(t.event)&&t.el.addClass("fc-allow-mouse-resize"),this.view.publiclyTrigger("eventMouseover",t.el[0],t.event,e))},handleSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.isEventResizable(t.event)&&t.el.removeClass("fc-allow-mouse-resize"),this.view.publiclyTrigger("eventMouseout",t.el[0],t.event,e))},handleSegMousedown:function(t,e){var n=this.startSegResize(t,e,{distance:5});!n&&this.view.isEventDraggable(t.event)&&this.buildSegDragListener(t).startInteraction(e,{distance:5})},handleSegTouchStart:function(t,e){var n,i,r=this.view,s=t.event,o=r.isEventSelected(s),l=r.isEventDraggable(s),a=r.isEventResizable(s),u=!1;o&&a&&(u=this.startSegResize(t,e)),u||!l&&!a||(i=r.opt("eventLongPressDelay"),null==i&&(i=r.opt("longPressDelay")),n=l?this.buildSegDragListener(t):this.buildSegSelectListener(t),n.startInteraction(e,{delay:o?0:i}))},startSegResize:function(e,n,i){return!!t(n.target).is(".fc-resizer")&&(this.buildSegResizeListener(e,t(n.target).is(".fc-start-resizer")).startInteraction(n,i),!0)},buildSegDragListener:function(t){var e,n,i,r=this,l=this.view,a=t.el,u=t.event;if(this.segDragListener)return this.segDragListener;var c=this.segDragListener=new ge(l,{scroll:l.opt("dragScroll"),subjectEl:a,subjectCenter:!0,interactionStart:function(i){t.component=r,e=!1,n=new ve(t.el,{additionalClass:"fc-dragging",parentEl:l.el,opacity:c.isTouch?null:l.opt("dragOpacity"),revertDuration:l.opt("dragRevertDuration"),zIndex:2}),n.hide(),n.start(i)},dragStart:function(n){c.isTouch&&!l.isEventSelected(u)&&l.selectEvent(u),e=!0,r.handleSegMouseout(t,n),r.segDragStart(t,n),l.hideEvent(u)},hitOver:function(e,o,a){var d,h,f,g=!0;t.hit&&(a=t.hit),d=a.component.getSafeHitSpan(a),h=e.component.getSafeHitSpan(e),d&&h?(i=r.computeEventDrop(d,h,u),g=i&&r.isEventLocationAllowed(i,u)):g=!1,g||(i=null,s()),i&&(f=l.renderDrag(i,t))?(f.addClass("fc-dragging"),c.isTouch||r.applyDragOpacity(f),n.hide()):n.show(),o&&(i=null)},hitOut:function(){l.unrenderDrag(),n.show(),i=null},hitDone:function(){o()},interactionEnd:function(s){delete t.component,n.stop(!i,function(){e&&(l.unrenderDrag(),r.segDragStop(t,s)),i?l.reportSegDrop(t,i,r.largeUnit,a,s):l.showEvent(u)}),r.segDragListener=null}});return c},buildSegSelectListener:function(t){var e=this,n=this.view,i=t.event;if(this.segDragListener)return this.segDragListener;var r=this.segDragListener=new fe({dragStart:function(t){r.isTouch&&!n.isEventSelected(i)&&n.selectEvent(i)},interactionEnd:function(t){e.segDragListener=null}});return r},segDragStart:function(t,e){this.isDraggingSeg=!0,this.view.publiclyTrigger("eventDragStart",t.el[0],t.event,e,{})},segDragStop:function(t,e){this.isDraggingSeg=!1,this.view.publiclyTrigger("eventDragStop",t.el[0],t.event,e,{})},computeEventDrop:function(t,e,n){var i,r,s=this.view.calendar,o=t.start,l=e.start;return o.hasTime()===l.hasTime()?(i=this.diffDates(l,o),n.allDay&&tt(i)?(r={start:n.start.clone(),end:s.getEventEnd(n),allDay:!1},s.normalizeEventTimes(r)):r=Rt(n),r.start.add(i),r.end&&r.end.add(i)):r={start:l.clone(),end:null,allDay:!l.hasTime()},r},applyDragOpacity:function(t){var e=this.view.opt("dragOpacity");null!=e&&t.css("opacity",e)},externalDragStart:function(e,n){var i,r,s=this.view;s.opt("droppable")&&(i=t((n?n.item:null)||e.target),r=s.opt("dropAccept"),(t.isFunction(r)?r.call(i[0],i):i.is(r))&&(this.isDraggingExternal||this.listenToExternalDrag(i,e,n)))},listenToExternalDrag:function(t,e,n){var i,r=this,l=this.view,a=Bt(t),u=r.externalDragListener=new ge(this,{interactionStart:function(){r.isDraggingExternal=!0},hitOver:function(t){var e=!0,n=t.component.getSafeHitSpan(t);n?(i=r.computeExternalDrop(n,a),e=i&&r.isExternalLocationAllowed(i,a.eventProps)):e=!1,e||(i=null,s()),i&&r.renderDrag(i)},hitOut:function(){i=null},hitDone:function(){o(),r.unrenderDrag()},interactionEnd:function(e){i&&l.reportExternalDrop(a,i,t,e,n),r.isDraggingExternal=!1,r.externalDragListener=null}});u.startDrag(e)},computeExternalDrop:function(t,e){var n=this.view.calendar,i={start:n.applyTimezone(t.start),end:null};return e.startTime&&!i.start.hasTime()&&i.start.time(e.startTime),e.duration&&(i.end=i.start.clone().add(e.duration)),i},renderDrag:function(t,e){},unrenderDrag:function(){},buildSegResizeListener:function(t,e){var n,i,r=this,l=this.view,a=l.calendar,u=t.el,c=t.event,d=a.getEventEnd(c),h=this.segResizeListener=new ge(this,{scroll:l.opt("dragScroll"),subjectEl:u,interactionStart:function(){n=!1},dragStart:function(e){n=!0,r.handleSegMouseout(t,e),r.segResizeStart(t,e)},hitOver:function(n,o,a){var u=!0,h=r.getSafeHitSpan(a),f=r.getSafeHitSpan(n);h&&f?(i=e?r.computeEventStartResize(h,f,c):r.computeEventEndResize(h,f,c),u=i&&r.isEventLocationAllowed(i,c)):u=!1,u?i.start.isSame(c.start.clone().stripZone())&&i.end.isSame(d.clone().stripZone())&&(i=null):(i=null,s()),i&&(l.hideEvent(c),r.renderEventResize(i,t))},hitOut:function(){
+i=null,l.showEvent(c)},hitDone:function(){r.unrenderEventResize(),o()},interactionEnd:function(e){n&&r.segResizeStop(t,e),i?l.reportSegResize(t,i,r.largeUnit,u,e):l.showEvent(c),r.segResizeListener=null}});return h},segResizeStart:function(t,e){this.isResizingSeg=!0,this.view.publiclyTrigger("eventResizeStart",t.el[0],t.event,e,{})},segResizeStop:function(t,e){this.isResizingSeg=!1,this.view.publiclyTrigger("eventResizeStop",t.el[0],t.event,e,{})},computeEventStartResize:function(t,e,n){return this.computeEventResize("start",t,e,n)},computeEventEndResize:function(t,e,n){return this.computeEventResize("end",t,e,n)},computeEventResize:function(t,e,n,i){var r,s,o=this.view.calendar,l=this.diffDates(n[t],e[t]);return r={start:i.start.clone(),end:o.getEventEnd(i),allDay:i.allDay},r.allDay&&tt(l)&&(r.allDay=!1,o.normalizeEventTimes(r)),r[t].add(l),r.start.isBefore(r.end)||(s=this.minResizeDuration||(i.allDay?o.defaultAllDayEventDuration:o.defaultTimedEventDuration),"start"==t?r.start=r.end.clone().subtract(s):r.end=r.start.clone().add(s)),r},renderEventResize:function(t,e){},unrenderEventResize:function(){},getEventTimeText:function(t,e,n){return null==e&&(e=this.eventTimeFormat),null==n&&(n=this.displayEventEnd),this.displayEventTime&&t.start.hasTime()?n&&t.end?this.view.formatRange(t,e):t.start.format(e):""},getSegClasses:function(t,e,n){var i=this.view,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(this.getSegCustomClasses(t));return e&&r.push("fc-draggable"),n&&r.push("fc-resizable"),i.isEventSelected(t.event)&&r.push("fc-selected"),r},getSegCustomClasses:function(t){var e=t.event;return[].concat(e.className,e.source?e.source.className:[])},getSegSkinCss:function(t){return{"background-color":this.getSegBackgroundColor(t),"border-color":this.getSegBorderColor(t),color:this.getSegTextColor(t)}},getSegBackgroundColor:function(t){return t.event.backgroundColor||t.event.color||this.getSegDefaultBackgroundColor(t)},getSegDefaultBackgroundColor:function(t){var e=t.event.source||{};return e.backgroundColor||e.color||this.view.opt("eventBackgroundColor")||this.view.opt("eventColor")},getSegBorderColor:function(t){return t.event.borderColor||t.event.color||this.getSegDefaultBorderColor(t)},getSegDefaultBorderColor:function(t){var e=t.event.source||{};return e.borderColor||e.color||this.view.opt("eventBorderColor")||this.view.opt("eventColor")},getSegTextColor:function(t){return t.event.textColor||this.getSegDefaultTextColor(t)},getSegDefaultTextColor:function(t){var e=t.event.source||{};return e.textColor||this.view.opt("eventTextColor")},isEventLocationAllowed:function(t,e){if(this.isEventLocationInRange(t)){var n,i=this.view.calendar,r=this.eventToSpans(t);if(r.length){for(n=0;n<r.length;n++)if(!i.isEventSpanAllowed(r[n],e))return!1;return!0}}return!1},isExternalLocationAllowed:function(t,e){if(this.isEventLocationInRange(t)){var n,i=this.view.calendar,r=this.eventToSpans(t);if(r.length){for(n=0;n<r.length;n++)if(!i.isExternalSpanAllowed(r[n],t,e))return!1;return!0}}return!1},isEventLocationInRange:function(t){return Q(this.eventToRawRange(t),this.view.validRange)},eventToSegs:function(t){return this.eventsToSegs([t])},eventToSpans:function(t){var e=this.eventToRange(t);return e?this.eventRangeToSpans(e,t):[]},eventsToSegs:function(e,n){var i=this,r=Lt(e),s=[];return t.each(r,function(t,e){var r,o,l=[],a=[];for(o=0;o<e.length;o++)r=i.eventToRange(e[o]),r&&(a.push(r),l.push(e[o]));if(It(e[0]))for(a=i.invertRanges(a),o=0;o<a.length;o++)s.push.apply(s,i.eventRangeToSegs(a[o],e[0],n));else for(o=0;o<a.length;o++)s.push.apply(s,i.eventRangeToSegs(a[o],l[o],n))}),s},eventToRange:function(t){return this.refineRawEventRange(this.eventToRawRange(t))},refineRawEventRange:function(t){var e=this.view,n=e.calendar,i=N(t,e.activeRange);if(i)return n.localizeMoment(i.start),n.localizeMoment(i.end),i},eventToRawRange:function(t){var e=this.view.calendar,n=t.start.clone().stripZone(),i=(t.end?t.end.clone():e.getDefaultEventEnd(null!=t.allDay?t.allDay:!t.start.hasTime(),t.start)).stripZone();return{start:n,end:i}},eventRangeToSegs:function(t,e,n){var i,r=this.eventRangeToSpans(t,e),s=[];for(i=0;i<r.length;i++)s.push.apply(s,this.eventSpanToSegs(r[i],e,n));return s},eventRangeToSpans:function(e,n){return[t.extend({},e)]},eventSpanToSegs:function(t,e,n){var i,r,s=n?n(t):this.spanToSegs(t);for(i=0;i<s.length;i++)r=s[i],t.isStart||(r.isStart=!1),t.isEnd||(r.isEnd=!1),r.event=e,r.eventStartMS=+t.start,r.eventDurationMS=t.end-t.start;return s},invertRanges:function(t){var e,n,i=this.view,r=i.activeRange.start.clone(),s=i.activeRange.end.clone(),o=[],l=r;for(t.sort(Mt),e=0;e<t.length;e++)n=t[e],n.start>l&&o.push({start:l,end:n.start}),l=n.end;return l<s&&o.push({start:l,end:s}),o},sortEventSegs:function(t){t.sort(mt(this,"compareEventSegs"))},compareEventSegs:function(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||M(t.event,e.event,this.view.eventOrderSpecs)}}),Zt.pluckEventDateProps=Rt,Zt.isBgEvent=xt,Zt.dataAttrPrefix="";var ye=Zt.DayTableMixin={breakOnWeeks:!1,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){for(var t,e,n,i=this.view,r=this.start.clone(),s=-1,o=[],l=[];r.isBefore(this.end);)i.isHiddenDay(r)?o.push(s+.5):(s++,o.push(s),l.push(r.clone())),r.add(1,"days");if(this.breakOnWeeks){for(e=l[0].day(),t=1;t<l.length&&l[t].day()!=e;t++);n=Math.ceil(l.length/t)}else n=1,t=l.length;this.dayDates=l,this.dayIndices=o,this.daysPerRow=t,this.rowCnt=n,this.updateDayTableCols()},updateDayTableCols:function(){this.colCnt=this.computeColCnt(),this.colHeadFormat=this.view.opt("columnFormat")||this.computeColHeadFormat()},computeColCnt:function(){return this.daysPerRow},getCellDate:function(t,e){return this.dayDates[this.getCellDayIndex(t,e)].clone()},getCellRange:function(t,e){var n=this.getCellDate(t,e),i=n.clone().add(1,"days");return{start:n,end:i}},getCellDayIndex:function(t,e){return t*this.daysPerRow+this.getColDayIndex(e)},getColDayIndex:function(t){return this.isRTL?this.colCnt-1-t:t},getDateDayIndex:function(t){var e=this.dayIndices,n=t.diff(this.start,"days");return n<0?e[0]-1:n>=e.length?e[e.length-1]+1:e[n]},computeColHeadFormat:function(){return this.rowCnt>1||this.colCnt>10?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},sliceRangeByRow:function(t){var e,n,i,r,s,o=this.daysPerRow,l=this.view.computeDayRange(t),a=this.getDateDayIndex(l.start),u=this.getDateDayIndex(l.end.clone().subtract(1,"days")),c=[];for(e=0;e<this.rowCnt;e++)n=e*o,i=n+o-1,r=Math.max(a,n),s=Math.min(u,i),r=Math.ceil(r),s=Math.floor(s),r<=s&&c.push({row:e,firstRowDayIndex:r-n,lastRowDayIndex:s-n,isStart:r===a,isEnd:s===u});return c},sliceRangeByDay:function(t){var e,n,i,r,s,o,l=this.daysPerRow,a=this.view.computeDayRange(t),u=this.getDateDayIndex(a.start),c=this.getDateDayIndex(a.end.clone().subtract(1,"days")),d=[];for(e=0;e<this.rowCnt;e++)for(n=e*l,i=n+l-1,r=n;r<=i;r++)s=Math.max(u,r),o=Math.min(c,r),s=Math.ceil(s),o=Math.floor(o),s<=o&&d.push({row:e,firstRowDayIndex:s-n,lastRowDayIndex:o-n,isStart:s===u,isEnd:o===c});return d},renderHeadHtml:function(){var t=this.view;return'<div class="fc-row '+t.widgetHeaderClass+'"><table><thead>'+this.renderHeadTrHtml()+"</thead></table></div>"},renderHeadIntroHtml:function(){return this.renderIntroHtml()},renderHeadTrHtml:function(){return"<tr>"+(this.isRTL?"":this.renderHeadIntroHtml())+this.renderHeadDateCellsHtml()+(this.isRTL?this.renderHeadIntroHtml():"")+"</tr>"},renderHeadDateCellsHtml:function(){var t,e,n=[];for(t=0;t<this.colCnt;t++)e=this.getCellDate(0,t),n.push(this.renderHeadDateCellHtml(e));return n.join("")},renderHeadDateCellHtml:function(t,e,n){var i=this.view,r=Z(t,i.activeRange),s=["fc-day-header",i.widgetHeaderClass],o=ct(t.format(this.colHeadFormat));return 1===this.rowCnt?s=s.concat(this.getDayClasses(t,!0)):s.push("fc-"+Kt[t.day()]),'<th class="'+s.join(" ")+'"'+(1===(r&&this.rowCnt)?' data-date="'+t.format("YYYY-MM-DD")+'"':"")+(e>1?' colspan="'+e+'"':"")+(n?" "+n:"")+">"+(r?i.buildGotoAnchorHtml({date:t,forceOff:this.rowCnt>1||1===this.colCnt},o):o)+"</th>"},renderBgTrHtml:function(t){return"<tr>"+(this.isRTL?"":this.renderBgIntroHtml(t))+this.renderBgCellsHtml(t)+(this.isRTL?this.renderBgIntroHtml(t):"")+"</tr>"},renderBgIntroHtml:function(t){return this.renderIntroHtml()},renderBgCellsHtml:function(t){var e,n,i=[];for(e=0;e<this.colCnt;e++)n=this.getCellDate(t,e),i.push(this.renderBgCellHtml(n));return i.join("")},renderBgCellHtml:function(t,e){var n=this.view,i=Z(t,n.activeRange),r=this.getDayClasses(t);return r.unshift("fc-day",n.widgetContentClass),'<td class="'+r.join(" ")+'"'+(i?' data-date="'+t.format("YYYY-MM-DD")+'"':"")+(e?" "+e:"")+"></td>"},renderIntroHtml:function(){},bookendCells:function(t){var e=this.renderIntroHtml();e&&(this.isRTL?t.append(e):t.prepend(e))}},Se=Zt.DayGrid=me.extend(ye,{numbersVisible:!1,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(t){var e,n,i=this.view,r=this.rowCnt,s=this.colCnt,o="";for(e=0;e<r;e++)o+=this.renderDayRowHtml(e,t);for(this.el.html(o),this.rowEls=this.el.find(".fc-row"),this.cellEls=this.el.find(".fc-day, .fc-disabled-day"),this.rowCoordCache=new he({els:this.rowEls,isVertical:!0}),this.colCoordCache=new he({els:this.cellEls.slice(0,this.colCnt),isHorizontal:!0}),e=0;e<r;e++)for(n=0;n<s;n++)i.publiclyTrigger("dayRender",null,this.getCellDate(e,n),this.getCellEl(e,n))},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var t=this.buildBusinessHourSegs(!0);this.renderFill("businessHours",t,"bgevent")},unrenderBusinessHours:function(){this.unrenderFill("businessHours")},renderDayRowHtml:function(t,e){var n=this.view,i=["fc-row","fc-week",n.widgetContentClass];return e&&i.push("fc-rigid"),'<div class="'+i.join(" ")+'"><div class="fc-bg"><table>'+this.renderBgTrHtml(t)+'</table></div><div class="fc-content-skeleton"><table>'+(this.numbersVisible?"<thead>"+this.renderNumberTrHtml(t)+"</thead>":"")+"</table></div></div>"},renderNumberTrHtml:function(t){return"<tr>"+(this.isRTL?"":this.renderNumberIntroHtml(t))+this.renderNumberCellsHtml(t)+(this.isRTL?this.renderNumberIntroHtml(t):"")+"</tr>"},renderNumberIntroHtml:function(t){return this.renderIntroHtml()},renderNumberCellsHtml:function(t){var e,n,i=[];for(e=0;e<this.colCnt;e++)n=this.getCellDate(t,e),i.push(this.renderNumberCellHtml(n));return i.join("")},renderNumberCellHtml:function(t){var e,n,i=this.view,r="",s=Z(t,i.activeRange),o=i.dayNumbersVisible&&s;return o||i.cellWeekNumbersVisible?(e=this.getDayClasses(t),e.unshift("fc-day-top"),i.cellWeekNumbersVisible&&(n="ISO"===t._locale._fullCalendar_weekCalc?1:t._locale.firstDayOfWeek()),r+='<td class="'+e.join(" ")+'"'+(s?' data-date="'+t.format()+'"':"")+">",i.cellWeekNumbersVisible&&t.day()==n&&(r+=i.buildGotoAnchorHtml({date:t,type:"week"},{class:"fc-week-number"},t.format("w"))),o&&(r+=i.buildGotoAnchorHtml(t,{class:"fc-day-number"},t.date())),r+="</td>"):"<td/>"},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return 1==this.colCnt},rangeUpdated:function(){this.updateDayTable()},spanToSegs:function(t){var e,n,i=this.sliceRangeByRow(t);for(e=0;e<i.length;e++)n=i[e],this.isRTL?(n.leftCol=this.daysPerRow-1-n.lastRowDayIndex,n.rightCol=this.daysPerRow-1-n.firstRowDayIndex):(n.leftCol=n.firstRowDayIndex,n.rightCol=n.lastRowDayIndex);return i},prepareHits:function(){this.colCoordCache.build(),this.rowCoordCache.build(),this.rowCoordCache.bottoms[this.rowCnt-1]+=this.bottomCoordPadding},releaseHits:function(){this.colCoordCache.clear(),this.rowCoordCache.clear()},queryHit:function(t,e){if(this.colCoordCache.isLeftInBounds(t)&&this.rowCoordCache.isTopInBounds(e)){var n=this.colCoordCache.getHorizontalIndex(t),i=this.rowCoordCache.getVerticalIndex(e);if(null!=i&&null!=n)return this.getCellHit(i,n)}},getHitSpan:function(t){return this.getCellRange(t.row,t.col)},getHitEl:function(t){return this.getCellEl(t.row,t.col)},getCellHit:function(t,e){return{row:t,col:e,component:this,left:this.colCoordCache.getLeftOffset(e),right:this.colCoordCache.getRightOffset(e),top:this.rowCoordCache.getTopOffset(t),bottom:this.rowCoordCache.getBottomOffset(t)}},getCellEl:function(t,e){return this.cellEls.eq(t*this.colCnt+e)},renderDrag:function(t,e){var n,i=this.eventToSpans(t);for(n=0;n<i.length;n++)this.renderHighlight(i[n]);if(e&&e.component!==this)return this.renderEventLocationHelper(t,e)},unrenderDrag:function(){this.unrenderHighlight(),this.unrenderHelper()},renderEventResize:function(t,e){var n,i=this.eventToSpans(t);for(n=0;n<i.length;n++)this.renderHighlight(i[n]);return this.renderEventLocationHelper(t,e)},unrenderEventResize:function(){this.unrenderHighlight(),this.unrenderHelper()},renderHelper:function(e,n){var i,r=[],s=this.eventToSegs(e);return s=this.renderFgSegEls(s),i=this.renderSegRows(s),this.rowEls.each(function(e,s){var o,l=t(s),a=t('<div class="fc-helper-skeleton"><table/></div>');o=n&&n.row===e?n.el.position().top:l.find(".fc-content-skeleton tbody").position().top,a.css("top",o).find("table").append(i[e].tbodyEl),l.append(a),r.push(a[0])}),this.helperEls=t(r)},unrenderHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},fillSegTag:"td",renderFill:function(e,n,i){var r,s,o,l=[];for(n=this.renderFillSegEls(e,n),r=0;r<n.length;r++)s=n[r],o=this.renderFillRow(e,s,i),this.rowEls.eq(s.row).append(o),l.push(o[0]);return this.elsByFill[e]=t(l),n},renderFillRow:function(e,n,i){var r,s,o=this.colCnt,l=n.leftCol,a=n.rightCol+1;return i=i||e.toLowerCase(),r=t('<div class="fc-'+i+'-skeleton"><table><tr/></table></div>'),s=r.find("tr"),l>0&&s.append('<td colspan="'+l+'"/>'),s.append(n.el.attr("colspan",a-l)),a<o&&s.append('<td colspan="'+(o-a)+'"/>'),this.bookendCells(s),r}});Se.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover(),me.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return me.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(e){var n=t.grep(e,function(t){return t.event.allDay});return me.prototype.renderBgSegs.call(this,n)},renderFgSegs:function(e){var n;return e=this.renderFgSegEls(e),n=this.rowStructs=this.renderSegRows(e),this.rowEls.each(function(e,i){t(i).find(".fc-content-skeleton > table").append(n[e].tbodyEl)}),e},unrenderFgSegs:function(){for(var t,e=this.rowStructs||[];t=e.pop();)t.tbodyEl.remove();this.rowStructs=null},renderSegRows:function(t){var e,n,i=[];for(e=this.groupSegRows(t),n=0;n<e.length;n++)i.push(this.renderSegRow(n,e[n]));return i},fgSegHtml:function(t,e){var n,i,r=this.view,s=t.event,o=r.isEventDraggable(s),l=!e&&s.allDay&&t.isStart&&r.isEventResizableFromStart(s),a=!e&&s.allDay&&t.isEnd&&r.isEventResizableFromEnd(s),u=this.getSegClasses(t,o,l||a),c=ht(this.getSegSkinCss(t)),d="";return u.unshift("fc-day-grid-event","fc-h-event"),t.isStart&&(n=this.getEventTimeText(s),n&&(d='<span class="fc-time">'+ct(n)+"</span>")),i='<span class="fc-title">'+(ct(s.title||"")||"&nbsp;")+"</span>",'<a class="'+u.join(" ")+'"'+(s.url?' href="'+ct(s.url)+'"':"")+(c?' style="'+c+'"':"")+'><div class="fc-content">'+(this.isRTL?i+" "+d:d+" "+i)+"</div>"+(l?'<div class="fc-resizer fc-start-resizer" />':"")+(a?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},renderSegRow:function(e,n){function i(e){for(;o<e;)c=(m[r-1]||[])[o],c?c.attr("rowspan",parseInt(c.attr("rowspan")||1,10)+1):(c=t("<td/>"),l.append(c)),v[r][o]=c,m[r][o]=c,o++}var r,s,o,l,a,u,c,d=this.colCnt,h=this.buildSegLevels(n),f=Math.max(1,h.length),g=t("<tbody/>"),p=[],v=[],m=[];for(r=0;r<f;r++){if(s=h[r],o=0,l=t("<tr/>"),p.push([]),v.push([]),m.push([]),s)for(a=0;a<s.length;a++){for(u=s[a],i(u.leftCol),c=t('<td class="fc-event-container"/>').append(u.el),u.leftCol!=u.rightCol?c.attr("colspan",u.rightCol-u.leftCol+1):m[r][o]=c;o<=u.rightCol;)v[r][o]=c,p[r][o]=u,o++;l.append(c)}i(d),this.bookendCells(l),g.append(l)}return{row:e,tbodyEl:g,cellMatrix:v,segMatrix:p,segLevels:h,segs:n}},buildSegLevels:function(t){var e,n,i,r=[];for(this.sortEventSegs(t),e=0;e<t.length;e++){for(n=t[e],i=0;i<r.length&&Ft(n,r[i]);i++);n.level=i,(r[i]||(r[i]=[])).push(n)}for(i=0;i<r.length;i++)r[i].sort(Nt);return r},groupSegRows:function(t){var e,n=[];for(e=0;e<this.rowCnt;e++)n.push([]);for(e=0;e<t.length;e++)n[t[e].row].push(t[e]);return n}}),Se.mixin({segPopover:null,popoverSegs:null,removeSegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,n,i=this.rowStructs||[];for(e=0;e<i.length;e++)this.unlimitRow(e),n=!!t&&("number"==typeof t?t:this.computeRowLevelLimit(e)),n!==!1&&this.limitRow(e,n)},computeRowLevelLimit:function(e){function n(e,n){s=Math.max(s,t(n).outerHeight())}var i,r,s,o=this.rowEls.eq(e),l=o.height(),a=this.rowStructs[e].tbodyEl.children();for(i=0;i<a.length;i++)if(r=a.eq(i).removeClass("fc-limited"),s=0,r.find("> td > :first-child").each(n),r.position().top+s>l)return i;return!1},limitRow:function(e,n){function i(i){for(;E<i;)u=S.getCellSegs(e,E,n),u.length&&(h=s[n-1][E],y=S.renderMoreLink(e,E,u),m=t("<div/>").append(y),h.append(m),b.push(m[0])),E++}var r,s,o,l,a,u,c,d,h,f,g,p,v,m,y,S=this,w=this.rowStructs[e],b=[],E=0;if(n&&n<w.segLevels.length){for(r=w.segLevels[n-1],s=w.cellMatrix,o=w.tbodyEl.children().slice(n).addClass("fc-limited").get(),l=0;l<r.length;l++){for(a=r[l],i(a.leftCol),d=[],c=0;E<=a.rightCol;)u=this.getCellSegs(e,E,n),d.push(u),c+=u.length,E++;if(c){for(h=s[n-1][a.leftCol],f=h.attr("rowspan")||1,g=[],p=0;p<d.length;p++)v=t('<td class="fc-more-cell"/>').attr("rowspan",f),u=d[p],y=this.renderMoreLink(e,a.leftCol+p,[a].concat(u)),m=t("<div/>").append(y),v.append(m),g.push(v[0]),b.push(v[0]);h.addClass("fc-limited").after(t(g)),o.push(h[0])}}i(this.colCnt),w.moreEls=t(b),w.limitedEls=t(o)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,n,i){var r=this,s=this.view;return t('<a class="fc-more"/>').text(this.getMoreLinkText(i.length)).on("click",function(o){var l=s.opt("eventLimitClick"),a=r.getCellDate(e,n),u=t(this),c=r.getCellEl(e,n),d=r.getCellSegs(e,n),h=r.resliceDaySegs(d,a),f=r.resliceDaySegs(i,a);"function"==typeof l&&(l=s.publiclyTrigger("eventLimitClick",null,{date:a,dayEl:c,moreEl:u,segs:h,hiddenSegs:f},o)),"popover"===l?r.showSegPopover(e,n,u,h):"string"==typeof l&&s.calendar.zoomTo(a,l)})},showSegPopover:function(t,e,n,i){var r,s,o=this,l=this.view,a=n.parent();r=1==this.rowCnt?l.el:this.rowEls.eq(t),s={className:"fc-more-popover",content:this.renderSegPopoverContent(t,e,i),parentEl:this.view.el,top:r.offset().top,autoHide:!0,viewportConstrain:l.opt("popoverViewportConstrain"),hide:function(){if(o.popoverSegs)for(var t,e=0;e<o.popoverSegs.length;++e)t=o.popoverSegs[e],l.publiclyTrigger("eventDestroy",t.event,t.event,t.el);o.segPopover.removeElement(),o.segPopover=null,o.popoverSegs=null}},this.isRTL?s.right=a.offset().left+a.outerWidth()+1:s.left=a.offset().left-1,this.segPopover=new de(s),this.segPopover.show(),this.bindSegHandlersToEl(this.segPopover.el)},renderSegPopoverContent:function(e,n,i){var r,s=this.view,o=s.opt("theme"),l=this.getCellDate(e,n).format(s.opt("dayPopoverFormat")),a=t('<div class="fc-header '+s.widgetHeaderClass+'"><span class="fc-close '+(o?"ui-icon ui-icon-closethick":"fc-icon fc-icon-x")+'"></span><span class="fc-title">'+ct(l)+'</span><div class="fc-clear"/></div><div class="fc-body '+s.widgetContentClass+'"><div class="fc-event-container"></div></div>'),u=a.find(".fc-event-container");for(i=this.renderFgSegEls(i,!0),this.popoverSegs=i,r=0;r<i.length;r++)this.hitsNeeded(),i[r].hit=this.getCellHit(e,n),this.hitsNotNeeded(),u.append(i[r].el);return a},resliceDaySegs:function(e,n){var i=t.map(e,function(t){return t.event}),r=n.clone(),s=r.clone().add(1,"days"),o={start:r,end:s};return e=this.eventsToSegs(i,function(t){var e=N(t,o);return e?[e]:[]}),this.sortEventSegs(e),e},getMoreLinkText:function(t){var e=this.view.opt("eventLimitText");return"function"==typeof e?e(t):"+"+t+" "+e},getCellSegs:function(t,e,n){for(var i,r=this.rowStructs[t].segMatrix,s=n||0,o=[];s<r.length;)i=r[s][e],i&&o.push(i),s++;return o}});var we=Zt.TimeGrid=me.extend(ye,{slotDuration:null,snapDuration:null,snapsPerSlot:null,labelFormat:null,labelInterval:null,colEls:null,slatContainerEl:null,slatEls:null,nowIndicatorEls:null,colCoordCache:null,slatCoordCache:null,constructor:function(){me.apply(this,arguments),this.processOptions()},renderDates:function(){this.el.html(this.renderHtml()),this.colEls=this.el.find(".fc-day, .fc-disabled-day"),this.slatContainerEl=this.el.find(".fc-slats"),this.slatEls=this.slatContainerEl.find("tr"),this.colCoordCache=new he({els:this.colEls,isHorizontal:!0}),this.slatCoordCache=new he({els:this.slatEls,isVertical:!0}),this.renderContentSkeleton()},renderHtml:function(){return'<div class="fc-bg"><table>'+this.renderBgTrHtml(0)+'</table></div><div class="fc-slats"><table>'+this.renderSlatRowHtml()+"</table></div>"},renderSlatRowHtml:function(){for(var t,n,i,r=this.view,s=this.isRTL,o="",l=e.duration(+this.view.minTime);l<this.view.maxTime;)t=this.start.clone().time(l),n=vt(Y(l,this.labelInterval)),i='<td class="fc-axis fc-time '+r.widgetContentClass+'" '+r.axisStyleAttr()+">"+(n?"<span>"+ct(t.format(this.labelFormat))+"</span>":"")+"</td>",o+='<tr data-time="'+t.format("HH:mm:ss")+'"'+(n?"":' class="fc-minor"')+">"+(s?"":i)+'<td class="'+r.widgetContentClass+'"/>'+(s?i:"")+"</tr>",l.add(this.slotDuration);return o},processOptions:function(){var n,i=this.view,r=i.opt("slotDuration"),s=i.opt("snapDuration");r=e.duration(r),s=s?e.duration(s):r,this.slotDuration=r,this.snapDuration=s,this.snapsPerSlot=r/s,this.minResizeDuration=s,n=i.opt("slotLabelFormat"),t.isArray(n)&&(n=n[n.length-1]),this.labelFormat=n||i.opt("smallTimeFormat"),n=i.opt("slotLabelInterval"),this.labelInterval=n?e.duration(n):this.computeLabelInterval(r)},computeLabelInterval:function(t){var n,i,r;for(n=Ge.length-1;n>=0;n--)if(i=e.duration(Ge[n]),r=Y(i,t),vt(r)&&r>1)return i;return e.duration(t)},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return!0},prepareHits:function(){this.colCoordCache.build(),this.slatCoordCache.build()},releaseHits:function(){this.colCoordCache.clear()},queryHit:function(t,e){var n=this.snapsPerSlot,i=this.colCoordCache,r=this.slatCoordCache;if(i.isLeftInBounds(t)&&r.isTopInBounds(e)){var s=i.getHorizontalIndex(t),o=r.getVerticalIndex(e);if(null!=s&&null!=o){var l=r.getTopOffset(o),a=r.getHeight(o),u=(e-l)/a,c=Math.floor(u*n),d=o*n+c,h=l+c/n*a,f=l+(c+1)/n*a;return{col:s,snap:d,component:this,left:i.getLeftOffset(s),right:i.getRightOffset(s),top:h,bottom:f}}}},getHitSpan:function(t){var e,n=this.getCellDate(0,t.col),i=this.computeSnapTime(t.snap);return n.time(i),e=n.clone().add(this.snapDuration),{start:n,end:e}},getHitEl:function(t){return this.colEls.eq(t.col)},rangeUpdated:function(){this.updateDayTable()},computeSnapTime:function(t){return e.duration(this.view.minTime+this.snapDuration*t)},spanToSegs:function(t){var e,n=this.sliceRangeByTimes(t);for(e=0;e<n.length;e++)this.isRTL?n[e].col=this.daysPerRow-1-n[e].dayIndex:n[e].col=n[e].dayIndex;return n},sliceRangeByTimes:function(t){var e,n,i,r,s=[];for(n=0;n<this.daysPerRow;n++)i=this.dayDates[n].clone().time(0),r={start:i.clone().add(this.view.minTime),end:i.clone().add(this.view.maxTime)},e=N(t,r),e&&(e.dayIndex=n,s.push(e));return s},updateSize:function(t){this.slatCoordCache.build(),t&&this.updateSegVerticals([].concat(this.fgSegs||[],this.bgSegs||[],this.businessSegs||[]))},getTotalSlatHeight:function(){return this.slatContainerEl.outerHeight()},computeDateTop:function(t,n){return this.computeTimeTop(e.duration(t-n.clone().stripTime()))},computeTimeTop:function(t){var e,n,i=this.slatEls.length,r=(t-this.view.minTime)/this.slotDuration;return r=Math.max(0,r),r=Math.min(i,r),e=Math.floor(r),e=Math.min(e,i-1),n=r-e,this.slatCoordCache.getTopPosition(e)+this.slatCoordCache.getHeight(e)*n},renderDrag:function(t,e){var n,i;if(e)return this.renderEventLocationHelper(t,e);for(n=this.eventToSpans(t),i=0;i<n.length;i++)this.renderHighlight(n[i])},unrenderDrag:function(){this.unrenderHelper(),this.unrenderHighlight()},renderEventResize:function(t,e){return this.renderEventLocationHelper(t,e)},unrenderEventResize:function(){this.unrenderHelper()},renderHelper:function(t,e){return this.renderHelperSegs(this.eventToSegs(t),e)},unrenderHelper:function(){this.unrenderHelperSegs()},renderBusinessHours:function(){this.renderBusinessSegs(this.buildBusinessHourSegs())},unrenderBusinessHours:function(){this.unrenderBusinessSegs()},getNowIndicatorUnit:function(){return"minute"},renderNowIndicator:function(e){var n,i=this.spanToSegs({start:e,end:e}),r=this.computeDateTop(e,e),s=[];for(n=0;n<i.length;n++)s.push(t('<div class="fc-now-indicator fc-now-indicator-line"></div>').css("top",r).appendTo(this.colContainerEls.eq(i[n].col))[0]);i.length>0&&s.push(t('<div class="fc-now-indicator fc-now-indicator-arrow"></div>').css("top",r).appendTo(this.el.find(".fc-content-skeleton"))[0]),this.nowIndicatorEls=t(s)},unrenderNowIndicator:function(){this.nowIndicatorEls&&(this.nowIndicatorEls.remove(),this.nowIndicatorEls=null)},renderSelection:function(t){this.view.opt("selectHelper")?this.renderEventLocationHelper(t):this.renderHighlight(t)},unrenderSelection:function(){this.unrenderHelper(),this.unrenderHighlight()},renderHighlight:function(t){this.renderHighlightSegs(this.spanToSegs(t))},unrenderHighlight:function(){this.unrenderHighlightSegs()}});we.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var e,n,i="";for(e=0;e<this.colCnt;e++)i+='<td><div class="fc-content-col"><div class="fc-event-container fc-helper-container"></div><div class="fc-event-container"></div><div class="fc-highlight-container"></div><div class="fc-bgevent-container"></div><div class="fc-business-container"></div></div></td>';n=t('<div class="fc-content-skeleton"><table><tr>'+i+"</tr></table></div>"),this.colContainerEls=n.find(".fc-content-col"),this.helperContainerEls=n.find(".fc-helper-container"),this.fgContainerEls=n.find(".fc-event-container:not(.fc-helper-container)"),this.bgContainerEls=n.find(".fc-bgevent-container"),this.highlightContainerEls=n.find(".fc-highlight-container"),this.businessContainerEls=n.find(".fc-business-container"),this.bookendCells(n.find("tr")),this.el.append(n)},renderFgSegs:function(t){return t=this.renderFgSegsIntoContainers(t,this.fgContainerEls),this.fgSegs=t,t},unrenderFgSegs:function(){this.unrenderNamedSegs("fgSegs")},renderHelperSegs:function(e,n){var i,r,s,o=[];for(e=this.renderFgSegsIntoContainers(e,this.helperContainerEls),i=0;i<e.length;i++)r=e[i],n&&n.col===r.col&&(s=n.el,r.el.css({left:s.css("left"),right:s.css("right"),"margin-left":s.css("margin-left"),"margin-right":s.css("margin-right")})),o.push(r.el[0]);return this.helperSegs=e,t(o)},unrenderHelperSegs:function(){this.unrenderNamedSegs("helperSegs")},renderBgSegs:function(t){return t=this.renderFillSegEls("bgEvent",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.bgContainerEls),this.bgSegs=t,t},unrenderBgSegs:function(){this.unrenderNamedSegs("bgSegs")},renderHighlightSegs:function(t){t=this.renderFillSegEls("highlight",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.highlightContainerEls),this.highlightSegs=t},unrenderHighlightSegs:function(){this.unrenderNamedSegs("highlightSegs")},renderBusinessSegs:function(t){t=this.renderFillSegEls("businessHours",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.businessContainerEls),this.businessSegs=t},unrenderBusinessSegs:function(){this.unrenderNamedSegs("businessSegs")},groupSegsByCol:function(t){var e,n=[];for(e=0;e<this.colCnt;e++)n.push([]);for(e=0;e<t.length;e++)n[t[e].col].push(t[e]);return n},attachSegsByCol:function(t,e){var n,i,r;for(n=0;n<this.colCnt;n++)for(i=t[n],r=0;r<i.length;r++)e.eq(n).append(i[r].el)},unrenderNamedSegs:function(t){var e,n=this[t];if(n){for(e=0;e<n.length;e++)n[e].el.remove();this[t]=null}},renderFgSegsIntoContainers:function(t,e){var n,i;for(t=this.renderFgSegEls(t),n=this.groupSegsByCol(t),i=0;i<this.colCnt;i++)this.updateFgSegCoords(n[i]);return this.attachSegsByCol(n,e),t},fgSegHtml:function(t,e){var n,i,r,s=this.view,o=t.event,l=s.isEventDraggable(o),a=!e&&t.isStart&&s.isEventResizableFromStart(o),u=!e&&t.isEnd&&s.isEventResizableFromEnd(o),c=this.getSegClasses(t,l,a||u),d=ht(this.getSegSkinCss(t));return c.unshift("fc-time-grid-event","fc-v-event"),s.isMultiDayEvent(o)?(t.isStart||t.isEnd)&&(n=this.getEventTimeText(t),i=this.getEventTimeText(t,"LT"),r=this.getEventTimeText(t,null,!1)):(n=this.getEventTimeText(o),i=this.getEventTimeText(o,"LT"),r=this.getEventTimeText(o,null,!1)),'<a class="'+c.join(" ")+'"'+(o.url?' href="'+ct(o.url)+'"':"")+(d?' style="'+d+'"':"")+'><div class="fc-content">'+(n?'<div class="fc-time" data-start="'+ct(r)+'" data-full="'+ct(i)+'"><span>'+ct(n)+"</span></div>":"")+(o.title?'<div class="fc-title">'+ct(o.title)+"</div>":"")+'</div><div class="fc-bg"/>'+(u?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},updateSegVerticals:function(t){this.computeSegVerticals(t),this.assignSegVerticals(t)},computeSegVerticals:function(t){var e,n,i;for(e=0;e<t.length;e++)n=t[e],i=this.dayDates[n.dayIndex],n.top=this.computeDateTop(n.start,i),n.bottom=this.computeDateTop(n.end,i)},assignSegVerticals:function(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.el.css(this.generateSegVerticalCss(n))},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},updateFgSegCoords:function(t){this.computeSegVerticals(t),this.computeFgSegHorizontals(t),this.assignSegVerticals(t),this.assignFgSegHorizontals(t)},computeFgSegHorizontals:function(t){var e,n,i;if(this.sortEventSegs(t),e=zt(t),At(e),n=e[0]){for(i=0;i<n.length;i++)Gt(n[i]);for(i=0;i<n.length;i++)this.computeFgSegForwardBack(n[i],0,0)}},computeFgSegForwardBack:function(t,e,n){var i,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(this.sortForwardSegs(r),this.computeFgSegForwardBack(r[0],e+1,n),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-n)/(e+1),i=0;i<r.length;i++)this.computeFgSegForwardBack(r[i],0,t.forwardCoord)},sortForwardSegs:function(t){t.sort(mt(this,"compareForwardSegs"))},compareForwardSegs:function(t,e){return e.forwardPressure-t.forwardPressure||(t.backwardCoord||0)-(e.backwardCoord||0)||this.compareEventSegs(t,e)},assignFgSegHorizontals:function(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.el.css(this.generateFgSegHorizontalCss(n)),n.bottom-n.top<30&&n.el.addClass("fc-short")},generateFgSegHorizontalCss:function(t){var e,n,i=this.view.opt("slotEventOverlap"),r=t.backwardCoord,s=t.forwardCoord,o=this.generateSegVerticalCss(t);return i&&(s=Math.min(1,r+2*(s-r))),this.isRTL?(e=1-s,n=r):(e=r,n=1-s),o.zIndex=t.level+1,o.left=100*e+"%",o.right=100*n+"%",i&&t.forwardPressure&&(o[this.isRTL?"marginLeft":"marginRight"]=20),o}});var be=Zt.View=wt.extend(ue,ce,{type:null,name:null,title:null,calendar:null,viewSpec:null,options:null,el:null,isDateSet:!1,isDateRendered:!1,dateRenderQueue:null,isEventsBound:!1,isEventsSet:!1,isEventsRendered:!1,eventRenderQueue:null,isRTL:!1,isSelected:!1,selectedEvent:null,eventOrderSpecs:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,nextDayThreshold:null,isHiddenDayHash:null,isNowIndicatorRendered:null,initialNowDate:null,initialNowQueriedMs:null,
+nowIndicatorTimeoutID:null,nowIndicatorIntervalID:null,constructor:function(t,n){this.calendar=t,this.viewSpec=n,this.type=n.type,this.options=n.options,this.name=this.type,this.nextDayThreshold=e.duration(this.opt("nextDayThreshold")),this.initThemingProps(),this.initHiddenDays(),this.isRTL=this.opt("isRTL"),this.eventOrderSpecs=L(this.opt("eventOrder")),this.dateRenderQueue=new Tt,this.eventRenderQueue=new Tt(this.opt("eventRenderWait")),this.initialize()},initialize:function(){},opt:function(t){return this.options[t]},publiclyTrigger:function(t,e){var n=this.calendar;return n.publiclyTrigger.apply(n,[t,e||this].concat(Array.prototype.slice.call(arguments,2),[this]))},rejectOn:function(t,e){var n=this;return new Dt(function(i,r){function s(){n.off(t,r)}n.one(t,r),e.then(function(t){s(),i(t)},function(){s(),r()})})},updateTitle:function(){this.title=this.computeTitle(),this.calendar.setToolbarsTitle(this.title)},computeTitle:function(){var t;return t=/^(year|month)$/.test(this.currentRangeUnit)?this.currentRange:this.activeRange,this.formatRange({start:this.calendar.applyTimezone(t.start),end:this.calendar.applyTimezone(t.end)},this.opt("titleFormat")||this.computeTitleFormat(),this.opt("titleRangeSeparator"))},computeTitleFormat:function(){return"year"==this.currentRangeUnit?"YYYY":"month"==this.currentRangeUnit?this.opt("monthYearFormat"):this.currentRangeAs("days")>1?"ll":"LL"},formatRange:function(t,e,n){var i=t.end;return i.hasTime()||(i=i.clone().subtract(1)),le(t.start,i,e,n,this.opt("isRTL"))},getAllDayHtml:function(){return this.opt("allDayHtml")||ct(this.opt("allDayText"))},buildGotoAnchorHtml:function(e,n,i){var r,s,o,l;return t.isPlainObject(e)?(r=e.date,s=e.type,o=e.forceOff):r=e,r=Zt.moment(r),l={date:r.format("YYYY-MM-DD"),type:s||"day"},"string"==typeof n&&(i=n,n=null),n=n?" "+ft(n):"",i=i||"",!o&&this.opt("navLinks")?"<a"+n+' data-goto="'+ct(JSON.stringify(l))+'">'+i+"</a>":"<span"+n+">"+i+"</span>"},setElement:function(t){this.el=t,this.bindGlobalHandlers(),this.renderSkeleton()},removeElement:function(){this.unsetDate(),this.unrenderSkeleton(),this.unbindGlobalHandlers(),this.el.remove()},renderSkeleton:function(){},unrenderSkeleton:function(){},setDate:function(t){var e=this.isDateSet;this.isDateSet=!0,this.handleRawDate(t),this.trigger(e?"dateReset":"dateSet",t)},unsetDate:function(){this.isDateSet&&(this.isDateSet=!1,this.handleDateUnset(),this.trigger("dateUnset"))},handleRawDate:function(t){var e=this,n=this.buildDateProfile(t,null,!0);this.isSameDateProfile(n)?this.dateRenderQueue.add(function(){e.isDateRendered||e.handleDate(n)}):this.handleDate(n)},handleDate:function(t){var e=this;this.unbindEvents(),this.requestDateRender(t).then(function(){e.bindEvents()})},handleDateUnset:function(){this.unbindEvents(),this.requestDateUnrender()},requestDateRender:function(t){var e=this;return this.dateRenderQueue.add(function(){return e.executeDateRender(t)})},requestDateUnrender:function(){var t=this;return this.dateRenderQueue.add(function(){return t.executeDateUnrender()})},executeDateRender:function(t){var e=this;return t&&e.setDateProfile(t),this.updateTitle(),this.calendar.updateToolbarButtons(),t?this.captureInitialScroll():this.captureScroll(),this.freezeHeight(),this.executeDateUnrender().then(function(){e.render&&e.render(),e.renderDates(),e.updateSize(),e.renderBusinessHours(),e.startNowIndicator(),e.thawHeight(),e.releaseScroll(),e.isDateRendered=!0,e.onDateRender(),e.trigger("dateRender")})},executeDateUnrender:function(){var t=this;return t.isDateRendered?this.requestEventsUnrender().then(function(){t.unselect(),t.stopNowIndicator(),t.triggerUnrender(),t.unrenderBusinessHours(),t.unrenderDates(),t.destroy&&t.destroy(),t.isDateRendered=!1,t.trigger("dateUnrender")}):Dt.resolve()},onDateRender:function(){this.triggerRender()},renderDates:function(){},unrenderDates:function(){},triggerRender:function(){this.publiclyTrigger("viewRender",this,this,this.el)},triggerUnrender:function(){this.publiclyTrigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo(pe.get(),{touchstart:this.processUnselect,mousedown:this.handleDocumentMousedown})},unbindGlobalHandlers:function(){this.stopListeningTo(pe.get())},initThemingProps:function(){var t=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=t+"-widget-header",this.widgetContentClass=t+"-widget-content",this.highlightStateClass=t+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var t,n,i,r=this;this.opt("nowIndicator")&&(t=this.getNowIndicatorUnit(),t&&(n=mt(this,"updateNowIndicator"),this.initialNowDate=this.calendar.getNow(),this.initialNowQueriedMs=+new Date,this.renderNowIndicator(this.initialNowDate),this.isNowIndicatorRendered=!0,i=this.initialNowDate.clone().startOf(t).add(1,t)-this.initialNowDate,this.nowIndicatorTimeoutID=setTimeout(function(){r.nowIndicatorTimeoutID=null,n(),i=+e.duration(1,t),i=Math.max(100,i),r.nowIndicatorIntervalID=setInterval(n,i)},i)))},updateNowIndicator:function(){this.isNowIndicatorRendered&&(this.unrenderNowIndicator(),this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs)))},stopNowIndicator:function(){this.isNowIndicatorRendered&&(this.nowIndicatorTimeoutID&&(clearTimeout(this.nowIndicatorTimeoutID),this.nowIndicatorTimeoutID=null),this.nowIndicatorIntervalID&&(clearTimeout(this.nowIndicatorIntervalID),this.nowIndicatorIntervalID=null),this.unrenderNowIndicator(),this.isNowIndicatorRendered=!1)},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},updateSize:function(t){t&&this.captureScroll(),this.updateHeight(t),this.updateWidth(t),this.updateNowIndicator(),t&&this.releaseScroll()},updateWidth:function(t){},updateHeight:function(t){var e=this.calendar;this.setHeight(e.getSuggestedViewHeight(),e.isHeightAuto())},setHeight:function(t,e){},capturedScroll:null,capturedScrollDepth:0,captureScroll:function(){return!this.capturedScrollDepth++&&(this.capturedScroll=this.isDateRendered?this.queryScroll():{},!0)},captureInitialScroll:function(e){this.captureScroll()&&(this.capturedScroll.isInitial=!0,e?t.extend(this.capturedScroll,e):this.capturedScroll.isComputed=!0)},releaseScroll:function(){var e=this.capturedScroll,n=this.discardScroll();e.isComputed&&(n?t.extend(e,this.computeInitialScroll()):e=null),e&&(e.isInitial?this.hardSetScroll(e):this.setScroll(e))},discardScroll:function(){return!--this.capturedScrollDepth&&(this.capturedScroll=null,!0)},computeInitialScroll:function(){return{}},queryScroll:function(){return{}},hardSetScroll:function(t){var e=this,n=function(){e.setScroll(t)};n(),setTimeout(n,0)},setScroll:function(t){},freezeHeight:function(){this.calendar.freezeContentHeight()},thawHeight:function(){this.calendar.thawContentHeight()},bindEvents:function(){var t=this;this.isEventsBound||(this.isEventsBound=!0,this.rejectOn("eventsUnbind",this.requestEvents()).then(function(e){t.listenTo(t.calendar,"eventsReset",t.setEvents),t.setEvents(e)}))},unbindEvents:function(){this.isEventsBound&&(this.isEventsBound=!1,this.stopListeningTo(this.calendar,"eventsReset"),this.unsetEvents(),this.trigger("eventsUnbind"))},setEvents:function(t){var e=this.isEventSet;this.isEventsSet=!0,this.handleEvents(t,e),this.trigger(e?"eventsReset":"eventsSet",t)},unsetEvents:function(){this.isEventsSet&&(this.isEventsSet=!1,this.handleEventsUnset(),this.trigger("eventsUnset"))},whenEventsSet:function(){var t=this;return this.isEventsSet?Dt.resolve(this.getCurrentEvents()):new Dt(function(e){t.one("eventsSet",e)})},handleEvents:function(t,e){this.requestEventsRender(t)},handleEventsUnset:function(){this.requestEventsUnrender()},requestEventsRender:function(t){var e=this;return this.eventRenderQueue.add(function(){return e.executeEventsRender(t)})},requestEventsUnrender:function(){var t=this;return this.isEventsRendered?this.eventRenderQueue.addQuickly(function(){return t.executeEventsUnrender()}):Dt.resolve()},requestCurrentEventsRender:function(){return this.isEventsSet?void this.requestEventsRender(this.getCurrentEvents()):Dt.reject()},executeEventsRender:function(t){var e=this;return this.captureScroll(),this.freezeHeight(),this.executeEventsUnrender().then(function(){e.renderEvents(t),e.thawHeight(),e.releaseScroll(),e.isEventsRendered=!0,e.onEventsRender(),e.trigger("eventsRender")})},executeEventsUnrender:function(){return this.isEventsRendered&&(this.onBeforeEventsUnrender(),this.captureScroll(),this.freezeHeight(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.thawHeight(),this.releaseScroll(),this.isEventsRendered=!1,this.trigger("eventsUnrender")),Dt.resolve()},onEventsRender:function(){this.renderedEventSegEach(function(t){this.publiclyTrigger("eventAfterRender",t.event,t.event,t.el)}),this.publiclyTrigger("eventAfterAllRender")},onBeforeEventsUnrender:function(){this.renderedEventSegEach(function(t){this.publiclyTrigger("eventDestroy",t.event,t.event,t.el)})},renderEvents:function(t){},unrenderEvents:function(){},requestEvents:function(){return this.calendar.requestEvents(this.activeRange.start,this.activeRange.end)},getCurrentEvents:function(){return this.calendar.getPrunedEventCache()},resolveEventEl:function(e,n){var i=this.publiclyTrigger("eventRender",e,e,n);return i===!1?n=null:i&&i!==!0&&(n=t(i)),n},showEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","hidden")},t)},renderedEventSegEach:function(t,e){var n,i=this.getEventSegs();for(n=0;n<i.length;n++)e&&i[n].event._id!==e._id||i[n].el&&t.call(this,i[n])},getEventSegs:function(){return[]},isEventDraggable:function(t){return this.isEventStartEditable(t)},isEventStartEditable:function(t){return ut(t.startEditable,(t.source||{}).startEditable,this.opt("eventStartEditable"),this.isEventGenerallyEditable(t))},isEventGenerallyEditable:function(t){return ut(t.editable,(t.source||{}).editable,this.opt("editable"))},reportSegDrop:function(t,e,n,i,r){var s=this.calendar,o=s.mutateSeg(t,e,n),l=function(){o.undo(),s.reportEventChange()};this.triggerEventDrop(t.event,o.dateDelta,l,i,r),s.reportEventChange()},triggerEventDrop:function(t,e,n,i,r){this.publiclyTrigger("eventDrop",i[0],t,e,n,r,{})},reportExternalDrop:function(e,n,i,r,s){var o,l,a=e.eventProps;a&&(o=t.extend({},a,n),l=this.calendar.renderEvent(o,e.stick)[0]),this.triggerExternalDrop(l,n,i,r,s)},triggerExternalDrop:function(t,e,n,i,r){this.publiclyTrigger("drop",n[0],e.start,i,r),t&&this.publiclyTrigger("eventReceive",null,t)},renderDrag:function(t,e){},unrenderDrag:function(){},isEventResizableFromStart:function(t){return this.opt("eventResizableFromStart")&&this.isEventResizable(t)},isEventResizableFromEnd:function(t){return this.isEventResizable(t)},isEventResizable:function(t){var e=t.source||{};return ut(t.durationEditable,e.durationEditable,this.opt("eventDurationEditable"),t.editable,e.editable,this.opt("editable"))},reportSegResize:function(t,e,n,i,r){var s=this.calendar,o=s.mutateSeg(t,e,n),l=function(){o.undo(),s.reportEventChange()};this.triggerEventResize(t.event,o.durationDelta,l,i,r),s.reportEventChange()},triggerEventResize:function(t,e,n,i,r){this.publiclyTrigger("eventResize",i[0],t,e,n,r,{})},select:function(t,e){this.unselect(e),this.renderSelection(t),this.reportSelection(t,e)},renderSelection:function(t){},reportSelection:function(t,e){this.isSelected=!0,this.triggerSelect(t,e)},triggerSelect:function(t,e){this.publiclyTrigger("select",null,this.calendar.applyTimezone(t.start),this.calendar.applyTimezone(t.end),e)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection&&this.destroySelection(),this.unrenderSelection(),this.publiclyTrigger("unselect",null,t))},unrenderSelection:function(){},selectEvent:function(t){this.selectedEvent&&this.selectedEvent===t||(this.unselectEvent(),this.renderedEventSegEach(function(t){t.el.addClass("fc-selected")},t),this.selectedEvent=t)},unselectEvent:function(){this.selectedEvent&&(this.renderedEventSegEach(function(t){t.el.removeClass("fc-selected")},this.selectedEvent),this.selectedEvent=null)},isEventSelected:function(t){return this.selectedEvent&&this.selectedEvent._id===t._id},handleDocumentMousedown:function(t){w(t)&&this.processUnselect(t)},processUnselect:function(t){this.processRangeUnselect(t),this.processEventUnselect(t)},processRangeUnselect:function(e){var n;this.isSelected&&this.opt("unselectAuto")&&(n=this.opt("unselectCancel"),n&&t(e.target).closest(n).length||this.unselect(e))},processEventUnselect:function(e){this.selectedEvent&&(t(e.target).closest(".fc-selected").length||this.unselectEvent())},triggerDayClick:function(t,e,n){this.publiclyTrigger("dayClick",e,this.calendar.applyTimezone(t.start),n)},computeDayRange:function(t){var e,n=t.start.clone().stripTime(),i=t.end,r=null;return i&&(r=i.clone().stripTime(),e=+i.time(),e&&e>=this.nextDayThreshold&&r.add(1,"days")),(!i||r<=n)&&(r=n.clone().add(1,"days")),{start:n,end:r}},isMultiDayEvent:function(t){var e=this.computeDayRange(t);return e.end.diff(e.start,"days")>1}});be.mixin({currentRange:null,currentRangeUnit:null,renderRange:null,activeRange:null,validRange:null,dateIncrement:null,currentDate:null,minTime:null,maxTime:null,usesMinMaxTime:!1,start:null,end:null,intervalStart:null,intervalEnd:null,isSameDateProfile:function(t){return this.activeRange&&X(this.activeRange,t.activeRange)},setDateProfile:function(t){this.currentRange=t.currentRange,this.currentRangeUnit=t.currentRangeUnit,this.renderRange=t.renderRange,this.activeRange=t.activeRange,this.validRange=t.validRange,this.dateIncrement=t.dateIncrement,this.currentDate=t.date,this.minTime=t.minTime,this.maxTime=t.maxTime,this.start=t.activeRange.start,this.end=t.activeRange.end,this.intervalStart=t.currentRange.start,this.intervalEnd=t.currentRange.end},buildPrevDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement);return this.buildDateProfile(e,-1)},buildNextDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).add(this.dateIncrement);return this.buildDateProfile(e,1)},buildDateProfile:function(t,n,i){var r,s,o,l,a=this.buildValidRange(),u=null,c=null;return i&&(t=q(t,a)),r=this.buildCurrentRangeInfo(t,n),s=this.buildRenderRange(r.range,r.unit),o=U(s),this.opt("showNonCurrentDates")||(o=j(o,r.range)),u=e.duration(this.opt("minTime")),c=e.duration(this.opt("maxTime")),this.adjustActiveRange(o,u,c),o=j(o,a),t=q(t,o),l=$(r.range,a),{validRange:a,currentRange:r.range,currentRangeUnit:r.unit,activeRange:o,renderRange:s,minTime:u,maxTime:c,isValid:l,date:t,dateIncrement:this.buildDateIncrement(r.duration)}},buildValidRange:function(){return this.getRangeOption("validRange",this.calendar.getNow())||{}},buildCurrentRangeInfo:function(t,e){var n,i=null,r=null,s=null;return this.viewSpec.duration?(i=this.viewSpec.duration,r=this.viewSpec.durationUnit,s=this.buildRangeFromDuration(t,e,i,r)):(n=this.opt("dayCount"))?(r="day",s=this.buildRangeFromDayCount(t,e,n)):(s=this.buildCustomVisibleRange(t))?r=O(s.start,s.end):(i=this.getFallbackDuration(),r=O(i),s=this.buildRangeFromDuration(t,e,i,r)),this.normalizeCurrentRange(s,r),{duration:i,unit:r,range:s}},getFallbackDuration:function(){return e.duration({days:1})},normalizeCurrentRange:function(t,e){/^(year|month|week|day)$/.test(e)?(t.start.stripTime(),t.end.stripTime()):(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0))},adjustActiveRange:function(t,e,n){var i=!1;this.usesMinMaxTime&&(e<0&&(t.start.time(0).add(e),i=!0),n>864e5&&(t.end.time(n-864e5),i=!0),i&&(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0)))},buildRangeFromDuration:function(t,n,i,r){var s,o,l,a=this.opt("dateAlignment"),u=t.clone();return i.as("days")<=1&&this.isHiddenDay(u)&&(u=this.skipHiddenDays(u,n),u.startOf("day")),a||(o=this.opt("dateIncrement"),o?(l=e.duration(o),a=l<i?V(l,o):r):a=r),u.startOf(a),s=u.clone().add(i),{start:u,end:s}},buildRangeFromDayCount:function(t,e,n){var i,r=this.opt("dateAlignment"),s=0,o=t.clone();r&&o.startOf(r),o.startOf("day"),o=this.skipHiddenDays(o,e),i=o.clone();do i.add(1,"day"),this.isHiddenDay(i)||s++;while(s<n);return{start:o,end:i}},buildCustomVisibleRange:function(t){var e=this.getRangeOption("visibleRange",this.calendar.moment(t));return!e||e.start&&e.end?e:null},buildRenderRange:function(t,e){return this.trimHiddenDays(t)},buildDateIncrement:function(t){var n,i=this.opt("dateIncrement");return i?e.duration(i):(n=this.opt("dateAlignment"))?e.duration(1,n):t?t:e.duration({days:1})},trimHiddenDays:function(t){return{start:this.skipHiddenDays(t.start),end:this.skipHiddenDays(t.end,-1,!0)}},currentRangeAs:function(t){var e=this.currentRange;return e.end.diff(e.start,t,!0)},getRangeOption:function(t){var e=this.opt(t);if("function"==typeof e&&(e=e.apply(null,Array.prototype.slice.call(arguments,1))),e)return this.calendar.parseRange(e)},initHiddenDays:function(){var e,n=this.opt("hiddenDays")||[],i=[],r=0;for(this.opt("weekends")===!1&&n.push(0,6),e=0;e<7;e++)(i[e]=t.inArray(e,n)!==-1)||r++;if(!r)throw"invalid hiddenDays";this.isHiddenDayHash=i},isHiddenDay:function(t){return e.isMoment(t)&&(t=t.day()),this.isHiddenDayHash[t]},skipHiddenDays:function(t,e,n){var i=t.clone();for(e=e||1;this.isHiddenDayHash[(i.day()+(n?e:0)+7)%7];)i.add(e,"days");return i}});var Ee=Zt.Scroller=wt.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(t){t=t||{},this.overflowX=t.overflowX||t.overflow||"auto",this.overflowY=t.overflowY||t.overflow||"auto"},render:function(){this.el=this.renderEl(),this.applyOverflow()},renderEl:function(){return this.scrollEl=t('<div class="fc-scroller"></div>')},clear:function(){this.setHeight("auto"),this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(t){var e=this.overflowX,n=this.overflowY;t=t||this.getScrollbarWidths(),"auto"===e&&(e=t.top||t.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"),"auto"===n&&(n=t.left||t.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"),this.scrollEl.css({"overflow-x":e,"overflow-y":n})},setHeight:function(t){this.scrollEl.height(t)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(t){this.scrollEl.scrollTop(t)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return p(this.scrollEl)}});Pt.prototype.proxyCall=function(t){var e=Array.prototype.slice.call(arguments,1),n=[];return this.items.forEach(function(i){n.push(i[t].apply(i,e))}),n};var De=Zt.Calendar=wt.extend({dirDefaults:null,localeDefaults:null,overrides:null,dynamicOverrides:null,options:null,viewSpecCache:null,view:null,currentDate:null,header:null,footer:null,loadingLevel:0,constructor:Yt,initialize:function(){},populateOptionsHash:function(){var t,e,i,r;t=ut(this.dynamicOverrides.locale,this.overrides.locale),e=Te[t],e||(t=De.defaults.locale,e=Te[t]||{}),i=ut(this.dynamicOverrides.isRTL,this.overrides.isRTL,e.isRTL,De.defaults.isRTL),r=i?De.rtlDefaults:{},this.dirDefaults=r,this.localeDefaults=e,this.options=n([De.defaults,r,e,this.overrides,this.dynamicOverrides]),Wt(this.options)},getViewSpec:function(t){var e=this.viewSpecCache;return e[t]||(e[t]=this.buildViewSpec(t))},getUnitViewSpec:function(e){var n,i,r;if(t.inArray(e,Jt)!=-1)for(n=this.header.getViewsWithButtons(),t.each(Zt.views,function(t){n.push(t)}),i=0;i<n.length;i++)if(r=this.getViewSpec(n[i]),r&&r.singleUnit==e)return r},buildViewSpec:function(t){for(var i,r,s,o,l,a=this.overrides.views||{},u=[],c=[],d=[],h=t;h;)i=$t[h],r=a[h],h=null,"function"==typeof i&&(i={class:i}),i&&(u.unshift(i),c.unshift(i.defaults||{}),s=s||i.duration,h=h||i.type),r&&(d.unshift(r),s=s||r.duration,h=h||r.type);return i=it(u),i.type=t,!!i.class&&(s=s||this.dynamicOverrides.duration||this.overrides.duration,s&&(o=e.duration(s),o.valueOf()&&(l=V(o,s),i.duration=o,i.durationUnit=l,1===o.as(l)&&(i.singleUnit=l,d.unshift(a[l]||{})))),i.defaults=n(c),i.overrides=n(d),this.buildViewSpecOptions(i),this.buildViewSpecButtonText(i,t),i)},buildViewSpecOptions:function(t){t.options=n([De.defaults,t.defaults,this.dirDefaults,this.localeDefaults,this.overrides,t.overrides,this.dynamicOverrides]),Wt(t.options)},buildViewSpecButtonText:function(t,e){function n(n){var i=n.buttonText||{};return i[e]||(t.buttonTextKey?i[t.buttonTextKey]:null)||(t.singleUnit?i[t.singleUnit]:null)}t.buttonTextOverride=n(this.dynamicOverrides)||n(this.overrides)||t.overrides.buttonText,t.buttonTextDefault=n(this.localeDefaults)||n(this.dirDefaults)||t.defaults.buttonText||n(De.defaults)||(t.duration?this.humanizeDuration(t.duration):null)||e},instantiateView:function(t){var e=this.getViewSpec(t);return new e.class(this,e)},isValidViewType:function(t){return Boolean(this.getViewSpec(t))},pushLoading:function(){this.loadingLevel++||this.publiclyTrigger("loading",null,!0,this.view)},popLoading:function(){--this.loadingLevel||this.publiclyTrigger("loading",null,!1,this.view)},buildSelectSpan:function(t,e){var n,i=this.moment(t).stripZone();return n=e?this.moment(e).stripZone():i.hasTime()?i.clone().add(this.defaultTimedEventDuration):i.clone().add(this.defaultAllDayEventDuration),{start:i,end:n}},initCurrentDate:function(){null!=this.options.defaultDate?this.currentDate=this.moment(this.options.defaultDate).stripZone():this.currentDate=this.getNow()},changeView:function(t,e){e&&(e.start&&e.end?this.recordOptionOverrides({visibleRange:e}):this.currentDate=this.moment(e).stripZone()),this.renderView(t)},prev:function(){var t=this.view.buildPrevDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},next:function(){var t=this.view.buildNextDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},prevYear:function(){this.currentDate.add(-1,"years"),this.renderView()},nextYear:function(){this.currentDate.add(1,"years"),this.renderView()},today:function(){this.currentDate=this.getNow(),this.renderView()},gotoDate:function(t){this.currentDate=this.moment(t).stripZone(),this.renderView()},incrementDate:function(t){this.currentDate.add(e.duration(t)),this.renderView()},getDate:function(){return this.applyTimezone(this.currentDate)},parseRange:function(t){var e=null,n=null;return t.start&&(e=this.moment(t.start).stripZone()),t.end&&(n=this.moment(t.end).stripZone()),e||n?e&&n&&n.isBefore(e)?null:{start:e,end:n}:null}});De.mixin(ue),De.mixin({optionHandlers:null,bindOption:function(t,e){this.bindOptions([t],e)},bindOptions:function(t,e){var n,i={func:e,names:t};for(n=0;n<t.length;n++)this.registerOptionHandlerObj(t[n],i);this.triggerOptionHandlerObj(i)},registerOptionHandlerObj:function(t,e){(this.optionHandlers[t]||(this.optionHandlers[t]=[])).push(e)},triggerOptionHandlers:function(t){var e,n=this.optionHandlers[t]||[];for(e=0;e<n.length;e++)this.triggerOptionHandlerObj(n[e])},triggerOptionHandlerObj:function(t){var e,n=t.names,i=[];for(e=0;e<n.length;e++)i.push(this.options[n[e]]);t.func.apply(this,i)}}),De.defaults={titleRangeSeparator:" – ",monthYearFormat:"MMMM YYYY",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",scrollTime:"06:00:00",minTime:"00:00:00",maxTime:"24:00:00",showNonCurrentDates:!0,lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,isRTL:!1,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"year",today:"today",month:"month",week:"week",day:"day"},buttonIcons:{prev:"left-single-arrow",next:"right-single-arrow",prevYear:"left-double-arrow",nextYear:"right-double-arrow"},allDayText:"all-day",theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventOrder:"title",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:100,longPressDelay:1e3},De.englishDefaults={dayPopoverFormat:"dddd, MMMM D"},De.rtlDefaults={header:{left:"next,prev today",center:"",right:"title"},buttonIcons:{prev:"right-single-arrow",next:"left-single-arrow",prevYear:"right-double-arrow",nextYear:"left-double-arrow"},themeButtonIcons:{prev:"circle-triangle-e",next:"circle-triangle-w",nextYear:"seek-prev",prevYear:"seek-next"}};var Te=Zt.locales={};Zt.datepickerLocale=function(e,n,i){var r=Te[e]||(Te[e]={});r.isRTL=i.isRTL,r.weekNumberTitle=i.weekHeader,t.each(Ce,function(t,e){r[t]=e(i)}),t.datepicker&&(t.datepicker.regional[n]=t.datepicker.regional[e]=i,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(i))},Zt.locale=function(e,i){var r,s;r=Te[e]||(Te[e]={}),i&&(r=Te[e]=n([r,i])),s=Ut(e),t.each(He,function(t,e){null==r[t]&&(r[t]=e(s,r))}),De.defaults.locale=e};var Ce={buttonText:function(t){return{prev:dt(t.prevText),next:dt(t.nextText),today:dt(t.currentText)}},monthYearFormat:function(t){return t.showMonthAfterYear?"YYYY["+t.yearSuffix+"] MMMM":"MMMM YYYY["+t.yearSuffix+"]"}},He={dayOfMonthFormat:function(t,e){var n=t.longDateFormat("l");return n=n.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),e.isRTL?n+=" ddd":n="ddd "+n,n},mediumTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"a")},smallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")},extraSmallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")},hourFormat:function(t){return t.longDateFormat("LT").replace(":mm","").replace(/(\Wmm)$/,"").replace(/\s*a$/i,"a")},noMeridiemTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"")}},Re={smallDayDateFormat:function(t){return t.isRTL?"D dd":"dd D"},weekFormat:function(t){return t.isRTL?"w[ "+t.weekNumberTitle+"]":"["+t.weekNumberTitle+" ]w"},smallWeekFormat:function(t){return t.isRTL?"w["+t.weekNumberTitle+"]":"["+t.weekNumberTitle+"]w"}};Zt.locale("en",De.englishDefaults),Zt.sourceNormalizers=[],Zt.sourceFetchers=[];var xe={dataType:"json",cache:!1},Ie=1;De.prototype.mutateSeg=function(t,e){return this.mutateEvent(t.event,e)},De.prototype.normalizeEvent=function(t){},De.prototype.spanContainsSpan=function(t,e){var n=t.start.clone().stripZone(),i=this.getEventEnd(t).stripZone();return e.start>=n&&e.end<=i},De.prototype.getPeerEvents=function(t,e){var n,i,r=this.getEventCache(),s=[];for(n=0;n<r.length;n++)i=r[n],e&&e._id===i._id||s.push(i);return s},De.prototype.isEventSpanAllowed=function(t,e){var n=e.source||{},i=ut(e.constraint,n.constraint,this.options.eventConstraint),r=ut(e.overlap,n.overlap,this.options.eventOverlap);return this.isSpanAllowed(t,i,r,e)&&(!this.options.eventAllow||this.options.eventAllow(t,e)!==!1)},De.prototype.isExternalSpanAllowed=function(e,n,i){var r,s;return i&&(r=t.extend({},i,n),s=this.expandEvent(this.buildEventFromInput(r))[0]),s?this.isEventSpanAllowed(e,s):this.isSelectionSpanAllowed(e)},De.prototype.isSelectionSpanAllowed=function(t){return this.isSpanAllowed(t,this.options.selectConstraint,this.options.selectOverlap)&&(!this.options.selectAllow||this.options.selectAllow(t)!==!1)},De.prototype.isSpanAllowed=function(t,e,n,i){var r,s,o,l,a,u;if(null!=e&&(r=this.constraintToEvents(e))){for(s=!1,l=0;l<r.length;l++)if(this.spanContainsSpan(r[l],t)){s=!0;break}if(!s)return!1}for(o=this.getPeerEvents(t,i),l=0;l<o.length;l++)if(a=o[l],this.eventIntersectsRange(a,t)){if(n===!1)return!1;if("function"==typeof n&&!n(a,i))return!1;if(i){if(u=ut(a.overlap,(a.source||{}).overlap),u===!1)return!1;if("function"==typeof u&&!u(i,a))return!1}}return!0},De.prototype.constraintToEvents=function(t){return"businessHours"===t?this.getCurrentBusinessHourEvents():"object"==typeof t?null!=t.start?this.expandEvent(this.buildEventFromInput(t)):null:this.clientEvents(t)},De.prototype.eventIntersectsRange=function(t,e){var n=t.start.clone().stripZone(),i=this.getEventEnd(t).stripZone();return e.start<i&&e.end>n};var ke={id:"_fcBusinessHours",start:"09:00",end:"17:00",dow:[1,2,3,4,5],rendering:"inverse-background"};De.prototype.getCurrentBusinessHourEvents=function(t){return this.computeBusinessHourEvents(t,this.options.businessHours)},De.prototype.computeBusinessHourEvents=function(e,n){return n===!0?this.expandBusinessHourEvents(e,[{}]):t.isPlainObject(n)?this.expandBusinessHourEvents(e,[n]):t.isArray(n)?this.expandBusinessHourEvents(e,n,!0):[]},De.prototype.expandBusinessHourEvents=function(e,n,i){var r,s,o=this.getView(),l=[];for(r=0;r<n.length;r++)s=n[r],i&&!s.dow||(s=t.extend({},ke,s),e&&(s.start=null,s.end=null),l.push.apply(l,this.expandEvent(this.buildEventFromInput(s),o.activeRange.start,o.activeRange.end)));return l};var Le=Zt.BasicView=be.extend({scroller:null,dayGridClass:Se,dayGrid:null,dayNumbersVisible:!1,colWeekNumbersVisible:!1,cellWeekNumbersVisible:!1,weekNumberWidth:null,headContainerEl:null,headRowEl:null,initialize:function(){this.dayGrid=this.instantiateDayGrid(),this.scroller=new Ee({overflowX:"hidden",overflowY:"auto"})},instantiateDayGrid:function(){var t=this.dayGridClass.extend(Me);return new t(this)},buildRenderRange:function(t,e){var n=be.prototype.buildRenderRange.apply(this,arguments);return/^(year|month)$/.test(e)&&(n.start.startOf("week"),n.end.weekday()&&n.end.add(1,"week").startOf("week")),this.trimHiddenDays(n)},renderDates:function(){this.dayGrid.breakOnWeeks=/year|month|week/.test(this.currentRangeUnit),this.dayGrid.setRange(this.renderRange),this.dayNumbersVisible=this.dayGrid.rowCnt>1,this.opt("weekNumbers")&&(this.opt("weekNumbersWithinDays")?(this.cellWeekNumbersVisible=!0,this.colWeekNumbersVisible=!1):(this.cellWeekNumbersVisible=!1,this.colWeekNumbersVisible=!0)),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.cellWeekNumbersVisible||this.colWeekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-day-grid-container"),n=t('<div class="fc-day-grid" />').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.dayGrid.setElement(n),this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml()),this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement(),this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.dayGrid.unrenderBusinessHours()},renderSkeletonHtml:function(){return'<table><thead class="fc-head"><tr><td class="fc-head-container '+this.widgetHeaderClass+'"></td></tr></thead><tbody class="fc-body"><tr><td class="'+this.widgetContentClass+'"></td></tr></tbody></table>'},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.colWeekNumbersVisible&&(this.weekNumberWidth=u(this.el.find(".fc-week-number")))},setHeight:function(t,e){var n,s,o=this.opt("eventLimit");this.scroller.clear(),r(this.headRowEl),this.dayGrid.removeSegPopover(),o&&"number"==typeof o&&this.dayGrid.limitRows(o),n=this.computeScrollerHeight(t),this.setGridHeight(n,e),o&&"number"!=typeof o&&this.dayGrid.limitRows(o),e||(this.scroller.setHeight(n),s=this.scroller.getScrollbarWidths(),
+(s.left||s.right)&&(i(this.headRowEl,s),n=this.computeScrollerHeight(t),this.scroller.setHeight(n)),this.scroller.lockOverflow(s))},computeScrollerHeight:function(t){return t-c(this.el,this.scroller.el)},setGridHeight:function(t,e){e?a(this.dayGrid.rowEls):l(this.dayGrid.rowEls,t,!0)},computeInitialScroll:function(){return{top:0}},queryScroll:function(){return{top:this.scroller.getScrollTop()}},setScroll:function(t){this.scroller.setScrollTop(t.top)},hitsNeeded:function(){this.dayGrid.hitsNeeded()},hitsNotNeeded:function(){this.dayGrid.hitsNotNeeded()},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(t,e){return this.dayGrid.queryHit(t,e)},getHitSpan:function(t){return this.dayGrid.getHitSpan(t)},getHitEl:function(t){return this.dayGrid.getHitEl(t)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(t,e){return this.dayGrid.renderDrag(t,e)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(t){this.dayGrid.renderSelection(t)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),Me={renderHeadIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<th class="fc-week-number '+t.widgetHeaderClass+'" '+t.weekNumberStyleAttr()+"><span>"+ct(t.opt("weekNumberTitle"))+"</span></th>":""},renderNumberIntroHtml:function(t){var e=this.view,n=this.getCellDate(t,0);return e.colWeekNumbersVisible?'<td class="fc-week-number" '+e.weekNumberStyleAttr()+">"+e.buildGotoAnchorHtml({date:n,type:"week",forceOff:1===this.colCnt},n.format("w"))+"</td>":""},renderBgIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<td class="fc-week-number '+t.widgetContentClass+'" '+t.weekNumberStyleAttr()+"></td>":""},renderIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<td class="fc-week-number" '+t.weekNumberStyleAttr()+"></td>":""}},Be=Zt.MonthView=Le.extend({buildRenderRange:function(){var t,e=Le.prototype.buildRenderRange.apply(this,arguments);return this.isFixedWeeks()&&(t=Math.ceil(e.end.diff(e.start,"weeks",!0)),e.end.add(6-t,"weeks")),e},setGridHeight:function(t,e){e&&(t*=this.rowCnt/6),l(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){return this.opt("fixedWeekCount")}});$t.basic={class:Le},$t.basicDay={type:"basic",duration:{days:1}},$t.basicWeek={type:"basic",duration:{weeks:1}},$t.month={class:Be,duration:{months:1},defaults:{fixedWeekCount:!0}};var Fe=Zt.AgendaView=be.extend({scroller:null,timeGridClass:we,timeGrid:null,dayGridClass:Se,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,usesMinMaxTime:!0,initialize:function(){this.timeGrid=this.instantiateTimeGrid(),this.opt("allDaySlot")&&(this.dayGrid=this.instantiateDayGrid()),this.scroller=new Ee({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){var t=this.timeGridClass.extend(Ne);return new t(this)},instantiateDayGrid:function(){var t=this.dayGridClass.extend(ze);return new t(this)},renderDates:function(){this.timeGrid.setRange(this.renderRange),this.dayGrid&&this.dayGrid.setRange(this.renderRange),this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-time-grid-container"),n=t('<div class="fc-time-grid" />').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.timeGrid.setElement(n),this.timeGrid.renderDates(),this.bottomRuleEl=t('<hr class="fc-divider '+this.widgetHeaderClass+'"/>').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement()),this.scroller.destroy()},renderSkeletonHtml:function(){return'<table><thead class="fc-head"><tr><td class="fc-head-container '+this.widgetHeaderClass+'"></td></tr></thead><tbody class="fc-body"><tr><td class="'+this.widgetContentClass+'">'+(this.dayGrid?'<div class="fc-day-grid"/><hr class="fc-divider '+this.widgetHeaderClass+'"/>':"")+"</td></tr></tbody></table>"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours(),this.dayGrid&&this.dayGrid.unrenderBusinessHours()},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(t){this.timeGrid.renderNowIndicator(t)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(t){this.timeGrid.updateSize(t),be.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=u(this.el.find(".fc-axis"))},setHeight:function(t,e){var n,s,o;this.bottomRuleEl.hide(),this.scroller.clear(),r(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),n=this.opt("eventLimit"),n&&"number"!=typeof n&&(n=Ae),n&&this.dayGrid.limitRows(n)),e||(s=this.computeScrollerHeight(t),this.scroller.setHeight(s),o=this.scroller.getScrollbarWidths(),(o.left||o.right)&&(i(this.noScrollRowEls,o),s=this.computeScrollerHeight(t),this.scroller.setHeight(s)),this.scroller.lockOverflow(o),this.timeGrid.getTotalSlatHeight()<s&&this.bottomRuleEl.show())},computeScrollerHeight:function(t){return t-c(this.el,this.scroller.el)},computeInitialScroll:function(){var t=e.duration(this.opt("scrollTime")),n=this.timeGrid.computeTimeTop(t);return n=Math.ceil(n),n&&n++,{top:n}},queryScroll:function(){return{top:this.scroller.getScrollTop()}},setScroll:function(t){this.scroller.setScrollTop(t.top)},hitsNeeded:function(){this.timeGrid.hitsNeeded(),this.dayGrid&&this.dayGrid.hitsNeeded()},hitsNotNeeded:function(){this.timeGrid.hitsNotNeeded(),this.dayGrid&&this.dayGrid.hitsNotNeeded()},prepareHits:function(){this.timeGrid.prepareHits(),this.dayGrid&&this.dayGrid.prepareHits()},releaseHits:function(){this.timeGrid.releaseHits(),this.dayGrid&&this.dayGrid.releaseHits()},queryHit:function(t,e){var n=this.timeGrid.queryHit(t,e);return!n&&this.dayGrid&&(n=this.dayGrid.queryHit(t,e)),n},getHitSpan:function(t){return t.component.getHitSpan(t)},getHitEl:function(t){return t.component.getHitEl(t)},renderEvents:function(t){var e,n,i=[],r=[],s=[];for(n=0;n<t.length;n++)t[n].allDay?i.push(t[n]):r.push(t[n]);e=this.timeGrid.renderEvents(r),this.dayGrid&&(s=this.dayGrid.renderEvents(i)),this.updateHeight()},getEventSegs:function(){return this.timeGrid.getEventSegs().concat(this.dayGrid?this.dayGrid.getEventSegs():[])},unrenderEvents:function(){this.timeGrid.unrenderEvents(),this.dayGrid&&this.dayGrid.unrenderEvents()},renderDrag:function(t,e){return t.start.hasTime()?this.timeGrid.renderDrag(t,e):this.dayGrid?this.dayGrid.renderDrag(t,e):void 0},unrenderDrag:function(){this.timeGrid.unrenderDrag(),this.dayGrid&&this.dayGrid.unrenderDrag()},renderSelection:function(t){t.start.hasTime()||t.end.hasTime()?this.timeGrid.renderSelection(t):this.dayGrid&&this.dayGrid.renderSelection(t)},unrenderSelection:function(){this.timeGrid.unrenderSelection(),this.dayGrid&&this.dayGrid.unrenderSelection()}}),Ne={renderHeadIntroHtml:function(){var t,e=this.view;return e.opt("weekNumbers")?(t=this.start.format(e.opt("smallWeekFormat")),'<th class="fc-axis fc-week-number '+e.widgetHeaderClass+'" '+e.axisStyleAttr()+">"+e.buildGotoAnchorHtml({date:this.start,type:"week",forceOff:this.colCnt>1},ct(t))+"</th>"):'<th class="fc-axis '+e.widgetHeaderClass+'" '+e.axisStyleAttr()+"></th>"},renderBgIntroHtml:function(){var t=this.view;return'<td class="fc-axis '+t.widgetContentClass+'" '+t.axisStyleAttr()+"></td>"},renderIntroHtml:function(){var t=this.view;return'<td class="fc-axis" '+t.axisStyleAttr()+"></td>"}},ze={renderBgIntroHtml:function(){var t=this.view;return'<td class="fc-axis '+t.widgetContentClass+'" '+t.axisStyleAttr()+"><span>"+t.getAllDayHtml()+"</span></td>"},renderIntroHtml:function(){var t=this.view;return'<td class="fc-axis" '+t.axisStyleAttr()+"></td>"}},Ae=5,Ge=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];$t.agenda={class:Fe,defaults:{allDaySlot:!0,slotDuration:"00:30:00",slotEventOverlap:!0}},$t.agendaDay={type:"agenda",duration:{days:1}},$t.agendaWeek={type:"agenda",duration:{weeks:1}};var Oe=be.extend({grid:null,scroller:null,initialize:function(){this.grid=new Ve(this),this.scroller=new Ee({overflowX:"hidden",overflowY:"auto"})},renderSkeleton:function(){this.el.addClass("fc-list-view "+this.widgetContentClass),this.scroller.render(),this.scroller.el.appendTo(this.el),this.grid.setElement(this.scroller.scrollEl)},unrenderSkeleton:function(){this.scroller.destroy()},setHeight:function(t,e){this.scroller.setHeight(this.computeScrollerHeight(t))},computeScrollerHeight:function(t){return t-c(this.el,this.scroller.el)},renderDates:function(){this.grid.setRange(this.renderRange)},renderEvents:function(t){this.grid.renderEvents(t)},unrenderEvents:function(){this.grid.unrenderEvents()},isEventResizable:function(t){return!1},isEventDraggable:function(t){return!1}}),Ve=me.extend({segSelector:".fc-list-item",hasDayInteractions:!1,spanToSegs:function(t){for(var e,n=this.view,i=n.renderRange.start.clone().time(0),r=0,s=[];i<n.renderRange.end;)if(e=N(t,{start:i,end:i.clone().add(1,"day")}),e&&(e.dayIndex=r,s.push(e)),i.add(1,"day"),r++,e&&!e.isEnd&&t.end.hasTime()&&t.end<i.clone().add(this.view.nextDayThreshold)){e.end=t.end.clone(),e.isEnd=!0;break}return s},computeEventTimeFormat:function(){return this.view.opt("mediumTimeFormat")},handleSegClick:function(e,n){var i;me.prototype.handleSegClick.apply(this,arguments),t(n.target).closest("a[href]").length||(i=e.event.url,i&&!n.isDefaultPrevented()&&(window.location.href=i))},renderFgSegs:function(t){return t=this.renderFgSegEls(t),t.length?this.renderSegList(t):this.renderEmptyMessage(),t},renderEmptyMessage:function(){this.el.html('<div class="fc-list-empty-wrap2"><div class="fc-list-empty-wrap1"><div class="fc-list-empty">'+ct(this.view.opt("noEventsMessage"))+"</div></div></div>")},renderSegList:function(e){var n,i,r,s=this.groupSegsByDay(e),o=t('<table class="fc-list-table"><tbody/></table>'),l=o.find("tbody");for(n=0;n<s.length;n++)if(i=s[n])for(l.append(this.dayHeaderHtml(this.view.renderRange.start.clone().add(n,"days"))),this.sortEventSegs(i),r=0;r<i.length;r++)l.append(i[r].el);this.el.empty().append(o)},groupSegsByDay:function(t){var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],(i[n.dayIndex]||(i[n.dayIndex]=[])).push(n);return i},dayHeaderHtml:function(t){var e=this.view,n=e.opt("listDayFormat"),i=e.opt("listDayAltFormat");return'<tr class="fc-list-heading" data-date="'+t.format("YYYY-MM-DD")+'"><td class="'+e.widgetHeaderClass+'" colspan="3">'+(n?e.buildGotoAnchorHtml(t,{class:"fc-list-heading-main"},ct(t.format(n))):"")+(i?e.buildGotoAnchorHtml(t,{class:"fc-list-heading-alt"},ct(t.format(i))):"")+"</td></tr>"},fgSegHtml:function(t){var e,n=this.view,i=["fc-list-item"].concat(this.getSegCustomClasses(t)),r=this.getSegBackgroundColor(t),s=t.event,o=s.url;return e=s.allDay?n.getAllDayHtml():n.isMultiDayEvent(s)?t.isStart||t.isEnd?ct(this.getEventTimeText(t)):n.getAllDayHtml():ct(this.getEventTimeText(s)),o&&i.push("fc-has-url"),'<tr class="'+i.join(" ")+'">'+(this.displayEventTime?'<td class="fc-list-item-time '+n.widgetContentClass+'">'+(e||"")+"</td>":"")+'<td class="fc-list-item-marker '+n.widgetContentClass+'"><span class="fc-event-dot"'+(r?' style="background-color:'+r+'"':"")+'></span></td><td class="fc-list-item-title '+n.widgetContentClass+'"><a'+(o?' href="'+ct(o)+'"':"")+">"+ct(t.event.title||"")+"</a></td></tr>"}});return $t.list={class:Oe,buttonTextKey:"list",defaults:{buttonText:"list",listDayFormat:"LL",noEventsMessage:"No events to display"}},$t.listDay={type:"list",duration:{days:1},defaults:{listDayFormat:"dddd"}},$t.listWeek={type:"list",duration:{weeks:1},defaults:{listDayFormat:"dddd",listDayAltFormat:"LL"}},$t.listMonth={type:"list",duration:{month:1},defaults:{listDayAltFormat:"dddd"}},$t.listYear={type:"list",duration:{year:1},defaults:{listDayAltFormat:"dddd"}},Zt}); \ No newline at end of file
diff --git a/plugins/Calendar/Assets/locale-all.js b/plugins/Calendar/Assets/locale-all.js
new file mode 100644
index 00000000..689a86e0
--- /dev/null
+++ b/plugins/Calendar/Assets/locale-all.js
@@ -0,0 +1,5 @@
+!function(e){"function"==typeof define&&define.amd?define(["jquery","moment"],e):"object"==typeof exports?module.exports=e(require("jquery"),require("moment")):e(jQuery,moment)}(function(e,a){!function(){!function(){var e=a.defineLocale("af",{months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),meridiemParse:/vm|nm/i,isPM:function(e){return/^nm$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"vm":"VM":t?"nm":"NM"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Vandag om] LT",nextDay:"[Môre om] LT",nextWeek:"dddd [om] LT",lastDay:"[Gister om] LT",lastWeek:"[Laas] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("af","af",{closeText:"Selekteer",prevText:"Vorige",nextText:"Volgende",currentText:"Vandag",monthNames:["Januarie","Februarie","Maart","April","Mei","Junie","Julie","Augustus","September","Oktober","November","Desember"],monthNamesShort:["Jan","Feb","Mrt","Apr","Mei","Jun","Jul","Aug","Sep","Okt","Nov","Des"],dayNames:["Sondag","Maandag","Dinsdag","Woensdag","Donderdag","Vrydag","Saterdag"],dayNamesShort:["Son","Maa","Din","Woe","Don","Vry","Sat"],dayNamesMin:["So","Ma","Di","Wo","Do","Vr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("af",{buttonText:{year:"Jaar",month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayHtml:"Heeldag",eventLimitText:"Addisionele",noEventsMessage:"Daar is geen gebeurtenis"})}(),function(){!function(){var e={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},t={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},n=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},r={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},s=function(e){return function(a,t,s,d){var i=n(a),o=r[e][n(a)];return 2===i&&(o=o[t?0:1]),o.replace(/%d/i,a)}},d=["كانون الثاني يناير","شباط فبراير","آذار مارس","نيسان أبريل","أيار مايو","حزيران يونيو","تموز يوليو","آب أغسطس","أيلول سبتمبر","تشرين الأول أكتوبر","تشرين الثاني نوفمبر","كانون الأول ديسمبر"],i=a.defineLocale("ar",{months:d,monthsShort:d,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/‏M/‏YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(e){return"م"===e},meridiem:function(e,a,t){return e<12?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:s("s"),m:s("m"),mm:s("m"),h:s("h"),hh:s("h"),d:s("d"),dd:s("d"),M:s("M"),MM:s("M"),y:s("y"),yy:s("y")},preparse:function(e){return e.replace(/\u200f/g,"").replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(e){return t[e]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return e[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return i}(),e.fullCalendar.datepickerLocale("ar","ar",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e=a.defineLocale("ar-dz",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اثنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"أح_إث_ثلا_أر_خم_جم_سب".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:0,doy:4}});return e}(),e.fullCalendar.datepickerLocale("ar-dz","ar-DZ",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويلية","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar-dz",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",0:"0"},t=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},n={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},r=function(e){return function(a,r,s,d){var i=t(a),o=n[e][t(a)];return 2===i&&(o=o[r?0:1]),o.replace(/%d/i,a)}},s=["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],d=a.defineLocale("ar-ly",{months:s,monthsShort:s,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/‏M/‏YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(e){return"م"===e},meridiem:function(e,a,t){return e<12?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:r("s"),m:r("m"),mm:r("m"),h:r("h"),hh:r("h"),d:r("d"),dd:r("d"),M:r("M"),MM:r("M"),y:r("y"),yy:r("y")},preparse:function(e){return e.replace(/\u200f/g,"").replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return e[a]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return d}(),e.fullCalendar.datepickerLocale("ar-ly","ar",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar-ly",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e=a.defineLocale("ar-ma",{months:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),weekdays:"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:6,doy:12}});return e}(),e.fullCalendar.datepickerLocale("ar-ma","ar",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar-ma",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},t={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},n=a.defineLocale("ar-sa",{months:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(e){return"م"===e},meridiem:function(e,a,t){return e<12?"ص":"م"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},preparse:function(e){return e.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(e){return t[e]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return e[a]}).replace(/,/g,"،")},week:{dow:0,doy:6}});return n}(),e.fullCalendar.datepickerLocale("ar-sa","ar",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar-sa",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e=a.defineLocale("ar-tn",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("ar-tn","ar",{closeText:"إغلاق",prevText:"&#x3C;السابق",nextText:"التالي&#x3E;",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ar-tn",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"})}(),function(){!function(){var e=a.defineLocale("bg",{months:"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"),monthsShort:"янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"),weekdays:"неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"),weekdaysShort:"нед_пон_вто_сря_чет_пет_съб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[Днес в] LT",nextDay:"[Утре в] LT",nextWeek:"dddd [в] LT",lastDay:"[Вчера в] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[В изминалата] dddd [в] LT";case 1:case 2:case 4:case 5:return"[В изминалия] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"след %s",past:"преди %s",s:"няколко секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дни",M:"месец",MM:"%d месеца",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-ев":0===t?e+"-ен":t>10&&t<20?e+"-ти":1===a?e+"-ви":2===a?e+"-ри":7===a||8===a?e+"-ми":e+"-ти"},week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("bg","bg",{closeText:"затвори",prevText:"&#x3C;назад",nextText:"напред&#x3E;",nextBigText:"&#x3E;&#x3E;",currentText:"днес",monthNames:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthNamesShort:["Яну","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Нов","Дек"],dayNames:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],dayNamesShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],dayNamesMin:["Не","По","Вт","Ср","Че","Пе","Съ"],weekHeader:"Wk",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("bg",{buttonText:{month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",eventLimitText:function(e){return"+още "+e},noEventsMessage:"Няма събития за показване"})}(),function(){!function(){var e=a.defineLocale("ca",{months:"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),monthsShort:"gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd D MMMM YYYY H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[demà a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"d'aquí %s",past:"fa %s",s:"uns segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},ordinalParse:/\d{1,2}(r|n|t|è|a)/,ordinal:function(e,a){var t=1===e?"r":2===e?"n":3===e?"r":4===e?"t":"è";return"w"!==a&&"W"!==a||(t="a"),e+t},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("ca","ca",{closeText:"Tanca",prevText:"Anterior",nextText:"Següent",currentText:"Avui",monthNames:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthNamesShort:["gen","feb","març","abr","maig","juny","jul","ag","set","oct","nov","des"],dayNames:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],dayNamesShort:["dg","dl","dt","dc","dj","dv","ds"],dayNamesMin:["dg","dl","dt","dc","dj","dv","ds"],weekHeader:"Set",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ca",{buttonText:{month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},allDayText:"Tot el dia",eventLimitText:"més",noEventsMessage:"No hi ha esdeveniments per mostrar"})}(),function(){!function(){function e(e){return e>1&&e<5&&1!==~~(e/10)}function t(a,t,n,r){var s=a+" ";switch(n){case"s":return t||r?"pár sekund":"pár sekundami";case"m":return t?"minuta":r?"minutu":"minutou";case"mm":return t||r?s+(e(a)?"minuty":"minut"):s+"minutami";case"h":return t?"hodina":r?"hodinu":"hodinou";case"hh":return t||r?s+(e(a)?"hodiny":"hodin"):s+"hodinami";case"d":return t||r?"den":"dnem";case"dd":return t||r?s+(e(a)?"dny":"dní"):s+"dny";case"M":return t||r?"měsíc":"měsícem";case"MM":return t||r?s+(e(a)?"měsíce":"měsíců"):s+"měsíci";case"y":return t||r?"rok":"rokem";case"yy":return t||r?s+(e(a)?"roky":"let"):s+"lety"}}var n="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),r="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"),s=a.defineLocale("cs",{months:n,monthsShort:r,monthsParse:function(e,a){var t,n=[];for(t=0;t<12;t++)n[t]=new RegExp("^"+e[t]+"$|^"+a[t]+"$","i");return n}(n,r),shortMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(r),longMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(n),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm",l:"D. M. YYYY"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:t,m:t,mm:t,h:t,hh:t,d:t,dd:t,M:t,MM:t,y:t,yy:t},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("cs","cs",{closeText:"Zavřít",prevText:"&#x3C;Dříve",nextText:"Později&#x3E;",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(e){return"+další: "+e},noEventsMessage:"Žádné akce k zobrazení"})}(),function(){!function(){var e=a.defineLocale("da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY HH:mm"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("da","da",{closeText:"Luk",prevText:"&#x3C;Forrige",nextText:"Næste&#x3E;",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere",noEventsMessage:"Ingen arrangementer at vise"})}(),function(){!function(){function e(e,a,t,n){var r={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?r[t][0]:r[t][1]}var t=a.defineLocale("de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:e,mm:"%d Minuten",h:e,hh:"%d Stunden",d:e,dd:e,M:e,MM:e,y:e,yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return t}(),e.fullCalendar.datepickerLocale("de","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(e){return"+ weitere "+e},noEventsMessage:"Keine Ereignisse anzuzeigen"})}(),function(){!function(){function e(e,a,t,n){var r={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?r[t][0]:r[t][1]}var t=a.defineLocale("de-at",{months:"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:e,mm:"%d Minuten",h:e,hh:"%d Stunden",d:e,dd:e,M:e,MM:e,y:e,yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return t}(),e.fullCalendar.datepickerLocale("de-at","de",{closeText:"Schließen",prevText:"&#x3C;Zurück",nextText:"Vor&#x3E;",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("de-at",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(e){return"+ weitere "+e},noEventsMessage:"Keine Ereignisse anzuzeigen"})}(),function(){!function(){function e(e){return e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}var t=a.defineLocale("el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(e,a){return/D/.test(a.substring(0,a.indexOf("MMMM")))?this._monthsGenitiveEl[e.month()]:this._monthsNominativeEl[e.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(e,a,t){return e>11?t?"μμ":"ΜΜ":t?"πμ":"ΠΜ"},isPM:function(e){return"μ"===(e+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(a,t){var n=this._calendarEl[a],r=t&&t.hours();return e(n)&&(n=n.apply(t)),n.replace("{}",r%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}});return t}(),e.fullCalendar.datepickerLocale("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα",noEventsMessage:"Δεν υπάρχουν γεγονότα για να εμφανιστεί"})}(),function(){!function(){var e=a.defineLocale("en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th";return e+t},week:{
+dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("en-au","en-AU",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("en-au")}(),function(){!function(){var e=a.defineLocale("en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th";return e+t}});return e}(),e.fullCalendar.locale("en-ca")}(),function(){!function(){var e=a.defineLocale("en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th";return e+t},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("en-gb","en-GB",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("en-gb")}(),function(){!function(){var e=a.defineLocale("en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th";return e+t},week:{dow:1,doy:4}});return e}(),e.fullCalendar.locale("en-ie")}(),function(){!function(){var e=a.defineLocale("en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th";return e+t},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("en-nz","en-NZ",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("en-nz")}(),function(){!function(){var e="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),t="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),n=a.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,n){return/-MMM-/.test(n)?t[a.month()]:e[a.month()]},monthsParseExact:!0,weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return n}(),e.fullCalendar.datepickerLocale("es","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más",noEventsMessage:"No hay eventos para mostrar"})}(),function(){!function(){var e="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),t="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),n=a.defineLocale("es-do",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(a,n){return/-MMM-/.test(n)?t[a.month()]:e[a.month()]},monthsParseExact:!0,weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY h:mm A",LLLL:"dddd, D [de] MMMM [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return n}(),e.fullCalendar.datepickerLocale("es-do","es",{closeText:"Cerrar",prevText:"&#x3C;Ant",nextText:"Sig&#x3E;",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("es-do",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo<br/>el día",eventLimitText:"más",noEventsMessage:"No hay eventos para mostrar"})}(),function(){!function(){var e=a.defineLocale("eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("eu","eu",{closeText:"Egina",prevText:"&#x3C;Aur",nextText:"Hur&#x3E;",currentText:"Gaur",monthNames:["urtarrila","otsaila","martxoa","apirila","maiatza","ekaina","uztaila","abuztua","iraila","urria","azaroa","abendua"],monthNamesShort:["urt.","ots.","mar.","api.","mai.","eka.","uzt.","abu.","ira.","urr.","aza.","abe."],dayNames:["igandea","astelehena","asteartea","asteazkena","osteguna","ostirala","larunbata"],dayNamesShort:["ig.","al.","ar.","az.","og.","ol.","lr."],dayNamesMin:["ig","al","ar","az","og","ol","lr"],weekHeader:"As",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("eu",{buttonText:{month:"Hilabetea",week:"Astea",day:"Eguna",list:"Agenda"},allDayHtml:"Egun<br/>osoa",eventLimitText:"gehiago",noEventsMessage:"Ez dago ekitaldirik erakusteko"})}(),function(){!function(){var e={1:"۱",2:"۲",3:"۳",4:"۴",5:"۵",6:"۶",7:"۷",8:"۸",9:"۹",0:"۰"},t={"۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","۰":"0"},n=a.defineLocale("fa",{months:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),monthsShort:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),weekdays:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysShort:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysMin:"ی_د_س_چ_پ_ج_ش".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/قبل از ظهر|بعد از ظهر/,isPM:function(e){return/بعد از ظهر/.test(e)},meridiem:function(e,a,t){return e<12?"قبل از ظهر":"بعد از ظهر"},calendar:{sameDay:"[امروز ساعت] LT",nextDay:"[فردا ساعت] LT",nextWeek:"dddd [ساعت] LT",lastDay:"[دیروز ساعت] LT",lastWeek:"dddd [پیش] [ساعت] LT",sameElse:"L"},relativeTime:{future:"در %s",past:"%s پیش",s:"چندین ثانیه",m:"یک دقیقه",mm:"%d دقیقه",h:"یک ساعت",hh:"%d ساعت",d:"یک روز",dd:"%d روز",M:"یک ماه",MM:"%d ماه",y:"یک سال",yy:"%d سال"},preparse:function(e){return e.replace(/[۰-۹]/g,function(e){return t[e]}).replace(/،/g,",")},postformat:function(a){return a.replace(/\d/g,function(a){return e[a]}).replace(/,/g,"،")},ordinalParse:/\d{1,2}م/,ordinal:"%dم",week:{dow:6,doy:12}});return n}(),e.fullCalendar.datepickerLocale("fa","fa",{closeText:"بستن",prevText:"&#x3C;قبلی",nextText:"بعدی&#x3E;",currentText:"امروز",monthNames:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["يکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],dayNamesShort:["ی","د","س","چ","پ","ج","ش"],dayNamesMin:["ی","د","س","چ","پ","ج","ش"],weekHeader:"هف",dateFormat:"yy/mm/dd",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("fa",{buttonText:{month:"ماه",week:"هفته",day:"روز",list:"برنامه"},allDayText:"تمام روز",eventLimitText:function(e){return"بیش از "+e},noEventsMessage:"هیچ رویدادی به نمایش"})}(),function(){!function(){function e(e,a,n,r){var s="";switch(n){case"s":return r?"muutaman sekunnin":"muutama sekunti";case"m":return r?"minuutin":"minuutti";case"mm":s=r?"minuutin":"minuuttia";break;case"h":return r?"tunnin":"tunti";case"hh":s=r?"tunnin":"tuntia";break;case"d":return r?"päivän":"päivä";case"dd":s=r?"päivän":"päivää";break;case"M":return r?"kuukauden":"kuukausi";case"MM":s=r?"kuukauden":"kuukautta";break;case"y":return r?"vuoden":"vuosi";case"yy":s=r?"vuoden":"vuotta"}return s=t(e,r)+" "+s}function t(e,a){return e<10?a?r[e]:n[e]:e}var n="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),r=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",n[7],n[8],n[9]],s=a.defineLocale("fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:e,m:e,mm:e,h:e,hh:e,d:e,dd:e,M:e,MM:e,y:e,yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("fi","fi",{closeText:"Sulje",prevText:"&#xAB;Edellinen",nextText:"Seuraava&#xBB;",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää",noEventsMessage:"Ei näytettäviä tapahtumia"})}(),function(){!function(){var e=a.defineLocale("fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(e){return e+(1===e?"er":"")},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("fr",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"})}(),function(){!function(){var e=a.defineLocale("fr-ca",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(e){return e+(1===e?"er":"e")}});return e}(),e.fullCalendar.datepickerLocale("fr-ca","fr-CA",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("fr-ca",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"})}(),function(){!function(){var e=a.defineLocale("fr-ch",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(e){return e+(1===e?"er":"e")},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("fr-ch","fr-CH",{closeText:"Fermer",prevText:"&#x3C;Préc",nextText:"Suiv&#x3E;",currentText:"Courant",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sm",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("fr-ch",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la<br/>journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"})}(),function(){!function(){var e=a.defineLocale("gl",{months:"xaneiro_febreiro_marzo_abril_maio_xuño_xullo_agosto_setembro_outubro_novembro_decembro".split("_"),monthsShort:"xan._feb._mar._abr._mai._xuñ._xul._ago._set._out._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"domingo_luns_martes_mércores_xoves_venres_sábado".split("_"),weekdaysShort:"dom._lun._mar._mér._xov._ven._sáb.".split("_"),weekdaysMin:"do_lu_ma_mé_xo_ve_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"ás":"á")+"] LT"},nextDay:function(){return"[mañá "+(1!==this.hours()?"ás":"á")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"ás":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"á":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"ás":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(e){return 0===e.indexOf("un")?"n"+e:"en "+e},past:"hai %s",s:"uns segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("gl","gl",{closeText:"Pechar",prevText:"&#x3C;Ant",nextText:"Seg&#x3E;",currentText:"Hoxe",monthNames:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthNamesShort:["Xan","Feb","Mar","Abr","Mai","Xuñ","Xul","Ago","Set","Out","Nov","Dec"],dayNames:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],dayNamesShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],dayNamesMin:["Do","Lu","Ma","Mé","Xo","Ve","Sá"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("gl",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Axenda"},allDayHtml:"Todo<br/>o día",eventLimitText:"máis",noEventsMessage:"Non hai eventos para amosar"})}(),function(){!function(){var e=a.defineLocale("he",{months:"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"),monthsShort:"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"),weekdays:"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"),weekdaysShort:"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"),weekdaysMin:"א_ב_ג_ד_ה_ו_ש".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [ב]MMMM YYYY",LLL:"D [ב]MMMM YYYY HH:mm",LLLL:"dddd, D [ב]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[היום ב־]LT",nextDay:"[מחר ב־]LT",nextWeek:"dddd [בשעה] LT",lastDay:"[אתמול ב־]LT",lastWeek:"[ביום] dddd [האחרון בשעה] LT",sameElse:"L"},relativeTime:{future:"בעוד %s",past:"לפני %s",s:"מספר שניות",m:"דקה",mm:"%d דקות",h:"שעה",hh:function(e){return 2===e?"שעתיים":e+" שעות"},d:"יום",dd:function(e){return 2===e?"יומיים":e+" ימים"},M:"חודש",MM:function(e){return 2===e?"חודשיים":e+" חודשים"},y:"שנה",yy:function(e){return 2===e?"שנתיים":e%10===0&&10!==e?e+" שנה":e+" שנים"}},meridiemParse:/אחה"צ|לפנה"צ|אחרי הצהריים|לפני הצהריים|לפנות בוקר|בבוקר|בערב/i,isPM:function(e){return/^(אחה"צ|אחרי הצהריים|בערב)$/.test(e)},meridiem:function(e,a,t){return e<5?"לפנות בוקר":e<10?"בבוקר":e<12?t?'לפנה"צ':"לפני הצהריים":e<18?t?'אחה"צ':"אחרי הצהריים":"בערב"}});return e}(),e.fullCalendar.datepickerLocale("he","he",{closeText:"סגור",prevText:"&#x3C;הקודם",nextText:"הבא&#x3E;",currentText:"היום",monthNames:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthNamesShort:["ינו","פבר","מרץ","אפר","מאי","יוני","יולי","אוג","ספט","אוק","נוב","דצמ"],dayNames:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת"],dayNamesShort:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],dayNamesMin:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("he",{buttonText:{month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},allDayText:"כל היום",eventLimitText:"אחר",noEventsMessage:"אין אירועים להצגה",weekNumberTitle:"שבוע"})}(),function(){!function(){var e={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},t={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"},n=a.defineLocale("hi",{months:"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"),monthsShort:"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"),monthsParseExact:!0,weekdays:"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm बजे",LTS:"A h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm बजे",LLLL:"dddd, D MMMM YYYY, A h:mm बजे"},calendar:{sameDay:"[आज] LT",nextDay:"[कल] LT",nextWeek:"dddd, LT",lastDay:"[कल] LT",lastWeek:"[पिछले] dddd, LT",sameElse:"L"},relativeTime:{future:"%s में",past:"%s पहले",s:"कुछ ही क्षण",m:"एक मिनट",mm:"%d मिनट",h:"एक घंटा",hh:"%d घंटे",d:"एक दिन",dd:"%d दिन",M:"एक महीने",MM:"%d महीने",y:"एक वर्ष",yy:"%d वर्ष"},preparse:function(e){return e.replace(/[१२३४५६७८९०]/g,function(e){return t[e]})},postformat:function(a){return a.replace(/\d/g,function(a){return e[a]})},meridiemParse:/रात|सुबह|दोपहर|शाम/,meridiemHour:function(e,a){return 12===e&&(e=0),"रात"===a?e<4?e:e+12:"सुबह"===a?e:"दोपहर"===a?e>=10?e:e+12:"शाम"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"रात":e<10?"सुबह":e<17?"दोपहर":e<20?"शाम":"रात"},week:{dow:0,doy:6}});return n}(),e.fullCalendar.datepickerLocale("hi","hi",{closeText:"बंद",prevText:"पिछला",nextText:"अगला",currentText:"आज",monthNames:["जनवरी ","फरवरी","मार्च","अप्रेल","मई","जून","जूलाई","अगस्त ","सितम्बर","अक्टूबर","नवम्बर","दिसम्बर"],monthNamesShort:["जन","फर","मार्च","अप्रेल","मई","जून","जूलाई","अग","सित","अक्ट","नव","दि"],dayNames:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],dayNamesShort:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],dayNamesMin:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],weekHeader:"हफ्ता",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("hi",{buttonText:{month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},allDayText:"सभी दिन",eventLimitText:function(e){return"+अधिक "+e},noEventsMessage:"कोई घटनाओं को प्रदर्शित करने के लिए"})}(),function(){!function(){function e(e,a,t){var n=e+" ";switch(t){case"m":return a?"jedna minuta":"jedne minute";case"mm":return n+=1===e?"minuta":2===e||3===e||4===e?"minute":"minuta";case"h":return a?"jedan sat":"jednog sata";case"hh":return n+=1===e?"sat":2===e||3===e||4===e?"sata":"sati";case"dd":return n+=1===e?"dan":"dana";case"MM":return n+=1===e?"mjesec":2===e||3===e||4===e?"mjeseca":"mjeseci";case"yy":return n+=1===e?"godina":2===e||3===e||4===e?"godine":"godina"}}var t=a.defineLocale("hr",{months:{format:"siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:e,mm:e,h:e,hh:e,d:"dan",dd:e,M:"mjesec",MM:e,y:"godinu",yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("hr","hr",{closeText:"Zatvori",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Danas",monthNames:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],
+monthNamesShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],dayNames:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],dayNamesShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayNamesMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],weekHeader:"Tje",dateFormat:"dd.mm.yy.",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("hr",{buttonText:{prev:"Prijašnji",next:"Sljedeći",month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},allDayText:"Cijeli dan",eventLimitText:function(e){return"+ još "+e},noEventsMessage:"Nema događaja za prikaz"})}(),function(){!function(){function e(e,a,t,n){var r=e;switch(t){case"s":return n||a?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(n||a?" perc":" perce");case"mm":return r+(n||a?" perc":" perce");case"h":return"egy"+(n||a?" óra":" órája");case"hh":return r+(n||a?" óra":" órája");case"d":return"egy"+(n||a?" nap":" napja");case"dd":return r+(n||a?" nap":" napja");case"M":return"egy"+(n||a?" hónap":" hónapja");case"MM":return r+(n||a?" hónap":" hónapja");case"y":return"egy"+(n||a?" év":" éve");case"yy":return r+(n||a?" év":" éve")}return""}function t(e){return(e?"":"[múlt] ")+"["+n[this.day()]+"] LT[-kor]"}var n="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" "),r=a.defineLocale("hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(e){return"u"===e.charAt(1).toLowerCase()},meridiem:function(e,a,t){return e<12?t===!0?"de":"DE":t===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return t.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return t.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:e,m:e,mm:e,h:e,hh:e,d:e,dd:e,M:e,MM:e,y:e,yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return r}(),e.fullCalendar.datepickerLocale("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),e.fullCalendar.locale("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további",noEventsMessage:"Nincs megjeleníthető események"})}(),function(){!function(){var e=a.defineLocale("id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"siang"===a?e>=11?e:e+12:"sore"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"siang":e<19?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("id","id",{closeText:"Tutup",prevText:"&#x3C;mundur",nextText:"maju&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari<br/>penuh",eventLimitText:"lebih",noEventsMessage:"Tidak ada acara untuk ditampilkan"})}(),function(){!function(){function e(e){return e%100===11||e%10!==1}function t(a,t,n,r){var s=a+" ";switch(n){case"s":return t||r?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return t?"mínúta":"mínútu";case"mm":return e(a)?s+(t||r?"mínútur":"mínútum"):t?s+"mínúta":s+"mínútu";case"hh":return e(a)?s+(t||r?"klukkustundir":"klukkustundum"):s+"klukkustund";case"d":return t?"dagur":r?"dag":"degi";case"dd":return e(a)?t?s+"dagar":s+(r?"daga":"dögum"):t?s+"dagur":s+(r?"dag":"degi");case"M":return t?"mánuður":r?"mánuð":"mánuði";case"MM":return e(a)?t?s+"mánuðir":s+(r?"mánuði":"mánuðum"):t?s+"mánuður":s+(r?"mánuð":"mánuði");case"y":return t||r?"ár":"ári";case"yy":return e(a)?s+(t||r?"ár":"árum"):s+(t||r?"ár":"ári")}}var n=a.defineLocale("is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:t,m:t,mm:t,h:"klukkustund",hh:t,d:t,dd:t,M:t,MM:t,y:t,yy:t},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return n}(),e.fullCalendar.datepickerLocale("is","is",{closeText:"Loka",prevText:"&#x3C; Fyrri",nextText:"Næsti &#x3E;",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("is",{buttonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan<br/>daginn",eventLimitText:"meira",noEventsMessage:"Engir viðburðir til að sýna"})}(),function(){!function(){var e=a.defineLocale("it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"Do_Lu_Ma_Me_Gi_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(e){return(/^[0-9].+$/.test(e)?"tra":"in")+" "+e},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("it","it",{closeText:"Chiudi",prevText:"&#x3C;Prec",nextText:"Succ&#x3E;",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"],monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il<br/>giorno",eventLimitText:function(e){return"+altri "+e},noEventsMessage:"Non ci sono eventi da visualizzare"})}(),function(){!function(){var e=a.defineLocale("ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"Ah時m分s秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah時m分",LLLL:"YYYY年M月D日Ah時m分 dddd"},meridiemParse:/午前|午後/i,isPM:function(e){return"午後"===e},meridiem:function(e,a,t){return e<12?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},ordinalParse:/\d{1,2}日/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"日";default:return e}},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}});return e}(),e.fullCalendar.datepickerLocale("ja","ja",{closeText:"閉じる",prevText:"&#x3C;前",nextText:"次&#x3E;",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),e.fullCalendar.locale("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(e){return"他 "+e+" 件"},noEventsMessage:"イベントが表示されないように"})}(),function(){!function(){var e={0:"-ші",1:"-ші",2:"-ші",3:"-ші",4:"-ші",5:"-ші",6:"-шы",7:"-ші",8:"-ші",9:"-шы",10:"-шы",20:"-шы",30:"-шы",40:"-шы",50:"-ші",60:"-шы",70:"-ші",80:"-ші",90:"-шы",100:"-ші"},t=a.defineLocale("kk",{months:"қаңтар_ақпан_наурыз_сәуір_мамыр_маусым_шілде_тамыз_қыркүйек_қазан_қараша_желтоқсан".split("_"),monthsShort:"қаң_ақп_нау_сәу_мам_мау_шіл_там_қыр_қаз_қар_жел".split("_"),weekdays:"жексенбі_дүйсенбі_сейсенбі_сәрсенбі_бейсенбі_жұма_сенбі".split("_"),weekdaysShort:"жек_дүй_сей_сәр_бей_жұм_сен".split("_"),weekdaysMin:"жк_дй_сй_ср_бй_жм_сн".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Бүгін сағат] LT",nextDay:"[Ертең сағат] LT",nextWeek:"dddd [сағат] LT",lastDay:"[Кеше сағат] LT",lastWeek:"[Өткен аптаның] dddd [сағат] LT",sameElse:"L"},relativeTime:{future:"%s ішінде",past:"%s бұрын",s:"бірнеше секунд",m:"бір минут",mm:"%d минут",h:"бір сағат",hh:"%d сағат",d:"бір күн",dd:"%d күн",M:"бір ай",MM:"%d ай",y:"бір жыл",yy:"%d жыл"},ordinalParse:/\d{1,2}-(ші|шы)/,ordinal:function(a){var t=a%10,n=a>=100?100:null;return a+(e[a]||e[t]||e[n])},week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("kk","kk",{closeText:"Жабу",prevText:"&#x3C;Алдыңғы",nextText:"Келесі&#x3E;",currentText:"Бүгін",monthNames:["Қаңтар","Ақпан","Наурыз","Сәуір","Мамыр","Маусым","Шілде","Тамыз","Қыркүйек","Қазан","Қараша","Желтоқсан"],monthNamesShort:["Қаң","Ақп","Нау","Сәу","Мам","Мау","Шіл","Там","Қыр","Қаз","Қар","Жел"],dayNames:["Жексенбі","Дүйсенбі","Сейсенбі","Сәрсенбі","Бейсенбі","Жұма","Сенбі"],dayNamesShort:["жкс","дсн","ссн","срс","бсн","жма","снб"],dayNamesMin:["Жк","Дс","Сс","Ср","Бс","Жм","Сн"],weekHeader:"Не",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("kk",{buttonText:{month:"Ай",week:"Апта",day:"Күн",list:"Күн тәртібі"},allDayText:"Күні бойы",eventLimitText:function(e){return"+ тағы "+e},noEventsMessage:"Көрсету үшін оқиғалар жоқ"})}(),function(){!function(){var e=a.defineLocale("ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 A h시 m분",LLLL:"YYYY년 MMMM D일 dddd A h시 m분"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",ss:"%d초",m:"일분",mm:"%d분",h:"한 시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한 달",MM:"%d달",y:"일 년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(e){return"오후"===e},meridiem:function(e,a,t){return e<12?"오전":"오후"}});return e}(),e.fullCalendar.datepickerLocale("ko","ko",{closeText:"닫기",prevText:"이전달",nextText:"다음달",currentText:"오늘",monthNames:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthNamesShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayNames:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],dayNamesShort:["일","월","화","수","목","금","토"],dayNamesMin:["일","월","화","수","목","금","토"],weekHeader:"주",dateFormat:"yy. m. d.",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"년"}),e.fullCalendar.locale("ko",{buttonText:{month:"월",week:"주",day:"일",list:"일정목록"},allDayText:"종일",eventLimitText:"개",noEventsMessage:"일정이 표시 없습니다"})}(),function(){!function(){function e(e,a,t,n){var r={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return a?r[t][0]:r[t][1]}function t(e){var a=e.substr(0,e.indexOf(" "));return r(a)?"a "+e:"an "+e}function n(e){var a=e.substr(0,e.indexOf(" "));return r(a)?"viru "+e:"virun "+e}function r(e){if(e=parseInt(e,10),isNaN(e))return!1;if(e<0)return!0;if(e<10)return 4<=e&&e<=7;if(e<100){var a=e%10,t=e/10;return r(0===a?t:a)}if(e<1e4){for(;e>=10;)e/=10;return r(e)}return e/=1e3,r(e)}var s=a.defineLocale("lb",{months:"Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._Mé._Dë._Më._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mé_Dë_Më_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[Gëschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:t,past:n,s:"e puer Sekonnen",m:e,mm:"%d Minutten",h:e,hh:"%d Stonnen",d:e,dd:"%d Deeg",M:e,MM:"%d Méint",y:e,yy:"%d Joer"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("lb","lb",{closeText:"Fäerdeg",prevText:"Zréck",nextText:"Weider",currentText:"Haut",monthNames:["Januar","Februar","Mäerz","Abrëll","Mee","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mäe","Abr","Mee","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonndeg","Méindeg","Dënschdeg","Mëttwoch","Donneschdeg","Freideg","Samschdeg"],dayNamesShort:["Son","Méi","Dën","Mët","Don","Fre","Sam"],dayNamesMin:["So","Mé","Dë","Më","Do","Fr","Sa"],weekHeader:"W",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("lb",{buttonText:{month:"Mount",week:"Woch",day:"Dag",list:"Terminiwwersiicht"},allDayText:"Ganzen Dag",eventLimitText:"méi",noEventsMessage:"Nee Evenementer ze affichéieren"})}(),function(){!function(){function e(e,a,t,n){return a?"kelios sekundės":n?"kelių sekundžių":"kelias sekundes"}function t(e,a,t,n){return a?r(t)[0]:n?r(t)[1]:r(t)[2]}function n(e){return e%10===0||e>10&&e<20}function r(e){return d[e].split("_")}function s(e,a,s,d){var i=e+" ";return 1===e?i+t(e,a,s[0],d):a?i+(n(e)?r(s)[1]:r(s)[0]):d?i+r(s)[1]:i+(n(e)?r(s)[1]:r(s)[2])}var d={m:"minutė_minutės_minutę",mm:"minutės_minučių_minutes",h:"valanda_valandos_valandą",hh:"valandos_valandų_valandas",d:"diena_dienos_dieną",dd:"dienos_dienų_dienas",M:"mėnuo_mėnesio_mėnesį",MM:"mėnesiai_mėnesių_mėnesius",y:"metai_metų_metus",yy:"metai_metų_metus"},i=a.defineLocale("lt",{months:{format:"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis".split("_"),isFormat:/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?|MMMM?(\[[^\[\]]*\]|\s)+D[oD]?/},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"),weekdaysMin:"S_P_A_T_K_Pn_Š".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[Šiandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Praėjusį] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prieš %s",s:e,m:t,mm:s,h:t,hh:s,d:t,dd:s,M:t,MM:s,y:t,yy:s},ordinalParse:/\d{1,2}-oji/,ordinal:function(e){return e+"-oji"},week:{dow:1,doy:4}});return i}(),e.fullCalendar.datepickerLocale("lt","lt",{closeText:"Uždaryti",prevText:"&#x3C;Atgal",nextText:"Pirmyn&#x3E;",currentText:"Šiandien",monthNames:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthNamesShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],dayNames:["sekmadienis","pirmadienis","antradienis","trečiadienis","ketvirtadienis","penktadienis","šeštadienis"],dayNamesShort:["sek","pir","ant","tre","ket","pen","šeš"],dayNamesMin:["Se","Pr","An","Tr","Ke","Pe","Še"],weekHeader:"SAV",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),e.fullCalendar.locale("lt",{buttonText:{month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},allDayText:"Visą dieną",eventLimitText:"daugiau",noEventsMessage:"Nėra įvykių rodyti"})}(),function(){!function(){function e(e,a,t){return t?a%10===1&&a%100!==11?e[2]:e[3]:a%10===1&&a%100!==11?e[0]:e[1]}function t(a,t,n){return a+" "+e(s[n],a,t)}function n(a,t,n){return e(s[n],a,t)}function r(e,a){return a?"dažas sekundes":"dažām sekundēm"}var s={m:"minūtes_minūtēm_minūte_minūtes".split("_"),mm:"minūtes_minūtēm_minūte_minūtes".split("_"),h:"stundas_stundām_stunda_stundas".split("_"),hh:"stundas_stundām_stunda_stundas".split("_"),d:"dienas_dienām_diena_dienas".split("_"),dd:"dienas_dienām_diena_dienas".split("_"),M:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),MM:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")},d=a.defineLocale("lv",{months:"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"),weekdays:"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[Šodien pulksten] LT",nextDay:"[Rīt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pagājušā] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"pēc %s",past:"pirms %s",s:r,m:n,mm:t,h:n,hh:t,d:n,dd:t,M:n,MM:t,y:n,yy:t},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return d}(),e.fullCalendar.datepickerLocale("lv","lv",{closeText:"Aizvērt",prevText:"Iepr.",nextText:"Nāk.",currentText:"Šodien",monthNames:["Janvāris","Februāris","Marts","Aprīlis","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],monthNamesShort:["Jan","Feb","Mar","Apr","Mai","Jūn","Jūl","Aug","Sep","Okt","Nov","Dec"],dayNames:["svētdiena","pirmdiena","otrdiena","trešdiena","ceturtdiena","piektdiena","sestdiena"],dayNamesShort:["svt","prm","otr","tre","ctr","pkt","sst"],dayNamesMin:["Sv","Pr","Ot","Tr","Ct","Pk","Ss"],weekHeader:"Ned.",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("lv",{buttonText:{month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},allDayText:"Visu dienu",eventLimitText:function(e){return"+vēl "+e},noEventsMessage:"Nav notikumu, lai parādītu"})}(),function(){!function(){var e=a.defineLocale("mk",{months:"јануари_февруари_март_април_мај_јуни_јули_август_септември_октомври_ноември_декември".split("_"),monthsShort:"јан_фев_мар_апр_мај_јун_јул_авг_сеп_окт_ное_дек".split("_"),weekdays:"недела_понеделник_вторник_среда_четврток_петок_сабота".split("_"),weekdaysShort:"нед_пон_вто_сре_чет_пет_саб".split("_"),weekdaysMin:"нe_пo_вт_ср_че_пе_сa".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[Денес во] LT",nextDay:"[Утре во] LT",nextWeek:"[Во] dddd [во] LT",lastDay:"[Вчера во] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[Изминатата] dddd [во] LT";case 1:case 2:case 4:case 5:return"[Изминатиот] dddd [во] LT"}},sameElse:"L"},relativeTime:{future:"после %s",past:"пред %s",s:"неколку секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дена",M:"месец",MM:"%d месеци",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-ев":0===t?e+"-ен":t>10&&t<20?e+"-ти":1===a?e+"-ви":2===a?e+"-ри":7===a||8===a?e+"-ми":e+"-ти"},week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("mk","mk",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Денес",monthNames:["Јануари","Февруари","Март","Април","Мај","Јуни","Јули","Август","Септември","Октомври","Ноември","Декември"],monthNamesShort:["Јан","Фев","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Ное","Дек"],dayNames:["Недела","Понеделник","Вторник","Среда","Четврток","Петок","Сабота"],dayNamesShort:["Нед","Пон","Вто","Сре","Чет","Пет","Саб"],dayNamesMin:["Не","По","Вт","Ср","Че","Пе","Са"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("mk",{buttonText:{month:"Месец",week:"Недела",day:"Ден",list:"График"},allDayText:"Цел ден",eventLimitText:function(e){return"+повеќе "+e},noEventsMessage:"Нема настани за прикажување"})}(),function(){!function(){var e=a.defineLocale("ms",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("ms","ms",{closeText:"Tutup",prevText:"&#x3C;Sebelum",nextText:"Selepas&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"],monthNamesShort:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ogo","Sep","Okt","Nov","Dis"],dayNames:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"],dayNamesShort:["Aha","Isn","Sel","Rab","kha","Jum","Sab"],dayNamesMin:["Ah","Is","Se","Ra","Kh","Ju","Sa"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ms",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayText:"Sepanjang hari",eventLimitText:function(e){return"masih ada "+e+" acara"},noEventsMessage:"Tiada peristiwa untuk dipaparkan"})}(),function(){!function(){var e=a.defineLocale("ms-my",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}});return e}(),e.fullCalendar.datepickerLocale("ms-my","ms",{closeText:"Tutup",prevText:"&#x3C;Sebelum",nextText:"Selepas&#x3E;",currentText:"hari ini",monthNames:["Januari","Februari","Mac","April","Mei","Jun","Julai","Ogos","September","Oktober","November","Disember"],monthNamesShort:["Jan","Feb","Mac","Apr","Mei","Jun","Jul","Ogo","Sep","Okt","Nov","Dis"],dayNames:["Ahad","Isnin","Selasa","Rabu","Khamis","Jumaat","Sabtu"],dayNamesShort:["Aha","Isn","Sel","Rab","kha","Jum","Sab"],dayNamesMin:["Ah","Is","Se","Ra","Kh","Ju","Sa"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ms-my",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayText:"Sepanjang hari",eventLimitText:function(e){return"masih ada "+e+" acara"},noEventsMessage:"Tiada peristiwa untuk dipaparkan"})}(),function(){!function(){var e=a.defineLocale("nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"sø._ma._ti._on._to._fr._lø.".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("nb","nb",{closeText:"Lukk",prevText:"&#xAB;Forrige",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til",noEventsMessage:"Ingen hendelser å vise"})}(),function(){!function(){var e="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),t="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),n=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],r=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,s=a.defineLocale("nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,n){return/-MMM-/.test(n)?t[a.month()]:e[a.month()]},monthsRegex:r,monthsShortRegex:r,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:n,longMonthsParse:n,shortMonthsParse:n,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",
+lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra",noEventsMessage:"Geen evenementen om te laten zien"})}(),function(){!function(){var e="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),t="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),n=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],r=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,s=a.defineLocale("nl-be",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(a,n){return/-MMM-/.test(n)?t[a.month()]:e[a.month()]},monthsRegex:r,monthsShortRegex:r,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:n,longMonthsParse:n,shortMonthsParse:n,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("nl-be","nl-BE",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("nl-be",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra",noEventsMessage:"Geen evenementen om te laten zien"})}(),function(){!function(){var e=a.defineLocale("nn",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sundag_måndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"),weekdaysShort:"sun_mån_tys_ons_tor_fre_lau".split("_"),weekdaysMin:"su_må_ty_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[I dag klokka] LT",nextDay:"[I morgon klokka] LT",nextWeek:"dddd [klokka] LT",lastDay:"[I går klokka] LT",lastWeek:"[Føregåande] dddd [klokka] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s sidan",s:"nokre sekund",m:"eit minutt",mm:"%d minutt",h:"ein time",hh:"%d timar",d:"ein dag",dd:"%d dagar",M:"ein månad",MM:"%d månader",y:"eit år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("nn","nn",{closeText:"Lukk",prevText:"&#xAB;Førre",nextText:"Neste&#xBB;",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["sun","mån","tys","ons","tor","fre","lau"],dayNames:["sundag","måndag","tysdag","onsdag","torsdag","fredag","laurdag"],dayNamesMin:["su","må","ty","on","to","fr","la"],weekHeader:"Veke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("nn",{buttonText:{month:"Månad",week:"Veke",day:"Dag",list:"Agenda"},allDayText:"Heile dagen",eventLimitText:"til",noEventsMessage:"Ingen hendelser å vise"})}(),function(){!function(){function e(e){return e%10<5&&e%10>1&&~~(e/10)%10!==1}function t(a,t,n){var r=a+" ";switch(n){case"m":return t?"minuta":"minutę";case"mm":return r+(e(a)?"minuty":"minut");case"h":return t?"godzina":"godzinę";case"hh":return r+(e(a)?"godziny":"godzin");case"MM":return r+(e(a)?"miesiące":"miesięcy");case"yy":return r+(e(a)?"lata":"lat")}}var n="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),r="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"),s=a.defineLocale("pl",{months:function(e,a){return""===a?"("+r[e.month()]+"|"+n[e.month()]+")":/D MMMM/.test(a)?r[e.month()]:n[e.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"ndz_pon_wt_śr_czw_pt_sob".split("_"),weekdaysMin:"Nd_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:t,mm:t,h:t,hh:t,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:t,y:"rok",yy:t},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("pl","pl",{closeText:"Zamknij",prevText:"&#x3C;Poprzedni",nextText:"Następny&#x3E;",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej",noEventsMessage:"Brak wydarzeń do wyświetlenia"})}(),function(){!function(){var e=a.defineLocale("pt",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais",noEventsMessage:"Não há eventos para mostrar"})}(),function(){!function(){var e=a.defineLocale("pt-br",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [às] HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"poucos segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"});return e}(),e.fullCalendar.datepickerLocale("pt-br","pt-BR",{closeText:"Fechar",prevText:"&#x3C;Anterior",nextText:"Próximo&#x3E;",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(e){return"mais +"+e},noEventsMessage:"Não há eventos para mostrar"})}(),function(){!function(){function e(e,a,t){var n={mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"},r=" ";return(e%100>=20||e>=100&&e%100===0)&&(r=" de "),e+r+n[t]}var t=a.defineLocale("ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[mâine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s în urmă",s:"câteva secunde",m:"un minut",mm:e,h:"o oră",hh:e,d:"o zi",dd:e,M:"o lună",MM:e,y:"un an",yy:e},week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("ro","ro",{closeText:"Închide",prevText:"&#xAB; Luna precedentă",nextText:"Luna următoare &#xBB;",currentText:"Azi",monthNames:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],monthNamesShort:["Ian","Feb","Mar","Apr","Mai","Iun","Iul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"],dayNamesShort:["Dum","Lun","Mar","Mie","Joi","Vin","Sâm"],dayNamesMin:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],weekHeader:"Săpt",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ro",{buttonText:{prev:"precedentă",next:"următoare",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},allDayText:"Toată ziua",eventLimitText:function(e){return"+alte "+e},noEventsMessage:"Nu există evenimente de afișat"})}(),function(){!function(){function e(e,a){var t=e.split("_");return a%10===1&&a%100!==11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}function t(a,t,n){var r={mm:t?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===n?t?"минута":"минуту":a+" "+e(r[n],+a)}var n=[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[йя]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],r=a.defineLocale("ru",{months:{format:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_"),standalone:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_")},monthsShort:{format:"янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.".split("_"),standalone:"янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.".split("_")},weekdays:{standalone:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),format:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_"),isFormat:/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/},weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:n,longMonthsParse:n,shortMonthsParse:n,monthsRegex:/^(январ[ья]|янв\.?|феврал[ья]|февр?\.?|марта?|мар\.?|апрел[ья]|апр\.?|ма[йя]|июн[ья]|июн\.?|июл[ья]|июл\.?|августа?|авг\.?|сентябр[ья]|сент?\.?|октябр[ья]|окт\.?|ноябр[ья]|нояб?\.?|декабр[ья]|дек\.?)/i,monthsShortRegex:/^(январ[ья]|янв\.?|феврал[ья]|февр?\.?|марта?|мар\.?|апрел[ья]|апр\.?|ма[йя]|июн[ья]|июн\.?|июл[ья]|июл\.?|августа?|авг\.?|сентябр[ья]|сент?\.?|октябр[ья]|окт\.?|ноябр[ья]|нояб?\.?|декабр[ья]|дек\.?)/i,monthsStrictRegex:/^(январ[яь]|феврал[яь]|марта?|апрел[яь]|ма[яй]|июн[яь]|июл[яь]|августа?|сентябр[яь]|октябр[яь]|ноябр[яь]|декабр[яь])/i,monthsShortStrictRegex:/^(янв\.|февр?\.|мар[т.]|апр\.|ма[яй]|июн[ья.]|июл[ья.]|авг\.|сент?\.|окт\.|нояб?\.|дек\.)/i,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., HH:mm",LLLL:"dddd, D MMMM YYYY г., HH:mm"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В следующее] dddd [в] LT";case 1:case 2:case 4:return"[В следующий] dddd [в] LT";case 3:case 5:case 6:return"[В следующую] dddd [в] LT"}},lastWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:t,mm:t,h:"час",hh:t,d:"день",dd:t,M:"месяц",MM:t,y:"год",yy:t},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(e){return/^(дня|вечера)$/.test(e)},meridiem:function(e,a,t){return e<4?"ночи":e<12?"утра":e<17?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":return e+"-й";case"D":return e+"-го";case"w":case"W":return e+"-я";default:return e}},week:{dow:1,doy:7}});return r}(),e.fullCalendar.datepickerLocale("ru","ru",{closeText:"Закрыть",prevText:"&#x3C;Пред",nextText:"След&#x3E;",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(e){return"+ ещё "+e},noEventsMessage:"Нет событий для отображения"})}(),function(){!function(){function e(e){return e>1&&e<5}function t(a,t,n,r){var s=a+" ";switch(n){case"s":return t||r?"pár sekúnd":"pár sekundami";case"m":return t?"minúta":r?"minútu":"minútou";case"mm":return t||r?s+(e(a)?"minúty":"minút"):s+"minútami";case"h":return t?"hodina":r?"hodinu":"hodinou";case"hh":return t||r?s+(e(a)?"hodiny":"hodín"):s+"hodinami";case"d":return t||r?"deň":"dňom";case"dd":return t||r?s+(e(a)?"dni":"dní"):s+"dňami";case"M":return t||r?"mesiac":"mesiacom";case"MM":return t||r?s+(e(a)?"mesiace":"mesiacov"):s+"mesiacmi";case"y":return t||r?"rok":"rokom";case"yy":return t||r?s+(e(a)?"roky":"rokov"):s+"rokmi"}}var n="január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"),r="jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"),s=a.defineLocale("sk",{months:n,monthsShort:r,weekdays:"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_št_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_št_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nedeľu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo štvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[včera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulú nedeľu o] LT";case 1:case 2:return"[minulý] dddd [o] LT";case 3:return"[minulú stredu o] LT";case 4:case 5:return"[minulý] dddd [o] LT";case 6:return"[minulú sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:t,m:t,mm:t,h:t,hh:t,d:t,dd:t,M:t,MM:t,y:t,yy:t},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return s}(),e.fullCalendar.datepickerLocale("sk","sk",{closeText:"Zavrieť",prevText:"&#x3C;Predchádzajúci",nextText:"Nasledujúci&#x3E;",currentText:"Dnes",monthNames:["január","február","marec","apríl","máj","jún","júl","august","september","október","november","december"],monthNamesShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],dayNames:["nedeľa","pondelok","utorok","streda","štvrtok","piatok","sobota"],dayNamesShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],dayNamesMin:["Ne","Po","Ut","St","Št","Pia","So"],weekHeader:"Ty",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("sk",{buttonText:{month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},allDayText:"Celý deň",eventLimitText:function(e){return"+ďalšie: "+e},noEventsMessage:"Žiadne akcie na zobrazenie"})}(),function(){!function(){function e(e,a,t,n){var r=e+" ";switch(t){case"s":return a||n?"nekaj sekund":"nekaj sekundami";case"m":return a?"ena minuta":"eno minuto";case"mm":return r+=1===e?a?"minuta":"minuto":2===e?a||n?"minuti":"minutama":e<5?a||n?"minute":"minutami":a||n?"minut":"minutami";case"h":return a?"ena ura":"eno uro";case"hh":return r+=1===e?a?"ura":"uro":2===e?a||n?"uri":"urama":e<5?a||n?"ure":"urami":a||n?"ur":"urami";case"d":return a||n?"en dan":"enim dnem";case"dd":return r+=1===e?a||n?"dan":"dnem":2===e?a||n?"dni":"dnevoma":a||n?"dni":"dnevi";case"M":return a||n?"en mesec":"enim mesecem";case"MM":return r+=1===e?a||n?"mesec":"mesecem":2===e?a||n?"meseca":"mesecema":e<5?a||n?"mesece":"meseci":a||n?"mesecev":"meseci";case"y":return a||n?"eno leto":"enim letom";case"yy":return r+=1===e?a||n?"leto":"letom":2===e?a||n?"leti":"letoma":e<5?a||n?"leta":"leti":a||n?"let":"leti"}}var t=a.defineLocale("sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._čet._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_če_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[včeraj ob] LT",lastWeek:function(){switch(this.day()){case 0:return"[prejšnjo] [nedeljo] [ob] LT";case 3:return"[prejšnjo] [sredo] [ob] LT";case 6:return"[prejšnjo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prejšnji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"čez %s",past:"pred %s",s:e,m:e,mm:e,h:e,hh:e,d:e,dd:e,M:e,MM:e,y:e,yy:e},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("sl","sl",{closeText:"Zapri",prevText:"&#x3C;Prejšnji",nextText:"Naslednji&#x3E;",currentText:"Trenutni",monthNames:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],dayNames:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"],dayNamesShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],dayNamesMin:["Ne","Po","To","Sr","Če","Pe","So"],weekHeader:"Teden",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("sl",{buttonText:{month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},allDayText:"Ves dan",eventLimitText:"več",noEventsMessage:"Ni dogodkov za prikaz"})}(),function(){!function(){var e={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(a,t,n){var r=e.words[n];return 1===n.length?t?r[0]:r[1]:a+" "+e.correctGrammaticalCase(a,r)}},t=a.defineLocale("sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var e=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return e[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:e.translate,mm:e.translate,h:e.translate,hh:e.translate,d:"dan",dd:e.translate,M:"mesec",MM:e.translate,y:"godinu",yy:e.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("sr","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(e){return"+ још "+e},noEventsMessage:"Нема догађаја за приказ"})}(),function(){!function(){var e={words:{m:["један минут","једне минуте"],mm:["минут","минуте","минута"],h:["један сат","једног сата"],hh:["сат","сата","сати"],dd:["дан","дана","дана"],MM:["месец","месеца","месеци"],yy:["година","године","година"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(a,t,n){var r=e.words[n];return 1===n.length?t?r[0]:r[1]:a+" "+e.correctGrammaticalCase(a,r)}},t=a.defineLocale("sr-cyrl",{months:"јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар".split("_"),monthsShort:"јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.".split("_"),monthsParseExact:!0,weekdays:"недеља_понедељак_уторак_среда_четвртак_петак_субота".split("_"),weekdaysShort:"нед._пон._уто._сре._чет._пет._суб.".split("_"),weekdaysMin:"не_по_ут_ср_че_пе_су".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[данас у] LT",nextDay:"[сутра у] LT",nextWeek:function(){switch(this.day()){case 0:return"[у] [недељу] [у] LT";case 3:return"[у] [среду] [у] LT";case 6:return"[у] [суботу] [у] LT";case 1:case 2:case 4:case 5:return"[у] dddd [у] LT"}},lastDay:"[јуче у] LT",lastWeek:function(){var e=["[прошле] [недеље] [у] LT","[прошлог] [понедељка] [у] LT","[прошлог] [уторка] [у] LT","[прошле] [среде] [у] LT","[прошлог] [четвртка] [у] LT","[прошлог] [петка] [у] LT","[прошле] [суботе] [у] LT"];return e[this.day()]},sameElse:"L"},relativeTime:{future:"за %s",past:"пре %s",s:"неколико секунди",m:e.translate,mm:e.translate,h:e.translate,hh:e.translate,d:"дан",dd:e.translate,M:"месец",MM:e.translate,y:"годину",yy:e.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("sr-cyrl","sr",{closeText:"Затвори",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("sr-cyrl",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(e){return"+ још "+e},noEventsMessage:"Нема догађаја за приказ"})}(),function(){!function(){var e=a.defineLocale("sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"[På] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(e){var a=e%10,t=1===~~(e%100/10)?"e":1===a?"a":2===a?"a":"e";return e+t},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("sv","sv",{closeText:"Stäng",prevText:"&#xAB;Förra",nextText:"Nästa&#xBB;",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till",noEventsMessage:"Inga händelser att visa"})}(),function(){!function(){var e=a.defineLocale("th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"ม.ค._ก.พ._มี.ค._เม.ย._พ.ค._มิ.ย._ก.ค._ส.ค._ก.ย._ต.ค._พ.ย._ธ.ค.".split("_"),monthsParseExact:!0,weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา H:mm",LLLL:"วันddddที่ D MMMM YYYY เวลา H:mm"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(e){return"หลังเที่ยง"===e},meridiem:function(e,a,t){return e<12?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",
+y:"1 ปี",yy:"%d ปี"}});return e}(),e.fullCalendar.datepickerLocale("th","th",{closeText:"ปิด",prevText:"&#xAB;&#xA0;ย้อน",nextText:"ถัดไป&#xA0;&#xBB;",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม",noEventsMessage:"ไม่มีกิจกรรมที่จะแสดง"})}(),function(){!function(){var e={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"},t=a.defineLocale("tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(a){if(0===a)return a+"'ıncı";var t=a%10,n=a%100-t,r=a>=100?100:null;return a+(e[t]||e[n]||e[r])},week:{dow:1,doy:7}});return t}(),e.fullCalendar.datepickerLocale("tr","tr",{closeText:"kapat",prevText:"&#x3C;geri",nextText:"ileri&#x3e",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla",noEventsMessage:"Herhangi bir etkinlik görüntülemek için"})}(),function(){!function(){function e(e,a){var t=e.split("_");return a%10===1&&a%100!==11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}function t(a,t,n){var r={mm:t?"хвилина_хвилини_хвилин":"хвилину_хвилини_хвилин",hh:t?"година_години_годин":"годину_години_годин",dd:"день_дні_днів",MM:"місяць_місяці_місяців",yy:"рік_роки_років"};return"m"===n?t?"хвилина":"хвилину":"h"===n?t?"година":"годину":a+" "+e(r[n],+a)}function n(e,a){var t={nominative:"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"),accusative:"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"),genitive:"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_")},n=/(\[[ВвУу]\]) ?dddd/.test(a)?"accusative":/\[?(?:минулої|наступної)? ?\] ?dddd/.test(a)?"genitive":"nominative";return t[n][e.day()]}function r(e){return function(){return e+"о"+(11===this.hours()?"б":"")+"] LT"}}var s=a.defineLocale("uk",{months:{format:"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня".split("_"),standalone:"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень".split("_")},monthsShort:"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"),weekdays:n,weekdaysShort:"нд_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY р.",LLL:"D MMMM YYYY р., HH:mm",LLLL:"dddd, D MMMM YYYY р., HH:mm"},calendar:{sameDay:r("[Сьогодні "),nextDay:r("[Завтра "),lastDay:r("[Вчора "),nextWeek:r("[У] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return r("[Минулої] dddd [").call(this);case 1:case 2:case 4:return r("[Минулого] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"за %s",past:"%s тому",s:"декілька секунд",m:t,mm:t,h:"годину",hh:t,d:"день",dd:t,M:"місяць",MM:t,y:"рік",yy:t},meridiemParse:/ночі|ранку|дня|вечора/,isPM:function(e){return/^(дня|вечора)$/.test(e)},meridiem:function(e,a,t){return e<4?"ночі":e<12?"ранку":e<17?"дня":"вечора"},ordinalParse:/\d{1,2}-(й|го)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":case"w":case"W":return e+"-й";case"D":return e+"-го";default:return e}},week:{dow:1,doy:7}});return s}(),e.fullCalendar.datepickerLocale("uk","uk",{closeText:"Закрити",prevText:"&#x3C;",nextText:"&#x3E;",currentText:"Сьогодні",monthNames:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthNamesShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],dayNames:["неділя","понеділок","вівторок","середа","четвер","п’ятниця","субота"],dayNamesShort:["нед","пнд","вів","срд","чтв","птн","сбт"],dayNamesMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Тиж",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("uk",{buttonText:{month:"Місяць",week:"Тиждень",day:"День",list:"Порядок денний"},allDayText:"Увесь день",eventLimitText:function(e){return"+ще "+e+"..."},noEventsMessage:"Немає подій для відображення"})}(),function(){!function(){var e=a.defineLocale("vi",{months:"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),monthsParseExact:!0,weekdays:"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(e){return/^ch$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"sa":"SA":t?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [năm] YYYY",LLL:"D MMMM [năm] YYYY HH:mm",LLLL:"dddd, D MMMM [năm] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[Hôm nay lúc] LT",nextDay:"[Ngày mai lúc] LT",nextWeek:"dddd [tuần tới lúc] LT",lastDay:"[Hôm qua lúc] LT",lastWeek:"dddd [tuần rồi lúc] LT",sameElse:"L"},relativeTime:{future:"%s tới",past:"%s trước",s:"vài giây",m:"một phút",mm:"%d phút",h:"một giờ",hh:"%d giờ",d:"một ngày",dd:"%d ngày",M:"một tháng",MM:"%d tháng",y:"một năm",yy:"%d năm"},ordinalParse:/\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("vi","vi",{closeText:"Đóng",prevText:"&#x3C;Trước",nextText:"Tiếp&#x3E;",currentText:"Hôm nay",monthNames:["Tháng Một","Tháng Hai","Tháng Ba","Tháng Tư","Tháng Năm","Tháng Sáu","Tháng Bảy","Tháng Tám","Tháng Chín","Tháng Mười","Tháng Mười Một","Tháng Mười Hai"],monthNamesShort:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],dayNames:["Chủ Nhật","Thứ Hai","Thứ Ba","Thứ Tư","Thứ Năm","Thứ Sáu","Thứ Bảy"],dayNamesShort:["CN","T2","T3","T4","T5","T6","T7"],dayNamesMin:["CN","T2","T3","T4","T5","T6","T7"],weekHeader:"Tu",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),e.fullCalendar.locale("vi",{buttonText:{month:"Tháng",week:"Tuần",day:"Ngày",list:"Lịch biểu"},allDayText:"Cả ngày",eventLimitText:function(e){return"+ thêm "+e},noEventsMessage:"Không có sự kiện để hiển thị"})}(),function(){!function(){var e=a.defineLocale("zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm分",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日Ah点mm分",LLLL:"YYYY年MMMD日ddddAh点mm分",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日Ah点mm分",llll:"YYYY年MMMD日ddddAh点mm分"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(e,a){return 12===e&&(e=0),"凌晨"===a||"早上"===a||"上午"===a?e:"下午"===a||"晚上"===a?e+12:e>=11?e:e+12},meridiem:function(e,a,t){var n=100*e+a;return n<600?"凌晨":n<900?"早上":n<1130?"上午":n<1230?"中午":n<1800?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var e,t;return e=a().startOf("week"),t=this.diff(e,"days")>=7?"[下]":"[本]",0===this.minutes()?t+"dddAh点整":t+"dddAh点mm"},lastWeek:function(){var e,t;return e=a().startOf("week"),t=this.unix()<e.unix()?"[上]":"[本]",0===this.minutes()?t+"dddAh点整":t+"dddAh点mm"},sameElse:"LL"},ordinalParse:/\d{1,2}(日|月|周)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"日";case"M":return e+"月";case"w":case"W":return e+"周";default:return e}},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},week:{dow:1,doy:4}});return e}(),e.fullCalendar.datepickerLocale("zh-cn","zh-CN",{closeText:"关闭",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),e.fullCalendar.locale("zh-cn",{buttonText:{month:"月",week:"周",day:"日",list:"日程"},allDayText:"全天",eventLimitText:function(e){return"另外 "+e+" 个"},noEventsMessage:"没有事件显示"})}(),function(){!function(){var e=a.defineLocale("zh-tw",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"週日_週一_週二_週三_週四_週五_週六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah點mm分",LTS:"Ah點m分s秒",L:"YYYY年MMMD日",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日Ah點mm分",LLLL:"YYYY年MMMD日ddddAh點mm分",l:"YYYY年MMMD日",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日Ah點mm分",llll:"YYYY年MMMD日ddddAh點mm分"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(e,a){return 12===e&&(e=0),"凌晨"===a||"早上"===a||"上午"===a?e:"中午"===a?e>=11?e:e+12:"下午"===a||"晚上"===a?e+12:void 0},meridiem:function(e,a,t){var n=100*e+a;return n<600?"凌晨":n<900?"早上":n<1130?"上午":n<1230?"中午":n<1800?"下午":"晚上"},calendar:{sameDay:"[今天]LT",nextDay:"[明天]LT",nextWeek:"[下]ddddLT",lastDay:"[昨天]LT",lastWeek:"[上]ddddLT",sameElse:"L"},ordinalParse:/\d{1,2}(日|月|週)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"日";case"M":return e+"月";case"w":case"W":return e+"週";default:return e}},relativeTime:{future:"%s內",past:"%s前",s:"幾秒",m:"1 分鐘",mm:"%d 分鐘",h:"1 小時",hh:"%d 小時",d:"1 天",dd:"%d 天",M:"1 個月",MM:"%d 個月",y:"1 年",yy:"%d 年"}});return e}(),e.fullCalendar.datepickerLocale("zh-tw","zh-TW",{closeText:"關閉",prevText:"&#x3C;上月",nextText:"下月&#x3E;",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy/mm/dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),e.fullCalendar.locale("zh-tw",{buttonText:{month:"月",week:"週",day:"天",list:"活動列表"},allDayText:"整天",eventLimitText:"顯示更多",noEventsMessage:"没有任何活動"})}(),a.locale("en"),e.fullCalendar.locale("en"),e.datepicker&&e.datepicker.setDefaults(e.datepicker.regional[""])}); \ No newline at end of file
diff --git a/plugins/Calendar/Assets/moment.min.js b/plugins/Calendar/Assets/moment.min.js
new file mode 100644
index 00000000..8c706712
--- /dev/null
+++ b/plugins/Calendar/Assets/moment.min.js
@@ -0,0 +1,551 @@
+//! moment.js
+//! version : 2.17.1
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return od.apply(null,arguments)}
+// This is done to register the method called with moment()
+// without creating circular dependencies.
+function b(a){od=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){
+// IE8 will treat undefined and null as object if it wasn't for
+// input != null
+return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a)
+// even if its not own property I'd still call it non-empty
+return!1;return!0}function f(a){return"number"==typeof a||"[object Number]"===Object.prototype.toString.call(a)}function g(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function h(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function i(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function j(a,b){for(var c in b)i(b,c)&&(a[c]=b[c]);return i(b,"toString")&&(a.toString=b.toString),i(b,"valueOf")&&(a.valueOf=b.valueOf),a}function k(a,b,c,d){return rb(a,b,c,d,!0).utc()}function l(){
+// We need to deep clone this object.
+return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function m(a){return null==a._pf&&(a._pf=l()),a._pf}function n(a){if(null==a._isValid){var b=m(a),c=qd.call(b.parsedDateParts,function(a){return null!=a}),d=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c);if(a._strict&&(d=d&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour),null!=Object.isFrozen&&Object.isFrozen(a))return d;a._isValid=d}return a._isValid}function o(a){var b=k(NaN);return null!=a?j(m(b),a):m(b).userInvalidated=!0,b}function p(a){return void 0===a}function q(a,b){var c,d,e;if(p(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),p(b._i)||(a._i=b._i),p(b._f)||(a._f=b._f),p(b._l)||(a._l=b._l),p(b._strict)||(a._strict=b._strict),p(b._tzm)||(a._tzm=b._tzm),p(b._isUTC)||(a._isUTC=b._isUTC),p(b._offset)||(a._offset=b._offset),p(b._pf)||(a._pf=m(b)),p(b._locale)||(a._locale=b._locale),rd.length>0)for(c in rd)d=rd[c],e=b[d],p(e)||(a[d]=e);return a}
+// Moment prototype object
+function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),
+// Prevent infinite loop in case updateOffset creates new moment
+// objects.
+sd===!1&&(sd=!0,a.updateOffset(this),sd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c}
+// compare two arrays, return the number of differences
+function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d<e;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function w(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function x(b,c){var d=!0;return j(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,b),d){for(var e,f=[],g=0;g<arguments.length;g++){if(e="","object"==typeof arguments[g]){e+="\n["+g+"] ";for(var h in arguments[0])e+=h+": "+arguments[0][h]+", ";e=e.slice(0,-2)}else e=arguments[g];f.push(e)}w(b+"\nArguments: "+Array.prototype.slice.call(f).join("")+"\n"+(new Error).stack),d=!1}return c.apply(this,arguments)},c)}function y(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),td[b]||(w(c),td[b]=!0)}function z(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function A(a){var b,c;for(c in a)b=a[c],z(b)?this[c]=b:this["_"+c]=b;this._config=a,
+// Lenient ordinal parsing accepts just a number in addition to
+// number + (possibly) stuff coming from _ordinalParseLenient.
+this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function B(a,b){var c,e=j({},a);for(c in b)i(b,c)&&(d(a[c])&&d(b[c])?(e[c]={},j(e[c],a[c]),j(e[c],b[c])):null!=b[c]?e[c]=b[c]:delete e[c]);for(c in a)i(a,c)&&!i(b,c)&&d(a[c])&&(
+// make sure changes to properties don't modify parent config
+e[c]=j({},e[c]));return e}function C(a){null!=a&&this.set(a)}function D(a,b,c){var d=this._calendar[a]||this._calendar.sameElse;return z(d)?d.call(b,c):d}function E(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function F(){return this._invalidDate}function G(a){return this._ordinal.replace("%d",a)}function H(a,b,c,d){var e=this._relativeTime[c];return z(e)?e(a,b,c,d):e.replace(/%d/i,a)}function I(a,b){var c=this._relativeTime[a>0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Dd[c]=Dd[c+"s"]=Dd[b]=a}function K(a){return"string"==typeof a?Dd[a]||Dd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)i(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Ed[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Ed[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}
+// MOMENTS
+function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d<c.length;d++)this[c[d].unit](a[c[d].unit])}else if(a=K(a),z(this[a]))return this[a](b);return this}function T(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}
+// token: 'M'
+// padded: ['MM', 2]
+// ordinal: 'Mo'
+// callback: function () { this.month() + 1 }
+function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Id[a]=e),b&&(Id[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Id[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Fd);for(b=0,c=d.length;b<c;b++)Id[d[b]]?d[b]=Id[d[b]]:d[b]=V(d[b]);return function(b){var e,f="";for(e=0;e<c;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}
+// format date using native date object
+function X(a,b){return a.isValid()?(b=Y(b,a.localeData()),Hd[b]=Hd[b]||W(b),Hd[b](a)):a.localeData().invalidDate()}function Y(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Gd.lastIndex=0;d>=0&&Gd.test(a);)a=a.replace(Gd,c),Gd.lastIndex=0,d-=1;return a}function Z(a,b,c){$d[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return i($d,a)?$d[a](b._strict,b._locale):new RegExp(_(a))}
+// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),f(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c<a.length;c++)_d[a[c]]=d}function ca(a,b){ba(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function da(a,b,c){null!=b&&i(_d,a)&&_d[a](b,c._a,c,a)}function ea(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function fa(a,b){return a?c(this._months)?this._months[a.month()]:this._months[(this._months.isFormat||ke).test(b)?"format":"standalone"][a.month()]:this._months}function ga(a,b){return a?c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[ke.test(b)?"format":"standalone"][a.month()]:this._monthsShort}function ha(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(
+// this is not used
+this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;d<12;++d)f=k([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:null):(e=je.call(this._longMonthsParse,g),e!==-1?e:null):"MMM"===b?(e=je.call(this._shortMonthsParse,g),e!==-1?e:(e=je.call(this._longMonthsParse,g),e!==-1?e:null)):(e=je.call(this._longMonthsParse,g),e!==-1?e:(e=je.call(this._shortMonthsParse,g),e!==-1?e:null))}function ia(a,b,c){var d,e,f;if(this._monthsParseExact)return ha.call(this,a,b,c);
+// TODO: add sorting
+// Sorting makes sure if one month (or abbr) is a prefix of another
+// see sorting in computeMonthsParse
+for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;d<12;d++){
+// test the regex
+if(
+// make the regex if we don't have it already
+e=k([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}
+// MOMENTS
+function ja(a,b){var c;if(!a.isValid())
+// No op
+return a;if("string"==typeof b)if(/^\d+$/.test(b))b=u(b);else
+// TODO: Another silent failure?
+if(b=a.localeData().monthsParse(b),!f(b))return a;return c=Math.min(a.date(),ea(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ka(b){return null!=b?(ja(this,b),a.updateOffset(this,!0),this):P(this,"Month")}function la(){return ea(this.year(),this.month())}function ma(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):(i(this,"_monthsShortRegex")||(this._monthsShortRegex=ne),this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex)}function na(a){return this._monthsParseExact?(i(this,"_monthsRegex")||oa.call(this),a?this._monthsStrictRegex:this._monthsRegex):(i(this,"_monthsRegex")||(this._monthsRegex=oe),this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex)}function oa(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;b<12;b++)
+// make the regex if we don't have it already
+c=k([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(
+// Sorting makes sure if one month (or abbr) is a prefix of another it
+// will match the longer piece.
+d.sort(a),e.sort(a),f.sort(a),b=0;b<12;b++)d[b]=aa(d[b]),e[b]=aa(e[b]);for(b=0;b<24;b++)f[b]=aa(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}
+// HELPERS
+function pa(a){return qa(a)?366:365}function qa(a){return a%4===0&&a%100!==0||a%400===0}function ra(){return qa(this.year())}function sa(a,b,c,d,e,f,g){
+//can't just apply() to create a date:
+//http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+var h=new Date(a,b,c,d,e,f,g);
+//the date constructor remaps years 0-99 to 1900-1999
+return a<100&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments));
+//the Date.UTC function remaps years 0-99 to 1900-1999
+return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}
+// start-of-first-week - start-of-year
+function ua(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other)
+d=7+b-c,
+// first-week day local weekday -- which local weekday is fwd
+e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1}
+//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7}
+// HELPERS
+// LOCALES
+function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy}
+// MOMENTS
+function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}
+// HELPERS
+function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=k([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){
+// test the regex
+if(
+// make the regex if we don't have it already
+e=k([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}
+// MOMENTS
+function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN;
+// behaves the same as moment#day except
+// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+// as a setter, sunday should belong to the previous week.
+if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,"_weekdaysRegex")||(this._weekdaysRegex=ue),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ve),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=we),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++)
+// make the regex if we don't have it already
+c=k([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for(
+// Sorting makes sure if one weekday (or abbr) is a prefix of another it
+// will match the longer piece.
+g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}
+// FORMATTING
+function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}
+// PARSING
+function Ua(a,b){return b._meridiemParse}
+// LOCALES
+function Va(a){
+// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+// Using charAt should be more compatible.
+return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a}
+// pick the locale from the array
+// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+function Ya(a){for(var b,c,d,e,f=0;f<a.length;){for(e=Xa(a[f]).split("-"),b=e.length,c=Xa(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1)
+//the next array item is better than a shallower substring of this one
+break;b--}f++}return null}function Za(a){var b=null;
+// TODO: Find a better way to register and load all the locales in Node
+if(!Be[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=xe._abbr,require("./locale/"+a),
+// because defineLocale currently also sets the global locale, we
+// want to undo that for lazy loaded locales
+$a(b)}catch(a){}return Be[a]}
+// This function will load locale and then set the global locale. If
+// no arguments are passed in, it will simply return the current global
+// locale key.
+function $a(a,b){var c;
+// moment.duration._locale = moment._locale = data;
+return a&&(c=p(b)?bb(a):_a(a,b),c&&(xe=c)),xe._abbr}function _a(a,b){if(null!==b){var c=Ae;if(b.abbr=a,null!=Be[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Be[a]._config;else if(null!=b.parentLocale){if(null==Be[b.parentLocale])return Ce[b.parentLocale]||(Ce[b.parentLocale]=[]),Ce[b.parentLocale].push({name:a,config:b}),null;c=Be[b.parentLocale]._config}
+// backwards compat for now: also set the locale
+// make sure we set the locale AFTER all child locales have been
+// created, so we won't end up with the child locale set.
+return Be[a]=new C(B(c,b)),Ce[a]&&Ce[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Be[a]}
+// useful for testing
+return delete Be[a],null}function ab(a,b){if(null!=b){var c,d=Ae;
+// MERGE
+null!=Be[a]&&(d=Be[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Be[a],Be[a]=c,
+// backwards compat for now: also set the locale
+$a(a)}else
+// pass null for config to unupdate, useful for tests
+null!=Be[a]&&(null!=Be[a].parentLocale?Be[a]=Be[a].parentLocale:null!=Be[a]&&delete Be[a]);return Be[a]}
+// returns locale data
+function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return xe;if(!c(a)){if(
+//short-circuit everything else
+b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return wd(Be)}function db(a){var b,c=a._a;return c&&m(a).overflow===-2&&(b=c[be]<0||c[be]>11?be:c[ce]<1||c[ce]>ea(c[ae],c[be])?ce:c[de]<0||c[de]>24||24===c[de]&&(0!==c[ee]||0!==c[fe]||0!==c[ge])?de:c[ee]<0||c[ee]>59?ee:c[fe]<0||c[fe]>59?fe:c[ge]<0||c[ge]>999?ge:-1,m(a)._overflowDayOfYear&&(b<ae||b>ce)&&(b=ce),m(a)._overflowWeeks&&b===-1&&(b=he),m(a)._overflowWeekday&&b===-1&&(b=ie),m(a).overflow=b),a}
+// date from iso format
+function eb(a){var b,c,d,e,f,g,h=a._i,i=De.exec(h)||Ee.exec(h);if(i){for(m(a).iso=!0,b=0,c=Ge.length;b<c;b++)if(Ge[b][1].exec(i[1])){e=Ge[b][0],d=Ge[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=He.length;b<c;b++)if(He[b][1].exec(i[3])){
+// match[2] should be 'T' or space
+f=(i[2]||" ")+He[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!Fe.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),kb(a)}else a._isValid=!1}
+// date from iso format or fallback
+function fb(b){var c=Ie.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(eb(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}
+// Pick the first defined of two or three arguments.
+function gb(a,b,c){return null!=a?a:null!=b?b:c}function hb(b){
+// hooks is actually the exported moment object
+var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}
+// convert an array to a date.
+// the array should mirror the parameters below
+// note: all values past the year are optional and will default to the lowest possible value.
+// [year, month, day , hour, minute, second, millisecond]
+function ib(a){var b,c,d,e,f=[];if(!a._d){
+// Default to current date.
+// * if no year, month, day of month are given, default to today
+// * if day of month is given, default month and year
+// * if month is given, default only year
+// * if year is given, don't default anything
+for(d=hb(a),
+//compute day of the year from weeks and weekdays
+a._w&&null==a._a[ce]&&null==a._a[be]&&jb(a),
+//if the day of the year is set, figure out what it is
+a._dayOfYear&&(e=gb(a._a[ae],d[ae]),a._dayOfYear>pa(e)&&(m(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[be]=c.getUTCMonth(),a._a[ce]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b];
+// Zero out whatever was not defaulted, including time
+for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];
+// Check for 24:00:00.000
+24===a._a[de]&&0===a._a[ee]&&0===a._a[fe]&&0===a._a[ge]&&(a._nextDay=!0,a._a[de]=0),a._d=(a._useUTC?ta:sa).apply(null,f),
+// Apply timezone offset from input. The actual utcOffset can be changed
+// with parseZone.
+null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[de]=24)}}function jb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4,
+// TODO: We need to take the current isoWeekYear, but that depends on
+// how we interpret now (local, utc, fixed offset). So create
+// a now version of current config (take local/utc/offset flags, and
+// create now).
+c=gb(b.GG,a._a[ae],wa(sb(),1,4).year),d=gb(b.W,1),e=gb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(sb(),f,g);c=gb(b.gg,a._a[ae],j.year),
+// Default to current week.
+d=gb(b.w,j.week),null!=b.d?(
+// weekday -- low day numbers are considered next week
+e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?(
+// local weekday -- counting starts from begining of week
+e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):
+// default to begining of week
+e=f}d<1||d>xa(c,f,g)?m(a)._overflowWeeks=!0:null!=i?m(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ae]=h.year,a._dayOfYear=h.dayOfYear)}
+// date from string and format string
+function kb(b){
+// TODO: Move this to another part of the creation flow to prevent circular deps
+if(b._f===a.ISO_8601)return void eb(b);b._a=[],m(b).empty=!0;
+// This array is used to make a Date, either with `new Date` or `Date.UTC`
+var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Fd)||[],c=0;c<e.length;c++)f=e[c],d=(h.match($(f,b))||[])[0],
+// console.log('token', token, 'parsedInput', parsedInput,
+// 'regex', getParseRegexForToken(token, config));
+d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&m(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),
+// don't parse if it's not a known token
+Id[f]?(d?m(b).empty=!1:m(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&m(b).unusedTokens.push(f);
+// add remaining unparsed input length to the string
+m(b).charsLeftOver=i-j,h.length>0&&m(b).unusedInput.push(h),
+// clear _12h flag if hour is <= 12
+b._a[de]<=12&&m(b).bigHour===!0&&b._a[de]>0&&(m(b).bigHour=void 0),m(b).parsedDateParts=b._a.slice(0),m(b).meridiem=b._meridiem,
+// handle meridiem
+b._a[de]=lb(b._locale,b._a[de],b._meridiem),ib(b),db(b)}function lb(a,b,c){var d;
+// Fallback
+return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b}
+// date from string and array of format strings
+function mb(a){var b,c,d,e,f;if(0===a._f.length)return m(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=q({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],kb(b),n(b)&&(
+// if there is any input that was not parsed add a penalty for that format
+f+=m(b).charsLeftOver,
+//or tokens
+f+=10*m(b).unusedTokens.length,m(b).score=f,(null==d||f<d)&&(d=f,c=b));j(a,c||b)}function nb(a){if(!a._d){var b=L(a._i);a._a=h([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),ib(a)}}function ob(a){var b=new r(db(pb(a)));
+// Adding is smart enough around DST
+return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function pb(a){var b=a._i,d=a._f;return a._locale=a._locale||bb(a._l),null===b||void 0===d&&""===b?o({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),s(b)?new r(db(b)):(g(b)?a._d=b:c(d)?mb(a):d?kb(a):qb(a),n(a)||(a._d=null),a))}function qb(b){var d=b._i;void 0===d?b._d=new Date(a.now()):g(d)?b._d=new Date(d.valueOf()):"string"==typeof d?fb(b):c(d)?(b._a=h(d.slice(0),function(a){return parseInt(a,10)}),ib(b)):"object"==typeof d?nb(b):f(d)?
+// from milliseconds
+b._d=new Date(d):a.createFromInputFallback(b)}function rb(a,b,f,g,h){var i={};
+// object construction must be done this way.
+// https://github.com/moment/moment/issues/1423
+return f!==!0&&f!==!1||(g=f,f=void 0),(d(a)&&e(a)||c(a)&&0===a.length)&&(a=void 0),i._isAMomentObject=!0,i._useUTC=i._isUTC=h,i._l=f,i._i=a,i._f=b,i._strict=g,ob(i)}function sb(a,b,c,d){return rb(a,b,c,d,!1)}
+// Pick a moment m from moments so that m[fn](other) is true for all
+// other. This relies on the function fn to be transitive.
+//
+// moments should either be an array of moment objects or an array, whose
+// first element is an array of moment objects.
+function tb(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return sb();for(d=b[0],e=1;e<b.length;++e)b[e].isValid()&&!b[e][a](d)||(d=b[e]);return d}
+// TODO: Use [].sort instead?
+function ub(){var a=[].slice.call(arguments,0);return tb("isBefore",a)}function vb(){var a=[].slice.call(arguments,0);return tb("isAfter",a)}function wb(a){var b=L(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;
+// representation for dateAddRemove
+this._milliseconds=+k+1e3*j+// 1000
+6e4*i+// 1000 * 60
+1e3*h*60*60,//using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
+// Because of dateAddRemove treats 24 hours as different from a
+// day when working around DST, we need to store them separately
+this._days=+g+7*f,
+// It is impossible translate months into days without knowing
+// which months you are are talking about, so we have to store
+// it separately.
+this._months=+e+3*d+12*c,this._data={},this._locale=bb(),this._bubble()}function xb(a){return a instanceof wb}function yb(a){return a<0?Math.round(-1*a)*-1:Math.round(a)}
+// FORMATTING
+function zb(a,b){U(a,0,0,function(){var a=this.utcOffset(),c="+";return a<0&&(a=-a,c="-"),c+T(~~(a/60),2)+b+T(~~a%60,2)})}function Ab(a,b){var c=(b||"").match(a);if(null===c)return null;var d=c[c.length-1]||[],e=(d+"").match(Me)||["-",0,0],f=+(60*e[1])+u(e[2]);return 0===f?0:"+"===e[0]?f:-f}
+// Return a moment from input, that is local/utc/zone equivalent to model.
+function Bb(b,c){var d,e;
+// Use low-level api, because this fn is low-level api.
+return c._isUTC?(d=c.clone(),e=(s(b)||g(b)?b.valueOf():sb(b).valueOf())-d.valueOf(),d._d.setTime(d._d.valueOf()+e),a.updateOffset(d,!1),d):sb(b).local()}function Cb(a){
+// On Firefox.24 Date#getTimezoneOffset returns a floating point.
+// https://github.com/moment/moment/pull/1871
+return 15*-Math.round(a._d.getTimezoneOffset()/15)}
+// MOMENTS
+// keepLocalTime = true means only change the timezone, without
+// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+// +0200, so we adjust the time as needed, to be valid.
+//
+// Keeping the time actually adds/subtracts (one hour)
+// from the actual represented time. That is why we call updateOffset
+// a second time. In case it wants us to change the offset again
+// _changeInProgress == true case, then we have to adjust, because
+// there is no such time in the given timezone.
+function Db(b,c){var d,e=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Ab(Xd,b),null===b)return this}else Math.abs(b)<16&&(b=60*b);return!this._isUTC&&c&&(d=Cb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Tb(this,Ob(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?e:Cb(this)}function Eb(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Fb(a){return this.utcOffset(0,a)}function Gb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Cb(this),"m")),this}function Hb(){if(null!=this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=Ab(Wd,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Ib(a){return!!this.isValid()&&(a=a?sb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Jb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Kb(){if(!p(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=pb(a),a._a){var b=a._isUTC?k(a._a):sb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Lb(){return!!this.isValid()&&!this._isUTC}function Mb(){return!!this.isValid()&&this._isUTC}function Nb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ob(a,b){var c,d,e,g=a,
+// matching against regexp is expensive, do it on demand
+h=null;// checks for null or undefined
+return xb(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:f(a)?(g={},b?g[b]=a:g.milliseconds=a):(h=Ne.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:u(h[ce])*c,h:u(h[de])*c,m:u(h[ee])*c,s:u(h[fe])*c,ms:u(yb(1e3*h[ge]))*c}):(h=Oe.exec(a))?(c="-"===h[1]?-1:1,g={y:Pb(h[2],c),M:Pb(h[3],c),w:Pb(h[4],c),d:Pb(h[5],c),h:Pb(h[6],c),m:Pb(h[7],c),s:Pb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Rb(sb(g.from),sb(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new wb(g),xb(a)&&i(a,"_locale")&&(d._locale=a._locale),d}function Pb(a,b){
+// We'd normally use ~~inp for this, but unfortunately it also
+// converts floats to ints.
+// inp may be undefined, so careful calling replace on it.
+var c=a&&parseFloat(a.replace(",","."));
+// apply sign while we're at it
+return(isNaN(c)?0:c)*b}function Qb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Rb(a,b){var c;return a.isValid()&&b.isValid()?(b=Bb(b,a),a.isBefore(b)?c=Qb(a,b):(c=Qb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}
+// TODO: remove 'name' arg after deprecation is removed
+function Sb(a,b){return function(c,d){var e,f;
+//invert the arguments, but complain about it
+return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ob(c,d),Tb(this,e,a),this}}function Tb(b,c,d,e){var f=c._milliseconds,g=yb(c._days),h=yb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Ub(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Vb(b,c){
+// We want to compare the start of today, vs this.
+// Getting start-of-today depends on whether we're local/utc/offset or not.
+var d=b||sb(),e=Bb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,sb(d)))}function Wb(){return new r(this)}function Xb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf())}function Yb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf())}function Zb(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function $b(a,b){var c,d=s(a)?a:sb(a);return!(!this.isValid()||!d.isValid())&&(b=K(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf()))}function _b(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function ac(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function bc(a,b,c){var d,e,f,g;// 1000
+// 1000 * 60
+// 1000 * 60 * 60
+// 1000 * 60 * 60 * 24, negate dst
+// 1000 * 60 * 60 * 24 * 7, negate dst
+return this.isValid()?(d=Bb(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=K(b),"year"===b||"month"===b||"quarter"===b?(g=cc(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:t(g)):NaN):NaN}function cc(a,b){
+// difference in months
+var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),
+// b is in (anchor - 1 month, anchor + 1 month)
+f=a.clone().add(e,"months");
+//check for negative zero, return zero if negative zero
+// linear across the month
+// linear across the month
+return b-f<0?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function dc(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ec(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?z(Date.prototype.toISOString)?this.toDate().toISOString():X(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):X(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}/**
+ * Return a human readable representation of a moment that can
+ * also be evaluated to get a new moment which is the same
+ *
+ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
+ */
+function fc(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var a="moment",b="";this.isLocal()||(a=0===this.utcOffset()?"moment.utc":"moment.parseZone",b="Z");var c="["+a+'("]',d=0<this.year()&&this.year()<=9999?"YYYY":"YYYYYY",e="-MM-DD[T]HH:mm:ss.SSS",f=b+'[")]';return this.format(c+d+e+f)}function gc(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=X(this,b);return this.localeData().postformat(c)}function hc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function ic(a){return this.from(sb(),a)}function jc(a,b){return this.isValid()&&(s(a)&&a.isValid()||sb(a).isValid())?Ob({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function kc(a){return this.to(sb(),a)}
+// If passed a locale key, it will set the locale for this
+// instance. Otherwise, it will return the locale configuration
+// variables for this instance.
+function lc(a){var b;return void 0===a?this._locale._abbr:(b=bb(a),null!=b&&(this._locale=b),this)}function mc(){return this._locale}function nc(a){
+// the following switch intentionally omits break keywords
+// to utilize falling through the cases.
+switch(a=K(a)){case"year":this.month(0);/* falls through */
+case"quarter":case"month":this.date(1);/* falls through */
+case"week":case"isoWeek":case"day":case"date":this.hours(0);/* falls through */
+case"hour":this.minutes(0);/* falls through */
+case"minute":this.seconds(0);/* falls through */
+case"second":this.milliseconds(0)}
+// weeks are a special case
+// quarters are also special
+return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function oc(a){
+// 'date' is an alias for 'day', so it should be considered as such.
+return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function pc(){return this._d.valueOf()-6e4*(this._offset||0)}function qc(){return Math.floor(this.valueOf()/1e3)}function rc(){return new Date(this.valueOf())}function sc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function tc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function uc(){
+// new Date(NaN).toJSON() === null
+return this.isValid()?this.toISOString():null}function vc(){return n(this)}function wc(){return j({},m(this))}function xc(){return m(this).overflow}function yc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function zc(a,b){U(0,[a,a.length],0,b)}
+// MOMENTS
+function Ac(a){return Ec.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Bc(a){return Ec.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Cc(){return xa(this.year(),1,4)}function Dc(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ec(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Fc.call(this,a,b,c,d,e))}function Fc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}
+// MOMENTS
+function Gc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}
+// HELPERS
+// MOMENTS
+function Hc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Ic(a,b){b[ge]=u(1e3*("0."+a))}
+// MOMENTS
+function Jc(){return this._isUTC?"UTC":""}function Kc(){return this._isUTC?"Coordinated Universal Time":""}function Lc(a){return sb(1e3*a)}function Mc(){return sb.apply(null,arguments).parseZone()}function Nc(a){return a}function Oc(a,b,c,d){var e=bb(),f=k().set(d,b);return e[c](f,a)}function Pc(a,b,c){if(f(a)&&(b=a,a=void 0),a=a||"",null!=b)return Oc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Oc(a,d,c,"month");return e}
+// ()
+// (5)
+// (fmt, 5)
+// (fmt)
+// (true)
+// (true, 5)
+// (true, fmt, 5)
+// (true, fmt)
+function Qc(a,b,c,d){"boolean"==typeof a?(f(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,f(b)&&(c=b,b=void 0),b=b||"");var e=bb(),g=a?e._week.dow:0;if(null!=c)return Oc(b,(c+g)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Oc(b,(h+g)%7,d,"day");return i}function Rc(a,b){return Pc(a,b,"months")}function Sc(a,b){return Pc(a,b,"monthsShort")}function Tc(a,b,c){return Qc(a,b,c,"weekdays")}function Uc(a,b,c){return Qc(a,b,c,"weekdaysShort")}function Vc(a,b,c){return Qc(a,b,c,"weekdaysMin")}function Wc(){var a=this._data;return this._milliseconds=Ze(this._milliseconds),this._days=Ze(this._days),this._months=Ze(this._months),a.milliseconds=Ze(a.milliseconds),a.seconds=Ze(a.seconds),a.minutes=Ze(a.minutes),a.hours=Ze(a.hours),a.months=Ze(a.months),a.years=Ze(a.years),this}function Xc(a,b,c,d){var e=Ob(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}
+// supports only 2.0-style add(1, 's') or add(duration)
+function Yc(a,b){return Xc(this,a,b,1)}
+// supports only 2.0-style subtract(1, 's') or subtract(duration)
+function Zc(a,b){return Xc(this,a,b,-1)}function $c(a){return a<0?Math.floor(a):Math.ceil(a)}function _c(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;
+// if we have a mix of positive and negative values, bubble down first
+// check: https://github.com/moment/moment/issues/2166
+// The following code bubbles up values, see the tests for
+// examples of what that means.
+// convert days to months
+// 12 months -> 1 year
+return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*$c(bd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ad(g)),h+=e,g-=$c(bd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ad(a){
+// 400 years have 146097 days (taking into account leap year rules)
+// 400 years have 12 months === 4800
+return 4800*a/146097}function bd(a){
+// the reverse of daysToMonths
+return 146097*a/4800}function cd(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ad(b),"month"===a?c:c/12;switch(
+// handle milliseconds separately because of floating point math errors (issue #1867)
+b=this._days+Math.round(bd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;
+// Math.floor prevents floating point math errors here
+case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}
+// TODO: Use this.as('ms')?
+function dd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)}function ed(a){return function(){return this.as(a)}}function fd(a){return a=K(a),this[a+"s"]()}function gd(a){return function(){return this._data[a]}}function hd(){return t(this.days()/7)}
+// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+function id(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function jd(a,b,c){var d=Ob(a).abs(),e=of(d.as("s")),f=of(d.as("m")),g=of(d.as("h")),h=of(d.as("d")),i=of(d.as("M")),j=of(d.as("y")),k=e<pf.s&&["s",e]||f<=1&&["m"]||f<pf.m&&["mm",f]||g<=1&&["h"]||g<pf.h&&["hh",g]||h<=1&&["d"]||h<pf.d&&["dd",h]||i<=1&&["M"]||i<pf.M&&["MM",i]||j<=1&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,id.apply(null,k)}
+// This function allows you to set the rounding function for relative time strings
+function kd(a){return void 0===a?of:"function"==typeof a&&(of=a,!0)}
+// This function allows you to set a threshold for relative time strings
+function ld(a,b){return void 0!==pf[a]&&(void 0===b?pf[a]:(pf[a]=b,!0))}function md(a){var b=this.localeData(),c=jd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function nd(){
+// for ISO strings we do not use the normal bubbling rules:
+// * milliseconds bubble up until they become hours
+// * days do not bubble at all
+// * months bubble up until they become years
+// This is because there is no context-free conversion between hours and days
+// (think of clock changes)
+// and also not between days and months (28-31 days per month)
+var a,b,c,d=qf(this._milliseconds)/1e3,e=qf(this._days),f=qf(this._months);
+// 3600 seconds -> 60 minutes -> 1 hour
+a=t(d/60),b=t(a/60),d%=60,a%=60,
+// 12 months -> 1 year
+c=t(f/12),f%=12;
+// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var od,pd;pd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d<c;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var qd=pd,rd=a.momentProperties=[],sd=!1,td={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var ud;ud=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)i(a,b)&&c.push(b);return c};var vd,wd=ud,xd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},yd={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},zd="Invalid date",Ad="%d",Bd=/\d{1,2}/,Cd={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Dd={},Ed={},Fd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Hd={},Id={},Jd=/\d/,Kd=/\d\d/,Ld=/\d{3}/,Md=/\d{4}/,Nd=/[+-]?\d{6}/,Od=/\d\d?/,Pd=/\d\d\d\d?/,Qd=/\d\d\d\d\d\d?/,Rd=/\d{1,3}/,Sd=/\d{1,4}/,Td=/[+-]?\d{1,6}/,Ud=/\d+/,Vd=/[+-]?\d+/,Wd=/Z|[+-]\d\d:?\d\d/gi,Xd=/Z|[+-]\d\d(?::?\d\d)?/gi,Yd=/[+-]?\d+(\.\d{1,3})?/,Zd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,$d={},_d={},ae=0,be=1,ce=2,de=3,ee=4,fe=5,ge=6,he=7,ie=8;vd=Array.prototype.indexOf?Array.prototype.indexOf:function(a){
+// I know
+var b;for(b=0;b<this.length;++b)if(this[b]===a)return b;return-1};var je=vd;
+// FORMATTING
+U("M",["MM",2],"Mo",function(){return this.month()+1}),U("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),U("MMMM",0,0,function(a){return this.localeData().months(this,a)}),
+// ALIASES
+J("month","M"),
+// PRIORITY
+M("month",8),
+// PARSING
+Z("M",Od),Z("MM",Od,Kd),Z("MMM",function(a,b){return b.monthsShortRegex(a)}),Z("MMMM",function(a,b){return b.monthsRegex(a)}),ba(["M","MM"],function(a,b){b[be]=u(a)-1}),ba(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);
+// if we didn't find a month name, mark the date as invalid.
+null!=e?b[be]=e:m(c).invalidMonth=a});
+// LOCALES
+var ke=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,le="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),me="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ne=Zd,oe=Zd;
+// FORMATTING
+U("Y",0,0,function(){var a=this.year();return a<=9999?""+a:"+"+a}),U(0,["YY",2],0,function(){return this.year()%100}),U(0,["YYYY",4],0,"year"),U(0,["YYYYY",5],0,"year"),U(0,["YYYYYY",6,!0],0,"year"),
+// ALIASES
+J("year","y"),
+// PRIORITIES
+M("year",1),
+// PARSING
+Z("Y",Vd),Z("YY",Od,Kd),Z("YYYY",Sd,Md),Z("YYYYY",Td,Nd),Z("YYYYYY",Td,Nd),ba(["YYYYY","YYYYYY"],ae),ba("YYYY",function(b,c){c[ae]=2===b.length?a.parseTwoDigitYear(b):u(b)}),ba("YY",function(b,c){c[ae]=a.parseTwoDigitYear(b)}),ba("Y",function(a,b){b[ae]=parseInt(a,10)}),
+// HOOKS
+a.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?1900:2e3)};
+// MOMENTS
+var pe=O("FullYear",!0);
+// FORMATTING
+U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"),
+// ALIASES
+J("week","w"),J("isoWeek","W"),
+// PRIORITIES
+M("week",5),M("isoWeek",5),
+// PARSING
+Z("w",Od),Z("ww",Od,Kd),Z("W",Od),Z("WW",Od,Kd),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var qe={dow:0,// Sunday is the first day of the week.
+doy:6};
+// FORMATTING
+U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"),
+// ALIASES
+J("day","d"),J("weekday","e"),J("isoWeekday","E"),
+// PRIORITY
+M("day",11),M("weekday",11),M("isoWeekday",11),
+// PARSING
+Z("d",Od),Z("e",Od),Z("E",Od),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);
+// if we didn't get a weekday name, mark the date as invalid
+null!=e?b.d=e:m(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)});
+// LOCALES
+var re="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),se="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),te="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ue=Zd,ve=Zd,we=Zd;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1),
+// ALIASES
+J("hour","h"),
+// PRIORITY
+M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Od),Z("h",Od),Z("HH",Od,Kd),Z("hh",Od,Kd),Z("hmm",Pd),Z("hmmss",Qd),Z("Hmm",Pd),Z("Hmmss",Qd),ba(["H","HH"],de),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[de]=u(a),m(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d)),m(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e)),m(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e))});var xe,ye=/[ap]\.?m?\.?/i,ze=O("Hours",!0),Ae={calendar:xd,longDateFormat:yd,invalidDate:zd,ordinal:Ad,ordinalParse:Bd,relativeTime:Cd,months:le,monthsShort:me,week:qe,weekdays:re,weekdaysMin:te,weekdaysShort:se,meridiemParse:ye},Be={},Ce={},De=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ee=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Fe=/Z|[+-]\d\d(?::?\d\d)?/,Ge=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],
+// YYYYMM is NOT allowed by the standard
+["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],He=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ie=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=x("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),
+// constant that refers to the ISO standard
+a.ISO_8601=function(){};var Je=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?a<this?this:a:o()}),Ke=x("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:o()}),Le=function(){return Date.now?Date.now():+new Date};zb("Z",":"),zb("ZZ",""),
+// PARSING
+Z("Z",Xd),Z("ZZ",Xd),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ab(Xd,a)});
+// HELPERS
+// timezone chunker
+// '+10:00' > ['10', '00']
+// '-1530' > ['-15', '30']
+var Me=/([\+\-]|\d\d)/gi;
+// HOOKS
+// This function will be called whenever a moment is mutated.
+// It is intended to keep the offset in sync with the timezone.
+a.updateOffset=function(){};
+// ASP.NET json date format regex
+var Ne=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Oe=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ob.fn=wb.prototype;var Pe=Sb(1,"add"),Qe=Sb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Re=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});
+// FORMATTING
+U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),zc("gggg","weekYear"),zc("ggggg","weekYear"),zc("GGGG","isoWeekYear"),zc("GGGGG","isoWeekYear"),
+// ALIASES
+J("weekYear","gg"),J("isoWeekYear","GG"),
+// PRIORITY
+M("weekYear",1),M("isoWeekYear",1),
+// PARSING
+Z("G",Vd),Z("g",Vd),Z("GG",Od,Kd),Z("gg",Od,Kd),Z("GGGG",Sd,Md),Z("gggg",Sd,Md),Z("GGGGG",Td,Nd),Z("ggggg",Td,Nd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),
+// FORMATTING
+U("Q",0,"Qo","quarter"),
+// ALIASES
+J("quarter","Q"),
+// PRIORITY
+M("quarter",7),
+// PARSING
+Z("Q",Jd),ba("Q",function(a,b){b[be]=3*(u(a)-1)}),
+// FORMATTING
+U("D",["DD",2],"Do","date"),
+// ALIASES
+J("date","D"),
+// PRIOROITY
+M("date",9),
+// PARSING
+Z("D",Od),Z("DD",Od,Kd),Z("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),ba(["D","DD"],ce),ba("Do",function(a,b){b[ce]=u(a.match(Od)[0],10)});
+// MOMENTS
+var Se=O("Date",!0);
+// FORMATTING
+U("DDD",["DDDD",3],"DDDo","dayOfYear"),
+// ALIASES
+J("dayOfYear","DDD"),
+// PRIORITY
+M("dayOfYear",4),
+// PARSING
+Z("DDD",Rd),Z("DDDD",Ld),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}),
+// FORMATTING
+U("m",["mm",2],0,"minute"),
+// ALIASES
+J("minute","m"),
+// PRIORITY
+M("minute",14),
+// PARSING
+Z("m",Od),Z("mm",Od,Kd),ba(["m","mm"],ee);
+// MOMENTS
+var Te=O("Minutes",!1);
+// FORMATTING
+U("s",["ss",2],0,"second"),
+// ALIASES
+J("second","s"),
+// PRIORITY
+M("second",15),
+// PARSING
+Z("s",Od),Z("ss",Od,Kd),ba(["s","ss"],fe);
+// MOMENTS
+var Ue=O("Seconds",!1);
+// FORMATTING
+U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),
+// ALIASES
+J("millisecond","ms"),
+// PRIORITY
+M("millisecond",16),
+// PARSING
+Z("S",Rd,Jd),Z("SS",Rd,Kd),Z("SSS",Rd,Ld);var Ve;for(Ve="SSSS";Ve.length<=9;Ve+="S")Z(Ve,Ud);for(Ve="S";Ve.length<=9;Ve+="S")ba(Ve,Ic);
+// MOMENTS
+var We=O("Milliseconds",!1);
+// FORMATTING
+U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var Xe=r.prototype;Xe.add=Pe,Xe.calendar=Vb,Xe.clone=Wb,Xe.diff=bc,Xe.endOf=oc,Xe.format=gc,Xe.from=hc,Xe.fromNow=ic,Xe.to=jc,Xe.toNow=kc,Xe.get=R,Xe.invalidAt=xc,Xe.isAfter=Xb,Xe.isBefore=Yb,Xe.isBetween=Zb,Xe.isSame=$b,Xe.isSameOrAfter=_b,Xe.isSameOrBefore=ac,Xe.isValid=vc,Xe.lang=Re,Xe.locale=lc,Xe.localeData=mc,Xe.max=Ke,Xe.min=Je,Xe.parsingFlags=wc,Xe.set=S,Xe.startOf=nc,Xe.subtract=Qe,Xe.toArray=sc,Xe.toObject=tc,Xe.toDate=rc,Xe.toISOString=ec,Xe.inspect=fc,Xe.toJSON=uc,Xe.toString=dc,Xe.unix=qc,Xe.valueOf=pc,Xe.creationData=yc,
+// Year
+Xe.year=pe,Xe.isLeapYear=ra,
+// Week Year
+Xe.weekYear=Ac,Xe.isoWeekYear=Bc,
+// Quarter
+Xe.quarter=Xe.quarters=Gc,
+// Month
+Xe.month=ka,Xe.daysInMonth=la,
+// Week
+Xe.week=Xe.weeks=Ba,Xe.isoWeek=Xe.isoWeeks=Ca,Xe.weeksInYear=Dc,Xe.isoWeeksInYear=Cc,
+// Day
+Xe.date=Se,Xe.day=Xe.days=Ka,Xe.weekday=La,Xe.isoWeekday=Ma,Xe.dayOfYear=Hc,
+// Hour
+Xe.hour=Xe.hours=ze,
+// Minute
+Xe.minute=Xe.minutes=Te,
+// Second
+Xe.second=Xe.seconds=Ue,
+// Millisecond
+Xe.millisecond=Xe.milliseconds=We,
+// Offset
+Xe.utcOffset=Db,Xe.utc=Fb,Xe.local=Gb,Xe.parseZone=Hb,Xe.hasAlignedHourOffset=Ib,Xe.isDST=Jb,Xe.isLocal=Lb,Xe.isUtcOffset=Mb,Xe.isUtc=Nb,Xe.isUTC=Nb,
+// Timezone
+Xe.zoneAbbr=Jc,Xe.zoneName=Kc,
+// Deprecations
+Xe.dates=x("dates accessor is deprecated. Use date instead.",Se),Xe.months=x("months accessor is deprecated. Use month instead",ka),Xe.years=x("years accessor is deprecated. Use year instead",pe),Xe.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Eb),Xe.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Kb);var Ye=C.prototype;Ye.calendar=D,Ye.longDateFormat=E,Ye.invalidDate=F,Ye.ordinal=G,Ye.preparse=Nc,Ye.postformat=Nc,Ye.relativeTime=H,Ye.pastFuture=I,Ye.set=A,
+// Month
+Ye.months=fa,Ye.monthsShort=ga,Ye.monthsParse=ia,Ye.monthsRegex=na,Ye.monthsShortRegex=ma,
+// Week
+Ye.week=ya,Ye.firstDayOfYear=Aa,Ye.firstDayOfWeek=za,
+// Day of Week
+Ye.weekdays=Fa,Ye.weekdaysMin=Ha,Ye.weekdaysShort=Ga,Ye.weekdaysParse=Ja,Ye.weekdaysRegex=Na,Ye.weekdaysShortRegex=Oa,Ye.weekdaysMinRegex=Pa,
+// Hours
+Ye.isPM=Va,Ye.meridiem=Wa,$a("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),
+// Side effect imports
+a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var Ze=Math.abs,$e=ed("ms"),_e=ed("s"),af=ed("m"),bf=ed("h"),cf=ed("d"),df=ed("w"),ef=ed("M"),ff=ed("y"),gf=gd("milliseconds"),hf=gd("seconds"),jf=gd("minutes"),kf=gd("hours"),lf=gd("days"),mf=gd("months"),nf=gd("years"),of=Math.round,pf={s:45,// seconds to minute
+m:45,// minutes to hour
+h:22,// hours to day
+d:26,// days to month
+M:11},qf=Math.abs,rf=wb.prototype;
+// Deprecations
+// Side effect imports
+// FORMATTING
+// PARSING
+// Side effect imports
+return rf.abs=Wc,rf.add=Yc,rf.subtract=Zc,rf.as=cd,rf.asMilliseconds=$e,rf.asSeconds=_e,rf.asMinutes=af,rf.asHours=bf,rf.asDays=cf,rf.asWeeks=df,rf.asMonths=ef,rf.asYears=ff,rf.valueOf=dd,rf._bubble=_c,rf.get=fd,rf.milliseconds=gf,rf.seconds=hf,rf.minutes=jf,rf.hours=kf,rf.days=lf,rf.weeks=hd,rf.months=mf,rf.years=nf,rf.humanize=md,rf.toISOString=nd,rf.toString=nd,rf.toJSON=nd,rf.locale=lc,rf.localeData=mc,rf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",nd),rf.lang=Re,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Vd),Z("X",Yd),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.17.1",b(sb),a.fn=Xe,a.min=ub,a.max=vb,a.now=Le,a.utc=k,a.unix=Lc,a.months=Rc,a.isDate=g,a.locale=$a,a.invalid=o,a.duration=Ob,a.isMoment=s,a.weekdays=Tc,a.parseZone=Mc,a.localeData=bb,a.isDuration=xb,a.monthsShort=Sc,a.weekdaysMin=Vc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Uc,a.normalizeUnits=K,a.relativeTimeRounding=kd,a.relativeTimeThreshold=ld,a.calendarFormat=Ub,a.prototype=Xe,a}); \ No newline at end of file
diff --git a/plugins/Calendar/Controller/CalendarController.php b/plugins/Calendar/Controller/CalendarController.php
new file mode 100644
index 00000000..94a1e5e2
--- /dev/null
+++ b/plugins/Calendar/Controller/CalendarController.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar\Controller;
+
+use Kanboard\Controller\BaseController;
+use Kanboard\Filter\TaskAssigneeFilter;
+use Kanboard\Filter\TaskDueDateRangeFilter;
+use Kanboard\Filter\TaskProjectFilter;
+use Kanboard\Filter\TaskStatusFilter;
+use Kanboard\Model\TaskModel;
+
+/**
+ * Calendar Controller
+ *
+ * @package Kanboard\Plugin\Calendar\Controller
+ * @author Frederic Guillot
+ * @author Timo Litzbarski
+ */
+class CalendarController extends BaseController
+{
+ public function user()
+ {
+ $user = $this->getUser();
+
+ $this->response->html($this->helper->layout->app('Calendar:calendar/user', array(
+ 'user' => $user,
+ )));
+ }
+
+ public function project()
+ {
+ $project = $this->getProject();
+
+ $this->response->html($this->helper->layout->app('Calendar:calendar/project', array(
+ 'project' => $project,
+ 'title' => $project['name'],
+ 'description' => $this->helper->projectHeader->getDescription($project),
+ )));
+ }
+
+ public function projectEvents()
+ {
+ $projectId = $this->request->getIntegerParam('project_id');
+ $startRange = $this->request->getStringParam('start');
+ $endRange = $this->request->getStringParam('end');
+ $search = $this->userSession->getFilters($projectId);
+ $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started');
+
+ $dueDateOnlyEvents = $this->taskLexer->build($search)
+ ->withFilter(new TaskProjectFilter($projectId))
+ ->withFilter(new TaskDueDateRangeFilter(array($startRange, $endRange)))
+ ->format($this->taskCalendarFormatter->setColumns('date_due'));
+
+ $startAndDueDateQueryBuilder = $this->taskLexer->build($search)
+ ->withFilter(new TaskProjectFilter($projectId));
+
+ $startAndDueDateQueryBuilder
+ ->getQuery()
+ ->addCondition($this->getConditionForTasksWithStartAndDueDate($startRange, $endRange, $startColumn, 'date_due'));
+
+ $startAndDueDateEvents = $startAndDueDateQueryBuilder
+ ->format($this->taskCalendarFormatter->setColumns($startColumn, 'date_due'));
+
+ $events = array_merge($dueDateOnlyEvents, $startAndDueDateEvents);
+
+ $events = $this->hook->merge('controller:calendar:project:events', $events, array(
+ 'project_id' => $projectId,
+ 'start' => $startRange,
+ 'end' => $endRange,
+ ));
+
+ $this->response->json($events);
+ }
+
+ public function userEvents()
+ {
+ $user_id = $this->request->getIntegerParam('user_id');
+ $startRange = $this->request->getStringParam('start');
+ $endRange = $this->request->getStringParam('end');
+ $startColumn = $this->configModel->get('calendar_project_tasks', 'date_started');
+
+ $dueDateOnlyEvents = $this->taskQuery
+ ->withFilter(new TaskAssigneeFilter($user_id))
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN))
+ ->withFilter(new TaskDueDateRangeFilter(array($startRange, $endRange)))
+ ->format($this->taskCalendarFormatter->setColumns('date_due'));
+
+ $startAndDueDateQueryBuilder = $this->taskQuery
+ ->withFilter(new TaskAssigneeFilter($user_id))
+ ->withFilter(new TaskStatusFilter(TaskModel::STATUS_OPEN));
+
+ $startAndDueDateQueryBuilder
+ ->getQuery()
+ ->addCondition($this->getConditionForTasksWithStartAndDueDate($startRange, $endRange, $startColumn, 'date_due'));
+
+ $startAndDueDateEvents = $startAndDueDateQueryBuilder
+ ->format($this->taskCalendarFormatter->setColumns($startColumn, 'date_due'));
+
+ $events = array_merge($dueDateOnlyEvents, $startAndDueDateEvents);
+
+ $events = $this->hook->merge('controller:calendar:user:events', $events, array(
+ 'user_id' => $user_id,
+ 'start' => $startRange,
+ 'end' => $endRange,
+ ));
+
+ $this->response->json($events);
+ }
+
+ public function save()
+ {
+ if ($this->request->isAjax() && $this->request->isPost()) {
+ $values = $this->request->getJson();
+
+ $this->taskModificationModel->update(array(
+ 'id' => $values['task_id'],
+ 'date_due' => substr($values['date_due'], 0, 10),
+ ));
+ }
+ }
+
+ protected function getConditionForTasksWithStartAndDueDate($startTime, $endTime, $startColumn, $endColumn)
+ {
+ $startTime = strtotime($startTime);
+ $endTime = strtotime($endTime);
+ $startColumn = $this->db->escapeIdentifier($startColumn);
+ $endColumn = $this->db->escapeIdentifier($endColumn);
+
+ $conditions = array(
+ "($startColumn >= '$startTime' AND $startColumn <= '$endTime')",
+ "($startColumn <= '$startTime' AND $endColumn >= '$startTime')",
+ "($startColumn <= '$startTime' AND ($endColumn = '0' OR $endColumn IS NULL))",
+ );
+
+ return $startColumn.' IS NOT NULL AND '.$startColumn.' > 0 AND ('.implode(' OR ', $conditions).')';
+ }
+}
diff --git a/plugins/Calendar/Controller/ConfigController.php b/plugins/Calendar/Controller/ConfigController.php
new file mode 100644
index 00000000..61c67f9d
--- /dev/null
+++ b/plugins/Calendar/Controller/ConfigController.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar\Controller;
+
+/**
+ * Class ConfigController
+ *
+ * @package Kanboard\Plugin\Calendar\Controller
+ */
+class ConfigController extends \Kanboard\Controller\ConfigController
+{
+ public function show()
+ {
+ $this->response->html($this->helper->layout->config('Calendar:config/calendar', array(
+ 'title' => t('Settings').' &gt; '.t('Calendar settings'),
+ )));
+ }
+
+ public function save()
+ {
+ $values = $this->request->getValues();
+
+ if ($this->configModel->save($values)) {
+ $this->flash->success(t('Settings saved successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to save your settings.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'Calendar')));
+ }
+}
diff --git a/plugins/Calendar/Formatter/ProjectApiFormatter.php b/plugins/Calendar/Formatter/ProjectApiFormatter.php
new file mode 100644
index 00000000..5520801c
--- /dev/null
+++ b/plugins/Calendar/Formatter/ProjectApiFormatter.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar\Formatter;
+
+/**
+ * Class ProjectApiFormatter
+ *
+ * @package Kanboard\Plugin\Calendar\Formatter
+ */
+class ProjectApiFormatter extends \Kanboard\Formatter\ProjectApiFormatter
+{
+ public function format()
+ {
+ $project = parent::format();
+
+ if (! empty($project)) {
+ $project['url']['calendar'] = $this->helper->url->to('CalendarController', 'project', array('project_id' => $project['id'], 'plugin' => 'Calendar'), '', true);
+ }
+
+ return $project;
+ }
+}
diff --git a/plugins/Calendar/Formatter/TaskCalendarFormatter.php b/plugins/Calendar/Formatter/TaskCalendarFormatter.php
new file mode 100644
index 00000000..9286eaf8
--- /dev/null
+++ b/plugins/Calendar/Formatter/TaskCalendarFormatter.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar\Formatter;
+
+use DateTime;
+use Kanboard\Core\Filter\FormatterInterface;
+use Kanboard\Formatter\BaseFormatter;
+
+/**
+ * Calendar event formatter for task filter
+ *
+ * @package Kanboard\Plugin\Calendar\Formatter
+ * @author Frederic Guillot
+ */
+class TaskCalendarFormatter extends BaseFormatter implements FormatterInterface
+{
+ /**
+ * Column used for event start date
+ *
+ * @access protected
+ * @var string
+ */
+ protected $startColumn = '';
+
+ /**
+ * Column used for event end date
+ *
+ * @access protected
+ * @var string
+ */
+ protected $endColumn = '';
+
+ /**
+ * Transform results to calendar events
+ *
+ * @access public
+ * @param string $start_column Column name for the start date
+ * @param string $end_column Column name for the end date
+ * @return $this
+ */
+ public function setColumns($start_column, $end_column = '')
+ {
+ $this->startColumn = $start_column;
+ $this->endColumn = $end_column ?: $start_column;
+ return $this;
+ }
+
+ /**
+ * Transform tasks to calendar events
+ *
+ * @access public
+ * @return array
+ */
+ public function format()
+ {
+ $events = array();
+
+ foreach ($this->query->findAll() as $task) {
+ $startDate = new DateTime();
+ $startDate->setTimestamp($task[$this->startColumn]);
+
+ $endDate = new DateTime();
+ if (! empty($task[$this->endColumn])) {
+ $endDate->setTimestamp($task[$this->endColumn]);
+ }
+
+ $allDay = $startDate == $endDate && $endDate->format('Hi') == '0000';
+ $format = $allDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+
+ $events[] = array(
+ 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(),
+ 'id' => $task['id'],
+ 'title' => t('#%d', $task['id']).' '.$task['title'],
+ 'backgroundColor' => $this->colorModel->getBackgroundColor($task['color_id']),
+ 'borderColor' => $this->colorModel->getBorderColor($task['color_id']),
+ 'textColor' => 'black',
+ 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ 'start' => $startDate->format($format),
+ 'end' => $endDate->format($format),
+ 'editable' => $allDay,
+ 'allday' => $allDay,
+ );
+ }
+
+ return $events;
+ }
+}
diff --git a/plugins/Calendar/Helper/CalendarHelper.php b/plugins/Calendar/Helper/CalendarHelper.php
new file mode 100644
index 00000000..5807b669
--- /dev/null
+++ b/plugins/Calendar/Helper/CalendarHelper.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar\Helper;
+
+use Kanboard\Core\Base;
+
+/**
+ * Calendar Helper
+ *
+ * @package Kanboard\Plugin\Calendar\Helper
+ * @author Frederic Guillot
+ */
+class CalendarHelper extends Base
+{
+ /**
+ * Render calendar component
+ *
+ * @param string $checkUrl
+ * @param string $saveUrl
+ * @return string
+ */
+ public function render($checkUrl, $saveUrl)
+ {
+ $params = array(
+ 'checkUrl' => $checkUrl,
+ 'saveUrl' => $saveUrl,
+ );
+
+ return '<div class="js-calendar" data-params=\''.json_encode($params, JSON_HEX_APOS).'\'></div>';
+ }
+}
diff --git a/plugins/Calendar/LICENSE b/plugins/Calendar/LICENSE
new file mode 100644
index 00000000..a19d63a3
--- /dev/null
+++ b/plugins/Calendar/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Frédéric Guillot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/plugins/Calendar/Locale/bs_BA/translations.php b/plugins/Calendar/Locale/bs_BA/translations.php
new file mode 100644
index 00000000..fd70b62b
--- /dev/null
+++ b/plugins/Calendar/Locale/bs_BA/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalendar',
+ 'Calendar settings' => 'Postavke kalendara',
+ 'Project calendar view' => 'Pregled kalendara projekta',
+ 'User calendar view' => 'Pregled korisničkog kalendara',
+ 'My calendar' => 'Moj kalendar',
+ 'Switch to the list view' => 'Promijeni da vidim listu',
+ 'Show tasks based on the start date' => 'Prikaži zadatke bazirano na vremenu početka rada',
+ 'Subtasks time tracking' => 'Vremensko praćenje pod-zadataka',
+);
+
diff --git a/plugins/Calendar/Locale/cs_CZ/translations.php b/plugins/Calendar/Locale/cs_CZ/translations.php
new file mode 100644
index 00000000..cd1d3384
--- /dev/null
+++ b/plugins/Calendar/Locale/cs_CZ/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalendář',
+ 'Calendar settings' => 'Nastavení kalendáře',
+ // 'Project calendar view' => '',
+ 'User calendar view' => 'Zobrazení kalendáře uživatele',
+ 'My calendar' => 'Můj kalendář',
+ 'Switch to the list view' => 'Přepnout na seznam zobrazení',
+ 'Show tasks based on the start date' => 'Zobrazit úkoly podle datumu zahájení',
+ 'Subtasks time tracking' => 'Dílčí úkoly s časovačem',
+);
+
diff --git a/plugins/Calendar/Locale/da_DK/translations.php b/plugins/Calendar/Locale/da_DK/translations.php
new file mode 100644
index 00000000..e67029b3
--- /dev/null
+++ b/plugins/Calendar/Locale/da_DK/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Kalender indstillinger',
+ 'Project calendar view' => 'Projekt kalender visning',
+ 'User calendar view' => 'Bruger kalender visning',
+ 'My calendar' => 'Min kalender',
+ 'Switch to the list view' => 'Skift til liste visning',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/de_DE/translations.php b/plugins/Calendar/Locale/de_DE/translations.php
new file mode 100644
index 00000000..01f1f740
--- /dev/null
+++ b/plugins/Calendar/Locale/de_DE/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Kalender-Einstellungen',
+ 'Project calendar view' => 'Projekt-Kalendarsicht',
+ 'User calendar view' => 'Benutzer-Kalendersicht',
+ 'My calendar' => 'Mein Kalender',
+ 'Switch to the list view' => 'Zur Listen-Ansicht',
+ 'Show tasks based on the start date' => 'Zeige Aufgaben basierend auf Beginndatum',
+ 'Subtasks time tracking' => 'Teilaufgaben-Zeiterfassung',
+);
+
diff --git a/plugins/Calendar/Locale/el_GR/translations.php b/plugins/Calendar/Locale/el_GR/translations.php
new file mode 100644
index 00000000..853173af
--- /dev/null
+++ b/plugins/Calendar/Locale/el_GR/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Ημερολόγιο',
+ 'Calendar settings' => 'Ρυθμίσεις ημερολογίου',
+ 'Project calendar view' => 'Προβολή ημερολογίου έργων',
+ 'User calendar view' => 'Προβολή του ημερολογίου του χρήστη',
+ 'My calendar' => 'Το ημερολόγιο μου',
+ 'Switch to the list view' => 'Εναλλαγή στην προβολή λίστας',
+ 'Show tasks based on the start date' => 'Εμφάνιση έργων με βάση την ημερομηνία δημιουργίας ',
+ 'Subtasks time tracking' => 'Παρακολούθηση χρόνου υπο-εργασίων',
+);
+
diff --git a/plugins/Calendar/Locale/es_ES/translations.php b/plugins/Calendar/Locale/es_ES/translations.php
new file mode 100644
index 00000000..6ca2c631
--- /dev/null
+++ b/plugins/Calendar/Locale/es_ES/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Calendario',
+ // 'Calendar settings' => '',
+ // 'Project calendar view' => '',
+ // 'User calendar view' => '',
+ // 'My calendar' => '',
+ // 'Switch to the list view' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/fi_FI/translations.php b/plugins/Calendar/Locale/fi_FI/translations.php
new file mode 100644
index 00000000..ce55bbc6
--- /dev/null
+++ b/plugins/Calendar/Locale/fi_FI/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ // 'Calendar' => '',
+ // 'Calendar settings' => '',
+ // 'Project calendar view' => '',
+ // 'User calendar view' => '',
+ // 'My calendar' => '',
+ // 'Switch to the list view' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/fr_FR/translations.php b/plugins/Calendar/Locale/fr_FR/translations.php
new file mode 100644
index 00000000..66820ff2
--- /dev/null
+++ b/plugins/Calendar/Locale/fr_FR/translations.php
@@ -0,0 +1,14 @@
+<?php
+
+return array(
+ 'Calendar' => 'Agenda',
+ 'Calendar settings' => 'Paramètres du calendrier',
+ 'Project calendar view' => 'Vue en mode projet du calendrier',
+ 'User calendar view' => 'Vue en mode utilisateur du calendrier',
+ 'My calendar' => 'Mon agenda',
+ 'Switch to the list view' => 'Basculer vers la vue en liste',
+ 'Show subtasks based on the time tracking' => 'Afficher les sous-tâches basé sur le suivi du temps',
+ 'Show tasks based on the creation date' => 'Afficher les tâches en fonction de la date de création',
+ 'Show tasks based on the start date' => 'Afficher les tâches en fonction de la date de début',
+ 'Subtasks time tracking' => 'Suivi du temps par rapport aux sous-tâches',
+);
diff --git a/plugins/Calendar/Locale/hr_HR/translations.php b/plugins/Calendar/Locale/hr_HR/translations.php
new file mode 100644
index 00000000..cf769358
--- /dev/null
+++ b/plugins/Calendar/Locale/hr_HR/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalendar',
+ 'Calendar settings' => 'Postavke kalendara',
+ 'Project calendar view' => 'Projektni kalendar',
+ 'User calendar view' => 'Korisnički kalendar',
+ 'My calendar' => 'Moj kalendar',
+ // 'Switch to the list view' => '',
+ 'Show tasks based on the start date' => 'Prikaz zadataka po datumu početka',
+ 'Subtasks time tracking' => 'Evidencija vremena pod-zadataka',
+);
+
diff --git a/plugins/Calendar/Locale/hu_HU/translations.php b/plugins/Calendar/Locale/hu_HU/translations.php
new file mode 100644
index 00000000..2cea0be0
--- /dev/null
+++ b/plugins/Calendar/Locale/hu_HU/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Naptár',
+ 'Calendar settings' => 'Naptár beállítások',
+ 'Project calendar view' => 'A projekt megjelenítése naptári formában',
+ 'User calendar view' => 'A felhasználó naptárának megjelenítése',
+ 'My calendar' => 'Naptáram',
+ 'Switch to the list view' => 'Átkapcsolás lista nézetbe',
+ 'Show tasks based on the start date' => 'A feladatok megjelenítése a kezdő dátum alapján',
+ 'Subtasks time tracking' => 'A részfeladatok idejének megjelenítése',
+);
+
diff --git a/plugins/Calendar/Locale/id_ID/translations.php b/plugins/Calendar/Locale/id_ID/translations.php
new file mode 100644
index 00000000..7c7b3a4a
--- /dev/null
+++ b/plugins/Calendar/Locale/id_ID/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Pengaturan kalender',
+ 'Project calendar view' => 'Tampilan kalender proyek',
+ 'User calendar view' => 'Tampilan kalender pengguna',
+ 'My calendar' => 'Kalender saya',
+ 'Switch to the list view' => 'Beralih ke tampilan daftar',
+ 'Show tasks based on the start date' => 'Tampilkan tugas berdasarkan tanggal mulai',
+ 'Subtasks time tracking' => 'Pelacakan waktu sub-tugas',
+);
+
diff --git a/plugins/Calendar/Locale/it_IT/translations.php b/plugins/Calendar/Locale/it_IT/translations.php
new file mode 100644
index 00000000..378e8b98
--- /dev/null
+++ b/plugins/Calendar/Locale/it_IT/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Calendario',
+ 'Calendar settings' => 'Impostazioni del calendario',
+ 'Project calendar view' => 'Vista di progetto a calendario',
+ 'User calendar view' => 'Vista utente a calendario',
+ 'My calendar' => 'Il mio calendario',
+ 'Switch to the list view' => 'Passa alla vista "elenco"',
+ 'Show tasks based on the start date' => 'Mostra i task in base alla data di inizio',
+ 'Subtasks time tracking' => 'Time tracking per i sotto-task',
+);
+
diff --git a/plugins/Calendar/Locale/ja_JP/translations.php b/plugins/Calendar/Locale/ja_JP/translations.php
new file mode 100644
index 00000000..4231ad6d
--- /dev/null
+++ b/plugins/Calendar/Locale/ja_JP/translations.php
@@ -0,0 +1,15 @@
+<?php
+
+return array(
+ 'Calendar' => 'カレンダー',
+ 'Calendar settings' => 'カレンダー設定',
+ 'Project calendar view' => 'プロジェクトカレンダー・ビュー',
+ 'User calendar view' => 'ユーザーカレンダー・ビュー',
+ 'My calendar' => '自分のカレンダー',
+ 'Switch to the list view' => 'リストビューに切り替える',
+ 'Show tasks based on the start date' => '開始日に基づいてタスクを表示',
+ 'Subtasks time tracking' => 'サブタスクの時間トラッキング',
+ 'Show tasks based on the creation date' => '作成日に基づいてタスクを表示',
+ 'Show subtasks based on the time tracking' => '時間トラッキングに基づいてサブタスクを表示',
+);
+
diff --git a/plugins/Calendar/Locale/ko_KR/translations.php b/plugins/Calendar/Locale/ko_KR/translations.php
new file mode 100644
index 00000000..b641faad
--- /dev/null
+++ b/plugins/Calendar/Locale/ko_KR/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => '달력',
+ 'Calendar settings' => '달력 설정',
+ 'Project calendar view' => '프로젝트 달력 보기',
+ 'User calendar view' => '담당자 달력 보기',
+ 'My calendar' => '내 캘린더',
+ 'Switch to the list view' => '리스트 보기로 전환',
+ 'Show tasks based on the start date' => '시작 날짜로 할일 보기',
+ 'Subtasks time tracking' => '서브 할일 시간 트래킹',
+);
+
diff --git a/plugins/Calendar/Locale/my_MY/translations.php b/plugins/Calendar/Locale/my_MY/translations.php
new file mode 100644
index 00000000..30b8f8b6
--- /dev/null
+++ b/plugins/Calendar/Locale/my_MY/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Pengaturan kalender',
+ 'Project calendar view' => 'Tampilan kalender projek',
+ 'User calendar view' => 'Pengguna tampilan kalender',
+ 'My calendar' => 'Kalender saya',
+ 'Switch to the list view' => 'Beralih ke tampilan daftar',
+ 'Show tasks based on the start date' => 'Tampilkan tugas berdasarkan tanggal mulai',
+ 'Subtasks time tracking' => 'Pelacakan waktu subtgas',
+);
+
diff --git a/plugins/Calendar/Locale/nb_NO/translations.php b/plugins/Calendar/Locale/nb_NO/translations.php
new file mode 100644
index 00000000..cde26f35
--- /dev/null
+++ b/plugins/Calendar/Locale/nb_NO/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Kalenderinstillinger',
+ 'Project calendar view' => 'Visning prosjektkalender',
+ // 'User calendar view' => '',
+ 'My calendar' => 'Min kalender',
+ 'Switch to the list view' => 'Listevisning',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/nl_NL/translations.php b/plugins/Calendar/Locale/nl_NL/translations.php
new file mode 100644
index 00000000..5a102c66
--- /dev/null
+++ b/plugins/Calendar/Locale/nl_NL/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Agenda',
+ 'Calendar settings' => 'Kalender instellingen',
+ // 'Project calendar view' => '',
+ // 'User calendar view' => '',
+ 'My calendar' => 'Mijn kalender',
+ // 'Switch to the list view' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/pl_PL/translations.php b/plugins/Calendar/Locale/pl_PL/translations.php
new file mode 100644
index 00000000..dfdc7a73
--- /dev/null
+++ b/plugins/Calendar/Locale/pl_PL/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalendarz',
+ 'Calendar settings' => 'Ustawienia kalendarza',
+ 'Project calendar view' => 'Widok kalendarza projektu',
+ 'User calendar view' => 'Widok kalendarza użytkownika',
+ 'My calendar' => 'Mój kalendarz',
+ 'Switch to the list view' => 'Przełącz na listę',
+ 'Show tasks based on the start date' => 'Pokaż zadania względem daty rozpoczęcia',
+ 'Subtasks time tracking' => 'Śledzenie czasu pod-zadań',
+);
+
diff --git a/plugins/Calendar/Locale/pt_BR/translations.php b/plugins/Calendar/Locale/pt_BR/translations.php
new file mode 100644
index 00000000..b0aa26b6
--- /dev/null
+++ b/plugins/Calendar/Locale/pt_BR/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Calendário',
+ 'Calendar settings' => 'Configurações do calendário',
+ 'Project calendar view' => 'Vista em modo projeto do calendário',
+ 'User calendar view' => 'Vista em modo utilizador do calendário',
+ 'My calendar' => 'Minha agenda',
+ 'Switch to the list view' => 'Mudar par o modo Lista',
+ 'Show tasks based on the start date' => 'Mostrar as tarefas em função da data de início',
+ 'Subtasks time tracking' => 'Monitoramento do tempo comparado as subtarefas',
+);
+
diff --git a/plugins/Calendar/Locale/pt_PT/translations.php b/plugins/Calendar/Locale/pt_PT/translations.php
new file mode 100644
index 00000000..234f7533
--- /dev/null
+++ b/plugins/Calendar/Locale/pt_PT/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Calendário',
+ 'Calendar settings' => 'Configurações do calendário',
+ 'Project calendar view' => 'Vista em modo projeto do calendário',
+ 'User calendar view' => 'Vista em modo utilizador do calendário',
+ 'My calendar' => 'A minha agenda',
+ 'Switch to the list view' => 'Mudar para o modo Lista',
+ 'Show tasks based on the start date' => 'Mostrar as tarefas em função da data de início',
+ 'Subtasks time tracking' => 'Monitoramento do tempo comparado as subtarefas',
+);
+
diff --git a/plugins/Calendar/Locale/ru_RU/translations.php b/plugins/Calendar/Locale/ru_RU/translations.php
new file mode 100644
index 00000000..5b3bb485
--- /dev/null
+++ b/plugins/Calendar/Locale/ru_RU/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Календарь',
+ 'Calendar settings' => 'Настройки календаря',
+ 'Project calendar view' => 'Вид календаря проекта',
+ 'User calendar view' => 'Просмотреть календарь пользователя',
+ 'My calendar' => 'Мой календарь',
+ 'Switch to the list view' => 'Переключиться в режим списка',
+ 'Show tasks based on the start date' => 'Показать задачи в зависимости от даты начала',
+ 'Subtasks time tracking' => 'Отслеживание времени подзадач',
+);
+
diff --git a/plugins/Calendar/Locale/sr_Latn_RS/translations.php b/plugins/Calendar/Locale/sr_Latn_RS/translations.php
new file mode 100644
index 00000000..3bf5e2a2
--- /dev/null
+++ b/plugins/Calendar/Locale/sr_Latn_RS/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalendar',
+ // 'Calendar settings' => '',
+ // 'Project calendar view' => '',
+ // 'User calendar view' => '',
+ // 'My calendar' => '',
+ // 'Switch to the list view' => '',
+ // 'Show tasks based on the start date' => '',
+ // 'Subtasks time tracking' => '',
+);
+
diff --git a/plugins/Calendar/Locale/sv_SE/translations.php b/plugins/Calendar/Locale/sv_SE/translations.php
new file mode 100644
index 00000000..7d73a429
--- /dev/null
+++ b/plugins/Calendar/Locale/sv_SE/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Kalender',
+ 'Calendar settings' => 'Inställningar för kalendern',
+ 'Project calendar view' => 'Projektkalendervy',
+ 'User calendar view' => 'Användarkalendervy',
+ 'My calendar' => 'Min kalender',
+ 'Switch to the list view' => 'Växla till listvy',
+ 'Show tasks based on the start date' => 'Visa uppgifter baserade på startdatum',
+ 'Subtasks time tracking' => 'Deluppgifter tidsspårning',
+);
+
diff --git a/plugins/Calendar/Locale/th_TH/translations.php b/plugins/Calendar/Locale/th_TH/translations.php
new file mode 100644
index 00000000..72efa951
--- /dev/null
+++ b/plugins/Calendar/Locale/th_TH/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'ปฏิทิน',
+ 'Calendar settings' => 'ตั้งค่าปฏิทิน',
+ 'Project calendar view' => 'มุมมองปฏิทินของโปรเจค',
+ 'User calendar view' => 'มุมมองปฏิทินของผู้ใช้',
+ 'My calendar' => 'ปฎิทินของฉัน',
+ 'Switch to the list view' => 'เปลี่ยนเป็นมุมมองลิสต์',
+ 'Show tasks based on the start date' => 'แสดงงานจากวันที่เริ่ม',
+ 'Subtasks time tracking' => 'การติดตามเวลางานย่อย',
+);
+
diff --git a/plugins/Calendar/Locale/tr_TR/translations.php b/plugins/Calendar/Locale/tr_TR/translations.php
new file mode 100644
index 00000000..7879802c
--- /dev/null
+++ b/plugins/Calendar/Locale/tr_TR/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => 'Takvim',
+ 'Calendar settings' => 'Takvim ayarları',
+ 'Project calendar view' => 'Proje takvim görünümü',
+ 'User calendar view' => 'Kullanıcı takvim görünümü',
+ 'My calendar' => 'Takvimim',
+ 'Switch to the list view' => 'Liste görünümüne geç',
+ 'Show tasks based on the start date' => 'Başlangıç zamanına göre görevleri göster',
+ 'Subtasks time tracking' => 'Alt görevler zaman takibi',
+);
+
diff --git a/plugins/Calendar/Locale/zh_CN/translations.php b/plugins/Calendar/Locale/zh_CN/translations.php
new file mode 100644
index 00000000..66be2aae
--- /dev/null
+++ b/plugins/Calendar/Locale/zh_CN/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Calendar' => '日程表',
+ 'Calendar settings' => '日程设置',
+ 'Project calendar view' => '项目日历表',
+ 'User calendar view' => '用户日程视图',
+ 'My calendar' => '我的日程表',
+ 'Switch to the list view' => '切换到列表视图',
+ 'Show tasks based on the start date' => '显示任务开始日期于',
+ 'Subtasks time tracking' => '子任务时间跟踪',
+);
+
diff --git a/plugins/Calendar/Makefile b/plugins/Calendar/Makefile
new file mode 100644
index 00000000..85fbf2c0
--- /dev/null
+++ b/plugins/Calendar/Makefile
@@ -0,0 +1,5 @@
+plugin=Calendar
+
+all:
+ @ echo "Build archive for plugin ${plugin} version=${version}"
+ @ git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip
diff --git a/plugins/Calendar/Plugin.php b/plugins/Calendar/Plugin.php
new file mode 100644
index 00000000..acc4f193
--- /dev/null
+++ b/plugins/Calendar/Plugin.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Kanboard\Plugin\Calendar;
+
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Plugin\Calendar\Formatter\ProjectApiFormatter;
+use Kanboard\Plugin\Calendar\Formatter\TaskCalendarFormatter;
+
+class Plugin extends Base
+{
+ public function initialize()
+ {
+ $this->helper->register('calendar', '\Kanboard\Plugin\Calendar\Helper\CalendarHelper');
+
+ $this->container['taskCalendarFormatter'] = $this->container->factory(function ($c) {
+ return new TaskCalendarFormatter($c);
+ });
+
+ $this->container['projectApiFormatter'] = $this->container->factory(function ($c) {
+ return new ProjectApiFormatter($c);
+ });
+
+ $this->template->hook->attach('template:dashboard:page-header:menu', 'Calendar:dashboard/menu');
+ $this->template->hook->attach('template:project:dropdown', 'Calendar:project/dropdown');
+ $this->template->hook->attach('template:project-header:view-switcher', 'Calendar:project_header/views');
+ $this->template->hook->attach('template:config:sidebar', 'Calendar:config/sidebar');
+
+ $this->hook->on('template:layout:css', array('template' => 'plugins/Calendar/Assets/fullcalendar.min.css'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Calendar/Assets/moment.min.js'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Calendar/Assets/fullcalendar.min.js'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Calendar/Assets/locale-all.js'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Calendar/Assets/calendar.js'));
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ }
+
+ public function getPluginName()
+ {
+ return 'Calendar';
+ }
+
+ public function getPluginDescription()
+ {
+ return t('Calendar view for Kanboard');
+ }
+
+ public function getPluginAuthor()
+ {
+ return 'Frédéric Guillot';
+ }
+
+ public function getPluginVersion()
+ {
+ return '1.1.0';
+ }
+
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/kanboard/plugin-calendar';
+ }
+
+ public function getCompatibleVersion()
+ {
+ return '>=1.0.44';
+ }
+}
+
diff --git a/plugins/Calendar/README.md b/plugins/Calendar/README.md
new file mode 100644
index 00000000..2bc9d16a
--- /dev/null
+++ b/plugins/Calendar/README.md
@@ -0,0 +1,27 @@
+Calendar Plugin
+===============
+
+Embedded calendar view for Kanboard.
+
+Author
+------
+
+- Frédéric Guillot
+- License MIT
+
+Requirements
+------------
+
+- Kanboard >= 1.0.44
+
+Installation
+------------
+
+You have the choice between 3 methods:
+
+1. Install the plugin from the Kanboard plugin manager in one click
+2. Download the zip file and decompress everything under the directory `plugins/Calendar`
+3. Clone this repository into the folder `plugins/Calendar`
+
+Note: Plugin folder is case-sensitive.
+
diff --git a/plugins/Calendar/Template/calendar/project.php b/plugins/Calendar/Template/calendar/project.php
new file mode 100644
index 00000000..f1a89d23
--- /dev/null
+++ b/plugins/Calendar/Template/calendar/project.php
@@ -0,0 +1,6 @@
+<?= $this->projectHeader->render($project, 'CalendarController', 'project', false, 'Calendar') ?>
+
+<?= $this->calendar->render(
+ $this->url->href('CalendarController', 'projectEvents', array('project_id' => $project['id'], 'plugin' => 'Calendar')),
+ $this->url->href('CalendarController', 'save', array('project_id' => $project['id'], 'plugin' => 'Calendar'))
+) ?>
diff --git a/plugins/Calendar/Template/calendar/user.php b/plugins/Calendar/Template/calendar/user.php
new file mode 100644
index 00000000..74f00cc9
--- /dev/null
+++ b/plugins/Calendar/Template/calendar/user.php
@@ -0,0 +1,4 @@
+<?= $this->calendar->render(
+ $this->url->href('CalendarController', 'userEvents', array('user_id' => $user['id'], 'plugin' => 'Calendar')),
+ $this->url->href('CalendarController', 'save', array('plugin' => 'Calendar'))
+) ?>
diff --git a/plugins/Calendar/Template/config/calendar.php b/plugins/Calendar/Template/config/calendar.php
new file mode 100644
index 00000000..2221294b
--- /dev/null
+++ b/plugins/Calendar/Template/config/calendar.php
@@ -0,0 +1,31 @@
+<div class="page-header">
+ <h2><?= t('Calendar settings') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('ConfigController', 'save', array('plugin' => 'Calendar')) ?>" autocomplete="off">
+
+ <?= $this->form->csrf() ?>
+
+ <fieldset>
+ <legend><?= t('Project calendar view') ?></legend>
+ <?= $this->form->radios('calendar_project_tasks', array(
+ 'date_creation' => t('Show tasks based on the creation date'),
+ 'date_started' => t('Show tasks based on the start date'),
+ ),
+ $values
+ ) ?>
+ </fieldset>
+
+ <fieldset>
+ <legend><?= t('User calendar view') ?></legend>
+ <?= $this->form->radios('calendar_user_tasks', array(
+ 'date_creation' => t('Show tasks based on the creation date'),
+ 'date_started' => t('Show tasks based on the start date'),
+ ),
+ $values
+ ) ?>
+ </fieldset>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ </div>
+</form>
diff --git a/plugins/Calendar/Template/config/sidebar.php b/plugins/Calendar/Template/config/sidebar.php
new file mode 100644
index 00000000..040f4807
--- /dev/null
+++ b/plugins/Calendar/Template/config/sidebar.php
@@ -0,0 +1,3 @@
+<li <?= $this->app->checkMenuSelection('ConfigController', 'show', 'Calendar') ?>>
+ <?= $this->url->link(t('Calendar settings'), 'ConfigController', 'show', array('plugin' => 'Calendar')) ?>
+</li> \ No newline at end of file
diff --git a/plugins/Calendar/Template/dashboard/menu.php b/plugins/Calendar/Template/dashboard/menu.php
new file mode 100644
index 00000000..5558cc87
--- /dev/null
+++ b/plugins/Calendar/Template/dashboard/menu.php
@@ -0,0 +1,3 @@
+<li>
+ <?= $this->modal->medium('calendar', t('My calendar'), 'CalendarController', 'user', array('plugin' => 'Calendar')) ?>
+</li>
diff --git a/plugins/Calendar/Template/project/dropdown.php b/plugins/Calendar/Template/project/dropdown.php
new file mode 100644
index 00000000..539f72ef
--- /dev/null
+++ b/plugins/Calendar/Template/project/dropdown.php
@@ -0,0 +1,3 @@
+<li>
+ <?= $this->url->icon('calendar', t('Calendar'), 'CalendarController', 'project', array('project_id' => $project['id'], 'plugin' => 'Calendar')) ?>
+</li> \ No newline at end of file
diff --git a/plugins/Calendar/Template/project_header/views.php b/plugins/Calendar/Template/project_header/views.php
new file mode 100644
index 00000000..01c246e1
--- /dev/null
+++ b/plugins/Calendar/Template/project_header/views.php
@@ -0,0 +1,3 @@
+<li <?= $this->app->checkMenuSelection('CalendarController') ?>>
+ <?= $this->url->icon('calendar', t('Calendar'), 'CalendarController', 'project', array('project_id' => $project['id'], 'search' => $filters['search'], 'plugin' => 'Calendar'), false, 'view-calendar', t('Keyboard shortcut: "%s"', 'v c')) ?>
+</li> \ No newline at end of file
diff --git a/plugins/Calendar/Test/PluginTest.php b/plugins/Calendar/Test/PluginTest.php
new file mode 100644
index 00000000..c6dd3fc8
--- /dev/null
+++ b/plugins/Calendar/Test/PluginTest.php
@@ -0,0 +1,20 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Plugin\Calendar\Plugin;
+
+class PluginTest extends Base
+{
+ public function testPlugin()
+ {
+ $plugin = new Plugin($this->container);
+ $this->assertSame(null, $plugin->initialize());
+ $this->assertSame(null, $plugin->onStartup());
+ $this->assertNotEmpty($plugin->getPluginName());
+ $this->assertNotEmpty($plugin->getPluginDescription());
+ $this->assertNotEmpty($plugin->getPluginAuthor());
+ $this->assertNotEmpty($plugin->getPluginVersion());
+ $this->assertNotEmpty($plugin->getPluginHomepage());
+ }
+}
diff --git a/plugins/Customizer/Assets/css/README.md b/plugins/Customizer/Assets/css/README.md
new file mode 100644
index 00000000..7b7416b3
--- /dev/null
+++ b/plugins/Customizer/Assets/css/README.md
@@ -0,0 +1,39 @@
+
+Preset Themes that come with Customizer
+--------
+
+<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Clemson_Tigers_logo.svg/2000px-Clemson_Tigers_logo.svg.png" height="20"> **Clemson Theme (Go Tigers!):**
+
+![image](https://user-images.githubusercontent.com/26339368/48094361-fccc3c80-e1df-11e8-9695-6b9c510aa522.png)
+
+:octocat: Github:
+
+![image](https://user-images.githubusercontent.com/26339368/47761386-8636b880-dc8e-11e8-9b6e-c46e7b5dcc44.png)
+
+:milky_way: Galaxy:
+
+![image](https://user-images.githubusercontent.com/26339368/47761350-68695380-dc8e-11e8-9e87-a9471e5e1adf.png)
+
+
+:partly_sunny: Breathe:
+
+![image](https://user-images.githubusercontent.com/26339368/47761312-47086780-dc8e-11e8-9460-5b1ce4b54d5e.png)
+
+:blue_book: Blueboard
+
+![image](https://user-images.githubusercontent.com/26339368/49310748-29a50400-f4ad-11e8-9734-eca2e5a558fc.png)
+
+:sparkles: Material
+
+![image](https://user-images.githubusercontent.com/26339368/49310723-1003bc80-f4ad-11e8-8c03-8390ecc78d20.png)
+
+:8ball: KindaDark
+
+<img src="https://i.imgur.com/Rg9RYwf.png" />
+<br/>
+
+
+
+## How to add your own css
+
+To add your own custom css files to Customizer, simply place your files in the `DATA_DIR . '/files/customizer/themes` folder. If that folder doesn't exist, create it, and then place your files in there. This will retain your custom themes through upgrades of both Kanboard or Customizer.
diff --git a/plugins/Customizer/Assets/css/customizer.css b/plugins/Customizer/Assets/css/customizer.css
new file mode 100644
index 00000000..9423a4d5
--- /dev/null
+++ b/plugins/Customizer/Assets/css/customizer.css
@@ -0,0 +1,316 @@
+/* KANBOARD PLUGIN - CUSTOMIZER CSS FILE */
+
+/*------ PAGE HEADER ------*/
+.logo > a > img {vertical-align:middle;} /* This aligns the logo certically to the text */
+
+.logo a {
+ opacity:1.0;
+}
+
+/*------ LOGIN PAGE-SPECIFIC STYLES MOVED TO logintop.php SO THEY WON'T AFFECT OTHER PARTS OF KANBOARD ------*/
+
+
+/*------ PLUGIN SETTINGS PAGE ------*/
+.form-help {margin-top: 5px;} /* This gives line spacing above the help text */
+
+code {
+ margin: 0;
+ /*padding: 2px 0.4em;*/
+ background-color: rgba(27, 31, 35, 0.32);
+ border-radius: 3px;
+ color: #FFF;
+ font-family: Tahoma;
+ font-size: 95%!important;
+ letter-spacing: 1px;
+} /* This styles the url link examples */
+
+.login-link-block > label {
+ font-weight: bold;
+ margin-bottom: 5px;
+} /* This is to highlight the title fields */
+
+.panel-heading {
+ margin: -2px -25px;
+ float: right;
+ font-size: 1.2em;
+} /* This adds a heading area to each section */
+
+.panel-title {
+ margin-top: 0;
+ font-weight: bold;
+} /* This is the title text for the heading area */
+
+.links-title {
+ margin: -2px 25px;
+} /* This is the title text for the links heading area, styled uniquely as the type is fieldset */
+
+.upload-link {
+ float: left;
+ list-style: outside none none;
+ width: auto;
+ display: block;
+ margin: 0px 10px;
+} /* This places the upload link on the same line as the remove link */
+
+.remove-link {
+ float: left;
+ list-style: outside none none;
+ width: auto;
+ display: block;
+ margin: 0px 10px;
+} /* This places the upload link on the same line as the remove link */
+
+.upload-link > a > i {
+ color: green;
+} /* This colours the upload icon to green */
+
+.remove-link > a > i {
+ color: red;
+} /* This colours the upload icon to red */
+
+/* Style the buttons that are used to open and close the accordion panel */
+.login-accordion {
+ font-weight: bold;
+ background-color: rgba(136,136,136,0.7);
+ cursor: pointer;
+ padding: 18px;
+ width: 100%;
+ text-align: left;
+ border: none;
+ outline: none;
+ transition: 0.4s;
+ color: #f5f5f5;
+ border-radius: 4px;
+ margin-bottom: 5px;
+ box-shadow: 0 1px 3px 0px rgba(70, 70, 70, 0.10);
+}
+
+/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
+.login-accordion:hover {
+ background-color: #f5f5f5;
+ color: #777777;
+ transition: 0.4s;
+}
+
+.current {
+ background-color: #f5f5f5;
+ color: #777777;
+ transition: 0.4s;
+}
+
+/* Style the accordion panel. Note: hidden by default */
+.login-accordian-panel {
+ max-height: 0;
+ overflow: hidden;
+ transition: 0.4s ease-in-out;
+ opacity: 0;
+}
+
+div.login-accordian-panel.show {
+ opacity: 1;
+ max-height: 5000px;
+}
+
+.title-creator {
+ border-left: 5px solid #333;
+ padding-left: 8px;
+}
+
+/*
+* Short classes
+* m - for classes that set margin
+* p - for classes that set padding
+* t - for classes that set margin-top or padding-top
+* b - for classes that set margin-bottom or padding-bottom
+* l - for classes that set margin-left or padding-left
+* r - for classes that set margin-right or padding-right
+* number(5) - for classes that set the margin or padding
+*/
+
+.mt-5 {
+ margin-top: 5px;
+}
+
+.mt-10 {
+ margin-top: 10px;
+}
+
+.mt-15 {
+ margin-top: 15px;
+}
+
+.mt-20 {
+ margin-top: 20px;
+}
+
+.mb-5 {
+ margin-bottom: 5px;
+}
+
+.mb-10 {
+ margin-bottom: 10px;
+}
+
+.mb-15 {
+ margin-bottom: 15px;
+}
+
+.mb-20 {
+ margin-bottom: 20px;
+}
+
+.ml-5 {
+ margin-left: 5px;
+}
+
+.ml-10 {
+ margin-left: 10px;
+}
+
+.ml-15 {
+ margin-left: 15px;
+}
+
+.ml-20 {
+ margin-left: 20px;
+}
+
+.mr-5 {
+ margin-right: 5px;
+}
+
+.mr-10 {
+ margin-right: 10px;
+}
+
+.mr-15 {
+ margin-right: 15px;
+}
+
+.mr-20 {
+ margin-right: 20px;
+}
+
+.pt-5 {
+ padding-top: 5px;
+}
+
+.pt-10 {
+ padding-top: 10px;
+}
+
+.pt-15 {
+ padding-top: 15px;
+}
+
+.pt-20 {
+ padding-top: 20px;
+}
+
+.pb-5 {
+ padding-bottom: 5px;
+}
+
+.pb-10 {
+ padding-bottom: 10px;
+}
+
+.pb-15 {
+ padding-bottom: 15px;
+}
+
+.pb-20 {
+ padding-bottom: 20px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.pl-10 {
+ padding-left: 10px;
+}
+
+.pl-15 {
+ padding-left: 15px;
+}
+
+.pl-20 {
+ padding-left: 20px;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pr-10 {
+ padding-right: 10px;
+}
+
+.pr-15 {
+ padding-right: 15px;
+}
+
+.pr-20 {
+ padding-right: 20px;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+}
+
+input:checked + .slider {
+ background-color: #2196F3;
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 1px #2196F3;
+}
+
+input:checked + .slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: 34px;
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
diff --git a/plugins/Customizer/Assets/css/theme.css b/plugins/Customizer/Assets/css/theme.css
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/plugins/Customizer/Assets/css/theme.css
@@ -0,0 +1 @@
+
diff --git a/plugins/Customizer/Assets/css/themes/Blueboard.css b/plugins/Customizer/Assets/css/themes/Blueboard.css
new file mode 100644
index 00000000..1fa8097b
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Blueboard.css
@@ -0,0 +1,265 @@
+/*!
+ * Blueboard by bgibout - Theme for Kanboard
+ * Licensed under the MIT license - blueboard/LICENSE
+ * https://github.com/bgibout/blueboard
+ * Copyright (c) 2017 bgibout
+ */
+
+body {
+ color: #868ba1;
+ background-color: #e9ecef;
+ font-family: "Roboto", "Helvetica Neue", Arial, sans-s
+}
+
+.page {
+ margin: 0;
+}
+
+header {
+ margin: 0 !important;
+ border: 0 !important;
+ background-color : #3B6998 !important;
+}
+
+.action-menu {
+ padding: 10px;
+}
+
+.views {
+ line-height: 30px;
+}
+
+.filter-box-component {
+ padding-top: 5px;
+}
+
+.project-header .dropdown-component {
+ margin-top: 4px;
+ margin-right: 5px;
+ float: left;
+ line-height: 30px;
+}
+
+header h1 {
+ color: #ffffff;
+ font-weight: 600;
+ font-size: 1.1rem;
+ line-height: 2rem
+}
+
+.tooltip i.fa {
+ color: #fff;
+}
+
+.btn {
+ text-decoration: none;
+ border-radius: 0;
+ min-height: 30px;
+ line-height: 30px;
+ color: #fff;
+ background-color: #3B6998;
+ border-color: #386491;
+ font-size: 12px;
+ text-align: center;
+ padding-left: 15px;
+ padding-right: 15px
+}
+
+label {
+ padding: 0px 40px 0 0px;
+ line-height: 25px;
+ vertical-align: middle;
+ font-size: .9rem;
+}
+
+/*
+ form {
+ margin-top: 40px;
+}
+*/
+
+fieldset {
+ background: #ffffff;
+}
+
+select, span.select2 {
+ width: 95% !important;
+}
+
+input[type="number"], input[type="date"], input[type="email"], input[type="password"], input[type="text"]:not(.input-addon-field) {
+ padding: 0.65rem 0.75rem;
+ font-size: 0.875rem;
+ line-height: 1.25;
+ color: #495057;
+ background-color: #fff;
+ background-image: none;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 2px;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ height: auto
+}
+
+.select-dropdown-input {
+ border: none !important;
+}
+
+.select-dropdown-input-container .select-dropdown-chevron {
+ top: 10px;
+}
+
+.js-submit-buttons-rendered {
+ margin-top: 40px;
+}
+
+.page .page-header {
+ padding: 10px;
+ background: #343a40;
+ font-size: 0.9rem;
+ margin: 0;
+}
+
+.page-header a {
+ color: #ffffff;
+ text-decoration: none;
+}
+
+#config-section .page-header a {
+ color: #000;
+ font-size: 0.8rem;
+}
+
+.page-header a .fa {
+ color: #868e96;
+}
+
+.menus-container a .fa {
+ color: #ffffff;
+}
+
+.margin-bottom {
+ margin-bottom: 20px;
+ padding: 10px;
+}
+
+.accordion-section {
+ margin: 20px;
+ background: #fff;
+ padding: 50px 30px;
+}
+
+.project-overview-column {
+ background: #fff;
+}
+
+.table-list {
+ font-size: 0.85em;
+ margin-bottom: 20px;
+ padding: 10px;
+ background: #ffffff;
+}
+
+.table-list-header {
+ background: #fbfbfb;
+ border: 1px solid #e5e5e5;
+ border-radius: 5px 5px 0 0;
+ line-height: 30px;
+ padding-left: 10px;
+ padding-right: 3px;
+ height: 30px;
+}
+
+.table-list-row {
+ padding: 10px;
+}
+
+.sidebar-content {
+ background: #f8f9fa;
+ margin-top: 10px;
+ padding: 10px;
+}
+
+.sidebar-container {
+ height: 100%;
+ width: 100%;
+}
+
+.sidebar-container .page-header {
+ background: none;
+}
+
+.sidebar {
+ margin-top: 20px;
+}
+
+.sidebar .js-select-dropdown-autocomplete-rendered {
+ padding: 10px;
+}
+
+
+.sidebar ul {
+ margin-top: 10px;
+}
+
+.sidebar ul:before {
+ content: "Navigation";
+ text-transform: uppercase;
+ font-size: 11px;
+ padding: 7px 15px;
+ display: block;
+ margin-bottom: 0;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+}
+
+.sidebar ul>li {
+ padding: 5px 0 0 15px;
+ border-top: 1px solid #ced4da;
+}
+
+.sidebar ul>li:last-child {
+ border-bottom: 1px solid #ced4da;
+}
+
+.sidebar ul>li a {
+ color: #868e96;
+ font-size: 0.9rem;
+}
+
+.sidebar>ul li.active {
+ background-color: #f8f9fa
+}
+
+.sidebar ul>li.active a {
+ color: #343a40;
+}
+
+#modal-box {
+ padding: 20px 20px 30px 20px;
+}
+
+table {
+ color: #000;
+}
+
+table.table-fixed th {
+ background: #3B6998;
+}
+table.table-fixed th, table.table-fixed th a {
+ color: #ffffff ;
+}
+
+table.table-fixed th, table.table-fixed td {
+ padding-left: 10px;
+}
+
+.board-add-icon i {
+ color: #3B6998;
+}
+
+
+@media (max-width: 768px) {
+ .sidebar {
+ max-width: 100%;
+ }
+}
diff --git a/plugins/Customizer/Assets/css/themes/Breathe.css b/plugins/Customizer/Assets/css/themes/Breathe.css
new file mode 100644
index 00000000..b262244c
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Breathe.css
@@ -0,0 +1,327 @@
+/*!
+ * Modified version of Oxygen - Theme for Kanboard
+ * Licensed under the MIT license - Oxygen/LICENSE
+ * https://github.com/kenlog/Oxygen
+ * Copyright (c) 2018 Valentino Pesce - https://iltuobrand.it
+ */
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+h1{
+ color:#007BA8;
+}
+a{
+ color:#007BA8;
+}
+.js-modal-medium{
+ color:#007BA8;
+}
+.sidebar>ul a:hover{
+ color:#007BA8;
+}
+div.task-board-recent {
+ box-shadow: none;
+ border-bottom: 6px solid rgba(0, 0, 0, 0.3);
+}
+div.ganttview-vtheader-series-name {
+ padding: 0 6px;
+}
+th,td {
+ padding: 10px;
+}
+header {
+ border-bottom: none;
+ box-shadow: 0px 1px 3px 0 rgba(46,61,73,.12);
+ padding: 15px 10px;
+ margin-bottom: 15px;
+ background-color: #e8fbff!important;
+}
+.header img {
+ float: left;
+}
+.header h2 {
+ position: relative;
+ color:#007BA8;
+ top: 13px;
+ left: 10px;
+ margin: 0;
+}
+label {
+ font-weight: bold;
+ margin-top: 18px;
+}
+.task-board {
+ margin-bottom: 8px;
+ padding: 12px;
+ border-radius: 6px;
+ box-shadow: 0px 5px 5px 0 rgba(46,61,73,.12);
+ border: none;
+}
+.task-board-title {
+ font-size: 1.25em;
+ font-weight: 600;
+}
+.task-show-details {
+ border-radius: 10px;
+ margin-bottom: 20px;
+}
+.task-show-details h2 {
+ background-color: rgba(0, 0, 0, 0.3);
+ padding: 20px;
+ border-radius: 8px 8px 0 0;
+ color: #fff;
+}
+.task-show-details ul {
+ padding: 20px;
+}
+.task-summary-container {
+ border: none;
+ border-radius: 7px;
+ border-bottom: 5px solid;
+ padding: 20px;
+}
+.table-small {
+ font-size: 1em;
+}
+table th {
+ text-align: left;
+ padding: 0.7em 3px;
+ border: 1px solid #eee;
+ background: #fbfbfb;
+}
+table td {
+ border: none;
+ padding: 0.5em 7px;
+ vertical-align: top;
+}
+.sidebar ul {
+ padding-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+.sidebar ul:last-of-type {
+ border-bottom: none;
+}
+.sidebar-collapse i,.sidebar-collapsed .sidebar {
+ background-color: #999;
+ border-radius: 24px;
+ padding: 5px 10px 5px 8px;
+}
+.sidebar-collapse i,.sidebar-collapsed .sidebar i {
+ color: #fff;
+}
+.sidebar-collapse i:hover,.sidebar-collapsed .sidebar:hover {
+ background-color: #ccc;
+}
+.page-header {
+ margin: 15px 0;
+ background-color: #eee;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+}
+.page-header li {
+ font-size: 1.1em;
+}
+.page-header h2 {
+ border-bottom: none;
+}
+.listing {
+ border: none;
+}
+.fa-play {
+ font-size: 1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 28px;
+ padding: 8px 0 0 15px;
+ margin-right: 8px;
+}
+.fa-play:hover {
+ background-color: #999;
+ color: #fff;
+}
+.fc-event .fc-content {
+ padding: 4px 8px;
+}
+.board-add-icon {
+ float: left;
+ padding: 0 5px
+}
+.board-add-icon i {
+ text-decoration: none;
+ color: #289E7B;
+ font-size: 1.4em
+}
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #23292d
+}
+.form-column select {
+ font-size: 1.2em;
+ margin: 10px 0 2px 0;
+}
+.form-column:nth-of-type(2) {
+ padding: 10px 25px;
+ background-color: #f7f7f7;
+}
+.form-actions {
+ font-size: 1.2em;
+}
+.page-header ul.dropdown-submenu-open {
+ margin: 10px 0 0 -10px;
+ padding: 0;
+}
+.select-dropdown-input-container {
+ max-width: 300px;
+}
+.dropdown-submenu-open li:nth-child(even) {
+ background-color: #f7f7f7;
+}
+.dropdown-submenu-open li {
+ padding: 6px 10px;
+}
+.dropdown-submenu-open li:hover {
+ background-color: #eee;
+}
+.filters {
+ border: none;
+}
+.filters ul.dropdown-submenu-open {
+ margin: 6px 0 0 8px;
+}
+.markdown pre {
+ background: #3333330d;
+ max-height: 600px;
+ overflow: auto;
+ margin-bottom: 1em;
+ padding: 5px;
+ color: #242729;
+ font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
+}
+a i.web-notification-icon {
+ color: #D45353;
+}
+.fa-play {
+ font-size:1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px!important;
+ padding: 9px 3px 0 9px!important;
+ margin-right: 8px;
+}
+#modal-box {
+ padding: 10px;
+}
+.table-list-category {
+ padding: 2px 2px 2px 5px;
+}
+.table-list-row:hover {
+ background: #f5f5f5;
+ border-bottom: 1px solid #f7f7f7;
+ border-right: 1px solid #f7f7f7;
+}
+.page-header {
+ margin: 15px 0;
+ background-color: #ffffff;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #f7f7f7;
+ border-top: 1px solid #f7f7f7;
+}
+.comments .comment-highlighted {
+ background-color: #fff;
+ border: 2px solid #F5E982;
+ border-radius: 5px;
+}
+.comments .comment:hover {
+ background: #f5f5f5;
+ border-radius: 5px;
+}
+.comments .comment:nth-child(even):not(.comment-highlighted):hover {
+ border-radius: 5px;
+ background: #f5f5f5;
+}
+.comments .comment:nth-child(even):not(.comment-highlighted) {
+ background: #fbfbfb;
+ border-radius: 5px;
+}
+.sidebar {
+ background-color: #f7f7f7;
+ padding: 7px;
+ border-radius: 7px;
+ border-left: 2px solid #e9e9e9;
+ box-shadow: 1px 0px 7px 0 rgba(46,61,73,.12);
+}
+input[type="number"]:focus, input[type="date"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="text"]:focus {
+ color: #000;
+ border-color: #007BA8;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222,222,222,0.25);
+}
+textarea:focus {
+ color: #000;
+ border-color: #007BA8;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222,222,222,0.25);
+}
+input[type="number"], input[type="date"], input[type="email"], input[type="password"], input[type="text"]:not(.input-addon-field) {
+ padding:3px;
+ transition: box-shadow 1s;
+}
+.text-editor textarea{
+ padding:3px;
+ transition: box-shadow 1s;
+}
+select{
+ border: 1px solid #ccc;
+ background: #f9f9f9;
+ padding: 3px;
+}
+.btn-blue {
+ border-color: #007BA8;
+ background: #007BA8;
+ color: #fff;
+ background-image: linear-gradient(-180deg, #007BA8 0%, #007387 90%);
+}
+.btn-blue:hover, .btn-blue:focus {
+ border-color: #007BA8;
+ background: #ffffff;
+ color: #007BA8;
+}
+.select2-container--default.select2-container--focus .select2-selection--multiple {
+ border: solid #007BA8 1px;
+ outline: 0;
+}
+.select2-container--default .select2-results__option--highlighted[aria-selected] {
+ background-color: #007BA8;
+ color: white;
+}
+.board-add-icon i:focus, .board-add-icon i:hover {
+ text-decoration: none;
+ color: #91C259;
+}
+.board-add-icon i {
+ text-decoration: none;
+ color: #007BA8;
+ font-size: 1.4em;
+}
+.dropdown-submenu-open li:not(.no-hover):hover {
+ background: #007BA8;
+ color: #fff;
+}
+.sidebar>ul li.active a {
+ color: #007BA8;
+ font-weight: bold;
+}
+.table-list-row .table-list-title a:hover, .table-list-row .table-list-title a:focus {
+ text-decoration: underline;
+ color: #007BA8;
+}
+.image-slideshow-overlay img {
+ display: block;
+ margin: auto;
+ max-width: 100%;
+}
diff --git a/plugins/Customizer/Assets/css/themes/Clemson.css b/plugins/Customizer/Assets/css/themes/Clemson.css
new file mode 100644
index 00000000..d7406315
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Clemson.css
@@ -0,0 +1,341 @@
+/*!
+ * Modified version of Oxygen - Theme for Kanboard
+ * Licensed under the MIT license - Oxygen/LICENSE
+ * https://github.com/kenlog/Oxygen
+ * Copyright (c) 2018 Valentino Pesce - https://iltuobrand.it
+ */
+
+@import url("https://fonts.googleapis.com/css?family=Raleway:400,700");
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+h1{
+ color:#FF6200;
+}
+a{
+ color:#FF6200;
+}
+.js-modal-medium{
+ color:#FF6200;
+}
+.sidebar>ul a:hover{
+ color:#FF6200;
+}
+div.ganttview-vtheader-series-name {
+ padding: 0 6px;
+}
+th,td {
+ padding: 10px;
+}
+header {
+ border-bottom: none;
+ box-shadow: 0px 1px 3px 0 rgba(46,61,73,.12);
+ padding: 15px 10px;
+ margin-bottom: 15px;
+ background-color: #ff6200; /* Old browsers */
+ background: -moz-linear-gradient(-45deg, #ff6200 36%, #9d00ff 100%); /* FF3.6-15 */
+ background: -webkit-linear-gradient(-45deg, #ff6200 36%,#9d00ff 100%); /* Chrome10-25,Safari5.1-6 */
+ background: linear-gradient(135deg, #ff6200 36%,#9d00ff 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff6200', endColorstr='#9d00ff',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
+}
+.header img {
+ float: left;
+}
+.header h2 {
+ position: relative;
+ color:#FFFFFF;!important;
+ top: 13px;
+ left: 10px;
+ margin: 0;
+}
+header h1 {
+ font-size: 1.4em!important;
+ color: #FFFFFF!important;
+}
+label {
+ font-weight: bold;
+ margin-top: 18px;
+}
+
+.task-board {
+ margin-bottom: 8px;
+ padding: 9px;
+ border-radius: 3px;
+ box-shadow: 0px 1px 4px 0.6px rgba(157,0,255,0.6);
+ border: 2px solid;
+ background-color: #ffffff!important;
+}
+.task-board-title {
+ font-size: 1.25em;
+ font-weight: 600;
+}
+.task-show-details {
+ border-radius: 10px;
+ margin-bottom: 20px;
+}
+.task-show-details h2 {
+ background-color: rgba(0, 0, 0, 0.3);
+ padding: 20px;
+ border-radius: 8px 8px 0 0;
+ color: #fff;
+}
+.task-show-details ul {
+ padding: 20px;
+}
+.task-summary-container {
+ border: none;
+ border-radius: 7px;
+ border-bottom: 5px solid;
+ padding: 20px;
+}
+.table-small {
+ font-size: 1em;
+}
+table th {
+ text-align: left;
+ padding: 0.7em 3px;
+ border: 1px solid #F2DCD3;
+ background: #FFEFEB;
+}
+table td {
+ border: none;
+ padding: 0.5em 7px;
+ vertical-align: top;
+}
+.sidebar ul {
+ padding-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+.sidebar ul:last-of-type {
+ border-bottom: none;
+}
+.sidebar-collapse i,.sidebar-collapsed .sidebar {
+ background-color: #999;
+ border-radius: 24px;
+ padding: 5px 10px 5px 8px;
+}
+.sidebar-collapse i,.sidebar-collapsed .sidebar i {
+ color: #fff;
+}
+.sidebar-collapse i:hover,.sidebar-collapsed .sidebar:hover {
+ background-color: #ccc;
+}
+.page-header {
+ margin: 15px 0;
+ background-color: #F2DCD3;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+}
+.page-header li {
+ font-size: 1.1em;
+}
+.page-header h2 {
+ border-bottom: none;
+}
+.listing {
+ border: none;
+}
+.fa-play {
+ font-size: 1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 28px;
+ padding: 8px 0 0 15px;
+ margin-right: 8px;
+}
+.fa-play:hover {
+ background-color: #999;
+ color: #fff;
+}
+.fc-event .fc-content {
+ padding: 4px 8px;
+}
+.board-add-icon {
+ float: left;
+ padding: 0 5px
+}
+.board-add-icon i {
+ text-decoration: none;
+ color: #9D00FF;
+ font-size: 1.4em
+}
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #23292d
+}
+.form-column select {
+ font-size: 1.2em;
+ margin: 10px 0 2px 0;
+}
+.form-column:nth-of-type(2) {
+ padding: 10px 25px;
+ background-color: #f7f7f7;
+}
+.form-actions {
+ font-size: 1.2em;
+}
+.page-header ul.dropdown-submenu-open {
+ margin: 10px 0 0 -10px;
+ padding: 0;
+}
+.select-dropdown-input-container {
+ max-width: 300px;
+}
+.dropdown-submenu-open li:nth-child(even) {
+ background-color: #f7f7f7;
+}
+.dropdown-submenu-open li {
+ padding: 6px 10px;
+}
+.dropdown-submenu-open li:hover {
+ background-color: #F2DCD3;
+}
+.filters {
+ border: none;
+}
+.filters ul.dropdown-submenu-open {
+ margin: 6px 0 0 8px;
+}
+.markdown pre {
+ background: #FFF;
+ max-height: 600px;
+ overflow: auto;
+ margin-bottom: 1em;
+ padding: 5px;
+ color: #242729;
+ font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
+}
+code {
+ background-color: rgba(249, 161, 0, 0.12)!important;
+ color: #ab05ff!important;
+}
+a i.web-notification-icon {
+ color: #FF6200;
+}
+.fa-play {
+ font-size:1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px!important;
+ padding: 9px 3px 0 9px!important;
+ margin-right: 8px;
+}
+#modal-box {
+ padding: 10px;
+}
+.table-list-category {
+ padding: 2px 2px 2px 5px;
+}
+.table-list-row:hover {
+ background: #f5f5f5;
+ border-bottom: 1px solid #f7f7f7;
+ border-right: 1px solid #f7f7f7;
+}
+.page-header {
+ margin: 15px 0;
+ background-color: #ffffff;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #f7f7f7;
+ border-top: 1px solid #f7f7f7;
+}
+.comments .comment-highlighted {
+ background-color: #fff;
+ border: 2px solid #AD3BFF;
+ border-radius: 5px;
+}
+.comments .comment:hover {
+ background: #f5f5f5;
+ border-radius: 5px;
+}
+.comments .comment:nth-child(even):not(.comment-highlighted):hover {
+ border-radius: 5px;
+ background: #f5f5f5;
+}
+.comments .comment:nth-child(even):not(.comment-highlighted) {
+ background: #FFEFEB;
+ border-radius: 5px;
+}
+.sidebar {
+ background-color: #f7f7f7;
+ padding: 7px;
+ border-radius: 7px;
+ border-left: 2px solid #e9e9e9;
+ box-shadow: 1px 0px 7px 0 rgba(46,61,73,.12);
+}
+input[type="number"]:focus, input[type="date"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="text"]:focus {
+ color: #000;
+ border-color: #FF6200;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222,222,222,0.25);
+}
+textarea:focus {
+ color: #000;
+ border-color: #FF6200;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222,222,222,0.25);
+}
+input[type="number"], input[type="date"], input[type="email"], input[type="password"], input[type="text"]:not(.input-addon-field) {
+ padding:3px;
+ transition: box-shadow 1s;
+}
+.text-editor textarea{
+ padding:3px;
+ transition: box-shadow 1s;
+}
+select{
+ border: 1px solid #ccc;
+ background: #f9f9f9;
+ padding: 3px;
+}
+.btn-blue {
+ border-color: #FF6200;
+ background: #FF6200;
+ color: #fff;
+ background-image: linear-gradient(-180deg, #FF6200 0%, #4B00B5 90%);
+}
+.btn-blue:hover, .btn-blue:focus {
+ border-color: #FF6200;
+ background: #ffffff;
+ color: #FF6200;
+}
+.select2-container--default.select2-container--focus .select2-selection--multiple {
+ border: solid #FF6200 1px;
+ outline: 0;
+}
+.select2-container--default .select2-results__option--highlighted[aria-selected] {
+ background-color: #FF6200;
+ color: white;
+}
+.board-add-icon i:focus, .board-add-icon i:hover {
+ text-decoration: none;
+ color: #E68332;
+}
+.board-add-icon i {
+ text-decoration: none;
+ color: #FF6200;
+ font-size: 1.4em;
+}
+.dropdown-submenu-open li:not(.no-hover):hover {
+ background: #FF6200;
+ color: #fff;
+}
+.sidebar>ul li.active a {
+ color: #FF6200;
+ font-weight: bold;
+}
+.table-list-row .table-list-title a:hover, .table-list-row .table-list-title a:focus {
+ text-decoration: underline;
+ color: #FF6200;
+}
+.image-slideshow-overlay img {
+ display: block;
+ margin: auto;
+ max-width: 100%;
+}
+
diff --git a/plugins/Customizer/Assets/css/themes/Galaxy.css b/plugins/Customizer/Assets/css/themes/Galaxy.css
new file mode 100644
index 00000000..9517cecc
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Galaxy.css
@@ -0,0 +1,2713 @@
+/*!
+ * Modified version of Nebula - Theme for Kanboard
+ * Licensed under the MIT license - Nebula/LICENSE
+ * https://github.com/kenlog/Nebula
+ * Copyright (c) 2018 Valentino Pesce - https://iltuobrand.it
+ */
+
+ h1,li,ul,ol,table,tr,td,th,p,blockquote,body {
+ margin:0;
+ padding:0;
+ font-size:100%
+}
+body {
+ padding-bottom:10px;
+ color:#ced4da;
+ background-color: #222;
+ font-family: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ text-rendering:optimizeLegibility
+}
+code {
+ background-color: #cccccc52!important;
+}
+small {
+ font-size:0.8em
+}
+::selection{background:rgba(113,113,113,0.5);color:#ffffff}::-moz-selection{background:rgba(113,113,113,0.5);color:#ffffff}
+hr {
+ border:0;
+ height:0;
+ border-top:0px solid rgba(0,0,0,0.1);
+ border-bottom:0px solid rgba(255,255,255,0.3)
+}
+::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+ color: #ffffff!important;
+ opacity: 1; /* Firefox */
+}
+
+:-ms-input-placeholder { /* Internet Explorer 10-11 */
+ color: #ffffff!important;
+}
+
+::-ms-input-placeholder { /* Microsoft Edge */
+ color: #ffffff!important;
+}
+.select-dropdown-input-container {
+ background-color: #252525!important;
+ max-width: 300px;
+}
+.page {
+ margin-left:10px;
+ margin-right:10px
+}
+.margin-top {
+ margin-top:20px
+}
+.margin-bottom {
+ margin-bottom:20px
+}
+.pull-right {
+ text-align:right
+}
+ul.no-bullet li {
+ list-style-type:none;
+ margin-left:0
+}
+#app-loading-icon {
+ position:fixed;
+ right:3px;
+ bottom:3px
+}
+.assign-me {
+ vertical-align:bottom
+}
+a {
+ color:#00bc8c;
+ border:none;
+ text-decoration:none;
+}
+a:focus {
+ outline:0;
+ color:#20c997;
+ text-decoration:none
+}
+a:hover {
+ color:#ced4da;
+ text-decoration:none
+}
+a .fa {
+ padding-right:3px;
+ text-decoration:none;
+ color:#ced4da
+}
+h1,h2,h3 {
+ font-weight:normal;
+ color:#ffffff
+}
+h1 {
+ font-size:1.5em
+}
+h2 {
+ font-size:1.4em;
+ margin-bottom:10px
+}
+h3 {
+ margin-top:10px;
+ font-size:1.2em
+}
+table {
+ width:100%;
+ border-collapse:collapse;
+ border-spacing:0;
+ margin-bottom:20px
+}
+table.table-fixed {
+ table-layout:fixed;
+ white-space:nowrap
+}
+table.table-fixed th {
+ overflow:hidden
+}
+table.table-fixed td {
+ white-space:nowrap;
+ overflow:hidden;
+ text-overflow:ellipsis
+}
+table.table-small {
+ font-size:0.8em
+}
+table.table-striped tr:nth-child(odd) {
+ background:#252525
+}
+@media (max-width: 768px) {
+ table.table-scrolling {
+ overflow-x:auto;
+ display:inline-block;
+ vertical-align:top;
+ max-width:100%;
+ white-space:nowrap
+ }
+}
+table th {
+ text-align:left;
+ padding:0.5em 3px;
+ border:0px solid #eee;
+ background:#252525
+}
+table th a {
+ text-decoration:none;
+ color:#ced4da
+}
+table th a:focus,table th a:hover {
+ text-decoration:underline
+}
+table td {
+ border:0px solid #eee;
+ padding:0.5em 3px;
+ vertical-align:top
+}
+table td li {
+ margin-left:20px
+}
+.column-1 {
+ width:1%
+}
+.column-2 {
+ width:2%
+}
+.column-3 {
+ width:3%
+}
+.column-4 {
+ width:4%
+}
+.column-5 {
+ width:5%
+}
+.column-6 {
+ width:6%
+}
+.column-7 {
+ width:7%
+}
+.column-8 {
+ width:8%
+}
+.column-9 {
+ width:9%
+}
+.column-10 {
+ width:10%
+}
+.column-11 {
+ width:11%
+}
+.column-12 {
+ width:12%
+}
+.column-13 {
+ width:13%
+}
+.column-14 {
+ width:14%
+}
+.column-15 {
+ width:15%
+}
+.column-16 {
+ width:16%
+}
+.column-17 {
+ width:17%
+}
+.column-18 {
+ width:18%
+}
+.column-19 {
+ width:19%
+}
+.column-20 {
+ width:20%
+}
+.column-21 {
+ width:21%
+}
+.column-22 {
+ width:22%
+}
+.column-23 {
+ width:23%
+}
+.column-24 {
+ width:24%
+}
+.column-25 {
+ width:25%
+}
+.column-26 {
+ width:26%
+}
+.column-27 {
+ width:27%
+}
+.column-28 {
+ width:28%
+}
+.column-29 {
+ width:29%
+}
+.column-30 {
+ width:30%
+}
+.column-31 {
+ width:31%
+}
+.column-32 {
+ width:32%
+}
+.column-33 {
+ width:33%
+}
+.column-34 {
+ width:34%
+}
+.column-35 {
+ width:35%
+}
+.column-36 {
+ width:36%
+}
+.column-37 {
+ width:37%
+}
+.column-38 {
+ width:38%
+}
+.column-39 {
+ width:39%
+}
+.column-40 {
+ width:40%
+}
+.column-41 {
+ width:41%
+}
+.column-42 {
+ width:42%
+}
+.column-43 {
+ width:43%
+}
+.column-44 {
+ width:44%
+}
+.column-45 {
+ width:45%
+}
+.column-46 {
+ width:46%
+}
+.column-47 {
+ width:47%
+}
+.column-48 {
+ width:48%
+}
+.column-49 {
+ width:49%
+}
+.column-50 {
+ width:50%
+}
+.column-51 {
+ width:51%
+}
+.column-52 {
+ width:52%
+}
+.column-53 {
+ width:53%
+}
+.column-54 {
+ width:54%
+}
+.column-55 {
+ width:55%
+}
+.column-56 {
+ width:56%
+}
+.column-57 {
+ width:57%
+}
+.column-58 {
+ width:58%
+}
+.column-59 {
+ width:59%
+}
+.column-60 {
+ width:60%
+}
+.column-61 {
+ width:61%
+}
+.column-62 {
+ width:62%
+}
+.column-63 {
+ width:63%
+}
+.column-64 {
+ width:64%
+}
+.column-65 {
+ width:65%
+}
+.column-66 {
+ width:66%
+}
+.column-67 {
+ width:67%
+}
+.column-68 {
+ width:68%
+}
+.column-69 {
+ width:69%
+}
+.column-70 {
+ width:70%
+}
+.column-71 {
+ width:71%
+}
+.column-72 {
+ width:72%
+}
+.column-73 {
+ width:73%
+}
+.column-74 {
+ width:74%
+}
+.column-75 {
+ width:75%
+}
+.column-76 {
+ width:76%
+}
+.column-77 {
+ width:77%
+}
+.column-78 {
+ width:78%
+}
+.column-79 {
+ width:79%
+}
+.column-80 {
+ width:80%
+}
+.column-81 {
+ width:81%
+}
+.column-82 {
+ width:82%
+}
+.column-83 {
+ width:83%
+}
+.column-84 {
+ width:84%
+}
+.column-85 {
+ width:85%
+}
+.column-86 {
+ width:86%
+}
+.column-87 {
+ width:87%
+}
+.column-88 {
+ width:88%
+}
+.column-89 {
+ width:89%
+}
+.column-90 {
+ width:90%
+}
+.column-91 {
+ width:91%
+}
+.column-92 {
+ width:92%
+}
+.column-93 {
+ width:93%
+}
+.column-94 {
+ width:94%
+}
+.column-95 {
+ width:95%
+}
+.column-96 {
+ width:96%
+}
+.column-97 {
+ width:97%
+}
+.column-98 {
+ width:98%
+}
+.column-99 {
+ width:99%
+}
+.column-100 {
+ width:100%
+}
+.draggable-row-handle {
+ cursor:move;
+ color: #ced4da;
+ padding-left: 10px;
+}
+.draggable-row-handle:hover {
+ color:#ced4da
+}
+.ui-widget-content {
+ color: #00bc8c;
+}
+tr.draggable-item-selected {
+ background:#fff;
+ border:2px solid #666;
+ box-shadow:4px 2px 10px -4px rgba(0,0,0,0.55)
+}
+tr.draggable-item-selected td {
+ border-top:none;
+ border-bottom:none
+}
+tr.draggable-item-selected td:first-child {
+ border-left:none
+}
+tr.draggable-item-selected td:last-child {
+ border-right:none
+}
+.table-stripped tr.draggable-item-hover,.table-stripped tr.draggable-item-hover {
+ background:#FEFFF2
+}
+.table-list {
+ font-size:0.85em;
+ margin-bottom:20px
+}
+.table-list-header {
+ background:#222;
+ border:0px solid #e5e5e5;
+ border-radius:5px 5px 0 0;
+ line-height:28px;
+ padding-left:3px;
+ padding-right:3px
+}
+.table-list-header a {
+ color:#ced4da;
+ font-weight:500;
+ text-decoration:none;
+ margin-right:10px
+}
+.table-list-header a:hover,.table-list-header a:focus {
+ color:#767676
+}
+.table-list-header .table-list-header-count {
+ color:#ced4d1;
+ display:inline-block;
+ float:left
+}
+.table-list-header .table-list-header-menu {
+ text-align:right
+}
+.table-list-row {
+ padding-left:3px;
+ padding-right:3px;
+ padding: 10px;
+ border-bottom:0px solid #e5e5e5;
+ border-right:0px solid #e5e5e5
+}
+.table-list-row.table-border-left {
+ border-left:0px solid #e5e5e5
+}
+.table-list-row:nth-child(odd) {
+ background:#252525
+}
+.table-list-row:last-child {
+ border-radius:0 0 5px 5px
+}
+.table-list-row:hover {
+ background:#252525;
+ border-bottom:0px solid #ffeb8e;
+ border-right:0px solid #ffeb8e
+}
+.table-list-row .table-list-title {
+ font-weight:500;
+ line-height:23px
+}
+.table-list-row .table-list-title.status-closed {
+ text-decoration:line-through;
+ margin-right:10px
+}
+.table-list-row .table-list-title.status-closed a {
+ font-style:italic
+}
+.table-list-row .table-list-title a {
+ color:#ced4da;
+ text-decoration:none
+}
+.table-list-row .table-list-title a:hover,.table-list-row .table-list-title a:focus {
+ text-decoration:underline
+}
+.table-list-row .table-list-details {
+ color:#f7f7f7;
+ font-weight:300;
+ line-height:20px
+}
+.table-list-row .table-list-details span {
+ margin-left:5px
+}
+.table-list-row .table-list-details span:first-child {
+ margin-left:0
+}
+.table-list-row .table-list-details li {
+ display:inline;
+ list-style-type:none
+}
+.table-list-row .table-list-details li:after {
+ content:', '
+}
+.table-list-row .table-list-details li:last-child:after {
+ content:''
+}
+.table-list-row .table-list-details strong {
+ font-weight:400;
+ color:#f7f7f7
+}
+.table-list-row .table-list-details-with-icons {
+ float:left
+}
+@media (max-width: 768px) {
+ .table-list-row .table-list-details-with-icons {
+ float:none
+ }
+}
+.table-list-row .table-list-icons {
+ font-size:0.8em;
+ text-align:right;
+ line-height:30px
+}
+@media (max-width: 768px) {
+ .table-list-row .table-list-icons {
+ text-align:left;
+ line-height:20px
+ }
+}
+.table-list-row .table-list-icons span {
+ margin-left:5px
+}
+.table-list-row .table-list-icons a {
+ text-decoration:none
+}
+.table-list-row .table-list-icons a:hover {
+ color:#ced4da
+}
+.table-list-row .table-list-icons a:hover i {
+ color:#ced4da
+}
+.table-list-category {
+ font-size:0.9em;
+ font-weight:500;
+ color:#000;
+ padding:1px 2px 1px 2px;
+ border-radius:3px;
+ background:#fcfcfc;
+ border:0px solid #ccc
+}
+.table-list-category a {
+ text-decoration:none;
+ color:#000
+}
+.table-list-category a:hover {
+ color:#00bc8c
+}
+fieldset {
+ border:0px solid #ddd;
+ margin-top:10px
+}
+legend {
+ font-weight:500;
+ font-size:1.2em
+}
+label {
+ cursor:pointer;
+ display:block;
+ margin-top:10px;
+ font-weight:400
+}
+input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field) {
+ width:300px;
+ max-width:95%;
+ font-size:1em;
+ height:25px;
+ padding-bottom:0;
+ padding-left:4px;
+
+ line-height: 1.5;
+ color: #fff;
+ background-color: #252525;
+ border: 2px solid #4a5368;
+ border-radius: .25rem;
+ -webkit-transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+
+ font-family:sans-serif;
+ -webkit-appearance:none;
+ -moz-appearance:none
+
+}
+input[type="number"]::-webkit-input-placeholder,input[type="date"]::-webkit-input-placeholder,input[type="email"]::-webkit-input-placeholder,input[type="password"]::-webkit-input-placeholder,input[type="text"]:not(.input-addon-field)::-webkit-input-placeholder {
+ color:#222
+}
+input[type="number"]::-moz-placeholder,input[type="date"]::-moz-placeholder,input[type="email"]::-moz-placeholder,input[type="password"]::-moz-placeholder,input[type="text"]:not(.input-addon-field)::-moz-placeholder {
+ color:#222
+}
+input[type="number"]:-ms-input-placeholder,input[type="date"]:-ms-input-placeholder,input[type="email"]:-ms-input-placeholder,input[type="password"]:-ms-input-placeholder,input[type="text"]:not(.input-addon-field):-ms-input-placeholder {
+ color:#222
+}
+input[type="number"]:focus,input[type="date"]:focus,input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus {
+ color:#ced4da;
+ border-color:rgba(82,168,236,0.8);
+ outline:0;
+ box-shadow:0 0 8px rgb(45, 60, 93)
+}
+input[type="number"] {
+ width:70px
+}
+input[type="text"]:not(.input-addon-field).form-numeric {
+ width:70px
+}
+input[type="text"]:not(.input-addon-field).form-datetime,input[type="text"]:not(.input-addon-field).form-date {
+ width:150px
+}
+input[type="text"]:not(.input-addon-field).form-input-large {
+ width:400px
+}
+input[type="text"]:not(.input-addon-field).form-input-small {
+ width:150px
+}
+textarea:focus {
+ color:#fff;
+ border-color:rgba(82,168,236,0.8);
+ outline:0;
+ box-shadow:0 0 8px rgba(82,168,236,0.6)
+}
+textarea {
+ padding:10px;
+ border:0px solid #ccc;
+ width:400px;
+ max-width:99%;
+ height:200px;
+ font-family:sans-serif;
+ font-size:1em;
+ line-height: 1.5;
+ color: #fff;
+ background-color: #252525;
+ border: 2px solid #4a5368;
+ border-radius: .25rem;
+ -webkit-transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
+ transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;
+}
+textarea::-webkit-input-placeholder {
+ color:#222
+}
+textarea::-moz-placeholder {
+ color:#222
+}
+textarea:-ms-input-placeholder {
+ color:#222
+}
+select{
+ max-width:95%;
+ border: 3px solid #4a5368;
+ background: #252525;
+ padding: 3px;
+ color:#eee;
+}
+select:focus {
+ outline:0
+}
+select[multiple] {
+ width:300px
+}
+.select2-container--default .select2-selection--multiple .select2-selection__choice {
+ background-color: #3b3e47;
+ border: 1px solid #3b3e47;
+ border-radius: 4px;
+ cursor: default;
+ float: left;
+ margin-right: 5px;
+ margin-top: 5px;
+ padding: 0 5px;
+}
+.tag-autocomplete {
+ width:400px
+}
+span.select2-container {
+ margin-top:2px
+}
+.form-actions {
+ padding-top:20px;
+ clear:both;
+}
+.form-required {
+ color:red;
+ padding-left:5px;
+ font-weight:bold
+}
+@media (max-width: 480px) {
+ .form-required {
+ display:none
+ }
+}
+input[type="text"].form-max-width {
+ width:100%
+}
+input.form-error,textarea.form-error {
+ border:2px solid #b94a48
+}
+input.form-error:focus,textarea.form-error:focus {
+ box-shadow:none;
+ border:2px solid #b94a48
+}
+.form-errors {
+ color:#b94a48;
+ list-style-type:none
+}
+ul.form-errors li {
+ margin-left:0
+}
+.form-help {
+ font-size: 0.9em;
+ color: #00c9ff;
+ margin-bottom:15px
+}
+.form-inline {
+ padding:0;
+ margin:0;
+ border:none
+}
+.form-inline label {
+ display:inline;
+ padding-right:3px
+}
+.form-inline input,.form-inline select {
+ margin:0 15px 0 0
+}
+.form-inline .form-required {
+ display:none
+}
+.form-inline .form-actions {
+ display:inline-block
+}
+.form-inline .js-submit-buttons-rendered {
+ display:inline-block
+}
+.form-inline-group {
+ display:inline
+}
+.form-columns {
+ display:-webkit-flex;
+ display:flex;
+ -webkit-flex-direction:row;
+ flex-direction:row;
+ -webkit-flex-wrap:wrap;
+ flex-wrap:wrap;
+ -webkit-justify-content:flex-start;
+ justify-content:flex-start
+}
+.form-columns .form-column {
+ margin-right:25px;
+ flex-grow:1
+}
+.form-columns fieldset {
+ margin-top:0
+}
+.form-login {
+ max-width:350px;
+ margin:5% auto 0
+}
+@media (max-width: 480px) {
+ .form-login {
+ margin-left:5px
+ }
+}
+.form-login li {
+ margin-left:25px;
+ line-height:25px
+}
+.form-login h2 {
+ margin-bottom:30px;
+ font-weight:bold
+}
+.reset-password {
+ margin-top:20px;
+ margin-bottom:20px
+}
+.reset-password a {
+ color:#999
+}
+.input-addon {
+ display:flex
+}
+.input-addon-field {
+ flex:1;
+ font-size:1em;
+ color:#ced4da;
+ background: #3e424d;
+ margin:0;
+ -webkit-appearance:none;
+ -moz-appearance:none
+}
+.input-addon-field:first-child {
+ border-radius:5px 0 0 5px
+}
+.input-addon-field:last-child {
+ border-radius:0 5px 5px 0
+}
+.input-addon-item {
+ background-color:rgba(147,128,108,0.1);
+ color:#666;
+ font:inherit;
+ font-weight:normal
+}
+.input-addon-item:first-child {
+ border-radius:5px 0 0 5px
+}
+.input-addon-item:last-child {
+ border-radius:0 5px 5px 0
+}
+@media (max-width: 480px) {
+ .input-addon-item .dropdown .fa-caret-down {
+ display:none
+ }
+}
+.input-addon-field,.input-addon-item {
+ border:0px solid rgba(147,128,108,0.25);
+ padding:4px 0.75em
+}
+.input-addon-field:not(:first-child),.input-addon-item:not(:first-child) {
+ border-left:0
+}
+.icon-success {
+ color:#468847
+}
+.icon-error {
+ color:#b94a48
+}
+.icon-fade-out {
+ opacity:1;
+ animation:icon-fadeout 5s linear forwards
+}
+@keyframes icon-fadeout {
+ 0% {
+ opacity:1
+ }
+ 100% {
+ opacity:0
+ }
+}
+.alert {
+ padding:8px 35px 8px 14px;
+ margin-top:5px;
+ margin-bottom:5px;
+ color:#fff;
+ background-color:#20c997;
+ border:0px solid #20c997;
+ border-radius:4px
+}
+.alert-success {
+ color: #ffffff;
+ background-color: #20c997;
+ border-color: #20c997;
+}
+.alert-error {
+ color:#b94a48;
+ background-color:#f2dede;
+ border-color:#eed3d7
+}
+.alert-info {
+ color:#3a87ad;
+ background-color:#d9edf7;
+ border-color:#bce8f1
+}
+.alert-normal {
+ color:#ced4da;
+ background-color:#f0f0f0;
+ border-color:#ddd
+}
+.alert ul {
+ margin-top:10px;
+ margin-bottom:10px
+}
+.alert li {
+ margin-left:25px
+}
+.alert-fade-out {
+ text-align:center;
+ position:fixed;
+ bottom:0;
+ left:20%;
+ width:60%;
+ padding-top:5px;
+ padding-bottom:5px;
+ margin-bottom:0;
+ border-width:1px 0 0;
+ border-radius:4px 4px 0 0;
+ z-index:9999;
+ opacity:1;
+ animation:fadeout 5s linear forwards
+}
+@keyframes fadeout {
+ 0% {
+ opacity:1
+ }
+ 100% {
+ opacity:0
+ }
+}
+a.btn {
+ text-decoration:none
+}
+.btn {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ font-size: 1.2em;
+ font-weight: normal;
+ cursor: pointer;
+ display: inline-block;
+ border-radius: 5px;
+ padding: 3px 10px;
+ margin: 0;
+ border: 0px solid #ddd;
+ background: #00bc8c;
+ color: #fbfbfb;
+}
+.btn:hover,.btn:focus {
+ border-color:#bbb;
+ background:#fafafa;
+ color:#000
+}
+.btn-red {
+ border-color:#b0281a;
+ background:#d14836;
+ color:#fff
+}
+.btn-red:hover,.btn-red:focus {
+ border-color:#b0281a;
+ background:#c53727;
+ color:#fff
+}
+.btn-blue {
+ border-color:#188ae2;
+ background:#188ae2;
+ color:#fff
+}
+.btn-blue:hover,.btn-blue:focus {
+ border-color:#1475bf;
+ background:#1475bf;
+ color:#fff
+}
+.btn:disabled {
+ color:#ccc;
+ border-color:#ccc;
+ background:#f7f7f7
+}
+.buttons-header {
+ font-size:0.8em;
+ margin-top:5px;
+ margin-bottom:15px
+}
+.tooltip i.fa {
+ cursor:pointer
+}
+.tooltip .fa-info-circle {
+ color:#999
+}
+#tooltip-container {
+ padding: 5px;
+ background: #3a404c;
+ border: 2px solid #ddd;
+ border-radius: 4px;
+ box-shadow: -1px -1px 7px #aaa;
+ position: absolute;
+ min-width: 350px;
+}
+#tooltip-container .markdown p {
+ margin-bottom:0
+}
+#tooltip-container .tooltip-large {
+ width:600px
+}
+h2 .dropdown ul {
+ display:none
+}
+.dropdown {
+ display:inline;
+ position:relative
+}
+.dropdown ul {
+ display:none
+}
+.dropdown-smaller {
+ font-size:0.85em
+}
+ul.dropdown-submenu-open {
+ display:block;
+ position:absolute;
+ z-index:1000;
+ min-width:285px;
+ list-style:none;
+ margin:3px 0 0 1px;
+ padding:6px 0;
+ background-color:#fff;
+ border:0px solid #b2b2b2;
+ border-radius:3px;
+ box-shadow:0 1px 3px rgba(0,0,0,0.15)
+}
+.dropdown-submenu-open li {
+ display:block;
+ margin:0;
+ padding:8px 10px;
+ font-size:0.9em;
+ border-bottom:0px solid #f8f8f8;
+ cursor:pointer
+}
+.dropdown-submenu-open li.no-hover {
+ cursor:default
+}
+.dropdown-submenu-open li:last-child {
+ border:none
+}
+.dropdown-submenu-open li:not(.no-hover):hover {
+ background:#222;
+ transition-duration: .05s;
+ color:#fff
+}
+.dropdown-submenu-open li:hover a {
+ color:#fff;
+ transition-duration: .05s;
+}
+.dropdown-submenu-open a {
+ text-decoration:none;
+ color:#3b3e47;
+ transition-duration: .05s;
+}
+.dropdown-submenu-open a:focus {
+ text-decoration:underline
+}
+.dropdown-menu-link-text,.dropdown-menu-link-icon {
+ color:#ced4da;
+ text-decoration:none
+}
+.dropdown-menu-link-text:hover {
+ text-decoration:underline
+}
+td a.dropdown-menu strong {
+ color:#ced4da
+}
+td a.dropdown-menu strong i {
+ color:#ced4da
+}
+td a.dropdown-menu i {
+ color:#ced4da
+}
+td a.dropdown-menu:hover strong {
+ color:#555
+}
+td a.dropdown-menu:hover strong i {
+ color:#555
+}
+td a.dropdown-menu:hover i {
+ color:#ced4da
+}
+.accordion-title {
+ background: #222;
+}
+.accordion-title h3 {
+ display:inline;
+ padding-right:5px;
+ background:#222;
+}
+.accordion-content {
+ margin-top:15px;
+ margin-bottom:25px
+}
+.accordion-toggle {
+ color:#ced4da;
+ text-decoration:none
+}
+.accordion-toggle:focus {
+ color:#ced4da
+}
+.accordion-toggle:hover {
+ color:#999
+}
+.accordion-toggle:before {
+ content:"\f0d7"
+}
+.accordion-collapsed {
+ margin-bottom:25px
+}
+.accordion-collapsed .accordion-toggle:before {
+ content:"\f0da"
+}
+.accordion-collapsed .accordion-content {
+ display:none
+}
+.select2-container--default .select2-selection--single {
+ background-color: #3b4658;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+}
+.select2-container--default .select2-selection--single .select2-selection__rendered {
+ color: #fff;
+ line-height: 28px;
+}
+.select2-container--default .select2-results__option--highlighted[aria-selected] {
+ background-color: #3b4658;
+ color: white;
+}
+.select2-container--default .select2-results>.select2-results__options {
+ max-height: 200px;
+ overflow-y: auto;
+ background-color: #3b4658;
+}
+.select2-container--default .select2-results__option[aria-selected="true"] {
+ background-color: #4e5663!important;
+ color: #fff!important;
+}
+#select-dropdown-menu {
+ position:absolute;
+ display:block;
+ z-index:1000;
+ min-width:160px;
+ padding:5px 0;
+ background:#3b4658;
+ list-style:none;
+ border:0px solid #ccc;
+ border-radius:3px;
+ box-shadow:0 6px 12px rgba(0,0,0,0.175);
+ overflow-x: hidden;
+}
+.select-dropdown-menu-item {
+ white-space:nowrap;
+ overflow:hidden;
+ padding:3px 10px;
+ color:#ced4da;
+ cursor:pointer;
+ border-bottom:0px solid #f8f8f8;
+ line-height:1.5em;
+ font-weight:400
+}
+.select-dropdown-menu-item.active {
+ color:#fff;
+ background:#428bca
+}
+.select-dropdown-menu-item:last-child {
+ border:none
+}
+.select-dropdown-input-container {
+ position:relative;
+ border:0px solid #ccc;
+ border-radius:5px;
+ background-color:#fff
+}
+.select-dropdown-input-container input.select-dropdown-input {
+ margin:0 0 0 5px;
+ border:none;
+ height:23px
+}
+.select-dropdown-input-container input.select-dropdown-input:focus {
+ border:none;
+ box-shadow:none
+}
+.select-dropdown-input-container .select-dropdown-chevron {
+ color:#555;
+ position:absolute;
+ top:4px;
+ right:5px;
+ cursor:pointer
+}
+.select-dropdown-input-container .select-loading-icon {
+ color:#555;
+ position:absolute;
+ top:4px;
+ right:5px;
+}
+#suggest-menu {
+ position:absolute;
+ display:block;
+ z-index:1000;
+ min-width:160px;
+ padding:5px 0;
+ background:#fff;
+ list-style:none;
+ border:0px solid #ccc;
+ border-radius:3px;
+ box-shadow:0 6px 12px rgba(0,0,0,0.175)
+}
+.suggest-menu-item {
+ white-space:nowrap;
+ padding:3px 10px;
+ color:#ced4da;
+ font-weight:bold;
+ cursor:pointer
+}
+.suggest-menu-item.active {
+ color:#fff;
+ background:#428bca
+}
+.suggest-menu-item.active small {
+ color:#fff
+}
+.suggest-menu-item small {
+ color:#999;
+ font-weight:normal
+}
+#modal-overlay {
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+ background:rgba(0,0,0,0.9);
+ overflow:auto;
+ z-index:100
+}
+#modal-box {
+ position:fixed;
+ max-height:calc(100% - 30px);
+ top:0%;
+ padding: 20px;
+ left:50%;
+ transform:translateX(-50%);
+ background:#252525;
+ overflow:auto;
+ border-radius:5px
+}
+#modal-content {
+ padding:0 5px 5px
+}
+#modal-header {
+ text-align:right;
+ padding-right:5px
+}
+#modal-close-button {
+ color:#ced4da
+}
+#modal-close-button:hover {
+ color:#b94a48
+}
+.pagination {
+ text-align:center;
+ font-size:0.9em
+}
+.pagination-showing {
+ margin-right:5px;
+ padding-right:5px;
+ border-right:0px solid #999
+}
+.pagination-next {
+ margin-left:5px
+}
+.pagination-previous {
+ margin-right:5px
+}
+header {
+ border-bottom: none;
+ box-shadow: 0px 1px 3px 0 rgba(46,61,73,.12);
+ padding: 15px 10px;
+ margin-bottom: 15px;
+ background: #303030;
+}
+.header img {
+ float: left;
+}
+.header h2 {
+ position: relative;
+ color:#ced4da;
+ top: 16px;
+ left: 10px;
+ margin: 0;
+}
+header .title-container {
+ flex:1;
+ min-width:300px
+}
+@media (max-width: 480px) {
+ header .title-container {
+ order:3
+ }
+}
+header .board-selector-container {
+ min-width:320px;
+ display:flex;
+ align-items:center
+}
+@media (max-width: 480px) {
+ header .board-selector-container {
+ order:2;
+ min-width:300px
+ }
+ header .board-selector-container input[type=text] {
+ max-width:280px
+ }
+}
+header .menus-container {
+ min-width:120px;
+ display:flex;
+ align-items:center;
+ justify-content:flex-end
+}
+@media (max-width: 480px) {
+ header .menus-container {
+ order:1;
+ margin-bottom:5px;
+ margin-left:auto
+ }
+}
+header h1 {
+ font-size:1.33em!important;
+}
+header h1 .tooltip {
+ opacity:0.3;
+ font-size:0.7em
+}
+a i.web-notification-icon {
+ color:#00bc8c
+}
+a i.web-notification-icon:focus,a i.web-notification-icon:hover {
+ color:#000
+}
+.logo a {
+ opacity:0.5;
+ color:#d40000;
+ text-decoration:none
+}
+.logo span {
+ color:#ced4da
+}
+.logo a:hover {
+ opacity:0.8;
+ color:#ced4da
+}
+.logo a:focus span,.logo a:hover span {
+ color:#d40000
+}
+.page-header {
+ margin-bottom:20px
+}
+.page-header .dropdown {
+ padding-right:10px
+}
+.page-header h2 {
+ margin:0;
+ padding:0;
+ font-weight:bold;
+ border-bottom:0px dotted #ccc
+}
+.page-header h2 a {
+ color:#ced4da;
+ text-decoration:none
+}
+.page-header h2 a:focus,.page-header h2 a:hover {
+ color:#999
+}
+.page-header ul {
+ text-align:left;
+ margin-top:5px;
+ display:inline-block
+}
+.page-header li {
+ display:inline;
+ padding-right:15px
+}
+@media (max-width: 480px) {
+ .page-header li {
+ display:block;
+ line-height:1.5em
+ }
+}
+.page-header li.active a {
+ color:#ced4da;
+ text-decoration:none;
+ font-weight:bold
+}
+.page-header li.active a:hover,.page-header li.active a:focus {
+ text-decoration:underline
+}
+.menu-inline {
+ margin-bottom:5px
+}
+.menu-inline li {
+ display:inline;
+ padding-right:15px
+}
+.menu-inline li .active a {
+ font-weight:bold;
+ color:#000;
+ text-decoration:none
+}
+.sidebar-container {
+ height:100%;
+ display:flex;
+ flex-flow:row
+}
+@media (max-width: 768px) {
+ .sidebar-container {
+ flex-flow:wrap
+ }
+}
+.sidebar-content {
+ padding-left:10px;
+ flex:1 100%;
+ max-width:85%;
+ overflow-wrap:break-word
+}
+@media (max-width: 768px) {
+ .sidebar-content {
+ padding-left:0;
+ order:1;
+ max-width:100%
+ }
+}
+@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1) {
+ .sidebar-content {
+ max-width:75%
+ }
+}
+.sidebar {
+ max-width:25%;
+ min-width:230px;
+ background-color: #252525;
+ padding: 20px;
+}
+@media (max-width: 768px) {
+ .sidebar {
+ flex:1 auto;
+ order:2
+ }
+}
+.sidebar h2 {
+ margin-top:0
+}
+.sidebar>ul a {
+ text-decoration:none;
+ color:#ced4da;
+ font-weight:300
+}
+.sidebar>ul a:hover {
+ color:#ced4da
+}
+.sidebar>ul li {
+ list-style-type:none;
+ line-height:35px;
+ border-bottom:0px dotted #efefef;
+ padding-left:13px
+}
+.sidebar>ul li:hover {
+ border-left:5px solid #00bc8c;
+ padding-left:8px
+}
+.sidebar>ul li.active {
+ border-left:5px solid #00bc8c;
+ padding-left:8px
+}
+.sidebar>ul li.active a {
+ color:#ced4da;
+ font-weight:bold
+}
+.sidebar-icons>ul li {
+ padding-left:10px
+}
+.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active {
+ padding-left: 10px;
+ border-left: none;
+ background-color: #3b404b;
+}
+.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover {
+ color:#ced4da
+}
+.sidebar>ul li:last-child {
+ margin-bottom:15px
+}
+.avatar img {
+ vertical-align:bottom
+}
+.avatar-left {
+ float:left;
+ margin-right:10px
+}
+.avatar-inline {
+ display:inline-block;
+ margin-right:3px
+}
+.avatar-48 img,.avatar-48 div {
+ border-radius:30px
+}
+.avatar-48 .avatar-letter {
+ line-height:48px;
+ width:48px;
+ font-size:25px
+}
+.avatar-20 img,.avatar-20 div {
+ border-radius:10px
+}
+.avatar-20 .avatar-letter {
+ line-height:20px;
+ width:20px;
+ font-size:11px
+}
+.avatar-letter {
+ color:#fff;
+ text-align:center
+}
+#file-dropzone,#screenshot-zone {
+ position:relative;
+ border:2px dashed #ccc;
+ width:99%;
+ height:250px;
+ overflow:auto
+}
+#file-dropzone-inner,#screenshot-inner {
+ position:absolute;
+ left:0;
+ bottom:48%;
+ width:100%;
+ text-align:center;
+ color:#aaa
+}
+#screenshot-zone.screenshot-pasted {
+ border:2px solid #ced4da
+}
+#file-list {
+ margin:20px
+}
+#file-list li {
+ list-style-type:none;
+ padding-top:8px;
+ padding-bottom:8px;
+ border-bottom:0px dotted #ddd;
+ width:95%
+}
+#file-list li .file-error {
+ font-weight:bold;
+ color:#b94a48
+}
+.file-thumbnails {
+ display:-webkit-flex;
+ display:flex;
+ -webkit-flex-direction:row;
+ flex-direction:row;
+ -webkit-flex-wrap:wrap;
+ flex-wrap:wrap;
+ -webkit-justify-content:flex-start;
+ justify-content:flex-start
+}
+.file-thumbnail {
+ width:250px;
+ border:0px solid #efefef;
+ border-radius:5px;
+ margin-bottom:20px;
+ box-shadow:4px 2px 10px -6px rgba(0,0,0,0.55);
+ margin-right:15px
+}
+.file-thumbnail img {
+ cursor:pointer;
+ border-top-left-radius:5px;
+ border-top-right-radius:5px
+}
+.file-thumbnail img:hover {
+ opacity:0.5
+}
+.file-thumbnail-content {
+ padding-left:8px;
+ padding-right:8px
+}
+.file-thumbnail-title {
+ font-weight:700;
+ font-size:0.9em;
+ color:#555;
+ overflow:hidden;
+ text-overflow:ellipsis
+}
+.file-thumbnail-description {
+ font-size:0.8em;
+ color:#999;
+ margin-top:8px;
+ margin-bottom:5px
+}
+.file-viewer {
+ position:relative
+}
+.file-viewer img {
+ max-width:95%;
+ max-height:85%;
+ margin-top:10px
+}
+.color-picker {
+ width:180px
+}
+.color-picker-option {
+ height:25px
+}
+.color-picker-square {
+ display:inline-block;
+ width:18px;
+ height:18px;
+ margin-right:5px;
+ border:0px solid #000
+}
+.color-picker-label {
+ display:inline-block;
+ vertical-align:bottom;
+ padding-bottom:3px
+}
+.filter-box {
+ max-width:1024px
+}
+.action-menu {
+ color:#ced4da;
+ text-decoration:none
+}
+.action-menu:hover,.action-menu:focus {
+ text-decoration:underline
+}
+.js-project-creation-options {
+ max-width:500px;
+ border-left:1px solid #6e727b;
+ margin-top:20px;
+ padding-left:15px;
+ padding-bottom:5px;
+ padding-top:5px
+}
+.project-overview-columns {
+ display:-webkit-flex;
+ display:flex;
+ -webkit-flex-direction:row;
+ flex-direction:row;
+ -webkit-flex-wrap:wrap;
+ flex-wrap:wrap;
+ -webkit-align-items:center;
+ align-items:center;
+ -webkit-justify-content:center;
+ justify-content:center;
+ margin-bottom:20px;
+ font-size:1.4em
+}
+@media (max-width: 480px) {
+ .project-overview-columns {
+ display:block
+ }
+}
+.project-overview-column {
+ text-align:center;
+ margin-right:3%;
+ margin-top:5px;
+ padding:3px 15px 3px 15px;
+ border:1px dashed #999
+}
+@media (max-width: 480px) {
+ .project-overview-column {
+ text-align:left
+ }
+}
+.project-overview-column small {
+ color:#e5e5e5
+}
+.project-overview-column strong {
+ color:#eee;
+ display:block
+}
+@media (max-width: 480px) {
+ .project-overview-column strong {
+ display:inline
+ }
+}
+.project-header {
+ margin-bottom:8px
+}
+.project-header .dropdown-component {
+ margin-top:4px;
+ margin-right:5px;
+ float:left
+}
+@media (max-width: 768px) {
+ .project-header .dropdown-component {
+ float:none
+ }
+}
+.project-header .views-switcher-component {
+ margin-top:4px;
+ float:left
+}
+@media (max-width: 768px) {
+ .project-header .views-switcher-component {
+ float:none;
+ margin-bottom:10px
+ }
+}
+.project-header .filter-box-component form {
+ margin:0
+}
+.views {
+ margin-right:10px;
+ margin-top:1px;
+ font-size:0.9em
+}
+@media (max-width: 560px) {
+ .views {
+ width:100%
+ }
+}
+@media (max-width: 768px) {
+ .views {
+ margin-top:10px;
+ font-size:1em
+ }
+}
+@media (max-width: 480px) {
+ .views {
+ margin-top:5px
+ }
+}
+.views li {
+ white-space:nowrap;
+ background:rgba(147,128,108,0.1);
+ border:0px solid #ddd;
+ border-right:none;
+ padding:4px 8px;
+ display:inline
+}
+@media (max-width: 560px) {
+ .views li {
+ display:block;
+ margin-top:5px;
+ border-radius:5px;
+ border:0px solid #ddd
+ }
+}
+.views li.active a {
+ font-weight:bold;
+ color:#00bc8c;
+ text-decoration:none
+}
+.views li:first-child {
+ border-top-left-radius:5px;
+ border-bottom-left-radius:5px
+}
+.views li:last-child {
+ border-right:0px solid #ddd;
+ border-top-right-radius:5px;
+ border-bottom-right-radius:5px
+}
+.views a {
+ color:#ced4da;
+ text-decoration:none
+}
+.views a:hover {
+ color:#fff;
+ text-decoration:none;
+}
+.dashboard-project-stats small {
+ margin-right:10px;
+ color:#999
+}
+.dashboard-table-link {
+ font-weight:bold;
+ color:#000;
+ text-decoration:none
+}
+.dashboard-table-link:focus,.dashboard-table-link:hover {
+ color:#999
+}
+.public-board {
+ margin-top:5px
+}
+.public-task {
+ max-width:800px;
+ margin:5px auto 0
+}
+#board-container {
+ overflow-x:auto
+}
+#board {
+ table-layout:fixed;
+ margin-bottom:0
+}
+#board th.board-column-header {
+ width:240px
+}
+#board td {
+ vertical-align:top
+}
+.board-container-compact {
+ overflow-x:initial
+}
+@media all and (-ms-high-contrast: active), (-ms-high-contrast: none) {
+ .board-container-compact #board {
+ table-layout:auto
+ }
+}
+#board th.board-column-header.board-column-compact {
+ width:initial
+}
+.board-column-collapsed {
+ display:none
+}
+td.board-column-task-collapsed {
+ font-weight:bold;
+ background-color:#222
+}
+#board th.board-column-header-collapsed {
+ width:28px;
+ min-width:28px;
+ text-align:center;
+ overflow:hidden
+}
+.board-rotation-wrapper {
+ position:relative;
+ padding:8px 4px;
+ min-height:150px;
+ overflow:hidden
+}
+.board-rotation {
+ white-space:nowrap;
+ -webkit-backface-visibility:hidden;
+ -webkit-transform:rotate(90deg);
+ -moz-transform:rotate(90deg);
+ -ms-transform:rotate(90deg);
+ transform:rotate(90deg);
+ -webkit-transform-origin:0 100%;
+ -moz-transform-origin:0 100%;
+ -ms-transform-origin:0 100%;
+ transform-origin:0 100%
+}
+.board-column-title .dropdown-menu {
+ text-decoration:none
+}
+.board-add-icon {
+ float:left;
+ padding:0 5px
+}
+.board-add-icon i {
+ text-decoration:none;
+ color:#00bc8c;
+ font-size:1.4em
+}
+.board-add-icon i:focus,.board-add-icon i:hover {
+ text-decoration:none;
+ color:#20c997
+}
+.board-column-header-task-count {
+ color:#999;
+ font-weight:normal
+}
+a.board-swimlane-toggle {
+ text-decoration:none
+}
+a.board-swimlane-toggle:hover,a.board-swimlane-toggle:focus {
+ color:#000;
+ text-decoration:none;
+ border:none
+}
+.board-task-list {
+ min-height:60px
+}
+.board-task-list .task-board:last-child {
+ margin-bottom:0
+}
+.board-task-list-limit {
+ background-color:#DF5353
+}
+.draggable-item {
+ cursor:pointer;
+ user-select:none;
+ -webkit-user-select:none;
+ -moz-user-select:none
+}
+.draggable-placeholder {
+ border:2px dashed #000;
+ background:#fafafa;
+ height:70px;
+ margin-bottom:10px
+}
+div.draggable-item-selected {
+ border:0px solid #000
+}
+.task-board-sort-handle {
+ float:left;
+ padding-right:5px
+}
+.task-board {
+ position:relative;
+ margin-bottom:4px;
+ border:2px solid;
+ padding:10px;
+ word-wrap:break-word;
+ font-size:0.9em;
+ border-radius:6px;
+ background-color:#252525!important;
+}
+div.task-board-recent {
+ border-width:2px
+}
+div.task-board-status-closed {
+ user-select:none;
+ border:0px dotted #555
+}
+.task-board a {
+ color:#00bc8c;
+ text-decoration:none
+}
+.task-board-collapsed {
+ overflow:hidden;
+ white-space:nowrap;
+ text-overflow:ellipsis
+}
+.task-board-title {
+ margin-top:5px;
+ margin-bottom:8px
+}
+.task-board-title a:hover {
+ text-decoration:underline
+}
+.task-board-saving-state {
+ opacity:0.3
+}
+.task-board-saving-icon {
+ position:absolute;
+ margin:auto;
+ width:100%;
+ text-align:center;
+ color:#000
+}
+.task-board-avatars {
+ text-align:right;
+ float:right
+}
+.task-board-change-assignee {
+ cursor:pointer
+}
+.task-board-change-assignee:hover {
+ opacity:0.6
+}
+.task-list-avatars {
+ display:inline-block;
+ float:left
+}
+.c3-chart-arc path {
+ stroke: #fff0;
+}
+@media (max-width: 768px) {
+ .task-list-avatars {
+ float:none;
+ display:block
+ }
+}
+.task-list-avatars .task-avatar-assignee {
+ font-weight:300;
+ color:#999
+}
+.task-list-avatars:hover .task-avatar-assignee {
+ font-weight:400;
+ color:#000
+}
+.task-board-icons,.task-list-icons {
+ font-size:0.8em;
+ text-align:right
+}
+.task-board-icons a,.task-list-icons a {
+ text-decoration:none
+}
+.task-board-icons a:hover,.task-list-icons a:hover {
+ color:#ced4da
+}
+.task-board-icons a:hover i,.task-list-icons a:hover i {
+ color:#ced4da
+}
+.task-board-icons .task-score,.task-list-icons .task-score {
+ font-weight:bold
+}
+.task-board-icons .flag-milestone,.task-list-icons .flag-milestone {
+ color:green
+}
+.task-board-icons {
+ margin-top:7px
+}
+.task-board-icons a {
+ opacity:0.5;
+}
+.task-board-icons span {
+ opacity:0.5;
+ margin-left:4px;
+ color:#97d2ff;
+}
+.task-board-icons a:hover {
+ opacity:1.0;
+ font-weight:bold
+}
+.task-board-icons .task-board-icons-row {
+ line-height:22px
+}
+.task-list-icons {
+ line-height:22px
+}
+.task-list-icons a,.task-list-icons span,.task-list-icons i {
+ color:#999;
+ opacity:1.0
+}
+.task-list-icons span {
+ margin-left:5px
+}
+@media (max-width: 768px) {
+ .task-list-icons {
+ text-align:left
+ }
+}
+.task-icon-age {
+ display:inline-block
+}
+span.task-icon-age-total {
+ border:0px solid #e5e5e5;
+ padding:1px 3px 1px 3px;
+ border-top-left-radius:3px;
+ border-bottom-left-radius:3px
+}
+span.task-icon-age-column {
+ border:0px solid #e5e5e5;
+ border-left:none;
+ margin-left:-5px;
+ padding:1px 3px 1px 3px;
+ border-top-right-radius:3px;
+ border-bottom-right-radius:3px
+}
+.task-board span.task-icon-age-total,.task-board span.task-icon-age-column {
+ border-color:#666
+}
+.task-board-category-container {
+ text-align:right;
+ margin-top:8px;
+ margin-bottom:8px
+}
+.task-board-category {
+ border:0px solid #555;
+ font-size:0.9em;
+ font-weight:500;
+ color:#000;
+ padding:1px 3px 1px 2px;
+ border-radius:3px
+}
+.task-board-category a:hover {
+ text-decoration:underline
+}
+.task-date {
+ font-weight:500;
+ color:#000
+}
+span.task-date-today {
+ opacity:1.0;
+ color:#00bc8c
+}
+span.task-date-overdue {
+ opacity:1.0;
+ color:#b94a48
+}
+.task-tags li {
+ display:inline-block;
+ margin:3px 3px 0 0;
+ padding:1px 3px 1px 3px;
+ border:0px solid #ced4da;
+ border-radius:4px;
+ background:#20c997;
+}
+.select2-container--default .select2-results__option[aria-selected="true"] {
+ background-color: #e5e5e5;
+ color: #333;
+}
+.task-summary-container .task-tags {
+ margin-top:10px
+}
+.task-list-tag {
+ background:#20c997;
+ border-color:#20c997;
+ padding-left: 3px;
+}
+#task-summary {
+ margin-bottom:15px
+}
+#task-summary h2 {
+ color:#f7f7f7;
+ font-size:1.6em;
+ margin-top:0;
+ padding-top:0
+}
+.task-summary-container {
+ border: 2px solid;
+ border-radius: 8px;
+ padding: 10px;
+ background-color:#29303e!important;
+}
+.task-summary-columns {
+ display:flex;
+ flex-flow:row;
+ justify-content:space-between
+}
+@media (max-width: 768px) {
+ .task-summary-columns {
+ flex-flow:column
+ }
+}
+.task-summary-column {
+ color:#ced4da
+}
+.task-summary-column span {
+ color:#ced4cb
+}
+.task-summary-column li {
+ line-height:23px
+}
+#external-task-view {
+ padding:10px;
+ margin-top:10px;
+ margin-bottom:10px;
+ border:0px dotted #ccc
+}
+.task-form-container {
+ box-sizing:border-box;
+ display:flex;
+ flex-wrap:wrap
+}
+.task-form-container>* {
+ box-sizing:border-box
+}
+.task-form-container>* {
+ width:1%
+}
+.task-form-main-column {
+ width:60%
+}
+@media (max-width: 1000px) {
+ .task-form-main-column {
+ width:100%
+ }
+}
+.task-form-main-column input[type="text"] {
+ width:700px;
+ max-width:99%
+}
+.task-form-secondary-column {
+ max-width:250px;
+ min-width:200px;
+ max-height:600px;
+ padding-left:10px;
+ overflow:auto;
+ width:20%
+}
+@media (max-width: 1000px) {
+ .task-form-secondary-column {
+ width:100%;
+ max-width:99%;
+ max-height:none
+ }
+}
+@media (max-width: 768px) {
+ .task-form-secondary-column {
+ padding-left:0
+ }
+}
+.task-form-secondary-column label:first-child {
+ margin-top:0
+}
+@media (max-width: 1000px) {
+ .task-form-secondary-column label:first-child {
+ margin-top:10px
+ }
+}
+.task-form-bottom {
+ width:100%
+}
+.comment-sorting {
+ text-align:right
+}
+.comment-sorting a {
+ color:#f7f7f7;
+ font-weight:normal;
+ text-decoration:none
+}
+.comment-sorting a:hover {
+ color:#999
+}
+.comment {
+ padding:5px;
+ margin-bottom:15px
+}
+.comment-title {
+ border-bottom:0px dotted #eee;
+ margin-left:55px
+}
+.comment-date {
+ color:#999;
+ font-weight:200
+}
+.comment-actions {
+ text-align:right
+}
+.comment-content {
+ margin-left:55px
+}
+.comments .text-editor textarea {
+ height:90px
+}
+.comments .text-editor .text-editor-preview-area {
+ height:90px
+}
+.comments .comment-highlighted {
+ background-color:#252525;
+ border:2px solid #ffeb8e
+}
+.comments .comment-highlighted:hover {
+ background-color:#252525
+}
+.comments .comment:hover {
+ background:#252525
+}
+.comments .comment:nth-child(even):not(.comment-highlighted) {
+ background:#222
+}
+.comments .comment:nth-child(even):not(.comment-highlighted):hover {
+ background:#252525
+}
+.subtask-cell {
+ padding:4px 10px;
+ border-top:0px dotted #222;
+ border-left:0px dotted #222;
+ display:table-cell;
+ vertical-align:middle;
+}
+.subtask-cell a {
+ color:#ced4da;
+ text-decoration:none
+}
+.subtask-cell a:hover,.subtask-cell a:focus {
+ color:#00bc8c;
+}
+.subtask-cell:first-child {
+ border-left:none
+}
+@media (max-width: 768px) {
+ .subtask-cell {
+ width:90%;
+ display:block;
+ border-left:none
+ }
+}
+.task-list-subtasks {
+ display:table;
+ width:100%
+}
+@media (max-width: 768px) {
+ .task-list-subtasks {
+ display:block
+ }
+}
+.task-list-subtask {
+ display:table-row
+}
+@media (max-width: 768px) {
+ .task-list-subtask {
+ display:block
+ }
+}
+@media (max-width: 768px) {
+ .subtask-assignee,.subtask-time-tracking-cell {
+ display:none
+ }
+}
+.task-links-table td {
+ vertical-align:middle
+}
+.task-links-task-count {
+ color:#999;
+ font-weight:normal
+}
+.task-link-closed {
+ text-decoration:line-through
+}
+.text-editor {
+ margin-top:10px
+}
+.text-editor-toolbar {
+ margin-bottom: 10px;
+}
+.text-editor a {
+ font-size:1em;
+ color:#999;
+ text-decoration:none;
+ margin-right:10px
+}
+.text-editor a:hover {
+ color:#00bc8c
+}
+.text-editor .text-editor-preview-area {
+ border:0px solid #222;
+ width:700px;
+ max-width:99%;
+ height:250px;
+ overflow:auto;
+ padding:2px
+}
+.text-editor textarea {
+ width:700px;
+ max-width:98%;
+ height:250px
+}
+.markdown {
+ line-height:1.4em
+}
+.markdown h1 {
+ margin-top:5px;
+ margin-bottom:10px;
+ font-weight:bold
+}
+.markdown h2 {
+ font-weight:bold
+}
+.markdown p {
+ margin-bottom:10px
+}
+.markdown ol,.markdown ul {
+ margin-left:25px;
+ margin-top:10px;
+ margin-bottom:10px
+}
+.markdown pre {
+ background:#242729;
+ padding:10px;
+ border-radius:5px;
+ border:0px solid #ddd;
+ overflow:auto;
+ overflow-wrap:initial;
+ color:#555
+}
+.markdown blockquote {
+ font-style:italic;
+ border-left:3px solid #ddd;
+ padding-left:10px;
+ margin-bottom:10px;
+ margin-left:20px
+}
+.markdown img {
+ display:block;
+ max-width:80%;
+ margin-top:10px
+}
+.documentation {
+ margin:0 auto;
+ padding:20px;
+ max-width:850px;
+ background:#fefefe;
+ border:0px solid #ccc;
+ border-radius:5px;
+ color:#555
+}
+.documentation img {
+ border:0px solid #ced4da
+}
+.documentation h1 {
+ text-decoration:none;
+ margin-bottom:30px
+}
+.documentation h2 {
+ text-decoration:none;
+ border-bottom:0px solid #ccc;
+ margin-bottom:25px
+}
+.documentation li {
+ line-height:30px
+}
+.panel {
+ border-radius:4px;
+ padding:8px 35px 8px 10px;
+ margin-top:10px;
+ margin-bottom:15px;
+ border:0px solid #ddd;
+ color:#ced4da;
+ background-color:#252525;
+ overflow:auto
+}
+.panel li {
+ list-style-type:square;
+ margin-left:20px;
+ line-height:1.35em
+}
+.activity-event {
+ margin-bottom:15px;
+ padding:10px
+}
+.activity-event:nth-child(even) {
+ background:#252525
+}
+.activity-event:hover {
+ background:#252525
+}
+.activity-date {
+ margin-left:10px;
+ font-weight:normal;
+ color:#999
+}
+.activity-content {
+ margin-left:55px
+}
+.activity-title {
+ font-weight:bold;
+ color:#ced4da;
+ border-bottom:0px dotted #efefef
+}
+.activity-description {
+ color:#f7f7f7;
+ margin-top:10px
+}
+@media (max-width: 480px) {
+ .activity-description {
+ overflow:auto
+ }
+}
+.activity-description li {
+ list-style-type:circle
+}
+.activity-description ul {
+ margin-top:10px;
+ margin-left:20px
+}
+.user-mention-link {
+ font-weight:bold;
+ color:#5897fb;
+ text-decoration:none
+}
+.user-mention-link:hover {
+ color:#fff
+}
+.image-slideshow-overlay {
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+ background:rgba(0,0,0,0.95);
+ overflow:auto;
+ z-index:100
+}
+.image-slideshow-overlay img {
+ display:block;
+ margin:auto;
+ max-width: 100%;
+}
+.image-slideshow-overlay figcaption {
+ color:#fff;
+ opacity:0.7;
+ position:absolute;
+ bottom:5px;
+ right:15px
+}
+.slideshow-icon {
+ color:#fff;
+ position:absolute;
+ font-size:2.5em;
+ opacity:0.6
+}
+.slideshow-icon:hover {
+ opacity:0.9;
+ cursor:pointer
+}
+.slideshow-previous-icon {
+ left:10px;
+ top:45%
+}
+.slideshow-next-icon {
+ right:10px;
+ top:45%
+}
+.slideshow-close-icon {
+ right:10px;
+ top:10px;
+ font-size:1.4em
+}
+.slideshow-download-icon {
+ left:10px;
+ bottom:10px;
+ font-size:1.3em
+}
+.list-item-links,.list-item-actions {
+ display:inline-block;
+ float:left;
+ margin-left:10px
+}
+.list-item-links a {
+ margin:0
+}
+.list-item-action-hidden {
+ display:none
+}
+.bulk-change-checkbox {
+ float:left
+}
+.bulk-change-inputs {
+ float:left;
+ padding-left:10px
+}
+.bulk-change-inputs label {
+ margin-top:0;
+ margin-bottom:3px
+}
+/* style plugin-gantt */
+div.ganttview-hzheader-month, div.ganttview-hzheader-day, div.ganttview-vtheader, div.ganttview-vtheader-item-name, div.ganttview-vtheader-series, div.ganttview-grid, div.ganttview-grid-row-cell {
+ float: left;
+}
+
+div.ganttview-hzheader-month, div.ganttview-hzheader-day {
+ text-align: center;
+}
+
+div.ganttview-grid-row-cell.last, div.ganttview-hzheader-day.last, div.ganttview-hzheader-month.last {
+ border-right: none;
+}
+
+div.ganttview {
+ border: 1px solid #999;
+}
+
+div.ganttview-hzheader-month {
+ width: 60px;
+ height: 20px;
+ border-right: 1px solid #d0d0d0;
+ line-height: 20px;
+ overflow: hidden;
+}
+
+div.ganttview-hzheader-day {
+ width: 20px;
+ height: 20px;
+ border-right: 1px solid #f0f0f0;
+ border-top: 1px solid #d0d0d0;
+ line-height: 20px;
+ color: #f5f5f5!important;
+}
+
+div.ganttview-vtheader {
+ margin-top: 41px;
+ width: 400px;
+ overflow: hidden;
+ background-color: #fff;
+}
+
+div.ganttview-vtheader-item {
+ color: #555;
+}
+
+div.ganttview-vtheader-series-name {
+ width: 400px;
+ height: 31px;
+ line-height: 31px;
+ padding-left: 3px;
+ color: #eee!important;
+ background: #2f3948!important;
+ border-top: 1px solid #d0d0d0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+div.ganttview-vtheader-series-name a {
+ color: #f5f5f5!important;
+ text-decoration: none;
+}
+
+div.ganttview-vtheader-series-name a:hover {
+ color: #333;
+ text-decoration: underline;
+}
+
+div.ganttview-vtheader-series-name a i {
+ color: #f5f5f5!important;
+}
+
+div.ganttview-vtheader-series-name a:hover i {
+ color: #eee!important;
+}
+
+div.ganttview-slide-container {
+ overflow: auto;
+ border-left: 1px solid #999;
+}
+
+div.ganttview-grid-row-cell {
+ width: 20px;
+ height: 31px;
+ border-right: 1px solid #f0f0f0;
+ border-top: 1px solid #f0f0f0;
+}
+
+div.ganttview-grid-row-cell.ganttview-weekend {
+ background-color: #2f3948!important;
+}
+
+div.ganttview-grid-row-cell.ganttview-today {
+ background-color: #0979b6!important;
+}
+
+div.ganttview-blocks {
+ margin-top: 40px;
+}
+
+div.ganttview-block-container {
+ height: 28px;
+ padding-top: 4px;
+}
+
+div.ganttview-block {
+ position: relative;
+ height: 25px;
+ background-color: #E5ECF9;
+ border: 1px solid #c0c0c0;
+ border-radius: 3px;
+}
+
+.ganttview-block-movable {
+ cursor: move;
+}
+
+div.ganttview-block-text {
+ position: absolute;
+ height: 12px;
+ font-size: 0.7em;
+ color: #666;
+ padding: 2px 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+div.ganttview-block div.ui-resizable-handle.ui-resizable-s {
+ bottom: 0;
+}
+
+div.ganttview-progress-bar {
+ z-index: 0;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ opacity: 0.4;
+}
diff --git a/plugins/Customizer/Assets/css/themes/Github.css b/plugins/Customizer/Assets/css/themes/Github.css
new file mode 100644
index 00000000..5fe5a3d3
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Github.css
@@ -0,0 +1,3260 @@
+/*!
+ * Derived from Moon - Theme for Kanboard by kenlog
+ * Licensed under the MIT license - Moon/LICENSE
+ * https://github.com/kenlog/Moon
+ * Copyright (c) 2018 Valentino Pesce - https://iltuobrand.it
+ */
+
+h1,
+li,
+ul,
+ol,
+table,
+tr,
+td,
+th,
+p,
+blockquote,
+body {
+ margin: 0;
+ padding: 0;
+ font-size: 100%
+}
+
+body {
+ padding-bottom: 10px;
+ color: #8d9498;
+ background-color: #fefefe;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ text-rendering: optimizeLegibility
+}
+
+::selection{background:rgba(113,113,113,0.5);color:#ffffff}::-moz-selection{background:rgba(113,113,113,0.5);color:#ffffff}
+
+small {
+ font-size: 0.8em
+}
+
+hr {
+ border: 0;
+ height: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3)
+}
+
+.page {
+ margin-left: 10px;
+ margin-right: 10px
+}
+
+.margin-top {
+ margin-top: 20px
+}
+
+.margin-bottom {
+ margin-bottom: 20px
+}
+
+.pull-right {
+ text-align: right
+}
+
+ul.no-bullet li {
+ list-style-type: none;
+ margin-left: 0
+}
+
+#app-loading-icon {
+ position: fixed;
+ right: 3px;
+ bottom: 3px
+}
+
+.assign-me {
+ vertical-align: bottom
+}
+
+a {
+ color: #0366d6;
+ border: none;
+ text-decoration: none;
+}
+
+a:focus {
+ outline: 0;
+ color: #0366d6;
+ text-decoration: none
+}
+
+a:hover {
+ color: #333;
+ text-decoration: none
+}
+
+a .fa {
+ padding-right: 3px;
+ text-decoration: none;
+ color: #acafb1
+}
+
+h1,
+h2,
+h3 {
+ font-weight: normal;
+ color: #333
+}
+
+h1 {
+ font-size: 1.5em
+}
+
+h2 {
+ font-size: 1.4em;
+ margin-bottom: 10px
+}
+
+h3 {
+ margin-top: 10px;
+ font-size: 1.2em
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ margin-bottom: 20px
+}
+
+table.table-fixed {
+ table-layout: fixed;
+ white-space: nowrap
+}
+
+table.table-fixed th {
+ overflow: hidden
+}
+
+table.table-fixed td {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis
+}
+
+table.table-small {
+ font-size: 0.8em
+}
+
+table.table-striped tr:nth-child(odd) {
+ background: #fefefe
+}
+
+@media (max-width: 768px) {
+ table.table-scrolling {
+ overflow-x: auto;
+ display: inline-block;
+ vertical-align: top;
+ max-width: 100%;
+ white-space: nowrap
+ }
+}
+
+table th {
+ text-align: left;
+ padding: 0.5em 5px;
+ border: 1px solid #eee;
+ background: #fafbfc;
+}
+
+table th a {
+ text-decoration: none;
+ color: #333
+}
+
+table th a:focus,
+table th a:hover {
+ text-decoration: underline
+}
+
+table td {
+ border: 1px solid #f5f5f5;
+ padding: 0.5em 7px;
+ vertical-align: top
+}
+
+table td li {
+ margin-left: 20px
+}
+
+.column-1 {
+ width: 1%
+}
+
+.column-2 {
+ width: 2%
+}
+
+.column-3 {
+ width: 3%
+}
+
+.column-4 {
+ width: 4%
+}
+
+.column-5 {
+ width: 5%
+}
+
+.column-6 {
+ width: 6%
+}
+
+.column-7 {
+ width: 7%
+}
+
+.column-8 {
+ width: 8%
+}
+
+.column-9 {
+ width: 9%
+}
+
+.column-10 {
+ width: 10%
+}
+
+.column-11 {
+ width: 11%
+}
+
+.column-12 {
+ width: 12%
+}
+
+.column-13 {
+ width: 13%
+}
+
+.column-14 {
+ width: 14%
+}
+
+.column-15 {
+ width: 15%
+}
+
+.column-16 {
+ width: 16%
+}
+
+.column-17 {
+ width: 17%
+}
+
+.column-18 {
+ width: 18%
+}
+
+.column-19 {
+ width: 19%
+}
+
+.column-20 {
+ width: 20%
+}
+
+.column-21 {
+ width: 21%
+}
+
+.column-22 {
+ width: 22%
+}
+
+.column-23 {
+ width: 23%
+}
+
+.column-24 {
+ width: 24%
+}
+
+.column-25 {
+ width: 25%
+}
+
+.column-26 {
+ width: 26%
+}
+
+.column-27 {
+ width: 27%
+}
+
+.column-28 {
+ width: 28%
+}
+
+.column-29 {
+ width: 29%
+}
+
+.column-30 {
+ width: 30%
+}
+
+.column-31 {
+ width: 31%
+}
+
+.column-32 {
+ width: 32%
+}
+
+.column-33 {
+ width: 33%
+}
+
+.column-34 {
+ width: 34%
+}
+
+.column-35 {
+ width: 35%
+}
+
+.column-36 {
+ width: 36%
+}
+
+.column-37 {
+ width: 37%
+}
+
+.column-38 {
+ width: 38%
+}
+
+.column-39 {
+ width: 39%
+}
+
+.column-40 {
+ width: 40%
+}
+
+.column-41 {
+ width: 41%
+}
+
+.column-42 {
+ width: 42%
+}
+
+.column-43 {
+ width: 43%
+}
+
+.column-44 {
+ width: 44%
+}
+
+.column-45 {
+ width: 45%
+}
+
+.column-46 {
+ width: 46%
+}
+
+.column-47 {
+ width: 47%
+}
+
+.column-48 {
+ width: 48%
+}
+
+.column-49 {
+ width: 49%
+}
+
+.column-50 {
+ width: 50%
+}
+
+.column-51 {
+ width: 51%
+}
+
+.column-52 {
+ width: 52%
+}
+
+.column-53 {
+ width: 53%
+}
+
+.column-54 {
+ width: 54%
+}
+
+.column-55 {
+ width: 55%
+}
+
+.column-56 {
+ width: 56%
+}
+
+.column-57 {
+ width: 57%
+}
+
+.column-58 {
+ width: 58%
+}
+
+.column-59 {
+ width: 59%
+}
+
+.column-60 {
+ width: 60%
+}
+
+.column-61 {
+ width: 61%
+}
+
+.column-62 {
+ width: 62%
+}
+
+.column-63 {
+ width: 63%
+}
+
+.column-64 {
+ width: 64%
+}
+
+.column-65 {
+ width: 65%
+}
+
+.column-66 {
+ width: 66%
+}
+
+.column-67 {
+ width: 67%
+}
+
+.column-68 {
+ width: 68%
+}
+
+.column-69 {
+ width: 69%
+}
+
+.column-70 {
+ width: 70%
+}
+
+.column-71 {
+ width: 71%
+}
+
+.column-72 {
+ width: 72%
+}
+
+.column-73 {
+ width: 73%
+}
+
+.column-74 {
+ width: 74%
+}
+
+.column-75 {
+ width: 75%
+}
+
+.column-76 {
+ width: 76%
+}
+
+.column-77 {
+ width: 77%
+}
+
+.column-78 {
+ width: 78%
+}
+
+.column-79 {
+ width: 79%
+}
+
+.column-80 {
+ width: 80%
+}
+
+.column-81 {
+ width: 81%
+}
+
+.column-82 {
+ width: 82%
+}
+
+.column-83 {
+ width: 83%
+}
+
+.column-84 {
+ width: 84%
+}
+
+.column-85 {
+ width: 85%
+}
+
+.column-86 {
+ width: 86%
+}
+
+.column-87 {
+ width: 87%
+}
+
+.column-88 {
+ width: 88%
+}
+
+.column-89 {
+ width: 89%
+}
+
+.column-90 {
+ width: 90%
+}
+
+.column-91 {
+ width: 91%
+}
+
+.column-92 {
+ width: 92%
+}
+
+.column-93 {
+ width: 93%
+}
+
+.column-94 {
+ width: 94%
+}
+
+.column-95 {
+ width: 95%
+}
+
+.column-96 {
+ width: 96%
+}
+
+.column-97 {
+ width: 97%
+}
+
+.column-98 {
+ width: 98%
+}
+
+.column-99 {
+ width: 99%
+}
+
+.column-100 {
+ width: 100%
+}
+
+.draggable-row-handle {
+ cursor: move;
+ color: #dedede
+}
+
+.draggable-row-handle:hover {
+ color: #333
+}
+
+tr.draggable-item-selected {
+ background: #fff;
+ border: 2px solid #666;
+ box-shadow: 4px 2px 10px -4px rgba(0, 0, 0, 0.55)
+}
+
+tr.draggable-item-selected td {
+ border-top: none;
+ border-bottom: none
+}
+
+tr.draggable-item-selected td:first-child {
+ border-left: none
+}
+
+tr.draggable-item-selected td:last-child {
+ border-right: none
+}
+
+.table-stripped tr.draggable-item-hover,
+.table-stripped tr.draggable-item-hover {
+ background: #FEFFF2
+}
+
+.table-list {
+ font-size: 0.85em;
+ margin-bottom: 20px
+}
+
+.table-list-header {
+ background: #f6f8fa;
+ border: 1px solid #d1d5da;
+ border-radius: 3px 3px 0px 0px;
+ line-height: 28px;
+ padding-left: 7px;
+ padding-right: 7px;
+}
+
+.table-list-header a {
+ color: #333;
+ font-weight: 500;
+ text-decoration: none;
+ margin-right: 10px
+}
+
+.table-list-header a:hover,
+.table-list-header a:focus {
+ color: #767676
+}
+
+.table-list-header .table-list-header-count {
+ color: #767676;
+ display: inline-block;
+ float: left
+}
+
+.table-list-header .table-list-header-menu {
+ text-align: right
+}
+
+.table-list-row {
+ padding-left: 7px;
+ padding-right: 7px;
+ border-bottom: 1px solid #e5e5e5;
+ border-right: 1px solid #e5e5e5
+}
+
+.table-list-row.table-border-left {
+ border-left: 1px solid #e5e5e5
+}
+
+.table-list-row:nth-child(odd) {
+ background: #fefefe
+}
+
+.table-list-row:last-child {
+ border-radius: 0 0 5px 5px
+}
+
+.table-list-row:hover {
+ background: #f6f8fa;
+ border-bottom: 1px solid #f6f8fa;
+ border-right: 1px solid #f6f8fa
+}
+
+.table-list-row .table-list-title {
+ font-weight: 500;
+ line-height: 23px
+}
+
+.table-list-row .table-list-title.status-closed {
+ text-decoration: line-through;
+ margin-right: 10px
+}
+
+.table-list-row .table-list-title.status-closed a {
+ font-style: italic
+}
+
+.table-list-row .table-list-title a {
+ color: #333;
+ text-decoration: none
+}
+
+.table-list-row .table-list-title a:hover,
+.table-list-row .table-list-title a:focus {
+ text-decoration: underline
+}
+
+.table-list-row .table-list-details {
+ color: #999;
+ font-weight: 300;
+ line-height: 20px
+}
+
+.table-list-row .table-list-details span {
+ margin-left: 5px
+}
+
+.table-list-row .table-list-details span:first-child {
+ margin-left: 0
+}
+
+.table-list-row .table-list-details li {
+ display: inline;
+ list-style-type: none
+}
+
+.table-list-row .table-list-details li:after {
+ content: ', '
+}
+
+.table-list-row .table-list-details li:last-child:after {
+ content: ''
+}
+
+.table-list-row .table-list-details strong {
+ font-weight: 400;
+ color: #555
+}
+
+.table-list-row .table-list-details-with-icons {
+ float: left
+}
+
+@media (max-width: 768px) {
+ .table-list-row .table-list-details-with-icons {
+ float: none
+ }
+}
+
+.table-list-row .table-list-icons {
+ font-size: 0.8em;
+ text-align: right;
+ line-height: 30px
+}
+
+@media (max-width: 768px) {
+ .table-list-row .table-list-icons {
+ text-align: left;
+ line-height: 20px
+ }
+}
+
+.table-list-row .table-list-icons span {
+ margin-left: 5px
+}
+
+.table-list-row .table-list-icons a {
+ text-decoration: none
+}
+
+.table-list-row .table-list-icons a:hover {
+ color: #333
+}
+
+.table-list-row .table-list-icons a:hover i {
+ color: #333
+}
+
+.table-list-category {
+ font-size: 0.9em;
+ font-weight: 500;
+ color: #000;
+ padding: 1px 2px 1px 2px;
+ border-radius: 3px;
+ background: #fcfcfc;
+ border: 1px solid #ccc
+}
+
+.table-list-category a {
+ text-decoration: none;
+ color: #000
+}
+
+.table-list-category a:hover {
+ color: #0366d6
+}
+
+fieldset {
+ border: 1px solid #ddd;
+ margin-top: 10px
+}
+
+legend {
+ font-weight: 500;
+ font-size: 1.2em
+}
+
+label {
+ cursor: pointer;
+ display: block;
+ margin-top: 10px;
+ font-weight: 400
+}
+
+.select2-container--default .select2-selection--multiple .select2-selection__choice {
+ background-color: #f9f9f9;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ cursor: default;
+ float: left;
+ margin-right: 5px;
+ margin-top: 5px;
+ padding: 0 5px;
+}
+
+input[type="number"],
+input[type="date"],
+input[type="email"],
+input[type="password"],
+input[type="text"]:not(.input-addon-field) {
+ color: #999;
+ border: 1px solid #ccc;
+ width: 300px;
+ max-width: 95%;
+ font-size: 1em;
+ height: 25px;
+ border-radius: 3px;
+ padding-bottom: 0;
+ padding-left: 4px;
+ font-family: sans-serif;
+ -webkit-appearance: none;
+ -moz-appearance: none
+}
+
+input[type="number"]::-webkit-input-placeholder,
+input[type="date"]::-webkit-input-placeholder,
+input[type="email"]::-webkit-input-placeholder,
+input[type="password"]::-webkit-input-placeholder,
+input[type="text"]:not(.input-addon-field)::-webkit-input-placeholder {
+ color: #dedede
+}
+
+input[type="number"]::-moz-placeholder,
+input[type="date"]::-moz-placeholder,
+input[type="email"]::-moz-placeholder,
+input[type="password"]::-moz-placeholder,
+input[type="text"]:not(.input-addon-field)::-moz-placeholder {
+ color: #dedede
+}
+
+input[type="number"]:-ms-input-placeholder,
+input[type="date"]:-ms-input-placeholder,
+input[type="email"]:-ms-input-placeholder,
+input[type="password"]:-ms-input-placeholder,
+input[type="text"]:not(.input-addon-field):-ms-input-placeholder {
+ color: #dedede
+}
+
+input[type="number"]:focus,
+input[type="date"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus,
+input[type="text"]:focus {
+ color: #000;
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ box-shadow: 0 0 0 2px rgba(222,222,222,0.25)
+}
+
+input[type="number"] {
+ width: 70px
+}
+
+input[type="text"]:not(.input-addon-field).form-numeric {
+ width: 70px
+}
+
+input[type="text"]:not(.input-addon-field).form-datetime,
+input[type="text"]:not(.input-addon-field).form-date {
+ width: 150px
+}
+
+input[type="text"]:not(.input-addon-field).form-input-large {
+ width: 400px
+}
+
+input[type="text"]:not(.input-addon-field).form-input-small {
+ width: 150px
+}
+
+textarea:focus {
+ color: #000;
+ border-color: rgba(82, 168, 236, 0.8);
+ outline: 0;
+ box-shadow: 0 0 0 2px rgba(222,222,222,0.25)
+}
+
+.text-editor-toolbar {
+ margin-bottom: 10px!important;
+}
+
+textarea {
+ padding: 4px;
+ border: 1px solid #ccc;
+ width: 400px;
+ max-width: 99%;
+ height: 200px;
+ border-radius: 3px;
+ font-family: sans-serif;
+ font-size: 1em
+}
+
+textarea::-webkit-input-placeholder {
+ color: #dedede
+}
+
+textarea::-moz-placeholder {
+ color: #dedede
+}
+
+textarea:-ms-input-placeholder {
+ color: #dedede
+}
+
+select {
+ font-size: 1.0em;
+ max-width: 95%;
+ border: 1px solid #AAA;
+ color: #555;
+ font-size: inherit;
+ overflow: hidden;
+ border-radius: 3px;
+ padding: 5px 10px;
+ background-color: #fff;
+}
+
+select:focus {
+ outline: 0
+}
+
+select[multiple] {
+ width: 300px
+}
+
+.tag-autocomplete {
+ width: 400px
+}
+
+span.select2-container {
+ margin-top: 2px
+}
+
+.form-actions {
+ padding-top: 20px;
+ clear: both
+}
+
+.form-required {
+ color: red;
+ padding-left: 5px;
+ font-weight: bold
+}
+
+@media (max-width: 480px) {
+ .form-required {
+ display: none
+ }
+}
+
+input[type="text"].form-max-width {
+ width: 100%
+}
+
+input.form-error,
+textarea.form-error {
+ border: 2px solid #b94a48
+}
+
+input.form-error:focus,
+textarea.form-error:focus {
+ box-shadow: none;
+ border: 2px solid #b94a48
+}
+
+.form-errors {
+ color: #b94a48;
+ list-style-type: none
+}
+
+ul.form-errors li {
+ margin-left: 0
+}
+
+.form-help {
+ font-size: 0.8em;
+ color: brown;
+ margin-bottom: 15px
+}
+
+.form-inline {
+ padding: 0;
+ margin: 0;
+ border: none
+}
+
+.form-inline label {
+ display: inline;
+ padding-right: 3px
+}
+
+.form-inline input,
+.form-inline select {
+ margin: 0 15px 0 0
+}
+
+.form-inline .form-required {
+ display: none
+}
+
+.form-inline .form-actions {
+ display: inline-block
+}
+
+.form-inline .js-submit-buttons-rendered {
+ display: inline-block
+}
+
+.form-inline-group {
+ display: inline
+}
+
+.form-columns {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-justify-content: flex-start;
+ justify-content: flex-start
+}
+
+.form-columns .form-column {
+ margin-right: 25px;
+ flex-grow: 1
+}
+
+.form-columns fieldset {
+ margin-top: 0
+}
+
+.form-login {
+ max-width: 350px;
+ margin: 5% auto 0
+}
+
+@media (max-width: 480px) {
+ .form-login {
+ margin-left: 5px
+ }
+}
+
+.form-login li {
+ margin-left: 25px;
+ line-height: 25px
+}
+
+.form-login h2 {
+ margin-bottom: 30px;
+ font-weight: bold
+}
+
+.reset-password {
+ margin-top: 20px;
+ margin-bottom: 20px
+}
+
+.reset-password a {
+ color: #999
+}
+
+.input-addon {
+ display: flex
+}
+
+.input-addon-field {
+ flex: 1;
+ font-size: 1em;
+ color: #999;
+ margin: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none
+}
+
+.input-addon-field:first-child {
+ border-radius: 5px 0 0 5px
+}
+
+.input-addon-field:last-child {
+ border-radius: 0 5px 5px 0
+}
+
+.input-addon-item {
+ background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%);
+ color: #666;
+ font: inherit;
+ font-weight: normal;
+}
+
+.input-addon-item:first-child {
+ border-radius: 5px 0 0 5px
+}
+
+.input-addon-item:last-child {
+ border-radius: 0 5px 5px 0
+}
+
+@media (max-width: 480px) {
+ .input-addon-item .dropdown .fa-caret-down {
+ display: none
+ }
+}
+
+.input-addon-field,
+.input-addon-item {
+ border: 1px solid rgba(147, 128, 108, 0.25);
+ padding: 4px 0.75em
+}
+
+.input-addon-field:not(:first-child),
+.input-addon-item:not(:first-child) {
+ border-left: 0
+}
+
+.icon-success {
+ color: #468847
+}
+
+.icon-error {
+ color: #b94a48
+}
+
+.icon-fade-out {
+ opacity: 1;
+ animation: icon-fadeout 5s linear forwards
+}
+
+@keyframes icon-fadeout {
+ 0% {
+ opacity: 1
+ }
+ 100% {
+ opacity: 0
+ }
+}
+
+.alert {
+ padding: 8px 35px 8px 14px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ color: #464646;
+ background-color: #FFEB3B;
+ border: 1px solid #f9df00;
+ border-radius: 4px
+}
+
+.alert-success {
+ color: #468847;
+ background-color: #dff0d8;
+ border-color: #d6e9c6
+}
+
+.alert-error {
+ color: #b94a48;
+ background-color: #f2dede;
+ border-color: #eed3d7
+}
+
+.alert-info {
+ color: #464646;
+ background-color: #92ddff;
+ border-color: #83d8ff;
+}
+
+.alert-normal {
+ color: #333;
+ background-color: #f0f0f0;
+ border-color: #ddd
+}
+
+.alert ul {
+ margin-top: 10px;
+ margin-bottom: 10px
+}
+
+.alert li {
+ margin-left: 25px
+}
+
+.alert-fade-out {
+ text-align: center;
+ position: fixed;
+ bottom: 0;
+ left: 20%;
+ width: 60%;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ margin-bottom: 0;
+ border-width: 1px 0 0;
+ border-radius: 4px 4px 0 0;
+ z-index: 9999;
+ opacity: 1;
+ animation: fadeout 5s linear forwards
+}
+
+@keyframes fadeout {
+ 0% {
+ opacity: 1
+ }
+ 100% {
+ opacity: 0
+ }
+}
+
+a.btn {
+ text-decoration: none
+}
+
+.btn {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ font-size: 1.2em;
+ font-weight: normal;
+ cursor: pointer;
+ display: inline-block;
+ border-radius: 2px;
+ padding: 3px 10px;
+ margin: 0;
+ border: 1px solid #ddd;
+ background: #f5f5f5;
+ color: #333
+}
+
+.btn:hover,
+.btn:focus {
+ background-color: #eee;
+ color: #000;
+ border-color: #eee;
+ border-color: #e5e5e5;
+}
+
+.btn-red {
+ border-color: #b0281a;
+ background: #d14836;
+ color: #fff
+}
+
+.btn-red:hover,
+.btn-red:focus {
+ background-color: #28a745;
+ background-image: linear-gradient(-180deg, #34d058 0%, #28a745 90%);
+ color: #fff;
+ border-color: #28a745;
+}
+
+.btn-blue {
+ border-color: #3079ed;
+ background-image: linear-gradient(-180deg, #4d90fe 0%, #335996 90%);
+ color: #fff
+}
+
+.btn-blue:hover,
+.btn-blue:focus {
+ background-color: #28a745;
+ background-image: linear-gradient(-180deg, #34d058 0%, #28a745 90%);
+ color: #fff;
+ border-color: #28a745;
+}
+
+.btn:disabled {
+ color: #ccc;
+ border-color: #ccc;
+ background: #f7f7f7
+}
+
+.buttons-header {
+ font-size: 0.8em;
+ margin-top: 5px;
+ margin-bottom: 15px
+}
+
+.tooltip i.fa {
+ cursor: pointer
+}
+
+.tooltip .fa-info-circle {
+ color: #999
+}
+
+#tooltip-container {
+ padding: 5px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ box-shadow: 0 6px 12px #aaa;
+ position: absolute;
+ min-width: 350px
+}
+
+#tooltip-container .markdown p {
+ margin-bottom: 0
+}
+
+#tooltip-container .tooltip-large {
+ width: 600px
+}
+
+h2 .dropdown ul {
+ display: none
+}
+
+.dropdown {
+ display: inline;
+ position: relative
+}
+
+.dropdown ul {
+ display: none
+}
+
+.dropdown-smaller {
+ font-size: 0.85em
+}
+
+ul.dropdown-submenu-open {
+ display: block;
+ position: absolute;
+ z-index: 1000;
+ min-width: 285px;
+ list-style: none;
+ margin: 3px 0 0 1px;
+ padding: 6px 0;
+ background-color: #fff;
+ border: 1px solid #b2b2b2;
+ border-radius: 3px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15)
+}
+
+.dropdown-submenu-open li {
+ display: block;
+ margin: 0;
+ padding: 9px 20px;
+ font-size: 0.9em;
+ border-bottom: none;
+ cursor: pointer;
+}
+
+.dropdown-submenu-open li.no-hover {
+ cursor: default
+}
+
+.dropdown-submenu-open li:last-child {
+ border: none
+}
+
+.dropdown-submenu-open li:not(.no-hover):hover {
+ background: #f6f8fa;
+ color: #8d9498
+}
+
+.dropdown-submenu-open li:hover a {
+ color: #262626
+}
+
+.dropdown-submenu-open a {
+ text-decoration: none;
+ color: #333
+}
+
+.dropdown-submenu-open a:focus {
+ text-decoration: underline
+}
+
+.dropdown-menu-link-text,
+.dropdown-menu-link-icon {
+ color: #333;
+ text-decoration: none
+}
+
+.dropdown-menu-link-text:hover {
+ text-decoration: underline
+}
+
+td a.dropdown-menu strong {
+ color: #cb2431
+}
+
+td a.dropdown-menu strong i {
+ color: #cb2431
+}
+
+td a.dropdown-menu i {
+ color: #dedede
+}
+
+td a.dropdown-menu:hover strong {
+ color: #555
+}
+
+td a.dropdown-menu:hover strong i {
+ color: #555
+}
+
+td a.dropdown-menu:hover i {
+ color: #333
+}
+
+.accordion-title {
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NEQ5RDgxQzc2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NEQ5RDgxQzg2RjQ5MTFFMjhEMUNENzFGRUMwRjhBRTciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0RDlEODFDNTZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0RDlEODFDNjZGNDkxMUUyOEQxQ0Q3MUZFQzBGOEFFNyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvXFWFAAAAAYSURBVHjaYvj//z8D0/Pnz/8zgFgAAQYAS5UJscReGMIAAAAASUVORK5CYII=) repeat-x scroll 0 10px
+}
+
+.accordion-title h3 {
+ display: inline;
+ padding-right: 5px;
+ background: #fff
+}
+
+.accordion-content {
+ margin-top: 15px;
+ margin-bottom: 25px
+}
+
+.accordion-toggle {
+ color: #333;
+ text-decoration: none
+}
+
+.accordion-toggle:focus {
+ color: #333
+}
+
+.accordion-toggle:hover {
+ color: #999
+}
+
+.accordion-toggle:before {
+ content: "\f0d7"
+}
+
+.accordion-collapsed {
+ margin-bottom: 25px
+}
+
+.accordion-collapsed .accordion-toggle:before {
+ content: "\f0da"
+}
+
+.accordion-collapsed .accordion-content {
+ display: none
+}
+
+#select-dropdown-menu {
+ position: absolute;
+ display: block;
+ z-index: 1000;
+ min-width: 160px;
+ padding: 5px 0;
+ background: #fff;
+ list-style: none;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ overflow-x: hidden;
+}
+
+.select-dropdown-menu-item {
+ white-space: nowrap;
+ overflow: hidden;
+ padding: 3px 10px;
+ color: #555;
+ cursor: pointer;
+ border-bottom: 1px solid #f8f8f8;
+ line-height: 1.5em;
+ font-weight: 400
+}
+
+.select-dropdown-menu-item.active {
+ color: #fff;
+ background: #0366d6
+}
+
+.select-dropdown-menu-item:last-child {
+ border: none
+}
+
+.select-dropdown-input-container {
+ position: relative;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ background-color: #fff;
+ max-width: 300px;
+}
+
+.select-dropdown-input-container input.select-dropdown-input {
+ margin: 0 0 0 5px;
+ border: none;
+ height: 23px
+}
+
+.select-dropdown-input-container input.select-dropdown-input:focus {
+ border: none;
+ box-shadow: none
+}
+
+.select-dropdown-input-container .select-dropdown-chevron {
+ color: #555;
+ position: absolute;
+ top: 4px;
+ right: 5px;
+ cursor: pointer
+}
+
+.select-dropdown-input-container .select-loading-icon {
+ color: #555;
+ position: absolute;
+ top: 4px;
+ right: 5px
+}
+
+#suggest-menu {
+ position: absolute;
+ display: block;
+ z-index: 1000;
+ min-width: 160px;
+ padding: 5px 0;
+ background: #fff;
+ list-style: none;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175)
+}
+
+.suggest-menu-item {
+ white-space: nowrap;
+ padding: 3px 10px;
+ color: #333;
+ font-weight: bold;
+ cursor: pointer
+}
+
+.suggest-menu-item.active {
+ color: #fff;
+ background: #0366d6
+}
+
+.suggest-menu-item.active small {
+ color: #fff
+}
+
+.suggest-menu-item small {
+ color: #999;
+ font-weight: normal
+}
+
+#modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.9);
+ overflow: auto;
+ z-index: 100
+}
+
+#modal-box {
+ position: fixed;
+ max-height: calc(100% - 30px);
+ top: 0;
+ padding: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #fafbfc;
+ overflow: auto;
+ border-radius: 0px 0px 3px 3px;
+}
+
+#modal-content {
+ padding: 0 5px 5px
+}
+
+#modal-header {
+ text-align: right;
+ padding-right: 5px
+}
+
+#modal-close-button {
+ color: #333
+}
+
+#modal-close-button:hover {
+ color: #b94a48
+}
+
+.pagination {
+ text-align: center;
+ font-size: 0.9em
+}
+
+.pagination-showing {
+ margin-right: 5px;
+ padding-right: 5px;
+ border-right: 1px solid #999
+}
+
+.pagination-next {
+ margin-left: 5px
+}
+
+.pagination-previous {
+ margin-right: 5px
+}
+
+.header img {
+ float: left;
+}
+
+.header h2 {
+ position: relative;
+ color:#fff;
+ top: 16px;
+ left: 10px;
+ margin: 0;
+}
+
+header {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 5px 10px;
+ margin-bottom: 5px;
+ border-bottom: 0px solid #dedede;
+ padding: 15px 10px;
+ margin-bottom: 15px;
+ background: #24292e;
+}
+
+header .title-container {
+ flex: 1;
+ min-width: 300px
+}
+
+@media (max-width: 480px) {
+ header .title-container {
+ order: 3
+ }
+}
+
+header .board-selector-container {
+ min-width: 320px;
+ display: flex;
+ align-items: center
+}
+
+@media (max-width: 480px) {
+ header .board-selector-container {
+ order: 2;
+ min-width: 300px
+ }
+ header .board-selector-container input[type=text] {
+ max-width: 280px
+ }
+}
+
+header .menus-container {
+ min-width: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end
+}
+
+@media (max-width: 480px) {
+ header .menus-container {
+ order: 1;
+ margin-bottom: 5px;
+ margin-left: auto
+ }
+}
+
+header h1 {
+ font-size: 1.4em!important;
+ color: #dddddd!important
+}
+
+header h1 .tooltip {
+ opacity: 0.3;
+ font-size: 0.7em
+}
+
+a i.web-notification-icon {
+ color: #0366d6
+}
+
+a i.web-notification-icon:focus,
+a i.web-notification-icon:hover {
+ color: #000
+}
+
+.logo a {
+ opacity: 0.5;
+ color: #d40000;
+ text-decoration: none
+}
+
+.logo span {
+ color: #333
+}
+
+.logo a:hover {
+ opacity: 0.8;
+ color: #333
+}
+
+.logo a:focus span,
+.logo a:hover span {
+ color: #d40000
+}
+
+.page-header {
+ margin-bottom: 20px;
+ padding: 10px;
+ background-color: #fafbfc;
+ border-radius: 3px;
+}
+
+.page-header .dropdown {
+ padding-right: 10px
+}
+
+.page-header h2 {
+ margin: 0;
+ padding: 0;
+ font-weight: bold;
+ border-bottom: none;
+}
+
+.page-header h2 a {
+ color: #333;
+ text-decoration: none
+}
+
+.page-header h2 a:focus,
+.page-header h2 a:hover {
+ color: #999
+}
+
+.page-header ul {
+ text-align: left;
+ margin-top: 5px;
+ display: inline-block
+}
+
+.page-header li {
+ display: inline;
+ padding-right: 15px
+}
+
+@media (max-width: 480px) {
+ .page-header li {
+ display: block;
+ line-height: 1.5em
+ }
+}
+
+.page-header li.active a {
+ color: #333;
+ text-decoration: none;
+ font-weight: bold
+}
+
+.page-header li.active a:hover,
+.page-header li.active a:focus {
+ text-decoration: underline
+}
+
+.menu-inline {
+ margin-bottom: 5px
+}
+
+.menu-inline li {
+ display: inline;
+ padding-right: 15px
+}
+
+.menu-inline li .active a {
+ font-weight: bold;
+ color: #000;
+ text-decoration: none
+}
+
+.sidebar-container {
+ height: 100%;
+ display: flex;
+ flex-flow: row
+}
+
+@media (max-width: 768px) {
+ .sidebar-container {
+ flex-flow: wrap
+ }
+}
+
+.sidebar-content {
+ padding-left: 10px;
+ flex: 1 100%;
+ max-width: 85%;
+ overflow-wrap: break-word
+}
+
+@media (max-width: 768px) {
+ .sidebar-content {
+ padding-left: 0;
+ order: 1;
+ max-width: 100%
+ }
+}
+
+@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1) {
+ .sidebar-content {
+ max-width: 75%
+ }
+}
+
+.sidebar {
+ max-width: 25%;
+ border: 1px solid #d1d5da;
+ min-width: 230px;
+ padding: 8px 10px;
+ border-radius: 3px;
+ background: #fafbfc;
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ flex: 1 auto;
+ order: 2
+ }
+}
+
+.sidebar h2 {
+ margin-top: 0
+}
+
+.sidebar>ul a {
+ text-decoration: none;
+ color: #586069;
+ font-weight: 500
+}
+
+.sidebar>ul a:hover {
+ color: #23292d
+}
+
+.sidebar>ul li {
+ list-style-type: none;
+ line-height: 35px;
+ border-bottom: 1px dotted #e5e5e5;
+ padding-left: 13px
+}
+
+.sidebar>ul li:hover {
+ border-left: 5px solid #555;
+ padding-left: 8px
+}
+
+.sidebar>ul li.active {
+ border-left: 5px solid #23292d;
+ padding-left: 8px
+}
+
+.sidebar>ul li.active a {
+ color: #23292d;
+ font-weight: bold
+}
+
+.sidebar-icons>ul li {
+ padding-left: 0
+}
+
+.sidebar-icons>ul li:hover,
+.sidebar-icons>ul li.active {
+ padding-left: 0;
+ border-left: none
+}
+
+.sidebar>ul li.active a:focus,
+.sidebar>ul li.active a:hover {
+ color: #555
+}
+
+.sidebar>ul li:last-child {
+ margin-bottom: 15px
+}
+
+.avatar img {
+ vertical-align: bottom
+}
+
+.avatar-left {
+ float: left;
+ margin-right: 10px
+}
+
+.avatar-inline {
+ display: inline-block;
+ margin-right: 3px
+}
+
+.avatar-48 img,
+.avatar-48 div {
+ border-radius: 30px
+}
+
+.avatar-48 .avatar-letter {
+ line-height: 48px;
+ width: 48px;
+ font-size: 25px
+}
+
+.avatar-20 img,
+.avatar-20 div {
+ border-radius: 10px
+}
+
+.avatar-20 .avatar-letter {
+ line-height: 20px;
+ width: 20px;
+ font-size: 11px
+}
+
+.avatar-letter {
+ color: #fff;
+ text-align: center
+}
+
+#file-dropzone,
+#screenshot-zone {
+ position: relative;
+ border: 2px dashed #ccc;
+ width: 99%;
+ height: 250px;
+ overflow: auto
+}
+
+#file-dropzone-inner,
+#screenshot-inner {
+ position: absolute;
+ left: 0;
+ bottom: 48%;
+ width: 100%;
+ text-align: center;
+ color: #aaa
+}
+
+#screenshot-zone.screenshot-pasted {
+ border: 2px solid #333
+}
+
+#file-list {
+ margin: 20px
+}
+
+#file-list li {
+ list-style-type: none;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px dotted #ddd;
+ width: 95%
+}
+
+#file-list li .file-error {
+ font-weight: bold;
+ color: #b94a48
+}
+
+.file-thumbnails {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-justify-content: flex-start;
+ justify-content: flex-start
+}
+
+.file-thumbnail {
+ width: 250px;
+ border: 1px solid #efefef;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ box-shadow: 4px 2px 10px -6px rgba(0, 0, 0, 0.55);
+ margin-right: 15px
+}
+
+.file-thumbnail img {
+ cursor: pointer;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px
+}
+
+.file-thumbnail img:hover {
+ opacity: 0.5
+}
+
+.file-thumbnail-content {
+ padding-left: 8px;
+ padding-right: 8px
+}
+
+.file-thumbnail-title {
+ font-weight: 700;
+ font-size: 0.9em;
+ color: #555;
+ overflow: hidden;
+ text-overflow: ellipsis
+}
+
+.file-thumbnail-description {
+ font-size: 0.8em;
+ color: #999;
+ margin-top: 8px;
+ margin-bottom: 5px
+}
+
+.file-viewer {
+ position: relative
+}
+
+.file-viewer img {
+ max-width: 95%;
+ max-height: 85%;
+ margin-top: 10px
+}
+
+.color-picker {
+ width: 180px
+}
+
+.color-picker-option {
+ height: 25px
+}
+
+.color-picker-square {
+ display: inline-block;
+ width: 18px;
+ height: 18px;
+ margin-right: 5px;
+ border: 1px solid #000
+}
+
+.color-picker-label {
+ display: inline-block;
+ vertical-align: bottom;
+ padding-bottom: 3px
+}
+
+.filter-box {
+ max-width: 1024px
+}
+
+.action-menu {
+ color: #333;
+ text-decoration: none
+}
+
+.action-menu:hover,
+.action-menu:focus {
+ text-decoration: underline
+}
+
+.js-project-creation-options {
+ max-width: 500px;
+ border-left: 3px solid #efefef;
+ margin-top: 20px;
+ padding-left: 15px;
+ padding-bottom: 5px;
+ padding-top: 5px
+}
+
+.project-overview-columns {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ -webkit-align-items: center;
+ align-items: center;
+ -webkit-justify-content: center;
+ justify-content: center;
+ margin-bottom: 20px;
+ font-size: 1.4em
+}
+
+@media (max-width: 480px) {
+ .project-overview-columns {
+ display: block
+ }
+}
+
+.project-overview-column {
+ text-align: center;
+ margin-right: 3%;
+ margin-top: 5px;
+ padding: 3px 15px 3px 15px;
+ border: 1px dashed #ddd
+}
+
+@media (max-width: 480px) {
+ .project-overview-column {
+ text-align: left
+ }
+}
+
+.project-overview-column small {
+ color: #999
+}
+
+.project-overview-column strong {
+ color: #555;
+ display: block
+}
+
+@media (max-width: 480px) {
+ .project-overview-column strong {
+ display: inline
+ }
+}
+
+.project-header {
+ margin-bottom: 8px
+}
+
+.project-header .dropdown-component {
+ margin-top: 4px;
+ margin-right: 5px;
+ float: left
+}
+
+@media (max-width: 768px) {
+ .project-header .dropdown-component {
+ float: none
+ }
+}
+
+.project-header .views-switcher-component {
+ margin-top: 4px;
+ float: left
+}
+
+@media (max-width: 768px) {
+ .project-header .views-switcher-component {
+ float: none;
+ margin-bottom: 10px
+ }
+}
+
+.project-header .filter-box-component form {
+ margin: 0
+}
+
+.views {
+ margin-right: 10px;
+ margin-top: 1px;
+ font-size: 0.9em
+}
+
+@media (max-width: 560px) {
+ .views {
+ width: 100%
+ }
+}
+
+@media (max-width: 768px) {
+ .views {
+ margin-top: 10px;
+ font-size: 1em
+ }
+}
+
+@media (max-width: 480px) {
+ .views {
+ margin-top: 5px
+ }
+}
+
+.views li {
+ white-space: nowrap;
+ background-color: #e6ebf1;
+ background-image: linear-gradient(-180deg, #f0f3f6 0%, #e6ebf1 90%);
+ background-position: -.5em;
+ border-color: rgba(27,31,35,0.35);
+ border: 1px solid #ddd;
+ border-right: none;
+ padding: 4px 8px;
+ display: inline;
+}
+
+.views li:hover {
+ white-space: nowrap;
+ background-image: linear-gradient(-180deg, #fafbfc 0%, #eff3f6 90%);
+ border: 1px solid #ddd;
+ border-right: none;
+ padding: 4px 8px;
+ display: inline;
+}
+
+@media (max-width: 560px) {
+ .views li {
+ display: block;
+ margin-top: 5px;
+ border-radius: 5px;
+ border: 1px solid #ddd
+ }
+}
+
+.views li.active a {
+ font-weight: bold;
+ color: #000;
+ text-decoration: none
+}
+
+.views li:first-child {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px
+}
+
+.views li:last-child {
+ border-right: 1px solid #ddd;
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px
+}
+
+.views a {
+ color: #555;
+ text-decoration: none
+}
+
+.views a:hover {
+ color: #333;
+ text-decoration: none;
+}
+
+.dashboard-project-stats small {
+ margin-right: 10px;
+ color: #999
+}
+
+.dashboard-table-link {
+ font-weight: bold;
+ color: #000;
+ text-decoration: none
+}
+
+.dashboard-table-link:focus,
+.dashboard-table-link:hover {
+ color: #999
+}
+
+.public-board {
+ margin-top: 5px
+}
+
+.public-task {
+ max-width: 800px;
+ margin: 5px auto 0
+}
+
+#board-container {
+ overflow-x: auto
+}
+
+#board {
+ table-layout: fixed;
+ margin-bottom: 0
+}
+
+#board th.board-column-header {
+ width: 240px
+}
+
+#board td {
+ vertical-align: top
+}
+
+.board-container-compact {
+ overflow-x: initial
+}
+
+@media all and (-ms-high-contrast: active),
+(-ms-high-contrast: none) {
+ .board-container-compact #board {
+ table-layout: auto
+ }
+}
+
+#board th.board-column-header.board-column-compact {
+ width: initial
+}
+
+.board-column-collapsed {
+ display: none
+}
+
+td.board-column-task-collapsed {
+ font-weight: bold;
+ background-color: #f6f8fa
+}
+
+#board th.board-column-header-collapsed {
+ width: 28px;
+ min-width: 28px;
+ text-align: center;
+ overflow: hidden
+}
+
+.board-rotation-wrapper {
+ position: relative;
+ padding: 8px 4px;
+ min-height: 150px;
+ overflow: hidden
+}
+
+.board-rotation {
+ white-space: nowrap;
+ -webkit-backface-visibility: hidden;
+ -webkit-transform: rotate(90deg);
+ -moz-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-transform-origin: 0 100%;
+ -moz-transform-origin: 0 100%;
+ -ms-transform-origin: 0 100%;
+ transform-origin: 0 100%
+}
+
+.board-column-title .dropdown-menu {
+ text-decoration: none
+}
+
+.board-add-icon {
+ float: left;
+ padding: 0 5px
+}
+
+.board-add-icon i {
+ text-decoration: none;
+ color: #279f43;
+ font-size: 1.4em
+}
+
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #23292d
+}
+
+.board-column-header-task-count {
+ color: #999;
+ font-weight: normal
+}
+
+a.board-swimlane-toggle {
+ text-decoration: none
+}
+
+a.board-swimlane-toggle:hover,
+a.board-swimlane-toggle:focus {
+ color: #000;
+ text-decoration: none;
+ border: none
+}
+
+.board-task-list {
+ min-height: 60px
+}
+
+.board-task-list .task-board:last-child {
+ margin-bottom: 0
+}
+
+.board-task-list-limit {
+ background-color: #DF5353
+}
+
+.draggable-item {
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none
+}
+
+.draggable-placeholder {
+ border: 2px dashed #000;
+ background: #fafafa;
+ height: 70px;
+ margin-bottom: 10px
+}
+
+div.draggable-item-selected {
+ border: 1px solid #000
+}
+
+.task-board-sort-handle {
+ float: left;
+ padding-right: 5px
+}
+
+.task-board {
+ margin-bottom: 8px;
+ padding: 12px;
+ border-radius: 3px;
+ box-shadow: 0px 1px 1px 0 rgba(46, 61, 73, 0.30);
+ border-bottom: none;
+ border-left: none;
+ border-right: 2px solid;
+ border-top: none;
+ background-color: #f9f9f9!important;
+}
+
+div.task-board-recent {
+ border-width: 2px
+}
+
+div.task-board-status-closed {
+ user-select: none;
+ border: 1px dotted #555
+}
+
+.task-board a {
+ color: #0366d6;
+ font-weight: 600;
+ text-decoration: none
+}
+
+.task-board-collapsed {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis
+}
+
+.task-board-title {
+ margin-top: 5px;
+ margin-bottom: 8px
+}
+
+.task-board-title a:hover {
+ text-decoration: underline
+}
+
+.task-board-saving-state {
+ opacity: 0.3
+}
+
+.task-board-saving-icon {
+ position: absolute;
+ margin: auto;
+ width: 100%;
+ text-align: center;
+ color: #000
+}
+
+.task-board-avatars {
+ text-align: right;
+ float: right
+}
+
+.task-board-change-assignee {
+ cursor: pointer
+}
+
+.task-board-change-assignee:hover {
+ opacity: 0.6
+}
+
+.task-list-avatars {
+ display: inline-block;
+ float: left
+}
+
+@media (max-width: 768px) {
+ .task-list-avatars {
+ float: none;
+ display: block
+ }
+}
+
+.task-list-avatars .task-avatar-assignee {
+ font-weight: 300;
+ color: #999
+}
+
+.task-list-avatars:hover .task-avatar-assignee {
+ font-weight: 400;
+ color: #000
+}
+
+.task-board-icons,
+.task-list-icons {
+ font-size: 0.8em;
+ text-align: right
+}
+
+.task-board-icons a,
+.task-list-icons a {
+ text-decoration: none
+}
+
+.task-board-icons a:hover,
+.task-list-icons a:hover {
+ color: #333
+}
+
+.task-board-icons a:hover i,
+.task-list-icons a:hover i {
+ color: #333
+}
+
+.task-board-icons .task-score,
+.task-list-icons .task-score {
+ font-weight: bold
+}
+
+.task-board-icons .flag-milestone,
+.task-list-icons .flag-milestone {
+ color: green
+}
+
+.task-board-icons {
+ margin-top: 7px
+}
+
+.task-board-icons a {
+ opacity: 0.5
+}
+
+.task-board-icons span {
+ opacity: 0.5;
+ color: #1e252a;
+ margin-left: 4px
+}
+
+.task-board-icons a:hover {
+ opacity: 1.0;
+ font-weight: bold
+}
+
+.task-board-icons .task-board-icons-row {
+ line-height: 22px
+}
+
+.task-list-icons {
+ line-height: 22px
+}
+
+.task-list-icons a,
+.task-list-icons span,
+.task-list-icons i {
+ color: #999;
+ opacity: 1.0
+}
+
+.task-list-icons span {
+ margin-left: 5px
+}
+
+@media (max-width: 768px) {
+ .task-list-icons {
+ text-align: left
+ }
+}
+
+.task-icon-age {
+ display: inline-block
+}
+
+span.task-icon-age-total {
+ border: 1px solid #e5e5e5;
+ padding: 1px 3px 1px 3px;
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px
+}
+
+span.task-icon-age-column {
+ border: 1px solid #e5e5e5;
+ border-left: none;
+ margin-left: -5px;
+ padding: 1px 3px 1px 3px;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px
+}
+
+.task-board span.task-icon-age-total,
+.task-board span.task-icon-age-column {
+ border-color: #666
+}
+
+.task-board-category-container {
+ text-align: right;
+ margin-top: 8px;
+ margin-bottom: 8px
+}
+
+.task-board-category {
+ border: 1px solid #555;
+ font-size: 0.9em;
+ font-weight: 500;
+ color: #000;
+ padding: 1px 3px 1px 2px;
+ border-radius: 3px
+}
+
+.task-board-category a:hover {
+ text-decoration: underline
+}
+
+.task-date {
+ font-weight: 500;
+ color: #000
+}
+
+span.task-date-today {
+ opacity: 1.0;
+ color: #0366d6
+}
+
+span.task-date-overdue {
+ opacity: 1.0;
+ color: #b94a48
+}
+
+.task-tags li {
+ display: inline-block;
+ margin: 3px 3px 0 0;
+ padding: 1px 3px 1px 3px;
+ color: #333;
+ border: 1px solid #333;
+ border-radius: 4px
+}
+
+.task-summary-container .task-tags {
+ margin-top: 10px
+}
+
+.task-list-tag {
+ padding: 0.1em 0.9em;
+ margin: 0 0.5em 0.5em 0;
+ white-space: nowrap;
+ background-color: #f9f9f9;
+ border-radius: 3px;
+}
+
+#task-summary {
+ margin-bottom: 15px
+}
+
+#task-summary h2 {
+ color: #555;
+ font-size: 1.6em;
+ margin-top: 0;
+ padding-top: 0
+}
+
+.task-summary-container {
+ border: none;
+ border-radius: 3px;
+ border-bottom: 5px solid;
+ padding: 20px;
+}
+
+.task-summary-columns {
+ display: flex;
+ flex-flow: row;
+ justify-content: space-between
+}
+
+@media (max-width: 768px) {
+ .task-summary-columns {
+ flex-flow: column
+ }
+}
+
+.task-summary-column {
+ color: #333
+}
+
+.task-summary-column span {
+ color: #555
+}
+
+.task-summary-column li {
+ line-height: 23px
+}
+
+#external-task-view {
+ padding: 10px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border: 1px dotted #ccc
+}
+
+.task-form-container {
+ box-sizing: border-box;
+ display: flex;
+ flex-wrap: wrap
+}
+
+.task-form-container>* {
+ box-sizing: border-box
+}
+
+.task-form-container>* {
+ width: 1%
+}
+
+.task-form-main-column {
+ width: 60%
+}
+
+@media (max-width: 1000px) {
+ .task-form-main-column {
+ width: 100%
+ }
+}
+
+.task-form-main-column input[type="text"] {
+ width: 700px;
+ max-width: 99%
+}
+
+.task-form-secondary-column {
+ max-width: 250px;
+ min-width: 200px;
+ max-height: 600px;
+ padding-left: 10px;
+ overflow: auto;
+ width: 20%
+}
+
+@media (max-width: 1000px) {
+ .task-form-secondary-column {
+ width: 100%;
+ max-width: 99%;
+ max-height: none
+ }
+}
+
+@media (max-width: 768px) {
+ .task-form-secondary-column {
+ padding-left: 0
+ }
+}
+
+.task-form-secondary-column label:first-child {
+ margin-top: 0
+}
+
+@media (max-width: 1000px) {
+ .task-form-secondary-column label:first-child {
+ margin-top: 10px
+ }
+}
+
+.task-form-bottom {
+ width: 100%
+}
+
+.comment-sorting {
+ text-align: right
+}
+
+.comment-sorting a {
+ color: #555;
+ font-weight: normal;
+ text-decoration: none
+}
+
+.comment-sorting a:hover {
+ color: #999
+}
+
+.comment {
+ padding: 5px;
+ margin-bottom: 15px
+}
+
+.comment-title {
+ border-bottom: 1px dotted #eee;
+ margin-left: 55px
+}
+
+.comment-date {
+ color: #999;
+ font-weight: 200
+}
+
+.comment-actions {
+ text-align: right
+}
+
+.comment-content {
+ margin-left: 55px
+}
+
+.comments .text-editor textarea {
+ height: 90px
+}
+
+.comments .text-editor .text-editor-preview-area {
+ height: 90px
+}
+
+.comments .comment-highlighted {
+ background-color: #f6f8fa;
+ border: 2px solid #ffeb8e
+}
+
+.comments .comment-highlighted:hover {
+ background-color: #f6f8fa
+}
+
+.comments .comment:hover {
+ background: #f6f8fa
+}
+
+.comments .comment:nth-child(even):not(.comment-highlighted) {
+ background: #f6f8fa
+}
+
+.comments .comment:nth-child(even):not(.comment-highlighted):hover {
+ background: #f6f8fa
+}
+
+.subtask-cell {
+ padding: 4px 10px;
+ border-top: 1px dotted #dedede;
+ border-left: 1px dotted #dedede;
+ display: table-cell;
+ vertical-align: middle
+}
+
+.subtask-cell a {
+ color: #333;
+ text-decoration: none
+}
+
+.subtask-cell a:hover,
+.subtask-cell a:focus {
+ color: #0366d6
+}
+
+.subtask-cell:first-child {
+ border-left: none
+}
+
+@media (max-width: 768px) {
+ .subtask-cell {
+ width: 90%;
+ display: block;
+ border-left: none
+ }
+}
+
+.task-list-subtasks {
+ display: table;
+ width: 100%
+}
+
+@media (max-width: 768px) {
+ .task-list-subtasks {
+ display: block
+ }
+}
+
+.task-list-subtask {
+ display: table-row
+}
+
+@media (max-width: 768px) {
+ .task-list-subtask {
+ display: block
+ }
+}
+
+@media (max-width: 768px) {
+ .subtask-assignee,
+ .subtask-time-tracking-cell {
+ display: none
+ }
+}
+
+.task-links-table td {
+ vertical-align: middle
+}
+
+.task-links-task-count {
+ color: #999;
+ font-weight: normal
+}
+
+.task-link-closed {
+ text-decoration: line-through
+}
+
+.text-editor {
+ margin-top: 10px
+}
+
+.text-editor a {
+ font-size: 1em;
+ color: #999;
+ text-decoration: none;
+ margin-right: 10px
+}
+
+.text-editor a:hover {
+ color: #0366d6
+}
+
+.text-editor .text-editor-preview-area {
+ border: none;
+ width: 700px;
+ max-width: 99%;
+ height: 250px;
+ overflow: auto;
+ padding: 2px
+}
+
+.text-editor textarea {
+ width: 700px;
+ max-width: 98%;
+ height: 250px;
+ padding: 10px;
+}
+
+.markdown {
+ line-height: 1.4em
+}
+
+.markdown h1 {
+ margin-top: 5px;
+ margin-bottom: 10px;
+ font-weight: bold
+}
+
+.markdown h2 {
+ font-weight: bold
+}
+
+.markdown p {
+ margin-bottom: 10px
+}
+
+.markdown ol,
+.markdown ul {
+ margin-left: 25px;
+ margin-top: 10px;
+ margin-bottom: 10px
+}
+
+.markdown pre {
+ background: #3333330d;
+ padding: 10px;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ overflow: auto;
+ overflow-wrap: initial;
+ color: #555
+}
+
+.markdown blockquote {
+ font-style: italic;
+ border-left: 3px solid #ddd;
+ padding-left: 10px;
+ margin-bottom: 10px;
+ margin-left: 20px
+}
+
+.markdown img {
+ display: block;
+ max-width: 80%;
+ margin-top: 10px
+}
+
+.documentation {
+ margin: 0 auto;
+ padding: 20px;
+ max-width: 850px;
+ background: #fefefe;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ color: #555
+}
+
+.documentation img {
+ border: 1px solid #333
+}
+
+.documentation h1 {
+ text-decoration: none;
+ margin-bottom: 30px
+}
+
+.documentation h2 {
+ text-decoration: none;
+ border-bottom: 1px solid #ccc;
+ margin-bottom: 25px
+}
+
+.documentation li {
+ line-height: 30px
+}
+
+.panel {
+ border-radius: 4px;
+ padding: 8px 35px 8px 10px;
+ margin-top: 10px;
+ margin-bottom: 15px;
+ border: 1px solid #ddd;
+ color: #333;
+ background-color: #fcfcfc;
+ overflow: auto
+}
+
+.panel li {
+ list-style-type: square;
+ margin-left: 20px;
+ line-height: 1.35em
+}
+
+.activity-event {
+ margin-bottom: 15px;
+ padding: 10px
+}
+
+.activity-event:nth-child(even) {
+ background: #fafafa
+}
+
+.activity-event:hover {
+ background: #f6f8fa
+}
+
+.activity-date {
+ margin-left: 10px;
+ font-weight: normal;
+ color: #999
+}
+
+.activity-content {
+ margin-left: 55px
+}
+
+.activity-title {
+ font-weight: bold;
+ color: #000;
+ border-bottom: 1px dotted #efefef
+}
+
+.activity-description {
+ color: #555;
+ margin-top: 10px
+}
+
+@media (max-width: 480px) {
+ .activity-description {
+ overflow: auto
+ }
+}
+
+.activity-description li {
+ list-style-type: circle
+}
+
+.activity-description ul {
+ margin-top: 10px;
+ margin-left: 20px
+}
+
+.user-mention-link {
+ font-weight: bold;
+ color: #000;
+ text-decoration: none
+}
+
+.user-mention-link:hover {
+ color: #555
+}
+
+.image-slideshow-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.95);
+ overflow: auto;
+ z-index: 100
+}
+
+.image-slideshow-overlay img {
+ display: block;
+ margin: auto
+}
+
+.image-slideshow-overlay figcaption {
+ color: #fff;
+ opacity: 0.7;
+ position: absolute;
+ bottom: 5px;
+ right: 15px
+}
+
+.slideshow-icon {
+ color: #fff;
+ position: absolute;
+ font-size: 2.5em;
+ opacity: 0.6
+}
+
+.slideshow-icon:hover {
+ opacity: 0.9;
+ cursor: pointer
+}
+
+.slideshow-previous-icon {
+ left: 10px;
+ top: 45%
+}
+
+.slideshow-next-icon {
+ right: 10px;
+ top: 45%
+}
+
+.slideshow-close-icon {
+ right: 10px;
+ top: 10px;
+ font-size: 1.4em
+}
+
+.slideshow-download-icon {
+ left: 10px;
+ bottom: 10px;
+ font-size: 1.3em
+}
+
+.list-item-links,
+.list-item-actions {
+ display: inline-block;
+ float: left;
+ margin-left: 10px
+}
+
+.list-item-links a {
+ margin: 0
+}
+
+.list-item-action-hidden {
+ display: none
+}
+
+.bulk-change-checkbox {
+ float: left
+}
+
+.bulk-change-inputs {
+ float: left;
+ padding-left: 10px
+}
+
+.bulk-change-inputs label {
+ margin-top: 0;
+ margin-bottom: 3px
+}
+
+#to-top {
+ margin: 0 0px 30px 0px;
+}
+
+#backToTop {
+ position: fixed;
+ left: 50%;
+ bottom: -25px;
+ display: block;
+ width: 50px;
+ height: 50px;
+ z-index: 10000;
+ transition: all 0.4s;
+ -webkit-transition: all 0.4s;
+ -moz-transition: all 0.4s;
+ -o-transition: all 0.4s;
+ -ms-transition: all 0.4s;
+}
+
+#backToTop.topshow {
+ bottom: 0;
+}
+
+#backToTop span {
+ position: absolute;
+ left: -25px;
+ bottom: -25px;
+ display: block;
+ width: 50px;
+ height: 50px;
+ background: rgba(50, 50, 50, 0.15);
+ text-decoration: none;
+ -webkit-border-radius: 35px;
+ -moz-border-radius: 35px;
+ border-radius: 35px;
+ transition: all 0.4s;
+ -webkit-transition: all 0.4s;
+ -moz-transition: all 0.4s;
+ -o-transition: all 0.4s;
+ -ms-transition: all 0.4s;
+}
+
+#backToTop span:hover {
+ bottom: 5px;
+ background: rgba(70, 70, 70, 0.9);
+}
+
+#backToTop i:before {
+ position: relative;
+ left: 12px;
+ top: 0;
+ display: block;
+ margin: 0;
+ width: 50px;
+ color: #fff;
+ font-size: 27px;
+ -webkit-transition: all 0.6s ease;
+ -moz-transition: all 0.6s ease;
+ -ms-transition: all 0.6s ease;
+ -o-transition: all 0.6s ease;
+ transition: all 0.6s ease;
+}
+
+#backToTop:hover i:before {
+ top: 9px;
+}
diff --git a/plugins/Customizer/Assets/css/themes/KindaDark.css b/plugins/Customizer/Assets/css/themes/KindaDark.css
new file mode 100644
index 00000000..542c69f8
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/KindaDark.css
@@ -0,0 +1,685 @@
+/*
+ * Modified version of "Breathe" Theme for Kanboard
+ * Licensed under the MIT license - Oxygen/LICENSE
+ * - https://github.com/kenlog/Oxygen
+ * Copyright (c) 2019 Samm Du [https://sammdu.com]
+ */
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+input,
+textarea {
+ background-color: #fff;
+ color: #000;
+}
+
+::-webkit-input-placeholder {
+ /* Chrome/Opera/Safari */
+ color: #3c3c3c;
+}
+
+::-moz-placeholder {
+ /* Firefox 19+ */
+ color: #3c3c3c;
+}
+
+:-ms-input-placeholder {
+ /* IE 10+ */
+ color: #3c3c3c;
+}
+
+:-moz-placeholder {
+ /* Firefox 18- */
+ color: #3c3c3c;
+}
+
+::placeholder {
+ color: #3c3c3c;
+}
+
+h1 {
+ color: #fff;
+}
+
+a {
+ color: #2c52e3;
+}
+
+.js-modal-medium {
+ color: #2c52e3;
+}
+
+.js-modal-large {
+}
+
+.page-header ul li .fa {
+ color: #2c52e3;
+}
+
+.page-header ul li a {
+ text-decoration: none;
+}
+
+#modal-overlay .fa {
+ color: #222427;
+}
+
+a .fa {
+ color: #fff;
+}
+
+.project-header * {
+ border-radius: 0 !important;
+}
+
+.project-header li .fa {
+ color: #222427;
+}
+
+.table-list-row .fa {
+ color: #222427;
+}
+
+.dropdown-submenu-open .fa {
+ color: #222427;
+}
+
+.project-header .action-menu .fa {
+ color: #222427;
+}
+
+.text-editor .fa {
+ color: #222427;
+}
+
+.sidebar .fa {
+ color: #222427;
+}
+
+.board-column-title .fa {
+ color: #222427;
+}
+
+.task-board a {
+ color: #222427;
+ text-decoration: none;
+}
+
+.sidebar>ul a:hover {
+ color: #2c52e3;
+}
+
+.logo>a>img {
+ vertical-align: middle;
+ padding: 0.5em 1em 0.5em 0.5em;
+}
+
+div.ganttview-vtheader-series-name {
+ padding: 0 6px;
+}
+
+th,
+td {
+ padding: 10px;
+}
+
+header {
+ border-bottom: none;
+ box-shadow: 0px 1px 3px 0 rgba(46, 61, 73, .12);
+ padding: 1em 0.6em;
+ margin-bottom: 1em;
+ background-color: #222427 !important;
+}
+
+.header img {
+ float: left;
+}
+
+.header h2 {
+ position: relative;
+ color: #2c52e3;
+ top: 13px;
+ left: 10px;
+ margin: 0;
+}
+
+header .title-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+label {
+ font-weight: bold;
+ margin-top: 18px;
+}
+
+.table-list-header {
+ background: #222427;
+ border-radius: 0;
+ border-color: #222427;
+ line-height: 28px;
+ padding: 0.2em;
+}
+
+.table-list-header .table-list-header-count {
+ color: #fff;
+ display: inline-block;
+ float: left;
+ margin-left: 0.4em;
+}
+
+.table-list-header a {
+ color: #fff;
+ font-weight: 500;
+}
+
+.table-list-header a:hover {
+ color: #fff;
+ font-weight: 500;
+}
+
+.table-list-row:last-child {
+ border-radius: 0;
+}
+
+.task-board {
+ margin-bottom: 8px;
+ padding: 12px;
+ border-radius: 0;
+ border-top: none;
+ border-right: none;
+ border-left: none;
+ border-bottom: 0.4em solid rgba(0, 0, 0, 0.3);
+}
+
+div.task-board-recent {
+ border-width: 0.4em;
+}
+
+.task-board-title {
+ font-size: 1.25em;
+ font-weight: 600;
+}
+
+.task-board-header i {
+ color: #222427 !important;
+}
+
+.task-show-details {
+ border-radius: 0;
+ margin-bottom: 20px;
+}
+
+.task-show-details h2 {
+ background-color: rgba(0, 0, 0, 0.3);
+ padding: 20px;
+ border-radius: 0;
+ color: #fff;
+}
+
+.task-show-details ul {
+ padding: 20px;
+}
+
+.task-summary-container {
+ border: none;
+ border-radius: 0;
+ border-bottom: 5px solid;
+ padding: 20px;
+}
+
+.table-small {
+ font-size: 1em;
+}
+
+table th {
+ text-align: left;
+ padding: 0.8em 3px 0.6em 3px;
+ border: 1px solid #eee;
+ background: #fbfbfb;
+}
+
+table td {
+ border: none;
+ padding: 0.5em 7px;
+ vertical-align: top;
+}
+
+.sidebar ul {
+ padding-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+ border-radius: 0;
+}
+
+.sidebar ul:last-of-type {
+ border-bottom: none;
+}
+
+.sidebar-collapse i,
+.sidebar-collapsed .sidebar {
+ background-color: #999;
+ border-radius: 0;
+ padding: 5px 10px 5px 8px;
+}
+
+.sidebar-collapse i,
+.sidebar-collapsed .sidebar i {
+ color: #fff;
+}
+
+.sidebar-collapse i:hover,
+.sidebar-collapsed .sidebar:hover {
+ background-color: #ccc;
+}
+
+.page-header {
+ margin: 15px 0;
+ background-color: #eee;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #ddd;
+ border-top: 1px solid #ddd;
+}
+
+.page-header li {
+ font-size: 1.1em;
+}
+
+.page-header h2 {
+ border-bottom: none;
+}
+
+header .avatar img {
+ width: 48px;
+ border-radius: 0;
+ margin-left: 0.5em;
+ margin-right: 0.4em;
+}
+
+.menus-container .avatar+i {
+ display: none;
+}
+
+.listing {
+ border: none;
+}
+
+.fa-play {
+ font-size: 1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 28px;
+ padding: 8px 0 0 15px;
+ margin-right: 8px;
+}
+
+.fa-play:hover {
+ background-color: #999;
+ color: #fff;
+}
+
+.fc-event .fc-content {
+ padding: 4px 8px;
+}
+
+.board-add-icon {
+ float: left;
+ padding: 0 5px;
+}
+
+.board-add-icon i {
+ text-decoration: none;
+ color: #289E7B;
+ font-size: 1.4em;
+}
+
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #23292d;
+}
+
+.form-column select {
+ font-size: 1.2em;
+ margin: 10px 0 2px 0;
+}
+
+.form-column:nth-of-type(2) {
+ padding: 10px 25px;
+ background-color: #f7f7f7;
+}
+
+.form-actions {
+ font-size: 1.2em;
+}
+
+.page-header ul.dropdown-submenu-open {
+ margin: 10px 0 0 -10px;
+ padding: 0;
+}
+
+.select-dropdown-input-container {
+ position: relative;
+ border: 2px solid #3c3c3c;
+ width: 20em;
+ max-width: 20em;
+ border-radius: 0;
+ background-color: #222427;
+ margin-right: 1em;
+}
+
+header input {
+ color: #fff !important;
+}
+
+.board-selector-container input,
+.board-selector-container textarea {
+ background-color: inherit;
+ color: #fff;
+}
+
+.select-dropdown-input-container .select-dropdown-chevron {
+ position: relative;
+ color: #fff;
+ top: auto;
+ right: -5%;
+}
+
+ul.dropdown-submenu-open {
+ margin: 10px 0 0 -10px;
+ padding: 0;
+ background-color: #fff;
+ border: 1px solid #222427;
+ border-radius: 0;
+ box-shadow: 0 0.1em 0.5em rgba(0, 0, 0, 0.5);
+}
+
+.dropdown-submenu-open .no-hover {
+ cursor: default;
+ padding: 1em;
+ font-size: 1em;
+ background-color: #222427;
+ color: #fff;
+}
+
+.dropdown-submenu-open li {
+ padding: 6px 10px;
+}
+
+#select-dropdown-menu {
+ background-color: #222427;
+ border-color: #222427;
+}
+
+.select-dropdown-menu-item.active {
+ color: #fff;
+ background: #2c52e3;
+}
+
+.views li {
+ padding: 0.4em;
+}
+
+.filters {
+ border: none;
+}
+
+.filters ul.dropdown-submenu-open {
+ margin: 6px 0 0 8px;
+}
+
+.markdown pre {
+ background: #3333330d;
+ max-height: 600px;
+ overflow: auto;
+ margin-bottom: 1em;
+ padding: 5px;
+ color: #242729;
+ font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, sans-serif;
+}
+
+a i.web-notification-icon {
+ color: #D45353;
+}
+
+.fa-play {
+ font-size: 1.2em;
+ background-color: #ddd;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px !important;
+ padding: 9px 3px 0 9px !important;
+ margin-right: 8px;
+}
+
+#modal-box {
+ padding: 10px;
+}
+
+.table-list-category {
+ padding: 2px 2px 2px 5px;
+}
+
+.table-list-row {
+ transition: 0.15s;
+}
+
+.table-list-row:hover {
+ background: #f5f5f5;
+ border: none;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
+}
+
+.page-header {
+ margin: 15px 0;
+ background-color: #ffffff;
+ padding: 6px 10px 10px 10px;
+ border-bottom: 1px solid #f7f7f7;
+ border-top: 1px solid #f7f7f7;
+}
+
+.comments .comment-highlighted {
+ background-color: #fff;
+ border: 2px solid #F5E982;
+ border-radius: 0;
+}
+
+.comments .comment:hover {
+ background: #f5f5f5;
+ border-radius: 0;
+}
+
+.comments .comment:nth-child(even):not(.comment-highlighted):hover {
+ border-radius: 0;
+ background: #f5f5f5;
+}
+
+.comments .comment:nth-child(even):not(.comment-highlighted) {
+ background: #fbfbfb;
+ border-radius: 0;
+}
+
+.sidebar {
+ background-color: #f7f7f7;
+ padding: 7px;
+ border-radius: 0;
+ border-left: 2px solid #e9e9e9;
+ box-shadow: 1px 0px 7px 0 rgba(46, 61, 73, .12);
+}
+
+.input-addon * {
+ border-radius: 0 !important;
+}
+
+input[type="number"]:focus,
+input[type="date"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus,
+input[type="text"]:focus {
+ color: #000;
+ border-color: #2c52e3;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222, 222, 222, 0.25);
+}
+
+textarea:focus {
+ color: #000;
+ border-color: #2c52e3;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(222, 222, 222, 0.25);
+}
+
+input[type="number"],
+input[type="date"],
+input[type="email"],
+input[type="password"],
+input[type="text"]:not(.input-addon-field) {
+ padding: 0.4em;
+ transition: box-shadow 1s;
+}
+
+.select-dropdown-input-container input.select-dropdown-input {
+ width: 80%;
+}
+
+.text-editor textarea {
+ padding: 3px;
+ transition: box-shadow 1s;
+}
+
+.table-list-row {
+ padding: 0.4em;
+}
+
+select {
+ border: 1px solid #ccc;
+ background: #f9f9f9;
+ padding: 3px;
+}
+
+.btn-blue {
+ border-color: #2c52e3;
+ background-color: #2c52e3;
+ color: #fff;
+}
+
+.btn-blue:hover,
+.btn-blue:focus {
+ border-color: #2c52e3;
+ background: #ffffff;
+ color: #2c52e3;
+}
+
+.select2-container--default.select2-container--focus .select2-selection--multiple {
+ border: solid #2c52e3 1px;
+ outline: 0;
+}
+
+.select2-container--default .select2-results__option--highlighted[aria-selected] {
+ background-color: #2c52e3;
+ color: #fff;
+}
+
+.board-column-header-task-count {
+ display: none;
+}
+
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #91C259;
+}
+
+.board-column-title *:hover {
+ color: #aaa;
+}
+/*
+.board-column-title *:hover {
+ color: #aaa;
+} */
+
+.board-add-icon i {
+ text-decoration: none;
+ color: #2c52e3;
+ font-size: 1.4em;
+}
+
+.dropdown-submenu-open li:not(.no-hover):hover {
+ background: #2c52e3;
+ color: #fff;
+}
+
+.sidebar>ul li.active a {
+ color: #2c52e3;
+ font-weight: bold;
+}
+
+.table-list-row .table-list-title a:hover,
+.table-list-row .table-list-title a:focus {
+ text-decoration: underline;
+ color: #2c52e3;
+}
+
+.image-slideshow-overlay img {
+ display: block;
+ margin: auto;
+ max-width: 100%;
+}
+
+.input-addon-item {
+ background-color: #222427;
+ color: #fff;
+}
+
+span.task-date-overdue {
+ opacity: 1.0;
+ color: #e90529;
+}
+
+.board-add-icon i:focus,
+.board-add-icon i:hover {
+ text-decoration: none;
+ color: #2BBF5F;
+}
+
+span.task-date-today {
+ opacity: 1.0;
+ color: #2c52e3;
+}
+
+#task-summary h2 {
+ color: #222427;
+}
+
+.input-addon {
+ position: relative;
+ top: -0.07em;
+}
+
+.input-addon-field, .input-addon-item {
+ padding: 0.25em 0.75em;
+}
+
+#modal-box {
+ border-radius: 0;
+}
+
+.selection * {
+ border-radius: 0 !important;
+}
+
+.task-date {
+ font-size: 1.5em;
+ color: #000;
+}
+
+.task-board-icons-row {
+ font-size: 1.2em;
+}
+
+.sidebar > ul a {
+ color: #222427;
+}
diff --git a/plugins/Customizer/Assets/css/themes/Material.css b/plugins/Customizer/Assets/css/themes/Material.css
new file mode 100644
index 00000000..706afaf0
--- /dev/null
+++ b/plugins/Customizer/Assets/css/themes/Material.css
@@ -0,0 +1,1427 @@
+/*!
+ * Material Like - Theme for Kanboard
+ * Licensed under the MIT license - kanboard-theme-material-like/LICENSE
+ * https://github.com/erichk4/kanboard-theme-material-like
+ * Copyright (c) 2017 Erich Munz
+ */
+
+@import "https://fonts.googleapis.com/css?family=Open+Sans";
+
+/* CSSTidy 1.5.2: Mon, 25 Sep 2017 09:04:23 -0700 */
+body
+{
+ font-family:'Open Sans',sans-serif;
+ font-size:14px;
+ background-color:#f1f1f1!important
+}
+
+div#modal-box {
+ width: max-content!important;
+}
+
+.logo a {
+ opacity: 1;
+ color: inherit;
+ text-decoration: none;
+}
+
+.title-container .logo {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.title-container .title:before {
+ font-family: FontAwesome;
+ content: "\f054";
+ display: inline-block;
+ padding-right: 2px;
+ vertical-align: middle;
+ font-size: 0.6em;
+}
+
+#task-summary h2:before {
+ font-family: FontAwesome;
+ content: "\f017";
+ display: inline-block;
+ padding-right: 8px;
+ vertical-align: middle;
+ font-size: 0.6em;
+}
+
+@media only screen and (max-width: 1024px) {
+ body
+ {
+ font-size:12px
+ }
+}
+
+.ui-widget-shadow {
+ -webkit-box-shadow: 0 0 5px #ccc;
+ box-shadow: 0 0 5px #ccc;
+}
+
+.ui-widget.ui-widget-content {
+ border: 1px solid #ccc;
+}
+
+.sidebar-content .task-list-avatars .avatar
+{
+ border:none;
+ background:none;
+ padding:0;
+ overflow:initial;
+ margin:0;
+ margin-right:3px;
+ margin-top:5px
+}
+
+tr td:last-child {
+ border-right:1px solid #e5e5e5;
+}
+
+tr:last-child td {
+ border-bottom:1px solid #e5e5e5;
+}
+
+.sidebar-content .accordion-section
+{
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ padding:12px
+}
+
+.accordion-section.accordion-collapsed .accordion-title
+{
+ border-bottom:none
+}
+
+.project-overview-columns
+{
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ padding:12px
+}
+
+.accordion-section
+{
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ padding:12px;
+ margin-bottom:25px
+}
+
+.sidebar-content .accordion-title h3
+{
+ margin-top:12px;
+ background-color:none;
+ border:none;
+ box-shadow:none;
+ position:relative;
+ padding:12px;
+ padding-left:0;
+ margin-bottom:-11px
+}
+
+.sidebar-content .filter-box
+{
+ float:none;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ max-width:inherit;
+ margin-top:12px
+}
+
+.sidebar-content .filter-box form
+{
+ margin-top:0
+}
+
+.filter-box form
+{
+ border:none;
+ box-shadow:none
+}
+
+.js-chart-task-time-column-rendered
+{
+ background:#fff;
+ margin-top:10px;
+ padding:20px 0;
+ margin-bottom:14px;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04)
+}
+
+.panel li
+{
+ list-style-type:square;
+ margin-left:20px;
+ line-height:1.8em
+}
+
+.sidebar-content .activity-event .avatar
+{
+ border-radius:0;
+ padding:8px 35px 8px 10px;
+ margin-top:10px;
+ margin-bottom:15px;
+ border:none;
+ color:#333;
+ background-color:transparent;
+ overflow:auto;
+ box-shadow:none
+}
+
+.project-header
+{
+ background:#fff;
+ padding:0;
+ margin-top:10px;
+ margin-bottom:10px
+}
+
+.task-board-title
+{
+ margin-top:5px;
+ margin-bottom:20px
+}
+
+#modal-overlay
+{
+ position:fixed;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+ background:rgba(0,0,0,0.36);
+ overflow:auto;
+ z-index:100
+}
+
+#modal-box .page-header
+{
+ box-shadow:none;
+ background:#fff;
+ padding:10px 20px 10px 0;
+ border:none;
+ border-bottom:1px solid #e5e5e5;
+ margin-top:0;
+ margin-bottom:10px
+}
+
+.task-form-secondary-column
+{
+ max-width:250px;
+ min-width:200px;
+ max-height:600px;
+ overflow:auto;
+ width:20%;
+ background:#f2f2f2;
+ padding:20px
+}
+
+#modal-box form
+{
+ margin-top:10px;
+ background-color:#fff;
+ border:none;
+ box-shadow:none;
+ position:relative;
+ padding-left:0
+}
+
+.table-list-row:hover
+{
+ background: rgba(51, 146, 204, 0.1);
+ border-bottom:none;
+ border-right:none
+}
+
+.table-list-row.table-border-left
+{
+ border-left:none
+}
+
+.filter-box
+{
+ float:right
+}
+
+.project-header .views-switcher-component
+{
+ margin-top:16px;
+ float:left
+}
+
+.project-header .dropdown-component
+{
+ margin-top:16px;
+ margin-right:5px;
+ float:left
+}
+
+.sidebar-title
+{
+ padding:8px
+}
+
+input[type="number"],input[type="date"],input[type="email"],input[type="password"],input[type="text"]:not(.input-addon-field)
+{
+ color:#999;
+ border:2px solid #ccc;
+ max-width:88.5%;
+ font-size:1em;
+ height:25px;
+ padding:8px;
+ font-family:inherit
+}
+
+.select-dropdown-input-container input[type="text"]
+{
+ border:none
+}
+
+.select-dropdown-input-container
+{
+ position:relative;
+ border:2px solid #ccc;
+ border-radius:0
+}
+
+.select-dropdown-input-container .select-dropdown-chevron
+{
+ color:#555;
+ position:absolute;
+ top:12px;
+ right:8px;
+ cursor:pointer
+}
+
+textarea
+{
+ max-width:99%;
+ height:200px;
+ font-size:1em;
+ color:#999;
+ border:2px solid #ccc;
+ width:300px;
+ padding:8px;
+ font-family:inherit
+}
+
+select
+{
+ padding:8px;
+ max-width:95%;
+ width:200px;
+ border:#ccc 2px solid
+}
+
+.sidebar>ul li.active a
+{
+ color:#3392cc;
+ font-weight:700
+}
+
+.sidebar>ul li.active
+{
+ border-left:5px solid #3392cc;
+ padding-left:8px
+}
+
+.page-header
+{
+ margin-top:14px;
+ margin-bottom:0
+}
+
+.btn
+{
+ padding:10px 16px
+}
+
+.page-header,header
+{
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background:#fff;
+ padding:20px
+}
+
+header {
+ box-sizing: border-box;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid #dedede;
+}
+
+header .menus-container
+{
+ width:10%;
+ margin-top:12px
+}
+
+header .title-container
+{
+ width:65%;
+ margin-top:10px
+}
+
+.sidebar-content fieldset
+{
+ margin-top:10px;
+ margin-bottom:10px;
+ position:relative
+}
+
+.sidebar-content .panel,.sidebar-content fieldset
+{
+ border:none;
+ border-bottom:1px solid #efefef
+}
+
+.sidebar-content .avatar,.sidebar-content .panel
+{
+ border-radius:0;
+ padding:8px 35px 8px 10px;
+ margin-top:10px;
+ margin-bottom:15px;
+ border:1px solid #e5e5e5;
+ color:#333;
+ background-color:#fff;
+ overflow:auto;
+ box-shadow:0 1px 1px rgba(0,0,0,.04)
+}
+
+.table-list
+{
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background-color:#fff;
+ border:1px solid #e5e5e5
+}
+
+.table-list-header,.table-list-row
+{
+ border:none
+}
+
+.sidebar
+{
+ max-width:240px;
+ min-width:190px;
+ width:18%;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ background-color:#fff;
+ margin-top:12px
+}
+
+label
+{
+ cursor:pointer;
+ display:block;
+ margin-top:10px;
+ margin-bottom:4px;
+ font-weight:700
+}
+
+.sidebar-content legend
+{
+ top:14px;
+ left:0;
+ right:0;
+ padding-bottom:7px;
+ font-size:1.2em
+}
+
+.sidebar-icons>ul li
+{
+ padding-left:12px
+}
+
+.sidebar-icons>ul li:hover,.sidebar-icons>ul li.active
+{
+ padding-left:12px;
+ border-left:none
+}
+
+.sidebar-content h3,form h3
+{
+ margin-top:12px;
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ position:relative;
+ padding:12px;
+ margin-bottom:-11px
+}
+
+#task-summary h2
+{
+ margin-top:12px;
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ position:relative;
+ padding:12px
+}
+
+.activity-date,.activity-description,.btn,.buttons-header,.documentation,.dropdown-submenu-open li,.fc-event,.form-help,.menu-inline li,.page-header li,.project-overview-column span,.project-overview-columns,.sidebar,.subtasks-table,.table-list,.table-list-row .table-list-icons,.table-small,.task-board,.task-board-category,.task-board-title,.task-summary-buttons,.task-summary-column,.textarea-dropdown li,.tooltip .fa-info-circle,.views,div.ganttview-vtheader-series-name,header ul,legend,table
+{
+ font-size:1em
+}
+
+.ui-widget
+{
+ font-family:'Open Sans',sans-serif;
+ font-size:1em
+}
+
+#ui-datepicker-div,.ui-datepicker table,div.ui-tooltip
+{
+ font-size:1em
+}
+
+.assign-me,.comment-actions,.comment-date,.dashboard-project-stats span
+{
+ font-size:.85em
+}
+
+input[type=date],input[type=email],input[type=number],input[type=password],input[type=text]
+{
+ font-family:'Open Sans',sans-serif;
+ font-size:14px
+}
+
+input[type=file]
+{
+ font-family:'Open Sans',sans-serif;
+ font-size:14px
+}
+
+select
+{
+ font-family:'Open Sans',sans-serif;
+ font-size:14px
+}
+
+.filter-box input[type=text]
+{
+ font-size:14px
+}
+
+.chosen-container
+{
+ font-size:14px
+}
+
+.CodeMirror,.editor-toolbar,.filter-box div.dropdown:last-child,.filter-box input[type=text],.input-addon-item:last-child,.panel,.table-list-header,.table-list-row:last-child,.views li:first-child,.views li:last-child
+{
+ border-bottom-left-radius:0;
+ border-top-left-radius:0;
+ border-bottom-right-radius:0;
+ border-top-right-radius:0
+}
+
+#modal-box,.alert,.btn,.documentation,.listing,.markdown pre,.task-board,.task-show-title,.task-summary-container,ul.dropdown-submenu-open
+{
+ border-radius:0
+}
+
+.ui-corner-all,.ui-corner-bottom,.ui-corner-br,.ui-corner-right
+{
+ border-bottom-right-radius:0;
+ border-bottom-left-radius:0;
+ border-top-right-radius:0;
+ border-top-left-radius:0
+}
+
+.select2-container--default .select2-selection--multiple,.select2-container--default .select2-selection--single
+{
+ border-radius:0
+}
+
+#select-dropdown-menu,.select-dropdown-input-container
+{
+ border-radius:0
+}
+
+#select-dropdown-menu
+{
+ border-top:none
+}
+
+.chosen-container .chosen-results li.highlighted
+{
+ background-color:#36c;
+ background-image:none
+}
+
+.chosen-container-single .chosen-single
+{
+ -webkit-box-shadow:none;
+ box-shadow:none;
+ background:#fff
+}
+
+.chosen-container-active.chosen-with-drop .chosen-single
+{
+ background:#fff
+}
+
+.color-picker-square.color-yellow,.task-board.color-yellow,.task-summary-container.color-yellow
+{
+ background-color:#ffeb8e!important;
+ border-color:#e4c600!important
+}
+
+.alert
+{
+ background-color:#f7e400;
+ border-color:#e8d700;
+ color:#776e00
+}
+
+a
+{
+ color:#3392cc
+}
+
+a:hover
+{
+ color:#3392cc
+}
+
+a:focus
+{
+ color:#3392cc
+}
+
+h2
+{
+ font-size:1.3em;
+ font-weight:700
+}
+
+h3
+{
+ font-size:1.3em;
+ font-weight:700
+}
+
+legend
+{
+ font-weight:700
+}
+
+input[type=date],input[type=email],input[type=number],input[type=password],input[type=text]
+{
+ padding-left:5px;
+ border-color:#ccc;
+ color:#333
+}
+
+input[type=date]:focus,input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=text]:focus
+{
+ -webkit-box-shadow:none;
+ box-shadow:none
+}
+
+input[type=text]:not(.input-addon-field)
+{
+ color:#333
+}
+
+textarea
+{
+ padding:5px
+}
+
+textarea:focus
+{
+ -webkit-box-shadow:none;
+ box-shadow:none
+}
+
+.select2-container--default,.select2-selection--single
+{
+ min-width:200px;
+ border-color:#ccc
+}
+
+.select2-container--default .select2-selection__rendered,.select2-selection--single .select2-selection__rendered
+{
+ color:#333
+}
+
+.select2-container--default .select2-selection--multiple,.select2-selection--single .select2-selection--multiple
+{
+ border-color:#ccc;
+ border-width:2px
+}
+
+.select2-container--default .select2-selection--single
+{
+ background-color:#fff;
+ border:2px solid #ccc;
+ border-radius:0
+}
+
+.color-picker-option
+{
+ height:20px
+}
+
+#select-dropdown-menu
+{
+ overflow:auto
+}
+
+table select
+{
+ margin-top:0
+}
+
+.filter-box input[type=text]
+{
+ padding-left:5px
+}
+
+.form-actions
+{
+ margin-bottom:4px
+}
+
+.form-column ul
+{
+ margin-top:5px
+}
+
+.form-help
+{
+ margin-top:5px;
+ font-size:.85em;
+ color:gray;
+ font-style:italic
+}
+
+.form-required
+{
+ color:#900
+}
+
+.buttons-header
+{
+ font-size:.8em;
+ margin-top:10px;
+ margin-bottom:15px
+}
+
+.btn
+{
+ font-size:1.2em;
+ font-weight:400;
+ cursor:pointer;
+ display:inline-block;
+ border-radius:2px;
+ padding:8px 18px;
+ margin:0;
+ border:1px solid #ccc;
+ background:#fff;
+ color:#000
+}
+
+.btn-blue
+{
+ background:#3392cc;
+ border:none;
+ color:#fff
+}
+
+.btn-blue:focus,.btn-blue:hover
+{
+ background:#036;
+ border:none
+}
+
+.btn-red
+{
+ background:#900;
+ border:none;
+ color: #fff;
+}
+
+.btn-red:focus,.btn-red:hover
+{
+ background:#600;
+ border:none
+}
+
+table
+{
+ border-collapse:separate;
+ margin-bottom:30px;
+ padding:20px;
+ background-color:#fff;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04)
+}
+
+.table-small {
+ border-collapse:separate;
+ margin-bottom:0;
+ padding:4px;
+ background-color:#fff;
+ border:none;
+ box-shadow:none
+}
+
+table th
+{
+ background:#eee
+}
+
+table td,table th
+{
+ border-top:1px solid #e5e5e5;
+ /*border-right:1px solid #e5e5e5;*/
+ border-bottom:none;
+ border-left:1px solid #e5e5e5;
+ padding:10px;
+ border-collapse:collapse
+}
+
+.table-list-header
+{
+ background:#fff;
+ border-color:#ccc;
+ border-bottom:1px solid #efefef
+}
+
+.table-list-header .table-list-header-count
+{
+ margin-left:15px;
+ font-weight:700
+}
+
+.table-list-row
+{
+ padding:10px 15px;
+ border-color:#e5e5e5
+}
+
+.table-list-row.color-yellow
+{
+ border-left:solid 5px #e4c600!important
+}
+
+.table-list-row:hover
+{
+ border-right-color:#e5e5e5;
+ border-bottom-color:#e5e5e5;
+ border-top-color:#e5e5e5
+}
+
+.table-list-row .table-list-details
+{
+ line-height:22px;
+ margin-bottom:5px
+}
+
+.sidebar-content
+{
+ width:calc(100% - 240px)
+}
+
+@media (max-width: 480px) {
+ .sidebar-content
+ {
+ width:100%
+ }
+}
+
+.page-header
+{
+ background:#fff;
+ padding:10px 20px 10px 15px;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04);
+ margin-top:15px;
+ margin-bottom:5px
+}
+
+.page-header:first-child
+{
+ border-top:none
+}
+
+.page-header h2
+{
+ font-size:1.3em;
+ border:none
+}
+
+.page-header ul
+{
+ margin-top:5px;
+ margin-bottom:5px
+}
+
+.page-header ul .fa
+{
+ color:#036
+}
+
+.page-header ul a
+{
+ text-decoration:none
+}
+
+.project-header
+{
+ background:#fff;
+ padding:12px;
+ margin-top:10px;
+ margin-bottom:10px;
+ overflow:auto;
+ border:1px solid #e5e5e5;
+ box-shadow:0 1px 1px rgba(0,0,0,.04)
+}
+
+.project-header .input-addon-item
+{
+ background:#fff;
+ border-color:#ccc
+}
+
+.sidebar-content .page-header ul
+{
+ margin-top:7px;
+ margin-bottom:3px
+}
+
+#board-container
+{
+ overflow-x:inherit
+}
+
+.board-add-icon
+{
+ padding-top:2px;
+ line-height:10px
+}
+
+.board-add-icon a i
+{
+ font-size:1em;
+ color:#3392cc
+}
+
+.board-add-icon a:hover i
+{
+ color:#036
+}
+
+.board-column-header-task-count
+{
+ color:#999;
+ font-size:.85em
+}
+
+.task-board
+{
+ padding:10px;
+ margin:2px 2px 6px
+}
+
+div.task-board-recent
+{
+ border-width:0
+}
+
+.task-board-icons,.task-list-icons
+{
+ font-size:.8em;
+ padding:4px;
+ background:rgba(255,255,255,0.41)
+}
+
+.task-board-category,.task-tags li
+{
+ padding:2px 6px
+}
+
+.task-tags li
+{
+ border-color:rgba(102,102,102,0.5);
+ color:#666;
+ background-color:rgba(255,255,255,0.5);
+ font-size:.9em
+}
+
+.project-overview-column strong
+{
+ font-size:2em
+}
+
+.project-overview-column
+{
+ border-radius:0;
+ min-width:100px;
+ margin-right:20px
+}
+
+.activity-event
+{
+ border-bottom:1px solid #e5e5e5;
+ padding:12px;
+ overflow:auto;
+ background:#fff
+}
+
+.activity-event:nth-child(even) {
+ background: #fff;
+}
+
+.activity-event:nth-child(even):hover
+{
+ background:rgba(51, 146, 204, 0.1)
+}
+
+.activity-event:hover {
+ background: rgba(51, 146, 204, 0.1);
+}
+
+.activity-title
+{
+ border:none
+}
+
+.editor-toolbar
+{
+ border-top-color:#ccc;
+ border-left-color:#ccc;
+ border-right-color:#ccc
+}
+
+.CodeMirror
+{
+ border-color:#ccc
+}
+
+#task-summary h2
+{
+ font-size:1.6em;
+ color:#333
+}
+
+.task-show-title h2
+{
+ font-size:1.6em;
+ color:#333
+}
+
+.comment-sorting
+{
+ font-size:11px
+}
+
+.task-summary-column
+{
+ color:#333
+}
+
+.task-list-tag
+{
+ background:#eee;
+ border-color:#ccc
+}
+
+.table-list-category
+{
+ border-color:#ccc
+}
+
+.subtask-cell
+{
+ border-color:#ccc
+}
+
+.subtask-cell:first-child
+{
+ padding-left:0
+}
+
+.task-list-subtasks
+{
+ margin-top:5px
+}
+
+.task-list-subtask:last-child .subtask-cell
+{
+ border-bottom:1px dotted #e5e5e5
+}
+
+.text-editor .text-editor-toolbar
+{
+ width:687px;
+ max-width:98%;
+ margin-top:20px;
+ margin-bottom:5px;
+ padding:12px;
+ background:#f2f2f2
+}
+
+.text-editor textarea
+{
+ width:696px;
+ max-width:98%
+}
+
+.alert
+{
+ margin-bottom:30px
+}
+
+.dropdown-submenu-open li:hover:not(.no-hover),.textarea-dropdown .active,.textarea-dropdown li:hover
+{
+ background:#3392cc
+}
+
+.accordion-title
+{
+ background:#fff;
+ padding:4px 0;
+ border-bottom:1px solid #ccc
+}
+
+.accordion-title h3
+{
+ background:0 0;
+ padding-left:0
+}
+
+.views li
+{
+ white-space:nowrap;
+ background:#fafafa;
+ border:1px solid #ddd;
+ border-right:none;
+ padding:8px;
+ display:inline
+}
+
+.pagination
+{
+ margin-bottom:30px
+}
+
+.activity-content
+{
+ margin-left:65px
+}
+
+#board_selector_chosen
+{
+ width:350px!important
+}
+
+#login-top
+{
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+#login-bottom
+{
+ margin-top:40px
+}
+
+#login-bottom ul
+{
+ list-style-type:none
+}
+
+#login-bottom ul li
+{
+ margin-left:0
+}
+
+#modal-header
+{
+ margin-top:5px;
+ margin-bottom:5px;
+ font-size:1.5em;
+ float:right
+}
+
+#modal-content
+{
+ padding:0 20px
+}
+
+.documentation img
+{
+ border-color:#ccc;
+ margin-top:40px;
+ margin-bottom:20px
+}
+
+.documentation li
+{
+ line-height:1.6em
+}
+
+.documentation h1
+{
+ padding-bottom:5px;
+ border-bottom:1px solid #e5e5e5;
+ margin-top:40px;
+ margin-bottom:30px
+}
+
+.documentation h1:first-child
+{
+ margin-top:5px
+}
+
+.documentation h2
+{
+ padding-bottom:5px;
+ border-color:#e5e5e5;
+ margin-top:25px;
+ margin-bottom:10px
+}
+
+.documentation h3
+{
+ margin-top:25px;
+ margin-bottom:10px
+}
+
+.markdown h1
+{
+ margin-top:20px;
+ margin-bottom:10px;
+ padding-bottom:5px;
+ border-bottom:1px solid #e5e5e5
+}
+
+.markdown h2
+{
+ margin-top:20px;
+ margin-bottom:10px;
+ padding-bottom:5px;
+ border-bottom:1px solid #e5e5e5
+}
+
+.markdown h3
+{
+ margin-top:20px;
+ margin-bottom:10px
+}
+
+.markdown p
+{
+ margin-bottom:5px
+}
+
+@media print {
+ a.btn
+ {
+ display:none
+ }
+
+ header
+ {
+ display:block
+ }
+
+ header nav ul
+ {
+ display:none
+ }
+
+ .page-header
+ {
+ display:block
+ }
+
+ .page-header ul
+ {
+ display:none
+ }
+
+ .project-header
+ {
+ display:none
+ }
+
+ .filter-box
+ {
+ display:none
+ }
+
+ .dropdown ul
+ {
+ display:none
+ }
+
+ #board-container .board-add-icon
+ {
+ display:none
+ }
+
+ #board-container a.dropdown-menu i
+ {
+ display:none
+ }
+
+ #task-view .sidebar
+ {
+ display:block
+ }
+
+ #task-view .sidebar h2
+ {
+ display:none
+ }
+
+ #task-view .sidebar ul
+ {
+ display:none
+ }
+
+ #task-view .sidebar h2:first-child
+ {
+ display:block
+ }
+
+ #task-summary h2,.task-show-title h2,.task-summary-column span
+ {
+ color:#000
+ }
+
+ .task-summary-column .smaller
+ {
+ display:none
+ }
+
+ .accordion-collapsed
+ {
+ display:none
+ }
+
+ #comments .comment-sorting
+ {
+ display:none
+ }
+
+ #comments .comment-actions
+ {
+ display:none
+ }
+
+ a,th a
+ {
+ color:#000;
+ text-decoration:none
+ }
+
+ .table-fixed
+ {
+ white-space:normal
+ }
+
+ .table-fixed td
+ {
+ -o-text-overflow:clip;
+ text-overflow:clip;
+ white-space:normal
+ }
+
+ header nav h1 .logo
+ {
+ display:block
+ }
+
+ .page-header
+ {
+ background:0 0;
+ padding:0 0 10px
+ }
+
+ .page-header h2
+ {
+ margin:0
+ }
+
+ header nav h1 .tooltip i
+ {
+ display:none
+ }
+
+ #board-container
+ {
+ overflow-x:visible
+ }
+
+ #board-container .board-column-header
+ {
+ padding-left:7px
+ }
+
+ #task-view .sidebar h2:first-child
+ {
+ margin-top:50px;
+ margin-bottom:5px;
+ font-size:1.6em;
+ font-weight:400
+ }
+
+ #task-summary .task-summary-container
+ {
+ padding-right:80px
+ }
+
+ .accordion-title
+ {
+ background:0 0;
+ padding:0;
+ margin-top:40px
+ }
+
+ .accordion-title h3
+ {
+ margin:0
+ }
+
+ #comments .avatar
+ {
+ float:left;
+ width:48px
+ }
+
+ #comments .avatar .avatar-letter
+ {
+ color:#fff;
+ text-align:center
+ }
+
+ #comments .avatar-48 .avatar-letter
+ {
+ font-size:25px;
+ line-height:48px;
+ width:48px
+ }
+
+ #comments .avatar-48 div,#comments .avatar-48 img
+ {
+ border-radius:30px
+ }
+}
diff --git a/plugins/Customizer/Assets/css/userthemes/niebieski.css b/plugins/Customizer/Assets/css/userthemes/niebieski.css
new file mode 100644
index 00000000..2d68776c
--- /dev/null
+++ b/plugins/Customizer/Assets/css/userthemes/niebieski.css
@@ -0,0 +1 @@
+header{background-color:#006;background-image:linear-gradient(-180deg,transparent 0%,#FF3333 90%)}header h1,header a .fa{color:#36F}a i.web-notification-icon{color:}body{background:;color:}ul.dropdown-submenu-open,.accordion-title h3{background-color:}.dropdown-submenu-open a{color:}a:hover{color:}a .fa{color:}h1,h2,h3,.accordion-toggle{color:}.table-list-header a{color:}.table-list-header .table-list-header-count{color:}.table-list-row .table-list-title a{color:}.table-list-row .table-list-details strong{color:}.dropdown-menu-link-icon{color:}.page-header h2 a{color:}.sidebar>ul a:hover{color:}.sidebar>ul li.active a{color:}.task-list-icons a:hover{color:}.task-list-icons a:hover i{color:}.subtask-cell a{color:}a{color:}.table-list-category a:hover{color:}.subtask-cell a:hover,.subtask-cell a:focus{color:}a:focus,a:hover{color:}.table-list-header a:hover,.table-list-header a:focus,.task-board a{color:}.table-list-row .table-list-details{color:}.input-addon-field{color:}.page-header h2 a:focus,.page-header h2 a:hover,code{color:}.sidebar>ul a{color:}.task-list-avatars .task-avatar-assignee{color:}.task-list-icons a,.task-list-icons span,.task-list-icons i,.task-date{color:}span.task-date-overdue{color:}.sidebar>ul li.active{border-left:5px solid}.sidebar>ul li.active a:focus,.sidebar>ul li.active a:hover{color:}.sidebar>ul li:hover{border-left:5px solid}.panel{color:}td a.dropdown-menu strong,td a.dropdown-menu strong i{color:}#task-summary h2{color:unset}.comments .comment:hover{background:#efefef40}.comments .comment:nth-child(even):not(.comment-highlighted):hover{background:#efefef40}.comments .comment:nth-child(even):not(.comment-highlighted){background:#efefef40}table.table-striped tr:nth-child(odd){background:#efefef40}header{box-shadow:0 -1px 5px 1px;border-bottom:none}.project-header{margin-bottom:8px;margin-top:8px}.panel{background-color:#efefef40;border:1px solid #efefef40}.task-board{border-width:2px;background:#efefef22!important}div.task-board-recent{border-width:2px}table td{border:none}table th:first-child{border-top-left-radius:8px}table th:last-child{border-top-right-radius:8px}.table-list-header{background:#efefef40;border:1px solid #efefef40;border-radius:5px 5px 0 0;line-height:28px;padding-left:3px;padding-right:3px}.table-list-row{padding-left:3px;padding-right:3px;border-bottom:1px solid #efefef40;border-right:1px solid #efefef40}.table-list-row.table-border-left{border-left:1px solid #efefef40}.table-list-row:nth-child(odd){background:#efefef30}.table-list-row:hover{background:#efefef32;border-bottom:1px solid #efefef32;border-right:1px solid #efefef32}.dropdown-menu-link-icon{text-decoration:none}.dropdown-submenu-open li{border-bottom:1px solid #efefef40}.page-header h2{margin:0;padding:0;font-weight:700;border-bottom:1px dotted #efefef40}.sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef40;padding-left:13px}span.task-icon-age-total{border:1px solid #efefef40;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px}span.task-icon-age-column{border:1px solid #efefef40;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px}.subtask-cell{padding:4px 10px;border-top:1px dotted #efefef42;border-left:1px dotted #efefef40;display:table-cell;vertical-align:middle}table th{text-align:left;padding:.5em 3px;border:none;background:#efefef40}.views li{white-space:nowrap;background:#efefef40;border:none;border-right:none;padding:4px 8px;display:inline} \ No newline at end of file
diff --git a/plugins/Customizer/Assets/img/logo-gen.png b/plugins/Customizer/Assets/img/logo-gen.png
new file mode 100644
index 00000000..1df1986f
--- /dev/null
+++ b/plugins/Customizer/Assets/img/logo-gen.png
Binary files differ
diff --git a/plugins/Customizer/Assets/js/customizer.js b/plugins/Customizer/Assets/js/customizer.js
new file mode 100644
index 00000000..ae7c4f13
--- /dev/null
+++ b/plugins/Customizer/Assets/js/customizer.js
@@ -0,0 +1,248 @@
+//Functionality for a slider value feedback
+
+var header_logo_output = $('header_logo_output')[0];
+
+$(document).on('input', 'input[name="headerlogo_size"]', function(e) {
+ header_logo_output.innerHTML = e.currentTarget.value;
+ document.getElementById("hl1").style.height = e.currentTarget.value + "px";
+});
+
+var login_logo_output = $('login_logo_output')[0];
+
+$(document).on('input', 'input[name="loginlogo_size"]', function(e) {
+ login_logo_output.innerHTML = e.currentTarget.value;
+ document.getElementById("ll1").style.height = e.currentTarget.value + "px";
+});
+
+var av_icon_output = $('av_icon_output')[0];
+
+$(document).on('input', 'input[name="av_size"]', function(e) {
+ var siz = e.currentTarget.value;
+ av_icon_output.innerHTML = siz;
+ if (document.querySelector(".avatar-preview .avatar-letter") !== null) {
+ document.querySelector(".avatar-preview .avatar-letter").style.lineHeight = siz + "px";
+ document.querySelector(".avatar-preview .avatar-letter").style.width = siz + "px";
+ document.querySelector(".avatar-preview .avatar-letter").style.fontSize = (siz / 2) + "px";
+ } else {
+ var link = document.querySelector(".avatar-preview img").src;
+ var changedLink = link.substring(0, link.length-2);
+ document.querySelector(".avatar-preview img").src = changedLink + siz;
+ }
+});
+
+var av_radius_output = $('av_radius_output')[0];
+
+$(document).on('input', 'input[name="av_radius"]', function(e) {
+ var rad = e.currentTarget.value;
+ av_radius_output.innerHTML = rad;
+ if (document.querySelector(".avatar-preview .avatar-letter") !== null) {
+ document.querySelector(".avatar-preview .avatar-letter").style.borderRadius = rad + "%";
+ } else {
+ document.querySelector(".avatar-preview img").style.borderRadius = rad + "%";
+ }
+});
+
+var b_av_icon_output = $('b_av_icon_output')[0];
+
+$(document).on('input', 'input[name="b_av_size"]', function(e) {
+ var siz = e.currentTarget.value;
+ b_av_icon_output.innerHTML = siz;
+ if (document.querySelector(".b-avatar-preview .avatar-letter") !== null) {
+ document.querySelector(".b-avatar-preview .avatar-letter").style.lineHeight = siz + "px";
+ document.querySelector(".b-avatar-preview .avatar-letter").style.width = siz + "px";
+ document.querySelector(".b-avatar-preview .avatar-letter").style.fontSize = (siz / 2) + "px";
+ } else {
+ var link = document.querySelector(".b-avatar-preview img").src;
+ var changedLink = link.substring(0, link.length-2);
+ document.querySelector(".b-avatar-preview img").src = changedLink + siz;
+ }
+});
+
+var b_av_radius_output = $('b_av_radius_output')[0];
+
+$(document).on('input', 'input[name="b_av_radius"]', function(e) {
+ var rad = e.currentTarget.value;
+ b_av_radius_output.innerHTML = rad;
+ if (document.querySelector(".b-avatar-preview .avatar-letter") !== null) {
+ document.querySelector(".b-avatar-preview .avatar-letter").style.borderRadius = rad + "%";
+ } else {
+ document.querySelector(".b-avatar-preview img").style.borderRadius = rad + "%";
+ }
+});
+
+
+//Accordion for settings page
+
+document.addEventListener("DOMContentLoaded", function(event) {
+
+
+ var acc = document.getElementsByClassName("login-accordion");
+ var panel = document.getElementsByClassName('login-accordian-panel');
+
+ for (var i = 0; i < acc.length; i++) {
+ acc[i].onclick = function() {
+ var setClasses = !this.classList.contains('current');
+ setClass(acc, 'current', 'remove');
+ setClass(panel, 'show', 'remove');
+
+ if (setClasses) {
+ this.classList.toggle("current");
+ this.nextElementSibling.classList.toggle("show");
+ }
+ }
+ }
+
+ function setClass(els, className, fnName) {
+ for (var i = 0; i < els.length; i++) {
+ els[i].classList[fnName](className);
+ }
+ }
+});
+
+// Valid for the form id="settings" checkboxes, when the "change" event occurs, the module is sent.
+
+$(document).ready(function(){
+ $("#settings").on("change", "input:checkbox", function(){
+ $("#settings").submit();
+ });
+});
+
+// auto submit on change for user theme
+if (document.getElementById('userthemeSelection')) {
+ document.getElementById('userthemeSelection').onchange = function() {
+ document.getElementById('ts').submit();
+ }
+}
+
+//Live Preview
+
+if (document.getElementById('loginpanel_color')) {
+ document.getElementById('loginpanel_color').oninput = function() {
+ document.getElementById('preview-form-login').style.backgroundColor = document.getElementById('loginpanel_color').value;
+ document.getElementById('preview-form-note').style.backgroundColor = document.getElementById('loginpanel_color').value;
+ }
+
+ document.getElementById('loginbackground_color').oninput = function() {
+ document.getElementById('preview').style.backgroundColor = document.getElementById('loginbackground_color').value;
+ }
+
+ document.getElementById('form-login_note').oninput = function() {
+ document.getElementById('preview-form-note').innerHTML = document.getElementById('form-login_note').value;
+ }
+
+ document.getElementById('login_shadow_color').oninput = function() {
+ var slider = document.getElementById("login_shadow").value;
+ var color = document.getElementById('login_shadow_color').value;
+ document.getElementById('preview-form-login').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ document.getElementById('preview-form-note').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ }
+
+ document.getElementById('login_border_color').oninput = function() {
+ document.getElementById('preview-form-login').style.borderColor = document.getElementById('login_border_color').value;
+ document.getElementById('preview-form-note').style.borderColor = document.getElementById('login_border_color').value;
+ }
+
+ document.getElementById('login_btn_color').oninput = function() {
+ document.getElementById('preview-login-btn').style.backgroundColor = document.getElementById('login_btn_color').value;
+ }
+
+ document.getElementById('login_btn_shadow_color').oninput = function() {
+ var slider = document.getElementById("login_btn_shadow").value;
+ var color = document.getElementById('login_btn_shadow_color').value;
+ document.getElementById('preview-login-btn').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ }
+
+ document.getElementById('login_btn_font_color').oninput = function() {
+ document.getElementById('preview-login-btn').style.color = document.getElementById('login_btn_font_color').value;
+ }
+
+ document.getElementById('login_btn_shade_color').oninput = function() {
+ var color = document.getElementById('login_btn_shade_color').value;
+ document.getElementById('preview-login-btn').style.backgroundImage = 'linear-gradient(-180deg, transparent 0%, ' + color + ' 90%';
+ }
+
+ document.getElementById('login_btn_shadow').oninput = function() {
+ var slider = document.getElementById("login_btn_shadow").value;
+ var color = document.getElementById('login_btn_shadow_color').value;
+ document.getElementById('preview-login-btn').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ }
+
+ document.getElementById('login_btn_border').oninput = function() {
+ var slider = document.getElementById("login_btn_border").value;
+ var color = document.getElementById('login_btn_border_color').value;
+ document.getElementById('preview-login-btn').style.border = slider + 'px solid ' + color;
+ }
+
+ document.getElementById('login_border').oninput = function() {
+ var slider = document.getElementById("login_border").value;
+ var color = document.getElementById('login_border_color').value;
+ document.getElementById('preview-form-login').style.border = slider + 'px solid ' + color;
+ document.getElementById('preview-form-note').style.border = slider + 'px solid ' + color;
+ }
+
+ document.getElementById('login_shadow').oninput = function() {
+ var slider = document.getElementById("login_shadow").value;
+ var color = document.getElementById('login_shadow_color').value;
+ document.getElementById('preview-form-login').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ document.getElementById('preview-form-note').style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + color;
+ }
+
+ document.getElementById('login_btn_width').oninput = function() {
+ var slider = document.getElementById("login_btn_width").value;
+ document.getElementById('preview-login-btn').style.width = slider + 'px';
+ }
+
+ document.getElementById('preview-login-btn').onmouseover = function() {
+ document.getElementById('preview-login-btn').style.color = document.getElementById('login_btn_color').value;
+ document.getElementById('preview-login-btn').style.backgroundColor = document.getElementById('login_btn_font_color').value;
+ }
+
+ document.getElementById('preview-login-btn').onmouseout = function() {
+ document.getElementById('preview-login-btn').style.backgroundColor = document.getElementById('login_btn_color').value;
+ document.getElementById('preview-login-btn').style.color = document.getElementById('login_btn_font_color').value;
+ }
+
+ document.getElementById('form-background_url').oninput = function() {
+ var val = document.getElementById("form-background_url").value;
+ document.getElementById('preview').style.background = 'url("' + val +'") no-repeat center center';
+ document.getElementById('preview').style.backgroundSize = 'cover';
+ }
+}
+
+function OnColorChanged(selectedColor, inputId) {
+ if (inputId == "login_shadow_color") {
+ var slider = document.getElementById("login_shadow").value;
+ document.getElementById("preview-form-login").style.color = selectedColor;
+ document.getElementById("preview-form-note").style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + selectedColor;
+ }
+ if (inputId == "loginbackground_color") {
+ var rgbaBox = document.getElementById("preview");
+ rgbaBox.style.backgroundColor = selectedColor;
+ }
+ if (inputId == "loginpanel_color") {
+ document.getElementById("preview-form-login").style.backgroundColor = selectedColor;
+ document.getElementById("preview-form-note").style.backgroundColor = selectedColor;
+ }
+ if (inputId == "login_border_color") {
+ document.getElementById("preview-form-login").style.borderColor = selectedColor;
+ document.getElementById("preview-form-note").style.borderColor = selectedColor;
+ }
+ if (inputId == "login_btn_color") {
+ var rgbaBox = document.getElementById("preview-login-btn");
+ rgbaBox.style.backgroundColor = selectedColor;
+ }
+ if (inputId == "login_btn_shadow_color") {
+ var rgbaBox = document.getElementById("preview-login-btn");
+ var slider = document.getElementById("login_btn_shadow").value;
+ rgbaBox.style.boxShadow = '0px 0px ' + slider + 'px ' + slider * 0.1 + 'px ' + selectedColor;
+ }
+ if (inputId == "login_btn_font_color") {
+ var rgbaBox = document.getElementById("preview-login-btn");
+ rgbaBox.style.color = selectedColor;
+ }
+ if (inputId == "login_btn_shade_color") {
+ var rgbaBox = document.getElementById("preview-login-btn");
+ rgbaBox.style.backgroundImage = 'linear-gradient(-180deg, transparent 0%, ' + selectedColor + ' 90%';
+ }
+}
+
diff --git a/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.css b/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.css
new file mode 100644
index 00000000..6db3d7b3
--- /dev/null
+++ b/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.css
@@ -0,0 +1,167 @@
+/*! Menucool rgba Color Picker v2018.9.12. www.menucool.com/rgba-color-picker */
+
+input.color{
+ width:110px;
+ height: 18px;
+ font-size:11px;
+ border:1px solid #dadada;
+ box-sizing:content-box;
+ padding-left:4px;
+}
+
+#colorpicker
+{
+ padding: 20px;
+ position: absolute;
+ top: 22px;
+ left:auto; right:0;/*If need align to the right, set: left:0;right:auto; */
+ background-color: #FFF;
+ border: 1px solid #BBB;
+ display: none;
+ z-index: 200;
+ box-sizing:content-box;
+ font: normal 10px verdana;
+ color: #666;
+ border-radius:4px;
+ box-shadow:0 1px 8px rgba(0,0,0,0.5);
+}
+
+input.color.up + .colorChooser > #colorpicker {
+ top: -306px;
+}
+
+#colorpicker div
+{
+ float:left;
+ padding:0;
+ box-sizing:content-box;
+}
+#colorpicker div.clear, #colorpicker div.separator
+{
+ float: none;
+ clear: both;
+ border: 0;
+ overflow:hidden;
+ height:0;
+ font-size:0;
+}
+#colorpicker div.separator
+{
+ margin-bottom:8px;
+}
+
+#colorpicker #colorContainer
+{
+ border:0;
+ border-right: 1px solid black;
+ border-bottom: 1px solid black;
+ cursor: pointer;
+ font-size:0;
+ width:234px;
+}
+#colorContainer div
+{
+ border:0;
+ border-top: solid 1px black;
+ border-left: solid 1px black;
+ width:12px;
+ height:12px;
+ overflow:hidden;
+}
+#colorpicker .w1, #colorpicker .w2 {
+ width:80px;
+ padding-left:4px;
+ border: 1px solid #999;
+ border-radius:3px;
+ position:relative;
+}
+div.w1, div.w2 {
+ height: 22px;
+ line-height: 22px;
+}
+input.w1, input.w2 {
+ font: normal 10px verdana;
+ height: 18px;
+ line-height: 18px;
+}
+#colorpicker .w2 {
+ width:120px;
+ float:right;
+
+}
+#colorpicker div.w2::before {
+ content: " ";
+ width:100%;
+ height:22px;
+ position:absolute;
+ left:0;top:0;
+ z-index:-1;
+}
+.opacitySpan {display:inline-block;padding-top:4px;}
+
+input.rgbaRange {
+ width:118px;
+ margin-left:20px;
+ vertical-align:middle;
+}
+/*brennaobrien.com/blog/2014/05/style-input-type-range-in-every-browser.html*/
+input[type=range]{
+ -webkit-appearance: none;
+ border:none;
+}
+
+input[type=range]::-webkit-slider-runnable-track {
+ width: 118px;
+ height: 5px;
+ background: #ddd;
+ border: none;
+ border-radius: 2px;
+}
+
+input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ border: none;
+ height: 15px;
+ width: 15px;
+ border-radius: 50%;
+ background: #888;
+ margin-top: -5px;
+}
+
+input[type=range]:focus {
+ outline: none;
+}
+
+input[type=range]:focus::-webkit-slider-runnable-track {
+ background: #ccc;
+}
+
+#colorpicker .btnOK {
+ width:40px;
+ float:right;
+ height:22px;
+ font-size: 12px;
+ vertical-align:middle;
+ cursor:pointer;
+}
+
+.transChooser, #colorpicker div.w2::before {
+ background: white url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB90RVh0U29mdHdhcmUATWFjcm9tZWRpYSBGaXJld29ya3MgOLVo0ngAAAAWdEVYdENyZWF0aW9uIFRpbWUAMDkvMDcvMTgBUiN5AAAAKklEQVQokWNkYGDwZcAC/v//j02YgQmrKB4wHDSw4AoNRkZG6tgwHDQAAMuIBmTkVeR2AAAAAElFTkSuQmCC') repeat;
+}
+
+/*Click span.colorChooser will popup the div#colorpicker */
+.colorChooser {
+ width: 46px;
+ height: 20px;
+ border: 1px solid rgba(0,0,0,0.3);
+ display: inline-block;
+ overflow: visible;
+ vertical-align: middle;
+ position: relative;
+ box-sizing: content-box;
+ border-radius: 0 3px 3px 0;
+ user-select: none;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAICAYAAAAftBSpAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAAB90RVh0U29mdHdhcmUATWFjcm9tZWRpYSBGaXJld29ya3MgOLVo0ngAAAAWdEVYdENyZWF0aW9uIFRpbWUAMDkvMjIvMThMTIArAAAAvklEQVQokZXMrw6CUBiG8dc/xUICIpliOHMjH4vZeQU2LV6BmWD2DrwYNgMJyqGQSNC4gMfEpk5xftvbnu83ASTpLmmu/y+QtBYgYO+cI4oiJP2c53lkWQZwAxYDIuBUliW+748CQRCQ5znAFfABPSMCjm3bEsfxRyAMQ5qmATg//70jU2BXFAVJkrwAxhiccwAHYDaGDNv0fY+1FklYa6nrGmD7qf+GCDBVVZGmKV3XAay+tWOIAANcgOVY9wCltDLGu2gdigAAAABJRU5ErkJggg==');
+ background-position:center center;
+ background-repeat:no-repeat;
+}
diff --git a/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.js b/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.js
new file mode 100644
index 00000000..6e7de940
--- /dev/null
+++ b/plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.js
@@ -0,0 +1,492 @@
+/*r
+ColorPickerCallback:cpC
+element:r
+hexInput:a
+bgDiv1:b
+bgDiv2:c
+initIt:d
+dColorContainer:e
+appendSeparator:f
+createColorDivs:g
+stopPropagation:h
+addColorElements:j
+createElm:k
+
+clrContainerEvtHdler_getSelectedColorAndThenUpdateInnerValues:m
+
+createDropdownSpans:o
+
+
+initColorPickerValues:s
+setColorPickerInsideValues:t
+
+rgbaInput:v
+iRgbaRange:w
+getParsedColors:x
+r*/
+
+/*
+Knowledge: 1. backgound-color:none; color:none; are not a valid color.
+ 2. Convert rgba to similar hex color: https://stackoverflow.com/questions/15898740/how-to-convert-rgba-to-a-transparency-adjusted-hex
+*/
+
+/*! Menucool rgba Color Picker v2018.9.23. menucool.com/rgba-color-picker */
+var MenuCoolRgbaColorPickerOptions = {
+ initOnPageLoad: true
+};
+
+var rgbaColorPicker = (function (myOptions) {
+ 'use strict';
+ // Private members
+ var addEvent = function (elem, evtType, func) {
+ if (elem.addEventListener) {
+ elem.addEventListener(evtType, func, false);
+ } else if (elem.attachEvent) {
+ elem.attachEvent("on" + evtType, func);
+ }
+ else {
+ // for IE/Mac, NN4, and older
+ elem["on" + evtType] = func;
+ }
+ };
+ var classIsColor = function (myClass) {
+ if (!myClass) return 0;
+ var pattern = /\bcolor\b/;
+ return pattern.test(myClass);
+ };
+ var len = "length";
+ var documentCreateElement = function (tagName) {
+ return document.createElement(tagName);
+ };
+ var hexToRgb = function (hex) {
+ var retVal = 0;
+ if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
+ var c = hex.substring(1).split('');
+ if (c[len] == 3) {
+ c = [c[0], c[0], c[1], c[1], c[2], c[2]];
+ }
+ c = '0x' + c.join('');
+ retVal = [(c >> 16) & 255, (c >> 8) & 255, c & 255];
+ }
+ return retVal;
+ };
+ var hexAlphaToRgba = function (hex, alpha) {
+ var retVal = hexToRgb(hex);
+ return retVal ? 'rgba(' + retVal.join(',') + ',' + alpha + ')' : _invalid;
+ };
+
+ var rgbaToSimilarHex = function (rgba, bgHexColor) {
+ var hex = '';
+ var match = rgba.match(/rgba\((\d+),(\d+),(\d+),([.\d]+)/i);
+ if (match) {
+ var rgbBg = hexToRgb(bgHexColor);
+ var rgbConverted = [];
+ var alpha = +match[4];
+ //now convert hex+alpha to similar hex. //rgba to similar rbg: Color = Color * alpha + Bkg * (1 - alpha);
+ for (var i = 0; i < 3; i++) {
+ rgbConverted.push(Math.floor(+match[i+1] * alpha + (+rgbBg[i]) * (1 - alpha)));
+ }
+ hex = rgbToHex(rgbConverted.join(','));
+ //console.log(hex, rgba);
+ }
+ return hex;
+ };
+
+ var componentToHex = function (c) {
+ var hex = c.toString(16).toUpperCase();
+ return hex[len] == 1 ? "0" + hex : hex;
+ };
+ var rgbToHex = function (rgbStr) {
+ var rgb = rgbStr.split(',');
+ return "#" + componentToHex(+rgb[0]) + componentToHex(+rgb[1]) + componentToHex(+rgb[2]);
+ };
+ /* //jsfiddle.net/salman/f9Re3/
+ var invertColor = function (color) {
+ color = parseInt(color.substring(1), 16); // convert to integer
+ color = 0xFFFFFF ^ color; // invert three bytes
+ color = color.toString(16); // convert to hex
+ color = "#" + ("000000" + color).slice(-6); // pad with leading zeros, also prepend #
+ return color;
+ };*/
+ var validColorNumbers = function (nums) {
+ var numArr = nums.split(',');
+ for (var i = 0; i < numArr[len]; i++) {
+ if (+numArr[i] < 0 || +numArr[i] > 255) return 0;
+ }
+ return 1;
+ };
+
+ var isiOS = (navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false);
+
+ var clickOrTouch = isiOS ? "touchstart" : "click";
+
+ var clsNm = "className", sty = "style", bgClr = "backgroundColor", dspl = "display", val = "value", _invalid = "invalid", appendCld = "appendChild", trans = "transparent", undef = "undefined";
+
+ var getStyle = function (elm) {
+ if (window.getComputedStyle) //modern browsers
+ var computedStyle = window.getComputedStyle(elm, null); //null can be replaced with pseudo such as 'hover'
+ else if (elm.currentStyle) //IE
+ computedStyle = elm.currentStyle;
+ else
+ computedStyle = elm[sty];
+
+ return computedStyle;
+ };
+
+ var picker, //the picker instance
+ HexInSession;//unconfirmed chosen hex color
+
+ var btnOKClickHandler = function () {
+ if (picker) {
+ if (+picker.iRgbaRange[val] == 1)
+ var color = picker.hexInput[val];
+ else
+ color = picker.rgbaInput[val];
+ picker.R[picker.i][val] = color;
+ picker.R[picker.i].onchange();
+ picker.element[sty][dspl] = "none";
+ if (typeof OnColorChanged !== "undefined") OnColorChanged(color, picker.R[picker.i].id);
+ }
+ };
+
+ //Picker class constructor
+ var Picker = function () {
+ var that = this;
+ that.hexInput = //div elelment for displaying selected color as its text
+ that.bgDiv1 = //div elelment for displaying selected color as its background
+ that.bgDiv2 =
+ that.dColorContainer = null; //div element containing colors
+ that.i = -1; //current target index
+ that.R = []; //target inputs that class contains "color"
+ that.S = []; //span elements after the R target inputs: the chooser
+
+ that.initIt();
+ };
+
+ Picker.prototype = {
+ appendSeparator: function (div) {
+ var sep = documentCreateElement("div");
+ if (!div) {
+ div = this.element;
+ sep[clsNm] = "separator";
+ }
+ else
+ sep[clsNm] = "clear";
+ div[appendCld](sep);
+ },
+
+ createColorDivs: function (r, b, g) {
+ var colorCell = documentCreateElement("div");
+ if (r == "TT") {
+ colorCell[clsNm] = "transChooser";
+ colorCell.setAttribute("rgb", trans);
+ }
+ else {
+ colorCell[sty][bgClr] = "#" + r + g + b;
+ colorCell.setAttribute("rgb", "#" + r + g + b);
+ }
+ return colorCell;
+ },
+
+ stopPropagation: function (e) {
+ e = e ? e : window.event;
+ e.cancelBubble = true;
+ if (e.stopPropagation) e.stopPropagation();
+ },
+
+ addColorElements: function () {
+ var that = this;
+ var colorCell;
+ var div = that.dColorContainer;
+
+ //add grayscales to div
+ var grays = ["00", "11", "22", "33", "44", "55", "66", "77", "88", "99", "AA", "BB", "CC", "DD", "EE", "F6", "FF", "TT"];
+ for (var a = 0; a < 18; a++) {
+ colorCell = that.createColorDivs(grays[a], grays[a], grays[a]);
+ div[appendCld](colorCell);
+ }
+ that.appendSeparator(div);
+
+ //add colors to div (first group)
+ var c = ["00", "33", "66", "99", "CC", "FF"];
+ for (var b = 0; b < 6; b++) {
+ for (var r = 0; r < 3; r++) {
+ for (var g = 0; g < 6; g++) {
+ colorCell = that.createColorDivs(c[r], c[g], c[b]);
+ div[appendCld](colorCell);
+ }
+ }
+ that.appendSeparator(div);
+ }
+ that.appendSeparator(div);
+
+ //add colors to div (second group)
+ for (var b = 0; b < 6; b++) {
+ for (var r = 3; r < 6; r++) {
+ for (var g = 0; g < 6; g++) {
+ colorCell = that.createColorDivs(c[r], c[g], c[b]);
+ div[appendCld](colorCell);
+ }
+ }
+ that.appendSeparator(div);
+ }
+ },
+ //type: null-div, 1-span, 2-input[type=text], 3-input[type=range], 4-button
+ createElm: function (id, type) {
+ var tagName;
+ switch (type) {
+ case 1:
+ tagName = "span";
+ break;
+ case 2:
+ case 3:
+ tagName = "input";
+ break;
+ case 4:
+ tagName = "button";
+ break;
+ default:
+ tagName = "div";
+ break;
+ }
+ var el = documentCreateElement(tagName);
+ if (id[0] == '#')
+ el.id = id.substring(1);
+ else
+ el[clsNm] = id;
+
+ if (type == 2) {
+ el.type = "text";
+ el.setAttribute("spellcheck", "false");
+ }
+ else if (type == 3) {
+ el.type = "range";
+ }
+ if (id != "#colorpicker" && id != "colorChooser")
+ this.element[appendCld](el);
+ return el;
+ },
+
+ initIt: function () {
+ var that = this;
+ // 1. create color picker
+ that.element = that.createElm("#colorpicker");
+ addEvent(that.element, clickOrTouch, that.stopPropagation);
+
+ that.bgDiv1 = that.createElm("w1");
+ that.bgDiv2 = that.createElm("w2");
+ that.appendSeparator();//----------
+ that.hexInput = that.createElm("w1", 2);
+ that.rgbaInput = that.createElm("w2", 2);
+ that.appendSeparator();//----------
+ var btnOK = that.createElm("btnOK", 4);//it will float to right
+ btnOK.setAttribute("type", "button");//avoid submitting form
+ btnOK.innerHTML = "OK";
+ var opacitySpan = that.createElm("opacitySpan", 1);
+ opacitySpan.innerHTML = "Opacity";
+ that.iRgbaRange = that.createElm("rgbaRange", 3);
+ that.appendSeparator();//----------
+
+ that.dColorContainer = that.createElm("#colorContainer");
+ addEvent(that.dColorContainer, "mouseover", function (e) { that.clrContainerEvtHdler_getSelectedColorAndThenUpdateInnerValues(e, 1); });
+ addEvent(that.dColorContainer, "mouseout", function (e) { that.clrContainerEvtHdler_getSelectedColorAndThenUpdateInnerValues(e, 2); });
+ //The following eventType ==3 will also update HexInSession
+ addEvent(that.dColorContainer, clickOrTouch, function (e) { that.clrContainerEvtHdler_getSelectedColorAndThenUpdateInnerValues(e, 3); });
+
+ //that.iRgbaRange.type = "range";
+ that.iRgbaRange[val] = 1;
+ that.iRgbaRange.min = 0;
+ that.iRgbaRange.max = 1;
+ that.iRgbaRange.step = 0.1;
+ addEvent(that.iRgbaRange, "input", function () { that.setColorPickerInsideValues(that.hexInput[val]); });
+
+
+ // 2. populate color picker cells
+ that.addColorElements();
+
+ // 3. create spans after all class="color" elements, set popup events for them
+ that.createDropdownSpans();
+
+ // 4. click on body will hide the #colorpicker
+ //I add this event to document.documentElement other than document.body as sometimes I clicked <html> instead of <body>
+ addEvent(document.documentElement, clickOrTouch, function () { that.element[sty][dspl] = "none"; });
+ addEvent(btnOK, clickOrTouch, btnOKClickHandler);
+
+ //if (typeof OnColorPickerLoaded != undef) OnColorPickerLoaded();
+
+ },
+
+ clrContainerEvtHdler_getSelectedColorAndThenUpdateInnerValues: function (e, eventType) {
+ if (eventType == 2) //mouseout of colorContainer
+ {
+ var selectedColor = HexInSession;
+ }
+ else{ //mouseover, or clickOrTouch
+ if (e.target) var target = e.target; //recognized by all except IE
+ else target = e.srcElement; //IE only knows srcElement. Chrome, Safari, Opera also knows it.
+ if (target.id != "colorContainer"){
+ selectedColor = target.getAttribute("rgb");
+ if (eventType == 3) HexInSession = selectedColor;
+ }
+ }
+ if(selectedColor)
+ picker.setColorPickerInsideValues(selectedColor);
+ //picker.stopPropagation(e);//don't need it anymore as we now have: addEvent(that.element, "click", that.stopPropagation);
+ },
+
+ // create choosers(span) after all class="color" elements, set popup events for them
+ createDropdownSpans: function () {
+ var colorInputs = document.getElementsByTagName("input");
+ var that = this;
+ for (var j = 0; j < colorInputs[len]; j++) {
+ if (classIsColor(colorInputs[j][clsNm])) { //search all input element who has "color" class
+ var i = that.R[len];
+ that.R[i] = colorInputs[j]; //that.R.push(colorInputs); will make the R[i].parentNode throw exception
+ that.R[i].i = i;
+ //create choosers
+ that.S[i] = that.createElm("colorChooser", 1);
+ that.S[i].i = i;
+ that.S[i].id = colorInputs[j]['name'];
+
+ //that.S[i].arrow = that.createElm("colorChooserArrow", 1);
+ //that.S[i][appendCld](that.S[i].arrow);
+
+ that.R[i].parentNode.insertBefore(that.S[i], that.R[i].nextSibling);
+ that.S[i][sty][bgClr] = that.R[i][val];
+ addEvent(that.S[i], clickOrTouch, function (e) {
+ if (that.element.parentNode == this && that.element[sty][dspl] == "block") that.element[sty][dspl] = "none";
+ else {
+ that.i = this.i;
+ this[appendCld](that.element)[sty][dspl] = "block";
+ that.initColorPickerValues();
+ }
+ that.stopPropagation(e);
+ });
+
+ //now we know what is S, so we can add the following event
+ that.R[i].onchange = function () {
+ that.S[this.i][sty][bgClr] = this[val];
+ };
+
+ if (typeof OnColorChanged !== "undefined") OnColorChanged(that.R[i][val], that.R[i].id);
+ }
+ }
+ },
+
+
+ //called by chooser click event handler: it will set values and BGs based on R[i][val]
+ initColorPickerValues: function () {
+ var that = this;
+ var parsedColor = that.getParsedColors(that.R[that.i][val]);
+ HexInSession = parsedColor[0];
+ that.iRgbaRange[val] = parsedColor[len] == 2 ? parsedColor[1] : 1;
+ if (HexInSession == _invalid) {
+ that.bgDiv1[sty][bgClr] = that.bgDiv2[sty][bgClr] = trans;
+ that.hexInput[val] = that.rgbaInput[val] = _invalid;
+ }
+ else
+ that.setColorPickerInsideValues(HexInSession);
+
+ },
+ ///possible return values: [''], ['transparent'], ['invalid'], ['#......'], ['#......', num <=1]
+ getParsedColors: function (color) { //,chooser
+ color = color.replace(/\s+/g, '').toLowerCase();
+ var retVal = [_invalid];
+ if (!color || color == trans) {
+ retVal = [color];
+ }
+ else if (color[0] == '#') {
+ if (/^#([a-f0-9]{3}){1,2}$/.test(color)) {
+ retVal = [color];
+ }
+ }
+ else if (/^rgba\(\d+,\d+,\d+,[\.\d]+\)$/.test(color)) {
+ var match = color.match(/^rgba\((\d+,\d+,\d+),([\.\d]+)\)$/);
+ if (match) {
+ if (validColorNumbers(match[1]) && +match[2] <= 1) {
+ retVal = [rgbToHex(match[1]), +match[2]];
+ }
+ }
+ }
+ else if (/^rgb\(\d+,\d+,\d+\)$/.test(color)) {
+ var match = color.match(/^rgb\((\d+,\d+,\d+)\)$/);
+ if (validColorNumbers(match[1])) {
+ retVal = [rgbToHex(match[1])];
+ }
+ }
+ else { //color name such as red, gray. Chrome will change bad name to 'rgba(0,0,0,0)', IE will be 'transparent'
+ var testBox = this.bgDiv1; //chooser ? chooser : this.bgDiv1;
+ testBox[sty][bgClr] = trans;//why having this line of code? It is a bug fix: If color is not a valid color, the next line won't change the previous color, and computedColor will still get the previous color. So we first change it to "transparent"
+ testBox[sty][bgClr] = color;
+ var computedColor = getStyle(testBox)[bgClr];
+
+ //console.log("computedColor", computedColor, color, chooser);
+ if (computedColor.indexOf('rgb(') != -1) {
+ retVal = [rgbToHex(computedColor.replace('rgb(', '').replace(')', ''))];
+ }
+ }
+ return retVal;
+ },
+
+ //called by many events. It will update two inputs and two bg divs
+ setColorPickerInsideValues: function (hex) {
+ var alpha = +picker.iRgbaRange[val];
+ picker.bgDiv1[sty][bgClr] = picker.hexInput[val] = (hex && hex[0] == '#' ? hex.toUpperCase() : hex);
+ picker.bgDiv2[sty][bgClr] = picker.rgbaInput[val] = (hex && hex[0] == '#' ? hexAlphaToRgba(hex, alpha) : hex);//the later hex will be (!hex || hex == 'transparent')
+ }
+ };
+
+ var buildPicker = function () {
+ if (!picker) picker = new Picker();
+ /*if (isiOS) {
+ //stackoverflow.com/questions/17567344/detect-left-right-swipe-on-touch-devices-but-allow-up-down-scrolling
+ //The following defines the touch event handlers for: fast click : 'fc', swipe horizontal: 'swh', swipe vertical: 'swv'
+ (function (d) {
+ var
+ ce = function (e, n) { var a = document.createEvent("CustomEvent"); a.initCustomEvent(n, true, true, e.target); e.target.dispatchEvent(a); a = null; return false },
+ nm = true, sp = { x: 0, y: 0 }, ep = { x: 0, y: 0 },
+ touch = {
+ touchstart: function (e) { sp = { x: e.touches[0].pageX, y: e.touches[0].pageY } },
+ touchmove: function (e) { nm = false; ep = { x: e.touches[0].pageX, y: e.touches[0].pageY } },
+ touchend: function (e) { if (nm) { ce(e, 'fc') } else { var x = ep.x - sp.x, xr = Math.abs(x), y = ep.y - sp.y, yr = Math.abs(y); if (Math.max(xr, yr) > 20) { ce(e, (xr > yr ? 'swh' : 'swv')) } }; nm = true },
+ touchcancel: function (e) { nm = false }
+ };
+ for (var a in touch) { d.addEventListener(a, touch[a], false); }
+ })(picker.element);
+ }*/
+ };
+
+ if (myOptions.initOnPageLoad)
+ addEvent(window, "load", buildPicker);
+
+ return {
+ //reload code below not work. Maybe the removed stuff is still there as it might be referenced somewhere else. So comment it out!
+ //reload: function () {
+ // if (picker.S && picker.S.length) {
+ // console.log(picker.S.length);
+ // for (var i = 0; i < picker.S.length; i++) {
+ // picker.S[i].parentNode.removeChild(picker.S[i]);
+ // picker.S[i] = null;
+ // }
+ // }
+ // picker.R=picker.S=[];
+ // picker.createDropdownSpans();
+ //},
+ hexAlphaToRgba: hexAlphaToRgba, //(hex, alpha) such as hexAlphaToRgba("#DD9999", 0.5)
+ rgbToHex: rgbToHex, //(rgbStr) such as rgbToHex("255,0,0")
+ rgbaToHex: rgbaToSimilarHex, //(rgba, bgHexColor) such as rgbaToSimilarHex("rgba(255,0,0,0.2)", "#DD9999")
+ init: buildPicker //when option is !MenuCool.cpInitOnLoad, you need to call init() manually when you are ready to populate color picker
+ };
+})(MenuCoolRgbaColorPickerOptions);
+
+/*
+ChangeSet #1 (2012-2-10): I use colorContainerEventDelegate to replace each color cell's onclick event. Greatly decreased the number of event listeners.
+ChangeSet #2 (2012-2-10): Use stopPropagation instead of timer to fix the event bubbling to body that will hide the color picker.
+ChangeSet #3 (2012-2-11): add if(target.id!="colorContainer") to fix the IE7 bug that the even responde to element "colorContainer".
+ChangeSet #4 (2012-6-27): add to support Ajax by calling reload. Requested by Birger on 6.26 (check email)
+ChangeSet #5 (2012-6-28): add transparent color to mcColorPicker.
+ChangeSet #6 (2012-8-30): Don't need the menucool link anymore.
+ChangeSet #7 (2014-9-29): DOMContentLoaded instead of window.onload; added DOMContentLoaded handler
+ChangeSet #8 (2014-10-27): If color fields are added quite late like my ddmenu skinBuilder, the buildPicker cannot run onDomReady or even window.onload. So I added option cpInitOnLoad and init() function.
+ChangeSet #9 (2014-11-??): Click color chooser will toggle the display.
+*/
diff --git a/plugins/Customizer/Controller/CustomizerConfigController.php b/plugins/Customizer/Controller/CustomizerConfigController.php
new file mode 100644
index 00000000..d816384d
--- /dev/null
+++ b/plugins/Customizer/Controller/CustomizerConfigController.php
@@ -0,0 +1,388 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Controller;
+
+require_once __DIR__.'/../vendor/autoload.php';
+
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\LanguageModel;
+use Kanboard\Controller\BaseController;
+use luizbills\CSS_Generator\Generator as CSS_Generator;
+
+/**
+ * Config Controller
+ *
+ * @package Customizer/Controller
+ * @author creecros
+ */
+class CustomizerConfigController extends BaseController
+{
+
+ /**
+ * Save settings
+ *
+ */
+ public function save()
+ {
+ if (isset($_POST['remove'])) {
+ $values = $this->request->getValues();
+ $this->remove($values['themeSelection']);
+ } else {
+
+ $values = $this->request->getValues();
+
+ if (array_key_exists('use_custom_login', $values) === false) { $this->configModel->save(['use_custom_login' => '']); }
+ if (array_key_exists('enable_cache', $values) === false) { $this->configModel->save(['enable_cache' => '']); }
+
+ if ($this->configModel->save($values)) {
+ $this->languageModel->loadCurrentLanguage();
+ $this->flash->success(t('Settings saved successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to save your settings.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+ }
+
+ /**
+ * Save user theme
+ *
+ */
+ public function usertheme()
+ {
+ $user = $this->getUser();
+ $values = $this->request->getValues();
+
+ if ($this->userMetadataModel->save($user['id'], $values)) {
+ $this->languageModel->loadCurrentLanguage();
+ $this->flash->success(t('Settings saved successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to save your settings.'));
+ }
+
+ $this->response->redirect($this->helper->url->to('UserViewController', 'show', array('user_id' => $user['id'])), true);
+
+ }
+
+ /**
+ * Reset all User themes
+ *
+ */
+ public function resetUserThemes()
+ {
+ $users = $this->userModel->getAll();
+ foreach ($users as $user) {
+ $this->userMetadataModel->remove($user['id'], 'themeSelection');
+ }
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+
+ /**
+ * Toggle User themes
+ *
+ */
+ public function enableDisableThemes()
+ {
+ $status = $this->configModel->get('toggle_user_themes', 'disable');
+
+ if ($status == 'disable') { $this->configModel->save(['toggle_user_themes' => 'enable']); } else { $this->configModel->save(['toggle_user_themes' => 'disable']); }
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+
+
+ /**
+ * Upload css theme
+ *
+ */
+ public function uploadcss()
+ {
+ $target_dir = DATA_DIR . '/files/customizer/themes/';
+ $target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
+ $uploadOk = 1;
+ $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
+ // Check if file already exists
+ if (file_exists($target_file)) {
+ $this->flash->failure(t('Sorry, file already exists.'));
+ $uploadOk = 0;
+ }
+ // Check file size
+ if ($_FILES["fileToUpload"]["size"] > 1000000) {
+ $this->flash->failure(t('Sorry, your file is too large.'));
+ $uploadOk = 0;
+ }
+ // Allow certain file formats
+ if($imageFileType != "css") {
+ $this->flash->failure(t('Sorry, only CSS files are allowed.'));
+ $uploadOk = 0;
+ }
+ // Check if $uploadOk is set to 0 by an error
+ if ($uploadOk == 0) {
+ $this->flash->failure(t('Sorry, your file was not uploaded.'));
+ // if everything is ok, try to upload file
+ } else {
+ if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
+ $this->flash->success(t('Theme file uploaded successfully.'));
+ } else {
+ $this->flash->failure(t('Sorry, there was an error uploading your file.'));
+ }
+ }
+
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+
+ public function remove($file)
+ {
+ $filename = basename($file);
+ if (file_exists(DATA_DIR . '/files/customizer/themes/' . $filename)) { unlink(DATA_DIR . '/files/customizer/themes/' . $filename); }
+ unlink($file);
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+
+ public function create_theme()
+ {
+ $values = $this->request->getValues();
+
+ $options = [
+ // default values
+ // 'indentation' => ' ', // 4 spaces
+ ];
+
+ $css = new CSS_Generator($options);
+
+ // Header
+ $css->add_rule('header',
+ [
+ 'background-color' => $values['header_background'],
+ 'background-image' => 'linear-gradient(-180deg, transparent 0%, '.$values['header_shade'].' 90%)'
+ ]
+ );
+ $css->add_rule(['header h1', 'header a .fa'],
+ [
+ 'color' => $values['header_title']
+ ]
+ );
+ $css->add_rule('a i.web-notification-icon',
+ [
+ 'color' => $values['notification_icon']
+ ]
+ );
+ // Body
+ $css->add_rule('body',
+ [
+ 'background' => $values['background_color'],
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule(['ul.dropdown-submenu-open', '.accordion-title h3'],
+ [
+ 'background-color' => $values['background_color']
+ ]
+ );
+ $css->add_rule('.dropdown-submenu-open a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('a:hover',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('a .fa',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule(['h1', 'h2', 'h3', '.accordion-toggle'],
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.table-list-header a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.table-list-header .table-list-header-count',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.table-list-row .table-list-title a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.table-list-row .table-list-details strong',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.dropdown-menu-link-icon',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.page-header h2 a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.sidebar>ul a:hover',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.sidebar>ul li.active a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.task-list-icons a:hover',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.task-list-icons a:hover i',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('.subtask-cell a',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule('a',
+ [
+ 'color' => $values['font_link']
+ ]
+ );
+ $css->add_rule('.table-list-category a:hover',
+ [
+ 'color' => $values['font_link']
+ ]
+ );
+ $css->add_rule(['.subtask-cell a:hover', '.subtask-cell a:focus'],
+ [
+ 'color' => $values['font_link']
+ ]
+ );
+ $css->add_rule(['a:focus', 'a:hover'],
+ [
+ 'color' => $values['font_link_focus']
+ ]
+ );
+ $css->add_rule(['.table-list-header a:hover', '.table-list-header a:focus', '.task-board a'],
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('.table-list-row .table-list-details',
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('.input-addon-field',
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule(['.page-header h2 a:focus', '.page-header h2 a:hover', 'code'],
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('.sidebar>ul a',
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('.task-list-avatars .task-avatar-assignee',
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule(['.task-list-icons a', '.task-list-icons span', '.task-list-icons i', '.task-date'],
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('span.task-date-overdue',
+ [
+ 'color' => $values['font_overdue']
+ ]
+ );
+ $css->add_rule('.sidebar>ul li.active',
+ [
+ 'border-left' => '5px solid '.$values['font_main']
+ ]
+ );
+ $css->add_rule(['.sidebar>ul li.active a:focus', '.sidebar>ul li.active a:hover'],
+ [
+ 'color' => $values['font_secondary']
+ ]
+ );
+ $css->add_rule('.sidebar>ul li:hover',
+ [
+ 'border-left' => '5px solid '.$values['font_secondary']
+ ]
+ );
+ $css->add_rule('.panel',
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+ $css->add_rule(['td a.dropdown-menu strong', 'td a.dropdown-menu strong i'],
+ [
+ 'color' => $values['font_main']
+ ]
+ );
+
+ $css->add_raw('
+ #task-summary h2 {color: unset;}
+ .comments .comment:hover {background: #efefef40;}
+ .comments .comment:nth-child(even):not(.comment-highlighted):hover {background: #efefef40;}
+ .comments .comment:nth-child(even):not(.comment-highlighted) {background: #efefef40;}
+ table.table-striped tr:nth-child(odd) {background: #efefef40;}
+ header {box-shadow: 0px -1px 5px 1px;border-bottom: none;}
+ .project-header {margin-bottom: 8px;margin-top: 8px;}
+ .panel{background-color: #efefef40;border: 1px solid #efefef40;}
+ .task-board{border-width: 2px;background: #efefef22!important;}
+ div.task-board-recent {border-width: 2px;}
+ table td {border:none;}
+ table th:first-child {border-top-left-radius:8px;}
+ table th:last-child {border-top-right-radius:8px;}
+ .table-list-header{background:#efefef40;border:1px solid #efefef40;border-radius:5px 5px 0 0;line-height:28px;padding-left:3px;padding-right:3px;}
+ .table-list-row{padding-left:3px;padding-right:3px;border-bottom:1px solid #efefef40;border-right:1px solid #efefef40;}
+ .table-list-row.table-border-left{border-left:1px solid #efefef40;}
+ .table-list-row:nth-child(odd){background:#efefef30;}
+ .table-list-row:hover{background:#efefef32;border-bottom:1px solid #efefef32;border-right:1px solid #efefef32;}
+ .dropdown-menu-link-icon{text-decoration:none;}
+ .dropdown-submenu-open li{border-bottom:1px solid #efefef40;}
+ .page-header h2{margin:0;padding:0;font-weight:bold;border-bottom:1px dotted #efefef40;}
+ .sidebar>ul li{list-style-type:none;line-height:35px;border-bottom:1px dotted #efefef40;padding-left:13px;}
+ span.task-icon-age-total{border:1px solid #efefef40;padding:1px 3px 1px 3px;border-top-left-radius:3px;border-bottom-left-radius:3px;}
+ span.task-icon-age-column{border:1px solid #efefef40;border-left:none;margin-left:-5px;padding:1px 3px 1px 3px;border-top-right-radius:3px;border-bottom-right-radius:3px;}
+ .subtask-cell{padding:4px 10px;border-top:1px dotted #efefef42;border-left:1px dotted #efefef40;display:table-cell;vertical-align:middle;}
+ table th {text-align: left;padding: 0.5em 3px;border:none;background: #efefef40;}
+ .views li {white-space: nowrap;background: #efefef40;border:none;border-right: none;padding: 4px 8px;display: inline;}
+ ');
+
+
+ $minify = true;
+
+ $extension = '.css';
+ $rename = str_replace('.', '', $values['theme_name']);
+
+ if (file_exists(DATA_DIR . '/files/customizer/themes')) {
+ file_put_contents(DATA_DIR . '/files/customizer/themes/' . $rename . $extension, $css->get_output($minify));
+ } else {
+ mkdir(DATA_DIR . '/files/customizer/themes', 0755);
+ file_put_contents(DATA_DIR . '/files/customizer/themes/' . $rename . $extension, $css->get_output($minify));
+ }
+
+ $this->response->redirect($this->helper->url->to('CustomizerFileController', 'show', array('plugin' => 'Customizer')));
+ }
+}
diff --git a/plugins/Customizer/Controller/CustomizerFileController.php b/plugins/Customizer/Controller/CustomizerFileController.php
new file mode 100644
index 00000000..75271320
--- /dev/null
+++ b/plugins/Customizer/Controller/CustomizerFileController.php
@@ -0,0 +1,313 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Controller;
+
+use Kanboard\Plugin\Customizer\Model\CustomizerFileModel;
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+use Kanboard\Controller\BaseController;
+use Kanboard\Model\ConfigModel;
+
+/**
+ * Customizer Controller
+ *
+ * @package Customizer\Controller
+ * @author creecros
+ */
+class CustomizerFileController extends BaseController
+{
+ /**
+ * Get file content from object storage
+ *
+ * @access protected
+ * @param array $file
+ * @return string
+ */
+ protected function getFileContent(array $file)
+ {
+ $content = '';
+
+ try {
+ if ($file['is_image'] == 0) {
+ $content = $this->objectStorage->get($file['path']);
+ }
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+
+ return $content;
+ }
+
+ /**
+ * Output file with cache
+ *
+ * @param array $file
+ * @param $mimetype
+ */
+ protected function renderFileWithCache(array $file, $mimetype)
+ {
+ $etag = md5($file['path']);
+
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ $this->response->status(304);
+ } else {
+ try {
+ $this->response->withContentType($mimetype);
+ $this->response->withCache(5 * 86400, $etag);
+ $this->response->send();
+ $this->objectStorage->output($file['path']);
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * Output file without cache
+ *
+ * @param array $file
+ * @param $mimetype
+ */
+ protected function renderFileWithoutCache(array $file, $mimetype)
+ {
+ $etag = md5($file['path']);
+
+ if ($this->request->getHeader('If-None-Match') === '"'.$etag.'"') {
+ $this->response->status(304);
+ } else {
+ try {
+ $this->response->withContentType($mimetype);
+ $this->response->withOutCache();
+ $this->response->send();
+ $this->objectStorage->output($file['path']);
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ }
+ }
+ }
+
+ public function show()
+ {
+ $logo = $this->customizerFileModel->getByType(1);
+ $flavicon = $this->customizerFileModel->getByType(2);
+ $loginlogo = $this->customizerFileModel->getByType(3);
+ $logopath = $logo['path'];
+ $flaviconpath = $flavicon['path'];
+ $this->response->html($this->helper->layout->config('customizer:file/show', array(
+ 'logo' => $logo,
+ 'title' => t('Settings').' &gt; '.t('Customizer'),
+ 'flavicon' => $flavicon,
+ 'logopath' => $logopath,
+ 'flaviconpath' => $flaviconpath,
+ 'loginlogo' => $loginlogo
+ )));
+
+ }
+
+ public function logo()
+ {
+ if ($this->logoexists()) {
+ $file = $this->customizerFileModel->getByType(1);
+ if ($this->configModel->get('enable_cache', '') == 'checked') {
+ $this->renderFileWithCache($file, $this->helper->file->getImageMimeType($file['name']));
+ } else {
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+ }
+
+ public function logo_setting()
+ {
+ if ($this->logoexists()) {
+ $file = $this->customizerFileModel->getByType(1);
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+
+ public function loginlogo()
+ {
+ if ($this->loginlogoexists()) {
+ $file = $this->customizerFileModel->getByType(3);
+ if ($this->configModel->get('enable_cache', '') == 'checked') {
+ $this->renderFileWithCache($file, $this->helper->file->getImageMimeType($file['name']));
+ } else {
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+ }
+
+ public function loginlogo_setting()
+ {
+ if ($this->loginlogoexists()) {
+ $file = $this->customizerFileModel->getByType(3);
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+
+ public function icon()
+ {
+ if ($this->iconexists()) {
+ $file = $this->customizerFileModel->getByType(2);
+ if ($this->configModel->get('enable_cache', '') == 'checked') {
+ $this->renderFileWithCache($file, $this->helper->file->getImageMimeType($file['name']));
+ } else {
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+ }
+
+ public function icon_setting()
+ {
+ if ($this->iconexists()) {
+ $file = $this->customizerFileModel->getByType(2);
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+
+ public function link()
+ {
+ if ($this->logoexists() && $this->linkexists()) {
+ return $this->response->redirect($this->configModel->get('login_link', 'https://kanboard.org'));
+ } else {
+ return $this->response->redirect($this->configModel->get('application_url', '') . 'login');
+ }
+ }
+
+ public function logoexists()
+ {
+ if (null !== $this->customizerFileModel->getByType(1)) { return true; } else { return false; }
+ }
+
+ public function loginlogoexists()
+ {
+ if (null !== $this->customizerFileModel->getByType(3)) {
+ $customizer['loginCheck'] = true;
+ return true;
+ } else {
+ $customizer['loginCheck'] = false;
+ return false;
+ }
+ }
+
+ public function linkexists()
+ {
+ if ($this->configModel->exists('login_link')) { return true; } else { return false; }
+ }
+
+ public function iconexists()
+ {
+ if (null !== $this->customizerFileModel->getByType(2)) { return true; } else { return false; }
+ }
+
+ public function image()
+ {
+ $file = $this->customizerFileModel->getById($this->request->getIntegerParam('file_id'));
+ if ($this->configModel->get('enable_cache', '') == 'checked') {
+ $this->renderFileWithCache($file, $this->helper->file->getImageMimeType($file['name']));
+ } else {
+ $this->renderFileWithoutCache($file, $this->helper->file->getImageMimeType($file['name']));
+ }
+ }
+
+ /**
+ * File upload form
+ *
+ * @access public
+ */
+ public function create()
+ {
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ $custom_id = $this->request->getIntegerParam('custom_id');
+ if ($custom_id == 1) {
+ $this->response->html($this->template->render('customizer:file/upload_logo', array(
+ 'custom_id' => $custom_id,
+ 'multiple' => false,
+ )));
+ } else if ($custom_id == 2) {
+ $this->response->html($this->template->render('customizer:file/upload_flavicon', array(
+ 'custom_id' => $custom_id,
+ 'multiple' => false,
+ )));
+ } else if ($custom_id == 3) {
+ $this->response->html($this->template->render('customizer:file/upload_loginlogo', array(
+ 'custom_id' => $custom_id,
+ 'multiple' => false,
+ )));
+ }
+ }
+
+ /**
+ * File upload (save files)
+ *
+ * @access public
+ */
+ public function save()
+ {
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ $custom_id = $this->request->getIntegerParam('custom_id');
+ if ($custom_id ==3) { $loginCheck = true; }
+
+ $result = $this->customizerFileModel->uploadFiles($custom_id, $this->request->getFileInfo('files'));
+ if ($this->request->isAjax()) {
+ if (!$result) {
+ $this->response->json(array('message' => t('Unable to upload files, check the permissions of your data folder.')), 500);
+ } else {
+ $this->response->json(array('message' => 'OK'));
+ }
+ } else {
+ if (!$result) {
+ $this->flash->failure(t('Unable to upload files, check the permissions of your data folder.'));
+ }
+ }
+ }
+ /**
+ * Remove a file
+ *
+ * @access public
+ */
+ public function remove()
+ {
+ $this->checkCSRFParam();
+ $custom_id = $this->request->getIntegerParam('custom_id');
+ $file = $this->customizerFileModel->getById($this->request->getIntegerParam('file_id'));
+ if ($this->customizerFileModel->remove($file['id'])) {
+ $this->flash->success(t('File removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this file.'));
+ }
+
+ }
+
+ public function removeform()
+ {
+ $this->checkCSRFParam();
+ $custom_id = $this->request->getIntegerParam('custom_id');
+ if ($custom_id == 3) { $loginCheck = false; }
+ $file = $this->customizerFileModel->getById($this->request->getIntegerParam('file_id'));
+ if ($this->customizerFileModel->remove($file['id'])) {
+ $this->flash->success(t('File removed successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to remove this file.'));
+ }
+
+ return $this->response->redirect($this->configModel->get('application_url', '') . 'settings/customizer');
+
+ }
+ /**
+ * Confirmation dialog before removing a file
+ *
+ * @access public
+ */
+ public function confirm()
+ {
+ $custom_id = $this->request->getIntegerParam('custom_id');
+ $file = $this->customizerFileModel->getById($this->request->getIntegerParam('file_id'));
+ $this->response->html($this->template->render('customizer:file/remove', array(
+ 'custom_id' => $custom_id,
+ 'file' => $file,
+ )));
+ }
+}
diff --git a/plugins/Customizer/Helper/DynamicAvatar.php b/plugins/Customizer/Helper/DynamicAvatar.php
new file mode 100644
index 00000000..cf7ab83f
--- /dev/null
+++ b/plugins/Customizer/Helper/DynamicAvatar.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Helper;
+
+use Kanboard\Helper\AvatarHelper;
+use Kanboard\Core\Base;
+/**
+ * Avatar Helper
+ *
+ * @package helper
+ * @author Craig Crosby
+ */
+class DynamicAvatar extends AvatarHelper
+{
+
+ public function dynamicRender($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48)
+ {
+ if (empty($user_id) && empty($username)) {
+ $html = $this->avatarManager->renderDefault($size);
+ } else {
+ $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size);
+ }
+ return '<div id="'.$css.'" class="avatar avatar-dyn '.$css.'">'.$html.'</div>';
+ }
+
+ public function dynamic($user_id, $username, $name, $email, $avatar_path, $css = '', $size)
+ {
+ return $this->dynamicRender($user_id, $username, $name, $email, $avatar_path, $css, $size);
+ }
+
+ public function currentUserDynamic($css = '')
+ {
+ $user = $this->userSession->getAll();
+ return $this->dynamic($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, $this->configModel->get('av_size', '20'));
+ }
+
+ public function boardDynamicRender($user_id, $username, $name, $email, $avatar_path, $css = 'avatar-left', $size = 48)
+ {
+ if (empty($user_id) && empty($username)) {
+ $html = $this->avatarManager->renderDefault($size);
+ } else {
+ $html = $this->avatarManager->render($user_id, $username, $name, $email, $avatar_path, $size);
+ }
+ return '<div id="'.$css.'" class="avatar avatar-bdyn '.$css.'">'.$html.'</div>';
+ }
+
+ public function boardDynamic($user_id, $username, $name, $email, $avatar_path, $css = '', $size)
+ {
+ return $this->boardDynamicRender($user_id, $username, $name, $email, $avatar_path, $css, $size);
+ }
+
+ public function boardCurrentUserDynamic($css = '')
+ {
+ $user = $this->userSession->getAll();
+ return $this->boardDynamic($user['id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, $this->configModel->get('b_av_size', '20'));
+ }
+
+
+ }
diff --git a/plugins/Customizer/Helper/ThemeHelper.php b/plugins/Customizer/Helper/ThemeHelper.php
new file mode 100644
index 00000000..c2f767d1
--- /dev/null
+++ b/plugins/Customizer/Helper/ThemeHelper.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Helper;
+
+use Kanboard\Core\Base;
+
+class ThemeHelper extends Base
+{
+
+ public function reverseSelect($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ $html = '<select name="'.$name.'" id="form-'.$name.'" class="'.$class.'" '.implode(' ', $attributes).'>';
+ foreach ($options as $id => $value) {
+ $html .= '<option value="'.$this->helper->text->e($value).'"';
+ if (isset($values->$name) && $value == $values->$name) {
+ $html .= ' selected="selected"';
+ }
+ if (isset($values[$name]) && $value == $values[$name]) {
+ $html .= ' selected="selected"';
+ }
+ $html .= '>'.$this->helper->text->e($id).'</option>';
+ }
+ $html .= '</select>';
+ $html .= $this->errorList($errors, $name);
+ return $html;
+ }
+
+ public function reverseSelectOnChange($name, array $options, array $values = array(), array $errors = array(), array $attributes = array(), $class = '')
+ {
+ $html = '<select name="'.$name.'" id="userthemeSelection" class="'.$class.'" '.implode(' ', $attributes).'>';
+ foreach ($options as $id => $value) {
+ $html .= '<option value="'.$this->helper->text->e($value).'"';
+ if (isset($values->$name) && $value == $values->$name) {
+ $html .= ' selected="selected"';
+ }
+ if (isset($values[$name]) && $value == $values[$name]) {
+ $html .= ' selected="selected"';
+ }
+ $html .= '>'.$this->helper->text->e($id).'</option>';
+ }
+ $html .= '</select>';
+ $html .= $this->errorList($errors, $name);
+ return $html;
+ }
+
+ private function errorClass(array $errors, $name)
+ {
+ return ! isset($errors[$name]) ? '' : ' form-error';
+ }
+
+ private function errorList(array $errors, $name)
+ {
+ $html = '';
+ if (isset($errors[$name])) {
+ $html .= '<ul class="form-errors">';
+ foreach ($errors[$name] as $error) {
+ $html .= '<li>'.$this->helper->text->e($error).'</li>';
+ }
+ $html .= '</ul>';
+ }
+ return $html;
+ }
+
+ }
diff --git a/plugins/Customizer/LICENSE b/plugins/Customizer/LICENSE
new file mode 100644
index 00000000..7c5e911a
--- /dev/null
+++ b/plugins/Customizer/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Craig Crosby
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/Customizer/Locale/it_IT/translations.php b/plugins/Customizer/Locale/it_IT/translations.php
new file mode 100644
index 00000000..aea56cfc
--- /dev/null
+++ b/plugins/Customizer/Locale/it_IT/translations.php
@@ -0,0 +1,49 @@
+<?php
+
+return [
+ 'Image Assets & Settings' => 'Risorse e impostazioni dell\'immagini',
+ 'Header Image' => 'Logo dell\'intestazione',
+ 'Login Image' => 'Logo di accesso',
+ 'Upload Header Logo' => 'Carica il logo dell\'intestazione',
+ 'Header Logo' => 'Logo intestazione',
+ 'Recommend 100 pixels in width, *.png, *.jpg, *.gif, max size 500kb.' => 'Dimensione consigliata in larghezza 100 pixel, formati: * .png, * .jpg, * .gif, dimensione massima 500kb.',
+ 'Drag and drop your file here' => 'Trascina e rilascia il tuo file qui',
+ 'or' => 'o',
+ 'choose file' => 'scegli il file',
+ 'Recommend 50x50 pixels, *.png only, max size 20kb.' => 'Dimensione consigliata 50x50 pixel, formato consentito * .png, dimensione massima 20kb.',
+ 'Remove Header Logo' => 'Rimuovi il logo dell\'intestazione',
+ 'Upload Login Logo' => 'Carica il logo di accesso',
+ 'Remove Login Logo' => 'Rimuovi il logo di accesso',
+ 'Favicon Image' => 'Immagine favicon',
+ 'Upload Favicon' => 'Carica favicon',
+ 'Remove Favicon' => 'Rimuovi favicon',
+ 'Links & Settings' => 'Collegamenti e impostazioni',
+ 'Login Link' => 'Link di accesso',
+ 'Example: <code>https://example.kanboard.org/</code> (used as logo link on login page)' => 'Esempio: <code> https://example.kanboard.org </code> (utilizzato come collegamento logo sulla pagina di accesso)',
+ 'Login Page Background Color' => 'Colore di sfondo della pagina di accesso',
+ 'Login Panel Shadow Color' => 'Colore dell\'ombra del pannello di accesso',
+ 'Login Panel Border Color' => 'Colore del bordo del pannello di accesso',
+ 'Login Panel Border Thickness' => 'Spessore del bordo del pannello di accesso',
+ 'Login Panel Color' => 'Colore del pannello di accesso',
+ 'Login Panel Shadow Intensity' => 'Intensità dell\'ombra del pannello di accesso',
+ 'Login Button Background Color' => 'Colore di sfondo del pulsante di accesso',
+ 'Login Button Shadow Color' => 'Colore dell\'ombra del pulsante di accesso',
+ 'Login Button Border Color' => 'Colore del bordo del pulsante di accesso',
+ 'Login Button Shade Color' => 'Colore sfumato del pulsante di accesso',
+ 'Login Button Font Color' => 'Colore del carattere del pulsante di accesso',
+ 'Login Button Shadow Intensity' => 'Intensità ombra del pulsante di accesso',
+ 'Login Button Border Thickness' => 'Spessore bordo del pulsante di accesso',
+ 'Login Button Width' => 'Larghezza del pulsante di accesso',
+ 'Preview' => 'Anteprima',
+ 'Login Background Image URL' => 'URL dell\'immagine di sfondo di accesso',
+ 'Header Logo Size' => 'Dimensioni del logo dell\'intestazione',
+ 'Example: <code>https://source.unsplash.com/random</code> (URL for a background image on the login page, centered, autoscale, no-repeat)' => 'Esempio: <codice> https://source.unsplash.com/random </ code> (URL per un\'immagine di sfondo nella pagina di accesso, centrata, automatica, senza ripetizione)',
+ 'Login Logo Size' => 'Dimensione logo di accesso',
+ 'Example: <code>30</code> (Default is 30px in height, intgers only, max 999)' => 'Esempio: <code> 30 </code> (l\'impostazione predefinita è 30px in altezza, solo numeri interi, max 999)',
+ 'Example: <code>50</code> (Default is 50px in height, intgers only, max 999)' => 'Esempio: <code> 50 </code> (l\'impostazione predefinita è 50px in altezza, solo numeri interi, max 999)',
+ 'Theme' => 'Tema',
+ 'You can see a preview only after saving' => 'Puoi vedere l\'anteprima solo dopo aver salvato',
+ 'Login Page Settings' => 'Impostazioni della pagina di accesso',
+ '&nbsp;pixels high' => '&nbsp;altezza pixel',
+ 'Use Custom Login Settings' => 'Usa le impostazioni di accesso personalizzate'
+];
diff --git a/plugins/Customizer/Model/CustomizerFileModel.php b/plugins/Customizer/Model/CustomizerFileModel.php
new file mode 100644
index 00000000..64dbc2c8
--- /dev/null
+++ b/plugins/Customizer/Model/CustomizerFileModel.php
@@ -0,0 +1,320 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Model;
+
+use Exception;
+use Kanboard\Core\Base;
+use Kanboard\Core\Thumbnail;
+use Kanboard\Core\ObjectStorage\ObjectStorageException;
+
+/**
+ * Customizer File Model
+ *
+ * @package Customizer\Model
+ * @author creecros
+ */
+class CustomizerFileModel extends Base
+{
+ /**
+ * Table name
+ *
+ * @var string
+ */
+ const TABLE = 'customizer_files';
+
+ /**
+ * Get the table
+ *
+ * @abstract
+ * @access protected
+ * @return string
+ */
+ public function getTable()
+ {
+ return self::TABLE;
+ }
+
+ /**
+ * Define the path prefix
+ *
+ * @abstract
+ * @access protected
+ * @return string
+ */
+ public function getPathPrefix()
+ {
+ return 'customizer';
+ }
+
+ /**
+ * Get a file by the Id
+ *
+ * @access public
+ * @param integer $file_id File id
+ * @return array
+ */
+ public function getById($file_id)
+ {
+ return $this->db->table($this->getTable())->eq('id', $file_id)->findOne();
+ }
+
+ /**
+ * Get a file by the type
+ *
+ * @access public
+ * @param integer $custom_id 1=logo 2=flavicon 3=loginlogo
+ * @return array
+ */
+ public function getByType($custom_id)
+ {
+ return $this->db->table($this->getTable())->eq('custom_id', $custom_id)->findOne();
+ }
+
+ /**
+ * Get a file id by the type
+ *
+ * @access public
+ * @param integer $custom_id 1=logo 2=flavicon 3=loginlogo
+ * @return array
+ */
+ public function getIdByType($custom_id)
+ {
+ $file = $this->db->table($this->getTable())->eq('custom_id', $custom_id)->findOne();
+ return $file['id'];
+ }
+
+ /**
+ * Get all files by the type
+ *
+ * @access public
+ * @param integer $custom_id 1=logo 2=flavicon 3=loginlogo
+ * @return array
+ */
+ public function getAllByType($custom_id)
+ {
+ return $this->db->table($this->getTable())->eq('custom_id', $custom_id)->findAll();
+ }
+
+ /**
+ * Create a file entry in the database
+ *
+ * @access public
+ * @param integer $custom_id 1=logo 2=flavicon
+ * @param string $name Filename
+ * @param string $path Path on the disk
+ * @param integer $size File size
+ * @return bool|integer
+ */
+ public function create($custom_id, $name, $path, $size)
+ {
+ $values = array(
+ 'custom_id' => $custom_id,
+ 'name' => substr($name, 0, 255),
+ 'path' => $path,
+ 'is_image' => $this->isImage($name) ? 1 : 0,
+ 'size' => $size,
+ 'user_id' => $this->userSession->getId() ?: 0,
+ 'date' => time(),
+ );
+ if (null !== $this->getByType($custom_id)) {
+ foreach ($this->getAllByType($custom_id) as $image) { $this->remove($image['id']); }
+ $result = $this->db->table($this->getTable())->insert($values);
+ } else {
+ $result = $this->db->table($this->getTable())->insert($values);
+ }
+ if ($result) {
+ $file_id = (int) $this->db->getLastId();
+ return $file_id;
+ }
+ return false;
+ }
+
+ /**
+ * Remove a file
+ *
+ * @access public
+ * @param integer $file_id File id
+ * @return bool
+ */
+ public function remove($file_id)
+ {
+ try {
+ $file = $this->getById($file_id);
+ $this->objectStorage->remove($file['path']);
+ if ($file['is_image'] == 1) {
+ $this->objectStorage->remove($this->getThumbnailPath($file['path']));
+ }
+ return $this->db->table($this->getTable())->eq('id', $file['id'])->remove();
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Check if a filename is an image (file types that can be shown as thumbnail)
+ *
+ * @access public
+ * @param string $filename Filename
+ * @return bool
+ */
+ public function isImage($filename)
+ {
+ switch (get_file_extension($filename)) {
+ case 'jpeg':
+ case 'jpg':
+ case 'png':
+ case 'gif':
+ return true;
+ }
+ return false;
+ }
+ /**
+ * Generate the path for a thumbnails
+ *
+ * @access public
+ * @param string $key Storage key
+ * @return string
+ */
+ public function getThumbnailPath($key)
+ {
+ return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
+ }
+
+ /**
+ * Generate the path for a new filename
+ *
+ * @access public
+ * @param integer $id Foreign key
+ * @param string $filename Filename
+ * @return string
+ */
+ public function generatePath($id, $filename)
+ {
+ return $this->getPathPrefix().DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
+ }
+
+ /**
+ * Upload multiple files
+ *
+ * @access public
+ * @param integer $id
+ * @param array $files
+ * @return bool
+ */
+ public function uploadFiles($id, array $files)
+ {
+ try {
+ if (empty($files)) {
+ return false;
+ }
+ foreach (array_keys($files['error']) as $key) {
+ $file = array(
+ 'name' => $files['name'][$key],
+ 'tmp_name' => $files['tmp_name'][$key],
+ 'size' => $files['size'][$key],
+ 'error' => $files['error'][$key],
+ );
+ $this->uploadFile($id, $file);
+ }
+ return true;
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Upload a file
+ *
+ * @access public
+ * @param integer $id
+ * @param array $file
+ * @throws Exception
+ */
+ public function uploadFile($id, array $file)
+ {
+ if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
+ $destination_filename = $this->generatePath($id, $file['name']);
+ if ($this->isImage($file['name'])) {
+ $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename);
+ }
+ $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
+ $this->create($id, $file['name'], $destination_filename, $file['size']);
+ } else {
+ throw new Exception('File not uploaded: '.var_export($file['error'], true));
+ }
+ }
+
+ /**
+ * Handle file upload (base64 encoded content)
+ *
+ * @access public
+ * @param integer $id
+ * @param string $originalFilename
+ * @param string $data
+ * @param bool $isEncoded
+ * @return bool|int
+ */
+ public function uploadContent($id, $originalFilename, $data, $isEncoded = true)
+ {
+ try {
+ if ($isEncoded) {
+ $data = base64_decode($data);
+ }
+ if (empty($data)) {
+ $this->logger->error(__METHOD__.': Content upload with no data');
+ return false;
+ }
+ $destinationFilename = $this->generatePath($id, $originalFilename);
+ $this->objectStorage->put($destinationFilename, $data);
+ if ($this->isImage($originalFilename)) {
+ $this->generateThumbnailFromData($destinationFilename, $data);
+ }
+ return $this->create(
+ $id,
+ $originalFilename,
+ $destinationFilename,
+ strlen($data)
+ );
+ } catch (ObjectStorageException $e) {
+ $this->logger->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Generate thumbnail from a blob
+ *
+ * @access public
+ * @param string $destination_filename
+ * @param string $data
+ */
+ public function generateThumbnailFromData($destination_filename, &$data)
+ {
+ $blob = Thumbnail::createFromString($data)
+ ->resize()
+ ->toString();
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
+ }
+ /**
+ * Generate thumbnail from a local file
+ *
+ * @access public
+ * @param string $uploaded_filename
+ * @param string $destination_filename
+ */
+ public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
+ {
+ $blob = Thumbnail::createFromFile($uploaded_filename)
+ ->resize()
+ ->toString();
+ $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
+ }
+
+ public function getUserSessionId()
+ {
+ return $this->userSession->getId();
+ }
+
+}
diff --git a/plugins/Customizer/Plugin.php b/plugins/Customizer/Plugin.php
new file mode 100644
index 00000000..4055d4a4
--- /dev/null
+++ b/plugins/Customizer/Plugin.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer;
+
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Core\Security\Role;
+use Kanboard\Event\AuthSuccessEvent;
+use Kanboard\Core\Security\AuthenticationManager;
+use Kanboard\Plugin\Customizer\Model\CustomizerFileModel;
+
+class Plugin extends Base
+{
+
+ public function initialize()
+ {
+ global $customizer;
+
+ // Themes
+ $customizer['themes'] = array(
+ 'Default' => 'plugins/Customizer/Assets/css/theme.css'
+ );
+
+ $scanned_temp_themes = array_diff(scandir('plugins/Customizer/Assets/css/userthemes'), array('..', '.'));
+ $scanned_preset_themes = array_diff(scandir('plugins/Customizer/Assets/css/themes'), array('..', '.'));
+
+ foreach ($scanned_temp_themes as $theme) {
+ unlink('plugins/Customizer/Assets/css/userthemes/' . $theme);
+ }
+
+ if (file_exists(DATA_DIR . '/files/customizer/themes')) {
+ $scanned_user_themes = array_diff(scandir(DATA_DIR . '/files/customizer/themes'), array('..', '.'));
+ foreach ($scanned_user_themes as $theme) {
+ copy(DATA_DIR . '/files/customizer/themes/' . $theme, 'plugins/Customizer/Assets/css/userthemes/' . $theme);
+ $customizer['themes'][rtrim($theme, '.css')] = 'plugins/Customizer/Assets/css/userthemes/' . $theme;
+ }
+ } else { mkdir(DATA_DIR . '/files/customizer/themes', 0755, true); }
+
+ foreach ($scanned_preset_themes as $theme) {
+ $customizer['themes'][rtrim($theme, '.css')] = 'plugins/Customizer/Assets/css/themes/' . $theme;
+ }
+
+
+
+ //Helper
+ $this->helper->register('themeHelper', '\Kanboard\Plugin\Customizer\Helper\ThemeHelper');
+ $this->helper->register('dynamicAvatar', '\Kanboard\Plugin\Customizer\Helper\DynamicAvatar');
+
+ //Check if login logo is set
+ if (null !== $this->customizerFileModel->getByType(3)) {
+ $customizer['loginCheck'] = true;
+ } else {
+ $customizer['loginCheck'] = false;
+ }
+
+ //Grabs login page settings from database
+ $customizer['backURL'] = $this->configModel->get('background_url', '');
+ $customizer['backColor'] = $this->configModel->get('loginbackground_color', '#ffffff');
+ $customizer['logoSize'] = $this->configModel->get('loginlogo_size', '50');
+ $customizer['loginpanel_color'] = $this->configModel->get('loginpanel_color', '#ffffff');
+ $customizer['login_shadow_color'] = $this->configModel->get('login_shadow_color', '#333');
+ $customizer['login_shadow'] = $this->configModel->get('login_shadow', '0');
+ $customizer['login_border_color'] = $this->configModel->get('login_border_color', '#ffffff');
+ $customizer['login_border'] = $this->configModel->get('login_border', '0');
+ $customizer['login_btn_color'] = $this->configModel->get('login_btn_color', '#3079ed');
+ $customizer['login_btn_shadow_color'] = $this->configModel->get('login_btn_shadow_color', '#333');
+ $customizer['login_btn_border_color'] = $this->configModel->get('login_btn_border_color', 'transparent');
+ $customizer['login_btn_shade_color'] = $this->configModel->get('login_btn_shade_color', 'transparent');
+ $customizer['login_btn_font_color'] = $this->configModel->get('login_btn_font_color', '#ffffff');
+ $customizer['login_btn_shadow'] = $this->configModel->get('login_btn_shadow', '0');
+ $customizer['login_btn_border'] = $this->configModel->get('login_btn_border', '0');
+ $customizer['login_btn_width'] = $this->configModel->get('login_btn_width', '95');
+ $customizer['login_note'] = $this->configModel->get('login_note', '');
+
+ //Templates and Assets
+ $this->template->hook->attach('template:config:sidebar', 'customizer:config/sidebar');
+ $this->template->setTemplateOverride('header/title', 'customizer:header/title');
+ $this->template->setTemplateOverride('header/user_dropdown', 'customizer:header/user_dropdown');
+ $this->template->setTemplateOverride('board/task_avatar', 'customizer:board/task_avatar');
+ $this->template->setTemplateOverride('layout', 'customizer:layout/layout');
+ $this->template->setTemplateOverride('auth/index', 'customizer:layout/index');
+ $this->hook->on('template:layout:css', array('template' => 'plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.css'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Customizer/Assets/rgbaColorPicker/rgbaColorPicker.js'));
+ $this->hook->on('template:layout:css', array('template' => 'plugins/Customizer/Assets/css/customizer.css'));
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Customizer/Assets/js/customizer.js'));
+ $this->template->hook->attach('customizer:config:themecreator', 'customizer:config/themecreator');
+
+ if ($customizer['login_note'] != '') {
+ $this->template->hook->attach('template:auth:login-form:newbox', 'customizer:layout/note');
+ }
+ if ($this->configModel->get('toggle_user_themes', 'disable') == 'enable') {
+ $this->template->setTemplateOverride('user_modification/show', 'customizer:user_mod/show');
+ }
+
+
+ if ($this->configModel->get('use_custom_login', '') == 'checked') {
+ $this->template->hook->attach('customizer:config:style', 'customizer:layout/preview_style');
+ $this->template->hook->attach('template:auth:login-form:before', 'customizer:layout/login_with_custom');
+ } else {
+ $this->template->hook->attach('template:auth:login-form:before', 'customizer:layout/login_no_custom');
+ }
+
+ //Routes
+ $this->route->addRoute('settings/customizer', 'CustomizerFileController', 'show', 'Customizer');
+
+ //Permissions for login page to access logos
+ $this->applicationAccessMap->add('CustomizerFileController', array('image', 'loginlogo', 'logo', 'link', 'logoexists', 'linkexists'), Role::APP_PUBLIC);
+
+ //Get accurate version
+ $wasmaster = str_replace('v', '', APP_VERSION);
+ $wasmaster = preg_replace('/\s+/', '', $wasmaster);
+
+ if (strpos(APP_VERSION, 'master') !== false && file_exists('ChangeLog')) { $wasmaster = trim(file_get_contents('ChangeLog', false, null, 8, 6), ' '); }
+ if (version_compare($wasmaster, '1.2.4') >= 0) {
+ $this->template->setTemplateOverride('header/title', 'customizer:header/title');
+ } else {
+ $this->template->setTemplateOverride('header/title', 'customizer:header/title_older_kb');
+ }
+
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ $user_id = $this->customizerFileModel->getUserSessionId();
+ $user_theme = $this->userMetadataModel->get($user_id, 'themeSelection', $this->configModel->get('themeSelection', 'plugins/Customizer/Assets/css/theme.css' ));
+ $default_theme = $this->configModel->get('themeSelection', 'plugins/Customizer/Assets/css/theme.css');
+ if ($this->configModel->get('toggle_user_themes', 'disable') == 'enable') {
+ $this->hook->on('template:layout:css', array('template' => $user_theme));
+ } else {
+ $this->hook->on('template:layout:css', array('template' => $default_theme));
+ }
+ }
+
+ public function getClasses() {
+ return array(
+ 'Plugin\Customizer\Model' => array(
+ 'CustomizerFileModel',
+ )
+ );
+ }
+
+ public function getPluginName()
+ {
+ return 'Customizer';
+ }
+
+ public function getPluginDescription()
+ {
+ return t('Completely customize your Kanboard experience with logos, favicons & themes.');
+ }
+
+ public function getPluginAuthor()
+ {
+ return 'Craig Crosby';
+ }
+
+ public function getPluginVersion()
+ {
+ return '1.13.1';
+ }
+
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/creecros/Customizer';
+ }
+
+ public function getCompatibleVersion()
+ {
+ return '>=1.0.42';
+ }
+}
+
diff --git a/plugins/Customizer/README.md b/plugins/Customizer/README.md
new file mode 100644
index 00000000..9ae17cdb
--- /dev/null
+++ b/plugins/Customizer/README.md
@@ -0,0 +1,142 @@
+## Checkout our latest project
+[![](https://raw.githubusercontent.com/docpht/docpht/master/public/assets/img/logo.png)](https://github.com/docpht/docpht)
+
+- With [DocPHT](https://github.com/docpht/docpht) you can take notes and quickly document anything and without the use of any database.
+-----------
+[![Latest release](https://img.shields.io/github/release/creecros/Customizer.svg)](https://github.com/creecros/Customizer/releases)
+[![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/creecros/Customizer/blob/master/LICENSE)
+[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/creecros/Customizer/graphs/contributors)
+[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)]()
+[![Downloads](https://img.shields.io/github/downloads/creecros/Customizer/total.svg)](https://github.com/creecros/Customizer/releases)
+
+Donate to help keep this project maintained.
+<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SEGNEVQFXHXGW&source=url">
+<img src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" /></a>
+
+**:star: If you use it, you should star it on Github!**
+*It's the least you can do for all the work put into it!*
+
+Customizer - it's like magic!
+----------
+
+## Author: [creecros](https://github.com/creecros)
+### Other [Contributors](https://github.com/creecros/Customizer/graphs/contributors)
+
+If you need a Logo and Favicon, try using this simple logo generator: https://creecros.github.io/simple_logo_gen/
+
+:sparkles: **Adds a settings panel Settings>Customizer:**
+
+![image](https://user-images.githubusercontent.com/26339368/49309828-7fc47800-f4aa-11e8-809e-8cc9686a2a2d.png)
+
+**GUI to customize...**
+
+* [header logo](#header-logo-rainbow)
+ * set header logo size
+* [upper right corner Avatar Icon](#header-avatar-icon-boy)
+ * set icon size
+ * set border radius
+* [task board Avatar Icon](#task-board-avatar-icon-girl)
+ * set icon size
+ * set border radius
+* [customizable login screen](#fully-customizable-login-screen-gem)
+ * [login logo](#login-logo-peach)
+ * [login page background image url](#login-screen-background-image-dart)
+ * login logo link
+ * set login logo size
+ * set login page background color
+ * set login panel color
+ * panel shadow adjustment (no shadow to heavy shadow with color adjustment)
+ * panel border adjustment (no border to thick border with color adjustment)
+ * add a [custom note panel](#login-screen-custom-note-book) on login screen
+* [favicon](#favicon-beginner)
+* theme management
+ * can upload css files to add to theme selection
+ * can remove themes from interface
+ * can enable/disable user themes (i.e. one global theme for everyone, or users get to select their own theme)
+ * admins can reset all users themes to default
+* [theme selector](#includes-preset-themes-mega) with 7 preloaded themes
+ * Github :octocat:
+ * Galaxy :milky_way:
+ * Breathe :partly_sunny:
+ * Clemson <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/72/Clemson_Tigers_logo.svg/2000px-Clemson_Tigers_logo.svg.png" height="20">
+ * Blueboard :blue_book:
+ * Material :sparkles:
+ * KindaDark :8ball:
+* [theme creator](#theme-creator-notes)
+
+
+**...to your site without any backend coding or config settings.**
+
+**Want more customization? Take a look at [BlueTek](https://github.com/BlueTeck)'s [BoardCustomizer](https://github.com/BlueTeck/kanboard_plugin_boardcustomizer) plugin**
+
+## Favicon :beginner:
+
+![image](https://user-images.githubusercontent.com/26339368/47174055-a43f0900-d2dd-11e8-9932-430e11b74fea.png)
+
+## Header Logo :rainbow:
+
+![image](https://user-images.githubusercontent.com/26339368/47369113-f9e62d80-d6b0-11e8-90e0-974c31b4b535.png)
+
+## Header Avatar Icon :boy:
+
+![image](https://user-images.githubusercontent.com/26339368/55774303-8bc8d380-5a62-11e9-8d59-6dc2a1c33387.png)
+
+## Task Board Avatar Icon :girl:
+
+![image](https://user-images.githubusercontent.com/26339368/59513842-ae73d000-8e89-11e9-94b5-ae27a0f8c651.png)
+
+## Login Logo :peach:
+
+![image](https://user-images.githubusercontent.com/26339368/48488290-622ab980-e7ee-11e8-8efd-58d7b834a02f.png)
+
+## Fully Customizable Login Screen :gem:
+
+![image](https://user-images.githubusercontent.com/26339368/48627714-44428d80-e983-11e8-8451-2e873572007a.png)
+
+![image](https://user-images.githubusercontent.com/26339368/48627526-ca120900-e982-11e8-9870-fd469c553124.png)
+
+## Login Screen Background Image :dart:
+
+![image](https://user-images.githubusercontent.com/26339368/47959793-573c8180-dfc3-11e8-84bc-ab654f8c50b5.png)
+
+## Login Screen Custom Note :book:
+
+![image](https://user-images.githubusercontent.com/26339368/64443449-e6ad3800-d09f-11e9-8d77-49e05db75e89.png)
+
+## Includes Preset Themes :mega:
+
+![image](https://user-images.githubusercontent.com/26339368/48488078-f2b4ca00-e7ed-11e8-8d4b-37d5b51f374b.png)
+![image](https://user-images.githubusercontent.com/26339368/48488101-ffd1b900-e7ed-11e8-8438-9ec7b91c98d9.png)
+![image](https://user-images.githubusercontent.com/26339368/49310809-64a73780-f4ad-11e8-81e6-82852275199a.png)
+
+:8ball: KindaDark
+
+<img src="https://i.imgur.com/Mw3DRBM.png" />
+
+
+## Theme Creator :notes:
+
+![image](https://user-images.githubusercontent.com/26339368/49310587-aedbe900-f4ac-11e8-935f-e499c14eb830.png)
+
+![image](https://user-images.githubusercontent.com/26339368/49310623-c024f580-f4ac-11e8-968e-a05762634b80.png)
+
+## Considerations :smirk:
+
+**Will probably not function fully with other CSS plugins that override templates related to layout or headers**
+
+## Theme Plugins that support Customizer
+ * https://github.com/kenlog/nebula :dash:
+ * https://github.com/kenlog/Moon :waning_gibbous_moon:
+ * https://github.com/aljawaid/KanboardCSS :computer:
+ * https://github.com/kenlog/Essential
+
+*Make pull request to add other theme support, must come from theme maintainer.*
+
+## Future Enhancements :lollipop:
+
+- [x] Custom Theme Creator with Color Pickers
+- [x] Preloaded Theme Selector
+- [x] Sizing options
+- [x] Increasing Compatibility
+- [x] Toggle switch to enable/disable cache for images related to Customizer sitewide
+
diff --git a/plugins/Customizer/Schema/Mysql.php b/plugins/Customizer/Schema/Mysql.php
new file mode 100644
index 00000000..978ceb0a
--- /dev/null
+++ b/plugins/Customizer/Schema/Mysql.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Schema;
+
+use PDO;
+
+const VERSION = 1;
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("CREATE TABLE customizer_files (
+ id INT NOT NULL AUTO_INCREMENT,
+ custom_id INT NOT NULL,
+ name VARCHAR(255),
+ path VARCHAR(255),
+ is_image TINYINT(1) DEFAULT 0,
+ date INT NOT NULL DEFAULT 0,
+ user_id INT NOT NULL DEFAULT 0,
+ size INT NOT NULL DEFAULT 0,
+ PRIMARY KEY (id)
+ ) ENGINE=InnoDB CHARSET=utf8"
+ );
+}
diff --git a/plugins/Customizer/Schema/Postgres.php b/plugins/Customizer/Schema/Postgres.php
new file mode 100644
index 00000000..54568c69
--- /dev/null
+++ b/plugins/Customizer/Schema/Postgres.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Schema;
+
+use PDO;
+
+const VERSION = 1;
+
+function version_1(PDO $pdo)
+{
+
+$pdo->exec("
+ CREATE TABLE customizer_files (
+ id SERIAL PRIMARY KEY,
+ custom_id INTEGER NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ path VARCHAR(255) NOT NULL,
+ is_image BOOLEAN DEFAULT '0',
+ size INTEGER DEFAULT 0 NOT NULL,
+ user_id INTEGER DEFAULT 0 NOT NULL,
+ date INTEGER DEFAULT 0 NOT NULL
+ )"
+ );
+
+}
diff --git a/plugins/Customizer/Schema/Sqlite.php b/plugins/Customizer/Schema/Sqlite.php
new file mode 100644
index 00000000..e8da08c1
--- /dev/null
+++ b/plugins/Customizer/Schema/Sqlite.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Kanboard\Plugin\Customizer\Schema;
+
+use PDO;
+
+const VERSION = 1;
+
+function version_1(PDO $pdo)
+{
+
+$pdo->exec("
+ CREATE TABLE customizer_files (
+ id INTEGER PRIMARY KEY,
+ custom_id INTEGER NOT NULL,
+ name TEXT COLLATE NOCASE NOT NULL,
+ path TEXT NOT NULL,
+ is_image INTEGER DEFAULT 0,
+ size INTEGER DEFAULT 0 NOT NULL,
+ user_id INTEGER DEFAULT 0 NOT NULL,
+ date INTEGER DEFAULT 0 NOT NULL
+ )"
+ );
+
+}
diff --git a/plugins/Customizer/Template/board/task_avatar.php b/plugins/Customizer/Template/board/task_avatar.php
new file mode 100644
index 00000000..3dd34a4c
--- /dev/null
+++ b/plugins/Customizer/Template/board/task_avatar.php
@@ -0,0 +1,26 @@
+
+<?php if (! empty($task['owner_id'])): ?>
+<div class="task-board-avatars">
+ <span
+ <?php if ($this->user->hasProjectAccess('TaskModificationController', 'edit', $task['project_id'])): ?>
+ class="task-board-assignee task-board-change-assignee"
+ data-url="<?= $this->url->href('TaskModificationController', 'edit', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>">
+ <?php else: ?>
+ class="task-board-assignee">
+ <?php endif ?>
+ <?= $this->helper->dynamicAvatar->boardDynamic(
+ $task['owner_id'],
+ $task['assignee_username'],
+ $task['assignee_name'],
+ $task['assignee_email'],
+ $task['assignee_avatar_path'],
+ 'avatar-inline',
+ $this->task->configModel->get('b_av_size', '20')
+ ) ?>
+ </span>
+</div>
+<?php endif ?>
+<style>
+.avatar-bdyn img, .avatar-bdyn div {border-radius: <?= $this->task->configModel->get('b_av_radius', '50') ?>%}
+.avatar-bdyn .avatar-letter {line-height:<?= $this->task->configModel->get('b_av_size', '20') ?>px;width:<?= $this->task->configModel->get('b_av_size', '20') ?>px;font-size:<?= $this->task->configModel->get('b_av_size', '20') / 2 ?>px;}
+</style> \ No newline at end of file
diff --git a/plugins/Customizer/Template/config/sidebar.php b/plugins/Customizer/Template/config/sidebar.php
new file mode 100644
index 00000000..b9d11934
--- /dev/null
+++ b/plugins/Customizer/Template/config/sidebar.php
@@ -0,0 +1,3 @@
+<li <?= $this->app->checkMenuSelection('CustomizerFileController', 'show') ?>>
+ <?= $this->url->link(t('Customizer'), 'CustomizerFileController', 'show', ['plugin' => 'Customizer']) ?>
+</li>
diff --git a/plugins/Customizer/Template/config/themecreator.php b/plugins/Customizer/Template/config/themecreator.php
new file mode 100644
index 00000000..218547da
--- /dev/null
+++ b/plugins/Customizer/Template/config/themecreator.php
@@ -0,0 +1,96 @@
+<form name="themecreator" id="themecreator" class="url-links" method="post" action="<?= $this->url->href('CustomizerConfigController', 'create_theme', array('plugin' => 'customizer', 'redirect' => 'application')) ?>" autocomplete="off">
+<?= $this->form->csrf() ?>
+ <div class="column-100" style="min-height: 600px;">
+ <?= t('Theme Name: ') ?><input type="text" name="theme_name" placeholder="<?= t('Theme Name') ?>" pattern="[a-zA-Z0-9]+" title="<?= t('it should only contain alphanumeric without spaces') ?>" required>
+ <br>
+ <br>
+ <table>
+ <tr>
+ <th colspan="2" class="title-creator">
+ <?= t('Header') ?>
+ </th>
+ <tr>
+ <td>
+ <strong><?= t('Header Background') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="header_background" value="">
+ </td>
+ <td>
+ <strong><?= t('Header Shade') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="header_shade" value="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong><?= t('Header Title') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="header_title" value="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong><?= t('Notification Icon') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="notification_icon" value="">
+ </td>
+ </tr>
+ <tr>
+ <th colspan="2" class="title-creator">
+ <?= t('Body') ?>
+ </th>
+ <tr>
+ <tr>
+ <td>
+ <strong><?= t('Background Color') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="background_color" value="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong><?= t('Main Font and Icons') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="font_main" value="">
+ </td>
+ <td>
+ <strong><?= t('Secondary Fonts and Icons') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="font_secondary" value="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong><?= t('Links') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="font_link" value="">
+ </td>
+ <td>
+ <strong><?= t('Link Hover & Focus') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="font_link_focus" value="">
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <strong><?= t('Over due') ?></strong>
+ </td>
+ <td>
+ <input class="color" name="font_overdue" value="">
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div class="form-actions" style="margin-bottom: 50px">
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ </div>
+</form>
diff --git a/plugins/Customizer/Template/file/remove.php b/plugins/Customizer/Template/file/remove.php
new file mode 100644
index 00000000..4031f8a8
--- /dev/null
+++ b/plugins/Customizer/Template/file/remove.php
@@ -0,0 +1,15 @@
+<div class="page-header">
+ <h2><?= t('Remove a file') ?></h2>
+</div>
+
+<div class="confirm">
+ <p class="alert alert-info">
+ <?= t('Do you really want to remove this file: "%s"?', $this->text->e($file['name'])) ?>
+ </p>
+
+ <?= $this->modal->confirmButtons(
+ 'CustomizerFileController',
+ 'removeform',
+ array('plugin' => 'customizer', 'custom_id' => $file['custom_id'], 'file_id' => $file['id'])
+ ) ?>
+</div>
diff --git a/plugins/Customizer/Template/file/show.php b/plugins/Customizer/Template/file/show.php
new file mode 100644
index 00000000..c184656c
--- /dev/null
+++ b/plugins/Customizer/Template/file/show.php
@@ -0,0 +1,435 @@
+<?php
+global $customizer;
+?>
+
+<?= $this->hook->render('customizer:config:style') ?>
+
+<div class="sidebar-content">
+ <form name="settings" id="settings" class="url-links" method="post" action="<?= $this->url->href('CustomizerConfigController', 'save', array('plugin' => 'customizer', 'redirect' => 'application')) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <fieldset class="login-link-block panel">
+
+ <button type="button" class="login-accordion"><i class="fa fa-picture-o" aria-hidden="true"></i> <?= t('Image Assets & Settings') ?></button>
+ <div class="login-accordian-panel mt-10">
+ <div class="panel header-logo-panel">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?= t('Header Image') ?></h3>
+ </div>
+ <img id="hl1" src="<?= $this->url->href('CustomizerFileController', 'logo_setting', array('plugin' => 'customizer', 'file_id' => $logo['id'])) ?>" alt="<?= $this->text->e($logo['name']) ?>" height="<?= $this->task->configModel->get('headerlogo_size', '30') ?>">
+ <br>
+ <br>
+ <ul class="upload-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+
+ <?= $this->modal->medium('file', t('Upload Header Logo'), 'CustomizerFileController', 'create', array('plugin' => 'customizer', 'custom_id' => 1))?>
+ </ul class="remove-link">
+ <?php if (null !== $this->task->customizerFileModel->getByType(1)) : ?>
+ <ul class="remove-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+
+ <?= $this->modal->medium('remove', t('Remove Header Logo'), 'CustomizerFileController', 'confirm', array('plugin' => 'customizer', 'custom_id' => 1, 'file_id' => $logo['id']))?>
+ </ul>
+ <?php endif ?>
+ <br><br>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Header Logo Size') ?></strong></th>
+ <th><input type="range" name="headerlogo_size" min="20" max="250" value="<?= $this->task->configModel->get('headerlogo_size','30') ?>">
+ <header_logo_output> <?= $this->task->configModel->get('headerlogo_size','30') ?></header_logo_output><?= t('&nbsp;pixels high') ?>
+ </th>
+ </tr>
+ </table>
+
+ </div>
+
+ <div class="panel login-logo-panel">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?= t('Login Image') ?></h3>
+ </div>
+
+ <img id="ll1" src="<?= $this->url->href('CustomizerFileController', 'loginlogo_setting', array('plugin' => 'customizer', 'file_id' => $loginlogo['id'])) ?>" alt="<?= $this->text->e($loginlogo['name']) ?>" height="<?= $this->task->configModel->get('loginlogo_size', '50') ?>">
+ <br>
+ <br>
+ <ul class="upload-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+
+ <?= $this->modal->medium('file', t('Upload Login Logo'), 'CustomizerFileController', 'create', array('plugin' => 'customizer', 'custom_id' => 3))?>
+ </ul>
+ <?php if (null !== $this->task->customizerFileModel->getByType(3)) : ?>
+ <ul class="remove-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+
+ <?= $this->modal->medium('remove', t('Remove Login Logo'), 'CustomizerFileController', 'confirm', array('plugin' => 'customizer', 'custom_id' => 3, 'file_id' => $loginlogo['id']))?>
+ </ul>
+ <?php endif ?>
+ <br><br>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Login Logo Size') ?></strong></th>
+ <th><input type="range" name="loginlogo_size" min="20" max="500" value="<?= $this->task->configModel->get('loginlogo_size','50') ?>">
+ <login_logo_output><?= $this->task->configModel->get('loginlogo_size','50') ?></login_logo_output><?= t('&nbsp;pixels high') ?>
+ </th>
+ </tr>
+ </table>
+
+ </div>
+
+ <div class="panel favicon-panel">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?= t('Favicon Image') ?></h3>
+ </div>
+
+ <img src="<?= $this->url->href('CustomizerFileController', 'icon_setting', array('plugin' => 'customizer', 'file_id' => $flavicon['id'])) ?>" alt="<?= $this->text->e($flavicon['name']) ?>" height="16">
+ <br>
+ <br>
+ <ul class="upload-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+ <?= $this->modal->medium('file', t('Upload Favicon'), 'CustomizerFileController', 'create', array('plugin' => 'customizer', 'custom_id' => 2))?>
+ </ul>
+ </ul>
+ <?php if (null !== $this->task->customizerFileModel->getByType(2)) : ?>
+ <ul class="remove-link">
+ <?php
+ ini_set('display_errors', 1);
+ ini_set('display_startup_errors', 1);
+ error_reporting(E_ALL);
+ ?>
+
+ <?= $this->modal->medium('remove', t('Remove Favicon'), 'CustomizerFileController', 'confirm', array('plugin' => 'customizer', 'custom_id' => 2, 'file_id' => $flavicon['id']))?>
+ </ul>
+ <?php endif ?>
+ </div>
+
+ <div class="panel avatar-sizing-panel">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?= t('Header Avatar Icon') ?></h3>
+ </div>
+ <?= $this->helper->dynamicAvatar->currentUserDynamic('avatar-preview') ?>
+ <br>
+ <br>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Header Avatar Icon Size') ?></strong></th>
+ <th><input type="range" name="av_size" id="av_size" min="20" max="50" value="<?= $this->task->configModel->get('av_size','20') ?>">
+ <av_icon_output> <?= $this->task->configModel->get('av_size','20') ?></av_icon_output><?= t('&nbsp;pixels') ?>
+ </th>
+ </tr>
+ <tr>
+ <th width="25%"><strong><?= t('Header Avatar Icon Radius') ?></strong></th>
+ <th><input type="range" name="av_radius" id="av_radius" min="0" max="50" value="<?= $this->task->configModel->get('av_radius','50') ?>">
+ <av_radius_output> <?= $this->task->configModel->get('av_radius','50') ?></av_radius_output><?= t('&nbsp;percent') ?>
+ </th>
+ </tr>
+ </table>
+
+ </div>
+
+ <div class="panel b-avatar-sizing-panel">
+ <div class="panel-heading">
+ <h3 class="panel-title"><?= t('Task Board Avatar Icon') ?></h3>
+ </div>
+ <?= $this->helper->dynamicAvatar->boardCurrentUserDynamic('b-avatar-preview') ?>
+ <br>
+ <br>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Avatar Icon Size') ?></strong></th>
+ <th><input type="range" name="b_av_size" id="b_av_size" min="20" max="50" value="<?= $this->task->configModel->get('b_av_size','20') ?>">
+ <b_av_icon_output> <?= $this->task->configModel->get('b_av_size','20') ?></b_av_icon_output><?= t('&nbsp;pixels') ?>
+ </th>
+ </tr>
+ <tr>
+ <th width="25%"><strong><?= t('Avatar Icon Radius') ?></strong></th>
+ <th><input type="range" name="b_av_radius" id="b_av_radius" min="0" max="50" value="<?= $this->task->configModel->get('b_av_radius','50') ?>">
+ <b_av_radius_output> <?= $this->task->configModel->get('b_av_radius','50') ?></b_av_radius_output><?= t('&nbsp;percent') ?>
+ </th>
+ </tr>
+ </table>
+
+ </div>
+<style>
+.avatar-bdyn img, .avatar-bdyn div {border-radius: <?= $this->task->configModel->get('b_av_radius', '50') ?>%}
+.avatar-bdyn .avatar-letter {line-height:<?= $this->task->configModel->get('b_av_size', '20') ?>px;width:<?= $this->task->configModel->get('b_av_size', '20') ?>px;font-size:<?= $this->task->configModel->get('b_av_size', '20') / 2 ?>px;}
+</style>
+
+
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Enable Cache') ?></strong>
+ <p class="form-help enable-cache-desc"><?= e('Once enabled, site assets will begin to be cached for 5 days, increasing speed of site. However, you will need to clear your cache to see any new images uploaded. The settings page, will be unaffected by this setting.') ?></p>
+ </th>
+ <th>
+ <label class="switch">
+ <input id="toggle" name="enable_cache" type="checkbox" value="checked" <?= $this->task->configModel->get('enable_cache','') ?>>
+ <span class="slider round"></span>
+ </label>
+ </th>
+ </tr>
+ </table>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Logo Generator') ?></strong>
+ <p class="form-help enable-cache-desc"><?= e('Experimental Tool, to create simple logos for those in need.') ?></p>
+ </th>
+ <th>
+ <a href="https://creecros.github.io/simple_logo_gen/">
+ <img border="0" alt="logo_gen" src="/plugins/Customizer/Assets/img/logo-gen.png">
+ </a>
+ </th>
+ </table>
+ <div class="form-actions mb-20 ml-15">
+ <button type="submit" name="save" value="save" class="btn btn-blue"><?= t('Save') ?></button>
+ </div>
+ </div>
+
+ <button type="button" class="login-accordion"><i class="fa fa-sign-in" aria-hidden="true"></i> <?= t('Login Page Settings') ?></button>
+ <?php if ($this->task->configModel->get('use_custom_login', '') == 'checked') : ?>
+ <div class="login-accordian-panel mt-10">
+ <?php else :?>
+ <div class="login-accordian-panel mt-10">
+ <?php endif ?>
+ <table>
+ <tr>
+ <th width="25%"><strong><?= t('Use Custom Login Settings') ?></strong></th>
+ <th>
+ <label class="switch">
+ <input id="toggle" name="use_custom_login" type="checkbox" value="checked" <?= $this->task->configModel->get('use_custom_login','') ?>>
+ <span class="slider round"></span>
+ </label>
+ </th>
+ </tr>
+ </table>
+
+ <?php if ($this->task->configModel->get('use_custom_login', '') == 'checked') : ?>
+ <?= $this->form->label(t('Login Link'), 'login_link') ?>
+ <?= $this->form->text('login_link', $values, $errors, array('placeholder="https://example.kanboard.org/"')) ?>
+ <p class="form-help login-link-desc"><?= e('Example: <code>https://example.kanboard.org/</code> (used as logo link on login page)') ?></p>
+ <?= $this->form->label(t('Login Background Image URL'), 'background_url') ?>
+ <?= $this->form->text('background_url', $values, $errors, array('placeholder="https://source.unsplash.com/random"')) ?>
+ <p class="form-help background-img-link-desc"><?= e('Example: <code>https://source.unsplash.com/random</code> (URL for a background image on the login page, centered, autoscale, no-repeat)') ?></p>
+ <?= $this->form->label(t('Login page note'), 'login_note') ?>
+ <?= $this->form->textarea('login_note', $values, $errors, array('placeholder="Welcome to Kanboard!"')) ?>
+ <p class="form-help login-note-desc"><?= e('Hint: Use HTML formatting to customize your note further.') ?></p>
+ <div class="column-100">
+ <table>
+ <tr>
+ <th>
+ <strong><?= t('Login Page Background Color') ?></strong>
+ </th>
+ <th>
+ <input id="loginbackground_color" class="color" name="loginbackground_color" value="<?= $this->task->configModel->get('loginbackground_color','#ffffff') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Panel Shadow Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_shadow_color" class="color" name="login_shadow_color" value="<?= $this->task->configModel->get('login_shadow_color','#333') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Panel Border Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_border_color" class="color" name="login_border_color" value="<?= $this->task->configModel->get('login_border_color','#ffffff') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Panel Border Thickness') ?></strong>
+ </th>
+ <th>
+ <input id="login_border" type="range" name="login_border" min="0" max="10" value="<?= $this->task->configModel->get('login_border','0') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Panel Color') ?></strong>
+ </th>
+ <th>
+ <input id="loginpanel_color" class="color" name="loginpanel_color" value="<?= $this->task->configModel->get('loginpanel_color','#ffffff') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Panel Shadow Intensity') ?></strong>
+ </th>
+ <th>
+ <input id="login_shadow" type="range" name="login_shadow" min="0" max="20" value="<?= $this->task->configModel->get('login_shadow','0') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Background Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_color" class="color" name="login_btn_color" value="<?= $this->task->configModel->get('login_btn_color','#3079ed') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Shadow Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_shadow_color" class="color" name="login_btn_shadow_color" value="<?= $this->task->configModel->get('login_btn_shadow_color','#333') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Border Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_border_color" class="color" name="login_btn_border_color" value="<?= $this->task->configModel->get('login_btn_border_color','transparent') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Shade Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_shade_color" class="color" name="login_btn_shade_color" value="<?= $this->task->configModel->get('login_btn_shade_color','transparent') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Font Color') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_font_color" class="color" name="login_btn_font_color" value="<?= $this->task->configModel->get('login_btn_font_color','#ffffff') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Shadow Intensity') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_shadow" type="range" name="login_btn_shadow" min="0" max="20" value="<?= $this->task->configModel->get('login_btn_shadow','0') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Border Thickness') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_border" type="range" name="login_btn_border" min="0" max="10" value="<?= $this->task->configModel->get('login_btn_border','0') ?>">
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <strong><?= t('Login Button Width') ?></strong>
+ </th>
+ <th>
+ <input id="login_btn_width" type="range" name="login_btn_width" min="95" max="300" value="<?= $this->task->configModel->get('login_btn_width','95') ?>">
+ </th>
+ </tr>
+ </table>
+ <p class="alert" style="max-width: 1000px;"><?= t('Changes must be saved in order to take effect.') ?> <button type="submit" name="save" value="save" class="btn btn-blue" style="float: right;margin-top: -6px;"><?= t('Save') ?></button></p>
+ <div class="panel" id="preview" style="background: url('<?= $customizer['backURL'] ?>') no-repeat center center;background-size: cover;height: 700px;max-width: 1000px;background-color: <?= $customizer['backColor'] ?>;">
+ <div>
+ <p style="color: #f5f5f5;"><?= t('Preview') ?></p>
+ </div>
+ <div id="preview-form-login" class="preview-form-login">
+ <?php if ($customizer['loginCheck']): ?>
+ <?= $this->url->link('<img src="' . $this->url->href('CustomizerFileController', 'loginlogo_setting', array('plugin' => 'customizer')) . '" height="' . $customizer['logoSize'] . '">', 'CustomizerFileController', 'link', array('plugin' => 'customizer')) ?>
+ <?php else: ?>
+ <?= $this->url->link('K<span>B</span>', 'DashboardController', 'show', array(), false, '', t('Dashboard')) ?>
+ <?php endif ?>
+
+ <label for="form-username"></label>
+ <input type="text" name="username" placeholder="<?= t('Enter your username') ?>" style="
+ border-radius: 5px;
+ ">
+ <span class="preview-form-required"></span>
+ <label for="form-password"></label>
+ <input type="password" name="password" placeholder="<?= t('Enter your password') ?>" style="
+ border-radius: 5px;
+ ">
+ <span class="preview-form-required"></span>
+ <label style="color:grey"><input type="checkbox" name="remember_me" value="1" checked="checked" disabled>&nbsp; <?= t('Remember Me') ?></label>
+ <div style="margin-bottom: 10px !important;"></div>
+ <div class="preview-form-actions">
+ <button type="button" id="preview-login-btn" class="btn preview-login-btn"><?= t('Sign in') ?></button>
+ </div>
+ <?php if ($this->app->config('password_reset') == 1): ?>
+ <div class="reset-password">
+ <?= $this->url->link(t('Forgot password?'), 'PasswordResetController', 'create') ?>
+ </div>
+ <?php endif ?>
+ </div>
+ <div id="preview-form-note" class="preview-form-note">
+ <div class="login-note">
+ <?= $customizer['login_note'] ?>
+ </div>
+ </div>
+ </div>
+ </div>
+ <?php endif ?>
+ </div>
+ <button type="button" class="login-accordion"><i class="fa fa-refresh" aria-hidden="true"></i> <?= t('Manage Themes') ?></button>
+ <div class="login-accordian-panel mt-10">
+ <div class="panel header-logo-panel">
+ <h3 class="panel-title"><?= t('Global Themes') ?></h3>
+ <?= $this->form->label(t('Theme'), 'themeSelection') ?>
+ <?= $this->helper->themeHelper->reverseSelect('themeSelection', $customizer['themes'], $values, $errors) ?>
+ <p class="form-help theme-select"><?= e('This will be the theme selection for all users who have not chosen their own theme.') ?></p>
+ <div class="form-actions" style="margin-bottom: 50px">
+ <button type="submit" name="save" value="save" class="btn btn-blue"><?= t('Save') ?> </button><button type="submit" name="remove" value="remove" class="btn btn-red"><?= t('Remove') ?></button>
+ </div>
+ </div>
+ </form>
+
+ <form method="post" enctype="multipart/form-data" action="<?= $this->url->href('CustomizerConfigController', 'uploadcss', array('plugin' => 'customizer')) ?>">
+ <div class="panel header-logo-panel">
+ <h3 class="panel-title"><?= t('Upload a theme') ?></h3>
+ <input type="file" name="fileToUpload" id="fileToUpload">
+ <input type="submit" class="btn btn-blue" value="<?= t('Add Theme') ?>" name="submit">
+ </div>
+ </form>
+ <form method="post" enctype="multipart/form-data" action="<?= $this->url->href('CustomizerConfigController', 'resetUserThemes', array('plugin' => 'customizer')) ?>">
+ <div class="panel header-logo-panel">
+ <h3 class="panel-title"><?= t('Users themes option') ?></h3>
+ <input type="submit" class="btn btn-red" value="<?= t('Reset All Users Themes') ?>" name="submit">
+ </div>
+ </form>
+ <form method="post" enctype="multipart/form-data" action="<?= $this->url->href('CustomizerConfigController', 'enableDisableThemes', array('plugin' => 'customizer')) ?>">
+ <div class="panel header-logo-panel">
+ <h3 class="panel-title"><?= t('Toggle Users themes') ?></h3>
+ <?php if ($this->task->configModel->get('toggle_user_themes', 'disable') == 'disable') : ?>
+ <input type="submit" class="btn btn-blue" value="<?= t('Enable Users Themes') ?>" name="submit">
+ <?php else :?>
+ <input type="submit" class="btn btn-red" value="<?= t('Disable Users Themes') ?>" name="submit">
+ <?php endif ?>
+ </div>
+ </form>
+
+
+ </div>
+
+ <button type="button" class="login-accordion"><i class="fa fa-magic" aria-hidden="true"></i> <?= t('Theme Creator') ?></button>
+ <div class="login-accordian-panel mt-10">
+ <?= $this->hook->render('customizer:config:themecreator') ?>
+ </div>
+ </div>
+</fieldset>
diff --git a/plugins/Customizer/Template/file/upload_flavicon.php b/plugins/Customizer/Template/file/upload_flavicon.php
new file mode 100644
index 00000000..b0e8988c
--- /dev/null
+++ b/plugins/Customizer/Template/file/upload_flavicon.php
@@ -0,0 +1,22 @@
+<div class="page-header">
+ <h2><?= t('Favicon') ?></h2>
+ <br>
+ <?= t('Recommend 50x50 pixels, *.png only, max size 20kb.') ?>
+</div>
+
+<?= $this->app->component('file-upload', array(
+ 'maxSize' => 20000,
+ 'url' => $this->url->to('CustomizerFileController', 'save', array('plugin' => 'customizer', 'custom_id' => 2)),
+ 'labelDropzone' => t('Drag and drop your file here'),
+ 'labelOr' => t('or'),
+ 'labelChooseFiles' => t('choose file'),
+ 'labelOversize' => t('The maximum allowed file size is %sB.', $this->text->bytes(20000)),
+ 'labelSuccess' => t('File has been uploaded successfully.'),
+ 'labelCloseSuccess' => t('Close this window'),
+ 'labelUploadError' => t('Unable to upload this file.'),
+)) ?>
+
+<?= $this->modal->submitButtons(array(
+ 'submitLabel' => t('Upload Favicon'),
+ 'disabled' => true,
+)) ?>
diff --git a/plugins/Customizer/Template/file/upload_loginlogo.php b/plugins/Customizer/Template/file/upload_loginlogo.php
new file mode 100644
index 00000000..fa715f36
--- /dev/null
+++ b/plugins/Customizer/Template/file/upload_loginlogo.php
@@ -0,0 +1,22 @@
+<div class="page-header">
+ <h2><?= t('Login Logo') ?></h2>
+ <br>
+ <?= t('Recommend 100 pixels in height, *.png, *.jpg, *.gif, max size 500kb.') ?>
+</div>
+
+<?= $this->app->component('file-upload', array(
+ 'maxSize' => 500000,
+ 'url' => $this->url->to('CustomizerFileController', 'save', array('plugin' => 'customizer', 'custom_id' => 3)),
+ 'labelDropzone' => t('Drag and drop your file here'),
+ 'labelOr' => t('or'),
+ 'labelChooseFiles' => t('choose file'),
+ 'labelOversize' => t('The maximum allowed file size is %sB.', $this->text->bytes(500000)),
+ 'labelSuccess' => t('File has been uploaded successfully.'),
+ 'labelCloseSuccess' => t('Close this window'),
+ 'labelUploadError' => t('Unable to upload this file.'),
+)) ?>
+
+<?= $this->modal->submitButtons(array(
+ 'submitLabel' => t('Upload Login Logo'),
+ 'disabled' => true,
+)) ?>
diff --git a/plugins/Customizer/Template/file/upload_logo.php b/plugins/Customizer/Template/file/upload_logo.php
new file mode 100644
index 00000000..0dae640c
--- /dev/null
+++ b/plugins/Customizer/Template/file/upload_logo.php
@@ -0,0 +1,22 @@
+<div class="page-header">
+ <h2><?= t('Header Logo') ?></h2>
+ <br>
+ <?= t('Recommend 100 pixels in width, *.png, *.jpg, *.gif, max size 500kb.') ?>
+</div>
+
+<?= $this->app->component('file-upload', array(
+ 'maxSize' => 500000,
+ 'url' => $this->url->to('CustomizerFileController', 'save', array('plugin' => 'customizer', 'custom_id' => 1)),
+ 'labelDropzone' => t('Drag and drop your file here'),
+ 'labelOr' => t('or'),
+ 'labelChooseFiles' => t('choose file'),
+ 'labelOversize' => t('The maximum allowed file size is %sB.', $this->text->bytes(500000)),
+ 'labelSuccess' => t('File has been uploaded successfully.'),
+ 'labelCloseSuccess' => t('Close this window'),
+ 'labelUploadError' => t('Unable to upload this file.'),
+)) ?>
+
+<?= $this->modal->submitButtons(array(
+ 'submitLabel' => t('Upload Header Logo'),
+ 'disabled' => true,
+)) ?>
diff --git a/plugins/Customizer/Template/header/title.php b/plugins/Customizer/Template/header/title.php
new file mode 100644
index 00000000..5080c9be
--- /dev/null
+++ b/plugins/Customizer/Template/header/title.php
@@ -0,0 +1,24 @@
+<h1>
+ <?php if (null !== $this->task->customizerFileModel->getByType(1)) : ?>
+ <span class="logo">
+ <a href="<?= $this->url->href('DashboardController', 'show', array(), false, '', t('Dashboard')) ?>">
+ <img src="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(1))) ?>" height="<?= $this->task->configModel->get('headerlogo_size', '30') ?>">
+ </a>
+ </span>
+ <?php else: ?>
+ <span class="logo">
+ <?= $this->url->link('K<span>B</span>', 'DashboardController', 'show', array(), false, '', t('Dashboard')) ?>
+ </span>
+ <?php endif ?>
+
+ <span class="title">
+ <?php if (! empty($project) && ! empty($task)): ?>
+ <?= $this->url->link($this->text->e($project['name']), 'BoardViewController', 'show', array('project_id' => $project['id'])) ?>
+ <?php else: ?>
+ <?= $this->text->e($title) ?>
+ <?php endif ?>
+ </span>
+ <?php if (! empty($description)): ?>
+ <?= $this->app->tooltipHTML($description) ?>
+ <?php endif ?>
+</h1>
diff --git a/plugins/Customizer/Template/header/title_older_kb.php b/plugins/Customizer/Template/header/title_older_kb.php
new file mode 100644
index 00000000..a38ff323
--- /dev/null
+++ b/plugins/Customizer/Template/header/title_older_kb.php
@@ -0,0 +1,26 @@
+<h1>
+ <?php if (null !== $this->task->customizerFileModel->getByType(1)) : ?>
+ <span class="logo">
+ <a href="<?= $this->url->href('DashboardController', 'show', array(), false, '', t('Dashboard')) ?>">
+ <img src="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(1))) ?>" height="<?= $this->task->configModel->get('headerlogo_size', '30') ?>">
+ </a>
+ </span>
+ <?php else: ?>
+ <span class="logo">
+ <?= $this->url->link('K<span>B</span>', 'DashboardController', 'show', array(), false, '', t('Dashboard')) ?>
+ </span>
+ <?php endif ?>
+
+ <span class="title">
+ <?php if (! empty($project) && ! empty($task)): ?>
+ <?= $this->url->link($this->text->e($project['name']), 'BoardViewController', 'show', array('project_id' => $project['id'])) ?>
+ <?php else: ?>
+ <?= $this->text->e($title) ?>
+ <?php endif ?>
+ </span>
+ <?php if (! empty($description)): ?>
+ <small class="tooltip" title="<?= $this->text->markdownAttribute($description) ?>">
+ <i class="fa fa-info-circle"></i>
+ </small>
+ <?php endif ?>
+</h1>
diff --git a/plugins/Customizer/Template/header/user_dropdown.php b/plugins/Customizer/Template/header/user_dropdown.php
new file mode 100644
index 00000000..d302924f
--- /dev/null
+++ b/plugins/Customizer/Template/header/user_dropdown.php
@@ -0,0 +1,45 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" style="display:flex;position:relative;align-items:center;flex-direction:row;"><?= $this->helper->dynamicAvatar->currentUserDynamic('avatar-inline') ?><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li class="no-hover"><strong><?= $this->text->e($this->user->getFullname()) ?></strong></li>
+ <li>
+ <?= $this->url->icon('tachometer', t('My dashboard'), 'DashboardController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('home', t('My profile'), 'UserViewController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('folder', t('Projects management'), 'ProjectListController', 'show') ?>
+ </li>
+ <?php if ($this->user->hasAccess('UserListController', 'show')): ?>
+ <li>
+ <?= $this->url->icon('user', t('Users management'), 'UserListController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('group', t('Groups management'), 'GroupListController', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cubes', t('Plugins'), 'PluginController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cog', t('Settings'), 'ConfigController', 'index') ?>
+ </li>
+ <?php endif ?>
+
+ <?= $this->hook->render('template:header:dropdown') ?>
+
+ <li>
+ <i class="fa fa-fw fa-life-ring" aria-hidden="true"></i>
+ <?= $this->url->doc(t('Documentation'), 'index') ?>
+ </li>
+ <?php if (! DISABLE_LOGOUT): ?>
+ <li>
+ <?= $this->url->icon('sign-out', t('Logout'), 'AuthController', 'logout') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
+<style>
+.avatar-dyn img, .avatar-dyn div {border-radius: <?= $this->task->configModel->get('av_radius', '50') ?>%}
+.avatar-dyn .avatar-letter {line-height:<?= $this->task->configModel->get('av_size', '20') ?>px;width:<?= $this->task->configModel->get('av_size', '20') ?>px;font-size:<?= $this->task->configModel->get('av_size', '20') / 2 ?>px;}
+</style>
diff --git a/plugins/Customizer/Template/layout/index.php b/plugins/Customizer/Template/layout/index.php
new file mode 100644
index 00000000..587b3e04
--- /dev/null
+++ b/plugins/Customizer/Template/layout/index.php
@@ -0,0 +1,47 @@
+<div class="form-login">
+
+ <?= $this->hook->render('template:auth:login-form:before') ?>
+
+ <?php if (isset($errors['login'])): ?>
+ <p class="alert alert-error"><?= $this->text->e($errors['login']) ?></p>
+ <?php endif ?>
+
+ <?php if (! HIDE_LOGIN_FORM): ?>
+ <form method="post" action="<?= $this->url->href('AuthController', 'check') ?>">
+
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', 'placeholder="Enter your username"')) ?>
+
+ <?= $this->form->label(t('Password'), 'password') ?>
+ <?= $this->form->password('password', $values, $errors, array('required', 'placeholder="Enter your password"')) ?>
+
+ <?php if (isset($captcha) && $captcha): ?>
+ <?= $this->form->label(t('Enter the text below'), 'captcha') ?>
+ <img src="<?= $this->url->href('CaptchaController', 'image') ?>" alt="Captcha">
+ <?= $this->form->text('captcha', array(), $errors, array('required')) ?>
+ <?php endif ?>
+
+ <?php if (REMEMBER_ME_AUTH == true): ?>
+ <?= $this->form->checkbox('remember_me', t('Remember Me'), 1, true) ?>
+ <div class="mb-10"></div>
+ <?php else: ?>
+ <div class="mb-15"></div>
+ <?php endif ?>
+
+ <div class="form-actions">
+ <button type="submit" class="btn login-btn"><?= t('Sign in') ?></button>
+ </div>
+ <?php if ($this->app->config('password_reset') == 1): ?>
+ <div class="reset-password">
+ <?= $this->url->link(t('Forgot password?'), 'PasswordResetController', 'create') ?>
+ </div>
+ <?php endif ?>
+ </form>
+ <?php endif ?>
+
+ <?= $this->hook->render('template:auth:login-form:after') ?>
+</div>
+
+<?= $this->hook->render('template:auth:login-form:newbox') ?>
diff --git a/plugins/Customizer/Template/layout/layout.php b/plugins/Customizer/Template/layout/layout.php
new file mode 100644
index 00000000..e5b4b772
--- /dev/null
+++ b/plugins/Customizer/Template/layout/layout.php
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <meta name="mobile-web-app-capable" content="yes">
+ <meta name="robots" content="noindex,nofollow">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="referrer" content="no-referrer">
+
+ <?php if (isset($board_public_refresh_interval)): ?>
+ <meta http-equiv="refresh" content="<?= $board_public_refresh_interval ?>">
+ <?php endif ?>
+
+ <?= $this->asset->colorCss() ?>
+ <?= $this->asset->css('assets/css/vendor.min.css') ?>
+ <?= $this->asset->css('assets/css/app.min.css') ?>
+ <?php if (file_exists('assets/css/print.min.css')) :?>
+ <?= $this->asset->css('assets/css/print.min.css', true, 'print') ?>
+ <?php endif ?>
+ <?= $this->asset->customCss() ?>
+
+ <?php if (! isset($not_editable)): ?>
+ <?= $this->asset->js('assets/js/vendor.min.js') ?>
+ <?= $this->asset->js('assets/js/app.min.js') ?>
+ <?php endif ?>
+
+ <?= $this->hook->asset('css', 'template:layout:css') ?>
+ <?= $this->hook->asset('js', 'template:layout:js') ?>
+ <?php if (null !== $this->task->customizerFileModel->getByType(2)) : ?>
+ <link rel="icon" type="image/png" href="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(2))) ?>">
+ <link rel="apple-touch-icon" href="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(2))) ?>">
+ <link rel="apple-touch-icon" sizes="72x72" href="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(2))) ?>">
+ <link rel="apple-touch-icon" sizes="114x114" href="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(2))) ?>">
+ <link rel="apple-touch-icon" sizes="144x144" href="<?= $this->url->href('CustomizerFileController', 'image', array('plugin' => 'customizer', 'file_id' => $this->task->customizerFileModel->getIdByType(2))) ?>">
+ <?php else: ?>
+ <link rel="icon" type="image/png" href="<?= $this->url->dir() ?>assets/img/favicon.png">
+ <link rel="apple-touch-icon" href="<?= $this->url->dir() ?>assets/img/touch-icon-iphone.png">
+ <link rel="apple-touch-icon" sizes="72x72" href="<?= $this->url->dir() ?>assets/img/touch-icon-ipad.png">
+ <link rel="apple-touch-icon" sizes="114x114" href="<?= $this->url->dir() ?>assets/img/touch-icon-iphone-retina.png">
+ <link rel="apple-touch-icon" sizes="144x144" href="<?= $this->url->dir() ?>assets/img/touch-icon-ipad-retina.png">
+ <?php endif ?>
+
+
+ <title>
+ <?php if (isset($page_title)): ?>
+ <?= $this->text->e($page_title) ?>
+ <?php elseif (isset($title)): ?>
+ <?= $this->text->e($title) ?>
+ <?php else: ?>
+ Kanboard
+ <?php endif ?>
+ </title>
+
+ <?= $this->hook->render('template:layout:head') ?>
+ </head>
+ <body data-status-url="<?= $this->url->href('UserAjaxController', 'status') ?>"
+ data-login-url="<?= $this->url->href('AuthController', 'login') ?>"
+ data-keyboard-shortcut-url="<?= $this->url->href('DocumentationController', 'shortcuts') ?>"
+ data-timezone="<?= $this->app->getTimezone() ?>"
+ data-js-lang="<?= $this->app->jsLang() ?>"
+ data-js-date-format="<?= $this->app->getJsDateFormat() ?>"
+ data-js-time-format="<?= $this->app->getJsTimeFormat() ?>"
+ data-js-modal-close-msg="<?= t('Close window?\\n\\nChanges that you made have not been saved.') ?>"
+ >
+
+ <?php if (isset($no_layout) && $no_layout): ?>
+ <?= $this->app->flashMessage() ?>
+ <?= $content_for_layout ?>
+ <?php else: ?>
+ <?= $this->hook->render('template:layout:top') ?>
+ <?= $this->render('header', array(
+ 'title' => $title,
+ 'description' => isset($description) ? $description : '',
+ 'board_selector' => isset($board_selector) ? $board_selector : array(),
+ 'project' => isset($project) ? $project : array(),
+ )) ?>
+ <section class="page">
+ <?= $this->app->flashMessage() ?>
+ <?= $content_for_layout ?>
+ </section>
+ <?= $this->hook->render('template:layout:bottom') ?>
+ <?php endif ?>
+ </body>
+</html>
diff --git a/plugins/Customizer/Template/layout/login_no_custom.php b/plugins/Customizer/Template/layout/login_no_custom.php
new file mode 100644
index 00000000..7e7cbd54
--- /dev/null
+++ b/plugins/Customizer/Template/layout/login_no_custom.php
@@ -0,0 +1,21 @@
+<?php global $customizer; ?>
+<?php if ($customizer['loginCheck']): ?>
+<?= $this->url->link('<img src="' . $this->url->href('CustomizerFileController', 'loginlogo', array('plugin' => 'customizer')) . '" height="' . $customizer['logoSize'] . '">', 'CustomizerFileController', 'link', array('plugin' => 'customizer')) ?>
+<?php endif ?>
+<?php
+if (function_exists('session_exists')) {
+if (session_exists('redirectAfterLogin') && ! filter_var(session_get('redirectAfterLogin'), FILTER_VALIDATE_URL)) {
+ $redirect = session_get('redirectAfterLogin');
+ if (strpos($redirect, 'Customizer') !== false) {
+ session_remove('redirectAfterLogin');
+ }
+}
+} else {
+if (isset($this->task->sessionStorage->redirectAfterLogin) && ! empty($this->task->sessionStorage->redirectAfterLogin) && ! filter_var($this->task->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
+ $redirect = $this->task->sessionStorage->redirectAfterLogin;
+ if (strpos($redirect, 'Customizer') !== false) {
+ unset($this->task->sessionStorage->redirectAfterLogin);
+ }
+}
+}
+?>
diff --git a/plugins/Customizer/Template/layout/login_with_custom.php b/plugins/Customizer/Template/layout/login_with_custom.php
new file mode 100644
index 00000000..1ca70d77
--- /dev/null
+++ b/plugins/Customizer/Template/layout/login_with_custom.php
@@ -0,0 +1,128 @@
+<?php global $customizer; ?>
+<?php if ($customizer['loginCheck']): ?>
+<?= $this->url->link('<img src="' . $this->url->href('CustomizerFileController', 'loginlogo', array('plugin' => 'customizer')) . '" height="' . $customizer['logoSize'] . '">', 'CustomizerFileController', 'link', array('plugin' => 'customizer')) ?>
+<?php endif ?>
+<style>
+body {
+ background: url("<?= $customizer['backURL'] ?>") no-repeat center center fixed;
+ background-size: cover;
+ background-color: <?= $customizer['backColor'] ?>;
+}
+.mb-10 {
+ margin-bottom: 10px !important;
+}
+.mb-15 {
+ margin-bottom: 15px !important;
+}
+
+.form-login > a > img {
+ display: block;
+ margin: auto;
+ padding-top: 10px
+} /* This aligns the logo to the text. Adds padding to top of logo. */
+
+.form-login {
+ background-color: <?= $customizer['loginpanel_color'] ?>;
+ -webkit-box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px <?= $customizer['login_shadow_color'] ?>;
+ -moz-box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px <?= $customizer['login_shadow_color'] ?>;
+ box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px <?= $customizer['login_shadow_color'] ?>;
+ padding: 10px;
+ border: <?= $customizer['login_border'] ?>px solid <?= $customizer['login_border_color'] ?>;
+ border-radius: 5px;
+ max-width: max-content;
+ text-align: center;
+}
+.login-btn {
+ width: <?= $customizer['login_btn_width'] ?>px;
+ -webkit-box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ -moz-box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ border: <?= $customizer['login_btn_border'] ?>px solid <?= $customizer['login_btn_border_color'] ?>;
+ background: <?= $customizer['login_btn_color'] ?>;
+ color: <?= $customizer['login_btn_font_color'] ?>;
+ background-image: linear-gradient(-180deg, <?= $customizer['login_btn_color'] ?> 0%, <?= $customizer['login_btn_shade_color'] ?> 90%);
+ border-radius: 5px;
+ }
+.login-btn:hover, .login-btn:focus {
+ border-color: <?= $customizer['login_btn_border_color'] ?>;
+ background: <?= $customizer['login_btn_font_color'] ?>;
+ color: <?= $customizer['login_btn_color'] ?>;
+}
+/*------ MOVED FROM PLUGIN CSS FILE TO AVOID AFFECTING OTHER PARTS OF KANBOARD. STYLES SET HERE APPLY ONLY TO THE LOGIN PAGE. ------*/
+.form-actions {
+ text-align: center;
+ padding-top: unset;
+ padding-bottom: 10px;
+} /* This moves the login button to the centre of the box and removes the useless padding above the login button. Adds padding to bottom of login button. */
+
+label:nth-of-type(3n) {
+ color: grey;
+ text-align: center;
+} /* This makes the 'remember me' smaller and centralised*/
+
+.form-actions > .login-btn {
+ font-variant-caps: all-small-caps;
+ text-align: center;
+ transition: cubic-bezier(0.1, 0.75, 0.57, 1) 0.4s;
+ -webkit-transition: cubic-bezier(0.1, 0.75, 0.57, 1) 0.4s;
+} /* This makes the title text of the login button all capitals. Also adds smoothing when hover on the login button */
+
+label:nth-of-type(1) {
+ visibility: hidden;
+} /* This hides (to maintain the gap) the text of the labels */
+
+label:nth-of-type(2n) {
+ visibility: hidden;
+ margin-top: -5px;
+} /* This hides (to maintain the gap) the text of the labels and also reduces the top margin */
+
+input::-webkit-input-placeholder {
+ color: #000;
+ opacity: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+} /* This styles the placeholder to emphasise it. Cross-browser compatibility */
+
+input::-moz-placeholder {
+ color: #000;
+ opacity: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+} /* This styles the placeholder to emphasise it. Cross-browser compatibility */
+
+input:-ms-input-placeholder {
+ color: #000;
+ opacity: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+} /* This styles the placeholder to emphasise it. Cross-browser compatibility */
+
+input::placeholder {
+ color: #000;
+ opacity: 1;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
+} /* This styles the placeholder to emphasise it. Cross-browser compatibility */
+
+input[type="password"], input[type="text"]:not(.input-addon-field) {
+ margin: auto;
+ display: block;
+ border-radius: 5px;
+} /* This centralises the input fields and makes the borders consistent with the outer form */
+
+.form-required { display: none;} /* This removes the standard required asterisk */
+
+</style>
+<?php
+if (function_exists('session_exists')) {
+if (session_exists('redirectAfterLogin') && ! filter_var(session_get('redirectAfterLogin'), FILTER_VALIDATE_URL)) {
+ $redirect = session_get('redirectAfterLogin');
+ if (strpos($redirect, 'Customizer') !== false) {
+ session_remove('redirectAfterLogin');
+ }
+}
+} else {
+if (isset($this->task->sessionStorage->redirectAfterLogin) && ! empty($this->task->sessionStorage->redirectAfterLogin) && ! filter_var($this->task->sessionStorage->redirectAfterLogin, FILTER_VALIDATE_URL)) {
+ $redirect = $this->task->sessionStorage->redirectAfterLogin;
+ if (strpos($redirect, 'Customizer') !== false) {
+ unset($this->task->sessionStorage->redirectAfterLogin);
+ }
+}
+}
+?>
diff --git a/plugins/Customizer/Template/layout/note.php b/plugins/Customizer/Template/layout/note.php
new file mode 100644
index 00000000..b8454e64
--- /dev/null
+++ b/plugins/Customizer/Template/layout/note.php
@@ -0,0 +1,7 @@
+<?php global $customizer; ?>
+
+<div class="form-login">
+ <div class="login-note">
+ <?= $customizer['login_note'] ?>
+ </div>
+</div>
diff --git a/plugins/Customizer/Template/layout/preview_style.php b/plugins/Customizer/Template/layout/preview_style.php
new file mode 100644
index 00000000..fdd9576f
--- /dev/null
+++ b/plugins/Customizer/Template/layout/preview_style.php
@@ -0,0 +1,77 @@
+<?php global $customizer; ?>
+<style>
+.preview-form-login > a > img {
+ display: block;
+ margin: auto;
+ padding-top: 10px
+} /* This aligns the logo to the text. Adds padding to top of logo. */
+
+
+.preview-form-login, .preview-form-note {
+ background-color: <?= $customizer['loginpanel_color'] ?>;
+ -webkit-box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px;
+ -moz-box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px;
+ box-shadow: 0px 0px <?= $customizer['login_shadow'] ?>px <?= $customizer['login_shadow'] * .1 ?>px;
+ padding: 10px;
+ border: <?= $customizer['login_border'] ?>px solid <?= $customizer['login_border_color'] ?>;
+ border-radius: 5px;
+ text-align: center;
+ max-width: max-content;
+ margin: 5% auto 0;;
+}
+.preview-login-btn {
+ width: <?= $customizer['login_btn_width'] ?>px;
+ -webkit-box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ -moz-box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ box-shadow: 0px 0px <?= $customizer['login_btn_shadow'] ?>px <?= $customizer['login_btn_shadow'] * .1 ?>px <?= $customizer['login_btn_shadow_color'] ?>;
+ border: <?= $customizer['login_btn_border'] ?>px solid <?= $customizer['login_btn_border_color'] ?>;
+ background: <?= $customizer['login_btn_color'] ?>;
+ color: <?= $customizer['login_btn_font_color'] ?>;
+ background-image: linear-gradient(-180deg, transparent 0%, <?= $customizer['login_btn_shade_color'] ?> 90%);
+ border-radius: 5px;
+ }
+
+.preview-login-btn:hover, .preview-login-btn:focus {
+ border-color: <?= $customizer['login_btn_border_color'] ?>;
+ background: <?= $customizer['login_btn_font_color'] ?>;
+ color: <?= $customizer['login_btn_color'] ?>;
+ background-image: unset;
+}
+
+.preview-form-actions > .preview-login-btn {
+ font-variant-caps: all-small-caps;
+ text-align: center;
+ transition: cubic-bezier(0.1, 0.75, 0.57, 1) 0.4s;
+ -webkit-transition: cubic-bezier(0.1, 0.75, 0.57, 1) 0.4s;
+}
+
+
+.preview-form-actions {
+ text-align: center;
+ padding-top: unset;
+ padding-bottom: 10px;
+} /* This moves the login button to the centre of the box and removes the useless padding above the login button. Adds padding to bottom of login button. */
+
+
+
+.preview-form-required { display: none;} /* This removes the standard required asterisk */
+
+.avatar-preview img, .avatar-preview div {
+ border-radius: <?= $this->task->configModel->get('av_radius','50') ?>%;
+}
+.avatar-preview .avatar-letter {
+ line-height: <?= $this->task->configModel->get('av_size','20') ?>px;
+ width: <?= $this->task->configModel->get('av_size','20') ?>px;
+ font-size: <?= $this->task->configModel->get('av_size','20') / 2 ?>px;
+}
+
+.b-avatar-preview img, .b-avatar-preview div {
+ border-radius: <?= $this->task->configModel->get('b_av_radius','50') ?>%;
+}
+.b-avatar-preview .avatar-letter {
+ line-height: <?= $this->task->configModel->get('b_av_size','20') ?>px;
+ width: <?= $this->task->configModel->get('b_av_size','20') ?>px;
+ font-size: <?= $this->task->configModel->get('b_av_size','20') / 2 ?>px;
+}
+
+</style>
diff --git a/plugins/Customizer/Template/user_mod/show.php b/plugins/Customizer/Template/user_mod/show.php
new file mode 100644
index 00000000..a4fbaf96
--- /dev/null
+++ b/plugins/Customizer/Template/user_mod/show.php
@@ -0,0 +1,58 @@
+<?php
+global $customizer;
+$user_theme['themeSelection'] = $this->task->userMetadataModel->get($user['id'], 'themeSelection', $this->task->configModel->get('themeSelection', ''));
+?>
+
+<div class="page-header">
+ <h2><?= t('Edit user') ?></h2>
+</div>
+
+<form method="post" id="ts" action="<?= $this->url->href('CustomizerConfigController', 'usertheme', array('plugin' => 'customizer', 'user_id' => $user['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+ <fieldset>
+ <legend><?= t('Themes') ?></legend>
+ <?= $this->form->label(t('User Theme'), 'themeSelection') ?>
+ <?= $this->helper->themeHelper->reverseSelectOnChange('themeSelection', $customizer['themes'], $user_theme, $errors) ?>
+ </fieldset>
+</form>
+
+<form method="post" action="<?= $this->url->href('UserModificationController', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+ <?= $this->form->hidden('id', $values) ?>
+
+ <fieldset>
+ <legend><?= t('Profile') ?></legend>
+ <?= $this->form->label(t('Username'), 'username') ?>
+ <?= $this->form->text('username', $values, $errors, array('autofocus', 'required', isset($values['is_ldap_user']) && $values['is_ldap_user'] == 1 && !$this->user->isAdmin() ? 'readonly' : '', 'maxlength="191"')) ?>
+
+ <?= $this->form->label(t('Name'), 'name') ?>
+ <?= $this->form->text('name', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_name') ? '' : 'readonly')) ?>
+
+ <?= $this->form->label(t('Email'), 'email') ?>
+ <?= $this->form->email('email', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_email') ? '' : 'readonly')) ?>
+ </fieldset>
+
+ <fieldset>
+ <legend><?= t('Preferences') ?></legend>
+ <?= $this->form->label(t('Timezone'), 'timezone') ?>
+ <?= $this->form->select('timezone', $timezones, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_timezone') ? '' : 'disabled')) ?>
+
+ <?= $this->form->label(t('Language'), 'language') ?>
+ <?= $this->form->select('language', $languages, $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_language') ? '' : 'disabled')) ?>
+
+ <?= $this->form->label(t('Filter'), 'filter') ?>
+ <?= $this->form->text('filter', $values, $errors, array($this->user->hasAccess('UserModificationController', 'show/edit_filter') ? '' : 'readonly')) ?>
+ </fieldset>
+
+ <?php if ($this->user->isAdmin()): ?>
+ <fieldset>
+ <legend><?= t('Security') ?></legend>
+ <?= $this->form->label(t('Application role'), 'role') ?>
+ <?= $this->form->select('role', $roles, $values, $errors) ?>
+ </fieldset>
+ <?php endif ?>
+
+ <?= $this->modal->submitButtons() ?>
+</form>
+ \ No newline at end of file
diff --git a/plugins/Customizer/_config.yml b/plugins/Customizer/_config.yml
new file mode 100644
index 00000000..be854e84
--- /dev/null
+++ b/plugins/Customizer/_config.yml
@@ -0,0 +1,3 @@
+theme: jekyll-theme-cayman
+plugins:
+ - jemoji
diff --git a/plugins/Customizer/composer.json b/plugins/Customizer/composer.json
new file mode 100644
index 00000000..8cc13120
--- /dev/null
+++ b/plugins/Customizer/composer.json
@@ -0,0 +1,5 @@
+{
+ "require": {
+ "luizbills/css-generator": "^3.1"
+ }
+}
diff --git a/plugins/Customizer/composer.lock b/plugins/Customizer/composer.lock
new file mode 100644
index 00000000..15ff3948
--- /dev/null
+++ b/plugins/Customizer/composer.lock
@@ -0,0 +1,164 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "7c8522680994b684990839bf7e954b59",
+ "packages": [
+ {
+ "name": "luizbills/css-generator",
+ "version": "v3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/luizbills/css-generator.php.git",
+ "reference": "5120ebd4e47f041c5e9bbe3395b05f1486264c26"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/luizbills/css-generator.php/zipball/5120ebd4e47f041c5e9bbe3395b05f1486264c26",
+ "reference": "5120ebd4e47f041c5e9bbe3395b05f1486264c26",
+ "shasum": ""
+ },
+ "require": {
+ "matthiasmullie/minify": "^1.3",
+ "php": ">=5.4.0"
+ },
+ "type": "package",
+ "autoload": {
+ "psr-4": {
+ "luizbills\\CSS_Generator\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Luiz Bills",
+ "email": "luizpbills@gmail.com"
+ }
+ ],
+ "description": "Write CSS programatically using PHP.",
+ "time": "2018-09-21T18:01:30+00:00"
+ },
+ {
+ "name": "matthiasmullie/minify",
+ "version": "1.3.60",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/matthiasmullie/minify.git",
+ "reference": "ab7fea80ce5ce6549baaf272bc8bd926a7e08f90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ab7fea80ce5ce6549baaf272bc8bd926a7e08f90",
+ "reference": "ab7fea80ce5ce6549baaf272bc8bd926a7e08f90",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "matthiasmullie/path-converter": "~1.1",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.0",
+ "matthiasmullie/scrapbook": "~1.0",
+ "phpunit/phpunit": "~4.8"
+ },
+ "suggest": {
+ "psr/cache-implementation": "Cache implementation to use with Minify::cache"
+ },
+ "bin": [
+ "bin/minifycss",
+ "bin/minifyjs"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\Minify\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "email": "minify@mullie.eu",
+ "homepage": "http://www.mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.",
+ "homepage": "http://www.minifier.org",
+ "keywords": [
+ "JS",
+ "css",
+ "javascript",
+ "minifier",
+ "minify"
+ ],
+ "time": "2018-04-18T08:50:35+00:00"
+ },
+ {
+ "name": "matthiasmullie/path-converter",
+ "version": "1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/matthiasmullie/path-converter.git",
+ "reference": "5e4b121c8b9f97c80835c1d878b0812ba1d607c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/matthiasmullie/path-converter/zipball/5e4b121c8b9f97c80835c1d878b0812ba1d607c9",
+ "reference": "5e4b121c8b9f97c80835c1d878b0812ba1d607c9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\PathConverter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "email": "pathconverter@mullie.eu",
+ "homepage": "http://www.mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "Relative path converter",
+ "homepage": "http://github.com/matthiasmullie/path-converter",
+ "keywords": [
+ "converter",
+ "path",
+ "paths",
+ "relative"
+ ],
+ "time": "2018-10-25T15:19:41+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": []
+}
diff --git a/plugins/Customizer/vendor/autoload.php b/plugins/Customizer/vendor/autoload.php
new file mode 100644
index 00000000..fe7dc899
--- /dev/null
+++ b/plugins/Customizer/vendor/autoload.php
@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit1a4d3188031158859b352a6b53ee769d::getLoader();
diff --git a/plugins/Customizer/vendor/bin/minifycss b/plugins/Customizer/vendor/bin/minifycss
new file mode 100644
index 00000000..04f60a4b
--- /dev/null
+++ b/plugins/Customizer/vendor/bin/minifycss
@@ -0,0 +1 @@
+../matthiasmullie/minify/bin/minifycss \ No newline at end of file
diff --git a/plugins/Customizer/vendor/bin/minifyjs b/plugins/Customizer/vendor/bin/minifyjs
new file mode 100644
index 00000000..61124467
--- /dev/null
+++ b/plugins/Customizer/vendor/bin/minifyjs
@@ -0,0 +1 @@
+../matthiasmullie/minify/bin/minifyjs \ No newline at end of file
diff --git a/plugins/Customizer/vendor/composer/ClassLoader.php b/plugins/Customizer/vendor/composer/ClassLoader.php
new file mode 100644
index 00000000..dc02dfb1
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/ClassLoader.php
@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see http://www.php-fig.org/psr/psr-0/
+ * @see http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ // PSR-4
+ private $prefixLengthsPsr4 = array();
+ private $prefixDirsPsr4 = array();
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ private $prefixesPsr0 = array();
+ private $fallbackDirsPsr0 = array();
+
+ private $useIncludePath = false;
+ private $classMap = array();
+ private $classMapAuthoritative = false;
+ private $missingClasses = array();
+ private $apcuPrefix;
+
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', $this->prefixesPsr0);
+ }
+
+ return array();
+ }
+
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 base directories
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath.'\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/plugins/Customizer/vendor/composer/LICENSE b/plugins/Customizer/vendor/composer/LICENSE
new file mode 100644
index 00000000..f27399a0
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/plugins/Customizer/vendor/composer/autoload_classmap.php b/plugins/Customizer/vendor/composer/autoload_classmap.php
new file mode 100644
index 00000000..7a91153b
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/autoload_classmap.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/plugins/Customizer/vendor/composer/autoload_namespaces.php b/plugins/Customizer/vendor/composer/autoload_namespaces.php
new file mode 100644
index 00000000..b7fc0125
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/plugins/Customizer/vendor/composer/autoload_psr4.php b/plugins/Customizer/vendor/composer/autoload_psr4.php
new file mode 100644
index 00000000..05c26176
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/autoload_psr4.php
@@ -0,0 +1,12 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ 'luizbills\\CSS_Generator\\' => array($vendorDir . '/luizbills/css-generator/src'),
+ 'MatthiasMullie\\PathConverter\\' => array($vendorDir . '/matthiasmullie/path-converter/src'),
+ 'MatthiasMullie\\Minify\\' => array($vendorDir . '/matthiasmullie/minify/src'),
+);
diff --git a/plugins/Customizer/vendor/composer/autoload_real.php b/plugins/Customizer/vendor/composer/autoload_real.php
new file mode 100644
index 00000000..49558bf6
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/autoload_real.php
@@ -0,0 +1,52 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit1a4d3188031158859b352a6b53ee769d
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInit1a4d3188031158859b352a6b53ee769d', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+ spl_autoload_unregister(array('ComposerAutoloaderInit1a4d3188031158859b352a6b53ee769d', 'loadClassLoader'));
+
+ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+ if ($useStaticLoader) {
+ require_once __DIR__ . '/autoload_static.php';
+
+ call_user_func(\Composer\Autoload\ComposerStaticInit1a4d3188031158859b352a6b53ee769d::getInitializer($loader));
+ } else {
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+ }
+
+ $loader->register(true);
+
+ return $loader;
+ }
+}
diff --git a/plugins/Customizer/vendor/composer/autoload_static.php b/plugins/Customizer/vendor/composer/autoload_static.php
new file mode 100644
index 00000000..11ddf899
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/autoload_static.php
@@ -0,0 +1,44 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInit1a4d3188031158859b352a6b53ee769d
+{
+ public static $prefixLengthsPsr4 = array (
+ 'l' =>
+ array (
+ 'luizbills\\CSS_Generator\\' => 24,
+ ),
+ 'M' =>
+ array (
+ 'MatthiasMullie\\PathConverter\\' => 29,
+ 'MatthiasMullie\\Minify\\' => 22,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'luizbills\\CSS_Generator\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/luizbills/css-generator/src',
+ ),
+ 'MatthiasMullie\\PathConverter\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/matthiasmullie/path-converter/src',
+ ),
+ 'MatthiasMullie\\Minify\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/matthiasmullie/minify/src',
+ ),
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit1a4d3188031158859b352a6b53ee769d::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit1a4d3188031158859b352a6b53ee769d::$prefixDirsPsr4;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/plugins/Customizer/vendor/composer/installed.json b/plugins/Customizer/vendor/composer/installed.json
new file mode 100644
index 00000000..9823b6e4
--- /dev/null
+++ b/plugins/Customizer/vendor/composer/installed.json
@@ -0,0 +1,154 @@
+[
+ {
+ "name": "luizbills/css-generator",
+ "version": "v3.1.1",
+ "version_normalized": "3.1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/luizbills/css-generator.php.git",
+ "reference": "5120ebd4e47f041c5e9bbe3395b05f1486264c26"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/luizbills/css-generator.php/zipball/5120ebd4e47f041c5e9bbe3395b05f1486264c26",
+ "reference": "5120ebd4e47f041c5e9bbe3395b05f1486264c26",
+ "shasum": ""
+ },
+ "require": {
+ "matthiasmullie/minify": "^1.3",
+ "php": ">=5.4.0"
+ },
+ "time": "2018-09-21T18:01:30+00:00",
+ "type": "package",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "luizbills\\CSS_Generator\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Luiz Bills",
+ "email": "luizpbills@gmail.com"
+ }
+ ],
+ "description": "Write CSS programatically using PHP."
+ },
+ {
+ "name": "matthiasmullie/minify",
+ "version": "1.3.60",
+ "version_normalized": "1.3.60.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/matthiasmullie/minify.git",
+ "reference": "ab7fea80ce5ce6549baaf272bc8bd926a7e08f90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ab7fea80ce5ce6549baaf272bc8bd926a7e08f90",
+ "reference": "ab7fea80ce5ce6549baaf272bc8bd926a7e08f90",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "matthiasmullie/path-converter": "~1.1",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.0",
+ "matthiasmullie/scrapbook": "~1.0",
+ "phpunit/phpunit": "~4.8"
+ },
+ "suggest": {
+ "psr/cache-implementation": "Cache implementation to use with Minify::cache"
+ },
+ "time": "2018-04-18T08:50:35+00:00",
+ "bin": [
+ "bin/minifycss",
+ "bin/minifyjs"
+ ],
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\Minify\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "email": "minify@mullie.eu",
+ "homepage": "http://www.mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.",
+ "homepage": "http://www.minifier.org",
+ "keywords": [
+ "JS",
+ "css",
+ "javascript",
+ "minifier",
+ "minify"
+ ]
+ },
+ {
+ "name": "matthiasmullie/path-converter",
+ "version": "1.1.2",
+ "version_normalized": "1.1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/matthiasmullie/path-converter.git",
+ "reference": "5e4b121c8b9f97c80835c1d878b0812ba1d607c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/matthiasmullie/path-converter/zipball/5e4b121c8b9f97c80835c1d878b0812ba1d607c9",
+ "reference": "5e4b121c8b9f97c80835c1d878b0812ba1d607c9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8"
+ },
+ "time": "2018-10-25T15:19:41+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\PathConverter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "email": "pathconverter@mullie.eu",
+ "homepage": "http://www.mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "Relative path converter",
+ "homepage": "http://github.com/matthiasmullie/path-converter",
+ "keywords": [
+ "converter",
+ "path",
+ "paths",
+ "relative"
+ ]
+ }
+]
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/.gitignore b/plugins/Customizer/vendor/luizbills/css-generator/.gitignore
new file mode 100644
index 00000000..55940e57
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/.gitignore
@@ -0,0 +1,2 @@
+/vendor/
+composer.lock \ No newline at end of file
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/LICENSE b/plugins/Customizer/vendor/luizbills/css-generator/LICENSE
new file mode 100644
index 00000000..7f9fca91
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Luiz Bills
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/README.md b/plugins/Customizer/vendor/luizbills/css-generator/README.md
new file mode 100644
index 00000000..28dee232
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/README.md
@@ -0,0 +1,102 @@
+# CSS Generator
+
+Write CSS programatically using PHP.
+
+## Install
+
+```php
+composer require luizbills/css-generator
+```
+
+## Usage
+
+```php
+use luizbills\CSS_Generator\Generator as CSS_Generator;
+
+$options = [
+ // default values
+ // 'indentation' => ' ', // 4 spaces
+];
+$css = new CSS_Generator( $options );
+
+// single selector
+$css->add_rule( '.color-white', [ 'color' => '#fff' ] );
+
+$css->open_block( 'media', 'screen and (min-width: 30em)' );
+
+// multiple selectors
+$css->add_rule( [ 'html', 'body' ], [
+ 'background-color' => 'black',
+ 'color' => 'white'
+] );
+
+$css->close_block(); // close a block
+
+$css->open_block( 'supports', '(display: grid)' );
+
+$css->add_rule( '.grid', [
+ 'display' => 'grid',
+] );
+
+// nested block
+$css->open_block( 'media', 'screen and (max-width: 30em)' );
+
+$css->add_rule( '.grid-sm', [
+ 'display' => 'grid',
+] );
+
+$css->close_blocks(); // close all blocks
+
+$minify = false;
+echo $css->get_output( $minify );
+```
+
+output:
+```css
+.color-white {
+ color: #fff;
+}
+@media screen and (min-width: 30em) {
+ html,
+ body {
+ background-color: black;
+ color: white;
+ }
+}
+@supports (display: grid) {
+ .grid {
+ display: grid;
+ }
+ @media screen and (max-width: 30em) {
+ .grid-sm {
+ display: grid;
+ }
+ }
+}
+```
+
+Changing `$minify` to `true` will outputs:
+```css
+.color-white{color:#fff}@media screen and (min-width:30em){body,html{background-color:#000;color:#fff}}@supports (display:grid){.grid{display:grid}@media screen and (max-width:30em){.grid-sm{display:grid}}}
+```
+
+There is also a method `add_raw` that adds any string to your css. Useful to comments or include a framework.
+```php
+$css = new CSS_Generator();
+
+$css->add_rule( '.color-white', [ 'color' => '#fff' ] );
+$css->add_raw('/* my comment */ a { text-decoration: none }');
+
+echo $css->get_output();
+```
+
+output:
+```css
+.color-white{
+ color:#fff;
+}
+/* my comment */ a { text-decoration: none }
+```
+
+## License
+MIT License &copy; 2018 Luiz Bills
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/composer.json b/plugins/Customizer/vendor/luizbills/css-generator/composer.json
new file mode 100644
index 00000000..c6e9417c
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/composer.json
@@ -0,0 +1,21 @@
+{
+ "name": "luizbills/css-generator",
+ "description": "Write CSS programatically using PHP.",
+ "type": "package",
+ "require": {
+ "php": ">=5.4.0",
+ "matthiasmullie/minify": "^1.3"
+ },
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Luiz Bills",
+ "email": "luizpbills@gmail.com"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "luizbills\\CSS_Generator\\": "src"
+ }
+ }
+}
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/demo/demo.php b/plugins/Customizer/vendor/luizbills/css-generator/demo/demo.php
new file mode 100644
index 00000000..69e6a4a1
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/demo/demo.php
@@ -0,0 +1,41 @@
+<?php
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use luizbills\CSS_Generator\Generator as CSS_Generator;
+
+$options = [
+ 'indentation' => ' ', // 2 spaces
+];
+$css = new CSS_Generator( $options );
+
+// single selector
+$css->add_rule( '.color-white', [ 'color' => '#fff' ] );
+
+$css->open_block( 'media', 'screen and (min-width: 30em)' );
+
+// multiple selectors
+$css->add_rule( [ 'html', 'body' ], [
+ 'background-color' => 'black',
+ 'color' => 'white'
+] );
+
+$css->close_block(); // close a block
+
+$css->open_block( 'supports', '(display: grid)' );
+
+$css->add_rule( '.grid', [
+ 'display' => 'grid',
+] );
+
+// nested block
+$css->open_block( 'media', 'screen and (max-width: 30em)' );
+
+$css->add_rule( '.grid-sm', [
+ 'display' => 'grid',
+] );
+
+$css->close_blocks(); // close all blocks
+
+$minify = false;
+echo $css->get_output( $minify ); \ No newline at end of file
diff --git a/plugins/Customizer/vendor/luizbills/css-generator/src/Generator.php b/plugins/Customizer/vendor/luizbills/css-generator/src/Generator.php
new file mode 100644
index 00000000..6ef80f00
--- /dev/null
+++ b/plugins/Customizer/vendor/luizbills/css-generator/src/Generator.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * CSS Generator
+ * Write css programatically using PHP.
+ *
+ * @author Luiz Bills <luizpbills@gmail.comm>
+ * @copyright 2018 Luiz Bills
+ * @license MIT
+*/
+namespace luizbills\CSS_Generator;
+
+use MatthiasMullie\Minify;
+
+class Generator {
+ const VERSION = '3.1.0';
+
+ protected $raw = '';
+ protected $block_level = 0;
+ protected $linebreak = "\n";
+ protected $minified = null; // for cache
+ protected $options = null;
+ protected $default_options = [
+ 'indentation' => ' ', // 4 spaces
+ ];
+
+ public function __construct ( $options = [] ) {
+ $this->options = array_merge( $this->default_options, $options );
+ }
+
+ public function get_output ( $compress = false ) {
+ $this->close_blocks();
+ if ( $compress ) {
+ return $this->minify();
+ }
+ return $this->raw;
+ }
+
+ protected function minify () {
+ if ( ! is_null( $this->minified ) ) {
+ return $this->minified;
+ }
+
+ $minifier = new Minify\CSS( $this->raw );
+ $this->minified = $minifier->minify();
+ return $this->minified;
+ }
+
+ public function add_raw ( $string ) {
+ $this->raw .= $string;
+ $this->clear_cache();
+ }
+
+ public function add_rule ( $selectors, $declarations_array ) {
+ $declarations = [];
+ $selector_indentation = str_repeat( $this->options['indentation'], $this->block_level );
+ $declaration_indentation = str_repeat( $this->options['indentation'], $this->block_level + 1 );
+
+ if ( ! is_array( $selectors ) ) {
+ $selectors = [ $selectors ];
+ }
+
+ foreach ( $selectors as $key => $value ) {
+ $selectors[ $key ] = $selector_indentation . trim( $value );
+ }
+
+ foreach ( $declarations_array as $key => $value ) {
+ $declarations[] = $declaration_indentation . trim( $key ) . ': ' . trim( $value ) . ';' . $this->linebreak;
+ }
+
+ $this->raw .= implode( ',' . $this->linebreak, $selectors ) . ' {';
+ $this->raw .= $this->linebreak . implode( '', $declarations );
+ $this->raw .= $selector_indentation . '}' . $this->linebreak;
+
+ $this->clear_cache();
+ }
+
+ public function open_block ( $type, $props = '' ) {
+ $block_indentation = str_repeat( $this->options['indentation'], $this->block_level );
+ $this->raw .= $block_indentation . '@' . $type . ' ' . trim( $props ) . ' {' . $this->linebreak;
+ $this->block_level++;
+ $this->clear_cache();
+ }
+
+ public function close_block () {
+ if ( $this->block_level > 0 ) {
+ $this->block_level--;
+ $block_indentation = str_repeat( $this->options['indentation'], $this->block_level );
+ $this->raw .= $block_indentation . '}' . $this->linebreak;
+ $this->clear_cache();
+ }
+ }
+
+ public function close_blocks () {
+ while ( $this->block_level > 0 ) {
+ $this->close_block();
+ }
+ }
+
+ protected function clear_cache() {
+ $this->minified = null;
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/CONTRIBUTING.md b/plugins/Customizer/vendor/matthiasmullie/minify/CONTRIBUTING.md
new file mode 100644
index 00000000..226cf976
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/CONTRIBUTING.md
@@ -0,0 +1,59 @@
+# How to contribute
+
+
+## Issues
+
+When [filing bugs](https://github.com/matthiasmullie/minify/issues/new),
+try to be as thorough as possible:
+* What version did you use?
+* What did you try to do? ***Please post the relevant parts of your code.***
+* What went wrong? ***Please include error messages, if any.***
+* What was the expected result?
+
+
+## Pull requests
+
+Bug fixes and general improvements to the existing codebase are always welcome.
+New features are also welcome, but will be judged on an individual basis. If
+you'd rather not risk wasting your time implementing a new feature only to see
+it turned down, please start the discussion by
+[opening an issue](https://github.com/matthiasmullie/minify/issues/new).
+
+Don't forget to add your changes to the [changelog](CHANGELOG.md).
+
+
+### Testing
+
+Please include tests for every change or addition to the code.
+To run the complete test suite:
+
+```sh
+vendor/bin/phpunit
+```
+
+When submitting a new pull request, please make sure that that the test suite
+passes (Travis CI will run it & report back on your pull request.)
+
+To run the tests on Windows, run `tests/convert_symlinks_to_windows_style.sh`
+from the command line in order to convert Linux-style test symlinks to
+Windows-style.
+
+
+### Coding standards
+
+All code must follow [PSR-2](http://www.php-fig.org/psr/psr-2/). Just make sure
+to run php-cs-fixer before submitting the code, it'll take care of the
+formatting for you:
+
+```sh
+vendor/bin/php-cs-fixer fix src
+vendor/bin/php-cs-fixer fix tests
+```
+
+Document the code thoroughly!
+
+
+## License
+
+Note that minify is MIT-licensed, which basically allows anyone to do
+anything they like with it, without restriction.
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/Dockerfile b/plugins/Customizer/vendor/matthiasmullie/minify/Dockerfile
new file mode 100644
index 00000000..d17f9d74
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/Dockerfile
@@ -0,0 +1,13 @@
+ARG version=cli
+FROM php:$version
+
+COPY . /var/www
+WORKDIR /var/www
+
+RUN apt-get update
+RUN apt-get install -y zip unzip zlib1g-dev
+RUN docker-php-ext-install zip
+RUN docker-php-ext-install pcntl
+RUN curl -sS https://getcomposer.org/installer | php
+RUN mv composer.phar /usr/local/bin/composer
+RUN composer install
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/LICENSE b/plugins/Customizer/vendor/matthiasmullie/minify/LICENSE
new file mode 100644
index 00000000..0c0d08a7
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2012 Matthias Mullie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifycss b/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifycss
new file mode 100644
index 00000000..6a681a85
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifycss
@@ -0,0 +1,45 @@
+#!/usr/bin/env php
+<?php
+use MatthiasMullie\Minify;
+
+// command line utility to minify CSS
+if (file_exists(__DIR__ . '/../../../autoload.php')) {
+ // if composer install
+ require_once __DIR__ . '/../../../autoload.php';
+} else {
+ require_once __DIR__ . '/../src/Minify.php';
+ require_once __DIR__ . '/../src/CSS.php';
+ require_once __DIR__ . '/../src/Exception.php';
+}
+
+error_reporting(E_ALL);
+// check PHP setup for cli arguments
+if (!isset($_SERVER['argv']) && !isset($argv)) {
+ fwrite(STDERR, 'Please enable the "register_argc_argv" directive in your php.ini' . PHP_EOL);
+ exit(1);
+} elseif (!isset($argv)) {
+ $argv = $_SERVER['argv'];
+}
+// check if path to file given
+if (!isset($argv[1])) {
+ fwrite(STDERR, 'Argument expected: path to file' . PHP_EOL);
+ exit(1);
+}
+// check if script run in cli environment
+if ('cli' !== php_sapi_name()) {
+ fwrite(STDERR, $argv[1] . ' must be run in the command line' . PHP_EOL);
+ exit(1);
+}
+// check if source file exists
+if (!file_exists($argv[1])) {
+ fwrite(STDERR, 'Source file "' . $argv[1] . '" not found' . PHP_EOL);
+ exit(1);
+}
+
+try {
+ $minifier = new Minify\CSS($argv[1]);
+ echo $minifier->minify();
+} catch (Exception $e) {
+ fwrite(STDERR, $e->getMessage(), PHP_EOL);
+ exit(1);
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifyjs b/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifyjs
new file mode 100644
index 00000000..4cbe63ff
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/bin/minifyjs
@@ -0,0 +1,45 @@
+#!/usr/bin/env php
+<?php
+use MatthiasMullie\Minify;
+
+// command line utility to minify JS
+if (file_exists(__DIR__ . '/../../../autoload.php')) {
+ // if composer install
+ require_once __DIR__ . '/../../../autoload.php';
+} else {
+ require_once __DIR__ . '/../src/Minify.php';
+ require_once __DIR__ . '/../src/JS.php';
+ require_once __DIR__ . '/../src/Exception.php';
+}
+
+error_reporting(E_ALL);
+// check PHP setup for cli arguments
+if (!isset($_SERVER['argv']) && !isset($argv)) {
+ fwrite(STDERR, 'Please enable the "register_argc_argv" directive in your php.ini' . PHP_EOL);
+ exit(1);
+} elseif (!isset($argv)) {
+ $argv = $_SERVER['argv'];
+}
+// check if path to file given
+if (!isset($argv[1])) {
+ fwrite(STDERR, 'Argument expected: path to file' . PHP_EOL);
+ exit(1);
+}
+// check if script run in cli environment
+if ('cli' !== php_sapi_name()) {
+ fwrite(STDERR, $argv[1] . ' must be run in the command line' . PHP_EOL);
+ exit(1);
+}
+// check if source file exists
+if (!file_exists($argv[1])) {
+ fwrite(STDERR, 'Source file "' . $argv[1] . '" not found' . PHP_EOL);
+ exit(1);
+}
+
+try {
+ $minifier = new Minify\JS($argv[1]);
+ echo $minifier->minify();
+} catch (Exception $e) {
+ fwrite(STDERR, $e->getMessage(), PHP_EOL);
+ exit(1);
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/composer.json b/plugins/Customizer/vendor/matthiasmullie/minify/composer.json
new file mode 100644
index 00000000..6d81b4f9
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "matthiasmullie/minify",
+ "type": "library",
+ "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.",
+ "keywords": ["minify", "minifier", "css", "js", "javascript"],
+ "homepage": "http://www.minifier.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "homepage": "http://www.mullie.eu",
+ "email": "minify@mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "ext-pcre": "*",
+ "matthiasmullie/path-converter": "~1.1"
+ },
+ "require-dev": {
+ "matthiasmullie/scrapbook": "~1.0",
+ "phpunit/phpunit": "~4.8",
+ "friendsofphp/php-cs-fixer": "~2.0"
+ },
+ "suggest": {
+ "psr/cache-implementation": "Cache implementation to use with Minify::cache"
+ },
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\Minify\\": "src/"
+ }
+ },
+ "bin": [
+ "bin/minifycss",
+ "bin/minifyjs"
+ ]
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_after.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_after.txt
new file mode 100644
index 00000000..5c8cba7f
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_after.txt
@@ -0,0 +1,7 @@
+in
+public
+extends
+private
+protected
+implements
+instanceof \ No newline at end of file
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_before.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_before.txt
new file mode 100644
index 00000000..5abf3579
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_before.txt
@@ -0,0 +1,26 @@
+do
+in
+let
+new
+var
+case
+else
+enum
+void
+with
+class
+const
+yield
+delete
+export
+import
+public
+static
+typeof
+extends
+package
+private
+function
+protected
+implements
+instanceof \ No newline at end of file
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_reserved.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_reserved.txt
new file mode 100644
index 00000000..2a3ad3c0
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/keywords_reserved.txt
@@ -0,0 +1,63 @@
+do
+if
+in
+for
+let
+new
+try
+var
+case
+else
+enum
+eval
+null
+this
+true
+void
+with
+break
+catch
+class
+const
+false
+super
+throw
+while
+yield
+delete
+export
+import
+public
+return
+static
+switch
+typeof
+default
+extends
+finally
+package
+private
+continue
+debugger
+function
+arguments
+interface
+protected
+implements
+instanceof
+abstract
+boolean
+byte
+char
+double
+final
+float
+goto
+int
+long
+native
+short
+synchronized
+throws
+transient
+volatile \ No newline at end of file
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators.txt
new file mode 100644
index 00000000..e66229ae
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators.txt
@@ -0,0 +1,46 @@
++
+-
+*
+/
+%
+=
++=
+-=
+*=
+/=
+%=
+<<=
+>>=
+>>>=
+&=
+^=
+|=
+&
+|
+^
+~
+<<
+>>
+>>>
+==
+===
+!=
+!==
+>
+<
+>=
+<=
+&&
+||
+!
+.
+[
+]
+?
+:
+,
+;
+(
+)
+{
+} \ No newline at end of file
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_after.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_after.txt
new file mode 100644
index 00000000..71a9b709
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_after.txt
@@ -0,0 +1,43 @@
++
+-
+*
+/
+%
+=
++=
+-=
+*=
+/=
+%=
+<<=
+>>=
+>>>=
+&=
+^=
+|=
+&
+|
+^
+<<
+>>
+>>>
+==
+===
+!=
+!==
+>
+<
+>=
+<=
+&&
+||
+.
+[
+]
+?
+:
+,
+;
+(
+)
+} \ No newline at end of file
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_before.txt b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_before.txt
new file mode 100644
index 00000000..ff50d870
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/data/js/operators_before.txt
@@ -0,0 +1,43 @@
++
+-
+*
+/
+%
+=
++=
+-=
+*=
+/=
+%=
+<<=
+>>=
+>>>=
+&=
+^=
+|=
+&
+|
+^
+~
+<<
+>>
+>>>
+==
+===
+!=
+!==
+>
+<
+>=
+<=
+&&
+||
+!
+.
+[
+?
+:
+,
+;
+(
+{
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/docker-compose.yml b/plugins/Customizer/vendor/matthiasmullie/minify/docker-compose.yml
new file mode 100644
index 00000000..5413e24b
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/docker-compose.yml
@@ -0,0 +1,31 @@
+version: '2.1'
+services:
+ php:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ volumes:
+ - ./src:/var/www/src
+ - ./data:/var/www/data
+ - ./tests:/var/www/tests
+ - ./phpunit.xml.dist:/var/www/phpunit.xml.dist
+ '7.2':
+ extends: php
+ build:
+ args:
+ version: 7.2-cli
+ '7.1':
+ extends: php
+ build:
+ args:
+ version: 7.1-cli
+ '7.0':
+ extends: php
+ build:
+ args:
+ version: 7.0-cli
+ '5.6':
+ extends: php
+ build:
+ args:
+ version: 5.6-cli
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/CSS.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/CSS.php
new file mode 100644
index 00000000..e92bb300
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/CSS.php
@@ -0,0 +1,736 @@
+<?php
+/**
+ * CSS Minifier
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+
+namespace MatthiasMullie\Minify;
+
+use MatthiasMullie\Minify\Exceptions\FileImportException;
+use MatthiasMullie\PathConverter\ConverterInterface;
+use MatthiasMullie\PathConverter\Converter;
+
+/**
+ * CSS minifier
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @package Minify
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @author Tijs Verkoyen <minify@verkoyen.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+class CSS extends Minify
+{
+ /**
+ * @var int maximum inport size in kB
+ */
+ protected $maxImportSize = 5;
+
+ /**
+ * @var string[] valid import extensions
+ */
+ protected $importExtensions = array(
+ 'gif' => 'data:image/gif',
+ 'png' => 'data:image/png',
+ 'jpe' => 'data:image/jpeg',
+ 'jpg' => 'data:image/jpeg',
+ 'jpeg' => 'data:image/jpeg',
+ 'svg' => 'data:image/svg+xml',
+ 'woff' => 'data:application/x-font-woff',
+ 'tif' => 'image/tiff',
+ 'tiff' => 'image/tiff',
+ 'xbm' => 'image/x-xbitmap',
+ );
+
+ /**
+ * Set the maximum size if files to be imported.
+ *
+ * Files larger than this size (in kB) will not be imported into the CSS.
+ * Importing files into the CSS as data-uri will save you some connections,
+ * but we should only import relatively small decorative images so that our
+ * CSS file doesn't get too bulky.
+ *
+ * @param int $size Size in kB
+ */
+ public function setMaxImportSize($size)
+ {
+ $this->maxImportSize = $size;
+ }
+
+ /**
+ * Set the type of extensions to be imported into the CSS (to save network
+ * connections).
+ * Keys of the array should be the file extensions & respective values
+ * should be the data type.
+ *
+ * @param string[] $extensions Array of file extensions
+ */
+ public function setImportExtensions(array $extensions)
+ {
+ $this->importExtensions = $extensions;
+ }
+
+ /**
+ * Move any import statements to the top.
+ *
+ * @param string $content Nearly finished CSS content
+ *
+ * @return string
+ */
+ protected function moveImportsToTop($content)
+ {
+ if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
+ // remove from content
+ foreach ($matches[0] as $import) {
+ $content = str_replace($import, '', $content);
+ }
+
+ // add to top
+ $content = implode(';', $matches[2]).';'.trim($content, ';');
+ }
+
+ return $content;
+ }
+
+ /**
+ * Combine CSS from import statements.
+ *
+ * @import's will be loaded and their content merged into the original file,
+ * to save HTTP requests.
+ *
+ * @param string $source The file to combine imports for
+ * @param string $content The CSS content to combine imports for
+ * @param string[] $parents Parent paths, for circular reference checks
+ *
+ * @return string
+ *
+ * @throws FileImportException
+ */
+ protected function combineImports($source, $content, $parents)
+ {
+ $importRegexes = array(
+ // @import url(xxx)
+ '/
+ # import statement
+ @import
+
+ # whitespace
+ \s+
+
+ # open url()
+ url\(
+
+ # (optional) open path enclosure
+ (?P<quotes>["\']?)
+
+ # fetch path
+ (?P<path>.+?)
+
+ # (optional) close path enclosure
+ (?P=quotes)
+
+ # close url()
+ \)
+
+ # (optional) trailing whitespace
+ \s*
+
+ # (optional) media statement(s)
+ (?P<media>[^;]*)
+
+ # (optional) trailing whitespace
+ \s*
+
+ # (optional) closing semi-colon
+ ;?
+
+ /ix',
+
+ // @import 'xxx'
+ '/
+
+ # import statement
+ @import
+
+ # whitespace
+ \s+
+
+ # open path enclosure
+ (?P<quotes>["\'])
+
+ # fetch path
+ (?P<path>.+?)
+
+ # close path enclosure
+ (?P=quotes)
+
+ # (optional) trailing whitespace
+ \s*
+
+ # (optional) media statement(s)
+ (?P<media>[^;]*)
+
+ # (optional) trailing whitespace
+ \s*
+
+ # (optional) closing semi-colon
+ ;?
+
+ /ix',
+ );
+
+ // find all relative imports in css
+ $matches = array();
+ foreach ($importRegexes as $importRegex) {
+ if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
+ $matches = array_merge($matches, $regexMatches);
+ }
+ }
+
+ $search = array();
+ $replace = array();
+
+ // loop the matches
+ foreach ($matches as $match) {
+ // get the path for the file that will be imported
+ $importPath = dirname($source).'/'.$match['path'];
+
+ // only replace the import with the content if we can grab the
+ // content of the file
+ if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
+ continue;
+ }
+
+ // check if current file was not imported previously in the same
+ // import chain.
+ if (in_array($importPath, $parents)) {
+ throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
+ }
+
+ // grab referenced file & minify it (which may include importing
+ // yet other @import statements recursively)
+ $minifier = new static($importPath);
+ $importContent = $minifier->execute($source, $parents);
+
+ // check if this is only valid for certain media
+ if (!empty($match['media'])) {
+ $importContent = '@media '.$match['media'].'{'.$importContent.'}';
+ }
+
+ // add to replacement array
+ $search[] = $match[0];
+ $replace[] = $importContent;
+ }
+
+ // replace the import statements
+ return str_replace($search, $replace, $content);
+ }
+
+ /**
+ * Import files into the CSS, base64-ized.
+ *
+ * @url(image.jpg) images will be loaded and their content merged into the
+ * original file, to save HTTP requests.
+ *
+ * @param string $source The file to import files for
+ * @param string $content The CSS content to import files for
+ *
+ * @return string
+ */
+ protected function importFiles($source, $content)
+ {
+ $regex = '/url\((["\']?)(.+?)\\1\)/i';
+ if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+ $search = array();
+ $replace = array();
+
+ // loop the matches
+ foreach ($matches as $match) {
+ $extension = substr(strrchr($match[2], '.'), 1);
+ if ($extension && !array_key_exists($extension, $this->importExtensions)) {
+ continue;
+ }
+
+ // get the path for the file that will be imported
+ $path = $match[2];
+ $path = dirname($source).'/'.$path;
+
+ // only replace the import with the content if we're able to get
+ // the content of the file, and it's relatively small
+ if ($this->canImportFile($path) && $this->canImportBySize($path)) {
+ // grab content && base64-ize
+ $importContent = $this->load($path);
+ $importContent = base64_encode($importContent);
+
+ // build replacement
+ $search[] = $match[0];
+ $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
+ }
+ }
+
+ // replace the import statements
+ $content = str_replace($search, $replace, $content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Minify the data.
+ * Perform CSS optimizations.
+ *
+ * @param string[optional] $path Path to write the data to
+ * @param string[] $parents Parent paths, for circular reference checks
+ *
+ * @return string The minified data
+ */
+ public function execute($path = null, $parents = array())
+ {
+ $content = '';
+
+ // loop CSS data (raw data and files)
+ foreach ($this->data as $source => $css) {
+ /*
+ * Let's first take out strings & comments, since we can't just
+ * remove whitespace anywhere. If whitespace occurs inside a string,
+ * we should leave it alone. E.g.:
+ * p { content: "a test" }
+ */
+ $this->extractStrings();
+ $this->stripComments();
+ $css = $this->replace($css);
+
+ $css = $this->stripWhitespace($css);
+ $css = $this->shortenHex($css);
+ $css = $this->shortenZeroes($css);
+ $css = $this->shortenFontWeights($css);
+ $css = $this->stripEmptyTags($css);
+
+ // restore the string we've extracted earlier
+ $css = $this->restoreExtractedData($css);
+
+ $source = is_int($source) ? '' : $source;
+ $parents = $source ? array_merge($parents, array($source)) : $parents;
+ $css = $this->combineImports($source, $css, $parents);
+ $css = $this->importFiles($source, $css);
+
+ /*
+ * If we'll save to a new path, we'll have to fix the relative paths
+ * to be relative no longer to the source file, but to the new path.
+ * If we don't write to a file, fall back to same path so no
+ * conversion happens (because we still want it to go through most
+ * of the move code, which also addresses url() & @import syntax...)
+ */
+ $converter = $this->getPathConverter($source, $path ?: $source);
+ $css = $this->move($converter, $css);
+
+ // combine css
+ $content .= $css;
+ }
+
+ $content = $this->moveImportsToTop($content);
+
+ return $content;
+ }
+
+ /**
+ * Moving a css file should update all relative urls.
+ * Relative references (e.g. ../images/image.gif) in a certain css file,
+ * will have to be updated when a file is being saved at another location
+ * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
+ *
+ * @param ConverterInterface $converter Relative path converter
+ * @param string $content The CSS content to update relative urls for
+ *
+ * @return string
+ */
+ protected function move(ConverterInterface $converter, $content)
+ {
+ /*
+ * Relative path references will usually be enclosed by url(). @import
+ * is an exception, where url() is not necessary around the path (but is
+ * allowed).
+ * This *could* be 1 regular expression, where both regular expressions
+ * in this array are on different sides of a |. But we're using named
+ * patterns in both regexes, the same name on both regexes. This is only
+ * possible with a (?J) modifier, but that only works after a fairly
+ * recent PCRE version. That's why I'm doing 2 separate regular
+ * expressions & combining the matches after executing of both.
+ */
+ $relativeRegexes = array(
+ // url(xxx)
+ '/
+ # open url()
+ url\(
+
+ \s*
+
+ # open path enclosure
+ (?P<quotes>["\'])?
+
+ # fetch path
+ (?P<path>.+?)
+
+ # close path enclosure
+ (?(quotes)(?P=quotes))
+
+ \s*
+
+ # close url()
+ \)
+
+ /ix',
+
+ // @import "xxx"
+ '/
+ # import statement
+ @import
+
+ # whitespace
+ \s+
+
+ # we don\'t have to check for @import url(), because the
+ # condition above will already catch these
+
+ # open path enclosure
+ (?P<quotes>["\'])
+
+ # fetch path
+ (?P<path>.+?)
+
+ # close path enclosure
+ (?P=quotes)
+
+ /ix',
+ );
+
+ // find all relative urls in css
+ $matches = array();
+ foreach ($relativeRegexes as $relativeRegex) {
+ if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
+ $matches = array_merge($matches, $regexMatches);
+ }
+ }
+
+ $search = array();
+ $replace = array();
+
+ // loop all urls
+ foreach ($matches as $match) {
+ // determine if it's a url() or an @import match
+ $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
+
+ $url = $match['path'];
+ if ($this->canImportByPath($url)) {
+ // attempting to interpret GET-params makes no sense, so let's discard them for awhile
+ $params = strrchr($url, '?');
+ $url = $params ? substr($url, 0, -strlen($params)) : $url;
+
+ // fix relative url
+ $url = $converter->convert($url);
+
+ // now that the path has been converted, re-apply GET-params
+ $url .= $params;
+ }
+
+ /*
+ * Urls with control characters above 0x7e should be quoted.
+ * According to Mozilla's parser, whitespace is only allowed at the
+ * end of unquoted urls.
+ * Urls with `)` (as could happen with data: uris) should also be
+ * quoted to avoid being confused for the url() closing parentheses.
+ * And urls with a # have also been reported to cause issues.
+ * Urls with quotes inside should also remain escaped.
+ *
+ * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
+ * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
+ * @see https://github.com/matthiasmullie/minify/issues/193
+ */
+ $url = trim($url);
+ if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
+ $url = $match['quotes'] . $url . $match['quotes'];
+ }
+
+ // build replacement
+ $search[] = $match[0];
+ if ($type === 'url') {
+ $replace[] = 'url('.$url.')';
+ } elseif ($type === 'import') {
+ $replace[] = '@import "'.$url.'"';
+ }
+ }
+
+ // replace urls
+ return str_replace($search, $replace, $content);
+ }
+
+ /**
+ * Shorthand hex color codes.
+ * #FF0000 -> #F00.
+ *
+ * @param string $content The CSS content to shorten the hex color codes for
+ *
+ * @return string
+ */
+ protected function shortenHex($content)
+ {
+ $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?=[; }])/i', '#$1$2$3', $content);
+
+ // we can shorten some even more by replacing them with their color name
+ $colors = array(
+ '#F0FFFF' => 'azure',
+ '#F5F5DC' => 'beige',
+ '#A52A2A' => 'brown',
+ '#FF7F50' => 'coral',
+ '#FFD700' => 'gold',
+ '#808080' => 'gray',
+ '#008000' => 'green',
+ '#4B0082' => 'indigo',
+ '#FFFFF0' => 'ivory',
+ '#F0E68C' => 'khaki',
+ '#FAF0E6' => 'linen',
+ '#800000' => 'maroon',
+ '#000080' => 'navy',
+ '#808000' => 'olive',
+ '#CD853F' => 'peru',
+ '#FFC0CB' => 'pink',
+ '#DDA0DD' => 'plum',
+ '#800080' => 'purple',
+ '#F00' => 'red',
+ '#FA8072' => 'salmon',
+ '#A0522D' => 'sienna',
+ '#C0C0C0' => 'silver',
+ '#FFFAFA' => 'snow',
+ '#D2B48C' => 'tan',
+ '#FF6347' => 'tomato',
+ '#EE82EE' => 'violet',
+ '#F5DEB3' => 'wheat',
+ );
+
+ return preg_replace_callback(
+ '/(?<=[: ])('.implode(array_keys($colors), '|').')(?=[; }])/i',
+ function ($match) use ($colors) {
+ return $colors[strtoupper($match[0])];
+ },
+ $content
+ );
+ }
+
+ /**
+ * Shorten CSS font weights.
+ *
+ * @param string $content The CSS content to shorten the font weights for
+ *
+ * @return string
+ */
+ protected function shortenFontWeights($content)
+ {
+ $weights = array(
+ 'normal' => 400,
+ 'bold' => 700,
+ );
+
+ $callback = function ($match) use ($weights) {
+ return $match[1].$weights[$match[2]];
+ };
+
+ return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
+ }
+
+ /**
+ * Shorthand 0 values to plain 0, instead of e.g. -0em.
+ *
+ * @param string $content The CSS content to shorten the zero values for
+ *
+ * @return string
+ */
+ protected function shortenZeroes($content)
+ {
+ // we don't want to strip units in `calc()` expressions:
+ // `5px - 0px` is valid, but `5px - 0` is not
+ // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
+ // `10 * 0` is invalid
+ // best to just leave `calc()`s alone, even if they could be optimized
+ // (which is a whole other undertaking, where units & order of
+ // operations all need to be considered...)
+ $calcs = $this->findCalcs($content);
+ $content = str_replace($calcs, array_keys($calcs), $content);
+
+ // reusable bits of code throughout these regexes:
+ // before & after are used to make sure we don't match lose unintended
+ // 0-like values (e.g. in #000, or in http://url/1.0)
+ // units can be stripped from 0 values, or used to recognize non 0
+ // values (where wa may be able to strip a .0 suffix)
+ $before = '(?<=[:(, ])';
+ $after = '(?=[ ,);}])';
+ $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
+
+ // strip units after zeroes (0px -> 0)
+ // NOTE: it should be safe to remove all units for a 0 value, but in
+ // practice, Webkit (especially Safari) seems to stumble over at least
+ // 0%, potentially other units as well. Only stripping 'px' for now.
+ // @see https://github.com/matthiasmullie/minify/issues/60
+ $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
+
+ // strip 0-digits (.0 -> 0)
+ $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
+ // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
+ $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
+ // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
+ $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
+ // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
+ $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
+
+ // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
+ $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
+
+ // IE doesn't seem to understand a unitless flex-basis value (correct -
+ // it goes against the spec), so let's add it in again (make it `%`,
+ // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
+ // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
+ $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
+ $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
+
+ // restore `calc()` expressions
+ $content = str_replace(array_keys($calcs), $calcs, $content);
+
+ return $content;
+ }
+
+ /**
+ * Strip empty tags from source code.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ protected function stripEmptyTags($content)
+ {
+ $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
+ $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
+
+ return $content;
+ }
+
+ /**
+ * Strip comments from source code.
+ */
+ protected function stripComments()
+ {
+ $this->registerPattern('/\/\*.*?\*\//s', '');
+ }
+
+ /**
+ * Strip whitespace.
+ *
+ * @param string $content The CSS content to strip the whitespace for
+ *
+ * @return string
+ */
+ protected function stripWhitespace($content)
+ {
+ // remove leading & trailing whitespace
+ $content = preg_replace('/^\s*/m', '', $content);
+ $content = preg_replace('/\s*$/m', '', $content);
+
+ // replace newlines with a single space
+ $content = preg_replace('/\s+/', ' ', $content);
+
+ // remove whitespace around meta characters
+ // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
+ $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
+ $content = preg_replace('/([\[(:])\s+/', '$1', $content);
+ $content = preg_replace('/\s+([\]\)])/', '$1', $content);
+ $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
+
+ // whitespace around + and - can only be stripped inside some pseudo-
+ // classes, like `:nth-child(3+2n)`
+ // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
+ // selectors like `div.weird- p`
+ $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
+ $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
+
+ // remove semicolon/whitespace followed by closing bracket
+ $content = str_replace(';}', '}', $content);
+
+ return trim($content);
+ }
+
+ /**
+ * Find all `calc()` occurrences.
+ *
+ * @param string $content The CSS content to find `calc()`s in.
+ *
+ * @return string[]
+ */
+ protected function findCalcs($content)
+ {
+ $results = array();
+ preg_match_all('/calc(\(.+?)(?=$|;|calc\()/', $content, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $match) {
+ $length = strlen($match[1]);
+ $expr = '';
+ $opened = 0;
+
+ for ($i = 0; $i < $length; $i++) {
+ $char = $match[1][$i];
+ $expr .= $char;
+ if ($char === '(') {
+ $opened++;
+ } elseif ($char === ')' && --$opened === 0) {
+ break;
+ }
+ }
+
+ $results['calc('.count($results).')'] = 'calc'.$expr;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Check if file is small enough to be imported.
+ *
+ * @param string $path The path to the file
+ *
+ * @return bool
+ */
+ protected function canImportBySize($path)
+ {
+ return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
+ }
+
+ /**
+ * Check if file a file can be imported, going by the path.
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ protected function canImportByPath($path)
+ {
+ return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
+ }
+
+ /**
+ * Return a converter to update relative paths to be relative to the new
+ * destination.
+ *
+ * @param string $source
+ * @param string $target
+ *
+ * @return ConverterInterface
+ */
+ protected function getPathConverter($source, $target)
+ {
+ return new Converter($source, $target);
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/Exception.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exception.php
new file mode 100644
index 00000000..d03898f0
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exception.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Base Exception
+ *
+ * @deprecated Use Exceptions\BasicException instead
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+namespace MatthiasMullie\Minify;
+
+/**
+ * Base Exception Class
+ * @deprecated Use Exceptions\BasicException instead
+ *
+ * @package Minify
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+abstract class Exception extends \Exception
+{
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/BasicException.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/BasicException.php
new file mode 100644
index 00000000..af5e81bc
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/BasicException.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * Basic exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+namespace MatthiasMullie\Minify\Exceptions;
+
+use MatthiasMullie\Minify\Exception;
+
+/**
+ * Basic Exception Class
+ *
+ * @package Minify\Exception
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+abstract class BasicException extends Exception
+{
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/FileImportException.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/FileImportException.php
new file mode 100644
index 00000000..912a2c90
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/FileImportException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * File Import Exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+namespace MatthiasMullie\Minify\Exceptions;
+
+/**
+ * File Import Exception Class
+ *
+ * @package Minify\Exception
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+class FileImportException extends BasicException
+{
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/IOException.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/IOException.php
new file mode 100644
index 00000000..b172eb48
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/Exceptions/IOException.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * IO Exception
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+namespace MatthiasMullie\Minify\Exceptions;
+
+/**
+ * IO Exception Class
+ *
+ * @package Minify\Exception
+ * @author Matthias Mullie <minify@mullie.eu>
+ */
+class IOException extends BasicException
+{
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/JS.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/JS.php
new file mode 100644
index 00000000..361afe35
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/JS.php
@@ -0,0 +1,598 @@
+<?php
+/**
+ * JavaScript minifier
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+namespace MatthiasMullie\Minify;
+
+/**
+ * JavaScript Minifier Class
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @package Minify
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @author Tijs Verkoyen <minify@verkoyen.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+class JS extends Minify
+{
+ /**
+ * Var-matching regex based on http://stackoverflow.com/a/9337047/802993.
+ *
+ * Note that regular expressions using that bit must have the PCRE_UTF8
+ * pattern modifier (/u) set.
+ *
+ * @var string
+ */
+ const REGEX_VARIABLE = '\b[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\x{02c1}\x{02c6}-\x{02d1}\x{02e0}-\x{02e4}\x{02ec}\x{02ee}\x{0370}-\x{0374}\x{0376}\x{0377}\x{037a}-\x{037d}\x{0386}\x{0388}-\x{038a}\x{038c}\x{038e}-\x{03a1}\x{03a3}-\x{03f5}\x{03f7}-\x{0481}\x{048a}-\x{0527}\x{0531}-\x{0556}\x{0559}\x{0561}-\x{0587}\x{05d0}-\x{05ea}\x{05f0}-\x{05f2}\x{0620}-\x{064a}\x{066e}\x{066f}\x{0671}-\x{06d3}\x{06d5}\x{06e5}\x{06e6}\x{06ee}\x{06ef}\x{06fa}-\x{06fc}\x{06ff}\x{0710}\x{0712}-\x{072f}\x{074d}-\x{07a5}\x{07b1}\x{07ca}-\x{07ea}\x{07f4}\x{07f5}\x{07fa}\x{0800}-\x{0815}\x{081a}\x{0824}\x{0828}\x{0840}-\x{0858}\x{08a0}\x{08a2}-\x{08ac}\x{0904}-\x{0939}\x{093d}\x{0950}\x{0958}-\x{0961}\x{0971}-\x{0977}\x{0979}-\x{097f}\x{0985}-\x{098c}\x{098f}\x{0990}\x{0993}-\x{09a8}\x{09aa}-\x{09b0}\x{09b2}\x{09b6}-\x{09b9}\x{09bd}\x{09ce}\x{09dc}\x{09dd}\x{09df}-\x{09e1}\x{09f0}\x{09f1}\x{0a05}-\x{0a0a}\x{0a0f}\x{0a10}\x{0a13}-\x{0a28}\x{0a2a}-\x{0a30}\x{0a32}\x{0a33}\x{0a35}\x{0a36}\x{0a38}\x{0a39}\x{0a59}-\x{0a5c}\x{0a5e}\x{0a72}-\x{0a74}\x{0a85}-\x{0a8d}\x{0a8f}-\x{0a91}\x{0a93}-\x{0aa8}\x{0aaa}-\x{0ab0}\x{0ab2}\x{0ab3}\x{0ab5}-\x{0ab9}\x{0abd}\x{0ad0}\x{0ae0}\x{0ae1}\x{0b05}-\x{0b0c}\x{0b0f}\x{0b10}\x{0b13}-\x{0b28}\x{0b2a}-\x{0b30}\x{0b32}\x{0b33}\x{0b35}-\x{0b39}\x{0b3d}\x{0b5c}\x{0b5d}\x{0b5f}-\x{0b61}\x{0b71}\x{0b83}\x{0b85}-\x{0b8a}\x{0b8e}-\x{0b90}\x{0b92}-\x{0b95}\x{0b99}\x{0b9a}\x{0b9c}\x{0b9e}\x{0b9f}\x{0ba3}\x{0ba4}\x{0ba8}-\x{0baa}\x{0bae}-\x{0bb9}\x{0bd0}\x{0c05}-\x{0c0c}\x{0c0e}-\x{0c10}\x{0c12}-\x{0c28}\x{0c2a}-\x{0c33}\x{0c35}-\x{0c39}\x{0c3d}\x{0c58}\x{0c59}\x{0c60}\x{0c61}\x{0c85}-\x{0c8c}\x{0c8e}-\x{0c90}\x{0c92}-\x{0ca8}\x{0caa}-\x{0cb3}\x{0cb5}-\x{0cb9}\x{0cbd}\x{0cde}\x{0ce0}\x{0ce1}\x{0cf1}\x{0cf2}\x{0d05}-\x{0d0c}\x{0d0e}-\x{0d10}\x{0d12}-\x{0d3a}\x{0d3d}\x{0d4e}\x{0d60}\x{0d61}\x{0d7a}-\x{0d7f}\x{0d85}-\x{0d96}\x{0d9a}-\x{0db1}\x{0db3}-\x{0dbb}\x{0dbd}\x{0dc0}-\x{0dc6}\x{0e01}-\x{0e30}\x{0e32}\x{0e33}\x{0e40}-\x{0e46}\x{0e81}\x{0e82}\x{0e84}\x{0e87}\x{0e88}\x{0e8a}\x{0e8d}\x{0e94}-\x{0e97}\x{0e99}-\x{0e9f}\x{0ea1}-\x{0ea3}\x{0ea5}\x{0ea7}\x{0eaa}\x{0eab}\x{0ead}-\x{0eb0}\x{0eb2}\x{0eb3}\x{0ebd}\x{0ec0}-\x{0ec4}\x{0ec6}\x{0edc}-\x{0edf}\x{0f00}\x{0f40}-\x{0f47}\x{0f49}-\x{0f6c}\x{0f88}-\x{0f8c}\x{1000}-\x{102a}\x{103f}\x{1050}-\x{1055}\x{105a}-\x{105d}\x{1061}\x{1065}\x{1066}\x{106e}-\x{1070}\x{1075}-\x{1081}\x{108e}\x{10a0}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{10fa}\x{10fc}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1380}-\x{138f}\x{13a0}-\x{13f4}\x{1401}-\x{166c}\x{166f}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16ea}\x{16ee}-\x{16f0}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17d7}\x{17dc}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191c}\x{1950}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19c1}-\x{19c7}\x{1a00}-\x{1a16}\x{1a20}-\x{1a54}\x{1aa7}\x{1b05}-\x{1b33}\x{1b45}-\x{1b4b}\x{1b83}-\x{1ba0}\x{1bae}\x{1baf}\x{1bba}-\x{1be5}\x{1c00}-\x{1c23}\x{1c4d}-\x{1c4f}\x{1c5a}-\x{1c7d}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf1}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{2160}-\x{2188}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{2e2f}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{31a0}-\x{31ba}\x{31f0}-\x{31ff}\x{3400}-\x{4db5}\x{4e00}-\x{9fcc}\x{a000}-\x{a48c}\x{a4d0}-\x{a4fd}\x{a500}-\x{a60c}\x{a610}-\x{a61f}\x{a62a}\x{a62b}\x{a640}-\x{a66e}\x{a67f}-\x{a697}\x{a6a0}-\x{a6ef}\x{a717}-\x{a71f}\x{a722}-\x{a788}\x{a78b}-\x{a78e}\x{a790}-\x{a793}\x{a7a0}-\x{a7aa}\x{a7f8}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a822}\x{a840}-\x{a873}\x{a882}-\x{a8b3}\x{a8f2}-\x{a8f7}\x{a8fb}\x{a90a}-\x{a925}\x{a930}-\x{a946}\x{a960}-\x{a97c}\x{a984}-\x{a9b2}\x{a9cf}\x{aa00}-\x{aa28}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa60}-\x{aa76}\x{aa7a}\x{aa80}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aadd}\x{aae0}-\x{aaea}\x{aaf2}-\x{aaf4}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{abc0}-\x{abe2}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{f900}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb36}\x{fb38}-\x{fb3c}\x{fb3e}\x{fb40}\x{fb41}\x{fb43}\x{fb44}\x{fb46}-\x{fbb1}\x{fbd3}-\x{fd3d}\x{fd50}-\x{fd8f}\x{fd92}-\x{fdc7}\x{fdf0}-\x{fdfb}\x{fe70}-\x{fe74}\x{fe76}-\x{fefc}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}0-9\x{0300}-\x{036f}\x{0483}-\x{0487}\x{0591}-\x{05bd}\x{05bf}\x{05c1}\x{05c2}\x{05c4}\x{05c5}\x{05c7}\x{0610}-\x{061a}\x{064b}-\x{0669}\x{0670}\x{06d6}-\x{06dc}\x{06df}-\x{06e4}\x{06e7}\x{06e8}\x{06ea}-\x{06ed}\x{06f0}-\x{06f9}\x{0711}\x{0730}-\x{074a}\x{07a6}-\x{07b0}\x{07c0}-\x{07c9}\x{07eb}-\x{07f3}\x{0816}-\x{0819}\x{081b}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082d}\x{0859}-\x{085b}\x{08e4}-\x{08fe}\x{0900}-\x{0903}\x{093a}-\x{093c}\x{093e}-\x{094f}\x{0951}-\x{0957}\x{0962}\x{0963}\x{0966}-\x{096f}\x{0981}-\x{0983}\x{09bc}\x{09be}-\x{09c4}\x{09c7}\x{09c8}\x{09cb}-\x{09cd}\x{09d7}\x{09e2}\x{09e3}\x{09e6}-\x{09ef}\x{0a01}-\x{0a03}\x{0a3c}\x{0a3e}-\x{0a42}\x{0a47}\x{0a48}\x{0a4b}-\x{0a4d}\x{0a51}\x{0a66}-\x{0a71}\x{0a75}\x{0a81}-\x{0a83}\x{0abc}\x{0abe}-\x{0ac5}\x{0ac7}-\x{0ac9}\x{0acb}-\x{0acd}\x{0ae2}\x{0ae3}\x{0ae6}-\x{0aef}\x{0b01}-\x{0b03}\x{0b3c}\x{0b3e}-\x{0b44}\x{0b47}\x{0b48}\x{0b4b}-\x{0b4d}\x{0b56}\x{0b57}\x{0b62}\x{0b63}\x{0b66}-\x{0b6f}\x{0b82}\x{0bbe}-\x{0bc2}\x{0bc6}-\x{0bc8}\x{0bca}-\x{0bcd}\x{0bd7}\x{0be6}-\x{0bef}\x{0c01}-\x{0c03}\x{0c3e}-\x{0c44}\x{0c46}-\x{0c48}\x{0c4a}-\x{0c4d}\x{0c55}\x{0c56}\x{0c62}\x{0c63}\x{0c66}-\x{0c6f}\x{0c82}\x{0c83}\x{0cbc}\x{0cbe}-\x{0cc4}\x{0cc6}-\x{0cc8}\x{0cca}-\x{0ccd}\x{0cd5}\x{0cd6}\x{0ce2}\x{0ce3}\x{0ce6}-\x{0cef}\x{0d02}\x{0d03}\x{0d3e}-\x{0d44}\x{0d46}-\x{0d48}\x{0d4a}-\x{0d4d}\x{0d57}\x{0d62}\x{0d63}\x{0d66}-\x{0d6f}\x{0d82}\x{0d83}\x{0dca}\x{0dcf}-\x{0dd4}\x{0dd6}\x{0dd8}-\x{0ddf}\x{0df2}\x{0df3}\x{0e31}\x{0e34}-\x{0e3a}\x{0e47}-\x{0e4e}\x{0e50}-\x{0e59}\x{0eb1}\x{0eb4}-\x{0eb9}\x{0ebb}\x{0ebc}\x{0ec8}-\x{0ecd}\x{0ed0}-\x{0ed9}\x{0f18}\x{0f19}\x{0f20}-\x{0f29}\x{0f35}\x{0f37}\x{0f39}\x{0f3e}\x{0f3f}\x{0f71}-\x{0f84}\x{0f86}\x{0f87}\x{0f8d}-\x{0f97}\x{0f99}-\x{0fbc}\x{0fc6}\x{102b}-\x{103e}\x{1040}-\x{1049}\x{1056}-\x{1059}\x{105e}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106d}\x{1071}-\x{1074}\x{1082}-\x{108d}\x{108f}-\x{109d}\x{135d}-\x{135f}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}\x{1753}\x{1772}\x{1773}\x{17b4}-\x{17d3}\x{17dd}\x{17e0}-\x{17e9}\x{180b}-\x{180d}\x{1810}-\x{1819}\x{18a9}\x{1920}-\x{192b}\x{1930}-\x{193b}\x{1946}-\x{194f}\x{19b0}-\x{19c0}\x{19c8}\x{19c9}\x{19d0}-\x{19d9}\x{1a17}-\x{1a1b}\x{1a55}-\x{1a5e}\x{1a60}-\x{1a7c}\x{1a7f}-\x{1a89}\x{1a90}-\x{1a99}\x{1b00}-\x{1b04}\x{1b34}-\x{1b44}\x{1b50}-\x{1b59}\x{1b6b}-\x{1b73}\x{1b80}-\x{1b82}\x{1ba1}-\x{1bad}\x{1bb0}-\x{1bb9}\x{1be6}-\x{1bf3}\x{1c24}-\x{1c37}\x{1c40}-\x{1c49}\x{1c50}-\x{1c59}\x{1cd0}-\x{1cd2}\x{1cd4}-\x{1ce8}\x{1ced}\x{1cf2}-\x{1cf4}\x{1dc0}-\x{1de6}\x{1dfc}-\x{1dff}\x{200c}\x{200d}\x{203f}\x{2040}\x{2054}\x{20d0}-\x{20dc}\x{20e1}\x{20e5}-\x{20f0}\x{2cef}-\x{2cf1}\x{2d7f}\x{2de0}-\x{2dff}\x{302a}-\x{302f}\x{3099}\x{309a}\x{a620}-\x{a629}\x{a66f}\x{a674}-\x{a67d}\x{a69f}\x{a6f0}\x{a6f1}\x{a802}\x{a806}\x{a80b}\x{a823}-\x{a827}\x{a880}\x{a881}\x{a8b4}-\x{a8c4}\x{a8d0}-\x{a8d9}\x{a8e0}-\x{a8f1}\x{a900}-\x{a909}\x{a926}-\x{a92d}\x{a947}-\x{a953}\x{a980}-\x{a983}\x{a9b3}-\x{a9c0}\x{a9d0}-\x{a9d9}\x{aa29}-\x{aa36}\x{aa43}\x{aa4c}\x{aa4d}\x{aa50}-\x{aa59}\x{aa7b}\x{aab0}\x{aab2}-\x{aab4}\x{aab7}\x{aab8}\x{aabe}\x{aabf}\x{aac1}\x{aaeb}-\x{aaef}\x{aaf5}\x{aaf6}\x{abe3}-\x{abea}\x{abec}\x{abed}\x{abf0}-\x{abf9}\x{fb1e}\x{fe00}-\x{fe0f}\x{fe20}-\x{fe26}\x{fe33}\x{fe34}\x{fe4d}-\x{fe4f}\x{ff10}-\x{ff19}\x{ff3f}]*\b';
+
+ /**
+ * Full list of JavaScript reserved words.
+ * Will be loaded from /data/js/keywords_reserved.txt.
+ *
+ * @see https://mathiasbynens.be/notes/reserved-keywords
+ *
+ * @var string[]
+ */
+ protected $keywordsReserved = array();
+
+ /**
+ * List of JavaScript reserved words that accept a <variable, value, ...>
+ * after them. Some end of lines are not the end of a statement, like with
+ * these keywords.
+ *
+ * E.g.: we shouldn't insert a ; after this else
+ * else
+ * console.log('this is quite fine')
+ *
+ * Will be loaded from /data/js/keywords_before.txt
+ *
+ * @var string[]
+ */
+ protected $keywordsBefore = array();
+
+ /**
+ * List of JavaScript reserved words that accept a <variable, value, ...>
+ * before them. Some end of lines are not the end of a statement, like when
+ * continued by one of these keywords on the newline.
+ *
+ * E.g.: we shouldn't insert a ; before this instanceof
+ * variable
+ * instanceof String
+ *
+ * Will be loaded from /data/js/keywords_after.txt
+ *
+ * @var string[]
+ */
+ protected $keywordsAfter = array();
+
+ /**
+ * List of all JavaScript operators.
+ *
+ * Will be loaded from /data/js/operators.txt
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
+ *
+ * @var string[]
+ */
+ protected $operators = array();
+
+ /**
+ * List of JavaScript operators that accept a <variable, value, ...> after
+ * them. Some end of lines are not the end of a statement, like with these
+ * operators.
+ *
+ * Note: Most operators are fine, we've only removed ++ and --.
+ * ++ & -- have to be joined with the value they're in-/decrementing.
+ *
+ * Will be loaded from /data/js/operators_before.txt
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
+ *
+ * @var string[]
+ */
+ protected $operatorsBefore = array();
+
+ /**
+ * List of JavaScript operators that accept a <variable, value, ...> before
+ * them. Some end of lines are not the end of a statement, like when
+ * continued by one of these operators on the newline.
+ *
+ * Note: Most operators are fine, we've only removed ), ], ++, --, ! and ~.
+ * There can't be a newline separating ! or ~ and whatever it is negating.
+ * ++ & -- have to be joined with the value they're in-/decrementing.
+ * ) & ] are "special" in that they have lots or usecases. () for example
+ * is used for function calls, for grouping, in if () and for (), ...
+ *
+ * Will be loaded from /data/js/operators_after.txt
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_Operators
+ *
+ * @var string[]
+ */
+ protected $operatorsAfter = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ call_user_func_array(array('parent', '__construct'), func_get_args());
+
+ $dataDir = __DIR__.'/../data/js/';
+ $options = FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES;
+ $this->keywordsReserved = file($dataDir.'keywords_reserved.txt', $options);
+ $this->keywordsBefore = file($dataDir.'keywords_before.txt', $options);
+ $this->keywordsAfter = file($dataDir.'keywords_after.txt', $options);
+ $this->operators = file($dataDir.'operators.txt', $options);
+ $this->operatorsBefore = file($dataDir.'operators_before.txt', $options);
+ $this->operatorsAfter = file($dataDir.'operators_after.txt', $options);
+ }
+
+ /**
+ * Minify the data.
+ * Perform JS optimizations.
+ *
+ * @param string[optional] $path Path to write the data to
+ *
+ * @return string The minified data
+ */
+ public function execute($path = null)
+ {
+ $content = '';
+
+ /*
+ * Let's first take out strings, comments and regular expressions.
+ * All of these can contain JS code-like characters, and we should make
+ * sure any further magic ignores anything inside of these.
+ *
+ * Consider this example, where we should not strip any whitespace:
+ * var str = "a test";
+ *
+ * Comments will be removed altogether, strings and regular expressions
+ * will be replaced by placeholder text, which we'll restore later.
+ */
+ $this->extractStrings('\'"`');
+ $this->stripComments();
+ $this->extractRegex();
+
+ // loop files
+ foreach ($this->data as $source => $js) {
+ // take out strings, comments & regex (for which we've registered
+ // the regexes just a few lines earlier)
+ $js = $this->replace($js);
+
+ $js = $this->propertyNotation($js);
+ $js = $this->shortenBools($js);
+ $js = $this->stripWhitespace($js);
+
+ // combine js: separating the scripts by a ;
+ $content .= $js.";";
+ }
+
+ // clean up leftover `;`s from the combination of multiple scripts
+ $content = ltrim($content, ';');
+ $content = (string) substr($content, 0, -1);
+
+ /*
+ * Earlier, we extracted strings & regular expressions and replaced them
+ * with placeholder text. This will restore them.
+ */
+ $content = $this->restoreExtractedData($content);
+
+ return $content;
+ }
+
+ /**
+ * Strip comments from source code.
+ */
+ protected function stripComments()
+ {
+ // single-line comments
+ $this->registerPattern('/\/\/.*$/m', '');
+
+ // multi-line comments
+ $this->registerPattern('/\/\*.*?\*\//s', '');
+ }
+
+ /**
+ * JS can have /-delimited regular expressions, like: /ab+c/.match(string).
+ *
+ * The content inside the regex can contain characters that may be confused
+ * for JS code: e.g. it could contain whitespace it needs to match & we
+ * don't want to strip whitespace in there.
+ *
+ * The regex can be pretty simple: we don't have to care about comments,
+ * (which also use slashes) because stripComments() will have stripped those
+ * already.
+ *
+ * This method will replace all string content with simple REGEX#
+ * placeholder text, so we've rid all regular expressions from characters
+ * that may be misinterpreted. Original regex content will be saved in
+ * $this->extracted and after doing all other minifying, we can restore the
+ * original content via restoreRegex()
+ */
+ protected function extractRegex()
+ {
+ // PHP only supports $this inside anonymous functions since 5.4
+ $minifier = $this;
+ $callback = function ($match) use ($minifier) {
+ $count = count($minifier->extracted);
+ $placeholder = '"'.$count.'"';
+ $minifier->extracted[$placeholder] = $match[0];
+
+ return $placeholder;
+ };
+
+ // match all chars except `/` and `\`
+ // `\` is allowed though, along with whatever char follows (which is the
+ // one being escaped)
+ // this should allow all chars, except for an unescaped `/` (= the one
+ // closing the regex)
+ // then also ignore bare `/` inside `[]`, where they don't need to be
+ // escaped: anything inside `[]` can be ignored safely
+ $pattern = '\\/(?:[^\\[\\/\\\\\n\r]+|(?:\\\\.)+|(?:\\[(?:[^\\]\\\\\n\r]+|(?:\\\\.)+)+\\])+)++\\/[gimuy]*';
+
+ // a regular expression can only be followed by a few operators or some
+ // of the RegExp methods (a `\` followed by a variable or value is
+ // likely part of a division, not a regex)
+ $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof');
+ $before = '([=:,;\+\-\*\/\}\(\{\[&\|!]|^|'.implode('|', $keywords).')\s*';
+ $propertiesAndMethods = array(
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2
+ 'constructor',
+ 'flags',
+ 'global',
+ 'ignoreCase',
+ 'multiline',
+ 'source',
+ 'sticky',
+ 'unicode',
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Methods_2
+ 'compile(',
+ 'exec(',
+ 'test(',
+ 'toSource(',
+ 'toString(',
+ );
+ $delimiters = array_fill(0, count($propertiesAndMethods), '/');
+ $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters);
+ $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))';
+ $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback);
+
+ // regular expressions following a `)` are rather annoying to detect...
+ // quite often, `/` after `)` is a division operator & if it happens to
+ // be followed by another one (or a comment), it is likely to be
+ // confused for a regular expression
+ // however, it's perfectly possible for a regex to follow a `)`: after
+ // a single-line `if()`, `while()`, ... statement, for example
+ // since, when they occur like that, they're always the start of a
+ // statement, there's only a limited amount of ways they can be useful:
+ // by calling the regex methods directly
+ // if a regex following `)` is not followed by `.<property or method>`,
+ // it's quite likely not a regex
+ $before = '\)\s*';
+ $after = '(?=\s*\.('.implode('|', $propertiesAndMethods).'))';
+ $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback);
+
+ // 1 more edge case: a regex can be followed by a lot more operators or
+ // keywords if there's a newline (ASI) in between, where the operator
+ // actually starts a new statement
+ // (https://github.com/matthiasmullie/minify/issues/56)
+ $operators = $this->getOperatorsForRegex($this->operatorsBefore, '/');
+ $operators += $this->getOperatorsForRegex($this->keywordsReserved, '/');
+ $after = '(?=\s*\n\s*('.implode('|', $operators).'))';
+ $this->registerPattern('/'.$pattern.$after.'/', $callback);
+ }
+
+ /**
+ * Strip whitespace.
+ *
+ * We won't strip *all* whitespace, but as much as possible. The thing that
+ * we'll preserve are newlines we're unsure about.
+ * JavaScript doesn't require statements to be terminated with a semicolon.
+ * It will automatically fix missing semicolons with ASI (automatic semi-
+ * colon insertion) at the end of line causing errors (without semicolon.)
+ *
+ * Because it's sometimes hard to tell if a newline is part of a statement
+ * that should be terminated or not, we'll just leave some of them alone.
+ *
+ * @param string $content The content to strip the whitespace for
+ *
+ * @return string
+ */
+ protected function stripWhitespace($content)
+ {
+ // uniform line endings, make them all line feed
+ $content = str_replace(array("\r\n", "\r"), "\n", $content);
+
+ // collapse all non-line feed whitespace into a single space
+ $content = preg_replace('/[^\S\n]+/', ' ', $content);
+
+ // strip leading & trailing whitespace
+ $content = str_replace(array(" \n", "\n "), "\n", $content);
+
+ // collapse consecutive line feeds into just 1
+ $content = preg_replace('/\n+/', "\n", $content);
+
+ $operatorsBefore = $this->getOperatorsForRegex($this->operatorsBefore, '/');
+ $operatorsAfter = $this->getOperatorsForRegex($this->operatorsAfter, '/');
+ $operators = $this->getOperatorsForRegex($this->operators, '/');
+ $keywordsBefore = $this->getKeywordsForRegex($this->keywordsBefore, '/');
+ $keywordsAfter = $this->getKeywordsForRegex($this->keywordsAfter, '/');
+
+ // strip whitespace that ends in (or next line begin with) an operator
+ // that allows statements to be broken up over multiple lines
+ unset($operatorsBefore['+'], $operatorsBefore['-'], $operatorsAfter['+'], $operatorsAfter['-']);
+ $content = preg_replace(
+ array(
+ '/('.implode('|', $operatorsBefore).')\s+/',
+ '/\s+('.implode('|', $operatorsAfter).')/',
+ ), '\\1', $content
+ );
+
+ // make sure + and - can't be mistaken for, or joined into ++ and --
+ $content = preg_replace(
+ array(
+ '/(?<![\+\-])\s*([\+\-])(?![\+\-])/',
+ '/(?<![\+\-])([\+\-])\s*(?![\+\-])/',
+ ), '\\1', $content
+ );
+
+ // collapse whitespace around reserved words into single space
+ $content = preg_replace('/(^|[;\}\s])\K('.implode('|', $keywordsBefore).')\s+/', '\\2 ', $content);
+ $content = preg_replace('/\s+('.implode('|', $keywordsAfter).')(?=([;\{\s]|$))/', ' \\1', $content);
+
+ /*
+ * We didn't strip whitespace after a couple of operators because they
+ * could be used in different contexts and we can't be sure it's ok to
+ * strip the newlines. However, we can safely strip any non-line feed
+ * whitespace that follows them.
+ */
+ $operatorsDiffBefore = array_diff($operators, $operatorsBefore);
+ $operatorsDiffAfter = array_diff($operators, $operatorsAfter);
+ $content = preg_replace('/('.implode('|', $operatorsDiffBefore).')[^\S\n]+/', '\\1', $content);
+ $content = preg_replace('/[^\S\n]+('.implode('|', $operatorsDiffAfter).')/', '\\1', $content);
+
+ /*
+ * Whitespace after `return` can be omitted in a few occasions
+ * (such as when followed by a string or regex)
+ * Same for whitespace in between `)` and `{`, or between `{` and some
+ * keywords.
+ */
+ $content = preg_replace('/\breturn\s+(["\'\/\+\-])/', 'return$1', $content);
+ $content = preg_replace('/\)\s+\{/', '){', $content);
+ $content = preg_replace('/}\n(else|catch|finally)\b/', '}$1', $content);
+
+ /*
+ * Get rid of double semicolons, except where they can be used like:
+ * "for(v=1,_=b;;)", "for(v=1;;v++)" or "for(;;ja||(ja=true))".
+ * I'll safeguard these double semicolons inside for-loops by
+ * temporarily replacing them with an invalid condition: they won't have
+ * a double semicolon and will be easy to spot to restore afterwards.
+ */
+ $content = preg_replace('/\bfor\(([^;]*);;([^;]*)\)/', 'for(\\1;-;\\2)', $content);
+ $content = preg_replace('/;+/', ';', $content);
+ $content = preg_replace('/\bfor\(([^;]*);-;([^;]*)\)/', 'for(\\1;;\\2)', $content);
+
+ /*
+ * Next, we'll be removing all semicolons where ASI kicks in.
+ * for-loops however, can have an empty body (ending in only a
+ * semicolon), like: `for(i=1;i<3;i++);`, of `for(i in list);`
+ * Here, nothing happens during the loop; it's just used to keep
+ * increasing `i`. With that ; omitted, the next line would be expected
+ * to be the for-loop's body... Same goes for while loops.
+ * I'm going to double that semicolon (if any) so after the next line,
+ * which strips semicolons here & there, we're still left with this one.
+ */
+ $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content);
+ $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);
+ /*
+ * Below will also keep `;` after a `do{}while();` along with `while();`
+ * While these could be stripped after do-while, detecting this
+ * distinction is cumbersome, so I'll play it safe and make sure `;`
+ * after any kind of `while` is kept.
+ */
+ $content = preg_replace('/(while\([^;\{]+\));(\}|$)/s', '\\1;;\\2', $content);
+
+ /*
+ * We also can't strip empty else-statements. Even though they're
+ * useless and probably shouldn't be in the code in the first place, we
+ * shouldn't be stripping the `;` that follows it as it breaks the code.
+ * We can just remove those useless else-statements completely.
+ *
+ * @see https://github.com/matthiasmullie/minify/issues/91
+ */
+ $content = preg_replace('/else;/s', '', $content);
+
+ /*
+ * We also don't really want to terminate statements followed by closing
+ * curly braces (which we've ignored completely up until now) or end-of-
+ * script: ASI will kick in here & we're all about minifying.
+ * Semicolons at beginning of the file don't make any sense either.
+ */
+ $content = preg_replace('/;(\}|$)/s', '\\1', $content);
+ $content = ltrim($content, ';');
+
+ // get rid of remaining whitespace af beginning/end
+ return trim($content);
+ }
+
+ /**
+ * We'll strip whitespace around certain operators with regular expressions.
+ * This will prepare the given array by escaping all characters.
+ *
+ * @param string[] $operators
+ * @param string $delimiter
+ *
+ * @return string[]
+ */
+ protected function getOperatorsForRegex(array $operators, $delimiter = '/')
+ {
+ // escape operators for use in regex
+ $delimiters = array_fill(0, count($operators), $delimiter);
+ $escaped = array_map('preg_quote', $operators, $delimiters);
+
+ $operators = array_combine($operators, $escaped);
+
+ // ignore + & - for now, they'll get special treatment
+ unset($operators['+'], $operators['-']);
+
+ // dot can not just immediately follow a number; it can be confused for
+ // decimal point, or calling a method on it, e.g. 42 .toString()
+ $operators['.'] = '(?<![0-9]\s)\.';
+
+ // don't confuse = with other assignment shortcuts (e.g. +=)
+ $chars = preg_quote('+-*\=<>%&|', $delimiter);
+ $operators['='] = '(?<!['.$chars.'])\=';
+
+ return $operators;
+ }
+
+ /**
+ * We'll strip whitespace around certain keywords with regular expressions.
+ * This will prepare the given array by escaping all characters.
+ *
+ * @param string[] $keywords
+ * @param string $delimiter
+ *
+ * @return string[]
+ */
+ protected function getKeywordsForRegex(array $keywords, $delimiter = '/')
+ {
+ // escape keywords for use in regex
+ $delimiter = array_fill(0, count($keywords), $delimiter);
+ $escaped = array_map('preg_quote', $keywords, $delimiter);
+
+ // add word boundaries
+ array_walk($keywords, function ($value) {
+ return '\b'.$value.'\b';
+ });
+
+ $keywords = array_combine($keywords, $escaped);
+
+ return $keywords;
+ }
+
+ /**
+ * Replaces all occurrences of array['key'] by array.key.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ protected function propertyNotation($content)
+ {
+ // PHP only supports $this inside anonymous functions since 5.4
+ $minifier = $this;
+ $keywords = $this->keywordsReserved;
+ $callback = function ($match) use ($minifier, $keywords) {
+ $property = trim($minifier->extracted[$match[1]], '\'"');
+
+ /*
+ * Check if the property is a reserved keyword. In this context (as
+ * property of an object literal/array) it shouldn't matter, but IE8
+ * freaks out with "Expected identifier".
+ */
+ if (in_array($property, $keywords)) {
+ return $match[0];
+ }
+
+ /*
+ * See if the property is in a variable-like format (e.g.
+ * array['key-here'] can't be replaced by array.key-here since '-'
+ * is not a valid character there.
+ */
+ if (!preg_match('/^'.$minifier::REGEX_VARIABLE.'$/u', $property)) {
+ return $match[0];
+ }
+
+ return '.'.$property;
+ };
+
+ /*
+ * Figure out if previous character is a variable name (of the array
+ * we want to use property notation on) - this is to make sure
+ * standalone ['value'] arrays aren't confused for keys-of-an-array.
+ * We can (and only have to) check the last character, because PHP's
+ * regex implementation doesn't allow unfixed-length look-behind
+ * assertions.
+ */
+ preg_match('/(\[[^\]]+\])[^\]]*$/', static::REGEX_VARIABLE, $previousChar);
+ $previousChar = $previousChar[1];
+
+ /*
+ * Make sure word preceding the ['value'] is not a keyword, e.g.
+ * return['x']. Because -again- PHP's regex implementation doesn't allow
+ * unfixed-length look-behind assertions, I'm just going to do a lot of
+ * separate look-behind assertions, one for each keyword.
+ */
+ $keywords = $this->getKeywordsForRegex($keywords);
+ $keywords = '(?<!'.implode(')(?<!', $keywords).')';
+
+ return preg_replace_callback('/(?<='.$previousChar.'|\])'.$keywords.'\[\s*(([\'"])[0-9]+\\2)\s*\]/u', $callback, $content);
+ }
+
+ /**
+ * Replaces true & false by !0 and !1.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ protected function shortenBools($content)
+ {
+ /*
+ * 'true' or 'false' could be used as property names (which may be
+ * followed by whitespace) - we must not replace those!
+ * Since PHP doesn't allow variable-length (to account for the
+ * whitespace) lookbehind assertions, I need to capture the leading
+ * character and check if it's a `.`
+ */
+ $callback = function ($match) {
+ if (trim($match[1]) === '.') {
+ return $match[0];
+ }
+
+ return $match[1].($match[2] === 'true' ? '!0' : '!1');
+ };
+ $content = preg_replace_callback('/(^|.\s*)\b(true|false)\b(?!:)/', $callback, $content);
+
+ // for(;;) is exactly the same as while(true), but shorter :)
+ $content = preg_replace('/\bwhile\(!0\){/', 'for(;;){', $content);
+
+ // now make sure we didn't turn any do ... while(true) into do ... for(;;)
+ preg_match_all('/\bdo\b/', $content, $dos, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
+
+ // go backward to make sure positional offsets aren't altered when $content changes
+ $dos = array_reverse($dos);
+ foreach ($dos as $do) {
+ $offsetDo = $do[0][1];
+
+ // find all `while` (now `for`) following `do`: one of those must be
+ // associated with the `do` and be turned back into `while`
+ preg_match_all('/\bfor\(;;\)/', $content, $whiles, PREG_OFFSET_CAPTURE | PREG_SET_ORDER, $offsetDo);
+ foreach ($whiles as $while) {
+ $offsetWhile = $while[0][1];
+
+ $open = substr_count($content, '{', $offsetDo, $offsetWhile - $offsetDo);
+ $close = substr_count($content, '}', $offsetDo, $offsetWhile - $offsetDo);
+ if ($open === $close) {
+ // only restore `while` if amount of `{` and `}` are the same;
+ // otherwise, that `for` isn't associated with this `do`
+ $content = substr_replace($content, 'while(!0)', $offsetWhile, strlen('for(;;)'));
+ break;
+ }
+ }
+ }
+
+ return $content;
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/minify/src/Minify.php b/plugins/Customizer/vendor/matthiasmullie/minify/src/Minify.php
new file mode 100644
index 00000000..bfbc1ad2
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/minify/src/Minify.php
@@ -0,0 +1,459 @@
+<?php
+/**
+ * Abstract minifier class
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+namespace MatthiasMullie\Minify;
+
+use MatthiasMullie\Minify\Exceptions\IOException;
+use Psr\Cache\CacheItemInterface;
+
+/**
+ * Abstract minifier class.
+ *
+ * Please report bugs on https://github.com/matthiasmullie/minify/issues
+ *
+ * @package Minify
+ * @author Matthias Mullie <minify@mullie.eu>
+ * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+abstract class Minify
+{
+ /**
+ * The data to be minified.
+ *
+ * @var string[]
+ */
+ protected $data = array();
+
+ /**
+ * Array of patterns to match.
+ *
+ * @var string[]
+ */
+ protected $patterns = array();
+
+ /**
+ * This array will hold content of strings and regular expressions that have
+ * been extracted from the JS source code, so we can reliably match "code",
+ * without having to worry about potential "code-like" characters inside.
+ *
+ * @var string[]
+ */
+ public $extracted = array();
+
+ /**
+ * Init the minify class - optionally, code may be passed along already.
+ */
+ public function __construct(/* $data = null, ... */)
+ {
+ // it's possible to add the source through the constructor as well ;)
+ if (func_num_args()) {
+ call_user_func_array(array($this, 'add'), func_get_args());
+ }
+ }
+
+ /**
+ * Add a file or straight-up code to be minified.
+ *
+ * @param string|string[] $data
+ *
+ * @return static
+ */
+ public function add($data /* $data = null, ... */)
+ {
+ // bogus "usage" of parameter $data: scrutinizer warns this variable is
+ // not used (we're using func_get_args instead to support overloading),
+ // but it still needs to be defined because it makes no sense to have
+ // this function without argument :)
+ $args = array($data) + func_get_args();
+
+ // this method can be overloaded
+ foreach ($args as $data) {
+ if (is_array($data)) {
+ call_user_func_array(array($this, 'add'), $data);
+ continue;
+ }
+
+ // redefine var
+ $data = (string) $data;
+
+ // load data
+ $value = $this->load($data);
+ $key = ($data != $value) ? $data : count($this->data);
+
+ // replace CR linefeeds etc.
+ // @see https://github.com/matthiasmullie/minify/pull/139
+ $value = str_replace(array("\r\n", "\r"), "\n", $value);
+
+ // store data
+ $this->data[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Minify the data & (optionally) saves it to a file.
+ *
+ * @param string[optional] $path Path to write the data to
+ *
+ * @return string The minified data
+ */
+ public function minify($path = null)
+ {
+ $content = $this->execute($path);
+
+ // save to path
+ if ($path !== null) {
+ $this->save($content, $path);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Minify & gzip the data & (optionally) saves it to a file.
+ *
+ * @param string[optional] $path Path to write the data to
+ * @param int[optional] $level Compression level, from 0 to 9
+ *
+ * @return string The minified & gzipped data
+ */
+ public function gzip($path = null, $level = 9)
+ {
+ $content = $this->execute($path);
+ $content = gzencode($content, $level, FORCE_GZIP);
+
+ // save to path
+ if ($path !== null) {
+ $this->save($content, $path);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Minify the data & write it to a CacheItemInterface object.
+ *
+ * @param CacheItemInterface $item Cache item to write the data to
+ *
+ * @return CacheItemInterface Cache item with the minifier data
+ */
+ public function cache(CacheItemInterface $item)
+ {
+ $content = $this->execute();
+ $item->set($content);
+
+ return $item;
+ }
+
+ /**
+ * Minify the data.
+ *
+ * @param string[optional] $path Path to write the data to
+ *
+ * @return string The minified data
+ */
+ abstract public function execute($path = null);
+
+ /**
+ * Load data.
+ *
+ * @param string $data Either a path to a file or the content itself
+ *
+ * @return string
+ */
+ protected function load($data)
+ {
+ // check if the data is a file
+ if ($this->canImportFile($data)) {
+ $data = file_get_contents($data);
+
+ // strip BOM, if any
+ if (substr($data, 0, 3) == "\xef\xbb\xbf") {
+ $data = substr($data, 3);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Save to file.
+ *
+ * @param string $content The minified data
+ * @param string $path The path to save the minified data to
+ *
+ * @throws IOException
+ */
+ protected function save($content, $path)
+ {
+ $handler = $this->openFileForWriting($path);
+
+ $this->writeToFile($handler, $content);
+
+ @fclose($handler);
+ }
+
+ /**
+ * Register a pattern to execute against the source content.
+ *
+ * @param string $pattern PCRE pattern
+ * @param string|callable $replacement Replacement value for matched pattern
+ */
+ protected function registerPattern($pattern, $replacement = '')
+ {
+ // study the pattern, we'll execute it more than once
+ $pattern .= 'S';
+
+ $this->patterns[] = array($pattern, $replacement);
+ }
+
+ /**
+ * We can't "just" run some regular expressions against JavaScript: it's a
+ * complex language. E.g. having an occurrence of // xyz would be a comment,
+ * unless it's used within a string. Of you could have something that looks
+ * like a 'string', but inside a comment.
+ * The only way to accurately replace these pieces is to traverse the JS one
+ * character at a time and try to find whatever starts first.
+ *
+ * @param string $content The content to replace patterns in
+ *
+ * @return string The (manipulated) content
+ */
+ protected function replace($content)
+ {
+ $processed = '';
+ $positions = array_fill(0, count($this->patterns), -1);
+ $matches = array();
+
+ while ($content) {
+ // find first match for all patterns
+ foreach ($this->patterns as $i => $pattern) {
+ list($pattern, $replacement) = $pattern;
+
+ // we can safely ignore patterns for positions we've unset earlier,
+ // because we know these won't show up anymore
+ if (!isset($positions[$i])) {
+ continue;
+ }
+
+ // no need to re-run matches that are still in the part of the
+ // content that hasn't been processed
+ if ($positions[$i] >= 0) {
+ continue;
+ }
+
+ $match = null;
+ if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) {
+ $matches[$i] = $match;
+
+ // we'll store the match position as well; that way, we
+ // don't have to redo all preg_matches after changing only
+ // the first (we'll still know where those others are)
+ $positions[$i] = $match[0][1];
+ } else {
+ // if the pattern couldn't be matched, there's no point in
+ // executing it again in later runs on this same content;
+ // ignore this one until we reach end of content
+ unset($matches[$i], $positions[$i]);
+ }
+ }
+
+ // no more matches to find: everything's been processed, break out
+ if (!$matches) {
+ $processed .= $content;
+ break;
+ }
+
+ // see which of the patterns actually found the first thing (we'll
+ // only want to execute that one, since we're unsure if what the
+ // other found was not inside what the first found)
+ $discardLength = min($positions);
+ $firstPattern = array_search($discardLength, $positions);
+ $match = $matches[$firstPattern][0][0];
+
+ // execute the pattern that matches earliest in the content string
+ list($pattern, $replacement) = $this->patterns[$firstPattern];
+ $replacement = $this->replacePattern($pattern, $replacement, $content);
+
+ // figure out which part of the string was unmatched; that's the
+ // part we'll execute the patterns on again next
+ $content = (string) substr($content, $discardLength);
+ $unmatched = (string) substr($content, strpos($content, $match) + strlen($match));
+
+ // move the replaced part to $processed and prepare $content to
+ // again match batch of patterns against
+ $processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched));
+ $content = $unmatched;
+
+ // first match has been replaced & that content is to be left alone,
+ // the next matches will start after this replacement, so we should
+ // fix their offsets
+ foreach ($positions as $i => $position) {
+ $positions[$i] -= $discardLength + strlen($match);
+ }
+ }
+
+ return $processed;
+ }
+
+ /**
+ * This is where a pattern is matched against $content and the matches
+ * are replaced by their respective value.
+ * This function will be called plenty of times, where $content will always
+ * move up 1 character.
+ *
+ * @param string $pattern Pattern to match
+ * @param string|callable $replacement Replacement value
+ * @param string $content Content to match pattern against
+ *
+ * @return string
+ */
+ protected function replacePattern($pattern, $replacement, $content)
+ {
+ if (is_callable($replacement)) {
+ return preg_replace_callback($pattern, $replacement, $content, 1, $count);
+ } else {
+ return preg_replace($pattern, $replacement, $content, 1, $count);
+ }
+ }
+
+ /**
+ * Strings are a pattern we need to match, in order to ignore potential
+ * code-like content inside them, but we just want all of the string
+ * content to remain untouched.
+ *
+ * This method will replace all string content with simple STRING#
+ * placeholder text, so we've rid all strings from characters that may be
+ * misinterpreted. Original string content will be saved in $this->extracted
+ * and after doing all other minifying, we can restore the original content
+ * via restoreStrings().
+ *
+ * @param string[optional] $chars
+ * @param string[optional] $placeholderPrefix
+ */
+ protected function extractStrings($chars = '\'"', $placeholderPrefix = '')
+ {
+ // PHP only supports $this inside anonymous functions since 5.4
+ $minifier = $this;
+ $callback = function ($match) use ($minifier, $placeholderPrefix) {
+ // check the second index here, because the first always contains a quote
+ if ($match[2] === '') {
+ /*
+ * Empty strings need no placeholder; they can't be confused for
+ * anything else anyway.
+ * But we still needed to match them, for the extraction routine
+ * to skip over this particular string.
+ */
+ return $match[0];
+ }
+
+ $count = count($minifier->extracted);
+ $placeholder = $match[1].$placeholderPrefix.$count.$match[1];
+ $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1];
+
+ return $placeholder;
+ };
+
+ /*
+ * The \\ messiness explained:
+ * * Don't count ' or " as end-of-string if it's escaped (has backslash
+ * in front of it)
+ * * Unless... that backslash itself is escaped (another leading slash),
+ * in which case it's no longer escaping the ' or "
+ * * So there can be either no backslash, or an even number
+ * * multiply all of that times 4, to account for the escaping that has
+ * to be done to pass the backslash into the PHP string without it being
+ * considered as escape-char (times 2) and to get it in the regex,
+ * escaped (times 2)
+ */
+ $this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
+ }
+
+ /**
+ * This method will restore all extracted data (strings, regexes) that were
+ * replaced with placeholder text in extract*(). The original content was
+ * saved in $this->extracted.
+ *
+ * @param string $content
+ *
+ * @return string
+ */
+ protected function restoreExtractedData($content)
+ {
+ if (!$this->extracted) {
+ // nothing was extracted, nothing to restore
+ return $content;
+ }
+
+ $content = strtr($content, $this->extracted);
+
+ $this->extracted = array();
+
+ return $content;
+ }
+
+ /**
+ * Check if the path is a regular file and can be read.
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ protected function canImportFile($path)
+ {
+ $parsed = parse_url($path);
+ if (
+ // file is elsewhere
+ isset($parsed['host']) ||
+ // file responds to queries (may change, or need to bypass cache)
+ isset($parsed['query'])
+ ) {
+ return false;
+ }
+
+ return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path);
+ }
+
+ /**
+ * Attempts to open file specified by $path for writing.
+ *
+ * @param string $path The path to the file
+ *
+ * @return resource Specifier for the target file
+ *
+ * @throws IOException
+ */
+ protected function openFileForWriting($path)
+ {
+ if (($handler = @fopen($path, 'w')) === false) {
+ throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.');
+ }
+
+ return $handler;
+ }
+
+ /**
+ * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions.
+ *
+ * @param resource $handler The resource to write to
+ * @param string $content The content to write
+ * @param string $path The path to the file (for exception printing only)
+ *
+ * @throws IOException
+ */
+ protected function writeToFile($handler, $content, $path = '')
+ {
+ if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) {
+ throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.');
+ }
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/path-converter/LICENSE b/plugins/Customizer/vendor/matthiasmullie/path-converter/LICENSE
new file mode 100644
index 00000000..491295ad
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/path-converter/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2015 Matthias Mullie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/plugins/Customizer/vendor/matthiasmullie/path-converter/composer.json b/plugins/Customizer/vendor/matthiasmullie/path-converter/composer.json
new file mode 100644
index 00000000..1cb6a4c5
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/path-converter/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "matthiasmullie/path-converter",
+ "type": "library",
+ "description": "Relative path converter",
+ "keywords": ["relative", "path", "converter", "paths"],
+ "homepage": "http://github.com/matthiasmullie/path-converter",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Matthias Mullie",
+ "homepage": "http://www.mullie.eu",
+ "email": "pathconverter@mullie.eu",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "ext-pcre": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8"
+ },
+ "autoload": {
+ "psr-4": {
+ "MatthiasMullie\\PathConverter\\": "src/"
+ }
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/path-converter/src/Converter.php b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/Converter.php
new file mode 100644
index 00000000..519d3c84
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/Converter.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace MatthiasMullie\PathConverter;
+
+/**
+ * Convert paths relative from 1 file to another.
+ *
+ * E.g.
+ * ../../images/icon.jpg relative to /css/imports/icons.css
+ * becomes
+ * ../images/icon.jpg relative to /css/minified.css
+ *
+ * Please report bugs on https://github.com/matthiasmullie/path-converter/issues
+ *
+ * @author Matthias Mullie <pathconverter@mullie.eu>
+ * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+class Converter implements ConverterInterface
+{
+ /**
+ * @var string
+ */
+ protected $from;
+
+ /**
+ * @var string
+ */
+ protected $to;
+
+ /**
+ * @param string $from The original base path (directory, not file!)
+ * @param string $to The new base path (directory, not file!)
+ * @param string $root Root directory (defaults to `getcwd`)
+ */
+ public function __construct($from, $to, $root = '')
+ {
+ $shared = $this->shared($from, $to);
+ if ($shared === '') {
+ // when both paths have nothing in common, one of them is probably
+ // absolute while the other is relative
+ $root = $root ?: getcwd();
+ $from = strpos($from, $root) === 0 ? $from : preg_replace('/\/+/', '/', $root.'/'.$from);
+ $to = strpos($to, $root) === 0 ? $to : preg_replace('/\/+/', '/', $root.'/'.$to);
+
+ // or traveling the tree via `..`
+ // attempt to resolve path, or assume it's fine if it doesn't exist
+ $from = @realpath($from) ?: $from;
+ $to = @realpath($to) ?: $to;
+ }
+
+ $from = $this->dirname($from);
+ $to = $this->dirname($to);
+
+ $from = $this->normalize($from);
+ $to = $this->normalize($to);
+
+ $this->from = $from;
+ $this->to = $to;
+ }
+
+ /**
+ * Normalize path.
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ protected function normalize($path)
+ {
+ // deal with different operating systems' directory structure
+ $path = rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $path), '/');
+
+ /*
+ * Example:
+ * /home/forkcms/frontend/cache/compiled_templates/../../core/layout/css/../images/img.gif
+ * to
+ * /home/forkcms/frontend/core/layout/images/img.gif
+ */
+ do {
+ $path = preg_replace('/[^\/]+(?<!\.\.)\/\.\.\//', '', $path, -1, $count);
+ } while ($count);
+
+ return $path;
+ }
+
+ /**
+ * Figure out the shared path of 2 locations.
+ *
+ * Example:
+ * /home/forkcms/frontend/core/layout/images/img.gif
+ * and
+ * /home/forkcms/frontend/cache/minified_css
+ * share
+ * /home/forkcms/frontend
+ *
+ * @param string $path1
+ * @param string $path2
+ *
+ * @return string
+ */
+ protected function shared($path1, $path2)
+ {
+ // $path could theoretically be empty (e.g. no path is given), in which
+ // case it shouldn't expand to array(''), which would compare to one's
+ // root /
+ $path1 = $path1 ? explode('/', $path1) : array();
+ $path2 = $path2 ? explode('/', $path2) : array();
+
+ $shared = array();
+
+ // compare paths & strip identical ancestors
+ foreach ($path1 as $i => $chunk) {
+ if (isset($path2[$i]) && $path1[$i] == $path2[$i]) {
+ $shared[] = $chunk;
+ } else {
+ break;
+ }
+ }
+
+ return implode('/', $shared);
+ }
+
+ /**
+ * Convert paths relative from 1 file to another.
+ *
+ * E.g.
+ * ../images/img.gif relative to /home/forkcms/frontend/core/layout/css
+ * should become:
+ * ../../core/layout/images/img.gif relative to
+ * /home/forkcms/frontend/cache/minified_css
+ *
+ * @param string $path The relative path that needs to be converted
+ *
+ * @return string The new relative path
+ */
+ public function convert($path)
+ {
+ // quit early if conversion makes no sense
+ if ($this->from === $this->to) {
+ return $path;
+ }
+
+ $path = $this->normalize($path);
+ // if we're not dealing with a relative path, just return absolute
+ if (strpos($path, '/') === 0) {
+ return $path;
+ }
+
+ // normalize paths
+ $path = $this->normalize($this->from.'/'.$path);
+
+ // strip shared ancestor paths
+ $shared = $this->shared($path, $this->to);
+ $path = mb_substr($path, mb_strlen($shared));
+ $to = mb_substr($this->to, mb_strlen($shared));
+
+ // add .. for every directory that needs to be traversed to new path
+ $to = str_repeat('../', count(array_filter(explode('/', $to))));
+
+ return $to.ltrim($path, '/');
+ }
+
+ /**
+ * Attempt to get the directory name from a path.
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ protected function dirname($path)
+ {
+ if (@is_file($path)) {
+ return dirname($path);
+ }
+
+ if (@is_dir($path)) {
+ return rtrim($path, '/');
+ }
+
+ // no known file/dir, start making assumptions
+
+ // ends in / = dir
+ if (mb_substr($path, -1) === '/') {
+ return rtrim($path, '/');
+ }
+
+ // has a dot in the name, likely a file
+ if (preg_match('/.*\..*$/', basename($path)) !== 0) {
+ return dirname($path);
+ }
+
+ // you're on your own here!
+ return $path;
+ }
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/path-converter/src/ConverterInterface.php b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/ConverterInterface.php
new file mode 100644
index 00000000..dc1b7657
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/ConverterInterface.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace MatthiasMullie\PathConverter;
+
+/**
+ * Convert file paths.
+ *
+ * Please report bugs on https://github.com/matthiasmullie/path-converter/issues
+ *
+ * @author Matthias Mullie <pathconverter@mullie.eu>
+ * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+interface ConverterInterface
+{
+ /**
+ * Convert file paths.
+ *
+ * @param string $path The path to be converted
+ *
+ * @return string The new path
+ */
+ public function convert($path);
+}
diff --git a/plugins/Customizer/vendor/matthiasmullie/path-converter/src/NoConverter.php b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/NoConverter.php
new file mode 100644
index 00000000..2fcfd0f2
--- /dev/null
+++ b/plugins/Customizer/vendor/matthiasmullie/path-converter/src/NoConverter.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace MatthiasMullie\PathConverter;
+
+/**
+ * Don't convert paths.
+ *
+ * Please report bugs on https://github.com/matthiasmullie/path-converter/issues
+ *
+ * @author Matthias Mullie <pathconverter@mullie.eu>
+ * @copyright Copyright (c) 2015, Matthias Mullie. All rights reserved
+ * @license MIT License
+ */
+class NoConverter implements ConverterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function convert($path)
+ {
+ return $path;
+ }
+}
diff --git a/plugins/Group_assign/.travis.yml b/plugins/Group_assign/.travis.yml
new file mode 100644
index 00000000..6e29a9fc
--- /dev/null
+++ b/plugins/Group_assign/.travis.yml
@@ -0,0 +1,30 @@
+language: php
+sudo: false
+
+php:
+ - 7.2
+
+env:
+ global:
+ - PLUGIN=Group_assign
+ - KANBOARD_REPO=https://github.com/kanboard/kanboard.git
+ matrix:
+ - DB=sqlite
+ - DB=mysql
+ - DB=postgres
+
+matrix:
+ fast_finish: true
+
+install:
+ - git clone --depth 1 $KANBOARD_REPO
+ - ln -s $TRAVIS_BUILD_DIR kanboard/plugins/$PLUGIN
+
+before_script:
+ - cd kanboard
+ - phpenv config-add tests/php.ini
+ - composer install
+ - ls -la plugins/
+
+script:
+ - phpunit -c tests/units.$DB.xml plugins/$PLUGIN/Test/
diff --git a/plugins/Group_assign/Action/AssignGroup.php b/plugins/Group_assign/Action/AssignGroup.php
new file mode 100644
index 00000000..4a63696b
--- /dev/null
+++ b/plugins/Group_assign/Action/AssignGroup.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Action\Base;
+
+class AssignGroup extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Assign the task to a specific group');
+ }
+
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_CREATE_UPDATE,
+ TaskModel::EVENT_MOVE_COLUMN,
+ );
+ }
+
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'group_id' => t('Group'),
+ );
+ }
+
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ ),
+ );
+ }
+
+ /**
+ * Execute the action (assign the given user)
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool True if the action was executed or false when not executed
+ */
+ public function doAction(array $data)
+ {
+ $values = array(
+ 'id' => $data['task_id'],
+ 'owner_gp' => $this->getParam('group_id'),
+ );
+
+ return $this->taskModificationModel->update($values);
+ }
+
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailGroup.php b/plugins/Group_assign/Action/EmailGroup.php
new file mode 100644
index 00000000..6d12bc73
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailGroup.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+class EmailGroup extends Base
+{
+
+ public function getDescription()
+ {
+ return t('Send a task by email to assigned group members');
+ }
+
+
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_MOVE_COLUMN,
+ TaskModel::EVENT_CLOSE,
+ TaskModel::EVENT_CREATE,
+ );
+ }
+
+
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'subject' => t('Email subject'),
+ );
+ }
+
+
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'owner_gp',
+ ),
+ );
+ }
+
+
+ public function doAction(array $data)
+ {
+ $groupmembers = $this->groupMemberModel->getMembers($data['task']['owner_gp']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+ if (! empty($user['email'])) {
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array(
+ 'task' => $data['task'],
+ ))
+ );
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailGroupDue.php b/plugins/Group_assign/Action/EmailGroupDue.php
new file mode 100644
index 00000000..8f1a66a7
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailGroupDue.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+/**
+ * Email a task notification of impending due date
+ */
+class EmailGroupDue extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email notification of impending due date to Group Members Assigned');
+ }
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_DAILY_CRONJOB,
+ );
+ }
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+
+ }
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $groupmembers = $this->groupMemberModel->getMembers($task['owner_gp']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+
+ $duration = $task['date_due'] - time();
+ if ($task['date_due'] > 0) {
+ if ($duration < $max) {
+ if (! empty($user['email'])) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinderModel->getDetails($task_id);
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task))
+ );
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailOtherAssignees.php b/plugins/Group_assign/Action/EmailOtherAssignees.php
new file mode 100644
index 00000000..f9ab091c
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailOtherAssignees.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+class EmailOtherAssignees extends Base
+{
+
+ public function getDescription()
+ {
+ return t('Send a task by email to the other assignees for a task.');
+ }
+
+
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_MOVE_COLUMN,
+ TaskModel::EVENT_CLOSE,
+ TaskModel::EVENT_CREATE,
+ );
+ }
+
+
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'column_id' => t('Column'),
+ 'subject' => t('Email subject'),
+ );
+ }
+
+
+ public function getEventRequiredParameters()
+ {
+ return array(
+ 'task_id',
+ 'task' => array(
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'owner_ms',
+ ),
+ );
+ }
+
+
+ public function doAction(array $data)
+ {
+ $multimembers = $this->multiselectMemberModel->getMembers($data['task']['owner_ms']);
+
+ if (! empty($multimembers)) {
+ foreach ($multimembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+ if (! empty($user['email'])) {
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array(
+ 'task' => $data['task'],
+ ))
+ );
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ public function hasRequiredCondition(array $data)
+ {
+ return $data['task']['column_id'] == $this->getParam('column_id');
+ }
+}
diff --git a/plugins/Group_assign/Action/EmailOtherAssigneesDue.php b/plugins/Group_assign/Action/EmailOtherAssigneesDue.php
new file mode 100644
index 00000000..abd15003
--- /dev/null
+++ b/plugins/Group_assign/Action/EmailOtherAssigneesDue.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Action;
+
+use Kanboard\Model\TaskModel;
+use Kanboard\Action\Base;
+
+/**
+ * Email a task notification of impending due date
+ */
+class EmailOtherAssigneesDue extends Base
+{
+ /**
+ * Get automatic action description
+ *
+ * @access public
+ * @return string
+ */
+ public function getDescription()
+ {
+ return t('Send email notification of impending due date to the other assignees of a task');
+ }
+ /**
+ * Get the list of compatible events
+ *
+ * @access public
+ * @return array
+ */
+ public function getCompatibleEvents()
+ {
+ return array(
+ TaskModel::EVENT_DAILY_CRONJOB,
+ );
+ }
+ /**
+ * Get the required parameter for the action (defined by the user)
+ *
+ * @access public
+ * @return array
+ */
+ public function getActionRequiredParameters()
+ {
+ return array(
+ 'subject' => t('Email subject'),
+ 'duration' => t('Duration in days'),
+ );
+ }
+ /**
+ * Get the required parameter for the event
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getEventRequiredParameters()
+ {
+ return array('tasks');
+
+ }
+ /**
+ * Check if the event data meet the action condition
+ *
+ * @access public
+ * @param array $data Event data dictionary
+ * @return bool
+ */
+ public function hasRequiredCondition(array $data)
+ {
+ return count($data['tasks']) > 0;
+ }
+
+ public function doAction(array $data)
+ {
+ $results = array();
+ $max = $this->getParam('duration') * 86400;
+
+ foreach ($data['tasks'] as $task) {
+ $groupmembers = $this->multiselectMemberModel->getMembers($task['owner_ms']);
+
+ if (! empty($groupmembers)) {
+ foreach ($groupmembers as $members) {
+ $user = $this->userModel->getById($members['id']);
+
+ $duration = $task['date_due'] - time();
+ if ($task['date_due'] > 0) {
+ if ($duration < $max) {
+ if (! empty($user['email'])) {
+ $results[] = $this->sendEmail($task['id'], $user);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return in_array(true, $results, true);
+ }
+ /**
+ * Send email
+ *
+ * @access private
+ * @param integer $task_id
+ * @param array $user
+ * @return boolean
+ */
+ private function sendEmail($task_id, array $user)
+ {
+ $task = $this->taskFinderModel->getDetails($task_id);
+ $this->emailClient->send(
+ $user['email'],
+ $user['name'] ?: $user['username'],
+ $this->getParam('subject'),
+ $this->template->render('notification/task_create', array('task' => $task))
+ );
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Assets/css/group_assign.css b/plugins/Group_assign/Assets/css/group_assign.css
new file mode 100644
index 00000000..e5693f83
--- /dev/null
+++ b/plugins/Group_assign/Assets/css/group_assign.css
@@ -0,0 +1,17 @@
+.avatar-13 .avatar-letter {
+ border-radius: 6.5px;
+ line-height: 13px;
+ width: 13px;
+ font-size: 7px;
+}
+.avatar-13 img {
+ border-radius: 6.5px;
+}
+
+.assigned-group {
+ display: inline-block;
+ margin: 3px 3px 0 0;
+ padding: 1px 3px 1px 3px;
+ color: #333;
+ border-radius: 4px;
+}
diff --git a/plugins/Group_assign/Assets/js/group_assign.js b/plugins/Group_assign/Assets/js/group_assign.js
new file mode 100644
index 00000000..daf47e89
--- /dev/null
+++ b/plugins/Group_assign/Assets/js/group_assign.js
@@ -0,0 +1,5 @@
+
+ KB.on('modal.afterRender', function () {
+ $(".group-assign-select").select2({
+ });
+ }); \ No newline at end of file
diff --git a/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php b/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php
new file mode 100644
index 00000000..f7dadbee
--- /dev/null
+++ b/plugins/Group_assign/Controller/GroupAssignTaskCreationController.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Controller;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Controller\BaseController;
+use Kanboard\Core\Controller\PageNotFoundException;
+
+
+class GroupAssignTaskCreationController extends BaseController
+{
+ /**
+ * Display a form to create a new task
+ *
+ * @access public
+ * @param array $values
+ * @param array $errors
+ * @throws PageNotFoundException
+ */
+ public function show(array $values = array(), array $errors = array())
+ {
+ $project = $this->getProject();
+ $swimlanesList = $this->swimlaneModel->getList($project['id'], false, true);
+ $values += $this->prepareValues($project['is_private'], $swimlanesList);
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-creation:form:default', $values, array('default_values' => $values));
+
+ $this->response->html($this->template->render('task_creation/show', array(
+ 'project' => $project,
+ 'errors' => $errors,
+ 'values' => $values + array('project_id' => $project['id']),
+ 'columns_list' => $this->columnModel->getList($project['id']),
+ 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($project['id'], true, false, $project['is_private'] == 1),
+ 'categories_list' => $this->categoryModel->getList($project['id']),
+ 'swimlanes_list' => $swimlanesList,
+ )));
+ }
+
+ /**
+ * Validate and save a new task
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+ $values['project_id'] = $project['id'];
+ if (isset($values['owner_ms']) && !empty($values['owner_ms'])) {
+ $ms_id = $this->multiselectModel->create();
+ foreach ($values['owner_ms'] as $user) {
+ $this->multiselectMemberModel->addUser($ms_id, $user);
+ }
+ unset($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+ }
+
+ list($valid, $errors) = $this->taskValidator->validateCreation($values);
+
+ if (! $valid) {
+ $this->flash->failure(t('Unable to create your task.'));
+ $this->show($values, $errors);
+ } else if (! $this->helper->projectRole->canCreateTaskInColumn($project['id'], $values['column_id'])) {
+ $this->flash->failure(t('You cannot create tasks in this column.'));
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ } else {
+ $task_id = $this->taskCreationModel->create($values);
+
+ if ($task_id > 0) {
+ $this->flash->success(t('Task created successfully.'));
+ $this->afterSave($project, $values, $task_id);
+ } else {
+ $this->flash->failure(t('Unable to create this task.'));
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+ }
+ }
+
+ /**
+ * Duplicate created tasks to multiple projects
+ *
+ * @throws PageNotFoundException
+ */
+ public function duplicateProjects()
+ {
+ $project = $this->getProject();
+ $values = $this->request->getValues();
+
+ if (isset($values['project_ids'])) {
+ foreach ($values['project_ids'] as $project_id) {
+ $this->taskProjectDuplicationModel->duplicateToProject($values['task_id'], $project_id);
+ }
+ }
+
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+
+ /**
+ * Executed after the task is saved
+ *
+ * @param array $project
+ * @param array $values
+ * @param integer $task_id
+ */
+ protected function afterSave(array $project, array &$values, $task_id)
+ {
+ if (isset($values['duplicate_multiple_projects']) && $values['duplicate_multiple_projects'] == 1) {
+ $this->chooseProjects($project, $task_id);
+ } elseif (isset($values['another_task']) && $values['another_task'] == 1) {
+ $this->show(array(
+ 'owner_id' => $values['owner_id'],
+ 'color_id' => $values['color_id'],
+ 'category_id' => isset($values['category_id']) ? $values['category_id'] : 0,
+ 'column_id' => $values['column_id'],
+ 'swimlane_id' => isset($values['swimlane_id']) ? $values['swimlane_id'] : 0,
+ 'another_task' => 1,
+ ));
+ } else {
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', array('project_id' => $project['id'])), true);
+ }
+ }
+
+ /**
+ * Prepare form values
+ *
+ * @access protected
+ * @param bool $isPrivateProject
+ * @param array $swimlanesList
+ * @return array
+ */
+ protected function prepareValues($isPrivateProject, array $swimlanesList)
+ {
+ $values = array(
+ 'swimlane_id' => $this->request->getIntegerParam('swimlane_id', key($swimlanesList)),
+ 'column_id' => $this->request->getIntegerParam('column_id'),
+ 'color_id' => $this->colorModel->getDefaultColor(),
+ );
+
+ if ($isPrivateProject) {
+ $values['owner_id'] = $this->userSession->getId();
+ }
+
+ return $values;
+ }
+
+ /**
+ * Choose projects
+ *
+ * @param array $project
+ * @param integer $task_id
+ */
+ protected function chooseProjects(array $project, $task_id)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $projects = $this->projectUserRoleModel->getActiveProjectsByUser($this->userSession->getId());
+ unset($projects[$project['id']]);
+
+ $this->response->html($this->template->render('task_creation/duplicate_projects', array(
+ 'project' => $project,
+ 'task' => $task,
+ 'projects_list' => $projects,
+ 'values' => array('task_id' => $task['id'])
+ )));
+ }
+}
diff --git a/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php b/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php
new file mode 100644
index 00000000..b975c3d9
--- /dev/null
+++ b/plugins/Group_assign/Controller/GroupAssignTaskModificationController.php
@@ -0,0 +1,208 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Controller;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Controller\BaseController;
+use Kanboard\Core\Controller\PageNotFoundException;
+
+
+/**
+ * Group Assign Task Modification controller
+ *
+ * @package Kanboard\Plugin\Group_assign\
+ * @author Craig Crosby
+ */
+class GroupAssignTaskModificationController extends BaseController
+{
+ public function assignToMe()
+ {
+ $task = $this->getTask();
+ $values = ['id' => $task['id'], 'owner_id' => $this->userSession->getId()];
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $this->taskModificationModel->update($values);
+ $this->redirectAfterQuickAction($task);
+ }
+
+ /**
+ * Set the start date automatically
+ *
+ * @access public
+ */
+ public function start()
+ {
+ $task = $this->getTask();
+ $values = ['id' => $task['id'], 'date_started' => time()];
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $this->taskModificationModel->update($values);
+ $this->redirectAfterQuickAction($task);
+ }
+
+ protected function redirectAfterQuickAction(array $task)
+ {
+ switch ($this->request->getStringParam('redirect')) {
+ case 'board':
+ $this->response->redirect($this->helper->url->to('BoardViewController', 'show', ['project_id' => $task['project_id']]));
+ break;
+ case 'list':
+ $this->response->redirect($this->helper->url->to('TaskListController', 'show', ['project_id' => $task['project_id']]));
+ break;
+ case 'dashboard':
+ $this->response->redirect($this->helper->url->to('DashboardController', 'show', [], 'project-tasks-'.$task['project_id']));
+ break;
+ case 'dashboard-tasks':
+ $this->response->redirect($this->helper->url->to('DashboardController', 'tasks', ['user_id' => $this->userSession->getId()]));
+ break;
+ default:
+ $this->response->redirect($this->helper->url->to('TaskViewController', 'show', ['project_id' => $task['project_id'], 'task_id' => $task['id']]));
+ }
+ }
+
+ /**
+ * Display a form to edit a task
+ *
+ * @access public
+ * @param array $values
+ * @param array $errors
+ * @throws \Kanboard\Core\Controller\AccessForbiddenException
+ * @throws \Kanboard\Core\Controller\PageNotFoundException
+ */
+ public function edit(array $values = array(), array $errors = array())
+ {
+ $task = $this->getTask();
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $project = $this->projectModel->getById($task['project_id']);
+
+ if (empty($values)) {
+ $values = $task;
+ }
+
+ $values = $this->hook->merge('controller:task:form:default', $values, array('default_values' => $values));
+ $values = $this->hook->merge('controller:task-modification:form:default', $values, array('default_values' => $values));
+
+ $params = array(
+ 'project' => $project,
+ 'values' => $values,
+ 'errors' => $errors,
+ 'task' => $task,
+ 'tags' => $this->taskTagModel->getList($task['id']),
+ 'users_list' => $this->projectUserRoleModel->getAssignableUsersList($task['project_id']),
+ 'categories_list' => $this->categoryModel->getList($task['project_id']),
+ );
+
+ $this->renderTemplate($task, $params);
+ }
+
+ protected function renderTemplate(array &$task, array &$params)
+ {
+ if (empty($task['external_uri'])) {
+ $this->response->html($this->template->render('task_modification/show', $params));
+ } else {
+
+ try {
+ $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']);
+ $params['template'] = $taskProvider->getModificationFormTemplate();
+ $params['external_task'] = $taskProvider->fetch($task['external_uri']);
+ } catch (ExternalTaskAccessForbiddenException $e) {
+ throw new AccessForbiddenException($e->getMessage());
+ } catch (ExternalTaskException $e) {
+ $params['error_message'] = $e->getMessage();
+ }
+
+ $this->response->html($this->template->render('external_task_modification/show', $params));
+ }
+ }
+
+ /**
+ * Validate and update a task
+ *
+ * @access public
+ */
+ public function update()
+ {
+ $previousMembers = array();
+ $task = $this->getTask();
+ $values = $this->request->getValues();
+ $values['id'] = $task['id'];
+ $values['project_id'] = $task['project_id'];
+ if (isset($values['owner_ms']) && !empty($values['owner_ms'])) {
+ if (!empty($task['owner_ms'])) {
+ $ms_id = $task['owner_ms'];
+ $previousMembers = $this->multiselectMemberModel->getMembers($ms_id);
+ $this->multiselectMemberModel->removeAllUsers($ms_id);
+ } else {
+ $ms_id = $this->multiselectModel->create();
+ }
+ foreach ($values['owner_ms'] as $user) {
+ if ($user !== 0) { $this->multiselectMemberModel->addUser($ms_id, $user); }
+ }
+ unset($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+
+ $newMembersSet = $this->multiselectMemberModel->getMembers($values['owner_ms']);
+ if (sort($previousMembers) !== sort($newMembersSet)) { $this->multiselectMemberModel->assigneeChanged($task, $values); }
+
+ if ($values['owner_gp'] !== $task['owner_gp']) { $this->multiselectMemberModel->assigneeChanged($task, $values); }
+ } else {
+ $this->multiselectMemberModel->removeAllUsers($task['owner_ms']);
+ }
+
+ list($valid, $errors) = $this->taskValidator->validateModification($values);
+
+ if ($valid && $this->updateTask($task, $values, $errors)) {
+ $this->flash->success(t('Task updated successfully.'));
+ $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('project_id' => $task['project_id'], 'task_id' => $task['id'])), true);
+ } else {
+ $this->flash->failure(t('Unable to update your task.'));
+ $this->edit($values, $errors);
+ }
+ }
+
+ protected function updateTask(array &$task, array &$values, array &$errors)
+ {
+ if (isset($values['owner_id']) && $values['owner_id'] != $task['owner_id'] && !$this->helper->projectRole->canChangeAssignee($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to change the assignee.'));
+ }
+
+ if (! $this->helper->projectRole->canUpdateTask($task)) {
+ throw new AccessForbiddenException(t('You are not allowed to update tasks assigned to someone else.'));
+ }
+
+ $result = $this->taskModificationModel->update($values);
+
+ if ($result && ! empty($task['external_uri'])) {
+ try {
+ $taskProvider = $this->externalTaskManager->getProvider($task['external_provider']);
+ $result = $taskProvider->save($task['external_uri'], $values, $errors);
+ } catch (ExternalTaskAccessForbiddenException $e) {
+ throw new AccessForbiddenException($e->getMessage());
+ } catch (ExternalTaskException $e) {
+ $this->logger->error($e->getMessage());
+ $result = false;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php b/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php
new file mode 100644
index 00000000..e7ffc06f
--- /dev/null
+++ b/plugins/Group_assign/Filter/TaskAllAssigneeFilter.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Filter;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Filter\BaseFilter;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use PicoDb\Database;
+
+
+class TaskAllAssigneeFilter extends BaseFilter implements FilterInterface
+{
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return TaskAssigneeFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Current user id
+ *
+ * @access private
+ * @var int
+ */
+ private $currentUserId = 0;
+
+ /**
+ * Set current user id
+ *
+ * @access public
+ * @param integer $userId
+ * @return TaskAssigneeFilter
+ */
+ public function setCurrentUserId($userId)
+ {
+ $this->currentUserId = $userId;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('allassignees');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ if (is_int($this->value) || ctype_digit($this->value)) {
+ $this->query->beginOr();
+ $this->query->eq(TaskModel::TABLE.'.owner_id', $this->value);
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->value')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->value')");
+ $this->query->closeOr();
+ } else {
+ switch ($this->value) {
+ case 'me':
+ $this->query->beginOr();
+ $this->query->eq(TaskModel::TABLE.'.owner_id', $this->currentUserId);
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$this->currentUserId')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$this->currentUserId')");
+ $this->query->closeOr();
+ break;
+ case 'nobody':
+ $this->query->eq(TaskModel::TABLE.'.owner_id', 0);
+ break;
+ default:
+ $useridsarray = $this->getSubQuery()->findAllByColumn('id');
+ $useridstring = implode("','", $useridsarray);
+ $this->query->beginOr();
+ $this->query->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%');
+ $this->query->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%');
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT id FROM ".GroupModel::TABLE." WHERE ".GroupModel::TABLE.".name='$this->value')");
+ $this->query->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id IN ('$useridstring'))");
+ $this->query->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id IN ('$useridstring'))");
+ $this->query->closeOr();
+ }
+ }
+ }
+ public function getSubQuery()
+ {
+ return $this->db->table(UserModel::TABLE)
+ ->columns(
+ UserModel::TABLE.'.id',
+ UserModel::TABLE.'.username',
+ UserModel::TABLE.'.name'
+ )
+ ->beginOr()
+ ->ilike(UserModel::TABLE.'.username', '%'.$this->value.'%')
+ ->ilike(UserModel::TABLE.'.name', '%'.$this->value.'%')
+ ->closeOr();
+ }
+
+}
diff --git a/plugins/Group_assign/Helper/NewTaskHelper.php b/plugins/Group_assign/Helper/NewTaskHelper.php
new file mode 100644
index 00000000..b15262a1
--- /dev/null
+++ b/plugins/Group_assign/Helper/NewTaskHelper.php
@@ -0,0 +1,389 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Helper;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Core\Base;
+
+
+class NewTaskHelper extends Base
+{
+ /**
+ * Local cache for project columns
+ *
+ * @access private
+ * @var array
+ */
+ private $columns = array();
+
+ public function getColors()
+ {
+ return $this->colorModel->getList();
+ }
+
+ public function recurrenceTriggers()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceTriggerList();
+ }
+
+ public function recurrenceTimeframes()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceTimeframeList();
+ }
+
+ public function recurrenceBasedates()
+ {
+ return $this->taskRecurrenceModel->getRecurrenceBasedateList();
+ }
+
+ public function renderTitleField(array $values, array $errors)
+ {
+ return $this->helper->form->text(
+ 'title',
+ $values,
+ $errors,
+ array(
+ 'autofocus',
+ 'required',
+ 'maxlength="200"',
+ 'tabindex="1"',
+ 'placeholder="'.t('Title').'"'
+ )
+ );
+ }
+
+ public function renderDescriptionField(array $values, array $errors)
+ {
+ return $this->helper->form->textEditor('description', $values, $errors, array('tabindex' => 2));
+ }
+
+ public function renderDescriptionTemplateDropdown($projectId)
+ {
+ $templates = $this->predefinedTaskDescriptionModel->getAll($projectId);
+
+ if (! empty($templates)) {
+ $html = '<div class="dropdown dropdown-smaller">';
+ $html .= '<a href="#" class="dropdown-menu dropdown-menu-link-icon"><i class="fa fa-floppy-o fa-fw" aria-hidden="true"></i>'.t('Template for the task description').' <i class="fa fa-caret-down" aria-hidden="true"></i></a>';
+ $html .= '<ul>';
+
+ foreach ($templates as $template) {
+ $html .= '<li>';
+ $html .= '<a href="#" data-template-target="textarea[name=description]" data-template="'.$this->helper->text->e($template['description']).'" class="js-template">';
+ $html .= $this->helper->text->e($template['title']);
+ $html .= '</a>';
+ $html .= '</li>';
+ }
+
+ $html .= '</ul></div>';
+ return $html;
+ }
+
+ return '';
+ }
+
+ public function renderTagField(array $project, array $tags = array())
+ {
+ $options = $this->tagModel->getAssignableList($project['id']);
+
+ $html = $this->helper->form->label(t('Tags'), 'tags[]');
+ $html .= '<input type="hidden" name="tags[]" value="">';
+ $html .= '<select name="tags[]" id="form-tags" class="tag-autocomplete" multiple>';
+
+ foreach ($options as $tag) {
+ $html .= sprintf(
+ '<option value="%s" %s>%s</option>',
+ $this->helper->text->e($tag),
+ in_array($tag, $tags) ? 'selected="selected"' : '',
+ $this->helper->text->e($tag)
+ );
+ }
+
+ $html .= '</select>';
+
+ return $html;
+ }
+
+ public function renderColorField(array $values)
+ {
+ $colors = $this->colorModel->getList();
+ $html = $this->helper->form->label(t('Color'), 'color_id');
+ $html .= $this->helper->form->select('color_id', $colors, $values, array(), array(), 'color-picker');
+ return $html;
+ }
+
+ public function renderAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+
+ $attributes = array_merge(array('tabindex="3"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assignee'), 'owner_id');
+ $html .= $this->helper->form->select('owner_id', $users, $values, $errors, $attributes);
+ $html .= '&nbsp;';
+ $html .= '<small>';
+ $html .= '<a href="#" class="assign-me" data-target-id="form-owner_id" data-current-id="'.$this->userSession->getId().'" title="'.t('Assign to me').'">'.t('Me').'</a>';
+ $html .= '</small>';
+
+ return $html;
+ }
+
+ public function renderMultiAssigneeField(array $users, array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+ $name = 'owner_ms';
+
+ $html = $this->helper->form->label(t('Other Assignees'), $name.'[]');
+
+ $html .= '<select class="group-assign-select" multiple="multiple" size="3" name="'.$name.'[]" id="form-'.$name.'" '.implode(' ', $attributes).'>';
+
+ foreach ($users as $id => $value) {
+ if($id !== 0){
+ $html .= '<option value="'.$this->helper->text->e($id).'"';
+ if (isset($values->$name)) {
+ $multiusers = $this->multiselectMemberModel->getMembers($values->$name);
+ foreach ($multiusers as $member) {
+ if ($member['user_id'] == $id){ $html .= ' selected="selected"'; break; }
+ }
+ }
+ if (isset($values[$name])) {
+ $multiusers = $this->multiselectMemberModel->getMembers($values[$name]);
+ foreach ($multiusers as $member) {
+ if ($member['user_id'] == $id){ $html .= ' selected="selected"'; break; }
+ }
+ }
+
+ $html .= '>'.$this->helper->text->e($value).'</option>';
+ }
+ }
+ $html .= '</select>';
+
+ return $html;
+ }
+
+ public function renderGroupField(array $values, array $errors = array(), array $attributes = array())
+ {
+ if (isset($values['project_id']) && ! $this->helper->projectRole->canChangeAssignee($values)) {
+ return '';
+ }
+ $groups = $this->projectGroupRoleModel->getGroups($values['project_id']);
+ $groupnames = array();
+ $groupids = array();
+
+ $groupids[] = 0;
+ $groupnames[] = t('Unassigned');
+
+
+ foreach ($groups as $group) {
+ // array_splice($groupnames, 1, 0, $group['name']);
+ $groupnames[] = $group['name'];
+ $groupids[] = $group['id'];
+ }
+
+ $groupvalues = array_combine($groupids, $groupnames);
+
+
+ $attributes = array_merge(array('tabindex="4"'), $attributes);
+
+ $html = $this->helper->form->label(t('Assigned Group'), 'owner_gp');
+ $html .= $this->helper->form->select('owner_gp', $groupvalues, $values, $errors, $attributes);
+ $html .= '&nbsp;';
+
+ return $html;
+ }
+
+ public function renderCategoryField(array $categories, array $values, array $errors = array(), array $attributes = array(), $allow_one_item = false)
+ {
+ $attributes = array_merge(array('tabindex="5"'), $attributes);
+ $html = '';
+
+ if (! (! $allow_one_item && count($categories) === 1 && key($categories) == 0)) {
+ $html .= $this->helper->form->label(t('Category'), 'category_id');
+ $html .= $this->helper->form->select('category_id', $categories, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function renderSwimlaneField(array $swimlanes, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="6"'), $attributes);
+ $html = '';
+
+ if (count($swimlanes) > 1) {
+ $html .= $this->helper->form->label(t('Swimlane'), 'swimlane_id');
+ $html .= $this->helper->form->select('swimlane_id', $swimlanes, $values, $errors, $attributes);
+ }
+
+ return $html;
+ }
+
+ public function renderColumnField(array $columns, array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="7"'), $attributes);
+
+ $html = $this->helper->form->label(t('Column'), 'column_id');
+ $html .= $this->helper->form->select('column_id', $columns, $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function renderPriorityField(array $project, array $values)
+ {
+ $range = range($project['priority_start'], $project['priority_end']);
+ $options = array_combine($range, $range);
+ $values += array('priority' => $project['priority_default']);
+
+ $html = $this->helper->form->label(t('Priority'), 'priority');
+ $html .= $this->helper->form->select('priority', $options, $values, array(), array('tabindex="8"'));
+
+ return $html;
+ }
+
+ public function renderScoreField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="13"'), $attributes);
+
+ $html = $this->helper->form->label(t('Complexity'), 'score');
+ $html .= $this->helper->form->number('score', $values, $errors, $attributes);
+
+ return $html;
+ }
+
+ public function renderReferenceField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="14"'), $attributes);
+
+ $html = $this->helper->form->label(t('Reference'), 'reference');
+ $html .= $this->helper->form->text('reference', $values, $errors, $attributes, 'form-input-small');
+
+ return $html;
+ }
+
+ public function renderTimeEstimatedField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="11"'), $attributes);
+
+ $html = $this->helper->form->label(t('Original estimate'), 'time_estimated');
+ $html .= $this->helper->form->numeric('time_estimated', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function renderTimeSpentField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="12"'), $attributes);
+
+ $html = $this->helper->form->label(t('Time spent'), 'time_spent');
+ $html .= $this->helper->form->numeric('time_spent', $values, $errors, $attributes);
+ $html .= ' '.t('hours');
+
+ return $html;
+ }
+
+ public function renderStartDateField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="10"'), $attributes);
+ return $this->helper->form->datetime(t('Start Date'), 'date_started', $values, $errors, $attributes);
+ }
+
+ public function renderDueDateField(array $values, array $errors = array(), array $attributes = array())
+ {
+ $attributes = array_merge(array('tabindex="9"'), $attributes);
+ return $this->helper->form->datetime(t('Due Date'), 'date_due', $values, $errors, $attributes);
+ }
+
+ public function renderPriority($priority)
+ {
+ $html = '<span class="task-priority" title="'.t('Task priority').'">';
+ $html .= $this->helper->text->e($priority >= 0 ? 'P'.$priority : '-P'.abs($priority));
+ $html .= '</span>';
+
+ return $html;
+ }
+
+ public function renderReference(array $task)
+ {
+ if (! empty($task['reference'])) {
+ $reference = $this->helper->text->e($task['reference']);
+
+ if (filter_var($task['reference'], FILTER_VALIDATE_URL) !== false) {
+ return sprintf('<a href="%s" target=_blank">%s</a>', $reference, $reference);
+ }
+
+ return $reference;
+ }
+
+ return '';
+ }
+
+ public function getProgress($task)
+ {
+ if (! isset($this->columns[$task['project_id']])) {
+ $this->columns[$task['project_id']] = $this->columnModel->getList($task['project_id']);
+ }
+
+ return $this->taskModel->getProgress($task, $this->columns[$task['project_id']]);
+ }
+
+ public function getNewBoardTaskButton(array $swimlane, array $column)
+ {
+ $html = '<div class="board-add-icon">';
+ $providers = $this->externalTaskManager->getProviders();
+
+ if (empty($providers)) {
+ $html .= $this->helper->modal->largeIcon(
+ 'plus',
+ t('Add a new task'),
+ 'TaskCreationController',
+ 'show', array(
+ 'project_id' => $column['project_id'],
+ 'column_id' => $column['id'],
+ 'swimlane_id' => $swimlane['id'],
+ )
+ );
+ } else {
+ $html .= '<div class="dropdown">';
+ $html .= '<a href="#" class="dropdown-menu"><i class="fa fa-plus" aria-hidden="true"></i></a><ul>';
+
+ $link = $this->helper->modal->large(
+ 'plus',
+ t('Add a new Kanboard task'),
+ 'TaskCreationController',
+ 'show', array(
+ 'project_id' => $column['project_id'],
+ 'column_id' => $column['id'],
+ 'swimlane_id' => $swimlane['id'],
+ )
+ );
+
+ $html .= '<li>'.$link.'</li>';
+
+ foreach ($providers as $provider) {
+ $link = $this->helper->url->link(
+ $provider->getMenuAddLabel(),
+ 'ExternalTaskCreationController',
+ 'step1',
+ array('project_id' => $column['project_id'], 'swimlane_id' => $swimlane['id'], 'column_id' => $column['id'], 'provider_name' => $provider->getName()),
+ false,
+ 'js-modal-large'
+ );
+
+ $html .= '<li>'.$provider->getIcon().' '.$link.'</li>';
+ }
+
+ $html .= '</ul></div>';
+ }
+
+ $html .= '</div>';
+
+ return $html;
+ }
+}
diff --git a/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php b/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php
new file mode 100644
index 00000000..1ee35c98
--- /dev/null
+++ b/plugins/Group_assign/Helper/SmallAvatarHelperExtend.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Helper;
+
+use Kanboard\Helper\AvatarHelper;
+use Kanboard\Core\Base;
+/**
+ * Avatar Helper
+ *
+ * @package helper
+ * @author Frederic Guillot
+ */
+class SmallAvatarHelperExtend extends AvatarHelper
+{
+
+ public function smallMultiple($owner_ms, $css = '') {
+ $assignees = $this->multiselectMemberModel->getMembers($owner_ms);
+ $html = "";
+ foreach ($assignees as $assignee) {
+ $user = $this->userModel->getById($assignee['user_id']);
+ $html .= $this->render($assignee['user_id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, 20);
+ }
+ return $html;
+ }
+
+ public function miniMultiple($owner_ms, $css = '') {
+ $assignees = $this->multiselectMemberModel->getMembers($owner_ms);
+ $html = "";
+ foreach ($assignees as $assignee) {
+ $user = $this->userModel->getById($assignee['user_id']);
+ $html .= $this->render($assignee['user_id'], $user['username'], $user['name'], $user['email'], $user['avatar_path'], $css, 13);
+ }
+ return $html;
+ }
+ }
diff --git a/plugins/Group_assign/LICENSE b/plugins/Group_assign/LICENSE
new file mode 100644
index 00000000..b2e46df8
--- /dev/null
+++ b/plugins/Group_assign/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 creecros
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/Group_assign/Locale/de_DE/translations.php b/plugins/Group_assign/Locale/de_DE/translations.php
new file mode 100644
index 00000000..eb5128ba
--- /dev/null
+++ b/plugins/Group_assign/Locale/de_DE/translations.php
@@ -0,0 +1,13 @@
+<?php
+
+return array(
+ 'Assigned Group' => 'Zugeordnete Gruppe',
+ 'Assigned Group:' => 'Zugeordnete Gruppe:',
+ 'Other Assignees' => 'Weitere Zuordnungen',
+ 'Other Assignees:' => 'Weitere Zuordnungen:',
+ 'Send a task by email to assigned group members' => 'Senden einer Aufgabe per E-Mail an zugewiesene Gruppenmitglieder',
+ 'Send a task by email to the other assignees for a task.' => 'Aufgabe per E-Mail an die weiteren zugeordneten der Aufgabe senden',
+ 'Send email notification of impending due date to Group Members Assigned' => 'E-Mail-Benachrichtigung über bevorstehendes Fälligkeitsdatum an die zugewiesenen Gruppenmitglieder senden',
+ 'Send email notification of impending due date to the other assignees of a task' => 'E-Mail-Benachrichtigung über das bevorstehende Fälligkeitsdatum an die anderen Empfänger einer Aufgabe senden',
+ 'Assign the task to a specific group' => 'Aufgabe einer bestimmten Gruppe zuordnen'
+);
diff --git a/plugins/Group_assign/Locale/pt_BR/translations.php b/plugins/Group_assign/Locale/pt_BR/translations.php
new file mode 100644
index 00000000..fbbdf207
--- /dev/null
+++ b/plugins/Group_assign/Locale/pt_BR/translations.php
@@ -0,0 +1,97 @@
+<?php
+
+return array(
+ 'Action' => 'Ação',
+ 'Action date' => 'Data de ação',
+ 'Add a new Kanboard task' => 'Adicionar uma nova tarefa do Kanboard',
+ 'Add a new task' => 'Adicionar uma nova tarefa',
+ 'All tasks' => 'Todas as tarefas',
+ 'Assignee' => 'Responsável',
+ 'Assigned Group' => 'Grupo Designado',
+ 'Assigned Group:' => 'Grupo Designado:',
+ 'Assign to me' => 'Atribuir para mim',
+ 'Assign the task to a specific group' => 'Atribuir uma tarefa para um grupo específico',
+ 'Category' => 'Categoria',
+ 'Create another task' => 'Criar outra tarefa',
+ 'Column' => 'Coluna',
+ 'Color' => 'Cor',
+ 'Complexity' => 'Complexidade',
+ 'Day(s)' => 'Dia(s)',
+ 'Documentation' => 'Documentação',
+ 'Due Date' => 'Data de Vencimento',
+ 'Duplicate to multiple projects' => 'Duplicar para múltiplos projetos',
+ 'Duration in days' => 'Duração em dias',
+ 'Email subject' => 'Assunto do email',
+ 'Enter one line per task, or leave blank to copy Task Title and create only one subtask.' => 'Digite uma linha por tarefa, ou deixe em branco para copiar o Título da Tarefa e criar apenas uma subtarefa.',
+ 'Event' => 'Evento',
+ 'Existing due date' => 'Data de vencimeto existente',
+ 'Group' => 'Grupo',
+ 'Groups management' => 'Gestão de grupos',
+ 'hours' => 'horas',
+ 'Logout' => 'Sair',
+ 'Me' => 'Eu',
+ 'Month(s)' => 'Mês(es)',
+ 'My dashboard' => 'Meu painel',
+ 'My profile' => 'Meu perfil',
+ 'New assignee: %s' => 'Novo responsável: %s',
+ 'New category: %s' => 'Nova categoria: %s',
+ 'New color: %s' => 'Nova cor: %s',
+ 'New complexity: %d' => 'Nova complexidade: %d',
+ 'New due date: ' => 'Nova data de vencimento: ',
+ 'New group assigned: %s' => 'Novo grupo atribuido: %s',
+ 'New task' => 'Nova tarefa',
+ 'New title: %s' => 'Novo título: %s',
+ 'No' => 'Não',
+ 'not assigned' => 'não atribuído',
+ 'Only for tasks assigned to me' => 'Apenas tarefas atribuídas a mim',
+ 'Only for tasks created by me' => 'Apenas tarefas criadas por mim',
+ 'Only for tasks created by me and tasks assigned to me' => 'Apenas tarefas criadas por mim ou tarefas atribuídas a mim',
+ 'Options' => 'Opções',
+ 'Original estimate' => 'Estimativa original',
+ 'Other Assignees' => 'Outros Responsáveis',
+ 'Other Assignees:' => 'Outros Responsáveis:',
+ 'Priority' => 'Prioridade',
+ 'Projects management' => 'Gestão de projetos',
+ 'Recurrence settings have been modified' => 'Configurações de recorrência foram modificadas',
+ 'Reference' => 'Referência',
+ 'Send a task by email to assigned group members' => 'Enviar uma tarefa por email para membros do grupo designado',
+ 'Send a task by email to the other assignees for a task.' => 'Enviar uma tarefa por email para os outros responsáveis por uma tarefa',
+ 'Send email notification of impending due date to Group Members Assigned' => 'Enviar email de notificação de data de vencimento iminente para os membros de grupos designados',
+ 'Send email notification of impending due date to the other assignees of a task' => 'Enviar email de notificação de data de vencimento iminente para os outros designados por uma tarefa',
+ 'Settings' => 'Definições',
+ 'Start Date' => 'Data de Início',
+ 'Start date changed: ' => 'Data de início alterada: ',
+ 'Swimlane' => 'Raia',
+ 'Tags' => 'Etiquetas',
+ 'Task created successfully.' => 'Tarefa criada com sucesso.',
+ 'Task priority' => 'Prioridade da tarefa',
+ 'Task updated successfully.' => 'Tarefa atualizada com sucesso',
+ 'Template for the task description' => 'Modelo para a descrição da tarefa',
+ 'The description has been modified:' => 'A descrição foi modificada:',
+ 'The due date have been removed' => 'A data de vencimento foi removida',
+ 'The field "%s" have been updated' => 'O campo "%s" foi atualizado',
+ 'The task is not assigned anymore' => 'A tarefa não está mais atribuída',
+ 'The task is not assigned to a group anymore' => 'A tarefa não está mais atribuída para um grupo',
+ 'The task is not assigned to multiple users anymore' => 'A tarefa não está mais atribuída para múltiplos usuários',
+ 'The task has been assigned other users' => 'A tarefa foi atribuída para outros usuários',
+ 'There is no category now' => 'Não há categoria agora',
+ 'There is no description anymore' => 'Não há descrição agora',
+ 'Time estimated changed: %sh' => 'Tempo estimado alterado: %sh',
+ 'Time spent' => 'Tempo gasto',
+ 'Time spent changed: %sh' => 'Tempo gasto alterado: %sh',
+ 'Title' => 'Título',
+ 'Unable to create this task.' => 'Incapaz de criar esta tarefa',
+ 'Unable to create your task.' => 'Incapaz de criar a sua tarefa.',
+ 'Unable to update your task.' => 'Incapaz de atualizar sua tarefa',
+ 'Unassigned' => 'Não atribuido',
+ 'Users management' => 'Gestão de usuários',
+ 'When task is moved from first column' => 'Quando a tarefa é movida da primeira coluna',
+ 'When task is moved to last column' => 'Quando a tarefa é movida para a última coluna',
+ 'When task is closed' => 'Quando a tarefa é fechada',
+ 'Year(s)' => 'Ano(s)',
+ 'Yes' => 'Sim',
+ 'You are not allowed to change the assignee.' => 'Você não tem permissão para mudar o responsável.',
+ 'You are not allowed to update tasks assigned to someone else.' => 'Você não tem permissão para atualizar tarefas designadas para outras pessoas.',
+ 'You cannot create tasks in this column.' => 'Você não pode criar tarefas nesta coluna.',
+ '[DUPLICATE]' => '[DUPLICADO]',
+);
diff --git a/plugins/Group_assign/Model/GroupAssignCalendarModel.php b/plugins/Group_assign/Model/GroupAssignCalendarModel.php
new file mode 100644
index 00000000..5b45ca09
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupAssignCalendarModel.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use DateTime;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Core\Base;
+
+/**
+ * Group_assign Calendar Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class GroupAssignCalendarModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'tasks';
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getUserCalendarEvents($user_id, $start, $end)
+ {
+ $getMS_Ids = $this->db->table(MultiselectMemberModel::TABLE)
+ ->eq('user_id', $user_id)
+ ->findAllByColumn('group_id');
+
+ $getGr_Ids = $this->db->table(GroupMemberModel::TABLE)
+ ->eq('user_id', $user_id)
+ ->findAllByColumn('group_id');
+
+ $tasks = $this->db->table(self::TABLE)
+ ->beginOr()
+ ->eq('owner_id', $user_id)
+ ->in('owner_gp', $getGr_Ids)
+ ->in('owner_ms', $getMS_Ids)
+ ->closeOr()
+ ->gte('date_due', strtotime($start))
+ ->lte('date_due', strtotime($end))
+ ->neq('is_active', 0);
+
+ $tasks = $tasks->findAll();
+
+ $events = array();
+
+ foreach ($tasks as $task) {
+
+ $startDate = new DateTime();
+ $startDate->setTimestamp($task['date_started']);
+
+ $endDate = new DateTime();
+ $endDate->setTimestamp($task['date_due']);
+
+ if ($startDate->getTimestamp() == 0) { $startDate = $endDate; }
+
+ $allDay = $startDate == $endDate && $endDate->format('Hi') == '0000';
+ $format = $allDay ? 'Y-m-d' : 'Y-m-d\TH:i:s';
+
+ $events[] = array(
+ 'timezoneParam' => $this->timezoneModel->getCurrentTimezone(),
+ 'id' => $task['id'],
+ 'title' => t('#%d', $task['id']).' '.$task['title'],
+ 'backgroundColor' => $this->colorModel->getBackgroundColor('dark_grey'),
+ 'borderColor' => $this->colorModel->getBorderColor($task['color_id']),
+ 'textColor' => 'white',
+ 'url' => $this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
+ 'start' => $startDate->format($format),
+ 'end' => $endDate->format($format),
+ 'editable' => $allDay,
+ 'allday' => $allDay,
+ );
+ }
+
+ return $events;
+ }
+
+}
diff --git a/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php b/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php
new file mode 100644
index 00000000..179f6630
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupAssignTaskDuplicationModel.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Core\Base;
+
+/**
+ * Task Duplication
+ *
+ * @package Kanboard\Model
+ * @author Frederic Guillot
+ */
+class GroupAssignTaskDuplicationModel extends Base
+{
+ /**
+ * Fields to copy when duplicating a task
+ *
+ * @access protected
+ * @var string[]
+ */
+ protected $fieldsToDuplicate = array(
+ 'title',
+ 'description',
+ 'date_due',
+ 'color_id',
+ 'project_id',
+ 'column_id',
+ 'owner_id',
+ 'score',
+ 'priority',
+ 'category_id',
+ 'time_estimated',
+ 'swimlane_id',
+ 'recurrence_status',
+ 'recurrence_trigger',
+ 'recurrence_factor',
+ 'recurrence_timeframe',
+ 'recurrence_basedate',
+ 'external_provider',
+ 'external_uri',
+ 'reference',
+ 'owner_ms',
+ 'owner_gp',
+ );
+
+ /**
+ * Duplicate a task to the same project
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Duplicated task id
+ */
+ public function duplicate($task_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['title'] = t('[DUPLICATE]').' '.$values['title'];
+
+ $new_task_id = $this->save($task_id, $values);
+
+ if ($new_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $new_task_id);
+ $this->taskLinkModel->create($new_task_id, $task_id, 4);
+ }
+
+ return $new_task_id;
+ }
+
+ /**
+ * Check if the assignee and the category are available in the destination project
+ *
+ * @access public
+ * @param array $values
+ * @return array
+ */
+ public function checkDestinationProjectValues(array &$values)
+ {
+ // Check if the assigned user is allowed for the destination project
+ if ($values['owner_id'] > 0 && ! $this->projectPermissionModel->isUserAllowed($values['project_id'], $values['owner_id'])) {
+ $values['owner_id'] = 0;
+ }
+
+ // Check if the category exists for the destination project
+ if ($values['category_id'] > 0) {
+ $values['category_id'] = $this->categoryModel->getIdByName(
+ $values['project_id'],
+ $this->categoryModel->getNameById($values['category_id'])
+ );
+ }
+
+ // Check if the swimlane exists for the destination project
+ $values['swimlane_id'] = $this->swimlaneModel->getIdByName(
+ $values['project_id'],
+ $this->swimlaneModel->getNameById($values['swimlane_id'])
+ );
+
+ if ($values['swimlane_id'] == 0) {
+ $values['swimlane_id'] = $this->swimlaneModel->getFirstActiveSwimlaneId($values['project_id']);
+ }
+
+ // Check if the column exists for the destination project
+ if ($values['column_id'] > 0) {
+ $values['column_id'] = $this->columnModel->getColumnIdByTitle(
+ $values['project_id'],
+ $this->columnModel->getColumnTitleById($values['column_id'])
+ );
+
+ $values['column_id'] = $values['column_id'] ?: $this->columnModel->getFirstColumnId($values['project_id']);
+ }
+
+ // Check if priority exists for destination project
+ $values['priority'] = $this->projectTaskPriorityModel->getPriorityForProject(
+ $values['project_id'],
+ empty($values['priority']) ? 0 : $values['priority']
+ );
+
+ return $values;
+ }
+
+ /**
+ * Duplicate fields for the new task
+ *
+ * @access protected
+ * @param integer $task_id Task id
+ * @return array
+ */
+ protected function copyFields($task_id)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $values = array();
+
+ foreach ($this->fieldsToDuplicate as $field) {
+ $values[$field] = $task[$field];
+ }
+
+ $ms_id = $this->multiselectModel->create();
+ $users_in_ms = $this->multiselectMemberModel->getMembers($values['owner_ms']);
+ $values['owner_ms'] = $ms_id;
+ foreach ($users_in_ms as $user) {
+ $this->multiselectMemberModel->addUser($ms_id, $user['id']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create the new task and duplicate subtasks
+ *
+ * @access protected
+ * @param integer $task_id Task id
+ * @param array $values Form values
+ * @return boolean|integer
+ */
+ protected function save($task_id, array $values)
+ {
+ $new_task_id = $this->taskCreationModel->create($values);
+
+ if ($new_task_id !== false) {
+ $this->subtaskModel->duplicate($task_id, $new_task_id);
+ }
+
+ return $new_task_id;
+ }
+}
diff --git a/plugins/Group_assign/Model/GroupColorExtension.php b/plugins/Group_assign/Model/GroupColorExtension.php
new file mode 100644
index 00000000..e8e19850
--- /dev/null
+++ b/plugins/Group_assign/Model/GroupColorExtension.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\GroupModel;
+
+class GroupColorExtension extends GroupModel
+{
+
+ public function getGroupColor($str) {
+ $code = dechex(crc32($str));
+ $code = substr($code, 0, 6);
+ return $code;
+ }
+
+ public function getFontColor($hex) {
+ // returns brightness value from 0 to 255
+ // strip off any leading #
+ $hex = str_replace('#', '', $hex);
+
+ $c_r = hexdec(substr($hex, 0, 2));
+ $c_g = hexdec(substr($hex, 2, 2));
+ $c_b = hexdec(substr($hex, 4, 2));
+
+ $brightness = (($c_r * 299) + ($c_g * 587) + ($c_b * 114)) / 1000;
+ if ($brightness > 130) { return 'black'; } else { return 'white'; }
+ }
+}
diff --git a/plugins/Group_assign/Model/MultiselectMemberModel.php b/plugins/Group_assign/Model/MultiselectMemberModel.php
new file mode 100644
index 00000000..ce0f3ace
--- /dev/null
+++ b/plugins/Group_assign/Model/MultiselectMemberModel.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Core\Queue\QueueManager;
+use Kanboard\Core\Base;
+
+/**
+ * Multiselect Member Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class MultiselectMemberModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'multiselect_has_users';
+
+ /**
+ * Get query to fetch all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return \PicoDb\Table
+ */
+ public function getQuery($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->join(UserModel::TABLE, 'id', 'user_id')
+ ->eq('group_id', $group_id);
+ }
+
+ /**
+ * Get all users
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getMembers($group_id)
+ {
+ return $this->getQuery($group_id)->findAll();
+ }
+
+ /**
+ * Get all not members
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getNotMembers($group_id)
+ {
+ $subquery = $this->db->table(self::TABLE)
+ ->columns('user_id')
+ ->eq('group_id', $group_id);
+
+ return $this->db->table(UserModel::TABLE)
+ ->notInSubquery('id', $subquery)
+ ->eq('is_active', 1)
+ ->findAll();
+ }
+
+ /**
+ * Add user to a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function addUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)->insert(array(
+ 'group_id' => $group_id,
+ 'user_id' => $user_id,
+ ));
+ }
+
+ /**
+ * Remove user from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeUser($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->remove();
+ }
+
+ /**
+ * Remove all users from a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function removeAllUsers($group_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->remove();
+ }
+
+ /**
+ * Check if a user is member
+ *
+ * @access public
+ * @param integer $group_id
+ * @param integer $user_id
+ * @return boolean
+ */
+ public function isMember($group_id, $user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->eq('group_id', $group_id)
+ ->eq('user_id', $user_id)
+ ->exists();
+ }
+
+ /**
+ * Get all groups for a given user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getGroups($user_id)
+ {
+ return $this->db->table(self::TABLE)
+ ->columns(MultiselectModel::TABLE.'.id', MultiselectModel::TABLE.'.external_id')
+ ->join(MultiselectModel::TABLE, 'id', 'group_id')
+ ->eq(self::TABLE.'.user_id', $user_id)
+ ->asc(MultiselectModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Fire Assignee Change
+ *
+ * @access protected
+ * @param array $task
+ * @param array $changes
+ */
+ public function assigneeChanged(array $task, array $changes)
+ {
+ $events = array();
+ $events[] = TaskModel::EVENT_ASSIGNEE_CHANGE;
+
+ if (! empty($events)) {
+ $this->queueManager->push($this->taskEventJob
+ ->withParams($task['id'], $events, $changes, array(), $task)
+ );
+ }
+ }
+}
diff --git a/plugins/Group_assign/Model/MultiselectModel.php b/plugins/Group_assign/Model/MultiselectModel.php
new file mode 100644
index 00000000..36f1a9d1
--- /dev/null
+++ b/plugins/Group_assign/Model/MultiselectModel.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Base;
+
+/**
+ * Multiselect Model
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class MultiselectModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const TABLE = 'multiselect';
+
+ /**
+ * Get query to fetch all groups
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getQuery()
+ {
+ return $this->db->table(self::TABLE)
+ ->columns('id', 'external_id')
+ ->subquery('SELECT COUNT(*) FROM '.MultiselectMemberModel::TABLE.' WHERE group_id='.self::TABLE.'.id', 'nb_users');
+ }
+
+ /**
+ * Get a specific group by id
+ *
+ * @access public
+ * @param integer $group_id
+ * @return array
+ */
+ public function getById($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->findOne();
+ }
+
+ /**
+ * Get a specific group by externalID
+ *
+ * @access public
+ * @param string $external_id
+ * @return array
+ */
+ public function getByExternalId($external_id)
+ {
+ return $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOne();
+ }
+
+ /**
+ * Get specific groups by externalIDs
+ *
+ * @access public
+ * @param string[] $external_ids
+ * @return array
+ */
+ public function getByExternalIds(array $external_ids)
+ {
+ if (empty($external_ids)) {
+ return [];
+ }
+
+ return $this->db->table(self::TABLE)->in('external_id', $external_ids)->findAll();
+ }
+
+ /**
+ * Get all groups
+ *
+ * @access public
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->getQuery()->asc('id')->findAll();
+ }
+
+ /**
+ * Remove a group
+ *
+ * @access public
+ * @param integer $group_id
+ * @return boolean
+ */
+ public function remove($group_id)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $group_id)->remove();
+ }
+
+ /**
+ * Create a new group
+ *
+ * @access public
+ * @param string $external_id
+ * @return integer|boolean
+ */
+ public function create($external_id = '')
+ {
+ return $this->db->table(self::TABLE)->persist(array(
+ 'external_id' => $external_id,
+ ));
+ }
+
+ /**
+ * Update existing group
+ *
+ * @access public
+ * @param array $values
+ * @return boolean
+ */
+ public function update(array $values)
+ {
+ return $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
+ }
+
+ /**
+ * Get groupId from externalGroupId and create the group if not found
+ *
+ * @access public
+ * @param string $name
+ * @param string $external_id
+ * @return bool|integer
+ */
+ public function getOrCreateExternalGroupId($name, $external_id)
+ {
+ $group_id = $this->db->table(self::TABLE)->eq('external_id', $external_id)->findOneColumn('id');
+
+ if (empty($group_id)) {
+ $group_id = $this->create($external_id);
+ }
+
+ return $group_id;
+ }
+}
diff --git a/plugins/Group_assign/Model/NewMetaMagikSubquery.php b/plugins/Group_assign/Model/NewMetaMagikSubquery.php
new file mode 100644
index 00000000..4870120e
--- /dev/null
+++ b/plugins/Group_assign/Model/NewMetaMagikSubquery.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+
+/**
+ * New Task Finder model
+ * Extends Group_assign Model
+ *
+ * @package Kanboard\Plugin\Group_assign\Model
+ */
+class NewMetaMagikSubQuery extends NewTaskFinderModel
+{
+ const METADATA_TABLE = 'task_has_metadata';
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ // add subquery to original Model, changing only what we want
+ return parent::getExtendedQuery()
+ ->subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata');
+ }
+} \ No newline at end of file
diff --git a/plugins/Group_assign/Model/NewTaskFinderModel.php b/plugins/Group_assign/Model/NewTaskFinderModel.php
new file mode 100644
index 00000000..b8b10916
--- /dev/null
+++ b/plugins/Group_assign/Model/NewTaskFinderModel.php
@@ -0,0 +1,491 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\ActionParameterModel;
+use Kanboard\Model\AvatarFileModel;
+use Kanboard\Model\BoardModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ColumnMoveRestrictionModel;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\CurrencyModel;
+use Kanboard\Model\CustomFilterModel;
+use Kanboard\Model\FileModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use Kanboard\Model\LanguageModel;
+use Kanboard\Model\LastLoginModel;
+use Kanboard\Model\LinkModel;
+use Kanboard\Model\MetadataModel;
+use Kanboard\Model\NotificationModel;
+use Kanboard\Model\NotificationTypeModel;
+use Kanboard\Model\PasswordResetModel;
+use Kanboard\Model\ProjectActivityModel;
+use Kanboard\Model\ProjectDailyColumnStatsModel;
+use Kanboard\Model\ProjectDailyStatsModel;
+use Kanboard\Model\ProjectDuplicationModel;
+use Kanboard\Model\ProjectFileModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\ProjectMetadataModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectNotificationModel;
+use Kanboard\Model\ProjectNotificationTypeModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectRoleModel;
+use Kanboard\Model\ProjectRoleRestrictionModel;
+use Kanboard\Model\ProjectTaskDuplicationModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\RememberMeSessionModel;
+use Kanboard\Model\SettingModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\SubtaskPositionModel;
+use Kanboard\Model\SubtaskStatusModel;
+use Kanboard\Model\SubtaskTaskConversionModel;
+use Kanboard\Model\SubtaskTimeTrackingModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskAnalyticModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskExternalLinkModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\TaskLinkModel;
+use Kanboard\Model\TaskMetadataModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskModificationModel;
+use Kanboard\Model\TaskPositionModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskProjectMoveModel;
+use Kanboard\Model\TaskRecurrenceModel;
+use Kanboard\Model\TaskStatusModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TransitionModel;
+use Kanboard\Model\UserLockingModel;
+use Kanboard\Model\UserMentionModel;
+use Kanboard\Model\UserMetadataModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\UserNotificationFilterModel;
+use Kanboard\Model\UserNotificationModel;
+use Kanboard\Model\UserNotificationTypeModel;
+use Kanboard\Model\UserUnreadNotificationModel;
+use Kanboard\Core\Base;
+
+
+class NewTaskFinderModel extends Base
+{
+ /**
+ * Get query for project user overview
+ *
+ * @access public
+ * @param array $project_ids
+ * @param integer $is_active
+ * @return \PicoDb\Table
+ */
+ public function getProjectUserOverviewQuery(array $project_ids, $is_active)
+ {
+ if (empty($project_ids)) {
+ $project_ids = array(-1);
+ }
+
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.time_spent',
+ TaskModel::TABLE.'.time_estimated',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->eq(TaskModel::TABLE.'.is_active', $is_active)
+ ->in(ProjectModel::TABLE.'.id', $project_ids)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get query for assigned user tasks
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->getExtendedQuery()
+ ->beginOr()
+ ->eq(TaskModel::TABLE.'.owner_id', $user_id)
+ ->addCondition(TaskModel::TABLE.".id IN (SELECT task_id FROM ".SubtaskModel::TABLE." WHERE ".SubtaskModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$user_id')")
+ ->closeOr()
+ ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
+ ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
+ ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
+ }
+
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
+ '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone',
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.reference',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.description',
+ TaskModel::TABLE.'.date_creation',
+ TaskModel::TABLE.'.date_modification',
+ TaskModel::TABLE.'.date_completed',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.column_id',
+ TaskModel::TABLE.'.swimlane_id',
+ TaskModel::TABLE.'.owner_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.position',
+ TaskModel::TABLE.'.is_active',
+ TaskModel::TABLE.'.score',
+ TaskModel::TABLE.'.category_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.date_moved',
+ TaskModel::TABLE.'.recurrence_status',
+ TaskModel::TABLE.'.recurrence_trigger',
+ TaskModel::TABLE.'.recurrence_factor',
+ TaskModel::TABLE.'.recurrence_timeframe',
+ TaskModel::TABLE.'.recurrence_basedate',
+ TaskModel::TABLE.'.recurrence_parent',
+ TaskModel::TABLE.'.recurrence_child',
+ TaskModel::TABLE.'.time_estimated',
+ TaskModel::TABLE.'.time_spent',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ UserModel::TABLE.'.email AS assignee_email',
+ UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
+ CategoryModel::TABLE.'.name AS category_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ CategoryModel::TABLE.'.color_id AS category_color_id',
+ ColumnModel::TABLE.'.title AS column_name',
+ ColumnModel::TABLE.'.position AS column_position',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ TaskModel::TABLE.'.owner_ms',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $status_id Status id
+ * @return array
+ */
+ public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->eq(TaskModel::TABLE.'.is_active', $status_id)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->in(TaskModel::TABLE.'.is_active', $status)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAllByColumn(TaskModel::TABLE.'.id');
+ }
+
+ /**
+ * Get overdue tasks query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getOverdueTasksQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.owner_id',
+ ProjectModel::TABLE.'.name AS project_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(UserModel::TABLE, 'id', 'owner_id')
+ ->eq(ProjectModel::TABLE.'.is_active', 1)
+ ->eq(TaskModel::TABLE.'.is_active', 1)
+ ->neq(TaskModel::TABLE.'.date_due', 0)
+ ->lte(TaskModel::TABLE.'.date_due', time());
+ }
+
+ /**
+ * Get a list of overdue tasks for all projects
+ *
+ * @access public
+ * @return array
+ */
+ public function getOverdueTasks()
+ {
+ return $this->getOverdueTasksQuery()->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getOverdueTasksByProject($project_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getOverdueTasksByUser($user_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
+ }
+
+ /**
+ * Get project id for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getProjectId($task_id)
+ {
+ return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
+ }
+
+ /**
+ * Fetch a task by the id
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getById($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
+ }
+
+ /**
+ * Fetch a task by the reference (external id)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $reference Task reference
+ * @return array
+ */
+ public function getByReference($project_id, $reference)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
+ }
+
+ /**
+ * Get task details (fetch more information from other tables)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getDetails($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.*',
+ CategoryModel::TABLE.'.name AS category_name',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_title',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ 'uc.username AS creator_username',
+ 'uc.name AS creator_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.position AS column_position',
+ GroupModel::TABLE.'.name AS assigned_groupname',
+ ColumnModel::TABLE.'.position AS column_position'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+
+ /**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->columns(
+ TaskModel::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.name AS creator_name',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
+ * Count all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param array $status
+ * @return int
+ */
+ public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
+ * @return integer
+ */
+ public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('is_active', 1)
+ ->count();
+ }
+
+ /**
+ * Return true if the task exists
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function exists($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
+ }
+
+ /**
+ * Get project token
+ *
+ * @access public
+ * @param integer $task_id
+ * @return string
+ */
+ public function getProjectToken($task_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->findOneColumn(ProjectModel::TABLE.'.token');
+ }
+}
diff --git a/plugins/Group_assign/Model/NewUserNotificationFilterModel.php b/plugins/Group_assign/Model/NewUserNotificationFilterModel.php
new file mode 100644
index 00000000..3fa67487
--- /dev/null
+++ b/plugins/Group_assign/Model/NewUserNotificationFilterModel.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\UserModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Core\Base;
+
+/**
+ * User Notification Filter
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class NewUserNotificationFilterModel extends Base
+{
+ /**
+ * SQL table name
+ *
+ * @var string
+ */
+ const PROJECT_TABLE = 'user_has_notifications';
+
+ /**
+ * User filters
+ *
+ * @var integer
+ */
+ const FILTER_NONE = 1;
+ const FILTER_ASSIGNEE = 2;
+ const FILTER_CREATOR = 3;
+ const FILTER_BOTH = 4;
+
+ /**
+ * Get the list of filters
+ *
+ * @access public
+ * @return array
+ */
+ public function getFilters()
+ {
+ return array(
+ self::FILTER_NONE => t('All tasks'),
+ self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'),
+ self::FILTER_CREATOR => t('Only for tasks created by me'),
+ self::FILTER_BOTH => t('Only for tasks created by me and tasks assigned to me'),
+ );
+ }
+
+ /**
+ * Get user selected filter
+ *
+ * @access public
+ * @param integer $user_id
+ * @return integer
+ */
+ public function getSelectedFilter($user_id)
+ {
+ return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter');
+ }
+
+ /**
+ * Save selected filter for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param string $filter
+ * @return boolean
+ */
+ public function saveFilter($user_id, $filter)
+ {
+ return $this->db->table(UserModel::TABLE)->eq('id', $user_id)->update(array(
+ 'notifications_filter' => $filter,
+ ));
+ }
+
+ /**
+ * Get user selected projects
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getSelectedProjects($user_id)
+ {
+ return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id');
+ }
+
+ /**
+ * Save selected projects for a user
+ *
+ * @access public
+ * @param integer $user_id
+ * @param integer[] $project_ids
+ * @return boolean
+ */
+ public function saveSelectedProjects($user_id, array $project_ids)
+ {
+ $results = array();
+ $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove();
+
+ foreach ($project_ids as $project_id) {
+ $results[] = $this->db->table(self::PROJECT_TABLE)->insert(array(
+ 'user_id' => $user_id,
+ 'project_id' => $project_id,
+ ));
+ }
+
+ return !in_array(false, $results, true);
+ }
+
+ /**
+ * Return true if the user should receive notification
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function shouldReceiveNotification(array $user, array $event_data)
+ {
+ $filters = array(
+ 'filterNone',
+ 'filterAssignee',
+ 'filterCreator',
+ 'filterBoth',
+ );
+
+ foreach ($filters as $filter) {
+ if ($this->$filter($user, $event_data)) {
+ return $this->filterProject($user, $event_data);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return true if the user will receive all notifications
+ *
+ * @access public
+ * @param array $user
+ * @return boolean
+ */
+ public function filterNone(array $user)
+ {
+ return $user['notifications_filter'] == self::FILTER_NONE;
+ }
+
+ /**
+ * Return true if the user is the assignee and selected the filter "assignee"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterAssignee(array $user, array $event_data)
+ {
+ if (!isset($event_data['task']['owner_ms'])) $event_data['task']['owner_ms'] = 0;
+ if (!isset($event_data['task']['owner_gp'])) $event_data['task']['owner_gp'] = 0;
+ return $user['notifications_filter'] == self::FILTER_ASSIGNEE &&
+ ($event_data['task']['owner_id'] == $user['id'] ||
+ $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) ||
+ $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id']));
+ }
+
+ /**
+ * Return true if the user is the creator and enabled the filter "creator"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterCreator(array $user, array $event_data)
+ {
+ return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id'];
+ }
+
+ /**
+ * Return true if the user is the assignee or the creator and selected the filter "both"
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterBoth(array $user, array $event_data)
+ {
+ if (!isset($event_data['task']['owner_ms'])) $event_data['task']['owner_ms'] = 0;
+ if (!isset($event_data['task']['owner_gp'])) $event_data['task']['owner_gp'] = 0;
+ return $user['notifications_filter'] == self::FILTER_BOTH &&
+ ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id'] ||
+ $this->multiselectMemberModel->isMember($event_data['task']['owner_ms'], $user['id']) ||
+ $this->groupMemberModel->isMember($event_data['task']['owner_gp'], $user['id']));
+ }
+
+ /**
+ * Return true if the user want to receive notification for the selected project
+ *
+ * @access public
+ * @param array $user
+ * @param array $event_data
+ * @return boolean
+ */
+ public function filterProject(array $user, array $event_data)
+ {
+ $projects = $this->getSelectedProjects($user['id']);
+
+ if (! empty($projects)) {
+ return in_array($event_data['task']['project_id'], $projects);
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/Group_assign/Model/OldMetaMagikSubquery.php b/plugins/Group_assign/Model/OldMetaMagikSubquery.php
new file mode 100644
index 00000000..787b7e4a
--- /dev/null
+++ b/plugins/Group_assign/Model/OldMetaMagikSubquery.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Plugin\Group_assign\Model\OldTaskFinderModel;
+
+/**
+ * New Task Finder model
+ * Extends Group_assign Model
+ *
+ * @package Kanboard\Plugin\Group_assign\Model
+ */
+class OldMetaMagikSubQuery extends OldTaskFinderModel
+{
+ const METADATA_TABLE = 'task_has_metadata';
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ // add subquery to original Model, changing only what we want
+ return parent::getExtendedQuery()
+ ->subquery('(SELECT COUNT(*) FROM '.self::METADATA_TABLE.' WHERE task_id=tasks.id)', 'nb_metadata');
+ }
+} \ No newline at end of file
diff --git a/plugins/Group_assign/Model/OldTaskFinderModel.php b/plugins/Group_assign/Model/OldTaskFinderModel.php
new file mode 100644
index 00000000..70d06126
--- /dev/null
+++ b/plugins/Group_assign/Model/OldTaskFinderModel.php
@@ -0,0 +1,489 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\ActionParameterModel;
+use Kanboard\Model\AvatarFileModel;
+use Kanboard\Model\BoardModel;
+use Kanboard\Model\CategoryModel;
+use Kanboard\Model\ColorModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\ColumnMoveRestrictionModel;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\ConfigModel;
+use Kanboard\Model\CurrencyModel;
+use Kanboard\Model\CustomFilterModel;
+use Kanboard\Model\FileModel;
+use Kanboard\Model\GroupMemberModel;
+use Kanboard\Model\GroupModel;
+use Kanboard\Model\LanguageModel;
+use Kanboard\Model\LastLoginModel;
+use Kanboard\Model\LinkModel;
+use Kanboard\Model\MetadataModel;
+use Kanboard\Model\NotificationModel;
+use Kanboard\Model\NotificationTypeModel;
+use Kanboard\Model\PasswordResetModel;
+use Kanboard\Model\ProjectActivityModel;
+use Kanboard\Model\ProjectDailyColumnStatsModel;
+use Kanboard\Model\ProjectDailyStatsModel;
+use Kanboard\Model\ProjectDuplicationModel;
+use Kanboard\Model\ProjectFileModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Model\ProjectMetadataModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\ProjectNotificationModel;
+use Kanboard\Model\ProjectNotificationTypeModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\ProjectRoleModel;
+use Kanboard\Model\ProjectRoleRestrictionModel;
+use Kanboard\Model\ProjectTaskDuplicationModel;
+use Kanboard\Model\ProjectTaskPriorityModel;
+use Kanboard\Model\ProjectUserRoleModel;
+use Kanboard\Model\RememberMeSessionModel;
+use Kanboard\Model\SettingModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\SubtaskPositionModel;
+use Kanboard\Model\SubtaskStatusModel;
+use Kanboard\Model\SubtaskTaskConversionModel;
+use Kanboard\Model\SubtaskTimeTrackingModel;
+use Kanboard\Model\SwimlaneModel;
+use Kanboard\Model\TagDuplicationModel;
+use Kanboard\Model\TagModel;
+use Kanboard\Model\TaskAnalyticModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskExternalLinkModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\TaskLinkModel;
+use Kanboard\Model\TaskMetadataModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\TaskModificationModel;
+use Kanboard\Model\TaskPositionModel;
+use Kanboard\Model\TaskProjectDuplicationModel;
+use Kanboard\Model\TaskProjectMoveModel;
+use Kanboard\Model\TaskRecurrenceModel;
+use Kanboard\Model\TaskStatusModel;
+use Kanboard\Model\TaskTagModel;
+use Kanboard\Model\TimezoneModel;
+use Kanboard\Model\TransitionModel;
+use Kanboard\Model\UserLockingModel;
+use Kanboard\Model\UserMentionModel;
+use Kanboard\Model\UserMetadataModel;
+use Kanboard\Model\UserModel;
+use Kanboard\Model\UserNotificationFilterModel;
+use Kanboard\Model\UserNotificationModel;
+use Kanboard\Model\UserNotificationTypeModel;
+use Kanboard\Model\UserUnreadNotificationModel;
+use Kanboard\Core\Base;
+
+
+class OldTaskFinderModel extends Base
+{
+ /**
+ * Get query for project user overview
+ *
+ * @access public
+ * @param array $project_ids
+ * @param integer $is_active
+ * @return \PicoDb\Table
+ */
+ public function getProjectUserOverviewQuery(array $project_ids, $is_active)
+ {
+ if (empty($project_ids)) {
+ $project_ids = array(-1);
+ }
+
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.time_spent',
+ TaskModel::TABLE.'.time_estimated',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->eq(TaskModel::TABLE.'.is_active', $is_active)
+ ->in(ProjectModel::TABLE.'.id', $project_ids)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get query for assigned user tasks
+ *
+ * @access public
+ * @param integer $user_id User id
+ * @return \PicoDb\Table
+ */
+ public function getUserQuery($user_id)
+ {
+ return $this->getExtendedQuery()
+ ->beginOr()
+ ->eq(TaskModel::TABLE.'.owner_id', $user_id)
+ ->addCondition(TaskModel::TABLE.".id IN (SELECT task_id FROM ".SubtaskModel::TABLE." WHERE ".SubtaskModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_gp IN (SELECT group_id FROM ".GroupMemberModel::TABLE." WHERE ".GroupMemberModel::TABLE.".user_id='$user_id')")
+ ->addCondition(TaskModel::TABLE.".owner_ms IN (SELECT group_id FROM ".MultiselectMemberModel::TABLE." WHERE ".MultiselectMemberModel::TABLE.".user_id='$user_id')")
+ ->closeOr()
+ ->eq(TaskModel::TABLE.'.is_active', TaskModel::STATUS_OPEN)
+ ->eq(ProjectModel::TABLE.'.is_active', ProjectModel::ACTIVE)
+ ->eq(ColumnModel::TABLE.'.hide_in_dashboard', 0);
+ }
+
+ /**
+ * Extended query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getExtendedQuery()
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->columns(
+ '(SELECT COUNT(*) FROM '.CommentModel::TABLE.' WHERE task_id=tasks.id) AS nb_comments',
+ '(SELECT COUNT(*) FROM '.TaskFileModel::TABLE.' WHERE task_id=tasks.id) AS nb_files',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id) AS nb_subtasks',
+ '(SELECT COUNT(*) FROM '.SubtaskModel::TABLE.' WHERE '.SubtaskModel::TABLE.'.task_id=tasks.id AND status=2) AS nb_completed_subtasks',
+ '(SELECT COUNT(*) FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id) AS nb_links',
+ '(SELECT COUNT(*) FROM '.TaskExternalLinkModel::TABLE.' WHERE '.TaskExternalLinkModel::TABLE.'.task_id = tasks.id) AS nb_external_links',
+ '(SELECT DISTINCT 1 FROM '.TaskLinkModel::TABLE.' WHERE '.TaskLinkModel::TABLE.'.task_id = tasks.id AND '.TaskLinkModel::TABLE.'.link_id = 9) AS is_milestone',
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.reference',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.description',
+ TaskModel::TABLE.'.date_creation',
+ TaskModel::TABLE.'.date_modification',
+ TaskModel::TABLE.'.date_completed',
+ TaskModel::TABLE.'.date_started',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.color_id',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.column_id',
+ TaskModel::TABLE.'.swimlane_id',
+ TaskModel::TABLE.'.owner_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.position',
+ TaskModel::TABLE.'.is_active',
+ TaskModel::TABLE.'.score',
+ TaskModel::TABLE.'.category_id',
+ TaskModel::TABLE.'.priority',
+ TaskModel::TABLE.'.date_moved',
+ TaskModel::TABLE.'.recurrence_status',
+ TaskModel::TABLE.'.recurrence_trigger',
+ TaskModel::TABLE.'.recurrence_factor',
+ TaskModel::TABLE.'.recurrence_timeframe',
+ TaskModel::TABLE.'.recurrence_basedate',
+ TaskModel::TABLE.'.recurrence_parent',
+ TaskModel::TABLE.'.recurrence_child',
+ TaskModel::TABLE.'.time_estimated',
+ TaskModel::TABLE.'.time_spent',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ UserModel::TABLE.'.email AS assignee_email',
+ UserModel::TABLE.'.avatar_path AS assignee_avatar_path',
+ CategoryModel::TABLE.'.name AS category_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.title AS column_name',
+ ColumnModel::TABLE.'.position AS column_position',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ TaskModel::TABLE.'.owner_ms',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE);
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $status_id Status id
+ * @return array
+ */
+ public function getAll($project_id, $status_id = TaskModel::STATUS_OPEN)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->eq(TaskModel::TABLE.'.is_active', $status_id)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAll();
+ }
+
+ /**
+ * Get all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id
+ * @param array $status
+ * @return array
+ */
+ public function getAllIds($project_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.project_id', $project_id)
+ ->in(TaskModel::TABLE.'.is_active', $status)
+ ->asc(TaskModel::TABLE.'.id')
+ ->findAllByColumn(TaskModel::TABLE.'.id');
+ }
+
+ /**
+ * Get overdue tasks query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getOverdueTasksQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.id',
+ TaskModel::TABLE.'.title',
+ TaskModel::TABLE.'.date_due',
+ TaskModel::TABLE.'.project_id',
+ TaskModel::TABLE.'.creator_id',
+ TaskModel::TABLE.'.owner_id',
+ ProjectModel::TABLE.'.name AS project_name',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name'
+ )
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->join(UserModel::TABLE, 'id', 'owner_id')
+ ->eq(ProjectModel::TABLE.'.is_active', 1)
+ ->eq(TaskModel::TABLE.'.is_active', 1)
+ ->neq(TaskModel::TABLE.'.date_due', 0)
+ ->lte(TaskModel::TABLE.'.date_due', time());
+ }
+
+ /**
+ * Get a list of overdue tasks for all projects
+ *
+ * @access public
+ * @return array
+ */
+ public function getOverdueTasks()
+ {
+ return $this->getOverdueTasksQuery()->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by project
+ *
+ * @access public
+ * @param integer $project_id
+ * @return array
+ */
+ public function getOverdueTasksByProject($project_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.project_id', $project_id)->findAll();
+ }
+
+ /**
+ * Get a list of overdue tasks by user
+ *
+ * @access public
+ * @param integer $user_id
+ * @return array
+ */
+ public function getOverdueTasksByUser($user_id)
+ {
+ return $this->getOverdueTasksQuery()->eq(TaskModel::TABLE.'.owner_id', $user_id)->findAll();
+ }
+
+ /**
+ * Get project id for a given task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return integer
+ */
+ public function getProjectId($task_id)
+ {
+ return (int) $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('project_id') ?: 0;
+ }
+
+ /**
+ * Fetch a task by the id
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getById($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOne();
+ }
+
+ /**
+ * Fetch a task by the reference (external id)
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param string $reference Task reference
+ * @return array
+ */
+ public function getByReference($project_id, $reference)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('project_id', $project_id)->eq('reference', $reference)->findOne();
+ }
+
+ /**
+ * Get task details (fetch more information from other tables)
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return array
+ */
+ public function getDetails($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->columns(
+ TaskModel::TABLE.'.*',
+ CategoryModel::TABLE.'.name AS category_name',
+ SwimlaneModel::TABLE.'.name AS swimlane_name',
+ ProjectModel::TABLE.'.name AS project_name',
+ ColumnModel::TABLE.'.title AS column_title',
+ UserModel::TABLE.'.username AS assignee_username',
+ UserModel::TABLE.'.name AS assignee_name',
+ 'uc.username AS creator_username',
+ 'uc.name AS creator_name',
+ CategoryModel::TABLE.'.description AS category_description',
+ ColumnModel::TABLE.'.position AS column_position',
+ GroupModel::TABLE.'.name AS assigned_groupname'
+ )
+ ->join(UserModel::TABLE, 'id', 'owner_id', TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->join(CategoryModel::TABLE, 'id', 'category_id', TaskModel::TABLE)
+ ->join(ColumnModel::TABLE, 'id', 'column_id', TaskModel::TABLE)
+ ->join(SwimlaneModel::TABLE, 'id', 'swimlane_id', TaskModel::TABLE)
+ ->join(ProjectModel::TABLE, 'id', 'project_id', TaskModel::TABLE)
+ ->join(GroupModel::TABLE, 'id', 'owner_gp', TaskModel::TABLE)
+ ->join(MultiselectModel::TABLE, 'id', 'owner_ms', TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->findOne();
+ }
+
+ /**
+ * Get iCal query
+ *
+ * @access public
+ * @return \PicoDb\Table
+ */
+ public function getICalQuery()
+ {
+ return $this->db->table(TaskModel::TABLE)
+ ->left(UserModel::TABLE, 'ua', 'id', TaskModel::TABLE, 'owner_id')
+ ->left(UserModel::TABLE, 'uc', 'id', TaskModel::TABLE, 'creator_id')
+ ->columns(
+ TaskModel::TABLE.'.*',
+ 'ua.email AS assignee_email',
+ 'ua.name AS assignee_name',
+ 'ua.username AS assignee_username',
+ 'uc.email AS creator_email',
+ 'uc.name AS creator_name',
+ 'uc.username AS creator_username'
+ );
+ }
+
+ /**
+ * Count all tasks for a given project and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param array $status List of status id
+ * @return integer
+ */
+ public function countByProjectId($project_id, array $status = array(TaskModel::STATUS_OPEN, TaskModel::STATUS_CLOSED))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and status
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param array $status
+ * @return int
+ */
+ public function countByColumnId($project_id, $column_id, array $status = array(TaskModel::STATUS_OPEN))
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->in('is_active', $status)
+ ->count();
+ }
+
+ /**
+ * Count the number of tasks for a given column and swimlane
+ *
+ * @access public
+ * @param integer $project_id Project id
+ * @param integer $column_id Column id
+ * @param integer $swimlane_id Swimlane id
+ * @return integer
+ */
+ public function countByColumnAndSwimlaneId($project_id, $column_id, $swimlane_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('column_id', $column_id)
+ ->eq('swimlane_id', $swimlane_id)
+ ->eq('is_active', 1)
+ ->count();
+ }
+
+ /**
+ * Return true if the task exists
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean
+ */
+ public function exists($task_id)
+ {
+ return $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->exists();
+ }
+
+ /**
+ * Get project token
+ *
+ * @access public
+ * @param integer $task_id
+ * @return string
+ */
+ public function getProjectToken($task_id)
+ {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->eq(TaskModel::TABLE.'.id', $task_id)
+ ->join(ProjectModel::TABLE, 'id', 'project_id')
+ ->findOneColumn(ProjectModel::TABLE.'.token');
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskProjectDuplicationModel.php b/plugins/Group_assign/Model/TaskProjectDuplicationModel.php
new file mode 100644
index 00000000..835f2063
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskProjectDuplicationModel.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Project Duplication
+ *
+ * @package Kanboard\Plugins\Group_assign
+ * @author Craig Crosby
+ */
+class TaskProjectDuplicationModel extends TaskDuplicationModel
+{
+ /**
+ * Duplicate a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean|integer
+ */
+ public function duplicateToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null, $owner_gp = 0, $owner_ms = 0)
+ {
+ $values = $this->prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id);
+
+ $this->checkDestinationProjectValues($values);
+ $new_task_id = $this->save($task_id, $values);
+ if ($new_task_id !== false) {
+ // Check if the group is allowed for the destination project
+ $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_gp');
+ if ($group_id > 0) {
+ $group_in_project = $this->db
+ ->table(ProjectGroupRoleModel::TABLE)
+ ->eq('project_id', $values['project_id'])
+ ->eq('group_id', $group_id)
+ ->exists();
+ if ($group_in_project) { $this->db->table(TaskModel::TABLE)->eq('id', $new_task_id)->update(['owner_gp' => $group_id]); }
+ }
+
+ // Check if the other assignees are allowed for the destination project
+ $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_ms');
+ if ($ms_id > 0) {
+ $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id);
+ $new_ms_id = $this->multiselectModel->create();
+ $this->db->table(TaskModel::TABLE)->eq('id', $new_task_id)->update(['owner_ms' => $new_ms_id]);
+ foreach ($users_in_ms as $user) {
+ if ($this->projectPermissionModel->isAssignable($values['project_id'], $user['id'])) {
+ $this->multiselectMemberModel->addUser($new_ms_id, $user['id']);
+ }
+ }
+ }
+
+ $this->tagDuplicationModel->duplicateTaskTagsToAnotherProject($task_id, $new_task_id, $project_id);
+ $this->taskLinkModel->create($new_task_id, $task_id, 4);
+ }
+
+ return $new_task_id;
+ }
+
+ /**
+ * Prepare values before duplication
+ *
+ * @access protected
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return array
+ */
+ protected function prepare($task_id, $project_id, $swimlane_id, $column_id, $category_id, $owner_id)
+ {
+ $values = $this->copyFields($task_id);
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $values['column_id'];
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $values['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $values['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $values['owner_id'];
+
+ return $values;
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskProjectMoveModel.php b/plugins/Group_assign/Model/TaskProjectMoveModel.php
new file mode 100644
index 00000000..554d3330
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskProjectMoveModel.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Project Move
+ *
+ * @package Kanboard\Plugins\Group_assign
+ * @author Craig Crosby
+ */
+class TaskProjectMoveModel extends TaskDuplicationModel
+{
+ /**
+ * Move a task to another project
+ *
+ * @access public
+ * @param integer $task_id
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @return boolean
+ */
+ public function moveToProject($task_id, $project_id, $swimlane_id = null, $column_id = null, $category_id = null, $owner_id = null)
+ {
+ $task = $this->taskFinderModel->getById($task_id);
+ $values = $this->prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, $task);
+
+ $this->checkDestinationProjectValues($values);
+ $this->tagDuplicationModel->syncTaskTagsToAnotherProject($task_id, $project_id);
+
+ // Check if the group is allowed for the destination project and unassign if not
+ $group_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_gp');
+ if ($group_id > 0) {
+ $group_in_project = $this->db
+ ->table(ProjectGroupRoleModel::TABLE)
+ ->eq('project_id', $project_id)
+ ->eq('group_id', $group_id)
+ ->exists();
+ if (!$group_in_project) { $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(['owner_gp' => 0]); }
+ }
+
+ // Check if the other assignees are allowed for the destination project and remove from ms group if not
+ $ms_id = $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->findOneColumn('owner_ms');
+ if ($ms_id > 0) {
+ $users_in_ms = $this->multiselectMemberModel->getMembers($ms_id);
+ foreach ($users_in_ms as $user) {
+ if (! $this->projectPermissionModel->isAssignable($project_id, $user['id'])) {
+ $this->multiselectMemberModel->removeUser($ms_id, $user['id']);
+ }
+ }
+ }
+
+
+ if ($this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update($values)) {
+ $this->queueManager->push($this->taskEventJob->withParams($task_id, array(TaskModel::EVENT_MOVE_PROJECT), $values));
+ }
+
+ return true;
+ }
+
+ /**
+ * Prepare new task values
+ *
+ * @access protected
+ * @param integer $project_id
+ * @param integer $swimlane_id
+ * @param integer $column_id
+ * @param integer $category_id
+ * @param integer $owner_id
+ * @param array $task
+ * @return array
+ */
+ protected function prepare($project_id, $swimlane_id, $column_id, $category_id, $owner_id, array $task)
+ {
+ $values = array();
+ $values['is_active'] = 1;
+ $values['project_id'] = $project_id;
+ $values['column_id'] = $column_id !== null ? $column_id : $task['column_id'];
+ $values['position'] = $this->taskFinderModel->countByColumnId($project_id, $values['column_id']) + 1;
+ $values['swimlane_id'] = $swimlane_id !== null ? $swimlane_id : $task['swimlane_id'];
+ $values['category_id'] = $category_id !== null ? $category_id : $task['category_id'];
+ $values['owner_id'] = $owner_id !== null ? $owner_id : $task['owner_id'];
+ $values['priority'] = $task['priority'];
+ return $values;
+ }
+}
diff --git a/plugins/Group_assign/Model/TaskRecurrenceModel.php b/plugins/Group_assign/Model/TaskRecurrenceModel.php
new file mode 100644
index 00000000..999751e1
--- /dev/null
+++ b/plugins/Group_assign/Model/TaskRecurrenceModel.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign\Model;
+
+use DateInterval;
+use DateTime;
+use Kanboard\Model\TaskDuplicationModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Model\ProjectPermissionModel;
+use Kanboard\Model\TaskLinkModel;
+
+/**
+ * Task Recurrence
+ *
+ * @package Kanboard\Plugin\Group_assign
+ * @author Craig Crosby
+ */
+class TaskRecurrenceModel extends GroupAssignTaskDuplicationModel
+{
+ /**
+ * Return the list user selectable recurrence status
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceStatusList()
+ {
+ return array(
+ TaskModel::RECURRING_STATUS_NONE => t('No'),
+ TaskModel::RECURRING_STATUS_PENDING => t('Yes'),
+ );
+ }
+
+ /**
+ * Return the list recurrence triggers
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTriggerList()
+ {
+ return array(
+ TaskModel::RECURRING_TRIGGER_FIRST_COLUMN => t('When task is moved from first column'),
+ TaskModel::RECURRING_TRIGGER_LAST_COLUMN => t('When task is moved to last column'),
+ TaskModel::RECURRING_TRIGGER_CLOSE => t('When task is closed'),
+ );
+ }
+
+ /**
+ * Return the list options to calculate recurrence due date
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceBasedateList()
+ {
+ return array(
+ TaskModel::RECURRING_BASEDATE_DUEDATE => t('Existing due date'),
+ TaskModel::RECURRING_BASEDATE_TRIGGERDATE => t('Action date'),
+ );
+ }
+
+ /**
+ * Return the list recurrence timeframes
+ *
+ * @access public
+ * @return array
+ */
+ public function getRecurrenceTimeframeList()
+ {
+ return array(
+ TaskModel::RECURRING_TIMEFRAME_DAYS => t('Day(s)'),
+ TaskModel::RECURRING_TIMEFRAME_MONTHS => t('Month(s)'),
+ TaskModel::RECURRING_TIMEFRAME_YEARS => t('Year(s)'),
+ );
+ }
+
+ /**
+ * Duplicate recurring task
+ *
+ * @access public
+ * @param integer $task_id Task id
+ * @return boolean|integer Recurrence task id
+ */
+ public function duplicateRecurringTask($task_id)
+ {
+ $values = $this->copyFields($task_id);
+
+ if ($values['recurrence_status'] == TaskModel::RECURRING_STATUS_PENDING) {
+ $values['recurrence_parent'] = $task_id;
+ $values['column_id'] = $this->columnModel->getFirstColumnId($values['project_id']);
+ $this->calculateRecurringTaskDueDate($values);
+
+ $recurring_task_id = $this->save($task_id, $values);
+
+ if ($recurring_task_id !== false) {
+ $this->tagDuplicationModel->duplicateTaskTags($task_id, $recurring_task_id);
+
+ $parent_update = $this->db
+ ->table(TaskModel::TABLE)
+ ->eq('id', $task_id)
+ ->update(array(
+ 'recurrence_status' => TaskModel::RECURRING_STATUS_PROCESSED,
+ 'recurrence_child' => $recurring_task_id,
+ ));
+
+ if ($parent_update) {
+ return $recurring_task_id;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Calculate new due date for new recurrence task
+ *
+ * @access public
+ * @param array $values Task fields
+ */
+ public function calculateRecurringTaskDueDate(array &$values)
+ {
+ if (! empty($values['date_due']) && $values['recurrence_factor'] != 0) {
+ if ($values['recurrence_basedate'] == TaskModel::RECURRING_BASEDATE_TRIGGERDATE) {
+ $values['date_due'] = time();
+ }
+
+ $factor = abs($values['recurrence_factor']);
+ $subtract = $values['recurrence_factor'] < 0;
+
+ switch ($values['recurrence_timeframe']) {
+ case TaskModel::RECURRING_TIMEFRAME_MONTHS:
+ $interval = 'P' . $factor . 'M';
+ break;
+ case TaskModel::RECURRING_TIMEFRAME_YEARS:
+ $interval = 'P' . $factor . 'Y';
+ break;
+ default:
+ $interval = 'P' . $factor . 'D';
+ }
+
+ $date_due = new DateTime();
+ $date_due->setTimestamp($values['date_due']);
+
+ $subtract ? $date_due->sub(new DateInterval($interval)) : $date_due->add(new DateInterval($interval));
+
+ $values['date_due'] = $date_due->getTimestamp();
+ }
+ }
+}
diff --git a/plugins/Group_assign/Plugin.php b/plugins/Group_assign/Plugin.php
new file mode 100644
index 00000000..6f69388c
--- /dev/null
+++ b/plugins/Group_assign/Plugin.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace Kanboard\Plugin\Group_assign;
+
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ProjectGroupRoleModel;
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+use Kanboard\Plugin\Group_assign\Model\NewUserNotificationFilterModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectModel;
+use Kanboard\Plugin\Group_assign\Model\MultiselectMemberModel;
+use Kanboard\Plugin\Group_assign\Model\OldTaskFinderModel;
+use Kanboard\Plugin\Group_assign\Helper\NewTaskHelper;
+use Kanboard\Plugin\Group_assign\Filter\TaskAllAssigneeFilter;
+use Kanboard\Plugin\Group_assign\Action\EmailGroup;
+use Kanboard\Plugin\Group_assign\Action\EmailGroupDue;
+use Kanboard\Plugin\Group_assign\Action\EmailOtherAssignees;
+use Kanboard\Plugin\Group_assign\Action\EmailOtherAssigneesDue;
+use Kanboard\Plugin\Group_assign\Action\AssignGroup;
+use Kanboard\Plugin\Group_assign\Model\GroupAssignCalendarModel;
+use Kanboard\Plugin\Group_assign\Model\GroupAssignTaskDuplicationModel;
+use Kanboard\Plugin\Group_assign\Model\TaskProjectDuplicationModel;
+use Kanboard\Plugin\Group_assign\Model\TaskProjectMoveModel;
+use Kanboard\Plugin\Group_assign\Model\TaskRecurrenceModel;
+use Kanboard\Plugin\Group_assign\Model\NewMetaMagikSubquery;
+use Kanboard\Plugin\Group_assign\Model\OldMetaMagikSubquery;
+use PicoDb\Table;
+use PicoDb\Database;
+use Kanboard\Core\Security\Role;
+
+class Plugin extends Base
+{
+
+ public function initialize()
+ {
+ //Events & Changes
+ $this->template->setTemplateOverride('task/changes', 'group_assign:task/changes');
+
+ //Notifications
+ $this->container['userNotificationFilterModel'] = $this->container->factory(function ($c) {
+ return new NewUserNotificationFilterModel($c);
+ });
+
+ //Helpers
+ $this->helper->register('newTaskHelper', '\Kanboard\Plugin\Group_assign\Helper\NewTaskHelper');
+ $this->helper->register('smallAvatarHelperExtend', '\Kanboard\Plugin\Group_assign\Helper\SmallAvatarHelperExtend');
+
+
+ //Models and backward compatibility
+
+ $applications_version = str_replace('v', '', APP_VERSION);
+ if (strpos(APP_VERSION, 'master') !== false && file_exists('ChangeLog')) { $applications_version = trim(file_get_contents('ChangeLog', false, null, 8, 6), ' '); }
+ $clean_appversion = preg_replace('/\s+/', '', $applications_version);
+
+ if (version_compare($clean_appversion, '1.2.5', '>')) {
+ if (file_exists('plugins/MetaMagik')){
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new NewMetaMagikSubquery($c);
+ });
+ } else {
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new NewTaskFinderModel($c);
+ });
+ }
+ $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) {
+ return new GroupAssignTaskDuplicationModel($c);
+ });
+ $this->container['taskProjectDuplicationModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectDuplicationModel ($c);
+ });
+ $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectMoveModel ($c);
+ });
+ $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) {
+ return new TaskRecurrenceModel ($c);
+ });
+ } else {
+ if (file_exists('plugins/MetaMagik')){
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new OldMetaMagikSubquery($c);
+ });
+ } else {
+ $this->container['taskFinderModel'] = $this->container->factory(function ($c) {
+ return new OldTaskFinderModel($c);
+ });
+ }
+ $this->container['taskDuplicationModel'] = $this->container->factory(function ($c) {
+ return new GroupAssignTaskDuplicationModel($c);
+ });
+ $this->container['taskProjectDuplicationModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectDuplicationModel ($c);
+ });
+ $this->container['taskProjectMoveModel '] = $this->container->factory(function ($c) {
+ return new TaskProjectMoveModel ($c);
+ });
+ $this->container['taskRecurrenceModel '] = $this->container->factory(function ($c) {
+ return new TaskRecurrenceModel ($c);
+ });
+ }
+
+ //Task - Template - details.php
+ $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/details');
+ $this->template->hook->attach('template:task:details:third-column', 'group_assign:task/multi');
+
+ //Forms - task_creation.php and task_modification.php
+ $this->template->setTemplateOverride('task_creation/show', 'group_assign:task_creation/show');
+ $this->template->setTemplateOverride('task_modification/show', 'group_assign:task_modification/show');
+
+ //Board
+ $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/group');
+ $this->template->hook->attach('template:board:private:task:before-title', 'group_assign:board/multi');
+ $groupmodel = $this->projectGroupRoleModel;
+ $this->template->hook->attachCallable('template:app:filters-helper:after', 'group_assign:board/filter', function($array = array()) use ($groupmodel) {
+ if(!empty($array) && $array['id'] >= 1){
+ return ['grouplist' => array_column($groupmodel->getGroups($array['id']), 'name')];
+ } else {
+ return ['grouplist' => array()];
+ }
+ });
+
+ //Filter
+ $this->container->extend('taskLexer', function($taskLexer, $c) {
+ $taskLexer->withFilter(TaskAllAssigneeFilter::getInstance()->setDatabase($c['db'])
+ ->setCurrentUserId($c['userSession']->getId()));
+ return $taskLexer;
+ });
+
+ //Actions
+ $this->actionManager->register(new EmailGroup($this->container));
+ $this->actionManager->register(new EmailGroupDue($this->container));
+ $this->actionManager->register(new EmailOtherAssignees($this->container));
+ $this->actionManager->register(new EmailOtherAssigneesDue($this->container));
+ $this->actionManager->register(new AssignGroup($this->container));
+
+ //Params
+ $this->template->setTemplateOverride('action_creation/params', 'group_assign:action_creation/params');
+
+ //CSS
+ $this->hook->on('template:layout:css', array('template' => 'plugins/Group_assign/Assets/css/group_assign.css'));
+
+ //JS
+ $this->hook->on('template:layout:js', array('template' => 'plugins/Group_assign/Assets/js/group_assign.js'));
+
+ //Calendar Events
+ $container = $this->container;
+
+ $this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
+ $model = new GroupAssignCalendarModel($container);
+ return $model->getUserCalendarEvents($user_id, $start, $end); // Return new events
+ });
+
+ //Roles
+
+ $this->template->hook->attach('template:config:application', 'group_assign:config/toggle');
+
+ if ($this->configModel->get('enable_am_group_management', '2') == 1) {
+ $this->applicationAccessMap->add('GroupListController', '*', Role::APP_MANAGER);
+ $this->applicationAccessMap->add('GroupCreationController', '*', Role::APP_MANAGER);
+ $this->template->setTemplateOverride('header/user_dropdown', 'group_assign:header/user_dropdown');
+ }
+
+
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ }
+
+ public function getClasses()
+ {
+ return [
+ 'Plugin\Group_assign\Model' => [
+ 'MultiselectMemberModel', 'MultiselectModel', 'GroupColorExtension', 'TaskProjectDuplicationModel', 'TaskProjectMoveModel', 'TaskRecurrenceModel',
+ ],
+ ];
+ }
+
+ public function getPluginName()
+ {
+ return 'Group_assign';
+ }
+ public function getPluginDescription()
+ {
+ return t('Add group assignment to tasks');
+ }
+ public function getPluginAuthor()
+ {
+ return 'Craig Crosby';
+ }
+ public function getPluginVersion()
+ {
+ return '1.7.8';
+ }
+ public function getPluginHomepage()
+ {
+ return 'https://github.com/creecros/group_assign';
+ }
+}
diff --git a/plugins/Group_assign/README.md b/plugins/Group_assign/README.md
new file mode 100644
index 00000000..43078d9c
--- /dev/null
+++ b/plugins/Group_assign/README.md
@@ -0,0 +1,95 @@
+## Checkout our latest project
+[![](https://raw.githubusercontent.com/docpht/docpht/master/public/assets/img/logo.png)](https://github.com/docpht/docpht)
+
+- With [DocPHT](https://github.com/docpht/docpht) you can take notes and quickly document anything and without the use of any database.
+-----------
+[![Latest release](https://img.shields.io/github/release/creecros/Group_assign.svg)](https://github.com/creecros/Group_assign/releases)
+[![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/creecros/Group_assign/blob/master/LICENSE)
+[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/creecros/Group_assign/graphs/contributors)
+[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)]()
+[![Downloads](https://img.shields.io/github/downloads/creecros/Group_assign/total.svg)](https://github.com/creecros/Group_assign/releases)
+
+Donate to help keep this project maintained.
+<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SEGNEVQFXHXGW&source=url">
+<img src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" border="0" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" /></a>
+
+:star: If you use it, you should star it on Github!
+It's the least you can do for all the work put into it!
+
+
+# Group_assign
+Assign Tasks to Groups or from Multi-Select of Users with permissions from the project
+
+# Requirements
+Kanboard v1.1.0 or Higher
+
+# Features and usage
+* A task can have an assigned group or selection of users
+* Can only assign groups or other assigness to a task that have permissions in the Project.
+* If a user is in a group that a task is assigned to, it will show up on their dashboard.
+* If a user is in other assignees multiselect that a task is assigned to, it will show up on their dashboard.
+* If a user is in a group that a task is assigned to, it will show up in their calendar.
+* If a user is in other assignees multiselect that a task is assigned to, it will show up in their calendar.
+* If a group is assigned or a user is assigneed in other assignees, it will be appear on the task in detail view, board view, creation, modification.
+* Includes 5 Automatic Actions to utilize the Assigned Group
+ * Email Assigned Group on Task Modification, Creation, Close, or Movement
+ * Email Assigned Group of impending Task Due Date
+ * Email Other Assignees on Task Modification, Creation, Close, or Movement
+ * Email Other Assignees of impending Task Due Date
+ * Assign task to a group on creation or movement
+* using ``allassignees:me`` (``assignee:me`` for pre 1.7.3 versions) in filter will find tasks assigned to groups that the user is in or assignee in other assignees is in.
+* using ``allassignees:GroupName`` (``assignee:GroupName`` for pre 1.7.3 versions) in filter will find tasks assigned to a group by NAME of the group.
+* using ``allassignees:GroupID`` (``assignee:GroupID`` for pre 1.7.3 versions) in filter will find tasks assigned to a group by ID number of group.
+* using ``allassignees:Username`` or ``allassignees:Name`` will find all tasks assigned to that user regardless of how they have been assigneed, whether in the group or in Other Assignees or Assignee.
+* User assigneed via a group or multiselect will now recieve notifications
+* Changing assigned group or any multiselect users will now trigger `EVENT_ASSIGNEE_CHANGE`
+* Duplicating Tasks will include assigned groups and other users.
+ * Duplicating to another project or moving to another project will check permissions of assignees, and remove those without permission.
+* Task Reccurences will include group assigned and other assignees in the recurrence.
+* Setting included to enable group managment for Application Managers
+ * Found in `Settings > Application settings`
+
+# Future enhancments
+Find bugs or missing functionality, please report it.
+
+- [x] Add a few basic automatic actions that utilize Groups assigned
+- [x] Add relationship for ``allassignees:Username`` or ``allassignees:Name`` in the table lookup
+- [x] Add an event for assigned group change.
+- [x] Incorporate into notifications
+- [x] Address Task Duplication
+- [x] Task Recurrence
+
+# Manual Installation
+
+- Find the release you wish to install: https://github.com/creecros/Group_assign/releases
+- Download the provided zip, not the source zip, i.e. `Group_assign-x.x.x.zip`
+- Unzip contents to the plugins folder
+
+In the event that you use the master repo, ensure that the directory of the plugin is named `Group_assign`, or else the plugin will not work.
+
+# Screenshots
+
+## Task Details:
+![image](https://user-images.githubusercontent.com/26339368/49951197-64546680-fec7-11e8-9473-82820b1a4f7e.png)
+
+## Task Creation/Modification:
+![image](https://user-images.githubusercontent.com/26339368/38753761-692db008-3f2d-11e8-8ce2-59d88ddf39b1.png)
+![image](https://user-images.githubusercontent.com/26339368/49557918-3c696f80-f8d7-11e8-91b8-7cef11c6eec0.png)
+
+## Board View:
+![image](https://user-images.githubusercontent.com/26339368/49951135-3a9b3f80-fec7-11e8-9bf6-3a777c09c675.png)
+
+## Users Calendar View
+
+- Tasks that a user is assigned too but not main assignee will show up in calendar, with Dark Grey Background and Task color Border, to differentiate that they are not the main assignee.
+
+![image](https://user-images.githubusercontent.com/26339368/49655821-b7cb3e00-fa09-11e8-9608-952abbf146fa.png)
+
+
+## Automatic Actions:
+![image](https://user-images.githubusercontent.com/26339368/38754253-0a0fd2de-3f2f-11e8-9dde-2036de011a6b.png)
+
+![image](https://user-images.githubusercontent.com/26339368/38754279-2285d0d4-3f2f-11e8-88c2-0ed91e452f90.png)
+
+![image](https://user-images.githubusercontent.com/26339368/38754288-310df2c6-3f2f-11e8-9993-39e96b55076c.png)
+
diff --git a/plugins/Group_assign/Schema/Mysql.php b/plugins/Group_assign/Schema/Mysql.php
new file mode 100644
index 00000000..17318a29
--- /dev/null
+++ b/plugins/Group_assign/Schema/Mysql.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_ms` INT DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE `multiselect` (
+ id INT NOT NULL AUTO_INCREMENT,
+ external_id VARCHAR(255) DEFAULT '',
+ PRIMARY KEY(id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INT NOT NULL,
+ user_id INT NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES `multiselect`(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ ) ENGINE=InnoDB CHARSET=utf8
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE `tasks` ADD COLUMN `owner_gp` INT DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Schema/Postgres.php b/plugins/Group_assign/Schema/Postgres.php
new file mode 100644
index 00000000..54b0a2be
--- /dev/null
+++ b/plugins/Group_assign/Schema/Postgres.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_ms INT DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE multiselect (
+ id SERIAL PRIMARY KEY,
+ external_id VARCHAR(255) DEFAULT ''
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INT DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Schema/Sqlite.php b/plugins/Group_assign/Schema/Sqlite.php
new file mode 100644
index 00000000..0d5d3e9c
--- /dev/null
+++ b/plugins/Group_assign/Schema/Sqlite.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Kanboard\Plugin\group_assign\Schema;
+
+use PDO;
+
+const VERSION = 2;
+
+function version_2(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_ms INTEGER DEFAULT '0'");
+
+ $pdo->exec("
+ CREATE TABLE multiselect (
+ id INTEGER PRIMARY KEY,
+ external_id TEXT DEFAULT ''
+ )
+ ");
+
+ $pdo->exec("
+ CREATE TABLE multiselect_has_users (
+ group_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY(group_id) REFERENCES multiselect(id) ON DELETE CASCADE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(group_id, user_id)
+ )
+ ");
+}
+
+function version_1(PDO $pdo)
+{
+ $pdo->exec("ALTER TABLE tasks ADD COLUMN owner_gp INTEGER DEFAULT '0'");
+}
diff --git a/plugins/Group_assign/Template/action_creation/params.php b/plugins/Group_assign/Template/action_creation/params.php
new file mode 100644
index 00000000..0506007b
--- /dev/null
+++ b/plugins/Group_assign/Template/action_creation/params.php
@@ -0,0 +1,73 @@
+<div class="page-header">
+ <h2><?= t('Define action parameters') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->url->href('ActionCreationController', 'save', array('project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <?= $this->form->hidden('event_name', $values) ?>
+ <?= $this->form->hidden('action_name', $values) ?>
+
+ <?= $this->form->label(t('Action'), 'action_name') ?>
+ <?= $this->form->select('action_name', $available_actions, $values, array(), array('disabled')) ?>
+
+ <?= $this->form->label(t('Event'), 'event_name') ?>
+ <?= $this->form->select('event_name', $events, $values, array(), array('disabled')) ?>
+
+ <?php foreach ($action_params as $param_name => $param_desc): ?>
+ <?php if ($this->text->contains($param_name, 'column_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $columns_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'user_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $users_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'group_id')): ?>
+ <?php $groups = $this->model->projectGroupRoleModel->getGroups($values['project_id']); ?>
+ <?php $groupnames = array_column($groups, 'name'); ?>
+ <?php $groupids = array_column($groups, 'id'); ?>
+ <?php array_unshift($groupnames, t('Unassigned')); ?>
+ <?php array_unshift($groupids, 0); ?>
+ <?php $groupvalues = array_combine($groupids, $groupnames); ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $groupvalues, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'check_box')): ?>
+ <?= $this->form->label(t('Options'), $param_name) ?>
+ <?= $this->form->checkbox('params['.$param_name.']', $param_desc, 1) ?>
+ <?php elseif ($this->text->contains($param_name, 'project_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $projects_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'color_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $colors_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'category_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $categories_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'link_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $links_list, $values) ?>
+ <?php elseif ($param_name === 'priority'): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $priorities_list, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'duration')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->number('params['.$param_name.']', $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'swimlane_id')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $swimlane_list, $values) ?>
+ <?php elseif (is_array($param_desc)): ?>
+ <?= $this->form->label(ucfirst($param_name), $param_name) ?>
+ <?= $this->form->select('params['.$param_name.']', $param_desc, $values) ?>
+ <?php elseif ($this->text->contains($param_name, 'multitasktitles')): ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->textarea('params['.$param_name.']', $values) ?>
+ <div class="form-help">
+ <?= t('Enter one line per task, or leave blank to copy Task Title and create only one subtask.') ?>
+ </div>
+ <?php else: ?>
+ <?= $this->form->label($param_desc, $param_name) ?>
+ <?= $this->form->text('params['.$param_name.']', $values) ?>
+ <?php endif ?>
+ <?php endforeach ?>
+
+ <?= $this->modal->submitButtons() ?>
+</form>
diff --git a/plugins/Group_assign/Template/board/filter.php b/plugins/Group_assign/Template/board/filter.php
new file mode 100644
index 00000000..322c483f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/filter.php
@@ -0,0 +1,12 @@
+<?php if (isset($grouplist) && !empty($grouplist)) : ?>
+</div>
+<div class="input-addon-item">
+ <div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon" title="<?= t('Group filters') ?>"><i class="fa fa-users fa-fw"></i><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <?php foreach ($grouplist as $group) : ?>
+ <li><a href="#" class="filter-helper" data-unique-filter='allassignees:"<?= $this->text->e($group) ?>"'><?= $this->text->e($group) ?></a></li>
+ <?php endforeach ?>
+ </ul>
+ </div>
+<?php endif ?> \ No newline at end of file
diff --git a/plugins/Group_assign/Template/board/group.php b/plugins/Group_assign/Template/board/group.php
new file mode 100644
index 00000000..6ac95c1f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/group.php
@@ -0,0 +1,7 @@
+<span>
+<?php if ($task['assigned_groupname']): ?>
+ <strong class="assigned-group-label"><?= t('Assigned Group:') ?></strong>
+ <span class="assigned-group" style="background-color: #<?= $this->task->groupColorExtension->getGroupColor($task['assigned_groupname']) ?>; color:<?= $this->task->groupColorExtension->getFontColor($this->task->groupColorExtension->getGroupColor($task['assigned_groupname'])) ?>;"><?= $this->text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?></span>
+ <br>
+<?php endif ?>
+</span>
diff --git a/plugins/Group_assign/Template/board/multi.php b/plugins/Group_assign/Template/board/multi.php
new file mode 100644
index 00000000..46084b5f
--- /dev/null
+++ b/plugins/Group_assign/Template/board/multi.php
@@ -0,0 +1,5 @@
+<?php if ($task['owner_ms'] > 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?>
+<strong class="assigned-other-label"><small><?= t('Other Assignees:') ?></small></strong>
+ <?= $this->helper->smallAvatarHelperExtend->miniMultiple($task['owner_ms'], 'avatar-inline') ?>
+<br>
+<?php endif ?>
diff --git a/plugins/Group_assign/Template/config/toggle.php b/plugins/Group_assign/Template/config/toggle.php
new file mode 100644
index 00000000..801a6a26
--- /dev/null
+++ b/plugins/Group_assign/Template/config/toggle.php
@@ -0,0 +1,4 @@
+<div class="panel">
+ <?= $this->form->radio('enable_am_group_management', 'Enable Group Managment for Application Managers' , 1, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==1) ?>
+ <?= $this->form->radio('enable_am_group_management', 'Disable Group Managment for Application Managers' , 2, isset($values['enable_am_group_management'])&& $values['enable_am_group_management']==2) ?>
+</div>
diff --git a/plugins/Group_assign/Template/header/user_dropdown.php b/plugins/Group_assign/Template/header/user_dropdown.php
new file mode 100644
index 00000000..e3a38787
--- /dev/null
+++ b/plugins/Group_assign/Template/header/user_dropdown.php
@@ -0,0 +1,46 @@
+<div class="dropdown">
+ <a href="#" class="dropdown-menu dropdown-menu-link-icon"><?= $this->avatar->currentUserSmall('avatar-inline') ?><i class="fa fa-caret-down"></i></a>
+ <ul>
+ <li class="no-hover"><strong><?= $this->text->e($this->user->getFullname()) ?></strong></li>
+ <li>
+ <?= $this->url->icon('tachometer', t('My dashboard'), 'DashboardController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('home', t('My profile'), 'UserViewController', 'show', array('user_id' => $this->user->getId())) ?>
+ </li>
+ <li>
+ <?= $this->url->icon('folder', t('Projects management'), 'ProjectListController', 'show') ?>
+ </li>
+ <?php if ($this->user->hasAccess('GroupListController', 'index') && $_SESSION['user']['role'] == 'app-manager'): ?>
+ <li>
+ <?= $this->url->icon('group', t('Groups management'), 'GroupListController', 'index') ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->user->hasAccess('UserListController', 'show')): ?>
+ <li>
+ <?= $this->url->icon('user', t('Users management'), 'UserListController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('group', t('Groups management'), 'GroupListController', 'index') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cubes', t('Plugins'), 'PluginController', 'show') ?>
+ </li>
+ <li>
+ <?= $this->url->icon('cog', t('Settings'), 'ConfigController', 'index') ?>
+ </li>
+ <?php endif ?>
+
+ <?= $this->hook->render('template:header:dropdown') ?>
+
+ <li>
+ <i class="fa fa-fw fa-life-ring" aria-hidden="true"></i>
+ <?= $this->url->doc(t('Documentation'), 'index') ?>
+ </li>
+ <?php if (! DISABLE_LOGOUT): ?>
+ <li>
+ <?= $this->url->icon('sign-out', t('Logout'), 'AuthController', 'logout') ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/plugins/Group_assign/Template/task/changes.php b/plugins/Group_assign/Template/task/changes.php
new file mode 100644
index 00000000..7a0d2720
--- /dev/null
+++ b/plugins/Group_assign/Template/task/changes.php
@@ -0,0 +1,92 @@
+<?php if (! empty($changes)): ?>
+ <ul>
+ <?php
+
+ foreach ($changes as $field => $value) {
+ switch ($field) {
+ case 'title':
+ echo '<li>'.t('New title: %s', $task['title']).'</li>';
+ break;
+ case 'owner_id':
+ if (empty($task['owner_id'])) {
+ echo '<li>'.t('The task is not assigned anymore').'</li>';
+ } else {
+ echo '<li>'.t('New assignee: %s', $task['assignee_name'] ?: $task['assignee_username']).'</li>';
+ }
+ break;
+ case 'category_id':
+ if (empty($task['category_id'])) {
+ echo '<li>'.t('There is no category now').'</li>';
+ } else {
+ echo '<li>'.t('New category: %s', $task['category_name']).'</li>';
+ }
+ break;
+ case 'color_id':
+ echo '<li>'.t('New color: %s', $this->text->in($task['color_id'], $this->task->getColors())).'</li>';
+ break;
+ case 'score':
+ echo '<li>'.t('New complexity: %d', $task['score']).'</li>';
+ break;
+ case 'date_due':
+ if (empty($task['date_due'])) {
+ echo '<li>'.t('The due date have been removed').'</li>';
+ } else {
+ echo '<li>'.t('New due date: ').$this->dt->datetime($task['date_due']).'</li>';
+ }
+ break;
+ case 'description':
+ if (empty($task['description'])) {
+ echo '<li>'.t('There is no description anymore').'</li>';
+ }
+ break;
+ case 'recurrence_status':
+ case 'recurrence_trigger':
+ case 'recurrence_factor':
+ case 'recurrence_timeframe':
+ case 'recurrence_basedate':
+ case 'recurrence_parent':
+ case 'recurrence_child':
+ echo '<li>'.t('Recurrence settings have been modified').'</li>';
+ break;
+ case 'time_spent':
+ echo '<li>'.t('Time spent changed: %sh', $task['time_spent']).'</li>';
+ break;
+ case 'time_estimated':
+ echo '<li>'.t('Time estimated changed: %sh', $task['time_estimated']).'</li>';
+ break;
+ case 'date_started':
+ if ($value != 0) {
+ echo '<li>'.t('Start date changed: ').$this->dt->datetime($task['date_started']).'</li>';
+ }
+ break;
+ case 'owner_gp':
+ if (empty($task['owner_gp'])) {
+ echo '<li>'.t('The task is not assigned to a group anymore').'</li>';
+ } else {
+ echo '<li>'.t('New group assigned: %s', $task['assigned_groupname']).'</li>';
+ }
+ break;
+ case 'owner_ms':
+ if (empty($task['owner_ms'])) {
+ echo '<li>'.t('The task is not assigned to multiple users anymore').'</li>';
+ } else {
+ echo '<li>'.t('The task has been assigned other users').'</li>';
+ }
+ break;
+ default:
+ echo '<li>'.t('The field "%s" have been updated', $field).'</li>';
+ }
+ }
+
+ ?>
+ </ul>
+
+ <?php if (! empty($changes['description'])): ?>
+ <p><strong><?= t('The description has been modified:') ?></strong></p>
+ <?php if (isset($public)): ?>
+ <div class="markdown"><?= $this->text->markdown($task['description'], true) ?></div>
+ <?php else: ?>
+ <div class="markdown"><?= $this->text->markdown($task['description']) ?></div>
+ <?php endif ?>
+ <?php endif ?>
+<?php endif ?>
diff --git a/plugins/Group_assign/Template/task/details.php b/plugins/Group_assign/Template/task/details.php
new file mode 100644
index 00000000..907739bc
--- /dev/null
+++ b/plugins/Group_assign/Template/task/details.php
@@ -0,0 +1,10 @@
+ <li>
+ <strong><?= t('Assigned Group:') ?></strong>
+ <span>
+ <?php if ($task['assigned_groupname']): ?>
+ <span class="assigned-group" style="background-color: #<?= $this->task->groupColorExtension->getGroupColor($task['assigned_groupname']) ?>; color:<?= $this->task->groupColorExtension->getFontColor($this->task->groupColorExtension->getGroupColor($task['assigned_groupname'])) ?>;"><?= $this->text->e($task['assigned_groupname'] ?: $task['owner_gp']) ?></span>
+ <?php else: ?>
+ <?= t('not assigned') ?>
+ <?php endif ?>
+ </span>
+ </li>
diff --git a/plugins/Group_assign/Template/task/multi.php b/plugins/Group_assign/Template/task/multi.php
new file mode 100644
index 00000000..5c1fcfb0
--- /dev/null
+++ b/plugins/Group_assign/Template/task/multi.php
@@ -0,0 +1,6 @@
+ <?php if ($task['owner_ms'] > 0 && count($this->task->multiselectMemberModel->getMembers($task['owner_ms'])) > 0) : ?>
+ <li>
+ <strong><?= t('Other Assignees:') ?></strong>
+ </li>
+ <?= $this->helper->smallAvatarHelperExtend->smallMultiple($task['owner_ms'], 'avatar-inline') ?>
+ <?php endif ?>
diff --git a/plugins/Group_assign/Template/task_creation/show.php b/plugins/Group_assign/Template/task_creation/show.php
new file mode 100644
index 00000000..81f610a8
--- /dev/null
+++ b/plugins/Group_assign/Template/task_creation/show.php
@@ -0,0 +1,50 @@
+<div class="page-header">
+ <h2><?= $this->text->e($project['name']) ?> &gt; <?= t('New task') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('GroupAssignTaskCreationController', 'save', array('plugin' => 'Group_assign', 'project_id' => $project['id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <div class="task-form-container">
+ <div class="task-form-main-column">
+ <?= $this->task->renderTitleField($values, $errors) ?>
+ <?= $this->task->renderDescriptionField($values, $errors) ?>
+ <?= $this->task->renderDescriptionTemplateDropdown($project['id']) ?>
+ <?= $this->task->renderTagField($project) ?>
+
+ <?= $this->hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderColorField($values) ?>
+ <?= $this->task->renderAssigneeField($users_list, $values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderGroupField($values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?>
+ <?= $this->task->renderCategoryField($categories_list, $values, $errors) ?>
+ <?= $this->task->renderSwimlaneField($swimlanes_list, $values, $errors) ?>
+ <?= $this->task->renderColumnField($columns_list, $values, $errors) ?>
+ <?= $this->task->renderPriorityField($project, $values) ?>
+
+ <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderDueDateField($values, $errors) ?>
+ <?= $this->task->renderStartDateField($values, $errors) ?>
+ <?= $this->task->renderTimeEstimatedField($values, $errors) ?>
+ <?= $this->task->renderTimeSpentField($values, $errors) ?>
+ <?= $this->task->renderScoreField($values, $errors) ?>
+ <?= $this->task->renderReferenceField($values, $errors) ?>
+
+ <?= $this->hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-bottom">
+ <?php if (! isset($duplicate)): ?>
+ <?= $this->form->checkbox('another_task', t('Create another task'), 1, isset($values['another_task']) && $values['another_task'] == 1) ?>
+ <?= $this->form->checkbox('duplicate_multiple_projects', t('Duplicate to multiple projects'), 1) ?>
+ <?php endif ?>
+
+ <?= $this->modal->submitButtons() ?>
+ </div>
+ </div>
+</form>
diff --git a/plugins/Group_assign/Template/task_modification/show.php b/plugins/Group_assign/Template/task_modification/show.php
new file mode 100644
index 00000000..31599ef6
--- /dev/null
+++ b/plugins/Group_assign/Template/task_modification/show.php
@@ -0,0 +1,43 @@
+<div class="page-header">
+ <h2><?= $this->text->e($project['name']) ?> &gt; <?= $this->text->e($task['title']) ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('GroupAssignTaskModificationController', 'update', array('plugin' => 'Group_assign', 'task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <div class="task-form-container">
+ <div class="task-form-main-column">
+ <?= $this->task->renderTitleField($values, $errors) ?>
+ <?= $this->task->renderDescriptionField($values, $errors) ?>
+ <?= $this->task->renderDescriptionTemplateDropdown($project['id']) ?>
+ <?= $this->task->renderTagField($project, $tags) ?>
+
+ <?= $this->hook->render('template:task:form:first-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderColorField($values) ?>
+ <?= $this->task->renderAssigneeField($users_list, $values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderGroupField($values, $errors) ?>
+ <?= $this->helper->newTaskHelper->renderMultiAssigneeField($users_list, $values) ?>
+ <?= $this->task->renderCategoryField($categories_list, $values, $errors) ?>
+ <?= $this->task->renderPriorityField($project, $values) ?>
+
+ <?= $this->hook->render('template:task:form:second-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-secondary-column">
+ <?= $this->task->renderDueDateField($values, $errors) ?>
+ <?= $this->task->renderStartDateField($values, $errors) ?>
+ <?= $this->task->renderTimeEstimatedField($values, $errors) ?>
+ <?= $this->task->renderTimeSpentField($values, $errors) ?>
+ <?= $this->task->renderScoreField($values, $errors) ?>
+ <?= $this->task->renderReferenceField($values, $errors) ?>
+
+ <?= $this->hook->render('template:task:form:third-column', array('values' => $values, 'errors' => $errors)) ?>
+ </div>
+
+ <div class="task-form-bottom">
+ <?= $this->modal->submitButtons() ?>
+ </div>
+ </div>
+</form>
diff --git a/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php b/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php
new file mode 100644
index 00000000..8c378474
--- /dev/null
+++ b/plugins/Group_assign/Test/Helper/NewTaskHelperTest.php
@@ -0,0 +1,34 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Core\Plugin\Loader;
+use Kanboard\Plugin\Group_assign\Helper\NewTaskHelper;
+
+class NewTaskHelperTest extends Base
+{
+ public function setUp()
+ {
+ parent::setUp();
+ $plugin = new Loader($this->container);
+ $plugin->scan();
+ }
+ public function testSelectPriority()
+ {
+ $helper = new NewTaskHelper($this->container);
+ $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '1', 'priority_start' => '5', 'priority_default' => '2'), array()));
+ $this->assertNotEmpty($helper->renderPriorityField(array('priority_end' => '3', 'priority_start' => '1', 'priority_default' => '2'), array()));
+ }
+ public function testFormatPriority()
+ {
+ $helper = new NewTaskHelper($this->container);
+ $this->assertEquals(
+ '<span class="task-priority" title="Task priority">P2</span>',
+ $helper->renderPriority(2)
+ );
+ $this->assertEquals(
+ '<span class="task-priority" title="Task priority">-P6</span>',
+ $helper->renderPriority(-6)
+ );
+ }
+}
diff --git a/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php b/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php
new file mode 100644
index 00000000..58ff4404
--- /dev/null
+++ b/plugins/Group_assign/Test/Model/NewTaskFinderModelTest.php
@@ -0,0 +1,184 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Core\Plugin\Loader;
+use Kanboard\Plugin\Group_assign\Model\NewTaskFinderModel;
+use Kanboard\Model\ColumnModel;
+use Kanboard\Model\TaskCreationModel;
+use Kanboard\Model\TaskFinderModel;
+use Kanboard\Model\ProjectModel;
+use Kanboard\Model\TaskModel;
+
+class NewTaskFinderModelTest extends Base
+{
+ public function setUp()
+ {
+ parent::setUp();
+ $plugin = new Loader($this->container);
+ $plugin->scan();
+ }
+
+ public function testGetDetails()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $categoryModel = new \Kanboard\Model\CategoryModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $categoryModel->create(array('project_id' => 1, 'name' => 'C1')));
+ $this->assertEquals(1, $taskCreationModel->create(array(
+ 'project_id' => 1,
+ 'title' => 'Task #1',
+ 'reference' => 'test',
+ 'description' => 'desc',
+ 'owner_id' => 1,
+ 'category_id' => 1,
+ )));
+ $task = $taskFinderModel->getDetails(1);
+ $this->assertEquals(1, $task['id']);
+ $this->assertEquals('test', $task['reference']);
+ $this->assertEquals('Task #1', $task['title']);
+ $this->assertEquals('desc', $task['description']);
+ $this->assertEquals(time(), $task['date_creation'], 'Delta', 1);
+ $this->assertEquals(time(), $task['date_modification'], 'Delta', 1);
+ $this->assertEquals(time(), $task['date_moved'], 'Delta', 1);
+ $this->assertEquals(0, $task['date_completed']);
+ $this->assertEquals(0, $task['date_due']);
+ $this->assertEquals(0, $task['date_started']);
+ $this->assertEquals(0, $task['time_estimated']);
+ $this->assertEquals(0, $task['time_spent']);
+ $this->assertEquals('yellow', $task['color_id']);
+ $this->assertEquals(1, $task['project_id']);
+ $this->assertEquals(1, $task['column_id']);
+ $this->assertEquals(1, $task['owner_id']);
+ $this->assertEquals(0, $task['creator_id']);
+ $this->assertEquals(1, $task['position']);
+ $this->assertEquals(TaskModel::STATUS_OPEN, $task['is_active']);
+ $this->assertEquals(0, $task['score']);
+ $this->assertEquals(1, $task['category_id']);
+ $this->assertEquals(0, $task['priority']);
+ $this->assertEquals(1, $task['swimlane_id']);
+ $this->assertEquals(TaskModel::RECURRING_STATUS_NONE, $task['recurrence_status']);
+ $this->assertEquals(TaskModel::RECURRING_TRIGGER_FIRST_COLUMN, $task['recurrence_trigger']);
+ $this->assertEquals(0, $task['recurrence_factor']);
+ $this->assertEquals(TaskModel::RECURRING_TIMEFRAME_DAYS, $task['recurrence_timeframe']);
+ $this->assertEquals(TaskModel::RECURRING_BASEDATE_DUEDATE, $task['recurrence_basedate']);
+ $this->assertEquals(0, $task['recurrence_parent']);
+ $this->assertEquals(0, $task['recurrence_child']);
+ $this->assertEquals('C1', $task['category_name']);
+ $this->assertEquals('Default swimlane', $task['swimlane_name']);
+ $this->assertEquals('Project #1', $task['project_name']);
+ $this->assertEquals('Backlog', $task['column_title']);
+ $this->assertEquals('admin', $task['assignee_username']);
+ $this->assertEquals('', $task['assignee_name']);
+ $this->assertEquals('', $task['creator_username']);
+ $this->assertEquals('', $task['creator_name']);
+ }
+ public function testGetTasksForDashboardWithHiddenColumn()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $columnModel = new ColumnModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'column_id' => 1, 'owner_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'column_id' => 2, 'owner_id' => 1)));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 1));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ $this->assertEquals(1, $tasks[0]['column_id']);
+ $this->assertTrue($columnModel->update(2, 'Test', 0, '', 0));
+ $tasks = $taskFinderModel->getUserQuery(1)->findAll();
+ $this->assertCount(2, $tasks);
+ }
+ public function testGetOverdueTasks()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasks();
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+ public function testGetOverdueTasksByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasksByProject(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(1, $tasks);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ }
+ public function testGetOverdueTasksByUser()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2, 'owner_id' => 1, 'date_due' => strtotime('-1 day'))));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 1, 'date_due' => strtotime('+1 day'))));
+ $this->assertEquals(4, $taskCreationModel->create(array('title' => 'Task #4', 'project_id' => 1, 'date_due' => 0)));
+ $this->assertEquals(5, $taskCreationModel->create(array('title' => 'Task #5', 'project_id' => 1)));
+ $tasks = $taskFinderModel->getOverdueTasksByUser(1);
+ $this->assertNotEmpty($tasks);
+ $this->assertTrue(is_array($tasks));
+ $this->assertCount(2, $tasks);
+ $this->assertEquals(1, $tasks[0]['id']);
+ $this->assertEquals('Task #1', $tasks[0]['title']);
+ $this->assertEquals(1, $tasks[0]['owner_id']);
+ $this->assertEquals(1, $tasks[0]['project_id']);
+ $this->assertEquals('Project #1', $tasks[0]['project_name']);
+ $this->assertEquals('admin', $tasks[0]['assignee_username']);
+ $this->assertEquals('', $tasks[0]['assignee_name']);
+ $this->assertEquals('Task #2', $tasks[1]['title']);
+ }
+ public function testCountByProject()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+ $this->assertEquals(3, $taskCreationModel->create(array('title' => 'Task #3', 'project_id' => 2)));
+ $this->assertEquals(1, $taskFinderModel->countByProjectId(1));
+ $this->assertEquals(2, $taskFinderModel->countByProjectId(2));
+ }
+ public function testGetProjectToken()
+ {
+ $taskCreationModel = new TaskCreationModel($this->container);
+ $taskFinderModel = new NewTaskFinderModel($this->container);
+ $projectModel = new ProjectModel($this->container);
+ $this->assertEquals(1, $projectModel->create(array('name' => 'Project #1')));
+ $this->assertEquals(2, $projectModel->create(array('name' => 'Project #2')));
+ $this->assertTrue($projectModel->enablePublicAccess(1));
+ $this->assertEquals(1, $taskCreationModel->create(array('title' => 'Task #1', 'project_id' => 1)));
+ $this->assertEquals(2, $taskCreationModel->create(array('title' => 'Task #2', 'project_id' => 2)));
+ $project = $projectModel->getById(1);
+ $this->assertEquals($project['token'], $taskFinderModel->getProjectToken(1));
+ $this->assertEmpty($taskFinderModel->getProjectToken(2));
+ }
+}
diff --git a/plugins/Group_assign/Test/PluginTest.php b/plugins/Group_assign/Test/PluginTest.php
new file mode 100644
index 00000000..c647786c
--- /dev/null
+++ b/plugins/Group_assign/Test/PluginTest.php
@@ -0,0 +1,19 @@
+<?php
+
+require_once 'tests/units/Base.php';
+
+use Kanboard\Plugin\Group_assign\Plugin;
+
+class PluginTest extends Base
+{
+ public function testPlugin()
+ {
+ $plugin = new Plugin($this->container);
+ $this->assertSame(null, $plugin->initialize());
+ $this->assertNotEmpty($plugin->getPluginName());
+ $this->assertNotEmpty($plugin->getPluginDescription());
+ $this->assertNotEmpty($plugin->getPluginAuthor());
+ $this->assertNotEmpty($plugin->getPluginVersion());
+ $this->assertNotEmpty($plugin->getPluginHomepage());
+ }
+}
diff --git a/plugins/Group_assign/_config.yml b/plugins/Group_assign/_config.yml
new file mode 100644
index 00000000..be854e84
--- /dev/null
+++ b/plugins/Group_assign/_config.yml
@@ -0,0 +1,3 @@
+theme: jekyll-theme-cayman
+plugins:
+ - jemoji
diff --git a/plugins/KanboardSearchPlugin/Controller/AdvancedSearchController.php b/plugins/KanboardSearchPlugin/Controller/AdvancedSearchController.php
new file mode 100644
index 00000000..73930fec
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Controller/AdvancedSearchController.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Kanboard\Plugin\KanboardSearchPlugin\Controller;
+
+
+use Kanboard\Controller\BaseController;
+
+
+class AdvancedSearchController extends BaseController
+{
+ /**
+ * Display the ASF settings page
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $this->response->html($this->helper->layout->config('KanboardSearchPlugin:config/advanced-search-filter', array(
+ 'db_size' => $this->configModel->getDatabaseSize(),
+ 'db_version' => $this->db->getDriver()->getDatabaseVersion(),
+ 'user_agent' => $this->request->getServerVariable('HTTP_USER_AGENT'),
+ 'title' => t('Settings').' &gt; '.t('Advanced Search Filter'),
+ )));
+ }
+
+ /**
+ * Save settings
+ *
+ */
+ public function save()
+ {
+ $values = $this->request->getValues();
+ $redirect = $this->request->getStringParam('redirect', 'index');
+ switch ($redirect) {
+ case 'index':
+ $values += array(
+ 'comment_search' => 0,
+ 'title_search' => 0,
+ 'description_search' => 0,
+ 'subtask_search' => 0,
+ 'attachment_search' => 0
+ );
+ break;
+ }
+
+ if ($this->configModel->save($values)) {
+ $this->languageModel->loadCurrentLanguage();
+ $this->flash->success(t('Settings saved successfully.'));
+ } else {
+ $this->flash->failure(t('Unable to save your settings.'));
+ }
+ $this->response->redirect($this->helper->url->to('AdvancedSearchController', 'index', array('plugin' => 'KanboardSearchPlugin')));
+ }
+}
diff --git a/plugins/KanboardSearchPlugin/Filter/AdvancedSearchFilter.php b/plugins/KanboardSearchPlugin/Filter/AdvancedSearchFilter.php
new file mode 100644
index 00000000..5809d37f
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Filter/AdvancedSearchFilter.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace Kanboard\Plugin\KanboardSearchPlugin\Filter;
+
+use Kanboard\Core\Filter\FilterInterface;
+use Kanboard\Filter\BaseFilter;
+use Kanboard\Model\CommentModel;
+use Kanboard\Model\TaskFileModel;
+use Kanboard\Model\SubtaskModel;
+use Kanboard\Model\TaskModel;
+use Kanboard\Model\ConfigModel;
+use PicoDb\Database;
+
+
+class AdvancedSearchFilter extends BaseFilter implements FilterInterface
+{
+
+ /**
+ * Database object
+ *
+ * @access private
+ * @var Database
+ */
+ private $db;
+
+ /**
+ * @var ConfigModel
+ */
+ private $config;
+
+ /**
+ * @var TaskFileModel
+ */
+ private $file;
+
+ /**
+ * Set database object
+ *
+ * @access public
+ * @param Database $db
+ * @return AdvancedSearchFilter
+ */
+ public function setDatabase(Database $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ /**
+ * Set configModel object
+ *
+ * @access public
+ * @param ConfigModel $config
+ * @return AdvancedSearchFilter
+ */
+ public function setConfigModel(ConfigModel $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Set TaskFileModel object
+ *
+ * @access public
+ * @param TaskFileModel $file
+ * @return AdvancedSearchFilter
+ */
+ public function setFileModel(TaskFileModel $file)
+ {
+ $this->file = $file;
+ return $this;
+ }
+
+ /**
+ * Get search attribute
+ *
+ * @access public
+ * @return string[]
+ */
+ public function getAttributes()
+ {
+ return array('title', 'comment', 'description', 'desc');
+ }
+
+ /**
+ * Apply filter
+ *
+ * @access public
+ * @return string
+ */
+ public function apply()
+ {
+ $commentTaskIds = $this->getTaskIdsWithGivenComment();
+ $titlesTaskIds = $this->getTaskIdsWithGivenTitles();
+ $descriptionTaskIds = $this->getTaskIdsWithGivenDescription();
+ $subtaskTitlesIds = $this->getTaskIdsWithGivenSubtaskTitles();
+ $attachmentIds = $this->getTaskIdsWithGivenAttachmentName();
+
+ $task_ids = array_merge($commentTaskIds, $titlesTaskIds, $descriptionTaskIds, $subtaskTitlesIds, $attachmentIds);
+
+ if (empty($task_ids)) {
+ $task_ids = array(-1);
+ }
+ $this->query->in(TaskModel::TABLE . '.id', $task_ids);
+
+ return $this;
+ }
+
+ /**
+ * Get task ids having this comment
+ *
+ * @access public
+ * @return array
+ */
+ protected function getTaskIdsWithGivenComment()
+ {
+ if($this->config->get('comment_search') == 1) {
+ return $this->db
+ ->table(CommentModel::TABLE)
+ ->ilike(CommentModel::TABLE . '.comment', '%' . $this->value . '%')
+ ->findAllByColumn(CommentModel::TABLE . '.task_id');
+ }
+ return array();
+ }
+
+
+ /**
+ * Get task ids having this description
+ *
+ * @access public
+ * @return array
+ */
+ protected function getTaskIdsWithGivenDescription()
+ {
+ if($this->config->get('description_search') == 1) {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->ilike(TaskModel::TABLE . '.description', '%' . $this->value . '%')
+ ->findAllByColumn(TaskModel::TABLE . '.id');
+ }
+ return array();
+ }
+
+
+ /**
+ * Get task ids having this title
+ *
+ * @access public
+ * @return array
+ */
+ private function getTaskIdsWithGivenTitles()
+ {
+ if($this->config->get('title_search') == 1) {
+ return $this->db
+ ->table(TaskModel::TABLE)
+ ->ilike(TaskModel::TABLE . '.title', '%' . $this->value . '%')
+ ->findAllByColumn(TaskModel::TABLE . '.id');
+ }
+ return array();
+ }
+
+
+ /**
+ * Get task ids having this Subtask title
+ *
+ * @access public
+ * @return array
+ */
+ private function getTaskIdsWithGivenSubtaskTitles()
+ {
+ if($this->config->get('subtask_search') == 1) {
+ return $this->db
+ ->table(SubtaskModel::TABLE)
+ ->ilike(SubtaskModel::TABLE . '.title', '%' . $this->value . '%')
+ ->findAllByColumn(SubtaskModel::TABLE . '.task_id');
+ }
+ return array();
+ }
+
+
+ /**
+ * Get task ids having this Attachment Name
+ *
+ * @access public
+ * @return array
+ */
+ private function getTaskIdsWithGivenAttachmentName()
+ {
+ if($this->config->get('attachment_search') == 1) {
+ return $this->db
+ ->table(TaskFileModel::TABLE)
+ ->ilike(TaskFileModel::TABLE . '.name', '%' . $this->value . '%')
+ ->findAllByColumn(TaskFileModel::TABLE . '.task_id');
+ }
+ return array();
+ }
+} \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/LICENSE b/plugins/KanboardSearchPlugin/LICENSE
new file mode 100644
index 00000000..d943a883
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 ipunkt Business Solutions
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/KanboardSearchPlugin/Locale/de_DE/translations.php b/plugins/KanboardSearchPlugin/Locale/de_DE/translations.php
new file mode 100644
index 00000000..23eecae2
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Locale/de_DE/translations.php
@@ -0,0 +1,13 @@
+<?php
+return array(
+ 'Advanced Search Filter' => 'Erweiterter Suchfilter',
+ 'Advanced Search Filter settings' => 'Erweiterte Suchfilterkonfiguration',
+ 'This plugin is used for advanced fulltext search within all Projects' => 'Dieses Plugin wird für die erweiterte Volltextsuche in allen Projekten verwendet',
+ 'Enable "Search in Comment"' => '"Suche in Kommentaren" aktivieren',
+ 'Enable "Search in Title"' => '"Suche im Titel" aktivieren',
+ 'Enable "Search in Description"' => '"Suche in Beschreibung" aktivieren',
+ 'Enable "Search in Subtask Title"' => '"Suche in Teilaufgaben" aktivieren',
+ 'Enable "Search in Attachments"' => '"Suche in Anhänge" aktivieren',
+ 'Save' => 'Speichern',
+ 'Settings' => 'Einstellungen',
+); \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/Locale/ru_RU/translations.php b/plugins/KanboardSearchPlugin/Locale/ru_RU/translations.php
new file mode 100644
index 00000000..411955a9
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Locale/ru_RU/translations.php
@@ -0,0 +1,13 @@
+<?php
+return array(
+ 'Advanced Search Filter' => 'Улучшенный поисковой фильтр',
+ 'Advanced Search Filter settings' => 'Настройки улучшеного поискового фильтра',
+ 'This plugin is used for advanced fulltext search within all Projects' => 'Этот плагин позволяет выполнять более детальный поиск во всех проэктах',
+ 'Enable "Search in Comment"' => 'активировать "Поиск в комментариях" ',
+ 'Enable "Search in Title"' => 'активировать "Поиск в заглавии" ',
+ 'Enable "Search in Description"' => 'активировать "Поиск по описанию" ',
+ 'Enable "Search in Subtask Title"' => 'активировать "Поиск в подзадачах" ',
+ 'Enable "Search in Attachments"' => 'активировать "Поиск во вложениях" ',
+ 'Save' => 'Сохранить',
+ 'Settings' => 'Настройки',
+); \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/Plugin.php b/plugins/KanboardSearchPlugin/Plugin.php
new file mode 100644
index 00000000..4b041d1a
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Plugin.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Kanboard\Plugin\KanboardSearchPlugin;
+
+use Kanboard\Core\Filter\LexerBuilder;
+use Kanboard\Core\Plugin\Base;
+use Kanboard\Core\Translator;
+use Kanboard\Plugin\KanboardSearchPlugin\Filter\AdvancedSearchFilter;
+
+class Plugin extends Base
+{
+ public function initialize()
+ {
+ $this->template->hook->attach("template:config:sidebar",
+ "KanboardSearchPlugin:config/sidebar");
+
+ $this->route->addRoute('settings/advancedsearch', 'AdvancedSearchController', 'index',
+ 'KanboardSearchPlugin');
+
+ $this->container->extend('taskLexer', function ($taskLexer, $c) {
+ /**
+ * @var LexerBuilder $taskLexer
+ */
+ $taskLexer->withFilter(AdvancedSearchFilter::getInstance()
+ ->setDatabase($c['db'])
+ ->setConfigModel($this->configModel)
+ ->setFileModel($this->taskFileModel), true);
+
+ return $taskLexer;
+ });
+ }
+
+ public function onStartup()
+ {
+ Translator::load($this->languageModel->getCurrentLanguage(), __DIR__.'/Locale');
+ }
+
+ public function getPluginName()
+ {
+ return 'KanboardSearchPlugin';
+ }
+
+ public function getPluginDescription()
+ {
+ return t('This plugin is used for advanced fulltext search within all Projects');
+ }
+
+ public function getPluginAuthor()
+ {
+ return 'ipunkt Business Solutions';
+ }
+
+ public function getPluginVersion()
+ {
+ return '1.0.0';
+ }
+
+ public function getPluginHomepage()
+ {
+ return 'https://www.ipunkt.biz/unternehmen/opensource';
+ }
+} \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/README.md b/plugins/KanboardSearchPlugin/README.md
new file mode 100644
index 00000000..0a964e0b
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/README.md
@@ -0,0 +1,6 @@
+# Kanboard Search Plugin
+
+
+This Plugin is created for advanced fulltext search within all Projects.
+The search will be performed in Task title, Task description, Task comments, Subtask title and in Attachments.
+All 5 filters can be turned on/off in Kanboard Settings page. \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/Template/config/advanced-search-filter.php b/plugins/KanboardSearchPlugin/Template/config/advanced-search-filter.php
new file mode 100644
index 00000000..c8dddb8b
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Template/config/advanced-search-filter.php
@@ -0,0 +1,30 @@
+<div class="page-header">
+ <h2><?= t('Advanced Search Filter') ?></h2>
+</div>
+<form method="post" action="<?= $this->url->href('AdvancedSearchController', 'save', ['plugin' => 'KanboardSearchPlugin']) ?>" autocomplete="off">
+ <?= $this->form->csrf() ?>
+
+ <fieldset>
+ <?= $this->form->checkbox('comment_search', t('Enable "Search in Comment"'), 1, $values['comment_search'] == 1) ?>
+ </fieldset>
+ <fieldset>
+ <?= $this->form->checkbox('title_search', t('Enable "Search in Title"'), 1, $values['title_search'] == 1) ?>
+ </fieldset>
+
+ <fieldset>
+ <?= $this->form->checkbox('description_search', t('Enable "Search in Description"'), 1, $values['description_search'] == 1) ?>
+ </fieldset>
+
+ <fieldset>
+ <?= $this->form->checkbox('subtask_search', t('Enable "Search in Subtask Title"'), 1, $values['subtask_search'] == 1) ?>
+ </fieldset>
+ <fieldset>
+ <?= $this->form->checkbox('attachment_search', t('Enable "Search in Attachments"'), 1, $values['attachment_search'] == 1) ?>
+ </fieldset>
+
+ <?= $this->hook->render('template:config:advanced-search-filter', array('values' => $values, 'errors' => $errors)) ?>
+
+ <div class="form-actions">
+ <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
+ </div>
+</form> \ No newline at end of file
diff --git a/plugins/KanboardSearchPlugin/Template/config/sidebar.php b/plugins/KanboardSearchPlugin/Template/config/sidebar.php
new file mode 100644
index 00000000..6c2c9b5e
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/Template/config/sidebar.php
@@ -0,0 +1,4 @@
+<li <?= $this->app->checkMenuSelection('AdvancedSearchController', 'index') ?>>
+ <?= $this->url->link(t('Advanced Search Filter settings'), 'AdvancedSearchController', 'index',
+ ['plugin' => 'KanboardSearchPlugin']) ?>
+</li>
diff --git a/plugins/KanboardSearchPlugin/docker-compose.yml b/plugins/KanboardSearchPlugin/docker-compose.yml
new file mode 100644
index 00000000..dd5fd508
--- /dev/null
+++ b/plugins/KanboardSearchPlugin/docker-compose.yml
@@ -0,0 +1,27 @@
+version: '2'
+services:
+ kanboard:
+ image: kanboard/kanboard:latest
+ ports:
+ - "80:80"
+ volumes:
+ - .:/var/www/app/plugins/KanboardSearchPlugin
+ - .:/usr/share/php7
+ environment:
+ DATABASE_URL: mysql://kb:kb-secret@db/kanboard
+ pma:
+ image: phpmyadmin/phpmyadmin
+ ports:
+ - "8000:80"
+ environment:
+ PMA_HOST: db
+ PMA_USER: kb
+ PMA_PASSWORD: kb-secret
+ db:
+ image: mariadb:latest
+ command: --default-authentication-plugin=mysql_native_password
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: kanboard
+ MYSQL_USER: kb
+ MYSQL_PASSWORD: kb-secret \ No newline at end of file
diff --git a/plugins/Moon b/plugins/Moon
new file mode 160000
+Subproject c56f60cbdba52cd0a24b853e7de15e208abddcb