Skip to content

Instantly share code, notes, and snippets.

@NoahRR
Last active December 21, 2020 20:41
Show Gist options
  • Save NoahRR/9c12e841502d5dff0fb79a4bc3255d78 to your computer and use it in GitHub Desktop.
Save NoahRR/9c12e841502d5dff0fb79a4bc3255d78 to your computer and use it in GitHub Desktop.

NexusOrganizer Code Snippet

This gist displays the only publicly available code for nexusorganizer.com



Technology

  • Django
  • PostreSQL
  • JavaScript/jQuery
  • HTML5 & CSS3/SASS
  • Grunt

Code Snippet

Unfortunately, I can't share my private project's code base with the world! However, I can show a snippet of functionality and code. I have arbitrarily chosen to demonstrate how I created the search bar within NexusOrganizer.

First, I write the front-end structure for the search bar. The search bar is a form.

HTML:

 <div class="top_header_container_mid">
      <form method="post" action="" id="search_bar_form">
      
          <input type="hidden" name="search_bool" value="1">
          <input name="SEARCH_INPUT" type="text" id="base_search_bar"
              placeholder="Search Titles, Tags, and Folders" autocomplete="off">
          <input type="button" value="clear" id="clear_button_on_search">
          <input type="button" value="add" id="add_button_on_search">
          
      </form>
  </div>

Next, I style the search bar to look how I want. (I'm excluding media queries here for simplicity)

CSS (SASS):

.top_header_container_mid{
   font-family: 'Libre Franklin', sans-serif;
   position: absolute;
   height: 100%;
   width: 37%;
   left: 50%;
   transform: translate(-50%, 0);
   z-index: 3000;
}
#base_search_bar{
   width: 37vw;
   height: 30px;
   position: absolute;
   left: 50%;
   top: 50%;
   transform: translate(-50%, -50%);
   z-index: 3001;
   text-indent: 10px;
   background-color: $defaultGrey1;
   border: none !important;
   border-radius: 4px;
   padding: 6px;
   font-family: 'Libre Franklin', sans-serif;
   font-size: .9em;
}
#clear_button_on_search{
   z-index: 99999 !important;
   position: absolute;
   top: 50%;
   right: -40px;
   text-align: right;
   transform: translate(0, -50%);
   visibility: hidden;
   cursor: pointer;
   background: none;
   border: none;
   font-family: 'Libre Franklin', sans-serif;
   color: #747474;
}
#add_button_on_search{
   z-index: 99999 !important;
   position: absolute;
   top: 50%;
   right: 0px;
   text-align: right;
   transform: translate(0, -50%);
   visibility: hidden;
   cursor: pointer;
   background: none;
   border: none;
   font-family: 'Libre Franklin', sans-serif;
   color: #747474;
}

Then, I build the functionality with JavaScript. The search bar has many parts to it: mouse interaction, autocomplete results, "classic" search, "add" search, and different query options. This can be confusing to describe without visuals, but below are all the functions built with JavaScript for this search bar.

  • The search bar responds to mouseover and mouseout events, but cease to do so when the user is actively interacting with the search bar.
  • On click, the search bar will load autocomplete results, showing all possible searches in a scrollable list. This list length is determined by the user (in settings). The list contains 3 kinds of search results: file title, file tag, and folder name- which are differentiated by icons.
  • The user can type in the search bar, and autocomplete results will change in real time according to the input.
  • If there is text in the search bar, two buttons will appear, "add" and "clear" (they will also disapear when input is removed).
  • After the user has typed something into the search bar, they can press "enter" on the keyboard to search for all matching queries (file titles, file tags, and folder names). This action will clear previous search results (which are shown in a seperate part of the screen) and display the new results.
  • After the user has typed something into the search bar, they can select the "add" button on the search bar. This action will keep the previous search results and simply add the new results.
  • After the user has typed something into the search bar, they can select an autocomplete result. This action will clear the previous search results and display only the specific kind of query selected via the autocomplete result.
  • After the user has typed something into the search bar, they can right click (or press the "..." button) and select one of the two options, "autofill" and "add". The "add" option will keep the previous search results, and simply add the new specific kind of result selected. The "autofill" option, will input the autocomplete text into the search bar (so that you can search for all maching results for that input).
  • There is also a loading symbol that will display while autocomplete results are loading, however, this is only a failsafe, the user ony significantly sees this if they have slower internet connection.
  • The autocomplete results also respond to mouseover and mouseout events.
  • There is also a keyboard combination to start typing in the search bar.

I won't show every feature's code, however, the major functionalities implemented in JavaScript are shown below.

First, the event listeners. Note: the JavaScript below is concatinated here for ease of viewing. In the codebase, my JavaScript is organized differently in seperate files, then combined using Grunt.

JavaScript (& jQuery):

(Some larger sections of my code contain both vanilla JavaScript and jQuery. In the future, I will only write in vanilla JS, but in building this app, I wanted to learn jQuery.)

$("#base_search_bar").on('mouseover', function () {

   if (globalIsSearchBarHoverOn == true) {
       $(this).css('box-shadow', '0 2px 4px rgba(0, 0, 0, 0.175)')
   }
})
$("#base_search_bar").on('mouseout', function () {

   if (globalIsSearchBarHoverOn == true) {
       $(this).css('box-shadow', 'none')
   }
})

// SHOW AUTOCOMPLETE RESULTS
$('#base_search_bar').on('click', function() {

   // prevent search box hover
   globalIsSearchBarHoverOn = false

   // remove hover effect from input
   $(this).css('box-shadow', 'none')

   // display grey out
   document.querySelector('.seach_bar_autocomplete_grey_out').style.visibility = 'visible'

   // display autocomplete
   display_autocomplete_search_results('search')
})

// SHOW AUTOCOMPLETE RESULTS
$('#base_search_bar').on('keyup', function(e) {

   if (e.key != 'Enter' && e.keyCode != 13) {

       // prevent search box hover
       globalIsSearchBarHoverOn = false

       // display grey out
       document.querySelector('.seach_bar_autocomplete_grey_out').style.visibility = 'visible'

       // display autocomplete
       display_autocomplete_search_results('search')
   }
})

// REMOVE AUTOCOMPLETE
$('body').on('click', function(e) {

   // doesn't follow through if click on input
   if (e.target.id == "base_search_bar") {return}
   if (e.target.id == "just_autcomplete_no_search") {return}

   // hide grey out
   document.querySelector('.seach_bar_autocomplete_grey_out').style.visibility = 'hidden'

   // hide autocomplete
   document.querySelector('.base_search_drop_down').style.visibility = 'hidden'

   // allow loading wheel again
   isSearchBarAutocompleteLoaded = false

   // reinitiate search box hover
   globalIsSearchBarHoverOn = true
})

// CLICK ON OPTION IN AUTOCOMPLETE - search
$('.base_search_drop_down').on('click', 'div', function(e) {

   // doesn't follow through if click on add
   if (e.target.id == "just_autcomplete_no_search") {return}

   on_click = {
       tipe: $(this).find('span')[0]['innerText'],
       name: $(this).find('.name_in_autocomplete')[0]['innerHTML'],
   }

   // display selected
   whole_search_bar_enter(0, on_click)
})

// SHOWING 'tripple dots' OPTION FOR SELECTING AUTOFILL AND ADD
$('.base_search_drop_down').on('mouseover', 'div', function() {
   $(this).children('svg').css('display', 'none')
   $(this).find('.ADD_IN_AUTOCOMPLETE_svg').css('display', 'unset')
})
$('.base_search_drop_down').on('mouseout', 'div', function() {
   $(this).children('svg').css('display', 'unset')
   $(this).find('.ADD_IN_AUTOCOMPLETE_svg').css('display', 'none')
})

// CLICK ON TRIPPLE DOTS (SAME AS CUSTOM CONTEXT MENU)
$('.base_search_drop_down').on('click', '.ADD_IN_AUTOCOMPLETE_svg', function() {
   for_new_tag = {
       name_in_autocomplete: $(this).parent().find('.name_in_autocomplete')[0]['innerHTML'],
       tipe: $(this).parent().find('span')[0]['innerText'],
   }
   show_custom_context_menu('search_AUTO', undefined, undefined, for_new_tag)
})

// CUSTOM CONTEXTMENU
$('.base_search_drop_down').on('contextmenu', 'div', function(e) {
   e.preventDefault()
   for_new_tag = {
       name_in_autocomplete: $(this).find('.name_in_autocomplete')[0]['innerHTML'],
       tipe: $(this).find('span')[0]['innerText'],
   }
   show_custom_context_menu('search_AUTO', undefined, undefined, for_new_tag)
})

// ADD BTN
document.querySelector("#add_button_on_search").addEventListener("click", function () {
   document.querySelector("#add_button_on_search").style.visibility = 'hidden';
   document.querySelector("#clear_button_on_search").style.visibility = 'hidden';
   whole_search_bar_enter(1, undefined, document.querySelector('#base_search_bar').value)
})

// SEARCH BAR ON SUBMIT
$('#search_bar_form').submit(function (e) {

   e.preventDefault()

   // allow loading wheel again
   isSearchBarAutocompleteLoaded = false

   // all functionality of after you submit the search
   whole_search_bar_enter(0)
})

Next, some fuctions that the event listeners call.

JavaScript:

// SUBMIT SEARCH
function whole_search_bar_enter(a_dd, on_click, name_for_checc) {

    // changing color back to default
    document.querySelector('.search_results_side_bar_btn').style.color = '#616161'

    // enabling hover on drop down icon
    document.querySelector(".search_results_side_bar_drop1").id = "caret_11"
    document.querySelector(".search_results_side_bar_drop2").id = 'caret_22'
    globalIsSearchRepoEnabled = true

    // allow loading wheel again
    isSearchBarAutocompleteLoaded = false

    // getting the tags that match the search
    var search_inpt = document.querySelector("#base_search_bar").value

    // if 'add' option is selected (just add to search results, don't replace)
    if (a_dd != '1') {
        // by default clear previous search matches if any
        document.getElementById('search_results_in_drop_down').innerHTML = ''
    }
    
    // displays results
    if (on_click) {
        drop_down_all_display(on_click['name'], undefined, 'search', undefined, on_click['tipe'])
    } else {
        drop_down_all_display(search_inpt, undefined, 'search')
    }

    // clear search box
    document.querySelector("#base_search_bar").value = ""
}


// DISPLAY AUTOCOMPLETE
function display_autocomplete_search_results(tipe) {
   // tipe is 'search' or 'add' ('search' is search bar, 'add' is new rel tag inpt)

   // display autocomplete
   if (tipe == 'add') {

       // define autocomplete
       var auto_enter = '.repo_customize_new_name_input_rel_tag_AUTOCOMPLETE'

       // get value from input
       var search_inpt = document.querySelector(".repo_customize_new_name_input_rel_tag").value

   } else if (tipe == 'search') {

       // define autocomplete
       var auto_enter = '.base_search_drop_down'

       // get value from search bar input
       var search_inpt = document.querySelector("#base_search_bar").value
   }

   // Call that gets the associated search autocomplete results
   $.ajax({
       url: globalDjangoLoadedUrls['homep'],
       type: 'get',
       async: true,
       data: {
           term: search_inpt,
           tipe: tipe,
           limit_num: globalUserSettings['search_length'],
       },
       success: function (response) {
           if (response == '') {
               // hide autocomplete if blank
               document.querySelector(auto_enter).style.visibility = 'hidden'
           } else {
               // show autocomplete if results
               document.querySelector(auto_enter).style.visibility = 'visible'
           }
           
           document.querySelector(auto_enter).innerHTML = ''
           document.querySelector(auto_enter).style.height = 'auto'
           
           // autocomplete_animation(auto_enter)
           for (let i = 0; i < response.length; i++) {

               // show folder
               if (response[i]['deep_index']) {
                   var inp_search_results_autocomplete = response[i]['name']
                   var SVG_INPt = `<path d="M9.828 4a3 3..."/>
                       <path fill-rule="evenodd" d="M13.81 4H2.19a1 1..."/>`
                   var clr_search_results_auto = '#4DA2CC'
                   var autocomplete_tipe = 'folder'
                   
               // show tag
               } else if (response[i]['parent_tag']) {
                   var inp_search_results_autocomplete = response[i]['tag']
                   var SVG_INPt = `<path fill-rule="evenodd" d="M2 2v4.586l7..."/>
                       <path fill-rule="evenodd" d="M4.5 5a.5.5 0 1..."/>`
                   var clr_search_results_auto = '#D25271'
                   var autocomplete_tipe = 'tag'
                   
               // show file
               } else {
                   var inp_search_results_autocomplete = response[i]['tag']
                   var SVG_INPt = ` <path fill-rule="evenodd" d="M4 0h8a2 2 0 0 1 2 2v12a2 2 0..."/>
                       <path fill-rule="evenodd" d="M4.5 10.5A.5.5 0..."/>`
                   var clr_search_results_auto = '#47B8AD'
                   var autocomplete_tipe = 'file'
               }

               document.querySelector(auto_enter).innerHTML += `
               <div>
                   <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-folder" fill="` + clr_search_results_auto + `" xmlns="http://www.w3.org/2000/svg">
                       ` + SVG_INPt + `
                   </svg>
                   <p class="name_in_autocomplete">` + inp_search_results_autocomplete + `</p>
                   <span style="display: none;">` + autocomplete_tipe + `</span>
                   <svg width="1em" height="1em" viewBox="0 0 16 16" id="just_autcomplete_no_search" class="bi bi-folder ADD_IN_AUTOCOMPLETE_svg" fill="` + clr_search_results_auto + `" xmlns="http://www.w3.org/2000/svg">
                       <path fill-rule="evenodd" d="M3 9.5a1.5..."/>
                   </svg>
               </div>`
           }
       },
       error: function () {
           alert('ERROR: autocomplete failed')
       }
   })

   // display loading spinner if not loaded yet
   if (isSearchBarAutocompleteLoaded == false) {
       isSearchBarAutocompleteLoaded = true
       document.querySelector(auto_enter).innerHTML = ''
       document.querySelector(auto_enter).style.visibility = 'visible'
       document.querySelector(auto_enter).style.height = '30px'
       document.querySelector(auto_enter).innerHTML = `<div></div><span class="loader"></span>`
   }
}

This next Javascript function, "drop_down_all_display", is the function that handles all the various types of search query and displaying the results. It is quite long, so I will only display the relevant ajax call that gets the search results.

JavaScript:

function drop_down_all_display(clicked_id, spc_case_id, type, type_2, type_3) {
   // type: 'folder', 'repo', 'search'
   // type_2 = 's' which means opening folder inside search
   // type_3 = 'file', 'tag', 'folder' for displaying only those unique types in search
   // in 'search' type, 'clicked_id' contains the value inputed into search bar

   // getting data to display tags and folders within drop down
   type_3_safe = type_3 || ''
   $.ajax({
       url: globalDjangoLoadedUrls['get_tags'],
       type: "get",
       data: {
           repo_id_clicked: clicked_id,
           type: type,
           type_3: type_3_safe,
       },
       success: function (response) {
           ... handles displaying results ...
       },
       error: function () {
           alert('ERROR: failed to load contents')
       },
   })
}

Next, we build the backend for the ajax calls. The vast majority of NexusOrganizer, including the search bar, uses ajax to communicate with the backend.

This is a Python-Django application. So the code actually goes from the JavaScript, to a file called "urls.py", then to "views.py". This functionality is mostly contained in views.py, so I will only display some relevant functions in views.py.

Ajax autocomplete results backend.

Python:

 # AUTOCOMPLETE
 if 'term' in request.GET:
 
     # specifies what kind of query we need to make
     tipe= request.GET.get('tipe')
     
     # specifies how many results we need
     limit_num = int(request.GET.get('limit_num'))
     
     qs = list()
     limit_num_MORE = limit_num + 5

     # NOT SEARCH RESULTS, tipe of 'add' is for another app component
     if tipe == 'add':
         place_holder = list(relatedtags.objects.filter(tag__icontains=request.GET.get('term'), localrepo__usern=request.user).values('tag', 'parent_tag').order_by('tag') )[:limit_num_MORE]
         qs_fake= list()
         counter = 0
         for item in place_holder:
             if item['tag'] not in qs_fake:
                 counter += 1
                 if counter <= limit_num:
                     qs_fake.append(item['tag'])
                     qs.append(item)

     # query file titles, file tags, and folder names
     elif tipe == 'search':
         place_holder= list(tags.objects.filter(tag__icontains=request.GET.get('term'), localrepo__usern=request.user).values('tag').order_by('tag'))[:limit_num_MORE]
         qs_fake= list()
         counter = 0
         for item in place_holder:
             if item['tag'] not in qs_fake:
                 counter += 1
                 if counter <= limit_num:
                     qs_fake.append(item['tag'])
                     qs.append(item)

         place_holder= list(folders.objects.filter(name__icontains=request.GET.get('term'), localrepo__usern=request.user).values('name', 'deep_index').order_by('name'))[:limit_num_MORE]
         qs_fake= list()
         counter = 0
         for item in place_holder:
             if item['name'] not in qs_fake:
                 counter += 1
                 if counter <= limit_num:
                     qs_fake.append(item['name'])
                     qs.append(item)

         place_holder = list(relatedtags.objects.filter(tag__icontains=request.GET.get('term'), localrepo__usern=request.user).values('tag', 'parent_tag').order_by('tag') )[:limit_num_MORE]
         qs_fake= list()
         counter = 0
         for item in place_holder:
             if item['tag'] not in qs_fake:
                 counter += 1
                 if counter <= limit_num:
                     qs_fake.append(item['tag'])
                     qs.append(item)

     return JsonResponse(qs, safe =False)

Search Results ajax call backend.

Python:

def get_tags(request):
    repo_id_clicked= request.GET.get('repo_id_clicked')
    tipe= request.GET.get('type')

    # This function is used for multiple 'tipe's. Search is the only one we are interested in here.
    if tipe == 'search':

        # type_3 determines what kind of search we want
        type_3= request.GET.get('type_3')
        
        # search for only file tags
        if type_3 == 'tag':
            # getting related tags
            a = relatedtags.objects.filter(tag =repo_id_clicked, localrepo__usern=request.user).values('parent_tag')

            # getting tags with matching related tags, and returning tags
            if a:
                results= list()
                b= list()
                for rel_tag in a:
                    if rel_tag['parent_tag'] not in b:
                        b.append(rel_tag['parent_tag'])
                for b_id in b:
                    results += list(tags.objects.filter(id=b_id, localrepo__usern=request.user).values(
                        'tag', 'id', 'foldercrumb', 'num'))

        # search for only file titles
        elif type_3 == 'file':
            # getting tags
            results= list(tags.objects.filter(tag=repo_id_clicked, localrepo__usern=request.user).values('tag', 'id', 'foldercrumb', 'num'))

        # search for only folder names
        elif type_3 == 'folder':
            # getting folders
            results= list(folders.objects.filter(name=repo_id_clicked, localrepo__usern=request.user).values('id', 'name', 'crumb', 'localrepo', 'deep_index'))

        # search for all maching.
        else:
            # getting folders
            results= list(folders.objects.filter(name=repo_id_clicked, localrepo__usern=request.user).values('id', 'name', 'crumb', 'localrepo', 'deep_index'))

            results += list(tags.objects.filter(tag=(repo_id_clicked + '\n'),
                            localrepo__usern=request.user).values('tag', 'id', 'foldercrumb', 'num'))
            results += list(tags.objects.filter(tag=(repo_id_clicked),
                            localrepo__usern=request.user).values('tag', 'id', 'foldercrumb', 'num'))

            #  getting related tags
            a = relatedtags.objects.filter(tag =repo_id_clicked, localrepo__usern=request.user).values('parent_tag')

            #  getting tags with matching related tags, and returning tags
            if a:
                b= list()
                for rel_tag in a:
                    if rel_tag['parent_tag'] not in b:
                        b.append(rel_tag['parent_tag'])
                for b_id in b:
                    results += list(tags.objects.filter(id=b_id, localrepo__usern=request.user).values(
                        'tag', 'id', 'foldercrumb', 'num'))


    return JsonResponse(results, safe=False)

And that concludes this snippet! That was the full-stack run through of one feature in NexusOrganizer! Feel free to check out the whole application live at: https://www.nexusorganizer.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment