Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="q-app">
  <div class="q-pa-lg">
    <div
    class="container row no-wrap items-center"
    :style="totalWidth"
    :class="{ focus: hasFocus, active: isActive }"
    @mouseenter="gotFocus"
    @mouseleave="lostFocus"
    @mousedown="onMousedown"
  >
    <q-select
      borderless
      :style="inputWidth"
      :popup-content-style="{width: '11.5rem'}"
      hide-dropdown-icon
      v-model="selectedCategory"
      :options="autoList"
      option-value="id"
      :option-label="item => item.name"
      use-input
      hide-selected
      fill-input
      :label="label"
      input-debounce="0"
      @filter="filterList"
      @input="setModel"
      emit-value
      map-options
      autofocus
    >
      <template v-slot:no-option>
        <q-item dense>
          <q-item-section>
          </q-item-section>
        </q-item>
      </template>
    </q-select>
    <q-btn
      tabindex="-1"
      flat
      dense
      color="grey"
      icon="fas fa-caret-down"
      size="sm"
      padding="12px 9px 12px 0"
    >
      <q-menu
        anchor="bottom right" self="top right"
        v-model="showMenu">
        <div :style="totalWidth" class="q-pl-sm q-pr-xs">
        <q-tree
          outlined dense
          :nodes="categoryTree"
          :selected.sync="selectedId"
          accordion
          node-key="id"
          :expanded.sync="expanded"

        />
        </div>
      </q-menu>
    </q-btn>

  </div>
  </div>
</div>

              
            
!

CSS

              
                .container
  position: relative
  height: 2.5rem // Matches 'dense' height of standard Quasar widgets
  border: 1px solid grey
  border-radius: 4px
  background-color: white
  padding-right: 3px
  padding-left: 10px
.focus
  border: 1px solid black
.active
  border: 2px solid blue
              
            
!

JS

              
                new Vue({
  el: '#q-app',
  name: 'ComboTree',
  //props: {
  //  label: {
  //    type: String,
  //    default: 'Select Category:'
  //  },
  //  categories: { // Object with id's as keys containing category objects
  //    type: Object,
  //    default: this.exampleCategories
  //  },
  //  width: {
  //   type: String,
  //   default: '13' // rem. Close to Quasar default value
  //  },
  //  include: { // If not null, include the -all categories- string or -none- string
  //    type: String,
  //   default: null // options are 'all' or 'none'
  //  },
  //  initialId: { // If not null, set this as the inital selected category id
  //    // To choose 'all' or 'none' set it to '0'
  //      type: String,
  //      default: null
  //    }
  //},
  mounted () {
    this.buildcategoryTree(this.categories)
    this.selectedId = this.initialId
    if (this.selectedId === '0') { // -all categories- or -none-
      this.selectedCategory = {
        id: '0',
        name: this.categoryTree[0].label
      }
    } else if (this.selectedId !== null) {
      this.selectedCategory = this.categories[this.selectedId]
    }
  },
  computed: {
    totalWidth () { // Width of entire widget
      return 'width: ' + this.width + 'rem;'
    },
    inputWidth () { // width of select component
      const width = this.width - 2.2
      return 'width: ' + width + 'rem'
    }
  },

  watch: {
    expanded: function () {
      // Remove expanded children if parent not expanded
      this.expanded.forEach((id) => {
        const parentId = this.categories[id].category
        if (parentId) {
          if (!this.expanded.includes(parentId)) {
            this.expanded = this.expanded.filter(e => e !== id)
          }
        }
      })
    }
  },

  methods: {
    buildcategoryTree (categories) {
      this.categoryTree = []
      this.autoListCandidates = []
      if (this.include) { // Include 'all categories' (default) or 'none'
        let label = this.allCategoriesString
        if (this.include === 'none') {
          label = this.noCategoriesString
        }
        const topNode = {
          id: '0',
          label: label,
          selectable: true,
          active: false,
          children: [],
          parent: null,
          index: null,
          handler: this.selectCategory
        }
        this.categoryTree.push(topNode)
      }
      Object.values(categories).forEach((category) => {
        if (!category.category) { // This is a root level category so start another
          // buildCategory will iterate to add all subcategories for this root
          this.buildCategory(null, category, categories)
        }
      })
      // Sort the main categories (sub-categories have been sorted by buildCategory)
      this.categoryTree = this.sortCategories(this.categoryTree)
    },
    buildCategory (parentNode, category, categories) {
      // Recursive function to build the tree from store data for each category
      // 'categories' is the source data object containing all category objects indexed by id
      // Create new category node and add it to it's parent's 'children' array
      // Add this new category node to the list of all categories
      const newCategory = this.addCategory(parentNode, category)
      if (!newCategory) { return }
      // If this category has subcategories, call ourself to add them
      if (category.subCategories.length > 0) {
        // category.subCategories is an array of id's
        category.subCategories.forEach((id) => {
          // For each id listed, get the corresponding subCategory object
          // and add it to this category node's list of leaf nodes
          const subCategory = categories[id]
          this.buildCategory(newCategory, subCategory, categories)
        })
      }
      // Sort the subCategories at this level
      if (newCategory.children.length > 0) {
        newCategory.children = this.sortCategories(newCategory.children)
      }
    },
    addCategory (parentNode, category) {
      try {
        const newCategory = {
          id: category.id,
          label: category.name,
          selectable: true,
          active: false,
          children: [],
          parent: parentNode,
          index: null,
          handler: this.selectCategory // Will be called when node is selected
        }
        // Add it to its parent's children array
        let parentArray
        if (!parentNode) { // top level
          parentArray = this.categoryTree
        } else {
          parentArray = parentNode.children
        }
        parentArray.push(newCategory) // Add this node to its parent
        this.autoListCandidates.push(newCategory.id) // And add it to the autofill list
        // (or to the main nodes array for top level)
        return newCategory // Return this new category node to buildcategory()
      } catch (err) {
        console.log('Bad category: ' + err + ' Category: ' + category)
        return null
      }
    },
    sortCategories (childArray) {
      // Called from buildcategory()
      // Sort the category's 'children' array of subcategories
      const tempCategories = []
      childArray.forEach((child) => {
        tempCategories.push(child)
      })
      tempCategories.sort((a, b) => a.label.localeCompare(b.label))
      // Return the sorted children array
      return tempCategories
    },

    filterList (text, populateAutoFill, abort) {
      // As user types, show list of matching items from the categories data
      if (text.length < 2) { // Don't show the drop-down if less than 2 chars
        abort()
        return
      }
      populateAutoFill(() => {
        this.autoList = []
        if (text.length > 0) {
          if (this.allCategoriesString.substr(0, text.length).toUpperCase() === text.toUpperCase()) {
            // Started to enter '-all categories-'
            this.autoList.push({ id: '0', name: this.allCategoriesString })
          } else {
            // Look for a match in the candidate list
            this.autoListCandidates.forEach((id) => {
              const name = this.categories[id].name
              if (name.substr(0, text.length).toUpperCase() === text.toUpperCase()) {
                this.autoList.push({ id: id, name: name })
              }
            })
            // Sort the list
            this.autoList.sort((a, b) => a.name.localeCompare(b.name))
          }
        }
      })
    },
    gotFocus () {
      this.hasFocus = true
    },
    lostFocus () {
      this.hasFocus = false
      this.isActive = false
    },
    onMousedown () {
      this.isActive = true
    },
    setModel (id) {
      if (id !== null) {
        this.$emit('selectCategory', id)
      }
    },
    selectCategory (node) {
      // Each tree node has this handler in it's object
      console.log('Select ' + node.label)
      this.selectedId = node.id
      this.selectedCategory = { id: node.id, name: node.label }
      this.expanded = []
      this.showMenu = false
      this.$emit('selectCategory', node.id)
    }
  },
  data () {
    return {
      // Normally these would be component props:
      label: 'Select Category',
      width: '13',
      include: null,
      initialId: null,
      categoryTree: [], // Hierearchical array of tree nodes sorted
      autoListCandidates: [], // Linear array of ids for autofill
      autoList: [], // Array of names matching input so far
      showMenu: false, // Used to show/hide menu
      showAutofill: false,
      hasFocus: false,
      isActive: false,
      selectedCategory: null,
      selectedId: null, // Used to sync tree selection
      expanded: null, // Array of expanded node ids
      allCategoriesString: '-all categories-',
      noCategoriesString: '-none-',
      categories: [
        {
          id: '1',
          name: 'Main 1',
          type: 'test',
          category: null,
          subCategories: ['11']
        },
        {
          id: '11',
          name: 'Sub 1-1',
          type: 'test',
          category: '1',
          subCategories: ['111', '112']
        },
        {
          id: '111',
          name: 'Sub 1-1-1',
          type: 'test',
          category: '11',
          subCategories: []
        },
        {
          id: '112',
          name: 'Sub 1-1-2',
          type: 'test',
          category: '11',
          subCategories: []
        },
        {
          id: '12',
          name: 'Sub 1-2',
          type: 'test',
          category: '1',
          subCategories: []
        },
        {
          id: '2',
          name: 'Main 2',
          type: 'test',
          category: null,
          subCategories: ['21', '22']
        },
        {
          id: '21',
          name: 'Sub 2-1',
          type: 'test',
          category: '2',
          subCategories: []
        },
        {
          id: '22',
          name: 'Sub 2-2',
          type: 'test',
          category: '2',
          subCategories: []
        },
        {
          id: '3',
          name: 'Main 3',
          type: 'test',
          category: null,
          subCategories: []
        },
        {
          id: '4',
          name: 'Main 4',
          type: 'test',
          category: null,
          subCategories: []
        },
      ]
    }
  }
})
  
              
            
!
999px

Console