From 17b79272f8b0650c200bccbee659e708474cb5cf Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 1 Nov 2016 16:38:35 -0500 Subject: [PATCH 01/12] Add filter/search bar --- app/assets/javascripts/dispatcher.js.es6 | 1 + .../filtered_search/filtered_search_bundle.js | 12 +++ .../filtered_search_manager.js.es6 | 87 +++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 57 ++++++++++++ app/views/projects/issues/index.html.haml | 5 +- .../shared/issuable/_search_bar.html.haml | 78 +++++++++++++++++ config/application.rb | 1 + 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_bundle.js create mode 100644 app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 create mode 100644 app/views/shared/issuable/_search_bar.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ff8b8f6d0aea..40612e8b7972 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -31,6 +31,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + new gl.FilteredSearchManager(); Issuable.init(); new gl.IssuableBulkActions(); shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 000000000000..e103748d499d --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,12 @@ +/* eslint-disable */ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +/*= require_tree . */ + +(function() { + +}).call(this); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 000000000000..9e626b012ed6 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,87 @@ +((global) => { + const TOKEN_KEYS = ['author', 'assignee', 'milestone', 'label', 'weight']; + + class FilteredSearchManager { + constructor() { + this.canDeleteTokenIfExists = false; + this.bindEvents(); + } + + bindEvents() { + const input = document.querySelector('.filtered-search'); + + input.addEventListener('focus', this.toggleContainerHighlight); + input.addEventListener('blur', this.toggleContainerHighlight); + input.addEventListener('input', this.checkTokens.bind(this)); + input.addEventListener('keyup', this.inputKeyup.bind(this)); + input.addEventListener('keydown', this.inputKeydown.bind(this)); + } + + toggleContainerHighlight(event) { + const container = document.querySelector('.filtered-search-container'); + container.classList.toggle('focus'); + } + + inputKeydown(event) { + if (event.key === 'Backspace' && event.target.value === '') { + this.canDeleteTokenIfExists = true; + } else { + this.canDeleteTokenIfExists = false; + } + } + + inputKeyup(event) { + if (event.key === 'Enter') { + this.search(); + } else if (this.canDeleteTokenIfExists) { + this.deleteToken(event.target); + } + } + + checkTokens(event) { + const text = event.target.value.toLowerCase(); + const hasColon = text[text.length - 1] === ':'; + + if (hasColon && TOKEN_KEYS.indexOf(text.slice(0, -1)) != -1) { + event.target.value = ''; + + const tokenKey = text.charAt(0).toUpperCase() + text.slice(1, -1); + this.addToken(tokenKey, event.target); + } + } + + addToken(key, inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + + let fragmentToken = document.createElement('li'); + fragmentToken.classList.add('fragment-token'); + + let fragmentKey = document.createElement('span'); + fragmentKey.classList.add('fragment-key'); + fragmentKey.innerText = key; + + fragmentToken.appendChild(fragmentKey); + fragmentList.insertBefore(fragmentToken, listItem); + } + + deleteToken(inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + const fragments = fragmentList.childNodes.length; + + if (fragments === 1) { + // Only input fragment found in fragmentList + return; + } + + fragmentList.removeChild(listItem.previousSibling); + } + + search() { + console.log('search'); + } + } + + global.FilteredSearchManager = FilteredSearchManager; +})(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385a..2bccd1fa2aa2 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,60 @@ } } +.filtered-search-container { + display: flex; +} + +.filtered-search-input-container { + display: flex; + + .fa-filter { + color: #444; + } + + .fragment-list { + margin: 0; + list-style-type: none; + padding-left: 5px; + flex: 1; + } +} + +.fragment-list { + display: flex; + + .fragment-token { + margin-right: 5px; + } + + .fragment-key { + color: #959494; + background-color: #f5f5f5; + padding: 2px 5px; + margin-right: 3px; + border-radius: 2px; + } + + .fragment-search { + flex: 1; + } + + .filtered-search { + width: 100%; + } +} + +.transparent { + border: none; + + &:focus { + outline: none; + border: none; + } +} + +.form-control { + &.focus { + @extend .form-control:focus + } +} diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index cc57cfdb3423..8b080063f6d9 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,6 +6,9 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") @@ -30,7 +33,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 000000000000..e9eaefbe3b5c --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,78 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- boards_page = controller.controller_name == 'boards' + +.issues-filters + .issues-details-filters.row-content-block.second-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container.form-control + = icon('filter') + %ul.fragment-list + %li.fragment-search + %input.filtered-search.transparent{ placeholder: 'Search or filter results...' } + .pull-right + - if boards_page + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + - else + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "reopen"}} Open + %li + %a{href: "#", data: {id: "close"}} Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "subscribe"}} Subscribe + %li + %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/config/application.rb b/config/application.rb index 946b632b0e80..008f7e8ff78d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,6 +96,7 @@ class Application < Rails::Application config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" + config.assets.precompile << "filtered_search/filtered_search_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" -- GitLab From d585f6a2be124ed67560847b7b754db0e9f09482 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 11:34:24 -0500 Subject: [PATCH 02/12] Vertically align filter icon --- app/assets/stylesheets/framework/filters.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2bccd1fa2aa2..569ae7b850c1 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -31,6 +31,7 @@ display: flex; .fa-filter { + line-height: 21px; color: #444; } -- GitLab From 19e2082fa6b1a483ccf11ce1d6af65b94333e51b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 15:17:56 -0500 Subject: [PATCH 03/12] Add tokenization of regular text --- .../filtered_search_manager.js.es6 | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 9e626b012ed6..01ee15237df5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -39,29 +39,43 @@ } checkTokens(event) { - const text = event.target.value.toLowerCase(); + const value = event.target.value; + + const split = value.toLowerCase().split(' '); + const text = split.length === 1 ? split[0] : split[split.length - 1]; const hasColon = text[text.length - 1] === ':'; + const token = text.slice(0, -1); - if (hasColon && TOKEN_KEYS.indexOf(text.slice(0, -1)) != -1) { - event.target.value = ''; + if (hasColon && TOKEN_KEYS.indexOf(token) != -1) { + // One for the colon and one for the space before it + const textWithoutToken = value.substring(0, value.length - token.length - 2) + this.addTextToken(textWithoutToken, event.target); - const tokenKey = text.charAt(0).toUpperCase() + text.slice(1, -1); + const tokenKey = token.charAt(0).toUpperCase() + token.slice(1); this.addToken(tokenKey, event.target); + + event.target.value = ''; } } + addTextToken(text, inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + + let fragmentToken = document.createElement('li'); + fragmentToken.innerHTML = `${text}` + + fragmentList.insertBefore(fragmentToken, listItem); + } + addToken(key, inputNode) { const listItem = inputNode.parentNode; const fragmentList = listItem.parentNode; let fragmentToken = document.createElement('li'); fragmentToken.classList.add('fragment-token'); + fragmentToken.innerHTML = `${key}` - let fragmentKey = document.createElement('span'); - fragmentKey.classList.add('fragment-key'); - fragmentKey.innerText = key; - - fragmentToken.appendChild(fragmentKey); fragmentList.insertBefore(fragmentToken, listItem); } -- GitLab From 1cb030aa1cead22309cafc518daac49fa8f4f45d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 15:58:21 -0500 Subject: [PATCH 04/12] Add droplab --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/droplab/droplab.js | 368 ++++++++++++++++++ .../javascripts/droplab/droplab_ajax.js | 44 +++ app/assets/stylesheets/framework/filters.scss | 5 + .../shared/issuable/_search_bar.html.haml | 3 +- 5 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/droplab/droplab.js create mode 100644 app/assets/javascripts/droplab/droplab_ajax.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7dd9adac736e..5d643d0c1560 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -51,6 +51,7 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 000000000000..3640be423d37 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,368 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o '#filter-dropdown' } + %ul.dropdown-menu.droplab-menu#filter-dropdown{ 'data-dropdown' => true} .pull-right - if boards_page #js-boards-seach.issue-boards-search -- GitLab From caba030e052d8f9989614690ef0bf94c6a7e865b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 15:58:59 -0500 Subject: [PATCH 05/12] Auto update input placeholder text --- .../filtered_search/filtered_search_manager.js.es6 | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 01ee15237df5..c937084b36b9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -23,7 +23,8 @@ } inputKeydown(event) { - if (event.key === 'Backspace' && event.target.value === '') { + const fragmentList = event.target.parentNode.parentNode; + if (event.key === 'Backspace' && event.target.value === '' && fragmentList.childElementCount > 1) { this.canDeleteTokenIfExists = true; } else { this.canDeleteTokenIfExists = false; @@ -36,6 +37,11 @@ } else if (this.canDeleteTokenIfExists) { this.deleteToken(event.target); } + + const fragmentList = event.target.parentNode.parentNode; + if (fragmentList.childElementCount === 1) { + event.target.placeholder = 'Search or filter results...'; + } } checkTokens(event) { @@ -55,6 +61,10 @@ this.addToken(tokenKey, event.target); event.target.value = ''; + event.target.placeholder = ''; + + event.target.nextElementSibling.innerHTML += `
  • test
  • `; + droplab.addHook(event.target); } } -- GitLab From abbfd548ed35b4bb62d9022df36fa9392060d070 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 16:01:09 -0500 Subject: [PATCH 06/12] Prevent form search propagation --- .../filtered_search/filtered_search_manager.js.es6 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index c937084b36b9..e2a925820ef7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -29,12 +29,16 @@ } else { this.canDeleteTokenIfExists = false; } - } - inputKeyup(event) { if (event.key === 'Enter') { this.search(); - } else if (this.canDeleteTokenIfExists) { + event.stopPropagation(); + event.preventDefault(); + } + } + + inputKeyup(event) { + if (this.canDeleteTokenIfExists) { this.deleteToken(event.target); } -- GitLab From 4cab2bfc001c07d7d1a63a32dde341601b26c53e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 16:20:29 -0500 Subject: [PATCH 07/12] Add input focus highlight --- app/assets/stylesheets/framework/filters.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 3efe95f30127..63f8a921ed66 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -25,6 +25,16 @@ .filtered-search-container { display: flex; + + &.focus { + .filtered-search-input-container { + @extend .form-control:focus + } + + .fa-filter { + color: #444; + } + } } .filtered-search-input-container { @@ -32,7 +42,7 @@ .fa-filter { line-height: 21px; - color: #444; + color: $gray-darkest; } .fragment-list { -- GitLab From 085d6219f2e036fa2b2ad3391d22f7154f135b04 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 2 Nov 2016 16:27:09 -0500 Subject: [PATCH 08/12] Remove original search form --- app/views/projects/issues/index.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 8b080063f6d9..f5db54986fd1 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -23,7 +23,6 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, -- GitLab From f005dc4891f1b4a2ce838e7232dd1b3b899d4d9e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 11:44:11 -0500 Subject: [PATCH 09/12] Refactor tokenizer --- .../filtered_search_manager.js.es6 | 64 +---------------- .../filtered_search/tokenizer.js.es6 | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/tokenizer.js.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index e2a925820ef7..d3311e904c12 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,6 +1,4 @@ ((global) => { - const TOKEN_KEYS = ['author', 'assignee', 'milestone', 'label', 'weight']; - class FilteredSearchManager { constructor() { this.canDeleteTokenIfExists = false; @@ -12,7 +10,7 @@ input.addEventListener('focus', this.toggleContainerHighlight); input.addEventListener('blur', this.toggleContainerHighlight); - input.addEventListener('input', this.checkTokens.bind(this)); + input.addEventListener('input', gl.Tokenizer.checkTokens); input.addEventListener('keyup', this.inputKeyup.bind(this)); input.addEventListener('keydown', this.inputKeydown.bind(this)); } @@ -39,7 +37,7 @@ inputKeyup(event) { if (this.canDeleteTokenIfExists) { - this.deleteToken(event.target); + gl.Tokenizer.deleteToken(event.target); } const fragmentList = event.target.parentNode.parentNode; @@ -48,64 +46,6 @@ } } - checkTokens(event) { - const value = event.target.value; - - const split = value.toLowerCase().split(' '); - const text = split.length === 1 ? split[0] : split[split.length - 1]; - const hasColon = text[text.length - 1] === ':'; - const token = text.slice(0, -1); - - if (hasColon && TOKEN_KEYS.indexOf(token) != -1) { - // One for the colon and one for the space before it - const textWithoutToken = value.substring(0, value.length - token.length - 2) - this.addTextToken(textWithoutToken, event.target); - - const tokenKey = token.charAt(0).toUpperCase() + token.slice(1); - this.addToken(tokenKey, event.target); - - event.target.value = ''; - event.target.placeholder = ''; - - event.target.nextElementSibling.innerHTML += `
  • test
  • `; - droplab.addHook(event.target); - } - } - - addTextToken(text, inputNode) { - const listItem = inputNode.parentNode; - const fragmentList = listItem.parentNode; - - let fragmentToken = document.createElement('li'); - fragmentToken.innerHTML = `${text}` - - fragmentList.insertBefore(fragmentToken, listItem); - } - - addToken(key, inputNode) { - const listItem = inputNode.parentNode; - const fragmentList = listItem.parentNode; - - let fragmentToken = document.createElement('li'); - fragmentToken.classList.add('fragment-token'); - fragmentToken.innerHTML = `${key}` - - fragmentList.insertBefore(fragmentToken, listItem); - } - - deleteToken(inputNode) { - const listItem = inputNode.parentNode; - const fragmentList = listItem.parentNode; - const fragments = fragmentList.childNodes.length; - - if (fragments === 1) { - // Only input fragment found in fragmentList - return; - } - - fragmentList.removeChild(listItem.previousSibling); - } - search() { console.log('search'); } diff --git a/app/assets/javascripts/filtered_search/tokenizer.js.es6 b/app/assets/javascripts/filtered_search/tokenizer.js.es6 new file mode 100644 index 000000000000..0e4f4d5e80ad --- /dev/null +++ b/app/assets/javascripts/filtered_search/tokenizer.js.es6 @@ -0,0 +1,72 @@ +((global) => { + const TOKEN_KEYS = ['author', 'assignee', 'milestone', 'label', 'weight']; + + class Tokenizer { + static checkTokens(event) { + const value = event.target.value; + + const split = value.toLowerCase().split(' '); + const text = split.length === 1 ? split[0] : split[split.length - 1]; + const hasColon = text[text.length - 1] === ':'; + const token = text.slice(0, -1); + + if (hasColon && TOKEN_KEYS.indexOf(token) != -1) { + // One for the colon and one for the space before it + const textWithoutToken = value.substring(0, value.length - token.length - 2) + gl.Tokenizer.addTextToken(textWithoutToken, event.target); + + const tokenKey = token.charAt(0).toUpperCase() + token.slice(1); + gl.Tokenizer.addToken(tokenKey, event.target); + + event.target.value = ''; + event.target.placeholder = ''; + + event.target.nextElementSibling.innerHTML += `
  • test
  • `; + droplab.addHook(event.target); + } + } + + static addTextToken(text, inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + + let fragmentToken = document.createElement('li'); + fragmentToken.innerHTML = `${text}` + + fragmentList.insertBefore(fragmentToken, listItem); + + // TODO: AddEventListener for Click => converts span into input + // Note: What if each text token remained as an input? + } + + static addToken(key, inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + + let fragmentToken = document.createElement('li'); + fragmentToken.classList.add('fragment-token'); + fragmentToken.innerHTML = `${key}` + + fragmentList.insertBefore(fragmentToken, listItem); + + // TODO: AddEventListener for Click => darken and move cursor + // TODO: AddEventListener for DoubleClick Editing Mode + // TODO: Add `x` for deleting entire token + } + + static deleteToken(inputNode) { + const listItem = inputNode.parentNode; + const fragmentList = listItem.parentNode; + const fragments = fragmentList.childNodes.length; + + if (fragments === 1) { + // Only input fragment found in fragmentList + return; + } + + fragmentList.removeChild(listItem.previousSibling); + } + } + + global.Tokenizer = Tokenizer; +})(window.gl || (window.gl = {})); -- GitLab From 37b13724c4a71eb48228dab87b17aa0153242ac2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 11:44:31 -0500 Subject: [PATCH 10/12] Add initial dropdown --- app/assets/stylesheets/framework/filters.scss | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 63f8a921ed66..4d17af08d102 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -95,4 +95,13 @@ .droplab-menu { position: relative; top: 0; -} \ No newline at end of file + + li { + padding: 8px; + } + + li:hover { + color: white; + background-color: $gl-link-color; + } +} -- GitLab From a1f25fd00d124dd376591140116b78811248d272 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 11:49:43 -0500 Subject: [PATCH 11/12] Add default dropdown --- .../filtered_search_manager.js.es6 | 26 +++++++++++++++++++ .../shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index d3311e904c12..1b07571c70bb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -3,6 +3,7 @@ constructor() { this.canDeleteTokenIfExists = false; this.bindEvents(); + this.renderDefaultDropdown(document.querySelector('#filter-dropdown')); } bindEvents() { @@ -46,6 +47,31 @@ } } + renderDefaultDropdown(filterDropdown) { + filterDropdown.innerHTML = ` +
  • + + Keep typing and press Enter +
  • +
  • + + author: <author> +
  • +
  • + + assignee: <assignee> +
  • +
  • + + milestone: <milestone> +
  • +
  • + + label: <label> +
  • + `; + } + search() { console.log('search'); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b6fe4754ec72..640cf141252b 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,7 +16,7 @@ %ul.fragment-list %li.fragment-search %input.filtered-search.transparent{ placeholder: 'Search or filter results...', 'data-dropdown-trigger' => '#filter-dropdown' } - %ul.dropdown-menu.droplab-menu#filter-dropdown{ 'data-dropdown' => true} + %ul.dropdown-menu.droplab-menu#filter-dropdown{ 'data-dropdown' => true } .pull-right - if boards_page #js-boards-seach.issue-boards-search -- GitLab From 54fd054b6596e31160cb63121d2af6a9206aadd5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 12:07:41 -0500 Subject: [PATCH 12/12] Load author dropdown using ajax --- .../javascripts/filtered_search/tokenizer.js.es6 | 15 ++++++++++++++- app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/tokenizer.js.es6 b/app/assets/javascripts/filtered_search/tokenizer.js.es6 index 0e4f4d5e80ad..e60358ffca97 100644 --- a/app/assets/javascripts/filtered_search/tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/tokenizer.js.es6 @@ -21,7 +21,20 @@ event.target.value = ''; event.target.placeholder = ''; - event.target.nextElementSibling.innerHTML += `
  • test
  • `; + if (token === 'author') { + + let ul = event.target.nextElementSibling; + ul.setAttribute('data-dynamic', true); + + ul.innerHTML = `
  • + + {{name}} + @{{username}} +
  • `; + droplab.addData('filterDropdownInput', '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + } + + // event.target.nextElementSibling.innerHTML += `
  • test
  • `; droplab.addHook(event.target); } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 640cf141252b..0d0a77bdbd93 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -15,7 +15,7 @@ = icon('filter') %ul.fragment-list %li.fragment-search - %input.filtered-search.transparent{ placeholder: 'Search or filter results...', 'data-dropdown-trigger' => '#filter-dropdown' } + %input.filtered-search.transparent{ placeholder: 'Search or filter results...', 'data-dropdown-trigger' => '#filter-dropdown', 'data-id' => 'filterDropdownInput' } %ul.dropdown-menu.droplab-menu#filter-dropdown{ 'data-dropdown' => true } .pull-right - if boards_page -- GitLab