UNPKG

@vdt-jquery/jquery-datatreeview

Version:

A jQuery plugin that makes it easy to create a tree view for your tree-like data. It allows you to create a tree view on the fly for a tree of data-objects while leaving you in full control of where the data comes from.

376 lines (316 loc) 15.7 kB
(function ($) { // Extension for creating datatreeview; supports multiple creations in one call $.fn.datatreeview = function (settings, callback) { // Allow callback to be the only argument if ($.isFunction(settings)) { callback = settings; settings = null; } return $(this).each(function () { // Get object from data let datatreeview = $(this).data('datatreeview'); if (!datatreeview) { // Validate data if (!settings || !settings.data) { throw 'datatreeview error: expected required option "data"'; } else if (!$.isArray(settings.data)) { throw 'datatreeview error: expected option "data" to be an array'; } // Create object let options = $.extend({}, $.fn.datatreeview.defaults, settings); datatreeview = new Datatreeview($(this), options); // Add object to data $(this).data('datatreeview', datatreeview); } // Call the callback, bound to the datatreeview if ($.isFunction(callback)) { callback.bind(datatreeview)(datatreeview); } }); } // Set defaults for extension $.fn.datatreeview.defaults = { // The property in a data node object to use as node value // Defaults to the data-property value-property with as fallback 'value' getValueProperty: function (element) { return $(element).data('value-property') || 'value'; }, // The property in a data node object to use as node text // Defaults to the data-property text-property with as fallback 'text' getTextProperty: function (element) { return $(element).data('text-property') || 'text'; }, // The property in a data node object to determine select status // Defaults to the data-property selected-property with as fallback 'selected' getSelectedProperty: function (element) { return $(element).data('selected-property') || 'selected'; }, // The property in a data node object to use to find a node's child nodes // Defaults to the data-property children-property with as fallback 'children' getChildrenProperty: function (element) { return $(element).data('children-property') || 'children'; }, // The field name to use for the generated checkboxes // Defaults to the data-property field-name getFieldName: function (element) { return $(element).data('field-name'); }, // Allow a freehand selection of nodes where selecting nodes does not affect child or parent nodes hasFreehandSelection: function (element) { return $(element).data('freehand-select') !== undefined && $(element).data('freehand-select') != false; }, // Initialize the treeview disabled or not // Defaults to the data-property disabled isDisabled: function (element) { return $(element).data('disabled') !== undefined && $(element).data('disabled') != false; }, // Get the options used for the node toggle event // Looks to the data-properties toggle-duration and toggle-easing with no animation as default getToggleOptions: function (element) { var duration = parseInt($(element).data('toggle-duration')); var easing = $(element).data('toggle-easing'); if (isNaN(duration) || duration < 0) { duration = 0; } return { duration: duration, easing: easing } }, // Initialize the treeview collapsed or not // Defaults to the data-property collapsed isCollapsed: function (element) { return $(element).data('collapsed') !== undefined && $(element).data('collapsed') != false; }, // Lists are containers for nodes // They always gets at least the class 'datatreeview-list' getListAttributes: function () { return {}; }, // Nodes are the visual representation of node data // They always gets at least the class 'datatreeview-node' getNodeAttributes: function () { return {}; }, // Node content is the content of the node (the toggler, checkbox and label) excluding child nodes // It always gets at least the class 'datatreeview-node-content' getNodeContentAttributes: function () { return {}; }, // Inputs are the checkboxes used to select nodes // They always gets at least the class 'datatreeview-field' getInputAttributes: function () { return {}; }, // Labels are the text displays for nodes // They always gets at least the class 'datatreeview-text' getLabelAttributes: function () { return {}; }, // Togglers are the elements used to collapse/expand node lists // They always gets at least the class 'datatreeview-toggler' getTogglerAttributes: function () { return {}; } } // Datatreeview implementation function Datatreeview(element, options) { let base = this; this.element = element; this.options = options; this.valueProperty = options.getValueProperty(this.element); this.textProperty = options.getTextProperty(this.element); this.selectedProperty = options.getSelectedProperty(this.element); this.childrenProperty = options.getChildrenProperty(this.element); this.fieldName = options.getFieldName(this.element); this.hasFreehandSelection = options.hasFreehandSelection(this.element); this.isDisabled = false; this.toggleOptions = options.getToggleOptions(this.element); this.element.children().hide(); this.element.addClass('datatreeview'); this.list = this.createElement('<ul>', 'datatreeview-list', this.options.getListAttributes()); this.element.append(this.list); // Start collapsed if needed let isCollapsed = options.isCollapsed(this.element); $.each(this.options.data, function (_, data) { base.createNode(base.list, data, isCollapsed); }); // Disable if needed if (this.options.isDisabled(this.element)) { this.disable(); } // Event handlers this.list.on('click', '.datatreeview-toggler', this, eventHandlers.togglerClick); this.list.on('change', '.datatreeview-field', this, eventHandlers.inputChange); } // Create an element and merge attribute objects to attributes Datatreeview.prototype.createElement = function (tagName, className) { let attributes = $.extend.apply({}, Array.prototype.slice.call(arguments, 2)); let element = $(tagName, attributes).addClass(className); return element; } // Create a list based on an array of node data Datatreeview.prototype.createList = function (node, dataArray, isCollapsed) { let base = this; let list = this.createElement('<ul>', 'datatreeview-list', this.options.getListAttributes()); let toggler = this.createElement('<div>', 'datatreeview-toggler', this.options.getTogglerAttributes()); if (isCollapsed) { node.addClass('datatreeview-node-collapsed'); list.hide(); } node.find('.datatreeview-node-content').prepend(toggler); node.append(list); $.each(dataArray, function (_, data) { base.createNode(list, data, isCollapsed); }); } // Create a node based on node data Datatreeview.prototype.createNode = function (list, data, isCollapsed) { let node = this.createElement('<li>', 'datatreeview-node', this.options.getNodeAttributes()); let content = this.createElement('<div>', 'datatreeview-node-content', this.options.getNodeContentAttributes()); let checkbox = this.createElement('<input>', 'datatreeview-field', { type: 'checkbox', name: this.fieldName, value: data[this.valueProperty] }, this.options.getInputAttributes()); let label = this.createElement('<label>', 'datatreeview-text', this.options.getLabelAttributes()) .text(data[this.textProperty]) .prepend(checkbox); let hasChildren = data[this.childrenProperty] && data[this.childrenProperty].length > 0; let useSelectedProperty = this.hasFreehandSelection || !hasChildren; content.append(label); node.append(content); node.data('node-data', data); list.append(node); if (hasChildren) { this.createList(node, data[this.childrenProperty], isCollapsed); } if (useSelectedProperty && data[this.selectedProperty]) { checkbox.prop('checked', true); } if (!useSelectedProperty && node.find('.datatreeview-list input.datatreeview-field:checked').length === node.find('.datatreeview-list input.datatreeview-field').length) { checkbox.prop('checked', true); } } // Remove the entire datatreeview; resets the base element to its former state Datatreeview.prototype.remove = function () { this.list.remove(); this.element.removeClass('datatreeview'); this.element.removeData('datatreeview'); this.element.children().show(); } // Get the selected nodes Datatreeview.prototype.getSelectedNodes = function () { return this.element.find('input.datatreeview-field:checked').closest('li.datatreeview-node'); } // Get the selected data nodes as an array Datatreeview.prototype.getSelectedData = function () { return this.getSelectedNodes().map(function () { return $(this).data('node-data'); }).get(); } // Get the selected values as an array Datatreeview.prototype.getSelectedValues = function () { let base = this; return this.getSelectedData().map(function (value) { return value[base.valueProperty]; }); } // Set selected nodes by selector/selection/function/element Datatreeview.prototype.setSelectedNodes = function (nodes) { nodes = $(nodes); if (!this.hasFreehandSelection) { nodes = nodes.not(':has(li)') } this.list.find('li.datatreeview-node').not(nodes).find('> .datatreeview-node-content > label.datatreeview-text > input.datatreeview-field').prop('checked', false); $(nodes).find('> .datatreeview-node-content > label.datatreeview-text > input.datatreeview-field').prop('checked', true); if (!this.hasFreehandSelection) { this.list.find('li.datatreeview-node:has(li.datatreeview-node):not(:has(li.datatreeview-node:not(:has(li.datatreeview-node)) input.datatreeview-field:not(:checked)))').find('> .datatreeview-node-content > label.datatreeview-text > input.datatreeview-field').prop('checked', true); } this.triggerSelectionChanged(); } // Trigger the selection changed event after automated or manual changes Datatreeview.prototype.triggerSelectionChanged = function () { this.element.trigger('datatreeview.selectionChanged', [this.getSelectedData(), this.getSelectedValues()]); } // Set selected node by filter function // Filter function argument is node data Datatreeview.prototype.setSelectedData = function (filter) { let nodes = this.list.find('li.datatreeview-node').filter(function () { return filter($(this).data('node-data')); }); this.setSelectedNodes(nodes); } // Set the selected nodes based on a value array Datatreeview.prototype.setSelectedValues = function (values) { let base = this; this.setSelectedData(function (nodeData) { return values.indexOf(nodeData[base.valueProperty]) > -1; }); } // Enable the treeview if it is disabled Datatreeview.prototype.enable = function () { if (!this.isDisabled) { return; } this.isDisabled = false; this.element.find('input.datatreeview-field').prop('disabled', this.isDisabled); this.element.removeClass('datatreeview-disabled'); } // Disable the treeview if it is enabled Datatreeview.prototype.disable = function () { if (this.isDisabled) { return; } this.isDisabled = true; this.element.find('input.datatreeview-field').prop('disabled', this.isDisabled); this.element.addClass('datatreeview-disabled'); } // Collapse one or more nodes by selector/selection/function/element Datatreeview.prototype.collapseNodes = function (nodes) { nodes = $(nodes).not('.datatreeview-node-collapsed'); if (nodes.length > 0) { nodes.addClass('datatreeview-node-collapsed'); nodes.children('.datatreeview-list').slideToggle(this.toggleOptions); this.element.trigger('datatreeview.nodesCollapsed', [nodes]); } } // Expand one or more nodes by selector/selection/function/element Datatreeview.prototype.expandNodes = function (nodes) { nodes = $(nodes).filter('.datatreeview-node-collapsed'); if (nodes.length > 0) { nodes.removeClass('datatreeview-node-collapsed'); nodes.children('.datatreeview-list').slideToggle(this.toggleOptions); this.element.trigger('datatreeview.nodesExpanded', [nodes]); } } // Event handlers should not be accessible from the object itself let eventHandlers = { togglerClick: function (e) { let node = $(e.target).closest('li'); if (node.hasClass('datatreeview-node-collapsed')) { e.data.expandNodes(node); } else { e.data.collapseNodes(node); } }, inputChange: function (e) { if (!e.data.hasFreehandSelection) { let checked = $(e.target).is(':checked'); let node = $(e.target).closest('li.datatreeview-node'); node.find('input.datatreeview-field').prop('checked', checked); if (checked) { node.parents('li.datatreeview-node').find('> .datatreeview-node-content > label.datatreeview-text > input.datatreeview-field').prop('checked', function () { return $(this).closest('li.datatreeview-node').find('input.datatreeview-field').not(this).not(':checked').length == 0; }); } else { node.parents('li.datatreeview-node').find('> .datatreeview-node-content> label.datatreeview-text > input.datatreeview-field').prop('checked', false); } } e.data.triggerSelectionChanged(); } }; }(jQuery));