3 Commits
v0.2 ... v1.0

9 changed files with 248 additions and 13 deletions

55
Asset/js/keep-scroll.js Normal file
View File

@@ -0,0 +1,55 @@
/*
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes.
*
* When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new
* element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring
* jump. This remembers the last scroll position and restores it the moment a replacement
* container appears, so the view stays put.
*
* We observe the STABLE parent (the container itself is replaced) and read the scroll from
* whichever #board-container is current, so it keeps working after every rebuild.
*/
(function () {
"use strict";
function init() {
var container = document.getElementById("board-container");
if (!container) {
return;
}
var parent = container.parentNode;
var lastScrollLeft = container.scrollLeft;
var lastContainer = container;
// Remember the position whenever the board is scrolled (capture: scroll does not
// bubble, and this survives the container being replaced).
document.addEventListener("scroll", function (e) {
var c = document.getElementById("board-container");
if (c && e.target === c) {
lastScrollLeft = c.scrollLeft;
}
}, true);
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0;
// restore the remembered position before the browser paints it.
if (window.MutationObserver) {
new MutationObserver(function () {
var c = document.getElementById("board-container");
if (c && c !== lastContainer) {
lastContainer = c;
if (c.scrollLeft !== lastScrollLeft) {
c.scrollLeft = lastScrollLeft;
}
}
}).observe(parent, { childList: true });
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@@ -1,17 +1,23 @@
/*
* OrganonTweaks -- place the "Remove this Column" entry inside the native column menu.
* OrganonTweaks -- move plugin-injected DOM bits into their native positions on the board.
*
* Kanboard only exposes the template:board:column:dropdown hook AFTER the column menu
* <ul>, and its dropdown JS clones just that <ul> when the menu opens. So the hook item
* cannot reach the menu on its own. column_dropdown.php renders the item hidden next to
* the menu; this script moves each such item into that column's real <ul> so it appears
* alongside the native entries. It re-runs when the board is redrawn (AJAX polling /
* drag-and-drop refresh).
* Both fixes are needed because Kanboard's relevant hooks render at the END of their
* container, not where the item belongs:
*
* 1. "Remove this Column": the template:board:column:dropdown hook renders AFTER the column
* menu <ul> (and the dropdown JS clones only that <ul>), so we move the hidden <li> into
* the column's real <ul>.
* 2. "Always show comment icon": the template:board:task:icons hook renders at the end of
* the card icons row, so the "+" bubble lands after priority instead of in the native
* comment slot. We move it just before the description icon (where the native comment
* icon sits), so the order matches whether or not the card has comments.
*
* Both re-run when the board is redrawn (AJAX polling / drag-and-drop refresh).
*/
(function () {
"use strict";
function relocate() {
function relocateColumnMenuItems() {
var items = document.querySelectorAll(".organontweaks-remove-column-item");
for (var i = 0; i < items.length; i++) {
@@ -34,17 +40,53 @@
}
}
function repositionCommentIcons() {
var items = document.querySelectorAll(".organontweaks-comment-add");
for (var i = 0; i < items.length; i++) {
var item = items[i];
var row = item.parentNode;
if (!row) {
continue;
}
// Native comment icon sits just before the description icon; if there is no
// description, before the age / closed block. That is the slot we move into.
var anchor = null;
var descIcon = row.querySelector("i.fa-file-text-o");
if (descIcon) {
anchor = descIcon.closest("a");
}
if (!anchor) {
anchor = row.querySelector(".task-icon-age") || row.querySelector(".task-board-closed");
}
// Move only if needed (keeps the MutationObserver from looping).
if (anchor && anchor.parentNode === row && item.nextElementSibling !== anchor) {
row.insertBefore(item, anchor);
}
}
}
function apply() {
relocateColumnMenuItems();
repositionCommentIcons();
}
function init() {
if (!document.getElementById("board")) {
return;
}
relocate();
apply();
var container = document.getElementById("board-container");
if (container && window.MutationObserver) {
new MutationObserver(relocate).observe(container, { childList: true, subtree: true });
new MutationObserver(apply).observe(container, { childList: true, subtree: true });
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Kanboard\Plugin\OrganonTweaks\Controller;
use Kanboard\Controller\BaseController;
/**
* Settings page for the OrganonTweaks plugin: one toggle per tweak.
*/
class ConfigController extends BaseController
{
public function show()
{
$this->response->html($this->helper->layout->config('organonTweaks:config/show', array(
'title' => t('Settings').' &gt; '.t('Organon Tweaks'),
'values' => array(
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
'organon_tweaks_keep_scroll' => (int) $this->configModel->get('organon_tweaks_keep_scroll', 1),
),
'errors' => array(),
)));
}
public function save()
{
$values = $this->request->getValues();
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
$keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
if ($this->configModel->save(array(
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
'organon_tweaks_keep_scroll' => $keepScroll,
))) {
$this->flash->success(t('Settings saved successfully.'));
} else {
$this->flash->failure(t('Unable to save your settings.'));
}
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'OrganonTweaks')));
}
}

View File

@@ -16,6 +16,24 @@ class Plugin extends Base
$this->hook->on('template:layout:js', array(
'template' => 'plugins/OrganonTweaks/Asset/js/relocate.js',
));
// Settings page for the plugin's tweaks.
$this->template->hook->attach('template:config:sidebar', 'organonTweaks:config/sidebar');
// Tweak: always show a comment icon on board cards. When a card has no comments
// yet, add a bubble "+" that opens the same comment modal (add the first comment
// straight from the board). Cards with comments already show the native count.
if ((int) $this->configModel->get('organon_tweaks_always_comment_icon', 1) === 1) {
$this->template->hook->attach('template:board:task:icons', 'organonTweaks:board/task_comment_icon');
}
// Tweak: keep the board horizontal scroll position across refreshes (Kanboard
// rebuilds #board-container on card drop / polling, resetting scrollLeft to 0).
if ((int) $this->configModel->get('organon_tweaks_keep_scroll', 1) === 1) {
$this->hook->on('template:layout:js', array(
'template' => 'plugins/OrganonTweaks/Asset/js/keep-scroll.js',
));
}
}
public function getHelpers()
@@ -32,7 +50,7 @@ class Plugin extends Base
public function getPluginDescription()
{
return t('Umbrella plugin for small Kanboard tweaks. First tweak: remove an empty board column from its header menu.');
return t('Umbrella plugin for small Kanboard tweaks: remove an empty board column, always show the card comment icon, and more.');
}
public function getPluginAuthor()
@@ -42,7 +60,7 @@ class Plugin extends Base
public function getPluginVersion()
{
return '0.2.0';
return '1.0.0';
}
public function getPluginHomepage()

View File

@@ -22,6 +22,38 @@ and returns to the board.
- Only users allowed to manage columns see it, and the controller re-checks both the
permission and the emptiness before deleting.
### Always show the comment icon
Kanboard shows the comment icon on a board card only once it has at least one comment, so
adding the *first* comment means opening the card and finding the sidebar link. With this
tweak on, a card with no comments shows a comment bubble with a **"+"** that opens the same
comment modal, letting you add the first comment straight from the board. Cards that already
have comments keep showing the count as before.
- Rendered by the `template:board:task:icons` hook (`Template/board/task_comment_icon.php`),
only when the card has no comments and the user may create one (so it does not show for
read-only viewers).
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
### Keep the board scroll position across refreshes
When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the
board with `$("#board-container").replaceWith(...)`, and the new element starts scrolled to
the far left -- so the view jumps back to the first column. This tweak remembers the
horizontal scroll position and restores it as soon as the rebuilt board appears, so the
board stays where you were.
- Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container
itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current.
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
## Settings
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
- **Always show the comment icon on board cards** -- default on.
- **Keep the board scroll position across refreshes** -- default on.
## Requirements
- Kanboard >= 1.2.0

View File

@@ -0,0 +1,18 @@
<?php /*
* Rendered by the template:board:task:icons hook, inside the card icons row. When a card
* has no comments yet, Kanboard shows no comment icon at all -- so adding the first comment
* means opening the card and finding the sidebar link. This adds a comment bubble with a
* "+" that opens the same comment modal (CommentListController::show) the native count icon
* uses, so the first comment can be added straight from the board. Cards that already have
* comments render nothing here (the native count icon already shows).
*
* Only shown to users who may create a comment (CommentController::save is PROJECT_MEMBER),
* so it does not appear for read-only viewers.
*/ ?>
<?php if (empty($task['nb_comments']) && $this->user->hasProjectAccess('CommentController', 'save', $task['project_id'])): ?>
<?php /* The hook renders at the END of the icons row; relocate.js moves this span up to
the native comment slot (before the description icon) so the order matches. */ ?>
<span class="organontweaks-comment-add">
<?= $this->modal->medium('comments-o', '+', 'CommentListController', 'show', array('task_id' => $task['id']), t('Add a comment')) ?>
</span>
<?php endif ?>

25
Template/config/show.php Normal file
View File

@@ -0,0 +1,25 @@
<div class="page-header">
<h2><?= t('Organon Tweaks') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('ConfigController', 'save', array('plugin' => 'OrganonTweaks')) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<fieldset>
<?= $this->form->checkbox('organon_tweaks_always_comment_icon', t('Always show the comment icon on board cards'), 1, isset($values['organon_tweaks_always_comment_icon']) && $values['organon_tweaks_always_comment_icon'] == 1) ?>
<p class="form-help">
<?= t('Show a comment bubble with a "+" on cards that have no comments yet, so you can add the first comment straight from the board. Cards that already have comments keep showing the count as usual.') ?>
</p>
</fieldset>
<fieldset>
<?= $this->form->checkbox('organon_tweaks_keep_scroll', t('Keep the board scroll position across refreshes'), 1, isset($values['organon_tweaks_keep_scroll']) && $values['organon_tweaks_keep_scroll'] == 1) ?>
<p class="form-help">
<?= t('When a card is dropped (or the board auto-refreshes), Kanboard rebuilds the board and jumps back to the first column. This restores the horizontal scroll position so the board stays where you were.') ?>
</p>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div>
</form>

View File

@@ -0,0 +1,3 @@
<li <?= $this->app->checkMenuSelection('ConfigController', 'show', 'OrganonTweaks') ?>>
<?= $this->url->link(t('Organon Tweaks'), 'ConfigController', 'show', array('plugin' => 'OrganonTweaks')) ?>
</li>

View File

@@ -1 +1 @@
OrganonTweaks v0.2
OrganonTweaks v1.0