diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index 461101a66c..3ddb7af0e1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -55,6 +55,11 @@ }); })); + scope.searchClick = function() { + var showSearch = appState.getSearchState("show"); + appState.setSearchState("show", !showSearch); + }; + // toggle the help dialog by raising the global app state to toggle the help drawer scope.helpClick = function () { var showDrawer = appState.getDrawerState("showDrawer"); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js new file mode 100644 index 0000000000..7ff76d5670 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js @@ -0,0 +1,118 @@ +(function () { + 'use strict'; + + /** + * A component to render the pop up search field + */ + var umbSearch = { + templateUrl: 'views/components/application/umb-search.html', + controllerAs: 'vm', + controller: umbSearchController, + bindings: { + onClose: "&" + } + }; + + function umbSearchController($timeout, backdropService, searchService) { + + var vm = this; + + vm.$onInit = onInit; + vm.$onDestroy = onDestroy; + vm.search = search; + vm.clickItem = clickItem; + vm.clearSearch = clearSearch; + vm.handleKeyUp = handleKeyUp; + vm.closeSearch = closeSearch; + vm.focusSearch = focusSearch; + + function onInit() { + vm.searchQuery = ""; + vm.searchResults = []; + vm.hasResults = false; + focusSearch(); + backdropService.open(); + } + + function onDestroy() { + backdropService.close(); + } + + /** + * Handles when a search result is clicked + */ + function clickItem() { + closeSearch(); + } + + /** + * Clears the search query + */ + function clearSearch() { + vm.searchQuery = ""; + vm.searchResults = []; + vm.hasResults = false; + focusSearch(); + } + + /** + * Add focus to the search field + */ + function focusSearch() { + vm.searchHasFocus = false; + $timeout(function(){ + vm.searchHasFocus = true; + }); + } + + /** + * Handles all keyboard events + * @param {object} event + */ + function handleKeyUp(event) { + // esc + if(event.keyCode === 27) { + closeSearch(); + } + } + + /** + * Used to proxy a callback + */ + function closeSearch() { + if(vm.onClose) { + vm.onClose(); + } + } + + /** + * Used to search + * @param {string} searchQuery + */ + function search(searchQuery) { + if(searchQuery.length > 0) { + var search = {"term": searchQuery}; + searchService.searchAll(search).then(function(result){ + //result is a dictionary of group Title and it's results + var filtered = {}; + _.each(result, function (value, key) { + if (value.results.length > 0) { + filtered[key] = value; + } + }); + // bind to view model + vm.searchResults = filtered; + // check if search has results + vm.hasResults = Object.keys(vm.searchResults).length > 0; + }); + + } else { + clearSearch(); + } + } + + } + + angular.module('umbraco.directives').component('umbSearch', umbSearch); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js index 085ba52b7e..d1ac3d39cf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js @@ -74,6 +74,11 @@ function appState(eventsService) { showMenu: null }; + var searchState = { + //Whether the search is being shown or not + show: null + }; + var drawerState = { //this view to show view: null, @@ -221,6 +226,35 @@ function appState(eventsService) { setState(menuState, key, value, "menuState"); }, + /** + * @ngdoc function + * @name umbraco.services.angularHelper#getSearchState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Returns the current search state value by key - we do not return an object here - we do NOT want this + * to be publicly mutable and allow setting arbitrary values + * + */ + getSearchState: function (key) { + return getState(searchState, key, "searchState"); + }, + + /** + * @ngdoc function + * @name umbraco.services.angularHelper#setSearchState + * @methodOf umbraco.services.appState + * @function + * + * @description + * Sets a section state value by key + * + */ + setSearchState: function (key, value) { + setState(searchState, key, value, "searchState"); + }, + /** * @ngdoc function * @name umbraco.services.angularHelper#getDrawerState diff --git a/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js b/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js index e463845a1c..4f977cb1b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js @@ -58,8 +58,11 @@ * */ function close() { - args.element = null; - args.show = false; + args.opacity = null, + args.element = null, + args.elementPreventClick = false, + args.disableEventsOnClick = false, + args.show = false eventsService.emit("appState.backdrop", args); } diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index 24318cce02..a10943c17e 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -15,6 +15,8 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.touchDevice = appState.getGlobalState("touchDevice"); $scope.editors = []; $scope.overlay = {}; + $scope.drawer = {}; + $scope.search = {}; $scope.removeNotification = function (index) { notificationsService.remove(index); @@ -41,6 +43,10 @@ function MainController($scope, $location, appState, treeService, notificationsS eventsService.emit("app.closeDialogs", event); }; + $scope.closeSearch = function() { + appState.setSearchState("show", false); + }; + var evts = []; //when a user logs out or timesout @@ -114,9 +120,15 @@ function MainController($scope, $location, appState, treeService, notificationsS }; })); + // events for search + evts.push(eventsService.on("appState.searchState.changed", function (e, args) { + if (args.key === "show") { + $scope.search.show = args.value; + } + })); + // events for drawer // manage the help dialog by subscribing to the showHelp appState - $scope.drawer = {}; evts.push(eventsService.on("appState.drawerState.changed", function (e, args) { // set view if (args.key === "view") { @@ -156,6 +168,7 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.backdrop = args; })); + // event for infinite editors evts.push(eventsService.on("appState.editors.add", function (name, args) { $scope.editors = args.editors; })); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js deleted file mode 100644 index bec3e97543..0000000000 --- a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @ngdoc controller - * @name Umbraco.SearchController - * @function - * - * @description - * Controls the search functionality in the site - * - */ -function SearchController($scope, searchService, $log, $location, navigationService, $q) { - - $scope.searchTerm = null; - $scope.searchResults = []; - $scope.isSearching = false; - $scope.selectedResult = -1; - - $scope.navigateResults = function (ev) { - //38: up 40: down, 13: enter - - switch (ev.keyCode) { - case 38: - iterateResults(true); - break; - case 40: - iterateResults(false); - break; - case 13: - if ($scope.selectedItem) { - $location.path($scope.selectedItem.editorPath); - navigationService.hideSearch(); - } - break; - } - }; - - var group = undefined; - var groupNames = []; - var groupIndex = -1; - var itemIndex = -1; - $scope.selectedItem = undefined; - - $scope.clearSearch = function () { - $scope.searchTerm = null; - }; - function iterateResults(up) { - //default group - if (!group) { - - for (var g in $scope.groups) { - if ($scope.groups.hasOwnProperty(g)) { - groupNames.push(g); - - } - } - - //Sorting to match the groups order - groupNames.sort(); - - group = $scope.groups[groupNames[0]]; - groupIndex = 0; - } - - if (up) { - if (itemIndex === 0) { - if (groupIndex === 0) { - gotoGroup(Object.keys($scope.groups).length - 1, true); - } else { - gotoGroup(groupIndex - 1, true); - } - } else { - gotoItem(itemIndex - 1); - } - } else { - if (itemIndex < group.results.length - 1) { - gotoItem(itemIndex + 1); - } else { - if (groupIndex === Object.keys($scope.groups).length - 1) { - gotoGroup(0); - } else { - gotoGroup(groupIndex + 1); - } - } - } - } - - function gotoGroup(index, up) { - groupIndex = index; - group = $scope.groups[groupNames[groupIndex]]; - - if (up) { - gotoItem(group.results.length - 1); - } else { - gotoItem(0); - } - } - - function gotoItem(index) { - itemIndex = index; - $scope.selectedItem = group.results[itemIndex]; - } - - //used to cancel any request in progress if another one needs to take it's place - var canceler = null; - - $scope.$watch("searchTerm", _.debounce(function (newVal, oldVal) { - $scope.$apply(function () { - $scope.hasResults = false; - if ($scope.searchTerm) { - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - - //Resetting for brand new search - group = undefined; - groupNames = []; - groupIndex = -1; - itemIndex = -1; - - $scope.isSearching = true; - navigationService.showSearch(); - $scope.selectedItem = undefined; - - //a canceler exists, so perform the cancelation operation and reset - if (canceler) { - canceler.resolve(); - canceler = $q.defer(); - } - else { - canceler = $q.defer(); - } - - searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function (result) { - - //result is a dictionary of group Title and it's results - var filtered = {}; - _.each(result, function (value, key) { - if (value.results.length > 0) { - filtered[key] = value; - } - }); - $scope.groups = filtered; - // check if search has results - $scope.hasResults = Object.keys($scope.groups).length > 0; - //set back to null so it can be re-created - canceler = null; - $scope.isSearching = false; - }); - } - } - else { - $scope.isSearching = false; - navigationService.hideSearch(); - $scope.selectedItem = undefined; - } - }); - }, 200)); - -} -//register it -angular.module('umbraco').controller("Umbraco.SearchController", SearchController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b2f2f99dd5..37ba0366eb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -84,6 +84,7 @@ @import "components/application/umb-app-content.less"; @import "components/application/umb-tour.less"; @import "components/application/umb-backdrop.less"; +@import "components/application/umb-search.less"; @import "components/application/umb-drawer.less"; @import "components/application/umb-language-picker.less"; @import "components/application/umb-dashboard.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less new file mode 100644 index 0000000000..a8fc9c7f8e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less @@ -0,0 +1,110 @@ + +/* + Search wrapper +*/ +.umb-search { + position: fixed; + z-index: @zindexUmbOverlay; + width: 660px; + max-width: 90%; + transform: translate(-50%, 0); + left: 50%; + top: 20%; + border-radius: @baseBorderRadius; + background: @white; + position: fixed; + box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14); +} + +/* + Search field +*/ + +.umb-search-input-icon { + font-size: 22px; + color: @gray-7; + padding-left: 20px; + display: flex; + align-items: center; + height: 70px; +} + +.umb-search-input.umb-search-input { + width: 100%; + height: 70px; + border: none; + padding: 20px 20px 20px 15px; + border-radius: @baseBorderRadius; + font-size: 22px; + margin-bottom: 0; +} + +.umb-search-input-clear { + background: none; + border: none; + font-size: 12px; + margin-right: 20px; + color: @gray-3; +} + +.umb-search-input-clear.ng-enter { + opacity: 0; + transition: opacity 100ms ease-in-out; +} + +.umb-search-input-clear.ng-enter.ng-enter-active { + opacity: 1; +} + +/* + Search results +*/ +.umb-search-results { + max-height: 50vh; + overflow-y: auto; +} + +.umb-search-group__title { + background: @gray-10; + padding: 3px 20px; +} + +.umb-search-items { + list-style: none; + margin: 0; + padding-top: 4px; + padding-bottom: 4px; +} + +.umb-search-item > a { + padding: 6px 20px; + display: flex; +} + +.umb-search-item > a:hover, +.umb-search-item > a:focus { + background-color: @gray-10; + text-decoration: none; + outline: none; +} + +.umb-search-item > a:focus { + padding-left: 25px; + transition: padding 60ms ease-in-out; +} + +.umb-search-result__icon { + font-size: 18px; + margin-right: 8px; + color: @gray-1; +} + +.umb-search-result__meta { + display: flex; + flex-direction: column; +} + +.umb-search-result__description { + color: @gray-5; + font-size: 13px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html new file mode 100644 index 0000000000..56d9eae16c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -0,0 +1,34 @@ + +