Rewrite task move position component and remove Vuejs

This commit is contained in:
Frederic Guillot 2016-11-26 16:00:52 -05:00
parent e64faae69a
commit 04ff67e26b
No known key found for this signature in database
GPG Key ID: 92D77191BA7FBC99
17 changed files with 464 additions and 247 deletions

View File

@ -2,45 +2,22 @@
<h2><?= t('Move task to another position on the board') ?></h2>
</div>
<script type="x/template" id="template-task-move-position">
<?= $this->form->label(t('Swimlane'), 'swimlane') ?>
<select v-model="swimlaneId" @change="onChangeSwimlane()" id="form-swimlane">
<option v-for="swimlane in board" v-bind:value="swimlane.id">
{{ swimlane.name }}
</option>
</select>
<form>
<div v-if="columns.length > 0">
<?= $this->form->label(t('Column'), 'column') ?>
<select v-model="columnId" @change="onChangeColumn()" id="form-column">
<option v-for="column in columns" v-bind:value="column.id">
{{ column.title }}
</option>
</select>
</div>
<?= $this->app->component('task-move-position', array(
'saveUrl' => $this->url->href('TaskMovePositionController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])),
'board' => $board,
'swimlaneLabel' => t('Swimlane'),
'columnLabel' => t('Column'),
'positionLabel' => t('Position'),
'beforeLabel' => t('Insert before this task'),
'afterLabel' => t('Insert after this task'),
)) ?>
<div v-if="tasks.length > 0">
<?= $this->form->label(t('Position'), 'position') ?>
<select v-model="position" id="form-position">
<option v-for="task in tasks" v-bind:value="task.position">#{{ task.id }} - {{ task.title }}</option>
</select>
<label><input type="radio" value="before" v-model="positionChoice"><?= t('Insert before this task') ?></label>
<label><input type="radio" value="after" v-model="positionChoice"><?= t('Insert after this task') ?></label>
</div>
<?= $this->app->component('submit-cancel', array(
'submitLabel' => t('Save'),
'orLabel' => t('or'),
'cancelLabel' => t('cancel'),
)) ?>
<div v-if="errorMessage">
<div class="alert alert-error">{{ errorMessage }}</div>
</div>
<submit-cancel
label-button="<?= t('Save') ?>"
label-or="<?= t('or') ?>"
label-cancel="<?= t('cancel') ?>"
:callback="onSubmit">
</submit-cancel>
</script>
<task-move-position
save-url="<?= $this->url->href('TaskMovePositionController', 'save', array('task_id' => $task['id'], 'project_id' => $task['project_id'])) ?>"
:board='<?= json_encode($board, JSON_HEX_APOS) ?>'
></task-move-position>
</form>

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,6 @@ KB.onClick('.accordion-toggle', function(e) {
if (section) {
KB.dom(section).toggleClass('accordion-collapsed');
KB.dom(KB.dom(section).child('.accordion-content')).toggle();
KB.dom(KB.dom(section).find('.accordion-content')).toggle();
}
});

View File

@ -1,35 +1,52 @@
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();
}
},
events: {
'submitCancelled': function() {
this.loading = false;
}
KB.component('submit-cancel', function (containerElement, options) {
var isLoading = false;
function onSubmit() {
isLoading = true;
KB.find('#modal-submit-button').replace(buildButton());
KB.trigger('modal.submit');
}
function onCancel() {
KB.trigger('modal.cancel');
_KB.get('Popover').close();
}
function onStop() {
isLoading = false;
KB.find('#modal-submit-button').replace(buildButton());
}
function buildButton() {
var button = KB.dom('button')
.click(onSubmit)
.attr('id', 'modal-submit-button')
.attr('type', 'submit')
.attr('class', 'btn btn-blue');
if (isLoading) {
button
.disable()
.add(KB.dom('i').attr('class', 'fa fa-spinner fa-pulse').build())
.text(' ')
;
}
return button
.text(options.submitLabel)
.build();
}
this.render = function () {
KB.on('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);
};
});

View File

@ -1,86 +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',
errorMessage: ''
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;
this.errorMessage = '';
},
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']);
}
}
});
},
onSubmit: function () {
var self = this;
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
}),
statusCode: {
200: function() {
window.location.reload(true);
},
403: function(jqXHR) {
var response = JSON.parse(jqXHR.responseText);
self.errorMessage = response.message;
self.$broadcast('submitCancelled');
}
}
});
}
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
}).success(function () {
window.location.reload(true);
}).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});
});
}
});
}
});
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);
var form = KB.dom('div')
.on('submit', onSubmit)
.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);
};
});

View File

@ -51,8 +51,8 @@ KB.component('text-editor', function (containerElement, options) {
.attr('tabindex', options.tabindex || '-1')
.attr('required', options.required || false)
.attr('autofocus', options.autofocus || null)
.attr('placeholder', options.placeholder || '')
.text(options.text)
.text(options.text) // Order is important for IE11
.attr('placeholder', options.placeholder || null)
.build();
return KB.dom('div')
@ -124,7 +124,7 @@ KB.component('text-editor', function (containerElement, options) {
document.execCommand('ms-beginUndoUnit');
} catch (error) {}
textarea.value = replaceTextRange(text, textarea.selectionStart, textarea.selectionEnd, replacedText);
textarea.value = replaceTextRange(textarea.value, textarea.selectionStart, textarea.selectionEnd, replacedText);
try {
document.execCommand('ms-endUndoUnit');

70
assets/js/core/base.js Normal file
View File

@ -0,0 +1,70 @@
var KB = {
components: {},
utils: {},
html: {},
http: {},
listeners: {
clicks: {},
internals: {}
}
};
KB.on = function (eventType, callback) {
if (! this.listeners.internals.hasOwnProperty(eventType)) {
this.listeners.internals[eventType] = [];
}
this.listeners.internals[eventType].push(callback);
};
KB.trigger = function (eventType, eventData) {
if (this.listeners.internals.hasOwnProperty(eventType)) {
for (var i = 0; i < this.listeners.internals[eventType].length; i++) {
if (! this.listeners.internals[eventType][i](eventData)) {
break;
}
}
}
};
KB.onClick = function (selector, callback) {
this.listeners.clicks[selector] = callback;
};
KB.listen = function () {
var self = this;
function onClick(e) {
for (var selector in self.listeners.clicks) {
if (self.listeners.clicks.hasOwnProperty(selector) && e.target.matches(selector)) {
e.preventDefault();
self.listeners.clicks[selector](e);
}
}
}
document.addEventListener('click', onClick, false);
};
KB.component = function (name, object) {
this.components[name] = object;
};
KB.getComponent = function (name, containerElement, options) {
var object = this.components[name];
return new object(containerElement, options);
};
KB.render = function () {
for (var name in this.components) {
var elementList = document.querySelectorAll('.js-' + name);
for (var i = 0; i < elementList.length; i++) {
if (this.components.hasOwnProperty(name)) {
var component = KB.getComponent(name, elementList[i], JSON.parse(elementList[i].dataset.params));
component.render();
elementList[i].className = elementList[i].className + '-rendered';
}
}
}
};

View File

@ -1,4 +1,3 @@
KB.dom = function (tag) {
function DomManipulation(tag) {
@ -31,19 +30,33 @@ KB.dom = function (tag) {
return this;
};
this.click = function (callback) {
element.onclick = function (e) {
this.on = function (eventName, callback) {
element.addEventListener(eventName, function (e) {
e.preventDefault();
callback();
};
callback(e.target);
});
return this;
};
this.click = function (callback) {
return this.on('click', callback);
};
this.change = function (callback) {
return this.on('change', callback);
};
this.add = function (node) {
element.appendChild(node);
return this;
};
this.replace = function (node) {
element.parentNode.replaceChild(node, element);
return this;
};
this.html = function (html) {
element.innerHTML = html;
return this;
@ -73,6 +86,16 @@ KB.dom = function (tag) {
return element.classList.contains(className);
};
this.disable = function () {
element.disabled = true;
return this;
};
this.enable = function () {
element.disabled = false;
return this;
};
this.parent = function (selector) {
for (; element && element !== document; element = element.parentNode) {
if (element.matches(selector)) {
@ -83,7 +106,7 @@ KB.dom = function (tag) {
return null;
};
this.child = function (selector) {
this.find = function (selector) {
return element.querySelector(selector);
};
@ -118,3 +141,13 @@ KB.dom = function (tag) {
return new DomManipulation(tag);
};
KB.find = function (selector) {
var element = document.querySelector(selector);
if (element) {
return KB.dom(element);
}
return null;
};

25
assets/js/core/html.js Normal file
View File

@ -0,0 +1,25 @@
KB.html.label = function (label, id) {
return KB.dom('label').attr('for', id).text(label).build();
};
KB.html.radio = function (label, name, value) {
return KB.dom('label')
.add(KB.dom('input')
.attr('type', 'radio')
.attr('name', name)
.attr('value', value)
.build()
)
.text(label)
.build();
};
KB.html.radios = function (items) {
var html = KB.dom('div');
for (var item in items) {
if (items.hasOwnProperty(item)) {
html.add(KB.html.radio(item.label, item.name, item.value));
}
}
};

66
assets/js/core/http.js Normal file
View File

@ -0,0 +1,66 @@
KB.http.request = function (method, url, headers, body) {
var successCallback = function() {};
var errorCallback = function() {};
function parseResponse(request) {
try {
return JSON.parse(request.responseText);
} catch (e) {
return request.responseText;
}
}
this.execute = function () {
var request = new XMLHttpRequest();
request.open(method, url, true);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
request.setRequestHeader(header, headers[header]);
}
}
request.onerror = function() {
errorCallback();
};
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
var response = parseResponse(request);
if (request.status === 200) {
successCallback(response);
} else {
errorCallback(response);
}
}
};
request.send(body);
return this;
};
this.success = function (callback) {
successCallback = callback;
return this;
};
this.error = function (callback) {
errorCallback = callback;
return this;
};
};
KB.http.get = function (url) {
return (new KB.http.request('GET', url)).execute();
};
KB.http.postJson = function (url, body) {
var headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
return (new KB.http.request('POST', url, headers, JSON.stringify(body))).execute();
};

View File

@ -1,58 +0,0 @@
var KB = {
components: {},
utils: {},
clickEvents: {}
};
KB.onClick = function (selector, callback) {
this.clickEvents[selector] = callback;
};
KB.listen = function () {
var self = this;
function onClick(e) {
for (var selector in self.clickEvents) {
if (self.clickEvents.hasOwnProperty(selector) && e.target.matches(selector)) {
e.preventDefault();
self.clickEvents[selector](e);
}
}
}
document.addEventListener('click', onClick, false);
};
KB.component = function (name, object) {
this.components[name] = object;
};
KB.render = function () {
for (var name in this.components) {
var elementList = document.querySelectorAll('.js-' + name);
for (var i = 0; i < elementList.length; i++) {
if (this.components.hasOwnProperty(name)) {
var object = this.components[name];
var component = new object(elementList[i], JSON.parse(elementList[i].dataset.params));
component.render();
elementList[i].className = elementList[i].className + '-rendered';
}
}
}
};
KB.utils.formatDuration = function (d) {
if (d >= 86400) {
return Math.round(d/86400) + "d";
}
else if (d >= 3600) {
return Math.round(d/3600) + "h";
}
else if (d >= 60) {
return Math.round(d/60) + "m";
}
return d + "s";
};

13
assets/js/core/utils.js Normal file
View File

@ -0,0 +1,13 @@
KB.utils.formatDuration = function (d) {
if (d >= 86400) {
return Math.round(d/86400) + "d";
}
else if (d >= 3600) {
return Math.round(d/3600) + "h";
}
else if (d >= 60) {
return Math.round(d/60) + "m";
}
return d + "s";
};

View File

@ -0,0 +1,14 @@
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}

View File

@ -157,9 +157,5 @@ Kanboard.Popover.prototype.afterOpen = function() {
this.app.autoComplete();
this.app.tagAutoComplete();
new Vue({
el: '#popover-container'
});
KB.render();
};

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,9 @@ var strip = require('gulp-strip-comments');
var src = {
js: [
'assets/js/core/kb.js',
'assets/js/core/dom.js',
'assets/js/polyfills/*.js',
'assets/js/core/base.js',
'assets/js/core/!(base|bootstrap)*.js',
'assets/js/components/*.js',
'assets/js/core/bootstrap.js',
'assets/js/src/Namespace.js',
@ -69,13 +70,6 @@ gulp.task('bower', function() {
});
gulp.task('vendor', function() {
gulp.src('node_modules/vue/dist/vue.min.js')
.pipe(strip({trim: true}))
.pipe(gulp.dest('node_modules/vue/dist/'))
;
vendor.js.push('node_modules/vue/dist/vue.min.js');
gulp.src(vendor.js)
.pipe(concat('vendor.min.js'))
.pipe(gulp.dest(dist.js))

View File

@ -1,6 +1,6 @@
{
"name": "kanboard",
"dependencies": {
"devDependencies": {
"bower": "^1.7.9",
"gulp": "^3.9.1",
"gulp-bower": "0.0.13",
@ -9,9 +9,6 @@
"gulp-sass": "^2.3.2",
"gulp-strip-comments": "^2.4.3",
"gulp-uglify": "^1.5.3",
"vue": "1.0.26-csp"
},
"devDependencies": {
"jshint": "^2.9.4"
}
}