Skip to content

Fix bugs with selectors #1164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 2 additions & 4 deletions assets/datagrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ export class Datagrid extends EventTarget {
this.ajax.addEventListener("success", ({detail: {payload}}) => {
// todo: maybe move?
if (payload._datagrid_name && payload._datagrid_name === this.name) {
this.el.querySelector<HTMLElement>("[data-datagrid-reset-filter-by-column]")
?.classList.add("hidden");
this.el.querySelectorAll<HTMLElement>("[data-datagrid-reset-filter-by-column]").forEach( el => el.classList.add("hidden"))

if (payload.non_empty_filters && payload.non_empty_filters.length >= 1) {
const resets = Array.from<HTMLElement>(this.el.querySelectorAll(
Expand All @@ -90,9 +89,8 @@ export class Datagrid extends EventTarget {
"data-datagrid-reset-filter-by-column"
)

/// tf?
for (const columnName of payload.non_empty_filters) {
resets.find(getColumnName)?.classList.remove("hidden");
resets.find(el => getColumnName(el) === columnName)?.classList.remove("hidden");
}

const href = this.el.querySelector(".reset-filter")
Expand Down
47 changes: 30 additions & 17 deletions assets/integrations/happy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { happyStyles } from "../css/happy.css";
export class Happy {
private colors: string[] = ["primary", "success", "info", "warning", "danger", "white", "gray"];

private sheet :null|CSSStyleSheet = null;

private templates = {
radio: '<div class="happy-radio"><b></b></div>',
checkbox:
Expand All @@ -15,8 +17,9 @@ export class Happy {
};

init() {
if (!document.querySelector('[data-happy-stylesheet]')) {
document.head.append(`<style data-happy-stylesheet>${happyStyles}</style>`)

if(!this.sheet) {
this.firstInit()
}
this.removeBySelector(".happy-radio");
this.removeBySelector(".happy-checkbox");
Expand All @@ -25,6 +28,24 @@ export class Happy {
this.initCheckbox();
}

firstInit(){
const styleElement = new CSSStyleSheet();
styleElement.replaceSync(happyStyles);
document.adoptedStyleSheets.push(styleElement);
this.sheet = styleElement;

/**
* The first init call, adds handlers for fancy looking checkboxes. The callback neds only be added once.
*/
document.addEventListener("click", this.checkCheckboxStateOnClick.bind(this));
document.addEventListener("change", this.checkCheckboxStateOnChange.bind(this));

/**
* Set aciton functionality for native change of radios
*/
document.addEventListener("change", this.radioOnChange.bind(this));
}

/**
* @deprecated
*/
Expand Down Expand Up @@ -90,15 +111,11 @@ export class Happy {
* Init state
*/
this.checkRadioState(input);

/**
* Set aciton functionality for native change
*/
document.addEventListener("change", this.radioOnChange.bind(this));
});
}

initCheckbox() {

document.querySelectorAll<HTMLInputElement>("input[type=checkbox].happy").forEach(input => {
/**
* Paste happy component into html
Expand All @@ -123,12 +140,6 @@ export class Happy {
* Init state
*/
this.checkCheckboxState(input);

/**
* Set action functionality for click || native change
*/
document.addEventListener("click", this.checkCheckboxStateOnClick.bind(this));
document.addEventListener("change", this.checkCheckboxStateOnChange.bind(this));
});
}

Expand All @@ -144,7 +155,6 @@ export class Happy {
: target instanceof SVGGraphicsElement
? target.closest("svg")?.parentNode
: target;

if (!(happyInput instanceof HTMLElement) || !happyInput.classList.contains("happy-checkbox")) {
return;
}
Expand All @@ -155,14 +165,17 @@ export class Happy {
const value = happyInput.getAttribute("data-value");

const input = document.querySelector(
`.happy-checkbox[data-name="${name}"]` + (!!value ? `[value="${value}"]` : "")
`input[type=checkbox].happy[name="${name}"]` + (!!value ? `[value="${value}"]` : "")
);
if (!(input instanceof HTMLInputElement)) return;

const checked = happyInput.classList.contains("active");

input.checked = !checked;
checked ? happyInput.classList.remove("active") : happyInput.classList.add("active");
if(input.checked !== !checked){
const evt = new Event('change',{bubbles: false, cancelable: true,composed: false});
input.checked = !checked;
input.dispatchEvent(evt);
}
}

checkCheckboxStateOnChange({target}: Event) {
Expand Down
10 changes: 9 additions & 1 deletion assets/integrations/tom-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ export class TomSelect implements Selectpicker {
const Select = this.select ?? window()?.TomSelect ?? null;

if (Select) {
elements.forEach(element => new Select(

elements.forEach(element => {
if(element.hasAttribute('data-Tom-Initialised')){
return;
}
element.setAttribute('data-Tom-Initialised','true');

new Select(
element as TomInput,
typeof this.opts === "function" ? this.opts(element) : this.opts)
}
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions assets/plugins/features/autosubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export class AutosubmitPlugin implements DatagridPlugin {
if (!inputEl) {
inputEl = pageSelectEl.parentElement?.querySelector("button[type=submit]");
}
console.log({ inputEl });
//console.log({ inputEl });
if (!(inputEl instanceof HTMLElement)) return;
const form = inputEl.closest('form');
console.log({ form });
//console.log({ form });
form && datagrid.ajax.submitForm(form);
});
});
Expand Down
124 changes: 80 additions & 44 deletions assets/plugins/features/checkboxes.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
import { DatagridPlugin } from "../../types";
import { Datagrid } from "../..";
import {DatagridPlugin} from "../../types";
import {Datagrid} from "../..";

export const CheckboxAttribute = "data-check";

export class CheckboxPlugin implements DatagridPlugin {

private wasInit: Array<Datagrid> = [];

loadCheckboxCount(datagrid: Datagrid) {
const counter = document.querySelector<HTMLElement>(".datagrid-selected-rows-count");
const total = Array.from(datagrid.el.querySelectorAll<HTMLInputElement>(`input[data-check='${datagrid.name}']`)).filter(c => !c.hasAttribute("data-check-all"));
const checked = total.filter(e => (e.checked));
if (counter) {
counter.innerText = `${checked.length}/${total.length}`;
}
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>(
".row-group-actions *[type='submit']"
).forEach(button => {
button.disabled = checked.length === 0;
});
const select = datagrid.el.querySelector<HTMLSelectElement>("select[name='group_action[group_action]']");
if (select) {
select.disabled = checked.length === 0;
}
}

onDatagridInit(datagrid: Datagrid): boolean {
let lastCheckbox = null;
if (!this.wasInit.includes(datagrid)) {
datagrid.ajax.addEventListener('complete', () => {
this.onDatagridInit(datagrid)
});
this.wasInit.push(datagrid);
this.loadCheckboxCount(datagrid);
}
this.loadCheckboxCount(datagrid);

datagrid.el.addEventListener("click", e => {
if (!(e.target instanceof HTMLElement)) return;
let lastCheckbox: null | HTMLElement = null;

datagrid.el.addEventListener("click", e => {
if (!(e.target instanceof HTMLElement)) {
return;
}
if (e.target.classList.contains("col-checkbox")) {
lastCheckbox = e.target;
if (e.shiftKey && lastCheckbox) {
const currentCheckboxRow = lastCheckbox.closest("tr");
const currentCheckboxRow = e.target.closest("tr");
if (!currentCheckboxRow) return;

const lastCheckboxRow = lastCheckbox.closest("tr");
Expand All @@ -23,68 +53,74 @@ export class CheckboxPlugin implements DatagridPlugin {
if (!lastCheckboxTbody) return;

const checkboxesRows = Array.from(lastCheckboxTbody.querySelectorAll<HTMLElement>("tr"));
const [start, end] = [lastCheckboxRow.rowIndex, currentCheckboxRow.rowIndex].sort();
const headerRows = Array.from(lastCheckboxTbody.closest('table')?.querySelectorAll<HTMLElement>("thead tr") ?? []).length;

const [start, end] = [lastCheckboxRow.rowIndex -headerRows, currentCheckboxRow.rowIndex -headerRows].sort();
const rows = checkboxesRows.slice(start, end + 1);

rows.forEach(row => {
const input = row.querySelector<HTMLInputElement>('.col-checkbox input[type="checkbox"]');
if (input) {
input.checked = true;
if (!input.checked) {
input.checked = true;
input.dispatchEvent(new Event('change', {bubbles: true}))
}

}
});
}
lastCheckbox = e.target;
}
});


let checkboxes = datagrid.el.querySelectorAll<HTMLInputElement>(`input[data-check='${datagrid.name}']`);
const select = datagrid.el.querySelector<HTMLSelectElement>("select[name='group_action[group_action]']");
const actionButtons = document.querySelectorAll<HTMLInputElement | HTMLButtonElement>(
".row-group-actions *[type='submit']"
);
const counter = document.querySelector<HTMLElement>(".datagrid-selected-rows-count");

// Handling a checkbox click + select all checkbox
let notUserInvoked = false;
checkboxes.forEach(checkEl => {
checkEl.addEventListener("change", () => {
// Select all
const isSelectAll = checkEl.hasAttribute("data-check-all");
if (isSelectAll) {
if (datagrid.name !== checkEl.getAttribute("data-check-all")) return;

checkboxes.forEach(checkbox => (checkbox.checked = checkEl.checked));

// todo: refactor not to repeat this code twice
actionButtons.forEach(button => (button.disabled = !checkEl.checked));

if (select) {
select.disabled = !checkEl.checked;
}

if (counter) {
const total = Array.from(checkboxes).filter(c => !c.hasAttribute("data-check-all")).length;
counter.innerText = `${checkEl.checked ? total : 0}/${total}`;
if (notUserInvoked) {
return;
}
if (datagrid.name !== checkEl.getAttribute("data-check-all")) return;
const targetCheck = checkEl.checked;//this is vital as it gets swithced around due to not all being checked just yet.
checkboxes.forEach(checkbox => {
if (checkbox !== checkEl && checkbox.checked !== targetCheck && !checkbox.hasAttribute("data-check-all")) {
checkbox.checked = targetCheck;
//this will end up calling this callback a lot. But it needs to eb done as otherwise the happy checkboxes fail horribly.
//Bubbles is needed as the happy callback catches on document
notUserInvoked = true;//prevent nesting
checkbox.dispatchEvent(new Event('change', {bubbles: true}));
notUserInvoked = false;
}
});
return;
} else {
const selectAll = datagrid.el.querySelector<HTMLInputElement>(`input[data-check='${datagrid.name}'][data-check-all]`);
if (selectAll) {
selectAll.checked = Array.from(checkboxes).filter(c => !c.hasAttribute("data-check-all")).every(c => c.checked);
}
}

const checkedBoxes = Array.from(checkboxes).filter(checkbox => checkbox.checked && !checkEl.hasAttribute("data-check-all"));
const hasChecked = checkedBoxes.length >= 1;

actionButtons.forEach(button => (button.disabled = !hasChecked));

if (select) {
select.disabled = !hasChecked;
}

if (counter) {
counter.innerText = `${checkedBoxes.length}/${checkboxes.length}`;
const selectAll = datagrid.el.querySelectorAll<HTMLInputElement>(`input[data-check='${datagrid.name}'][data-check-all]`);
if (selectAll.length > 0) {
const allChecked = Array.from(checkboxes).filter(c => !c.hasAttribute("data-check-all")).every(c => c.checked);
if (allChecked != selectAll.checked) {
selectAll.forEach(el => {
if(el.hasAttribute("data-override-check-all")){
return;
}
notUserInvoked = true;
el.checked = allChecked;
el.dispatchEvent(new Event('change', {bubbles: true}));
notUserInvoked = false;
})
}
}
});

checkEl.addEventListener("change", () => {
this.loadCheckboxCount(datagrid);
})
});

return true;
Expand Down
9 changes: 8 additions & 1 deletion assets/plugins/features/confirm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ export class ConfirmPlugin implements DatagridPlugin {
confirmEl.addEventListener("click", e => this.confirmEventHandler.bind(datagrid)(e.target as HTMLElement, e))
);

datagrid.ajax.addEventListener("success", e =>
datagrid.el
.querySelectorAll<HTMLElement>(`[${ConfirmAttribute}]:not(.ajax)`)
.forEach(confirmEl =>
confirmEl.addEventListener("click", e => this.confirmEventHandler.bind(datagrid)(e.target as HTMLElement, e))
)
);
datagrid.ajax.addEventListener("interact", e => this.confirmEventHandler.bind(datagrid)(e.detail.element, e));

return true;
}

confirmEventHandler(this: Datagrid, el: HTMLElement, e: Event) {
const message = el.closest('a').getAttribute(ConfirmAttribute)!;
const message = el.closest('a')?.getAttribute(ConfirmAttribute)!;
if (!message) return;

if (!window.confirm.bind(window)(message)) {
Expand Down
17 changes: 12 additions & 5 deletions assets/plugins/integrations/happy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Datagrid } from "../..";
import { DatagridPlugin } from "../../types";
import { window } from "../../utils";
import type { Happy } from "../../integrations";
import {Datagrid} from "../..";
import {DatagridPlugin} from "../../types";
import {window} from "../../utils";
import type {Happy} from "../../integrations";

export class HappyPlugin implements DatagridPlugin {
constructor(private happy?: Happy) {
}

onDatagridInit(datagrid: Datagrid): boolean {
doInit(datagrid: Datagrid): boolean {
const happy = this.happy ?? window().happy ?? null;

if (happy) {
Expand All @@ -16,4 +16,11 @@ export class HappyPlugin implements DatagridPlugin {

return true;
}

onDatagridInit(datagrid: Datagrid): boolean {
datagrid.ajax.addEventListener('complete', (event) => {
this.doInit(datagrid)
});
return this.doInit(datagrid);
}
}
Loading