forked from decidim/decidim
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix use of browse history with filters (decidim#5749)
* 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
Showing
9 changed files
with
384 additions
and
208 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
292 changes: 164 additions & 128 deletions
292
decidim-core/app/assets/javascripts/decidim/check_boxes_tree.js.es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
26
decidim-core/app/assets/javascripts/decidim/delayed.js.es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Oops, something went wrong.