diff options
Diffstat (limited to 'assets/js/components')
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); + } +}); |