summaryrefslogtreecommitdiff
path: root/assets/js/components
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js/components')
-rw-r--r--assets/js/components/accordion.js7
-rw-r--r--assets/js/components/calendar.js48
-rw-r--r--assets/js/components/chart-project-avg-time-column.js41
-rw-r--r--assets/js/components/chart-project-burndown.js49
-rw-r--r--assets/js/components/chart-project-cumulative-flow.js48
-rw-r--r--assets/js/components/chart-project-lead-cycle-time.js47
-rw-r--r--assets/js/components/chart-project-task-distribution.js15
-rw-r--r--assets/js/components/chart-project-time-comparison.js23
-rw-r--r--assets/js/components/chart-project-user-distribution.js15
-rw-r--r--assets/js/components/chart-task-time-column.js42
-rw-r--r--assets/js/components/comment-highlight.js21
-rw-r--r--assets/js/components/confirm-buttons.js58
-rw-r--r--assets/js/components/external-task-view.js12
-rw-r--r--assets/js/components/file-upload.js195
-rw-r--r--assets/js/components/form-export.js9
-rw-r--r--assets/js/components/image-slideshow.js161
-rw-r--r--assets/js/components/keyboard-shortcuts.js131
-rw-r--r--assets/js/components/modal.js39
-rw-r--r--assets/js/components/project-creation-select-options.js9
-rw-r--r--assets/js/components/project-select-role.js72
-rw-r--r--assets/js/components/screenshot.js122
-rw-r--r--assets/js/components/select-dropdown-autocomplete.js290
-rw-r--r--assets/js/components/session-check.js10
-rw-r--r--assets/js/components/submit-buttons.js102
-rw-r--r--assets/js/components/submit-cancel.js30
-rw-r--r--assets/js/components/suggest-menu.js225
-rw-r--r--assets/js/components/task-move-position.js224
-rw-r--r--assets/js/components/text-editor.js163
28 files changed, 2086 insertions, 122 deletions
diff --git a/assets/js/components/accordion.js b/assets/js/components/accordion.js
new file mode 100644
index 00000000..2f617fb5
--- /dev/null
+++ b/assets/js/components/accordion.js
@@ -0,0 +1,7 @@
+KB.onClick('.accordion-toggle', function (e) {
+ var sectionElement = KB.dom(e.target).parent('.accordion-section');
+
+ if (sectionElement) {
+ KB.dom(sectionElement).toggleClass('accordion-collapsed');
+ }
+});
diff --git a/assets/js/components/calendar.js b/assets/js/components/calendar.js
new file mode 100644
index 00000000..d07c911d
--- /dev/null
+++ b/assets/js/components/calendar.js
@@ -0,0 +1,48 @@
+KB.component('calendar', function (containerElement, options) {
+
+ this.render = function () {
+ var calendar = $(containerElement);
+
+ calendar.fullCalendar({
+ locale: $("body").data("js-lang"),
+ editable: true,
+ eventLimit: true,
+ defaultView: "month",
+ 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() {
+ 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');
+ });
+ }
+ });
+ };
+});
diff --git a/assets/js/components/chart-project-avg-time-column.js b/assets/js/components/chart-project-avg-time-column.js
new file mode 100644
index 00000000..18564367
--- /dev/null
+++ b/assets/js/components/chart-project-avg-time-column.js
@@ -0,0 +1,41 @@
+KB.component('chart-project-avg-time-column', function (containerElement, options) {
+
+ this.render = function () {
+ var metrics = options.metrics;
+ var plots = [options.label];
+ var categories = [];
+
+ for (var column_id in metrics) {
+ plots.push(metrics[column_id].average);
+ categories.push(metrics[column_id].title);
+ }
+
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
+ c3.generate({
+ data: {
+ columns: [plots],
+ type: 'bar'
+ },
+ bar: {
+ width: {
+ ratio: 0.5
+ }
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: categories
+ },
+ y: {
+ tick: {
+ format: KB.utils.formatDuration
+ }
+ }
+ },
+ legend: {
+ show: false
+ }
+ });
+ };
+}); \ No newline at end of file
diff --git a/assets/js/components/chart-project-burndown.js b/assets/js/components/chart-project-burndown.js
new file mode 100644
index 00000000..abcab925
--- /dev/null
+++ b/assets/js/components/chart-project-burndown.js
@@ -0,0 +1,49 @@
+KB.component('chart-project-burndown', function (containerElement, options) {
+
+ this.render = function () {
+ var metrics = options.metrics;
+ var columns = [[options.labelTotal]];
+ var categories = [];
+ var inputFormat = d3.time.format("%Y-%m-%d");
+ var outputFormat = d3.time.format(options.dateFormat);
+
+ for (var i = 0; i < metrics.length; i++) {
+
+ for (var j = 0; j < metrics[i].length; j++) {
+
+ if (i === 0) {
+ columns.push([metrics[i][j]]);
+ } else {
+ columns[j + 1].push(metrics[i][j]);
+
+ if (j > 0) {
+
+ if (columns[0][i] === undefined) {
+ columns[0].push(0);
+ }
+
+ columns[0][i] += metrics[i][j];
+ }
+
+ if (j === 0) {
+ categories.push(outputFormat(inputFormat.parse(metrics[i][j])));
+ }
+ }
+ }
+ }
+
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
+ c3.generate({
+ data: {
+ columns: columns
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: categories
+ }
+ }
+ });
+ };
+});
diff --git a/assets/js/components/chart-project-cumulative-flow.js b/assets/js/components/chart-project-cumulative-flow.js
new file mode 100644
index 00000000..7b258230
--- /dev/null
+++ b/assets/js/components/chart-project-cumulative-flow.js
@@ -0,0 +1,48 @@
+KB.component('chart-project-cumulative-flow', function (containerElement, options) {
+
+ this.render = function () {
+ var metrics = options.metrics;
+ var columns = [];
+ var groups = [];
+ var categories = [];
+ var inputFormat = d3.time.format("%Y-%m-%d");
+ var outputFormat = d3.time.format(options.dateFormat);
+
+ for (var i = 0; i < metrics.length; i++) {
+
+ for (var j = 0; j < metrics[i].length; j++) {
+
+ if (i === 0) {
+ columns.push([metrics[i][j]]);
+
+ if (j > 0) {
+ groups.push(metrics[i][j]);
+ }
+ } else {
+
+ columns[j].push(metrics[i][j]);
+
+ if (j === 0) {
+ categories.push(outputFormat(inputFormat.parse(metrics[i][j])));
+ }
+ }
+ }
+ }
+
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
+ c3.generate({
+ data: {
+ columns: columns,
+ type: 'area-spline',
+ groups: [groups]
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: categories
+ }
+ }
+ });
+ };
+});
diff --git a/assets/js/components/chart-project-lead-cycle-time.js b/assets/js/components/chart-project-lead-cycle-time.js
new file mode 100644
index 00000000..208f7ce6
--- /dev/null
+++ b/assets/js/components/chart-project-lead-cycle-time.js
@@ -0,0 +1,47 @@
+KB.component('chart-project-lead-cycle-time', function (containerElement, options) {
+
+ this.render = function () {
+ var metrics = options.metrics;
+ var cycle = [options.labelCycle];
+ var lead = [options.labelLead];
+ var categories = [];
+
+ var types = {};
+ types[options.labelCycle] = 'area';
+ types[options.labelLead] = 'area-spline';
+
+ var colors = {};
+ colors[options.labelLead] = '#afb42b';
+ colors[options.labelCycle] = '#4e342e';
+
+ for (var i = 0; i < metrics.length; i++) {
+ cycle.push(parseInt(metrics[i].avg_cycle_time));
+ lead.push(parseInt(metrics[i].avg_lead_time));
+ categories.push(metrics[i].day);
+ }
+
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
+ c3.generate({
+ data: {
+ columns: [
+ lead,
+ cycle
+ ],
+ types: types,
+ colors: colors
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: categories
+ },
+ y: {
+ tick: {
+ format: KB.utils.formatDuration
+ }
+ }
+ }
+ });
+ };
+});
diff --git a/assets/js/components/chart-project-task-distribution.js b/assets/js/components/chart-project-task-distribution.js
index b5d712ce..3f48d230 100644
--- a/assets/js/components/chart-project-task-distribution.js
+++ b/assets/js/components/chart-project-task-distribution.js
@@ -1,18 +1,19 @@
-Vue.component('chart-project-task-distribution', {
- props: ['metrics'],
- template: '<div id="chart"></div>',
- ready: function () {
+KB.component('chart-project-task-distribution', function (containerElement, options) {
+
+ this.render = function () {
var columns = [];
- for (var i = 0; i < this.metrics.length; i++) {
- columns.push([this.metrics[i].column_title, this.metrics[i].nb_tasks]);
+ for (var i = 0; i < options.metrics.length; i++) {
+ columns.push([options.metrics[i].column_title, options.metrics[i].nb_tasks]);
}
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
c3.generate({
data: {
columns: columns,
type : 'donut'
}
});
- }
+ };
});
diff --git a/assets/js/components/chart-project-time-comparison.js b/assets/js/components/chart-project-time-comparison.js
index 6fbdb919..e4b31a13 100644
--- a/assets/js/components/chart-project-time-comparison.js
+++ b/assets/js/components/chart-project-time-comparison.js
@@ -1,17 +1,18 @@
-Vue.component('chart-project-time-comparison', {
- props: ['metrics', 'labelSpent', 'labelEstimated', 'labelClosed', 'labelOpen'],
- template: '<div id="chart"></div>',
- ready: function () {
- var spent = [this.labelSpent];
- var estimated = [this.labelEstimated];
+KB.component('chart-project-time-comparison', function (containerElement, options) {
+
+ this.render = function () {
+ var spent = [options.labelSpent];
+ var estimated = [options.labelEstimated];
var categories = [];
- for (var status in this.metrics) {
- spent.push(this.metrics[status].time_spent);
- estimated.push(this.metrics[status].time_estimated);
- categories.push(status === 'open' ? this.labelOpen : this.labelClosed);
+ for (var status in options.metrics) {
+ spent.push(options.metrics[status].time_spent);
+ estimated.push(options.metrics[status].time_estimated);
+ categories.push(status === 'open' ? options.labelOpen : options.labelClosed);
}
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
c3.generate({
data: {
columns: [spent, estimated],
@@ -32,5 +33,5 @@ Vue.component('chart-project-time-comparison', {
show: true
}
});
- }
+ };
});
diff --git a/assets/js/components/chart-project-user-distribution.js b/assets/js/components/chart-project-user-distribution.js
index a569e0be..d3c88d56 100644
--- a/assets/js/components/chart-project-user-distribution.js
+++ b/assets/js/components/chart-project-user-distribution.js
@@ -1,18 +1,19 @@
-Vue.component('chart-project-user-distribution', {
- props: ['metrics'],
- template: '<div id="chart"></div>',
- ready: function () {
+KB.component('chart-project-user-distribution', function (containerElement, options) {
+
+ this.render = function () {
var columns = [];
- for (var i = 0; i < this.metrics.length; i++) {
- columns.push([this.metrics[i].user, this.metrics[i].nb_tasks]);
+ for (var i = 0; i < options.metrics.length; i++) {
+ columns.push([options.metrics[i].user, options.metrics[i].nb_tasks]);
}
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
+
c3.generate({
data: {
columns: columns,
type : 'donut'
}
});
- }
+ };
});
diff --git a/assets/js/components/chart-task-time-column.js b/assets/js/components/chart-task-time-column.js
new file mode 100644
index 00000000..89709952
--- /dev/null
+++ b/assets/js/components/chart-task-time-column.js
@@ -0,0 +1,42 @@
+KB.component('chart-task-time-column', function (containerElement, options) {
+
+ this.render = function () {
+ var metrics = options.metrics;
+ var plots = [options.label];
+ var categories = [];
+
+ for (var i = 0; i < metrics.length; i++) {
+ plots.push(metrics[i].time_spent);
+ categories.push(metrics[i].title);
+ }
+
+ KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart-task-time-column').build());
+
+ c3.generate({
+ bindto: '#chart-task-time-column',
+ data: {
+ columns: [plots],
+ type: 'bar'
+ },
+ bar: {
+ width: {
+ ratio: 0.5
+ }
+ },
+ axis: {
+ x: {
+ type: 'category',
+ categories: categories
+ },
+ y: {
+ tick: {
+ format: KB.utils.formatDuration
+ }
+ }
+ },
+ legend: {
+ show: false
+ }
+ });
+ };
+}); \ No newline at end of file
diff --git a/assets/js/components/comment-highlight.js b/assets/js/components/comment-highlight.js
new file mode 100644
index 00000000..cc0b9bc0
--- /dev/null
+++ b/assets/js/components/comment-highlight.js
@@ -0,0 +1,21 @@
+KB.on('dom.ready', function () {
+ function highlightComment() {
+ if (window.location.hash.indexOf('#comment-') === 0) {
+ var commentElement = KB.find(window.location.hash);
+
+ if (commentElement) {
+ var commentsElement = document.querySelectorAll('.comment');
+
+ commentsElement.forEach(function (element) {
+ KB.dom(element).removeClass('comment-highlighted');
+ });
+
+ commentElement.addClass('comment-highlighted');
+ }
+ }
+ }
+
+ window.addEventListener('hashchange', highlightComment);
+
+ highlightComment();
+});
diff --git a/assets/js/components/confirm-buttons.js b/assets/js/components/confirm-buttons.js
new file mode 100644
index 00000000..da31df55
--- /dev/null
+++ b/assets/js/components/confirm-buttons.js
@@ -0,0 +1,58 @@
+KB.component('confirm-buttons', function (containerElement, options) {
+ var isLoading = false;
+
+ function onSubmit() {
+ isLoading = true;
+ KB.find('#modal-confirm-button').replace(buildButton());
+ KB.http.get(options.url);
+ }
+
+ function onCancel() {
+ KB.trigger('modal.close');
+ }
+
+ function onStop() {
+ isLoading = false;
+ KB.find('#modal-confirm-button').replace(buildButton());
+ }
+
+ function buildButton() {
+ var button = KB.dom('button')
+ .click(onSubmit)
+ .attr('id', 'modal-confirm-button')
+ .attr('type', 'button')
+ .attr('class', 'btn btn-red');
+
+ if (isLoading) {
+ button
+ .disable()
+ .add(KB.dom('i').attr('class', 'fa fa-spinner fa-pulse').build())
+ .text(' ')
+ ;
+ }
+
+ if (options.tabindex) {
+ button.attr('tabindex', options.tabindex);
+ }
+
+ return button
+ .text(options.submitLabel)
+ .build();
+ }
+
+ this.render = function () {
+ KB.on('modal.stop', onStop);
+ KB.on('modal.close', function () {
+ KB.removeListener('modal.stop', onStop);
+ });
+
+ var element = KB.dom('div')
+ .attr('class', 'form-actions')
+ .add(buildButton())
+ .text(' ' + options.orLabel + ' ')
+ .add(KB.dom('a').attr('href', '#').click(onCancel).text(options.cancelLabel).build())
+ .build();
+
+ containerElement.appendChild(element);
+ };
+});
diff --git a/assets/js/components/external-task-view.js b/assets/js/components/external-task-view.js
new file mode 100644
index 00000000..d402640b
--- /dev/null
+++ b/assets/js/components/external-task-view.js
@@ -0,0 +1,12 @@
+KB.component('external-task-view', function (containerElement, options) {
+
+ this.render = function () {
+ $.ajax({
+ cache: false,
+ url: options.url,
+ success: function(data) {
+ KB.dom(containerElement).html('<div id="external-task-view">' + data + '</div>');
+ }
+ });
+ };
+});
diff --git a/assets/js/components/file-upload.js b/assets/js/components/file-upload.js
new file mode 100644
index 00000000..f4445a44
--- /dev/null
+++ b/assets/js/components/file-upload.js
@@ -0,0 +1,195 @@
+KB.component('file-upload', function (containerElement, options) {
+ var inputFileElement = null;
+ var dropzoneElement = null;
+ var files = [];
+ var currentFileIndex = 0;
+
+ function onProgress(e) {
+ if (e.lengthComputable) {
+ var progress = e.loaded / e.total;
+ var percentage = Math.floor(progress * 100);
+
+ KB.find('#file-progress-' + currentFileIndex).attr('value', progress);
+ KB.find('#file-percentage-' + currentFileIndex).replaceText('(' + percentage + '%)');
+ }
+ }
+
+ function onError() {
+ var errorElement = KB.dom('div').addClass('file-error').text(options.labelUploadError).build();
+ KB.find('#file-item-' + currentFileIndex).add(errorElement);
+ }
+
+ function onComplete() {
+ currentFileIndex++;
+
+ if (currentFileIndex < files.length) {
+ KB.http.uploadFile(options.url, files[currentFileIndex], onProgress, onComplete, onError);
+ } else {
+ KB.trigger('modal.stop');
+ KB.trigger('modal.hide');
+
+ var alertElement = KB.dom('div')
+ .addClass('alert')
+ .addClass('alert-success')
+ .text(options.labelSuccess)
+ .build();
+
+ var buttonElement = KB.dom('button')
+ .attr('type', 'button')
+ .addClass('btn')
+ .addClass('btn-blue')
+ .click(onCloseWindow)
+ .text(options.labelCloseSuccess)
+ .build();
+
+ KB.dom(dropzoneElement).replace(KB.dom('div').add(alertElement).add(buttonElement).build());
+ }
+ }
+
+ function onCloseWindow() {
+ window.location.reload();
+ }
+
+ function onSubmit() {
+ currentFileIndex = 0;
+ uploadFiles();
+ }
+
+ function onFileChange() {
+ files = inputFileElement.files;
+ showFiles();
+ }
+
+ function onClickFileBrowser() {
+ files = [];
+ currentFileIndex = 0;
+ inputFileElement.click();
+ }
+
+ function onDragOver(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ function onDrop(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ files = e.dataTransfer.files;
+ showFiles();
+ }
+
+ function uploadFiles() {
+ if (files.length > 0) {
+ KB.http.uploadFile(options.url, files[currentFileIndex], onProgress, onComplete, onError);
+ }
+ }
+
+ function showFiles() {
+ if (files.length > 0) {
+ KB.trigger('modal.enable');
+
+ KB.dom(dropzoneElement)
+ .empty()
+ .add(buildFileListElement());
+ } else {
+ KB.trigger('modal.disable');
+
+ KB.dom(dropzoneElement)
+ .empty()
+ .add(buildInnerDropzoneElement());
+ }
+ }
+
+ function buildFileInputElement() {
+ return KB.dom('input')
+ .attr('id', 'file-input-element')
+ .attr('type', 'file')
+ .attr('name', 'files[]')
+ .attr('multiple', true)
+ .on('change', onFileChange)
+ .hide()
+ .build();
+ }
+
+ function buildInnerDropzoneElement() {
+ var dropzoneLinkElement = KB.dom('a')
+ .attr('href', '#')
+ .text(options.labelChooseFiles)
+ .click(onClickFileBrowser)
+ .build();
+
+ return KB.dom('div')
+ .attr('id', 'file-dropzone-inner')
+ .text(options.labelDropzone + ' ' + options.labelOr + ' ')
+ .add(dropzoneLinkElement)
+ .build();
+ }
+
+ function buildDropzoneElement() {
+ var dropzoneElement = KB.dom('div')
+ .attr('id', 'file-dropzone')
+ .add(buildInnerDropzoneElement())
+ .build();
+
+ dropzoneElement.ondragover = onDragOver;
+ dropzoneElement.ondrop = onDrop;
+ dropzoneElement.ondragover = onDragOver;
+
+ return dropzoneElement;
+ }
+
+ function buildFileListItem(index) {
+ var isOversize = false;
+ var progressElement = KB.dom('progress')
+ .attr('id', 'file-progress-' + index)
+ .attr('value', 0)
+ .build();
+
+ var percentageElement = KB.dom('span')
+ .attr('id', 'file-percentage-' + index)
+ .text('(0%)')
+ .build();
+
+ var itemElement = KB.dom('li')
+ .attr('id', 'file-item-' + index)
+ .add(progressElement)
+ .text(' ' + files[index].name + ' ')
+ .add(percentageElement);
+
+ if (files[index].size > options.maxSize) {
+ itemElement.add(KB.dom('div').addClass('file-error').text(options.labelOversize).build());
+ isOversize = true;
+ }
+
+ if (isOversize) {
+ KB.trigger('modal.disable');
+ }
+
+ return itemElement.build();
+ }
+
+ function buildFileListElement() {
+ var fileListElement = KB.dom('ul')
+ .attr('id', 'file-list')
+ .build();
+
+ for (var i = 0; i < files.length; i++) {
+ fileListElement.appendChild(buildFileListItem(i));
+ }
+
+ return fileListElement;
+ }
+
+ this.render = function () {
+ KB.on('modal.submit', onSubmit);
+ KB.on('modal.close', function () {
+ KB.removeListener('modal.submit', onSubmit);
+ });
+
+ inputFileElement = buildFileInputElement();
+ dropzoneElement = buildDropzoneElement();
+ containerElement.appendChild(inputFileElement);
+ containerElement.appendChild(dropzoneElement);
+ };
+});
diff --git a/assets/js/components/form-export.js b/assets/js/components/form-export.js
new file mode 100644
index 00000000..c46fede1
--- /dev/null
+++ b/assets/js/components/form-export.js
@@ -0,0 +1,9 @@
+KB.onClick('.js-form-export', function(e) {
+ var formElement = document.querySelector('#modal-content form');
+ var fromElement = formElement.querySelector('#form-from');
+ var toElement = formElement.querySelector('#form-to');
+
+ if (fromElement.value !== '' && toElement.value !== '') {
+ formElement.submit();
+ }
+});
diff --git a/assets/js/components/image-slideshow.js b/assets/js/components/image-slideshow.js
new file mode 100644
index 00000000..9db72bb9
--- /dev/null
+++ b/assets/js/components/image-slideshow.js
@@ -0,0 +1,161 @@
+KB.component('image-slideshow', function (containerElement, options) {
+ var currentImage;
+
+ function onKeyDown(e) {
+ switch (KB.utils.getKey(e)) {
+ case 'Escape':
+ destroySlide();
+ break;
+ case 'ArrowRight':
+ renderNextSlide();
+ break;
+ case 'ArrowLeft':
+ renderPreviousSlide();
+ break;
+ }
+ }
+
+ function onOverlayClick(element) {
+ if (element.matches('.slideshow-next-icon')) {
+ renderNextSlide();
+ } else if (element.matches('.slideshow-previous-icon')) {
+ renderPreviousSlide();
+ } else if (element.matches('.slideshow-download-icon')) {
+ window.location.href = element.href;
+ } else {
+ destroySlide();
+ }
+ }
+
+ function onClick(element) {
+ var imageId = KB.dom(element).data('imageId');
+ var image = getImage(imageId);
+
+ renderSlide(image);
+ }
+
+ function renderNextSlide() {
+ destroySlide();
+
+ for (var i = 0; i < options.images.length; i++) {
+ if (options.images[i].id === currentImage.id) {
+ var index = i + 1;
+
+ if (index >= options.images.length) {
+ index = 0;
+ }
+
+ currentImage = options.images[index];
+ break;
+ }
+ }
+
+ renderSlide();
+ }
+
+ function renderPreviousSlide() {
+ destroySlide();
+
+ for (var i = 0; i < options.images.length; i++) {
+ if (options.images[i].id === currentImage.id) {
+ var index = i - 1;
+
+ if (index < 0) {
+ index = options.images.length - 1;
+ }
+
+ currentImage = options.images[index];
+ break;
+ }
+ }
+
+ renderSlide();
+ }
+
+ function renderSlide() {
+ var closeElement = KB.dom('div')
+ .attr('class', 'fa fa-window-close slideshow-icon slideshow-close-icon')
+ .build();
+
+ var downloadElement = KB.dom('a')
+ .attr('class', 'fa fa-download slideshow-icon slideshow-download-icon')
+ .attr('href', getUrl(currentImage, 'download'))
+ .build();
+
+ var previousElement = KB.dom('div')
+ .attr('class', 'fa fa-chevron-circle-left slideshow-icon slideshow-previous-icon')
+ .build();
+
+ var nextElement = KB.dom('div')
+ .attr('class', 'fa fa-chevron-circle-right slideshow-icon slideshow-next-icon')
+ .build();
+
+ var imageElement = KB.dom('img')
+ .attr('src', getUrl(currentImage, 'image'))
+ .attr('alt', currentImage.name)
+ .attr('title', currentImage.name)
+ .style('maxHeight', (window.innerHeight - 50) + 'px')
+ .build();
+
+ var captionElement = KB.dom('figcaption')
+ .text(currentImage.name)
+ .build();
+
+ var figureElement = KB.dom('figure')
+ .add(imageElement)
+ .add(captionElement)
+ .build();
+
+ var overlayElement = KB.dom('div')
+ .addClass('image-slideshow-overlay')
+ .add(closeElement)
+ .add(downloadElement)
+ .add(previousElement)
+ .add(nextElement)
+ .add(figureElement)
+ .click(onOverlayClick)
+ .build();
+
+ document.body.appendChild(overlayElement);
+ document.addEventListener('keydown', onKeyDown, false);
+ }
+
+ function destroySlide() {
+ var overlayElement = KB.find('.image-slideshow-overlay');
+
+ if (overlayElement !== null) {
+ document.removeEventListener('keydown', onKeyDown, false);
+ overlayElement.remove();
+ }
+ }
+
+ function getImage(imageId) {
+ for (var i = 0; i < options.images.length; i++) {
+ if (options.images[i].id === imageId) {
+ return options.images[i];
+ }
+ }
+
+ return null;
+ }
+
+ function getUrl(image, type) {
+ var regex = new RegExp(options.regex, 'g');
+ return options.url[type].replace(regex, image.id);
+ }
+
+ function buildThumbnailElement(image) {
+ return KB.dom('img')
+ .attr('src', getUrl(image, 'thumbnail'))
+ .attr('alt', image.name)
+ .attr('title', image.name)
+ .data('imageId', image.id)
+ .click(onClick)
+ .build();
+ }
+
+ this.render = function () {
+ currentImage = options.image;
+ containerElement.appendChild(buildThumbnailElement(currentImage));
+ };
+});
diff --git a/assets/js/components/keyboard-shortcuts.js b/assets/js/components/keyboard-shortcuts.js
new file mode 100644
index 00000000..cffcd790
--- /dev/null
+++ b/assets/js/components/keyboard-shortcuts.js
@@ -0,0 +1,131 @@
+KB.keyboardShortcuts = function () {
+ function goToLink (selector) {
+ if (! KB.modal.isOpen()) {
+ var element = KB.find(selector);
+
+ if (element !== null) {
+ window.location = element.attr('href');
+ }
+ }
+ }
+
+ function submitForm() {
+ if (KB.modal.isOpen()) {
+ KB.modal.submitForm();
+ } else {
+ var forms = $("form");
+
+ if (forms.length == 1) {
+ forms.submit();
+ } else if (forms.length > 1) {
+ if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
+ $(document.activeElement).parents("form").submit();
+ }
+ }
+ }
+ }
+
+ KB.onKey('?', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('body').data('keyboardShortcutUrl'));
+ }
+ });
+
+ KB.onKey('Escape', function () {
+ if (! KB.exists('#suggest-menu')) {
+ KB.trigger('modal.close');
+ _KB.get("Dropdown").close();
+ }
+ });
+
+ KB.onKey('Enter', submitForm, true, true);
+ KB.onKey('Enter', submitForm, true, false, true);
+
+ KB.onKey('b', function () {
+ if (! KB.modal.isOpen()) {
+ KB.trigger('board.selector.open');
+ }
+ });
+
+ if (KB.exists('#board')) {
+ KB.onKey('c', function () {
+ if (! KB.modal.isOpen()) {
+ _KB.get('BoardHorizontalScrolling').toggle();
+ }
+ });
+
+ KB.onKey('s', function () {
+ if (! KB.modal.isOpen()) {
+ _KB.get('BoardCollapsedMode').toggle();
+ }
+ });
+
+ KB.onKey('n', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('#board').data('taskCreationUrl'), 'large', false);
+ }
+ });
+ }
+
+ if (KB.exists('#task-view')) {
+ KB.onKey('e', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('#task-view').data('editUrl'), 'large', false);
+ }
+ });
+
+ KB.onKey('c', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('#task-view').data('commentUrl'), 'medium', false);
+ }
+ });
+
+ KB.onKey('s', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('#task-view').data('subtaskUrl'), 'medium', false);
+ }
+ });
+
+ KB.onKey('l', function () {
+ if (! KB.modal.isOpen()) {
+ KB.modal.open(KB.find('#task-view').data('internalLinkUrl'), 'medium', false);
+ }
+ });
+ }
+
+ KB.onKey('f', function () {
+ if (! KB.modal.isOpen()) {
+ KB.focus('#form-search');
+ }
+ });
+
+ KB.onKey('r', function () {
+ if (! KB.modal.isOpen()) {
+ var reset = $(".filter-reset").data("filter");
+ var input = $("#form-search");
+
+ input.val(reset);
+ $("form.search").submit();
+ }
+ });
+
+ KB.onKey('v+o', function () {
+ goToLink('a.view-overview');
+ });
+
+ KB.onKey('v+b', function () {
+ goToLink('a.view-board');
+ });
+
+ KB.onKey('v+c', function () {
+ goToLink('a.view-calendar');
+ });
+
+ KB.onKey('v+l', function () {
+ goToLink('a.view-listing');
+ });
+
+ KB.onKey('v+g', function () {
+ goToLink('a.view-gantt');
+ });
+};
diff --git a/assets/js/components/modal.js b/assets/js/components/modal.js
new file mode 100644
index 00000000..ce6f34de
--- /dev/null
+++ b/assets/js/components/modal.js
@@ -0,0 +1,39 @@
+(function () {
+ function getLink(e) {
+ if (e.target.tagName === 'I') {
+ return e.target.parentNode.getAttribute('href');
+ }
+
+ return e.target.getAttribute('href');
+ }
+
+ KB.onClick('.js-modal-large', function (e) {
+ KB.modal.open(getLink(e), 'large', false);
+ });
+
+ KB.onClick('.js-modal-medium', function (e) {
+ KB.modal.open(getLink(e), 'medium', false);
+ });
+
+ KB.onClick('.js-modal-small', function (e) {
+ KB.modal.open(getLink(e), 'small', false);
+ });
+
+ KB.onClick('.js-modal-confirm', function (e) {
+ KB.modal.open(getLink(e), 'small');
+ });
+
+ KB.onClick('.js-modal-close', function () {
+ KB.modal.close();
+ });
+
+ KB.onClick('.js-modal-replace', function (e) {
+ var link = getLink(e);
+
+ if (KB.modal.isOpen()) {
+ KB.modal.replace(link);
+ } else {
+ window.location.href = link;
+ }
+ });
+}());
diff --git a/assets/js/components/project-creation-select-options.js b/assets/js/components/project-creation-select-options.js
new file mode 100644
index 00000000..618ec86c
--- /dev/null
+++ b/assets/js/components/project-creation-select-options.js
@@ -0,0 +1,9 @@
+KB.onChange('.js-project-creation-select-options', function (element) {
+ var projectId = element.value;
+
+ if (projectId === '0') {
+ KB.find('.js-project-creation-options').hide();
+ } else {
+ KB.find('.js-project-creation-options').show();
+ }
+});
diff --git a/assets/js/components/project-select-role.js b/assets/js/components/project-select-role.js
new file mode 100644
index 00000000..904d1dbd
--- /dev/null
+++ b/assets/js/components/project-select-role.js
@@ -0,0 +1,72 @@
+KB.component('project-select-role', function (containerElement, options) {
+ var isLoading = false;
+ var isSuccess = false;
+ var isError = false;
+ var componentElement;
+
+ function onChange(element) {
+ isLoading = true;
+ options.role = element.value;
+ replaceComponentElement();
+ updateRole();
+ }
+
+ function updateRole() {
+ KB.http.postJson(options.url, {
+ id: options.id,
+ role: options.role
+ }).success(function () {
+ isLoading = false;
+ isSuccess = true;
+ replaceComponentElement();
+ }).error(function () {
+ isLoading = false;
+ isSuccess = false;
+ isError = true;
+ replaceComponentElement();
+ });
+ }
+
+ function replaceComponentElement() {
+ KB.dom(componentElement).remove();
+ componentElement = buildComponentElement();
+ containerElement.appendChild(componentElement);
+ }
+
+ function buildComponentElement() {
+ var roles = [];
+ var container = KB.dom('div');
+
+ for (var role in options.roles) {
+ if (options.roles.hasOwnProperty(role)) {
+ var item = {value: role, text: options.roles[role]};
+
+ if (options.role === role) {
+ item.selected = 'selected';
+ }
+
+ roles.push(item);
+ }
+ }
+
+ container.add(KB.dom('select').change(onChange).for('option', roles).build());
+
+ if (isLoading) {
+ container.text(' ');
+ container.add(KB.dom('i').attr('class', 'fa fa-spinner fa-pulse fa-fw').build());
+ } else if (isSuccess) {
+ container.text(' ');
+ container.add(KB.dom('i').attr('class', 'fa fa-check fa-fw icon-fade-out icon-success').build());
+ } else if (isError) {
+ container.text(' ');
+ container.add(KB.dom('i').attr('class', 'fa fa-check fa-fw icon-fade-out icon-error').build());
+ }
+
+ return container.build();
+ }
+
+ this.render = function () {
+ componentElement = buildComponentElement();
+ containerElement.appendChild(componentElement);
+ };
+});
diff --git a/assets/js/components/screenshot.js b/assets/js/components/screenshot.js
new file mode 100644
index 00000000..a8acd64e
--- /dev/null
+++ b/assets/js/components/screenshot.js
@@ -0,0 +1,122 @@
+KB.component('screenshot', function (containerElement) {
+ var pasteCatcher = null;
+ var inputElement = null;
+
+ function onFileLoaded(e) {
+ createImage(e.target.result);
+ }
+
+ function onPaste(e) {
+ // Firefox doesn't have the property e.clipboardData.items (only Chrome)
+ if (e.clipboardData && e.clipboardData.items) {
+ var items = e.clipboardData.items;
+
+ if (items) {
+ for (var i = 0; i < items.length; i++) {
+ // Find an image in pasted elements
+ if (items[i].type.indexOf("image") !== -1) {
+ var blob = items[i].getAsFile();
+
+ // Get the image as base64 data
+ var reader = new FileReader();
+ reader.onload = onFileLoaded;
+ reader.readAsDataURL(blob);
+ }
+ }
+ }
+ } else {
+
+ // Handle Firefox
+ setTimeout(checkInput, 100);
+ }
+ }
+
+ function initialize() {
+ destroy();
+
+ if (! window.Clipboard) {
+ // Insert the content editable at the top to avoid scrolling down in the board view
+ pasteCatcher = document.createElement('div');
+ pasteCatcher.id = 'screenshot-pastezone';
+ pasteCatcher.contentEditable = true;
+ pasteCatcher.style.opacity = 0;
+ pasteCatcher.style.position = 'fixed';
+ pasteCatcher.style.top = 0;
+ pasteCatcher.style.right = 0;
+ pasteCatcher.style.width = 0;
+ document.body.insertBefore(pasteCatcher, document.body.firstChild);
+
+ pasteCatcher.focus();
+
+ // Set the focus when clicked anywhere in the document
+ document.addEventListener('click', setFocus);
+
+ // Set the focus when clicked in screenshot dropzone
+ document.getElementById('screenshot-zone').addEventListener('click', setFocus);
+ }
+
+ window.addEventListener('paste', onPaste, false);
+ }
+
+ function destroy() {
+ if (KB.exists('#screenshot-pastezone')) {
+ KB.find('#screenshot-pastezone').remove();
+ }
+
+ document.removeEventListener('click', setFocus);
+ pasteCatcher = null;
+ }
+
+ function setFocus() {
+ if (pasteCatcher !== null) {
+ pasteCatcher.focus();
+ }
+ }
+
+ function checkInput() {
+ var child = pasteCatcher.childNodes[0];
+
+ if (child) {
+ // If the user pastes an image, the src attribute
+ // will represent the image as a base64 encoded string.
+ if (child.tagName === 'IMG') {
+ createImage(child.src);
+ }
+ }
+
+ pasteCatcher.innerHTML = '';
+ }
+
+ function createImage(blob) {
+ var pastedImage = new Image();
+ pastedImage.src = blob;
+
+ // Send the image content to the form variable
+ pastedImage.onload = function() {
+ var sourceSplit = blob.split('base64,');
+ inputElement.value = sourceSplit[1];
+ };
+
+ var zone = document.getElementById('screenshot-zone');
+ zone.innerHTML = '';
+ zone.className = 'screenshot-pasted';
+ zone.appendChild(pastedImage);
+
+ destroy();
+ initialize();
+ }
+
+ KB.on('modal.close', function () {
+ destroy();
+ });
+
+ this.render = function () {
+ inputElement = KB.dom('input')
+ .attr('type', 'hidden')
+ .attr('name', 'screenshot')
+ .build();
+
+ containerElement.appendChild(inputElement);
+ initialize();
+ };
+});
diff --git a/assets/js/components/select-dropdown-autocomplete.js b/assets/js/components/select-dropdown-autocomplete.js
new file mode 100644
index 00000000..c2c36f5b
--- /dev/null
+++ b/assets/js/components/select-dropdown-autocomplete.js
@@ -0,0 +1,290 @@
+KB.component('select-dropdown-autocomplete', function(containerElement, options) {
+ var componentElement, inputElement, inputHiddenElement, chevronIconElement, loadingIconElement;
+
+ function onLoadingStart() {
+ KB.dom(loadingIconElement).show();
+ KB.dom(chevronIconElement).hide();
+ }
+
+ function onLoadingStop() {
+ KB.dom(loadingIconElement).hide();
+ KB.dom(chevronIconElement).show();
+ }
+
+ function onScroll() {
+ var menuElement = KB.find('#select-dropdown-menu');
+
+ if (menuElement) {
+ var componentPosition = componentElement.getBoundingClientRect();
+ menuElement.style('top', (document.body.scrollTop + componentPosition.bottom) + 'px');
+ }
+ }
+
+ function onKeyDown(e) {
+ switch (KB.utils.getKey(e)) {
+ case 'Escape':
+ inputElement.value = '';
+ destroyDropdownMenu();
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ moveUp();
+ break;
+ case 'ArrowDown':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ moveDown();
+ break;
+ case 'Enter':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ insertSelectedItem();
+ break;
+ }
+ }
+
+ function onInputChanged() {
+ destroyDropdownMenu();
+ renderDropdownMenu();
+ }
+
+ function onItemMouseOver(element) {
+ if (KB.dom(element).hasClass('select-dropdown-menu-item')) {
+ KB.find('.select-dropdown-menu-item.active').removeClass('active');
+ KB.dom(element).addClass('active');
+ }
+ }
+
+ function onItemClick() {
+ insertSelectedItem();
+ }
+
+ function onDocumentClick(e) {
+ if (! containerElement.contains(e.target)) {
+ inputElement.value = '';
+ destroyDropdownMenu();
+ }
+ }
+
+ function toggleDropdownMenu() {
+ var menuElement = KB.find('#select-dropdown-menu');
+
+ if (menuElement === null) {
+ renderDropdownMenu();
+ } else {
+ destroyDropdownMenu();
+ }
+ }
+
+ function insertSelectedItem() {
+ var element = KB.find('.select-dropdown-menu-item.active');
+ var value = element.data('value');
+ inputHiddenElement.value = value;
+ inputElement.value = options.items[value];
+ destroyDropdownMenu();
+
+ if (options.redirect) {
+ window.location = options.redirect.url.replace(new RegExp(options.redirect.regex, 'g'), value);
+ } else if (options.replace) {
+ onLoadingStart();
+ KB.modal.replace(options.replace.url.replace(new RegExp(options.replace.regex, 'g'), value));
+ }
+ }
+
+ function resetSelection() {
+ var elements = document.querySelectorAll('.select-dropdown-menu-item');
+
+ for (var i = 0; i < elements.length; i++) {
+ if (KB.dom(elements[i]).hasClass('active')) {
+ KB.dom(elements[i]).removeClass('active');
+ break;
+ }
+ }
+
+ return {items: elements, index: i};
+ }
+
+ function moveUp() {
+ var result = resetSelection();
+
+ if (result.index > 0) {
+ result.index = result.index - 1;
+ }
+
+ KB.dom(result.items[result.index]).addClass('active');
+ }
+
+ function moveDown() {
+ var result = resetSelection();
+
+ if (result.index < result.items.length - 1) {
+ result.index++;
+ }
+
+ KB.dom(result.items[result.index]).addClass('active');
+ }
+
+ function buildItems(items) {
+ var elements = [];
+
+ for (var key in items) {
+ if (items.hasOwnProperty(key)) {
+ elements.push({
+ 'class': 'select-dropdown-menu-item',
+ 'text': items[key],
+ 'data-label': items[key],
+ 'data-value': key
+ });
+ }
+ }
+
+ if (options.sortByKeys) {
+ elements.sort(function (a, b) {
+ var value1 = a['data-value'].toLowerCase();
+ var value2 = b['data-value'].toLowerCase();
+ return value1 < value2 ? -1 : (value1 > value2 ? 1 : 0);
+ });
+ } else {
+ elements.sort(function (a, b) {
+ var value1 = a['data-label'].toLowerCase();
+ var value2 = b['data-label'].toLowerCase();
+ return value1 < value2 ? -1 : (value1 > value2 ? 1 : 0);
+ });
+ }
+
+ return elements;
+ }
+
+ function filterItems(text, items) {
+ var filteredItems = [];
+ var hasActiveItem = false;
+
+ for (var i = 0; i < items.length; i++) {
+ if (text.length === 0 || items[i]['data-label'].toLowerCase().indexOf(text.toLowerCase()) === 0) {
+ var item = items[i];
+
+ if (typeof options.defaultValue !== 'undefined' && String(options.defaultValue) === item['data-value']) {
+ item.class += ' active';
+ hasActiveItem = true;
+ }
+
+ filteredItems.push(item);
+ }
+ }
+
+ if (! hasActiveItem && filteredItems.length > 0) {
+ filteredItems[0].class += ' active';
+ }
+
+ return filteredItems;
+ }
+
+ function buildDropdownMenu() {
+ var itemElements = filterItems(inputElement.value, buildItems(options.items));
+ var componentPosition = componentElement.getBoundingClientRect();
+
+ if (itemElements.length === 0) {
+ return null;
+ }
+
+ return KB.dom('ul')
+ .attr('id', 'select-dropdown-menu')
+ .style('top', (document.body.scrollTop + componentPosition.bottom) + 'px')
+ .style('left', componentPosition.left + 'px')
+ .style('width', componentPosition.width + 'px')
+ .style('maxHeight', (window.innerHeight - componentPosition.bottom - 20) + 'px')
+ .mouseover(onItemMouseOver)
+ .click(onItemClick)
+ .for('li', itemElements)
+ .build();
+ }
+
+ function destroyDropdownMenu() {
+ var menuElement = KB.find('#select-dropdown-menu');
+
+ if (menuElement !== null) {
+ menuElement.remove();
+ }
+
+ document.removeEventListener('keydown', onKeyDown, false);
+ document.removeEventListener('click', onDocumentClick, false);
+ }
+
+ function renderDropdownMenu() {
+ var element = buildDropdownMenu();
+
+ if (element !== null) {
+ document.body.appendChild(element);
+ }
+
+ document.addEventListener('keydown', onKeyDown, false);
+ document.addEventListener('click', onDocumentClick, false);
+ }
+
+ function getPlaceholderValue() {
+ if (options.defaultValue && options.defaultValue in options.items) {
+ return options.items[options.defaultValue];
+ }
+
+ if (options.placeholder) {
+ return options.placeholder;
+ }
+
+ return '';
+ }
+
+ this.render = function () {
+ KB.on('select.dropdown.loading.start', onLoadingStart);
+ KB.on('select.dropdown.loading.stop', onLoadingStop);
+
+ KB.on('modal.close', function () {
+ KB.removeListener('select.dropdown.loading.start', onLoadingStart);
+ KB.removeListener('select.dropdown.loading.stop', onLoadingStop);
+ });
+
+ chevronIconElement = KB.dom('i')
+ .attr('class', 'fa fa-chevron-down select-dropdown-chevron')
+ .click(toggleDropdownMenu)
+ .build();
+
+ loadingIconElement = KB.dom('span')
+ .hide()
+ .addClass('select-loading-icon')
+ .add(KB.dom('i').attr('class', 'fa fa-spinner fa-pulse').build())
+ .build();
+
+ inputHiddenElement = KB.dom('input')
+ .attr('type', 'hidden')
+ .attr('name', options.name)
+ .attr('value', options.defaultValue || '')
+ .build();
+
+ inputElement = KB.dom('input')
+ .attr('type', 'text')
+ .attr('placeholder', getPlaceholderValue())
+ .addClass('select-dropdown-input')
+ .style('width', (containerElement.offsetWidth - 30) + 'px')
+ .on('focus', toggleDropdownMenu)
+ .on('input', onInputChanged, true)
+ .build();
+
+ componentElement = KB.dom('div')
+ .addClass('select-dropdown-input-container')
+ .add(inputHiddenElement)
+ .add(inputElement)
+ .add(chevronIconElement)
+ .add(loadingIconElement)
+ .build();
+
+ containerElement.appendChild(componentElement);
+
+ if (options.onFocus) {
+ options.onFocus.forEach(function (eventName) {
+ KB.on(eventName, function() { inputElement.focus(); });
+ });
+ }
+
+ window.addEventListener('scroll', onScroll, false);
+ };
+});
diff --git a/assets/js/components/session-check.js b/assets/js/components/session-check.js
new file mode 100644
index 00000000..01f6f620
--- /dev/null
+++ b/assets/js/components/session-check.js
@@ -0,0 +1,10 @@
+KB.interval(60, function () {
+ var statusUrl = KB.find('body').data('statusUrl');
+ var loginUrl = KB.find('body').data('loginUrl');
+
+ if (KB.find('.form-login') === null) {
+ KB.http.get(statusUrl).error(function () {
+ window.location = loginUrl;
+ });
+ }
+});
diff --git a/assets/js/components/submit-buttons.js b/assets/js/components/submit-buttons.js
new file mode 100644
index 00000000..2745841a
--- /dev/null
+++ b/assets/js/components/submit-buttons.js
@@ -0,0 +1,102 @@
+KB.component('submit-buttons', function (containerElement, options) {
+ var isLoading = false;
+ var isDisabled = options.disabled || false;
+ var submitLabel = options.submitLabel;
+ var formActionElement = null;
+
+ function onSubmit() {
+ isLoading = true;
+ KB.find('#modal-submit-button').replace(buildButton());
+ KB.trigger('modal.submit');
+ }
+
+ function onCancel() {
+ KB.trigger('modal.close');
+ }
+
+ function onStop() {
+ isLoading = false;
+ KB.find('#modal-submit-button').replace(buildButton());
+ }
+
+ function onDisable() {
+ isLoading = false;
+ isDisabled = true;
+ KB.find('#modal-submit-button').replace(buildButton());
+ }
+
+ function onEnable() {
+ isLoading = false;
+ isDisabled = false;
+ KB.find('#modal-submit-button').replace(buildButton());
+ }
+
+ function onHide() {
+ KB.dom(formActionElement).hide();
+ }
+
+ function onUpdateSubmitLabel(eventData) {
+ submitLabel = eventData.submitLabel;
+ KB.find('#modal-submit-button').replace(buildButton());
+ }
+
+ function buildButton() {
+ var button = KB.dom('button')
+ .attr('id', 'modal-submit-button')
+ .attr('type', 'submit')
+ .attr('class', 'btn btn-' + (options.color || 'blue'));
+
+ if (KB.modal.isOpen()) {
+ button.click(onSubmit);
+ }
+
+ if (options.tabindex) {
+ button.attr('tabindex', options.tabindex);
+ }
+
+ if (isLoading) {
+ button
+ .disable()
+ .add(KB.dom('i').attr('class', 'fa fa-spinner fa-pulse').build())
+ .text(' ')
+ ;
+ }
+
+ if (isDisabled) {
+ button.disable();
+ }
+
+ return button
+ .text(submitLabel)
+ .build();
+ }
+
+ this.render = function () {
+ KB.on('modal.stop', onStop);
+ KB.on('modal.disable', onDisable);
+ KB.on('modal.enable', onEnable);
+ KB.on('modal.hide', onHide);
+ KB.on('modal.submit.label', onUpdateSubmitLabel);
+
+ KB.on('modal.close', function () {
+ KB.removeListener('modal.stop', onStop);
+ KB.removeListener('modal.disable', onDisable);
+ KB.removeListener('modal.enable', onEnable);
+ KB.removeListener('modal.hide', onHide);
+ KB.removeListener('modal.submit.label', onUpdateSubmitLabel);
+ });
+
+ var formActionElementBuilder = KB.dom('div')
+ .attr('class', 'form-actions')
+ .add(buildButton());
+
+ if (KB.modal.isOpen()) {
+ formActionElementBuilder
+ .text(' ' + options.orLabel + ' ')
+ .add(KB.dom('a').attr('href', '#').click(onCancel).text(options.cancelLabel).build());
+ }
+
+ formActionElement = formActionElementBuilder.build();
+ containerElement.appendChild(formActionElement);
+ };
+});
diff --git a/assets/js/components/submit-cancel.js b/assets/js/components/submit-cancel.js
deleted file mode 100644
index 1950c060..00000000
--- a/assets/js/components/submit-cancel.js
+++ /dev/null
@@ -1,30 +0,0 @@
-Vue.component('submit-cancel', {
- props: ['labelButton', 'labelOr', 'labelCancel', 'callback'],
- template: '<div class="form-actions">' +
- '<button type="button" class="btn btn-blue" @click="onSubmit" :disabled="isLoading">' +
- '<span v-show="isLoading"><i class="fa fa-spinner fa-pulse"></i> </span>' +
- '{{ labelButton }}' +
- '</button> ' +
- '{{ labelOr }} <a href="#" v-on:click.prevent="onCancel">{{ labelCancel }}</a>' +
- '</div>'
- ,
- data: function () {
- return {
- loading: false
- };
- },
- computed: {
- isLoading: function () {
- return this.loading;
- }
- },
- methods: {
- onSubmit: function () {
- this.loading = true;
- this.callback();
- },
- onCancel: function () {
- _KB.get('Popover').close();
- }
- }
-});
diff --git a/assets/js/components/suggest-menu.js b/assets/js/components/suggest-menu.js
new file mode 100644
index 00000000..7f3e6f62
--- /dev/null
+++ b/assets/js/components/suggest-menu.js
@@ -0,0 +1,225 @@
+KB.component('suggest-menu', function(containerElement, options) {
+
+ function onKeyDown(e) {
+ switch (KB.utils.getKey(e)) {
+ case 'Escape':
+ destroy();
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ moveUp();
+ break;
+ case 'ArrowDown':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ moveDown();
+ break;
+ case 'Enter':
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ insertSelectedItem();
+ break;
+ }
+ }
+
+ function onClick() {
+ insertSelectedItem();
+ }
+
+ function onMouseOver(element) {
+ if (KB.dom(element).hasClass('suggest-menu-item')) {
+ KB.find('.suggest-menu-item.active').removeClass('active');
+ KB.dom(element).addClass('active');
+ }
+ }
+
+ function insertSelectedItem() {
+ containerElement.focus();
+
+ var element = KB.find('.suggest-menu-item.active');
+ var value = element.data('value');
+ var trigger = element.data('trigger');
+ var content = containerElement.value;
+ var text = getLastWord(containerElement);
+ var substitute = trigger + value + ' ';
+ var selectionPosition = KB.utils.getSelectionPosition(containerElement);
+ var before = content.substring(0, selectionPosition.selectionStart - text.length);
+ var after = content.substring(selectionPosition.selectionEnd);
+ var position = before.length + substitute.length;
+
+ containerElement.value = before + substitute + after;
+ containerElement.setSelectionRange(position, position);
+
+ destroy();
+ }
+
+ function getLastWord(element) {
+ var lines = element.value.substring(0, element.selectionEnd).split("\n");
+ var lastLine = lines[lines.length - 1];
+ var words = lastLine.split(' ');
+ return words[words.length - 1];
+ }
+
+ function getParentElement() {
+ var selectors = ['#modal-content form', '#modal-content', 'body'];
+
+ for (var i = 0; i < selectors.length; i++) {
+ var element = document.querySelector(selectors[i]);
+
+ if (element !== null) {
+ return element;
+ }
+ }
+
+ return null;
+ }
+
+ function resetSelection() {
+ var elements = document.querySelectorAll('.suggest-menu-item');
+
+ for (var i = 0; i < elements.length; i++) {
+ if (KB.dom(elements[i]).hasClass('active')) {
+ KB.dom(elements[i]).removeClass('active');
+ break;
+ }
+ }
+
+ return {items: elements, index: i};
+ }
+
+ function moveUp() {
+ var result = resetSelection();
+
+ if (result.index > 0) {
+ result.index = result.index - 1;
+ }
+
+ KB.dom(result.items[result.index]).addClass('active');
+ }
+
+ function moveDown() {
+ var result = resetSelection();
+
+ if (result.index < result.items.length - 1) {
+ result.index++;
+ }
+
+ KB.dom(result.items[result.index]).addClass('active');
+ }
+
+ function destroy() {
+ var element = KB.find('#suggest-menu');
+
+ if (element !== null) {
+ element.remove();
+ }
+
+ document.removeEventListener('keydown', onKeyDown, false);
+ }
+
+ function search(element) {
+ var text = getLastWord(element);
+ var trigger = getTrigger(text, options.triggers);
+
+ destroy();
+
+ if (trigger !== null) {
+ fetchItems(trigger, text.substring(trigger.length), options.triggers[trigger]);
+ }
+ }
+
+ function getTrigger(text, triggers) {
+ for (var trigger in triggers) {
+ if (triggers.hasOwnProperty(trigger) && text.indexOf(trigger) === 0) {
+ return trigger;
+ }
+ }
+
+ return null;
+ }
+
+ function fetchItems(trigger, text, params) {
+ if (typeof params === 'string') {
+ var regex = new RegExp('SEARCH_TERM', 'g');
+ var url = params.replace(regex, text);
+
+ KB.http.get(url).success(function (response) {
+ onItemFetched(trigger, text, response);
+ });
+ } else {
+ onItemFetched(trigger, text, params);
+ }
+ }
+
+ function onItemFetched(trigger, text, items) {
+ items = filterItems(text, items);
+
+ if (items.length > 0) {
+ renderMenu(buildItems(trigger, items));
+ }
+ }
+
+ function filterItems(text, items) {
+ var filteredItems = [];
+
+ if (text.length === 0) {
+ return items;
+ }
+
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].value.toLowerCase().indexOf(text.toLowerCase()) === 0) {
+ filteredItems.push(items[i]);
+ }
+ }
+
+ return filteredItems;
+ }
+
+ function buildItems(trigger, items) {
+ var elements = [];
+
+ for (var i = 0; i < items.length; i++) {
+ var className = 'suggest-menu-item';
+
+ if (i === 0) {
+ className += ' active';
+ }
+
+ elements.push({
+ class: className,
+ html: items[i].html,
+ 'data-value': items[i].value,
+ 'data-trigger': trigger
+ });
+ }
+
+ return elements;
+ }
+
+ function renderMenu(items) {
+ var parentElement = getParentElement();
+ var caretPosition = getCaretCoordinates(containerElement, containerElement.selectionEnd);
+ var left = caretPosition.left + containerElement.offsetLeft - containerElement.scrollLeft;
+ var top = caretPosition.top + containerElement.offsetTop - containerElement.scrollTop + 16;
+
+ document.addEventListener('keydown', onKeyDown, false);
+
+ var menu = KB.dom('ul')
+ .attr('id', 'suggest-menu')
+ .click(onClick)
+ .mouseover(onMouseOver)
+ .style('left', left + 'px')
+ .style('top', top + 'px')
+ .for('li', items)
+ .build();
+
+ parentElement.appendChild(menu);
+ }
+
+ this.render = function () {
+ containerElement.addEventListener('input', function () {
+ search(this);
+ });
+ };
+});
diff --git a/assets/js/components/task-move-position.js b/assets/js/components/task-move-position.js
index 390dcff1..e5a68b94 100644
--- a/assets/js/components/task-move-position.js
+++ b/assets/js/components/task-move-position.js
@@ -1,74 +1,164 @@
-Vue.component('task-move-position', {
- props: ['board', 'saveUrl'],
- template: '#template-task-move-position',
- data: function () {
- return {
- swimlaneId: 0,
- columnId: 0,
- position: 1,
- columns: [],
- tasks: [],
- positionChoice: 'before'
+KB.component('task-move-position', function (containerElement, options) {
+
+ function getSelectedValue(id) {
+ var element = KB.dom(document).find('#' + id);
+
+ if (element) {
+ return parseInt(element.options[element.selectedIndex].value);
}
- },
- ready: function () {
- this.columns = this.board[0].columns;
- this.columnId = this.columns[0].id;
- this.tasks = this.columns[0].tasks;
- },
- methods: {
- onChangeSwimlane: function () {
- var self = this;
- this.columnId = 0;
- this.position = 1;
- this.columns = [];
- this.tasks = [];
- this.positionChoice = 'before';
-
- this.board.forEach(function(swimlane) {
- if (swimlane.id === self.swimlaneId) {
- self.columns = swimlane.columns;
- self.tasks = self.columns[0].tasks;
- self.columnId = self.columns[0].id;
- }
- });
- },
- onChangeColumn: function () {
- var self = this;
- this.position = 1;
- this.tasks = [];
- this.positionChoice = 'before';
-
- this.columns.forEach(function(column) {
- if (column.id == self.columnId) {
- self.tasks = column.tasks;
-
- if (self.tasks.length > 0) {
- self.position = parseInt(self.tasks[0]['position']);
+
+ return null;
+ }
+
+ function getSwimlaneId() {
+ var swimlaneId = getSelectedValue('form-swimlanes');
+ return swimlaneId === null ? options.board[0].id : swimlaneId;
+ }
+
+ function getColumnId() {
+ var columnId = getSelectedValue('form-columns');
+ return columnId === null ? options.board[0].columns[0].id : columnId;
+ }
+
+ function getPosition() {
+ var position = getSelectedValue('form-position');
+ return position === null ? 1 : position;
+ }
+
+ function getPositionChoice() {
+ var element = KB.find('input[name=positionChoice]:checked');
+
+ if (element) {
+ return element.value;
+ }
+
+ return 'before';
+ }
+
+ function onSwimlaneChanged() {
+ var columnSelect = KB.dom(document).find('#form-columns');
+ KB.dom(columnSelect).replace(buildColumnSelect());
+
+ var taskSection = KB.dom(document).find('#form-tasks');
+ KB.dom(taskSection).replace(buildTasks());
+ }
+
+ function onColumnChanged() {
+ var taskSection = KB.dom(document).find('#form-tasks');
+ KB.dom(taskSection).replace(buildTasks());
+ }
+
+ function onError(message) {
+ KB.trigger('modal.stop');
+
+ KB.find('#message-container')
+ .replace(KB.dom('div')
+ .attr('id', 'message-container')
+ .attr('class', 'alert alert-error')
+ .text(message)
+ .build()
+ );
+ }
+
+ function onSubmit() {
+ var position = getPosition();
+ var positionChoice = getPositionChoice();
+
+ if (positionChoice === 'after') {
+ position++;
+ }
+
+ KB.find('#message-container').replace(KB.dom('div').attr('id', 'message-container').build());
+
+ KB.http.postJson(options.saveUrl, {
+ "column_id": getColumnId(),
+ "swimlane_id": getSwimlaneId(),
+ "position": position
+ }).error(function (response) {
+ if (response) {
+ onError(response.message);
+ }
+ });
+ }
+
+ function buildSwimlaneSelect() {
+ var swimlanes = [];
+
+ options.board.forEach(function(swimlane) {
+ swimlanes.push({'value': swimlane.id, 'text': swimlane.name});
+ });
+
+ return KB.dom('select')
+ .attr('id', 'form-swimlanes')
+ .change(onSwimlaneChanged)
+ .for('option', swimlanes)
+ .build();
+ }
+
+ function buildColumnSelect() {
+ var columns = [];
+ var swimlaneId = getSwimlaneId();
+
+ options.board.forEach(function(swimlane) {
+ if (swimlaneId === swimlane.id) {
+ swimlane.columns.forEach(function(column) {
+ columns.push({'value': column.id, 'text': column.title});
+ });
+ }
+ });
+
+ return KB.dom('select')
+ .attr('id', 'form-columns')
+ .change(onColumnChanged)
+ .for('option', columns)
+ .build();
+ }
+
+ function buildTasks() {
+ var tasks = [];
+ var swimlaneId = getSwimlaneId();
+ var columnId = getColumnId();
+ var container = KB.dom('div').attr('id', 'form-tasks');
+
+ options.board.forEach(function (swimlane) {
+ if (swimlaneId === swimlane.id) {
+ swimlane.columns.forEach(function (column) {
+ if (columnId === column.id) {
+ column.tasks.forEach(function (task) {
+ tasks.push({'value': task.position, 'text': '#' + task.id + ' - ' + task.title});
+ });
}
- }
- });
- },
- onSubmit: function () {
- if (this.positionChoice == 'after') {
- this.position++;
+ });
}
+ });
- $.ajax({
- cache: false,
- url: this.saveUrl,
- contentType: "application/json",
- type: "POST",
- processData: false,
- data: JSON.stringify({
- "column_id": this.columnId,
- "swimlane_id": this.swimlaneId,
- "position": this.position
- }),
- complete: function() {
- window.location.reload(true);
- }
- });
+ if (tasks.length > 0) {
+ container
+ .add(KB.html.label(options.positionLabel, 'form-position'))
+ .add(KB.dom('select').attr('id', 'form-position').for('option', tasks).build())
+ .add(KB.html.radio(options.beforeLabel, 'positionChoice', 'before'))
+ .add(KB.html.radio(options.afterLabel, 'positionChoice', 'after'))
+ ;
}
+
+ return container.build();
}
+
+ this.render = function () {
+ KB.on('modal.submit', onSubmit);
+ KB.on('modal.close', function () {
+ KB.removeListener('modal.submit', onSubmit);
+ });
+
+ var form = KB.dom('div')
+ .add(KB.dom('div').attr('id', 'message-container').build())
+ .add(KB.html.label(options.swimlaneLabel, 'form-swimlanes'))
+ .add(buildSwimlaneSelect())
+ .add(KB.html.label(options.columnLabel, 'form-columns'))
+ .add(buildColumnSelect())
+ .add(buildTasks())
+ .build();
+
+ containerElement.appendChild(form);
+ };
});
diff --git a/assets/js/components/text-editor.js b/assets/js/components/text-editor.js
new file mode 100644
index 00000000..57bc0f78
--- /dev/null
+++ b/assets/js/components/text-editor.js
@@ -0,0 +1,163 @@
+KB.component('text-editor', function (containerElement, options) {
+ var textarea, viewModeElement, writeModeElement, previewElement, selectionStart, selectionEnd;
+
+ this.render = function() {
+ writeModeElement = buildWriteMode();
+ viewModeElement = buildViewMode();
+
+ containerElement.appendChild(KB.dom('div')
+ .attr('class', 'text-editor')
+ .add(viewModeElement)
+ .add(writeModeElement)
+ .build());
+
+ if (options.autofocus) {
+ textarea.focus();
+ }
+ };
+
+ function buildViewMode() {
+ var toolbarElement = KB.dom('div')
+ .attr('class', 'text-editor-toolbar')
+ .for('a', [
+ {href: '#', html: '<i class="fa fa-pencil-square-o fa-fw"></i> ' + options.labelWrite, click: function() { toggleViewMode(); }}
+ ])
+ .build();
+
+ previewElement = KB.dom('div')
+ .attr('class', 'text-editor-preview-area markdown')
+ .build();
+
+ return KB.dom('div')
+ .attr('class', 'text-editor-view-mode')
+ .add(toolbarElement)
+ .add(previewElement)
+ .hide()
+ .build();
+ }
+
+ function buildWriteMode() {
+ var toolbarElement = KB.dom('div')
+ .attr('class', 'text-editor-toolbar')
+ .for('a', [
+ {href: '#', html: '<i class="fa fa-eye fa-fw"></i> ' + options.labelPreview, click: function() { toggleViewMode(); }},
+ {href: '#', html: '<i class="fa fa-bold fa-fw"></i>', click: function() { insertEnclosedTag('**'); }},
+ {href: '#', html: '<i class="fa fa-italic fa-fw"></i>', click: function() { insertEnclosedTag('_'); }},
+ {href: '#', html: '<i class="fa fa-strikethrough fa-fw"></i>', click: function() { insertEnclosedTag('~~'); }},
+ {href: '#', html: '<i class="fa fa-quote-right fa-fw"></i>', click: function() { insertPrependTag('> '); }},
+ {href: '#', html: '<i class="fa fa-list-ul fa-fw"></i>', click: function() { insertPrependTag('* '); }},
+ {href: '#', html: '<i class="fa fa-code fa-fw"></i>', click: function() { insertBlockTag('```'); }}
+ ])
+ .build();
+
+ var textareaElement = KB.dom('textarea');
+ textareaElement.attr('name', options.name);
+
+ if (options.tabindex) {
+ textareaElement.attr('tabindex', options.tabindex);
+ }
+
+ if (options.required) {
+ textareaElement.attr('required', 'required');
+ }
+
+ // Order is important for IE11 (especially for the placeholder)
+ textareaElement.text(options.text);
+
+ if (options.placeholder) {
+ textareaElement.attr('placeholder', options.placeholder);
+ }
+
+ textarea = textareaElement.build();
+
+ if (options.suggestOptions) {
+ KB.getComponent('suggest-menu', textarea, options.suggestOptions).render();
+ }
+
+ return KB.dom('div')
+ .attr('class', 'text-editor-write-mode')
+ .add(toolbarElement)
+ .add(textarea)
+ .build();
+ }
+
+ function toggleViewMode() {
+ KB.dom(previewElement).html(marked(textarea.value, {sanitize: true}));
+ KB.dom(viewModeElement).toggle();
+ KB.dom(writeModeElement).toggle();
+ }
+
+ function getSelectedText() {
+ return textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
+ }
+
+ function replaceTextRange(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+ }
+
+ function insertEnclosedTag(tag) {
+ var selectedText = getSelectedText();
+
+ insertText(tag + selectedText + tag);
+ setCursorBeforeClosingTag(tag);
+ }
+
+ function insertBlockTag(tag) {
+ var selectedText = getSelectedText();
+
+ insertText('\n' + tag + '\n' + selectedText + '\n' + tag);
+ setCursorBeforeClosingTag(tag, 2);
+ }
+
+ function insertPrependTag(tag) {
+ var selectedText = getSelectedText();
+
+ if (selectedText.indexOf('\n') === -1) {
+ insertText('\n' + tag + selectedText);
+ } else {
+ var lines = selectedText.split('\n');
+
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].indexOf(tag) === -1) {
+ lines[i] = tag + lines[i];
+ }
+ }
+
+ insertText(lines.join('\n'));
+ }
+
+ setCursorBeforeClosingTag(tag, 1);
+ }
+
+ function insertText(replacedText) {
+ textarea.focus();
+
+ var result = false;
+ var selectionPosition = KB.utils.getSelectionPosition(textarea);
+
+ selectionStart = selectionPosition.selectionStart;
+ selectionEnd = selectionPosition.selectionEnd;
+
+ if (document.queryCommandSupported('insertText')) {
+ result = document.execCommand('insertText', false, replacedText);
+ }
+
+ if (! result) {
+ try {
+ document.execCommand('ms-beginUndoUnit');
+ } catch (error) {}
+
+ textarea.value = replaceTextRange(textarea.value, selectionStart, selectionEnd, replacedText);
+
+ try {
+ document.execCommand('ms-endUndoUnit');
+ } catch (error) {}
+ }
+ }
+
+ function setCursorBeforeClosingTag(tag, offset) {
+ offset = offset || 0;
+ var position = selectionEnd + tag.length + offset;
+ textarea.setSelectionRange(position, position);
+ }
+});