Skip to content

Commit

Permalink
Fix use of browse history with filters (decidim#5749)
Browse files Browse the repository at this point in the history
* Fix bug on decidim#5654

Parent checkbox updating was failing

* Add CheckBoxesTree component to Decidim exports

* Fix search updating when using the browser history

* Push initial filters state to URL to allow returning back to that page

* Additional tests

* Changelog entry added

* Move delayed function to a separate file

Triggered by "Too long file" linting message

* Move check_boxes_tree related code to the component

Triggered by "Too long file" linting message

* Avoid unnecesary lets

* Fix and complete JS tests

* Fix lint issues

* Fix rubocop issue

* Undo 71ef93a and fix bug on decidim#5654

The problem was caused because scopes and categories trees where different.

* Use history state to store initial filters, instead of using URL

Changing the initial URL breaks too many things

Co-authored-by: Oliver Valls <tramuntanal@gmail.com>
  • Loading branch information
2 people authored and mrcasals committed Mar 11, 2020
1 parent d05bc3c commit 833bd6c
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 208 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -101,6 +101,7 @@ Thanks to [#5342](https://github.com/decidim/decidim/pull/5342), Decidim now sup
- **decidim-core**: Expand top-level navigation on mobile by default [#5580](https://github.com/decidim/decidim/pull/5580)
- **decidim-proposals**: Filtering by state working when searching amendments [#5703](https://github.com/decidim/decidim/pull/5703)
- **decidim-core**: Fix: Display values on translated fields with hashtaggable option on edit forms [#5661](https://github.com/decidim/decidim/pull/5661)
- **decidim-core**: Fix: use of browse history with filters [#5749](https://github.com/decidim/decidim/pull/5749)
- **decidim-budgets**: Add a missing fix applied to proposals in [\#5654](https://github.com/decidim/decidim/pull/5654) but not to projects [\#5743](https://github.com/decidim/decidim/pull/5743)
- **decidim-proposals**: Admin: fix "Answer Proposal" action tooltip [/#5750](https://github.com/decidim/decidim/pull/5750)

Expand Down
4 changes: 4 additions & 0 deletions decidim-core/app/assets/javascripts/decidim.js.es6
Expand Up @@ -24,6 +24,7 @@
// = require decidim/tooltip_keep_on_hover
// = require decidim/diff_mode_dropdown
// = require decidim/check_boxes_tree
// = require decidim/delayed
// = require_tree ./decidim/vizzs

// = require_self
Expand All @@ -37,6 +38,9 @@ $(() => {
if (window.Decidim.DataPicker) {
window.theDataPicker = new window.Decidim.DataPicker($(".data-picker"));
}
if (window.Decidim.CheckBoxesTree) {
window.theCheckBoxesTree = new window.Decidim.CheckBoxesTree();
}

$(document).foundation();

Expand Down
292 changes: 164 additions & 128 deletions decidim-core/app/assets/javascripts/decidim/check_boxes_tree.js.es6
@@ -1,154 +1,190 @@
/**
* Checkboxes tree component.
* CheckBoxesTree component.
*/
class CheckBoxesTree {
constructor() {
this.checkboxesTree = document.querySelectorAll("[data-checkboxes-tree]");
if (!this.checkboxesTree) {
return;
((exports) => {
class CheckBoxesTree {
constructor() {
this.checkboxesTree = document.querySelectorAll("[data-checkboxes-tree]");
if (!this.checkboxesTree) {
return;
}

this.globalChecks = document.querySelectorAll("[data-global-checkbox] input");
this.globalChecks.forEach((global) => {
if (global.value === "") {
global.classList.add("ignore-filter")
}
});
this.checkGlobalCheck();

// Event listeners
this.checkboxesTree.forEach((input) => input.addEventListener("click", (event) => this.checkTheCheckBoxes(event.target)));
document.querySelectorAll("[data-children-checkbox] input").forEach((input) => {
input.addEventListener("change", (event) => this.checkTheCheckParent(event.target));
});

// Review parent checkboxes on initial load
document.querySelectorAll("[data-children-checkbox] input").forEach((input) => {
this.checkTheCheckParent(input);
});
}

this.globalChecks = document.querySelectorAll("[data-global-checkbox] input");
this.globalChecks.forEach((global) => {
if (global.value === "") {
global.classList.add("ignore-filter")
}
});
this.checkGlobalCheck();

// Event listeners
this.checkboxesTree.forEach((input) => input.addEventListener("click", this.checkTheCheckBoxes));
document.querySelectorAll("[data-children-checkbox] input").forEach((input) => {
input.addEventListener("change", (event) => this.checkTheCheckParent(event.target));
});

// Review parent checkboxes on initial load
document.querySelectorAll("[data-children-checkbox] input").forEach((input) => {
this.checkTheCheckParent(input);
});
}
/**
* Set checkboxes as checked if included in given values
* @public
* @param {Array} checkboxes - array of checkboxs to check
* @param {Array} values - values of checkboxes that should be checked
* @returns {Void} - Returns nothing.
*/
updateChecked(checkboxes, values) {
checkboxes.each((index, checkbox) => {
if ((checkbox.value === "" && values.length === 1) || (checkbox.value !== "" && values.includes(checkbox.value))) {
checkbox.checked = true;
this.checkTheCheckBoxes(checkbox);
this.checkTheCheckParent(checkbox);
}
});
}

/**
* Handles the click action on any checkbox.
* @private
* @param {Event} event - the click event related information
* @returns {Void} - Returns nothing.
*/
checkTheCheckBoxes(event) {
// Quis custodiet ipsos custodes?
const targetChecks = event.target.dataset.checkboxesTree;
const checkStatus = event.target.checked;
const allChecks = document.querySelectorAll(`#${targetChecks} input[type='checkbox']`);

allChecks.forEach((input) => {
input.checked = checkStatus;
input.indeterminate = false;
input.classList.add("ignore-filter");
});
}
/**
* Set the container form(s) for the component, to disable ignored filters before submitting them
* @public
* @param {query} theForm - form or forms where the component will be used
* @returns {Void} - Returns nothing.
*/
setContainerForm(theForm) {
theForm.on("submit ajax:before", () => {
theForm.find(".ignore-filters input, input.ignore-filter").each((idx, elem) => {
elem.disabled = true;
});
});

theForm.on("ajax:send", () => {
theForm.find(".ignore-filters input, input.ignore-filter").each((idx, elem) => {
elem.disabled = false;
});
});
}

/**
* Handles the click action on any checkbox.
* @private
* @param {Input} target - the input that has been checked
* @returns {Void} - Returns nothing.
*/
checkTheCheckBoxes(target) {
// Quis custodiet ipsos custodes?
const targetChecks = target.dataset.checkboxesTree;
const checkStatus = target.checked;
const allChecks = document.querySelectorAll(`#${targetChecks} input[type='checkbox']`);

allChecks.forEach((input) => {
input.checked = checkStatus;
input.indeterminate = false;
input.classList.add("ignore-filter");
});
}

/**
* Update global checkboxes state when the currention selection changes
* @private
* @returns {Void} - Returns nothing.
*/
checkGlobalCheck() {
this.globalChecks.forEach((global) => {
const checksContext = global.dataset.checkboxesTree;
const totalInputs = document.querySelectorAll(
`#${checksContext} input[type='checkbox']`
/**
* Update global checkboxes state when the current selection changes
* @private
* @returns {Void} - Returns nothing.
*/
checkGlobalCheck() {
this.globalChecks.forEach((global) => {
const checksContext = global.dataset.checkboxesTree;
const totalInputs = document.querySelectorAll(
`#${checksContext} input[type='checkbox']`
);
const checkedInputs = document.querySelectorAll(
`#${checksContext} input[type='checkbox']:checked`
);
const indeterminateInputs = Array.from(checkedInputs).filter((checkbox) => checkbox.indeterminate)

if (checkedInputs.length === 0) {
global.checked = false;
global.indeterminate = false;
} else if (checkedInputs.length === totalInputs.length && indeterminateInputs.length === 0) {
global.checked = true;
global.indeterminate = false;
} else {
global.checked = true;
global.indeterminate = true;
}

totalInputs.forEach((input) => {
if (global.indeterminate && !input.indeterminate) {
input.classList.remove("ignore-filter");
} else {
input.classList.add("ignore-filter");
}
const subfilters = input.parentNode.parentNode.nextElementSibling;
if (subfilters && subfilters.classList.contains("filters__subfilters")) {
if (input.indeterminate) {
subfilters.classList.remove("ignore-filters");
} else {
subfilters.classList.add("ignore-filters");
}
}
});
});
}

/**
* Update children checkboxes state when the current selection changes
* @private
* @param {Input} input - the checkbox to check its parent
* @returns {Void} - Returns nothing.
*/
checkTheCheckParent(input) {
const checkBoxContext = $(input).parents(".filters__subfilters").attr("id");
if (!checkBoxContext) {
this.checkGlobalCheck();
return;
}

const parentCheck = document.querySelector(
`[data-checkboxes-tree=${checkBoxContext}]`
);
const checkedInputs = document.querySelectorAll(
`#${checksContext} input[type='checkbox']:checked`
const totalCheckSiblings = document.querySelectorAll(
`#${checkBoxContext} > div > [data-children-checkbox] > input, #${checkBoxContext} > [data-children-checkbox] > input`
);
const indeterminateInputs = document.querySelectorAll(
`#${checksContext} input[type='checkbox']:indeterminate`
const checkedSiblings = document.querySelectorAll(
`#${checkBoxContext} > div > [data-children-checkbox] > input:checked, #${checkBoxContext} > [data-children-checkbox] > input:checked`
);
const indeterminateSiblings = Array.from(checkedSiblings).filter((checkbox) => checkbox.indeterminate)

if (checkedInputs.length === 0) {
global.checked = false;
global.indeterminate = false;
} else if (checkedInputs.length === totalInputs.length && indeterminateInputs.length === 0) {
global.checked = true;
global.indeterminate = false;
if (checkedSiblings.length === 0) {
parentCheck.checked = false;
parentCheck.indeterminate = false;
} else if (checkedSiblings.length === totalCheckSiblings.length && indeterminateSiblings.length === 0) {
parentCheck.checked = true;
parentCheck.indeterminate = false;
} else {
global.checked = true;
global.indeterminate = true;
parentCheck.checked = true;
parentCheck.indeterminate = true;
}

totalInputs.forEach((input) => {
if (global.indeterminate && !input.indeterminate) {
input.classList.remove("ignore-filter");
totalCheckSiblings.forEach((sibling) => {
if (parent.indeterminate && !sibling.indeterminate) {
sibling.classList.remove("ignore-filter");
} else {
input.classList.add("ignore-filter");
sibling.classList.add("ignore-filter");
}
const subfilters = input.parentNode.parentNode.nextElementSibling;
const subfilters = sibling.parentNode.parentNode.nextElementSibling;
if (subfilters && subfilters.classList.contains("filters__subfilters")) {
if (input.indeterminate) {
if (sibling.indeterminate) {
subfilters.classList.remove("ignore-filters");
} else {
subfilters.classList.add("ignore-filters");
}
}
});
});
}

/**
* Update children checkboxes state when the currention selection changes
* @private
* @param {Input} input - the checkbox to check its parent
* @returns {Void} - Returns nothing.
*/
checkTheCheckParent(input) {
const checkBoxContext = input.parentNode.parentNode.parentNode.getAttribute("id");
if (!checkBoxContext) {
this.checkGlobalCheck();
return;
}

const parentCheck = document.querySelector(
`[data-checkboxes-tree=${checkBoxContext}]`
);
const totalCheckSiblings = document.querySelectorAll(
`#${checkBoxContext} > div > [data-children-checkbox] > input`
);
const checkedSiblings = document.querySelectorAll(
`#${checkBoxContext} > div > [data-children-checkbox] > input:checked`
);
const indeterminateSiblings = document.querySelectorAll(
`#${checkBoxContext} > div > [data-children-checkbox] > input:indeterminate`
);

if (checkedSiblings.length === 0) {
parentCheck.checked = false;
parentCheck.indeterminate = false;
} else if (checkedSiblings.length === totalCheckSiblings.length && indeterminateSiblings.length === 0) {
parentCheck.checked = true;
parentCheck.indeterminate = false;
} else {
parentCheck.checked = true;
parentCheck.indeterminate = true;
this.checkTheCheckParent(parentCheck);
}

totalCheckSiblings.forEach((sibling) => {
if (parent.indeterminate && !sibling.indeterminate) {
sibling.classList.remove("ignore-filter");
} else {
sibling.classList.add("ignore-filter");
}
const subfilters = sibling.parentNode.parentNode.nextElementSibling;
if (subfilters && subfilters.classList.contains("filters__subfilters")) {
if (sibling.indeterminate) {
subfilters.classList.remove("ignore-filters");
} else {
subfilters.classList.add("ignore-filters");
}
}
});

this.checkTheCheckParent(parentCheck);
}
}

$(() => new CheckBoxesTree());
exports.Decidim = exports.Decidim || {};
exports.Decidim.CheckBoxesTree = CheckBoxesTree;
})(window);
26 changes: 26 additions & 0 deletions decidim-core/app/assets/javascripts/decidim/delayed.js.es6
@@ -0,0 +1,26 @@
((exports) => {

/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds.
* @param {Object} context - the context for the called function.
* @param {Function} func - the function to be executed.
* @param {int} wait - number of milliseconds to wait before executing the function.
* @private
* @returns {Void} - Returns nothing.
*/
exports.delayed = (context, func, wait) => {
let timeout = null;

return function(...args) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = null;
Reflect.apply(func, context, args);
}, wait);
}
}
})(window);

0 comments on commit 833bd6c

Please sign in to comment.