Rewrite board drag and drop with jquery (touch devices, IE, auto-update)

This commit is contained in:
Frédéric Guillot
2014-05-17 17:35:39 -04:00
parent 09da5720e8
commit 5e4b40665f
18 changed files with 375 additions and 379 deletions

View File

@@ -29,7 +29,7 @@ body {
/* links */ /* links */
a { a {
color: #3366CC; color: #3366CC;
border: 1px solid rgba(255, 255, 255, 0); border: none;
} }
a:focus { a:focus {
@@ -123,10 +123,6 @@ label {
margin-top: 10px; margin-top: 10px;
} }
input[type="checkbox"] {
border: 1px solid #ccc;
}
input[type="number"], input[type="number"],
input[type="date"], input[type="date"],
input[type="email"], input[type="email"],
@@ -574,17 +570,15 @@ td div.over {
} }
.draggable-item { .draggable-item {
margin-right: 5px;
margin-bottom: 10px;
}
[draggable] {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
[draggable=true]:hover { .draggable-placeholder {
box-shadow: 0 0 3px #333; border: 2px dashed #000;
background: #fafafa;
height: 70px;
margin-bottom: 10px;
} }
tr td.task a, tr td.task a,
@@ -607,6 +601,8 @@ div.task-title a {
div.task { div.task {
position: relative; position: relative;
margin-right: 5px;
margin-bottom: 10px;
} }
.task-score { .task-score {

View File

@@ -1,259 +1,102 @@
(function () { (function () {
function handleItemDragStart(e) var checkInterval = null;
// Setup the board
function board_load_events()
{ {
this.style.opacity = '0.4'; $(".column").sortable({
connectWith: ".column",
dragSrcItem = this; placeholder: "draggable-placeholder",
dragSrcColumn = this.parentNode; stop: function(event, ui) {
board_save();
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('text/plain', this.innerHTML);
}
function handleItemDragEnd(e)
{
// Restore styles
removeOver();
this.style.opacity = '1.0';
dragSrcColumn = null;
dragSrcItem = null;
}
function handleItemDragOver(e)
{
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
return false;
}
function handleItemDragEnter(e)
{
if (dragSrcItem != this) {
removeOver();
this.classList.add('over');
}
}
function handleItemDrop(e)
{
if (e.preventDefault) e.preventDefault();
if (e.stopPropagation) e.stopPropagation();
// Drop the element if the item is not the same
if (dragSrcItem != this) {
var position = getItemPosition(this);
var item = createItem(e.dataTransfer.getData('text/plain'));
if (countColumnItems(this.parentNode) == position) {
this.parentNode.appendChild(item);
}
else {
this.parentNode.insertBefore(item, this);
} }
});
dragSrcItem.parentNode.removeChild(dragSrcItem); var interval = parseInt($("#board").attr("data-check-interval"));
saveBoard(); if (interval > 0) {
} checkInterval = window.setInterval(board_check, interval * 1000);
dragSrcColumn = null;
dragSrcItem = null;
return false;
}
function handleColumnDragOver(e)
{
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
return false;
}
function handleColumnDragEnter(e)
{
if (dragSrcColumn != this) {
removeOver();
this.classList.add('over');
} }
} }
function handleColumnDrop(e) // Stop events
function board_unload_events()
{ {
if (e.preventDefault) e.preventDefault(); clearInterval(checkInterval);
if (e.stopPropagation) e.stopPropagation();
// Drop the element if the column is not the same
if (dragSrcColumn != this) {
var item = createItem(e.dataTransfer.getData('text/plain'));
this.appendChild(item);
dragSrcColumn.removeChild(dragSrcItem);
saveBoard();
}
return false;
} }
function saveBoard() // Save and refresh the board
function board_save()
{ {
var data = []; var data = [];
var projectId = document.getElementById("board").getAttribute("data-project-id"); var projectId = $("#board").attr("data-project-id");
var cols = document.querySelectorAll('.column');
[].forEach.call(cols, function(col) { board_unload_events();
var task_limit = col.getAttribute("data-task-limit"); $(".column").each(function() {
var columnId = $(this).attr("data-column-id");
if (task_limit != "" && task_limit != "0") {
task_limit = parseInt(task_limit);
if (col.children.length > task_limit) {
col.classList.add("task-limit-warning");
}
else {
col.classList.remove("task-limit-warning");
}
var counter = document.getElementById("task-number-column-" + col.getAttribute("data-column-id"));
if (counter) counter.innerHTML = col.children.length;
}
[].forEach.call(col.children, function(item) {
$("#column-" + columnId + " .task").each(function(index) {
data.push({ data.push({
"task_id": item.firstElementChild.getAttribute("data-task-id"), "task_id": parseInt($(this).attr("data-task-id")),
"position": getItemPosition(item), "position": index + 1,
"column_id": col.getAttribute("data-column-id") "column_id": parseInt(columnId)
}) });
}); });
}); });
var xhr = new XMLHttpRequest(); $.ajax({
xhr.open("POST", "?controller=board&action=save&project_id=" + projectId, true); url: "?controller=board&action=save&project_id=" + projectId,
data: {positions: data},
type: "POST",
success: function(data) {
$("#board").remove();
$("#main").append(data);
board_load_events();
applyFilter(getSelectedUserFilter(), hasDueDateFilter());
}
});
}
xhr.onreadystatechange = function(response) { // Check if a board have been changed by someone else
function board_check()
{
var projectId = $("#board").attr("data-project-id");
var timestamp = $("#board").attr("data-time");
if (this.readyState == this.DONE) { if (projectId != undefined && timestamp != undefined) {
try { $.ajax({
var response = JSON.parse(this.responseText); url: "?controller=board&action=check&project_id=" + projectId + "&timestamp=" + timestamp,
statusCode: {
if (response.result == false) { 200: function(data) {
window.alert('Unable to update the board'); $("#board").remove();
} $("#main").append(data);
else if (response.refresh == true) { board_unload_events();
window.location = "?controller=board&action=show&project_id=" + projectId; board_load_events();
applyFilter(getSelectedUserFilter(), hasDueDateFilter());
} }
} }
catch (e) {} });
}
};
xhr.send(JSON.stringify(data));
}
function getItemPosition(element)
{
var i = 0;
while ((element = element.previousSibling) != null) {
if (element.nodeName == "DIV" && element.className == "draggable-item") {
i++;
}
} }
return i + 1;
} }
function countColumnItems(element) // Get the selected user id
{
return element.children.length;
}
function createItem(html)
{
var item = document.createElement("div");
item.className = "draggable-item";
item.draggable = true;
item.innerHTML = html;
item.ondragstart = handleItemDragStart;
item.ondragend = handleItemDragEnd;
item.ondragenter = handleItemDragEnter;
item.ondragover = handleItemDragOver;
item.ondrop = handleItemDrop;
return item;
}
function removeOver()
{
// Remove column over
[].forEach.call(document.querySelectorAll('.column'), function (col) {
col.classList.remove('over');
});
// Remove item over
[].forEach.call(document.querySelectorAll('.draggable-item'), function (item) {
item.classList.remove('over');
});
}
// Drag and drop events
var dragSrcItem = null;
var dragSrcColumn = null;
var items = document.querySelectorAll('.draggable-item');
[].forEach.call(items, function(item) {
item.addEventListener('dragstart', handleItemDragStart, false);
item.addEventListener('dragend', handleItemDragEnd, false);
item.addEventListener('dragenter', handleItemDragEnter, false);
item.addEventListener('dragover', handleItemDragOver, false);
item.addEventListener('drop', handleItemDrop, false);
});
var cols = document.querySelectorAll('.column');
[].forEach.call(cols, function(col) {
col.addEventListener('dragenter', handleColumnDragEnter, false);
col.addEventListener('dragover', handleColumnDragOver, false);
col.addEventListener('drop', handleColumnDrop, false);
});
[].forEach.call(document.querySelectorAll('[data-task-id]'), function (item) {
item.addEventListener('click', function() {
window.location.href = '?controller=task&action=show&task_id=' + item.getAttribute('data-task-id');
});
});
// Filtering
function getSelectedUserFilter() function getSelectedUserFilter()
{ {
var select = document.getElementById("form-user_id"); return $("#form-user_id").val();
return select.options[select.selectedIndex].value;
} }
// Return true if the filter is activated
function hasDueDateFilter() function hasDueDateFilter()
{ {
var dateFilter = document.getElementById("filter-due-date"); return $("#filter-due-date").hasClass("filter-on");
return dateFilter.classList.contains("filter-on");
} }
// Apply user or date filter (change tasks opacity)
function applyFilter(selectedUserId, filterDueDate) function applyFilter(selectedUserId, filterDueDate)
{ {
[].forEach.call(document.querySelectorAll('[data-task-id]'), function (item) { $("[data-task-id]").each(function(index, item) {
var ownerId = item.getAttribute("data-owner-id"); var ownerId = item.getAttribute("data-owner-id");
var dueDate = item.getAttribute("data-due-date"); var dueDate = item.getAttribute("data-due-date");
@@ -271,22 +114,24 @@
}); });
} }
var userFilter = document.getElementById("form-user_id"); // Load filter events
var dateFilter = document.getElementById("filter-due-date"); function filter_load_events()
{
if (userFilter) { $("#form-user_id").change(function() {
userFilter.onchange = function() {
applyFilter(getSelectedUserFilter(), hasDueDateFilter()); applyFilter(getSelectedUserFilter(), hasDueDateFilter());
}; });
}
if (dateFilter) { $("#filter-due-date").click(function(e) {
$(this).toggleClass("filter-on");
dateFilter.onclick = function(e) {
dateFilter.classList.toggle("filter-on");
applyFilter(getSelectedUserFilter(), hasDueDateFilter()); applyFilter(getSelectedUserFilter(), hasDueDateFilter());
e.preventDefault(); e.preventDefault();
}; });
} }
// Initialization
$(function() {
board_load_events();
filter_load_events();
});
}()); }());

4
assets/js/jquery-1.11.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11
assets/js/jquery.ui.touch-punch.min.js vendored Normal file
View File

@@ -0,0 +1,11 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);

View File

@@ -6,7 +6,7 @@ require __DIR__.'/core/translator.php';
$registry = new Core\Registry; $registry = new Core\Registry;
$registry->db_version = 14; $registry->db_version = 15;
$registry->db = function() use ($registry) { $registry->db = function() use ($registry) {
require __DIR__.'/vendor/PicoDb/Database.php'; require __DIR__.'/vendor/PicoDb/Database.php';
@@ -112,8 +112,11 @@ $registry->google = function() use ($registry) {
if (file_exists('config.php')) require 'config.php'; if (file_exists('config.php')) require 'config.php';
// Auto-refresh frequency in seconds for the public board view // Board refresh frequency in seconds for the public board view
defined('AUTO_REFRESH_DURATION') or define('AUTO_REFRESH_DURATION', 60); defined('BOARD_PUBLIC_CHECK_INTERVAL') or define('BOARD_PUBLIC_CHECK_INTERVAL', 60);
// Board refresh frequency in seconds (the value 0 disable this feature)
defined('BOARD_CHECK_INTERVAL') or define('BOARD_CHECK_INTERVAL', 10);
// Custom session save path // Custom session save path
defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', ''); defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', '');

View File

@@ -212,8 +212,9 @@ abstract class Base
$this->response->redirect('?controller=user&action=forbidden'); $this->response->redirect('?controller=user&action=forbidden');
} }
// Attach events for automatic actions // Attach events
$this->action->attachEvents(); $this->action->attachEvents();
$this->project->attachEvents();
} }
/** /**

View File

@@ -174,20 +174,13 @@ class Board extends Base
$this->notfound(); $this->notfound();
} }
$filters = array();
$users = $this->project->getUsersList($project_id, true, true);
if ($user_id !== \Model\User::EVERYBODY_ID && in_array($user_id, array_keys($users))) {
$filters[] = array('column' => 'owner_id', 'operator' => 'eq', 'value' => $user_id);
}
$this->response->html($this->template->layout('board_index', array( $this->response->html($this->template->layout('board_index', array(
'users' => $users, 'users' => $this->project->getUsersList($project_id, true, true),
'filters' => array('user_id' => $user_id), 'filters' => array('user_id' => $user_id),
'projects' => $projects, 'projects' => $projects,
'current_project_id' => $project_id, 'current_project_id' => $project_id,
'current_project_name' => $projects[$project_id], 'current_project_name' => $projects[$project_id],
'columns' => $this->board->get($project_id, $filters), 'board' => $this->board->get($project_id),
'menu' => 'boards', 'menu' => 'boards',
'title' => $projects[$project_id] 'title' => $projects[$project_id]
))); )));
@@ -351,14 +344,43 @@ class Board extends Base
public function save() public function save()
{ {
$project_id = $this->request->getIntegerParam('project_id'); $project_id = $this->request->getIntegerParam('project_id');
$values = $this->request->getValues();
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) { if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->json(array('result' => false), 401); $this->response->text('Not Authorized', 401);
} }
$this->response->json(array( if (isset($values['positions'])) {
'result' => $this->board->saveTasksPosition($this->request->getValues()), $this->board->saveTasksPosition($values['positions']);
'refresh' => $this->event->getLastListenerExecuted() !== '' }
));
$this->response->html(
$this->template->load('board_show', array('current_project_id' => $project_id, 'board' => $this->board->get($project_id))),
201
);
}
/**
* Check if the board have been changed
*
* @access public
*/
public function check()
{
$project_id = $this->request->getIntegerParam('project_id');
$timestamp = $this->request->getIntegerParam('timestamp');
if ($project_id > 0 && ! $this->project->isUserAllowed($project_id, $this->acl->getUserId())) {
$this->response->text('Not Authorized', 401);
}
if ($this->project->isModifiedSince($project_id, $timestamp)) {
$this->response->html(
$this->template->load('board_show', array('current_project_id' => $project_id, 'board' => $this->board->get($project_id)))
);
}
else {
$this->response->status(304);
}
} }
} }

View File

@@ -14,12 +14,8 @@ class Response
*/ */
public function status($status_code) public function status($status_code)
{ {
if (strpos(php_sapi_name(), 'apache') !== false) { header('Status: '.$status_code);
header('HTTP/1.0 '.$status_code); header($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
}
else {
header('Status: '.$status_code);
}
} }
public function redirect($url) public function redirect($url)

View File

@@ -0,0 +1,51 @@
<?php
namespace Event;
use \Core\Listener;
use \Model\Project;
/**
* Task modification listener
*
* @package events
* @author Frederic Guillot
*/
class TaskModification implements Listener
{
/**
* Project model
*
* @accesss private
* @var \Model\Project
*/
private $project;
/**
* Constructor
*
* @access public
* @param \Model\Project $project Project model instance
*/
public function __construct(Project $project)
{
$this->project = $project;
}
/**
* Execute the action
*
* @access public
* @param array $data Event data dictionary
* @return bool True if the action was executed or false when not executed
*/
public function execute(array $data)
{
if (isset($data['project_id'])) {
$this->project->updateModificationDate($data['project_id']);
return true;
}
return false;
}
}

View File

@@ -32,8 +32,8 @@ class Acl extends Base
*/ */
private $user_actions = array( private $user_actions = array(
'app' => array('index'), 'app' => array('index'),
'board' => array('index', 'show', 'assign', 'assigntask', 'save'), 'board' => array('index', 'show', 'assign', 'assigntask', 'save', 'check'),
'project' => array('tasks', 'index', 'forbidden'), 'project' => array('tasks', 'index', 'forbidden', 'search'),
'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'confirmclose', 'open', 'confirmopen', 'description', 'duplicate'), 'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'confirmclose', 'open', 'confirmopen', 'description', 'duplicate'),
'comment' => array('save', 'confirm', 'remove', 'update', 'edit'), 'comment' => array('save', 'confirm', 'remove', 'update', 'edit'),
'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle'), 'user' => array('index', 'edit', 'update', 'forbidden', 'logout', 'index', 'unlinkgoogle'),

View File

@@ -6,9 +6,11 @@ require_once __DIR__.'/base.php';
require_once __DIR__.'/acl.php'; require_once __DIR__.'/acl.php';
require_once __DIR__.'/board.php'; require_once __DIR__.'/board.php';
require_once __DIR__.'/task.php'; require_once __DIR__.'/task.php';
require_once __DIR__.'/../events/task_modification.php';
use \SimpleValidator\Validator; use \SimpleValidator\Validator;
use \SimpleValidator\Validators; use \SimpleValidator\Validators;
use \Event\TaskModification;
/** /**
* Project model * Project model
@@ -389,6 +391,36 @@ class Project extends Base
return (int) $project_id; return (int) $project_id;
} }
/**
* Check if the project have been modified
*
* @access public
* @param integer $project_id Project id
* @param integer $timestamp Timestamp
* @return bool
*/
public function isModifiedSince($project_id, $timestamp)
{
return (bool) $this->db->table(self::TABLE)
->eq('id', $project_id)
->gt('last_modified', $timestamp)
->count();
}
/**
* Update modification date
*
* @access public
* @param integer $project_id Project id
* @return bool
*/
public function updateModificationDate($project_id)
{
return $this->db->table(self::TABLE)->eq('id', $project_id)->save(array(
'last_modified' => time()
));
}
/** /**
* Update a project * Update a project
* *
@@ -508,4 +540,25 @@ class Project extends Base
$v->getErrors() $v->getErrors()
); );
} }
/**
* Attach events
*
* @access public
*/
public function attachEvents()
{
$events = array(
Task::EVENT_UPDATE,
Task::EVENT_CREATE,
Task::EVENT_CLOSE,
Task::EVENT_OPEN,
);
$listener = new TaskModification($this);
foreach ($events as $event_name) {
$this->event->attach($event_name, $listener);
}
}
} }

View File

@@ -2,6 +2,11 @@
namespace Schema; namespace Schema;
function version_15($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INT DEFAULT 0");
}
function version_14($pdo) function version_14($pdo)
{ {
$pdo->exec("ALTER TABLE users ADD COLUMN name VARCHAR(255)"); $pdo->exec("ALTER TABLE users ADD COLUMN name VARCHAR(255)");

View File

@@ -2,6 +2,11 @@
namespace Schema; namespace Schema;
function version_15($pdo)
{
$pdo->exec("ALTER TABLE projects ADD COLUMN last_modified INTEGER DEFAULT 0");
}
function version_14($pdo) function version_14($pdo)
{ {
$pdo->exec("ALTER TABLE users ADD COLUMN name TEXT"); $pdo->exec("ALTER TABLE users ADD COLUMN name TEXT");

View File

@@ -27,88 +27,10 @@
</ul> </ul>
</div> </div>
<?php if (empty($columns)): ?> <?php if (empty($board)): ?>
<p class="alert alert-error"><?= t('There is no column in your project!') ?></p> <p class="alert alert-error"><?= t('There is no column in your project!') ?></p>
<?php else: ?> <?php else: ?>
<?= Helper\template('board_show', array('current_project_id' => $current_project_id, 'board' => $board)) ?>
<table id="board" data-project-id="<?= $current_project_id ?>">
<tr>
<?php $column_with = round(100 / count($columns), 2); ?>
<?php foreach ($columns as $column): ?>
<th width="<?= $column_with ?>%">
<a href="?controller=task&amp;action=create&amp;project_id=<?= $column['project_id'] ?>&amp;column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a>
<?= Helper\escape($column['title']) ?>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>" class="task-limit">
(
<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>
/
<?= Helper\escape($column['task_limit']) ?>
)
</span>
<?php endif ?>
</th>
<?php endforeach ?>
</tr>
<tr>
<?php foreach ($columns as $column): ?>
<td
id="column-<?= $column['id'] ?>"
class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"
data-column-id="<?= $column['id'] ?>"
data-task-limit="<?= $column['task_limit'] ?>"
dropzone="copy">
<?php foreach ($column['tasks'] as $task): ?>
<div class="draggable-item" draggable="true">
<div class="task task-<?= $task['color_id'] ?>"
data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
data-due-date="<?= $task['date_due'] ?>"
title="<?= t('View this task') ?>">
<a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> -
<span class="task-user">
<?php if (! empty($task['owner_id'])): ?>
<a href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"><?= t('Assigned to %s', $task['username']) ?></a>
<?php else: ?>
<a href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>" class="task-nobody"><?= t('Nobody assigned') ?></a>
<?php endif ?>
</span>
<?php if ($task['score']): ?>
<span class="task-score"><?= Helper\escape($task['score']) ?></span>
<?php endif ?>
<div class="task-title">
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a>
</div>
<div class="task-footer">
<?php if (! empty($task['date_due'])): ?>
<div class="task-date">
<?= dt('%B %e, %G', $task['date_due']) ?>
</div>
<?php endif ?>
<div class="task-icons">
<?php if (! empty($task['nb_comments'])): ?>
<?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
<i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i>
<?php endif ?>
</div>
</div>
</div>
</div>
<?php endforeach ?>
</td>
<?php endforeach ?>
</tr>
</table>
<?php endif ?> <?php endif ?>
</section> </section>

View File

@@ -19,46 +19,44 @@
<?php foreach ($columns as $column): ?> <?php foreach ($columns as $column): ?>
<td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"> <td class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>">
<?php foreach ($column['tasks'] as $task): ?> <?php foreach ($column['tasks'] as $task): ?>
<div class="draggable-item"> <div class="task task-<?= $task['color_id'] ?>">
<div class="task task-<?= $task['color_id'] ?>">
#<?= $task['id'] ?> - #<?= $task['id'] ?> -
<span class="task-user"> <span class="task-user">
<?php if (! empty($task['owner_id'])): ?> <?php if (! empty($task['owner_id'])): ?>
<?= t('Assigned to %s', $task['username']) ?> <?= t('Assigned to %s', $task['username']) ?>
<?php else: ?> <?php else: ?>
<span class="task-nobody"><?= t('Nobody assigned') ?></span> <span class="task-nobody"><?= t('Nobody assigned') ?></span>
<?php endif ?> <?php endif ?>
</span> </span>
<?php if ($task['score']): ?> <?php if ($task['score']): ?>
<span class="task-score"><?= Helper\escape($task['score']) ?></span> <span class="task-score"><?= Helper\escape($task['score']) ?></span>
<?php endif ?> <?php endif ?>
<div class="task-title"> <div class="task-title">
<?= Helper\escape($task['title']) ?> <?= Helper\escape($task['title']) ?>
</div>
<div class="task-footer">
<?php if (! empty($task['date_due'])): ?>
<div class="task-date">
<?= dt('%B %e, %G', $task['date_due']) ?>
</div> </div>
<?php endif ?>
<div class="task-footer"> <div class="task-icons">
<?php if (! empty($task['date_due'])): ?> <?php if (! empty($task['nb_comments'])): ?>
<div class="task-date"> <?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i>
<?= dt('%B %e, %G', $task['date_due']) ?>
</div>
<?php endif ?> <?php endif ?>
<div class="task-icons"> <?php if (! empty($task['description'])): ?>
<?php if (! empty($task['nb_comments'])): ?> <i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i>
<?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i> <?php endif ?>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
<i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i>
<?php endif ?>
</div>
</div> </div>
</div> </div>
</div> </div>
<?php endforeach ?> <?php endforeach ?>
</td> </td>

75
templates/board_show.php Normal file
View File

@@ -0,0 +1,75 @@
<table id="board" data-project-id="<?= $current_project_id ?>" data-time="<?= time() ?>" data-check-interval="<?= BOARD_CHECK_INTERVAL ?>">
<tr>
<?php $column_with = round(100 / count($board), 2); ?>
<?php foreach ($board as $column): ?>
<th width="<?= $column_with ?>%">
<a href="?controller=task&amp;action=create&amp;project_id=<?= $column['project_id'] ?>&amp;column_id=<?= $column['id'] ?>" title="<?= t('Add a new task') ?>">+</a>
<?= Helper\escape($column['title']) ?>
<?php if ($column['task_limit']): ?>
<span title="<?= t('Task limit') ?>" class="task-limit">
(
<span id="task-number-column-<?= $column['id'] ?>"><?= count($column['tasks']) ?></span>
/
<?= Helper\escape($column['task_limit']) ?>
)
</span>
<?php endif ?>
</th>
<?php endforeach ?>
</tr>
<tr>
<?php foreach ($board as $column): ?>
<td
id="column-<?= $column['id'] ?>"
class="column <?= $column['task_limit'] && count($column['tasks']) > $column['task_limit'] ? 'task-limit-warning' : '' ?>"
data-column-id="<?= $column['id'] ?>"
data-task-limit="<?= $column['task_limit'] ?>"
>
<?php foreach ($column['tasks'] as $task): ?>
<div class="task draggable-item task-<?= $task['color_id'] ?>"
data-task-id="<?= $task['id'] ?>"
data-owner-id="<?= $task['owner_id'] ?>"
data-due-date="<?= $task['date_due'] ?>"
title="<?= t('View this task') ?>">
<a href="?controller=task&amp;action=edit&amp;task_id=<?= $task['id'] ?>" title="<?= t('Edit this task') ?>">#<?= $task['id'] ?></a> -
<span class="task-user">
<?php if (! empty($task['owner_id'])): ?>
<a href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>"><?= t('Assigned to %s', $task['username']) ?></a>
<?php else: ?>
<a href="?controller=board&amp;action=assign&amp;task_id=<?= $task['id'] ?>" title="<?= t('Change assignee') ?>" class="task-nobody"><?= t('Nobody assigned') ?></a>
<?php endif ?>
</span>
<?php if ($task['score']): ?>
<span class="task-score"><?= Helper\escape($task['score']) ?></span>
<?php endif ?>
<div class="task-title">
<a href="?controller=task&amp;action=show&amp;task_id=<?= $task['id'] ?>" title="<?= t('View this task') ?>"><?= Helper\escape($task['title']) ?></a>
</div>
<div class="task-footer">
<?php if (! empty($task['date_due'])): ?>
<div class="task-date">
<?= dt('%B %e, %G', $task['date_due']) ?>
</div>
<?php endif ?>
<div class="task-icons">
<?php if (! empty($task['nb_comments'])): ?>
<?= $task['nb_comments'] ?> <i class="fa fa-comment-o" title="<?= p($task['nb_comments'], t('%d comment', $task['nb_comments']), t('%d comments', $task['nb_comments'])) ?>"></i>
<?php endif ?>
<?php if (! empty($task['description'])): ?>
<i class="fa fa-file-text-o" title="<?= t('Description') ?>"></i>
<?php endif ?>
</div>
</div>
</div>
<?php endforeach ?>
</td>
<?php endforeach ?>
</tr>
</table>

View File

@@ -3,6 +3,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<script src="assets/js/jquery-1.11.1.min.js"></script>
<script src="assets/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="assets/js/jquery.ui.touch-punch.min.js"></script>
<link rel="stylesheet" href="assets/css/app.css" media="screen"> <link rel="stylesheet" href="assets/css/app.css" media="screen">
<link rel="stylesheet" href="assets/css/font-awesome.min.css" media="screen"> <link rel="stylesheet" href="assets/css/font-awesome.min.css" media="screen">
<link rel="icon" type="image/png" href="assets/img/favicon.png"> <link rel="icon" type="image/png" href="assets/img/favicon.png">
@@ -12,7 +15,7 @@
<link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png"> <link rel="apple-touch-icon" sizes="144x144" href="assets/img/touch-icon-ipad-retina.png">
<title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title> <title><?= isset($title) ? Helper\escape($title).' - Kanboard' : 'Kanboard' ?></title>
<?php if (isset($auto_refresh)): ?> <?php if (isset($auto_refresh)): ?>
<meta http-equiv="refresh" content="<?= AUTO_REFRESH_DURATION ?>" > <meta http-equiv="refresh" content="<?= BOARD_PUBLIC_CHECK_INTERVAL ?>" >
<?php endif ?> <?php endif ?>
</head> </head>
<body> <body>