Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17ebfcccd9 | |||
| 95561e5565 | |||
| ff6abf1d49 | |||
| 310c34620f | |||
| a5721890a6 | |||
| 9c27b51eed | |||
| 1bea749fed |
35
Asset/js/click-guard.js
Normal file
35
Asset/js/click-guard.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* OrganonTweaks -- only open a card on a quick click.
|
||||
*
|
||||
* A real click is a quick press-release; a drag is a press, some movement, and a release
|
||||
* much later. When you drop a card, jQuery UI can leak a trailing "click" on it, and
|
||||
* Kanboard opens the card -- an unwanted side effect of the drag.
|
||||
*
|
||||
* We simply time the press on a card: record the moment of mousedown / touchstart, and on
|
||||
* the click that follows, if more than THRESHOLD ms elapsed the press was a drag (held),
|
||||
* not a click, so we swallow the click in the capture phase before Kanboard's open handler
|
||||
* runs. A normal click is well under the threshold and opens the card as usual.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var THRESHOLD = 100; // ms; a press on a card held longer than this is a drag, not a click
|
||||
var pressStart = 0;
|
||||
|
||||
function onDown(e) {
|
||||
pressStart = (e.target && e.target.closest && e.target.closest(".task-board")) ? Date.now() : 0;
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", onDown, true);
|
||||
document.addEventListener("touchstart", onDown, true);
|
||||
|
||||
document.addEventListener("click", function (e) {
|
||||
if (pressStart &&
|
||||
e.target && e.target.closest && e.target.closest(".task-board") &&
|
||||
(Date.now() - pressStart) > THRESHOLD) {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
pressStart = 0;
|
||||
}, true);
|
||||
})();
|
||||
94
Asset/js/enhance-recurrence.js
Normal file
94
Asset/js/enhance-recurrence.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* OrganonTweaks -- enhance Kanboard's native "Edit recurrence" dialog.
|
||||
*
|
||||
* That template (task_recurrence/edit.php) has no hook, so we transform the form in JS when
|
||||
* the modal opens:
|
||||
* 1. Replace the "Generate recurrent task" Yes/No dropdown with two radio buttons (one
|
||||
* click instead of open + select). The original <select> is kept, hidden, and stays in
|
||||
* sync, so the form still submits recurrence_status normally.
|
||||
* 2. Add a small [+7] button next to the "Factor" input that adds 7 (a week of days).
|
||||
*
|
||||
* Detection: the modal is injected into #modal-box dynamically, so we watch the document for
|
||||
* an un-enhanced recurrence form appearing. Loaded only when the setting is enabled.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function enhanceStatus(select) {
|
||||
var group = document.createElement("span");
|
||||
group.className = "ot-recurrence-status";
|
||||
|
||||
for (var i = 0; i < select.options.length; i++) {
|
||||
var opt = select.options[i];
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.style.marginRight = "14px";
|
||||
label.style.cursor = "pointer";
|
||||
|
||||
var radio = document.createElement("input");
|
||||
radio.type = "radio";
|
||||
radio.name = "ot-recurrence-status";
|
||||
radio.value = opt.value;
|
||||
radio.checked = opt.value === select.value;
|
||||
radio.style.marginRight = "4px";
|
||||
radio.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
select.value = this.value;
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
});
|
||||
|
||||
label.appendChild(radio);
|
||||
label.appendChild(document.createTextNode(opt.text));
|
||||
group.appendChild(label);
|
||||
}
|
||||
|
||||
select.style.display = "none";
|
||||
select.parentNode.insertBefore(group, select.nextSibling);
|
||||
}
|
||||
|
||||
function enhanceFactor(input) {
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "btn";
|
||||
btn.textContent = "+7";
|
||||
btn.title = "Add 7 (one week of days)";
|
||||
btn.style.marginLeft = "6px";
|
||||
btn.addEventListener("click", function () {
|
||||
input.value = (parseInt(input.value, 10) || 0) + 7;
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
input.parentNode.insertBefore(btn, input.nextSibling);
|
||||
}
|
||||
|
||||
function enhance() {
|
||||
var select = document.querySelector('select[name="recurrence_status"]:not([data-ot-recurrence])');
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
|
||||
select.setAttribute("data-ot-recurrence", "1");
|
||||
enhanceStatus(select);
|
||||
|
||||
var factor = document.querySelector('input[name="recurrence_factor"]:not([data-ot-recurrence])');
|
||||
if (factor) {
|
||||
factor.setAttribute("data-ot-recurrence", "1");
|
||||
enhanceFactor(factor);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
enhance();
|
||||
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(enhance).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
55
Asset/js/keep-scroll.js
Normal file
55
Asset/js/keep-scroll.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
@@ -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,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// NB: the description icon is wrapped in <span class="tooltip"> (Kanboard's
|
||||
// tooltipLink), not an <a>, so we walk up to whatever is the row's direct child.
|
||||
var anchor = row.querySelector("i.fa-file-text-o") ||
|
||||
row.querySelector(".task-icon-age") ||
|
||||
row.querySelector(".task-board-closed");
|
||||
|
||||
while (anchor && anchor.parentNode !== row) {
|
||||
anchor = anchor.parentNode;
|
||||
}
|
||||
|
||||
// Move only if needed (keeps the MutationObserver from looping).
|
||||
if (anchor && anchor !== item && item.nextElementSibling !== anchor) {
|
||||
row.insertBefore(item, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function apply() {
|
||||
relocateColumnMenuItems();
|
||||
repositionCommentIcons();
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!document.getElementById("board")) {
|
||||
var container = document.getElementById("board-container");
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
relocate();
|
||||
apply();
|
||||
|
||||
var container = document.getElementById("board-container");
|
||||
// Observe the STABLE parent, not #board-container: Kanboard replaces the container
|
||||
// on every card drop / AJAX poll (BoardDragAndDrop.refresh), which would kill an
|
||||
// observer bound to the container itself and leave the items un-relocated after a
|
||||
// move. The parent survives, so apply() keeps running on every rebuild.
|
||||
var parent = container.parentNode;
|
||||
|
||||
if (container && window.MutationObserver) {
|
||||
new MutationObserver(relocate).observe(container, { childList: true, subtree: true });
|
||||
if (parent && window.MutationObserver) {
|
||||
new MutationObserver(apply).observe(parent, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ class ConfigController extends BaseController
|
||||
'title' => t('Settings').' > '.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),
|
||||
'organon_tweaks_quick_click_only' => (int) $this->configModel->get('organon_tweaks_quick_click_only', 1),
|
||||
'organon_tweaks_enhance_recurrence' => (int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1),
|
||||
),
|
||||
'errors' => array(),
|
||||
)));
|
||||
@@ -25,8 +28,14 @@ class ConfigController extends BaseController
|
||||
$values = $this->request->getValues();
|
||||
|
||||
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
|
||||
$keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
|
||||
$quickClickOnly = isset($values['organon_tweaks_quick_click_only']) ? 1 : 0;
|
||||
|
||||
if ($this->configModel->save(array('organon_tweaks_always_comment_icon' => $alwaysCommentIcon))) {
|
||||
if ($this->configModel->save(array(
|
||||
'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
|
||||
'organon_tweaks_keep_scroll' => $keepScroll,
|
||||
'organon_tweaks_quick_click_only' => $quickClickOnly,
|
||||
))) {
|
||||
$this->flash->success(t('Settings saved successfully.'));
|
||||
} else {
|
||||
$this->flash->failure(t('Unable to save your settings.'));
|
||||
|
||||
26
Plugin.php
26
Plugin.php
@@ -26,6 +26,30 @@ class Plugin extends Base
|
||||
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',
|
||||
));
|
||||
}
|
||||
|
||||
// Tweak: only open a card on a quick click (a press held > 100ms is a drag, so the
|
||||
// trailing click is swallowed and the card does not open).
|
||||
if ((int) $this->configModel->get('organon_tweaks_quick_click_only', 1) === 1) {
|
||||
$this->hook->on('template:layout:js', array(
|
||||
'template' => 'plugins/OrganonTweaks/Asset/js/click-guard.js',
|
||||
));
|
||||
}
|
||||
|
||||
// Tweak: enhance the native "Edit recurrence" dialog (Yes/No radios + a [+7] button
|
||||
// on the factor). No template hook there, so it is done in JS.
|
||||
if ((int) $this->configModel->get('organon_tweaks_enhance_recurrence', 1) === 1) {
|
||||
$this->hook->on('template:layout:js', array(
|
||||
'template' => 'plugins/OrganonTweaks/Asset/js/enhance-recurrence.js',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function getHelpers()
|
||||
@@ -52,7 +76,7 @@ class Plugin extends Base
|
||||
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '0.3.0';
|
||||
return '1.3.0';
|
||||
}
|
||||
|
||||
public function getPluginHomepage()
|
||||
|
||||
39
README.md
39
README.md
@@ -35,11 +35,50 @@ have comments keep showing the count as before.
|
||||
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".
|
||||
|
||||
### Open a card only on a quick click
|
||||
|
||||
Dropping a card sometimes leaks a trailing "click" that opens the card -- an unwanted side
|
||||
effect of the drag. A real click is a quick press-release; a drag is a press held much
|
||||
longer. So `Asset/js/click-guard.js` times the press on a card (`mousedown`/`touchstart` to
|
||||
the following `click`) and, if it was held longer than **100 ms**, swallows the click in the
|
||||
capture phase before Kanboard's open handler -- so a drag never opens the card. Normal
|
||||
clicks (well under 100 ms) still open cards as usual. The 100 ms threshold is fixed.
|
||||
|
||||
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
|
||||
|
||||
### Enhance the recurrence dialog
|
||||
|
||||
Kanboard's native "Edit recurrence" dialog is a plain form. This makes two small usability
|
||||
edits when it opens (there is no template hook, so it is done in JS,
|
||||
`Asset/js/enhance-recurrence.js`):
|
||||
|
||||
- the "Generate recurrent task" **Yes/No dropdown** becomes two **radio buttons** (one click
|
||||
to set, instead of open + select). The original select is kept hidden and stays in sync, so
|
||||
the form submits exactly as before.
|
||||
- a small **[+7]** button next to the "Factor" input adds 7 to it (one week of days).
|
||||
|
||||
- **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.
|
||||
- **Open a card only on a quick click** -- default on.
|
||||
- **Enhance the recurrence dialog** -- default on.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
* 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 ?>
|
||||
|
||||
@@ -12,6 +12,27 @@
|
||||
</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>
|
||||
|
||||
<fieldset>
|
||||
<?= $this->form->checkbox('organon_tweaks_quick_click_only', t('Open a card only on a quick click'), 1, isset($values['organon_tweaks_quick_click_only']) && $values['organon_tweaks_quick_click_only'] == 1) ?>
|
||||
<p class="form-help">
|
||||
<?= t('A press on a card held longer than 100 ms is treated as a drag, so releasing it does not open the card. Fixes a drag sometimes opening the card on release. Normal clicks are well under 100 ms.') ?>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<?= $this->form->checkbox('organon_tweaks_enhance_recurrence', t('Enhance the recurrence dialog'), 1, isset($values['organon_tweaks_enhance_recurrence']) && $values['organon_tweaks_enhance_recurrence'] == 1) ?>
|
||||
<p class="form-help">
|
||||
<?= t('In the native "Edit recurrence" dialog, replace the Generate recurrent task dropdown with Yes/No radio buttons (one click), and add a [+7] button next to the factor that adds 7 (one week of days).') ?>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user