text: import $ from 'jquery';
  
  $.bonsai = {};
  $.bonsai.defaults = {
    expandAll: false, // expand all items
    expand: null, // optional function to expand an item
    collapse: null, // optional function to collapse an item
    addExpandAll: false, // add a link to expand all items
    addSelectAll: false, // add a link to select all checkboxes
    selectAllExclude: null, // a filter selector or function for selectAll
    idAttribute: 'id', // which attribute of the list items to use as an id
  
    // createInputs: create checkboxes or radio buttons for each list item
    // by setting createInputs to "checkbox" or "radio".
    //
    // The name and value for the inputs can be declared in the
    // markup using `data-name` and `data-value`.
    //
    // The name is inherited from parent items if not specified.
    //
    // Checked state can be indicated using `data-checked`.
    createInputs: false,
    // checkboxes: run qubit(this.options) on the root node (requires jquery.qubit)
    checkboxes: false,
    // handleDuplicateCheckboxes: update any other checkboxes that
    // have the same value
    handleDuplicateCheckboxes: false,
    // createRadioButtons: creates radio buttons for each list item.
    //
    // The name and value for the checkboxes can be declared in the
    // markup using `data-name` and `data-value`.
    //
    // The name is inherited from parent items if not specified.
    //
    // Checked state can be indicated using `data-checked`.
    createRadioButtons: false,
  };
  let Bonsai = function (el, options) {
    let self = this;
    options = options || {};
    this.options = $.extend({}, $.bonsai.defaults, options);
    this.el = $(el).addClass('bonsai').data('bonsai', this);
  
    // store the scope in the options for child nodes
    if (!this.options.scope) {
      this.options.scope = this.el;
    }
    this.update();
    if (this.isRootNode()) {
      if (this.options.createCheckboxes) this.createInputs = 'checkbox';
      if (this.options.handleDuplicateCheckboxes) this.handleDuplicateCheckboxes();
      if (this.options.checkboxes) this.el.qubit(this.options);
      if (this.options.addExpandAll) this.addExpandAllLink();
      if (this.options.addSelectAll) this.addSelectAllLink();
      this.el.on('click', '.thumb', (ev) => {
        self.toggle($(ev.currentTarget).closest('li'));
      });
    }
    if (this.options.expandAll) this.expandAll();
  };
  Bonsai.prototype = {
    isRootNode() {
      return this.options.scope === this.el;
    },
  
    listItem(id) {
      if (typeof id === 'object') return $(id);
      return this.el.find(`[${this.options.idAttribute}="${id}"]`);
    },
  
    toggle(listItem) {
      if (!$(listItem).hasClass('expanded')) {
        return this.expand(listItem);
      }
  
      return this.collapse(listItem);
    },
  
    expand(listItem) {
      return this.setExpanded(listItem, true);
    },
  
    collapse(listItem) {
      return this.setExpanded(listItem, false);
    },
  
    setExpanded(listItem, expanded) {
      let $li = this.listItem(listItem);
      if ($li.length > 1) {
        let self = this;
        $li.each(function () {
          self.setExpanded(this, expanded);
        });
        return null;
      }
      if (expanded) {
        if (!$li.data('subList')) return null;
        $li = $($li).addClass('expanded').removeClass('collapsed');
        $($li.data('subList')).css('height', 'auto');
      } else {
        $li = $($li).addClass('collapsed')
          .removeClass('expanded');
        $($li.data('subList')).height(0);
      }
      return $li;
    },
  
    expandAll() {
      this.expand(this.el.find('li'));
    },
  
    collapseAll() {
      this.collapse(this.el.find('li'));
    },
  
    expandTo(listItem) {
      let self = this;
      let $li = this.listItem(listItem);
      $li.parents('li').each(function () {
        self.expand($(this));
      });
      return $li;
    },
  
    update() {
      let self = this;
      // look for a nested list (if any)
      this.el.children().each(function () {
        let item = $(this);
        if (self.options.createInputs) self.insertInput(item);
  
        // insert a thumb if it doesn't already exist
        if (item.children().filter('.thumb').length === 0) {
          let thumb = $('<div class="thumb"></div>');
          item.prepend(thumb);
        }
        let subLists = item.children().filter('ol, ul');
        item.toggleClass('has-children', subLists.find('li').length > 0);
        // if there is a child list
        subLists.each(function () {
          // that's not empty
          if ($('li', this).length === 0) return;
  
          // then this el has children
          item.data('subList', this);
          // collapse the nested list
          if (item.hasClass('expanded')) {
            self.expand(item);
          } else {
            self.collapse(item);
          }
          // handle any deeper nested lists
          let exists = !!$(this).data('bonsai');
          $(this).bonsai(exists ? 'update' : self.options);
        });
      });
  
      this.expand = this.options.expand || this.expand;
      this.collapse = this.options.collapse || this.collapse;
    },
  
    serialize() {
      let idAttr = this.options.idAttribute;
      return this.el.find('li').toArray().reduce((acc, li) => {
        let $li = $(li);
        let id = $li.attr(idAttr);
        // only items with IDs can be serialized
        if (id) {
          let state = $li.hasClass('expanded')
              ? 'expanded'
              : ($li.hasClass('collapsed') ? 'collapsed' : null);
          if (state) acc[$li.hasClass('expanded') ? 'expanded' : 'collapsed'].push(id);
        }
        return acc;
      }, { expanded: [], collapsed: [], version: 2 });
    },
  
    restore(state) {
      let self = this;
      if (state.version > 1) {
        state.expanded.map(this.expand.bind(this));
        state.collapsed.map(this.collapse.bind(this));
      } else {
        Object.keys(state).forEach((id) => {
          self.setExpanded(id, state[id] === 'expanded');
        });
      }
    },
  
    insertInput(listItem) {
      let type = this.options.createInputs;
      if (listItem.find(`> input[type=${type}]`).length) return;
      let id = this.inputIdFor(listItem);
      let checkbox = $(`<input type="${type}" name="${
         this.inputNameFor(listItem)}" id="${id}" /> `
      );
      let children = listItem.children();
      // get the first text node for the label
      let text = listItem.contents().filter(function () {
        return this.nodeType === 3;
      }).first();
      checkbox.val(listItem.data('value'));
      checkbox.prop('checked', listItem.data('checked'));
      children.detach();
      listItem.append(checkbox)
        .append(
          $(`<label for="${id}">`).append(text.length > 0 ? text : children.first())
        )
        .append(text.length > 0 ? children : children.slice(1));
    },
  
    checkboxPrefix: 'bonsai-checkbox-',
  
    inputIdFor(listItem) {
      let id = $(listItem).data('id');
      while (!id || ($(`#${id}`).length > 0)) {
        id = this.checkboxPrefix + Bonsai.uniqueId++;
      }
      return id;
    },
  
    inputNameFor(listItem) {
      return listItem.data('name')
        || listItem.parents().filter('[data-name]').data('name');
    },
  
    handleDuplicateCheckboxes() {
      let self = this;
      self.el.on('change', 'input[type=checkbox]', (ev) => {
        let checkbox = $(ev.target);
        if (!checkbox.val()) return;
        // select all duplicate checkboxes that need to be updated
        let selector = `${'input[type=checkbox]'
          + '[value="'}${checkbox.val()}"]${
           checkbox.attr('name') ? `[name="${checkbox.attr('name')}"]` : ''
           }${checkbox.prop('checked') ? ':not(:checked)' : ':checked'}`;
        self.el.find(selector).prop({
          checked: checkbox.prop('checked'),
          indeterminate: checkbox.prop('indeterminate'),
        }).trigger('change');
      });
    },
  
    addExpandAllLink() {
      let self = this;
      $('<div class="expand-all">')
        .append(
          $('<a class="all">Expand all</a>').on('click', () => {
            self.expandAll();
          })
        )
        .append('<i class="separator"></i>')
        .append(
          $('<a class="none">Collapse all</a>').on('click', () => {
            self.collapseAll();
          })
        )
        .insertBefore(this.el);
    },
  
    addSelectAllLink() {
      let scope = this.options.scope;
      let self = this;
      function getCheckboxes() {
        // return all checkboxes that are not in hidden list items
        return scope.find('li')
          .filter(self.options.selectAllExclude || function () {
            return $(this).css('display') !== 'none';
          })
          .find('> input[type=checkbox]');
      }
      $('<div class="check-all">')
        .append($('<a class="all">Select all</a>')
          .css('cursor', 'pointer')
          .on('click', () => {
            getCheckboxes().prop({
              checked: true,
              indeterminate: false,
            });
          }))
        .append('<i class="separator"></i>')
        .append($('<a class="none">Select none</a>')
          .css('cursor', 'pointer')
          .on('click', () => {
            getCheckboxes().prop({
              checked: false,
              indeterminate: false,
            });
          })
      )
        .insertAfter(this.el);
    },
  
    setCheckedValues(values) {
      let all = this.options.scope.find('input[type=checkbox]');
      $.each(values, (key, value) => {
        all.filter(`[value="${value}"]`)
          .prop('checked', true)
          .trigger('change');
      });
    },
  };
  
  $.extend(Bonsai, {
    uniqueId: 0,
  });
  
  $.fn.bonsai = function (options, ...args) {
    return this.each(function () {
      let bonsai = $(this).data('bonsai');
      if (!bonsai) {
        bonsai = new Bonsai(this, options);
        $(this).data('bonsai', bonsai);
      }
      if (typeof options === 'string') {
        let method = options;
        return bonsai[method](...args);
      }
    });
  };
  