Skip to content

Commit

Permalink
Add master checkbox to Grid (#1921)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhuser authored Nov 24, 2024
1 parent c480f98 commit de0e11c
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 11 deletions.
23 changes: 23 additions & 0 deletions demos/_unit-test/grid-master-checkbox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Atk4\Ui\Demos;

use Atk4\Ui\App;
use Atk4\Ui\Grid;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsToast;

/** @var App $app */
require_once __DIR__ . '/../init-app.php';

$model = new Country($app->db);
$grid = Grid::addTo($app, ['ipp' => 5]);
$grid->setModel($model);

$grid->addSelection();

$grid->addBulkAction('Show selected', static function (Jquery $j, array $ids) use ($grid) {
return new JsToast('Selected: ' . implode(', ', array_map(static fn ($id) => $grid->getApp()->uiPersistence->typecastSaveField($grid->model->getIdField(), $id), $ids)) . '#');
});
62 changes: 62 additions & 0 deletions js/src/helpers/grid-checkbox.helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import $ from 'external/jquery';

function recomputeMasterCheckbox($table) {
const $masterCheckbox = $table.find('.master.checkbox');
const $childCheckbox = $table.find('.child.checkbox');

const checkedCount = $childCheckbox.filter('.checked').length;
const allChecked = checkedCount === $childCheckbox.length;
const allUnchecked = checkedCount === 0;

if (allChecked) {
$masterCheckbox.checkbox('set checked');
} else if (allUnchecked) {
$masterCheckbox.checkbox('set unchecked');
} else {
$masterCheckbox.checkbox('set indeterminate');
}
}

export default {
/**
* Simple helper for master and child checkboxes connection.
*/
setupMasterCheckbox: function (tableSelector) {
const $table = $(tableSelector);
let skipRecomputeMasterCheckbox = false;

$table.find('.master.checkbox').checkbox({
onChecked: function () {
const $childCheckbox = $table.find('.child.checkbox');

skipRecomputeMasterCheckbox = true;
try {
$childCheckbox.checkbox('check');
} finally {
skipRecomputeMasterCheckbox = false;
}
},

onUnchecked: function () {
const $childCheckbox = $table.find('.child.checkbox');

skipRecomputeMasterCheckbox = true;
try {
$childCheckbox.checkbox('uncheck');
} finally {
skipRecomputeMasterCheckbox = false;
}
},
});

$table.find('.child.checkbox').checkbox({
onChange: function () {
if (skipRecomputeMasterCheckbox) {
return;
}

recomputeMasterCheckbox($table);
},
});
},
};
2 changes: 2 additions & 0 deletions js/src/setup-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import $ from 'external/jquery';
import mitt from 'mitt';
import lodashDebounce from 'lodash/debounce';
import atk from 'atk';
import gridCheckboxHelper from './helpers/grid-checkbox.helper';
import tableDropdownHelper from './helpers/table-dropdown.helper';
import urlHelper from './helpers/url.helper';

Expand Down Expand Up @@ -82,6 +83,7 @@ atk.utils = {
},
};

atk.gridCheckboxHelper = gridCheckboxHelper;
atk.tableDropdownHelper = tableDropdownHelper;
atk.urlHelper = urlHelper;

Expand Down
98 changes: 92 additions & 6 deletions public/js/atkjs-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,89 @@
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({

/***/ "./src/helpers/grid-checkbox.helper.js":
/*!*********************************************!*\
!*** ./src/helpers/grid-checkbox.helper.js ***!
\*********************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var core_js_modules_esnext_async_iterator_filter_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/esnext.async-iterator.filter.js */ "./node_modules/core-js/modules/esnext.async-iterator.filter.js");
/* harmony import */ var core_js_modules_esnext_async_iterator_filter_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_esnext_async_iterator_filter_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_esnext_async_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/esnext.async-iterator.find.js */ "./node_modules/core-js/modules/esnext.async-iterator.find.js");
/* harmony import */ var core_js_modules_esnext_async_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_esnext_async_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var core_js_modules_esnext_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/esnext.iterator.constructor.js */ "./node_modules/core-js/modules/esnext.iterator.constructor.js");
/* harmony import */ var core_js_modules_esnext_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_esnext_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var core_js_modules_esnext_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/esnext.iterator.filter.js */ "./node_modules/core-js/modules/esnext.iterator.filter.js");
/* harmony import */ var core_js_modules_esnext_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_esnext_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var core_js_modules_esnext_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/esnext.iterator.find.js */ "./node_modules/core-js/modules/esnext.iterator.find.js");
/* harmony import */ var core_js_modules_esnext_iterator_find_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_esnext_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! external/jquery */ "external/jquery");
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(external_jquery__WEBPACK_IMPORTED_MODULE_5__);






function recomputeMasterCheckbox($table) {
const $masterCheckbox = $table.find('.master.checkbox');
const $childCheckbox = $table.find('.child.checkbox');
const checkedCount = $childCheckbox.filter('.checked').length;
const allChecked = checkedCount === $childCheckbox.length;
const allUnchecked = checkedCount === 0;
if (allChecked) {
$masterCheckbox.checkbox('set checked');
} else if (allUnchecked) {
$masterCheckbox.checkbox('set unchecked');
} else {
$masterCheckbox.checkbox('set indeterminate');
}
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
/**
* Simple helper for master and child checkboxes connection.
*/
setupMasterCheckbox: function (tableSelector) {
const $table = external_jquery__WEBPACK_IMPORTED_MODULE_5___default()(tableSelector);
let skipRecomputeMasterCheckbox = false;
$table.find('.master.checkbox').checkbox({
onChecked: function () {
const $childCheckbox = $table.find('.child.checkbox');
skipRecomputeMasterCheckbox = true;
try {
$childCheckbox.checkbox('check');
} finally {
skipRecomputeMasterCheckbox = false;
}
},
onUnchecked: function () {
const $childCheckbox = $table.find('.child.checkbox');
skipRecomputeMasterCheckbox = true;
try {
$childCheckbox.checkbox('uncheck');
} finally {
skipRecomputeMasterCheckbox = false;
}
}
});
$table.find('.child.checkbox').checkbox({
onChange: function () {
if (skipRecomputeMasterCheckbox) {
return;
}
recomputeMasterCheckbox($table);
}
});
}
});

/***/ }),

/***/ "./src/helpers/table-dropdown.helper.js":
/*!**********************************************!*\
!*** ./src/helpers/table-dropdown.helper.js ***!
Expand Down Expand Up @@ -3763,10 +3846,12 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! external/jquery */ "external/jquery");
/* harmony import */ var external_jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(external_jquery__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var mitt__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! mitt */ "./node_modules/mitt/dist/mitt.mjs");
/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! lodash/debounce */ "./node_modules/lodash/debounce.js");
/* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! lodash/debounce */ "./node_modules/lodash/debounce.js");
/* harmony import */ var atk__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! atk */ "./src/setup-atk.js");
/* harmony import */ var _helpers_table_dropdown_helper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./helpers/table-dropdown.helper */ "./src/helpers/table-dropdown.helper.js");
/* harmony import */ var _helpers_url_helper__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./helpers/url.helper */ "./src/helpers/url.helper.js");
/* harmony import */ var _helpers_grid_checkbox_helper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./helpers/grid-checkbox.helper */ "./src/helpers/grid-checkbox.helper.js");
/* harmony import */ var _helpers_table_dropdown_helper__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./helpers/table-dropdown.helper */ "./src/helpers/table-dropdown.helper.js");
/* harmony import */ var _helpers_url_helper__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./helpers/url.helper */ "./src/helpers/url.helper.js");




Expand Down Expand Up @@ -3820,7 +3905,7 @@ atk__WEBPACK_IMPORTED_MODULE_2__["default"].createDebouncedFx = function (func,
}, 25);
(external_jquery__WEBPACK_IMPORTED_MODULE_0___default().active)++;
}
lodashDebouncedFx = (0,lodash_debounce__WEBPACK_IMPORTED_MODULE_5__["default"])(func, wait, options);
lodashDebouncedFx = (0,lodash_debounce__WEBPACK_IMPORTED_MODULE_6__["default"])(func, wait, options);
function debouncedFx() {
if (timerId === null) {
createTimer();
Expand All @@ -3842,8 +3927,9 @@ atk__WEBPACK_IMPORTED_MODULE_2__["default"].utils = {
window.location = atk__WEBPACK_IMPORTED_MODULE_2__["default"].urlHelper.appendParams(url, params);
}
};
atk__WEBPACK_IMPORTED_MODULE_2__["default"].tableDropdownHelper = _helpers_table_dropdown_helper__WEBPACK_IMPORTED_MODULE_3__["default"];
atk__WEBPACK_IMPORTED_MODULE_2__["default"].urlHelper = _helpers_url_helper__WEBPACK_IMPORTED_MODULE_4__["default"];
atk__WEBPACK_IMPORTED_MODULE_2__["default"].gridCheckboxHelper = _helpers_grid_checkbox_helper__WEBPACK_IMPORTED_MODULE_3__["default"];
atk__WEBPACK_IMPORTED_MODULE_2__["default"].tableDropdownHelper = _helpers_table_dropdown_helper__WEBPACK_IMPORTED_MODULE_4__["default"];
atk__WEBPACK_IMPORTED_MODULE_2__["default"].urlHelper = _helpers_url_helper__WEBPACK_IMPORTED_MODULE_5__["default"];
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (null);

/***/ }),
Expand Down
2 changes: 1 addition & 1 deletion public/js/atkjs-ui.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js.map

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/Behat/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,30 @@ public function elementAttributeShouldContainText(string $selector, string $attr
}
}

/**
* @Then Element :arg1 should contain class :arg3
*/
public function elementShouldContainClass(string $selector, string $class): void
{
$element = $this->findElement(null, $selector);
$classes = explode(' ', $element->getAttribute('class'));
if (!in_array($class, $classes, true)) {
throw new \Exception('Element "' . $selector . '" does not contain "' . $class . '" class');
}
}

/**
* @Then Element :arg1 should not contain class :arg3
*/
public function elementShouldNotContainClass(string $selector, string $class): void
{
$element = $this->findElement(null, $selector);
$classes = explode(' ', $element->getAttribute('class'));
if (in_array($class, $classes, true)) {
throw new \Exception('Element "' . $selector . '" contains "' . $class . '" class');
}
}

// }}}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/Table/Column/Checkbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ public function getHeaderCellHtml(?Field $field = null, $value = null): string
->addMoreInfo('field', $field);
}
$this->table->js(true)->find('.' . $this->class)->checkbox();
$this->table->js(true, new JsExpression('atk.gridCheckboxHelper.setupMasterCheckbox([]);', [$this->table]));

return parent::getHeaderCellHtml($field);
return $this->getTag('head', [], [['div', ['class' => 'ui master fitted checkbox ' . $this->class], [['input/', ['type' => 'checkbox']]]]]);
}

#[\Override]
public function getDataCellTemplate(?Field $field = null): string
{
return $this->getApp()->getTag('div', ['class' => 'ui fitted checkbox ' . $this->class], [['input/', ['type' => 'checkbox']]]);
return $this->getApp()->getTag('div', ['class' => 'ui child fitted checkbox ' . $this->class], [['input/', ['type' => 'checkbox']]]);
}
}
29 changes: 29 additions & 0 deletions tests-behat/grid.feature
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,35 @@ Feature: Grid
Then No toast should be displayed
Then PATCH MINK the url should match "~_unit-test/grid-rowclick.php#test~"

Scenario: master checkbox
Given I am on "_unit-test/grid-master-checkbox.php"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
When I press button "Show selected"
Then Toast display should contain text "Selected: #"
When I click using selector "//tr[1]//div.ui.child.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should contain class "indeterminate"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 1#"
When I click using selector "//div.ui.master.checkbox"
Then Element "//div.ui.master.checkbox" should contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 1, 2, 3, 4, 5#"
When I click using selector "//div.ui.master.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
Then I press button "Show selected"
Then Toast display should contain text "Selected: #"
Then I click paginator page "2"
When I click using selector "//tr[2]//div.ui.child.checkbox"
When I click using selector "//tr[4]//div.ui.child.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should contain class "indeterminate"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 7, 9#"

Scenario: popup column header
Given I am on "collection/tablecolumnmenu.php"
Then I should not see "Name popup"
Expand Down

0 comments on commit de0e11c

Please sign in to comment.