diff --git a/assets/js/lib/TristateCheckbox.js b/assets/js/lib/TristateCheckbox.js new file mode 100644 index 00000000..c8ceed0e --- /dev/null +++ b/assets/js/lib/TristateCheckbox.js @@ -0,0 +1,207 @@ +const DEFAULT_OPTIONS = { + true: "true", + false: "false", + null: "indeterminate", +}; + +/** + * A simple tristate checkbox + */ +export default class TristateCheckbox { + + static instances = new Map(); + + /** + * + * @type {null|boolean} + * @private + */ + _state = false; + + /** + * The element representing the checkbox. + * @type {HTMLInputElement} + * @private + */ + _element = null; + + /** + * The hidden input element representing the value of the checkbox + * @type {HTMLInputElement} + * @private + */ + _hiddenInput = null; + + /** + * The values of the checkbox. + * @type {{null: string, true: string, false: string}} + * @private + */ + _options = DEFAULT_OPTIONS; + + /** + * Retrieve the instance of the TristateCheckbox for the given element if already existing, otherwise a new one is created. + * @param element + * @param options + * @return {any} + */ + static getInstance(element, options = {}) + { + if(!TristateCheckbox.instances.has(element)) { + TristateCheckbox.instances.set(element, new TristateCheckbox(element, options)); + } + + return TristateCheckbox.instances.get(element); + } + + /** + * @param {HTMLElement} element + */ + constructor(element, options = {}) + { + if(!element instanceof HTMLInputElement || !(element.tagName === 'INPUT' && element.type === 'checkbox')) { + throw new Error("The given element is not an input checkbox"); + } + + //Apply options + this._options = Object.assign(this._options, options); + + this._element = element; + + //Set the state of our element to the value of the passed input value + this._parseInitialState(); + + //Create a hidden input field to store the value of the checkbox, because this will be always be submitted in the form + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = this._element.name; + this._hiddenInput.value = this._element.value; + + //Insert the hidden input field after the checkbox and remove the checkbox from form submission (by removing the name property) + element.after(this._hiddenInput); + this._element.removeAttribute('name'); + + //Do a refresh to set the correct styling of the checkbox + this._refresh(); + + this._element.addEventListener('click', this.click.bind(this)); + } + + /** + * Parse the attributes of the checkbox and set the correct state. + * @private + */ + _parseInitialState() + { + if(this._element.hasAttribute('value')) { + this._state = this._stringToState(this._element.getAttribute('value')); + return; + } + + if(this._element.checked) { + this._state = true; + return; + } + + if(this._element.indeterminate) { + this._state = null; + return; + } + + this._state = false; + } + + _refresh() + { + this._element.indeterminate = this._state === null; + this._element.checked = this._state === true; + //Set the value field of the checkbox and the hidden input to correct value + this._element.value = this._stateToString(this._state); + this._hiddenInput.value = this._stateToString(this._state); + } + + + /** + * Returns the current state of the checkbox. True if checked, false if unchecked, null if indeterminate. + * @return {boolean|null} + */ + get state() { + return this._state; + } + + /** + * Sets the state of the checkbox. True if checked, false if unchecked, null if indeterminate. + * @param state + */ + set state(state) { + this._state = state; + this._refresh(); + } + + /** + * Returns the current state of the checkbox as string, according to the options. + * @return {string} + */ + get stateString() { + return this._stateToString(this._state); + } + + set stateString(string) { + this.state = this._stringToState(string); + this._refresh(); + } + + /** + * @param {boolean|null} state + * @return string + * @private + */ + _stateToString(state) + { + if (this.state === null) { + return this._options.null; + } else if (this.state === true) { + return this._options.true; + } else if (this.state === false) { + return this._options.false; + } + + throw new Error("Invalid state " + state); + } + + /** + * Converts a string to a state according to the options. + * @param string + * @param throwError + * @return {null|boolean} + * @private + */ + _stringToState(string, throwError = true) + { + if (string === this._options.true) { + return true; + } else if (string === this._options.false) { + return false; + } else if (string === this._options.null) { + return null; + } + + if(throwError) { + throw new Error("Invalid state string " + string); + } else { + return null; + } + } + + click() + { + switch (this._state) { + case true: this._state = false; break; + case false: this._state = null; break; + default: this._state = true; break; + } + + this._refresh(); + } + +} \ No newline at end of file diff --git a/assets/js/lib/jquery.tristate.js b/assets/js/lib/jquery.tristate.js deleted file mode 100644 index c1a85d29..00000000 --- a/assets/js/lib/jquery.tristate.js +++ /dev/null @@ -1,213 +0,0 @@ -/*jslint devel: true, bitwise: true, regexp: true, browser: true, confusion: true, unparam: true, eqeq: true, white: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */ -/*globals jQuery */ - -/*! - * Tristate v1.2.1 - * - * Copyright (c) 2013-2017 Martijn W. van der Lee - * Licensed under the MIT. - */ -/* Based on work by: - * Chris Coyier (http://css-tricks.com/indeterminate-checkboxes/) - * - * Tristate checkbox with support features - * pseudo selectors - * val() overwrite - */ - -;(function($, undefined) { - 'use strict'; - - var pluginName = 'tristate', - defaults = { - 'change': undefined, - 'checked': undefined, - 'indeterminate': undefined, - 'init': undefined, - 'reverse': false, - 'state': undefined, - 'unchecked': undefined, - 'value': undefined // one-way only! - }, - valFunction = $.fn.val; - - function Plugin(element, options) { - if($(element).is(':checkbox')) { - this.element = $(element); - this.settings = $.extend( {}, defaults, options ); - this._create(); - } - } - - $.extend(Plugin.prototype, { - _create: function() { - var that = this, - state; - - // Fix for #1 - if (window.navigator.userAgent.indexOf('Trident') >= 0) { - this.element.click(function(e) { - that._change.call(that, e); - that.element.closest('form').change(); - }); - } else { - this.element.change(function(e) { - that._change.call(that, e); - }); - } - - this.settings.checked = this.element.attr('checkedvalue') || this.settings.checked; - this.settings.unchecked = this.element.attr('uncheckedvalue') || this.settings.unchecked; - this.settings.indeterminate = this.element.attr('indeterminatevalue') || this.settings.indeterminate; - - // Initially, set state based on option state or attributes - if (typeof this.settings.state === 'undefined') { - this.settings.state = typeof this.element.attr('indeterminate') !== 'undefined'? null : this.element.is(':checked'); - } - - // If value specified, overwrite with value - if (typeof this.settings.value !== 'undefined') { - state = this._parseValue(this.settings.value); - if (typeof state !== 'undefined') { - this.settings.state = state; - } - } - - this._refresh(this.settings.init); - - return this; - }, - - _change: function(e) { - if (e.isTrigger || !e.hasOwnProperty('which')) { - e.preventDefault(); - } - - switch (this.settings.state) { - case true: this.settings.state = (this.settings.reverse ? false : null); break; - case false: this.settings.state = (this.settings.reverse ? null : true); break; - default: this.settings.state = (this.settings.reverse ? true : false); break; - } - - this._refresh(this.settings.change); - }, - - _refresh: function(callback) { - var value = this.value(); - - this.element.data("vanderlee." + pluginName, value); - - this.element[this.settings.state === null ? 'attr' : 'removeAttr']('indeterminate', 'indeterminate'); - this.element.prop('indeterminate', this.settings.state === null); - this.element.get(0).indeterminate = this.settings.state === null; - - this.element[this.settings.state === true ? 'attr' : 'removeAttr']('checked', true); - this.element.prop('checked', this.settings.state === true); - - if ($.isFunction(callback)) { - callback.call(this.element, this.settings.state, this.value()); - } - }, - - state: function(value) { - if (typeof value === 'undefined') { - return this.settings.state; - } else if (value === true || value === false || value === null) { - this.settings.state = value; - - this._refresh(this.settings.change); - } - return this; - }, - - _parseValue: function(value) { - if (value === this.settings.checked) { - return true; - } else if (value === this.settings.unchecked) { - return false; - } else if (value === this.settings.indeterminate) { - return null; - } - }, - - value: function(value) { - if (typeof value === 'undefined') { - var value; - switch (this.settings.state) { - case true: - value = this.settings.checked; - break; - - case false: - value = this.settings.unchecked; - break; - - case null: - value = this.settings.indeterminate; - break; - } - return typeof value === 'undefined'? this.element.attr('value') : value; - } else { - var state = this._parseValue(value); - if (typeof state !== 'undefined') { - this.settings.state = state; - this._refresh(this.settings.change); - } - } - } - }); - - $.fn[pluginName] = function (options, value) { - var result = this; - - this.each(function() { - if (!$.data(this, "plugin.vanderlee." + pluginName)) { - $.data(this, "plugin.vanderlee." + pluginName, new Plugin(this, options)); - } else if (typeof options === 'string') { - if (typeof value === 'undefined') { - result = $(this).data("plugin.vanderlee." + pluginName)[options](); - return false; - } else { - $(this).data("plugin.vanderlee." + pluginName)[options](value); - } - } - }); - - return result; - }; - - // Overwrite fn.val - $.fn.val = function(value) { - var data = this.data("vanderlee." + pluginName); - if (typeof data === 'undefined') { - if (typeof value === 'undefined') { - return valFunction.call(this); - } else { - return valFunction.call(this, value); - } - } else { - if (typeof value === 'undefined') { - return data; - } else { - this.data("vanderlee." + pluginName, value); - return this; - } - } - }; - - // :indeterminate pseudo selector - $.expr.filters.indeterminate = function(element) { - var $element = $(element); - return typeof $element.data("vanderlee." + pluginName) !== 'undefined' && $element.prop('indeterminate'); - }; - - // :determinate pseudo selector - $.expr.filters.determinate = function(element) { - return !($.expr.filters.indeterminate(element)); - }; - - // :tristate selector - $.expr.filters.tristate = function(element) { - return typeof $(element).data("vanderlee." + pluginName) !== 'undefined'; - }; -})(jQuery); diff --git a/assets/js/tristate_checkboxes.js b/assets/js/tristate_checkboxes.js index 5ec16b19..c59a3e22 100644 --- a/assets/js/tristate_checkboxes.js +++ b/assets/js/tristate_checkboxes.js @@ -1,49 +1,37 @@ 'use strict'; -import "./lib/jquery.tristate" +import TristateCheckbox from "./lib/TristateCheckbox"; class TristateHelper { constructor() { this.registerTriStateCheckboxes(); - this.registerSubmitHandler(); - } - - registerSubmitHandler() { - document.addEventListener("turbo:submit-start", (e) => { - var form = e.detail.formSubmission.formElement; - var formData = e.detail.formSubmission.formData; - - var $tristate_checkboxes = $('.tristate:checkbox', form); - - //Iterate over each tristate checkbox in the form and set formData to the correct value - $tristate_checkboxes.each(function() { - var $checkbox = $(this); - var state = $checkbox.tristate('state'); - - formData.set($checkbox.attr('name'), state); - }); - }); } registerTriStateCheckboxes() { //Initialize tristate checkboxes and if needed the multicheckbox functionality const listener = () => { - $(".tristate").tristate( { - checked: "true", - unchecked: "false", - indeterminate: "indeterminate", + + const tristates = document.querySelectorAll("input.tristate"); + + tristates.forEach(tristate => { + TristateCheckbox.getInstance(tristate); }); - $('.permission_multicheckbox:checkbox').change(function() { - //Find the other checkboxes in this row, and change their value - var $row = $(this).parents('tr'); - //@ts-ignore - var new_state = $(this).tristate('state'); + //Register multi checkboxes in permission tables + const multicheckboxes = document.querySelectorAll("input.permission_multicheckbox"); + multicheckboxes.forEach(multicheckbox => { + multicheckbox.addEventListener("change", (event) => { + const newValue = TristateCheckbox.getInstance(event.target).state; + const row = event.target.closest("tr"); - //@ts-ignore - $('.tristate:checkbox', $row).tristate('state', new_state); + //Find all tristate checkboxes in the same row and set their state to the new value + const tristateCheckboxes = row.querySelectorAll("input.tristate"); + tristateCheckboxes.forEach(tristateCheckbox => { + TristateCheckbox.getInstance(tristateCheckbox).state = newValue; + }); + }); }); }