diff --git a/.gitignore b/.gitignore index 046df47..87ba9b4 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,10 @@ $RECYCLE.BIN/ # Mac crap .DS_Store /Breaking change Nov 2014.txt +*.sln +*.suo +acute/Properties +acute/*.config +acute/*.user +acute/*.csproj +acute/acute.core/.* diff --git a/acute-select.sln b/acute-select.sln deleted file mode 100644 index 48ca0cc..0000000 --- a/acute-select.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 2012 for Web -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "acute", "acute\acute.csproj", "{916A0C7C-7A56-42E0-9E77-6D8D4E493ADF}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {916A0C7C-7A56-42E0-9E77-6D8D4E493ADF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {916A0C7C-7A56-42E0-9E77-6D8D4E493ADF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {916A0C7C-7A56-42E0-9E77-6D8D4E493ADF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {916A0C7C-7A56-42E0-9E77-6D8D4E493ADF}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/acute-select.v11.suo b/acute-select.v11.suo deleted file mode 100644 index 1d9a63e..0000000 Binary files a/acute-select.v11.suo and /dev/null differ diff --git a/acute/Properties/AssemblyInfo.cs b/acute/Properties/AssemblyInfo.cs deleted file mode 100644 index 06c0086..0000000 --- a/acute/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("acute")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Microsoft")] -[assembly: AssemblyProduct("acute")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("23b05f9e-0e70-46cd-a1a9-1b6d0dddd5c1")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/acute/Web.config b/acute/Web.config deleted file mode 100644 index 6da78ec..0000000 --- a/acute/Web.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/acute/acute.core/acute.core.directives.js b/acute/acute.core/acute.core.directives.js deleted file mode 100644 index 65433eb..0000000 --- a/acute/acute.core/acute.core.directives.js +++ /dev/null @@ -1,59 +0,0 @@ -angular.module("acute.core.directives", []) -// Directive to set focus to an element when a specified expression is true -.directive('acuteFocus', function ($timeout, $parse) { - return { - restrict: "A", - link: function (scope, element, attributes) { - var setFocus = $parse(attributes.acuteFocus); - scope.$watch(setFocus, function (value) { - if (value === true) { - $timeout(function () { - element[0].focus(); - }); - } - }); - // Set the "setFocus" attribute value to 'false' on blur event - // using the "assign" method on the function that $parse returns - element.bind('blur', function () { - scope.$apply(setFocus.assign(scope, false)); - }); - } - }; -}) - -// Directive for a scroll container. Set acute-scroll-top to an expression and the div will scroll when it changes -.directive('acScrollTo', function () { - return { - restrict: "A", - scope: false, - controller: function ($scope, $element, $attrs) { - var expression = $attrs.acScrollTo; - $scope.$watch(expression, function () { - var scrollTop = $scope.$eval(expression); - angular.element($element)[0].scrollTop = scrollTop; - }); - } - }; -}) - -// Call a function when the element is scrolled -// E.g. ac-on-scroll="listScrolled()" -// N.B. take care not to use the result to directly update an acScrollTo expression -// as this will result in an infinite recursion! -.directive('acOnScroll', function () { - return { - restrict: "A", - link: function (scope, element, attrs) { - var callbackName = attrs.acOnScroll; - if (callbackName.indexOf("()") === callbackName.length - 2) { - callbackName = callbackName.substr(0, callbackName.length - 2); - } - var callback = scope[callbackName]; - if (typeof callback === "function") { - element.bind("scroll", function () { - callback(element[0].scrollTop); - }); - } - } - }; -}); \ No newline at end of file diff --git a/acute/acute.core/acute.core.services.js b/acute/acute.core/acute.core.services.js deleted file mode 100644 index b13958d..0000000 --- a/acute/acute.core/acute.core.services.js +++ /dev/null @@ -1,112 +0,0 @@ -// Common services -angular.module("acute.core.services", []) - -// safeApply service, courtesy Alex Vanston and Andrew Reutter -.factory('safeApply', [function ($rootScope) { - return function ($scope, fn) { - var phase = $scope.$root.$$phase; - if (phase == '$apply' || phase == '$digest') { - if (fn) { - $scope.$eval(fn); - } - } else { - if (fn) { - $scope.$apply(fn); - } else { - $scope.$apply(); - } - } - } -}]) - -.factory('keyCode', function () { - return { - 'backspace': 8, - 'tab': 9, - 'enter': 13, - 'shift': 16, - 'ctrl': 17, - 'alt': 18, - 'pause': 19, - 'capsLock': 20, - 'esc': 27, - 'escape': 27, - 'pageUp': 33, - 'pageDown': 34, - 'end': 35, - 'home': 36, - 'leftArrow': 37, - 'upArrow': 38, - 'rightArrow': 39, - 'downArrow': 40, - 'insert': 45, - 'del': 46, // Note cannot use "delete" as it breaks IE8 because it's a reserved word - '0': 48, - '1': 49, - '2': 50, - '3': 51, - '4': 52, - '5': 53, - '6': 54, - '7': 55, - '8': 56, - '9': 57, - 'a': 65, - 'b': 66, - 'c': 67, - 'd': 68, - 'e': 69, - 'f': 70, - 'g': 71, - 'h': 72, - 'i': 73, - 'j': 74, - 'k': 75, - 'l': 76, - 'm': 77, - 'n': 78, - 'o': 79, - 'p': 80, - 'q': 81, - 'r': 82, - 's': 83, - 't': 84, - 'u': 85, - 'v': 86, - 'w': 87, - 'x': 88, - 'y': 89, - 'z': 90, - '0numpad': 96, - '1numpad': 97, - '2numpad': 98, - '3numpad': 99, - '4numpad': 100, - '5numpad': 101, - '6numpad': 102, - '7numpad': 103, - '8numpad': 104, - '9numpad': 105, - 'multiply': 106, - 'plus': 107, - 'minus': 109, - 'dot': 110, - 'slash1': 111, - 'F1': 112, - 'F2': 113, - 'F3': 114, - 'F4': 115, - 'F5': 116, - 'F6': 117, - 'F7': 118, - 'F8': 119, - 'F9': 120, - 'F10': 121, - 'F11': 122, - 'F12': 123, - 'equals': 187, - 'comma': 188, - 'slash': 191, - 'backslash': 220 - }; -}); \ No newline at end of file diff --git a/acute/acute.csproj b/acute/acute.csproj deleted file mode 100644 index d1d6e3f..0000000 --- a/acute/acute.csproj +++ /dev/null @@ -1,148 +0,0 @@ - - - - - Debug - AnyCPU - - - 2.0 - {916A0C7C-7A56-42E0-9E77-6D8D4E493ADF} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - acute - acute - v4.5 - true - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TestWS.asmx - Component - - - - - - - Web.config - - - Web.config - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 51773 - / - http://localhost:49751/ - False - False - - - False - - - - - - \ No newline at end of file diff --git a/acute/acute.csproj.user b/acute/acute.csproj.user deleted file mode 100644 index 2ee016f..0000000 --- a/acute/acute.csproj.user +++ /dev/null @@ -1,30 +0,0 @@ - - - - ShowAllFiles - - - - - - test-pages/TestAcuteSelect.htm - SpecificPage - True - False - False - False - - - - - - - - - False - True - - - - - \ No newline at end of file diff --git a/acute/acute.select/acute.select.js b/acute/acute.select/acute.select.js index 74225b9..dd5dbe8 100644 --- a/acute/acute.select/acute.select.js +++ b/acute/acute.select/acute.select.js @@ -1,4 +1,4 @@ -/// +/// // Directive that creates a searchable dropdown list. @@ -11,244 +11,132 @@ // Note:- ac-options works like ng-options, but does not support option groups angular.module("acute.select", []) -.directive("acSelect", function($parse, acuteSelectService) { +.directive("acSelect", ["$parse", "acuteSelectService", function ($parse, acuteSelectService) { var defaultSettings = acuteSelectService.getSettings(); return { restrict: "EAC", scope: { "acSettings": "@", - "acOptions": "@", - "model": "=acModel", "acChange": "&", "keyField": "@acKey", - "acRefresh": "=", - "acFocusWhen": "=" + "model": "=acModel", }, replace: true, templateUrl: defaultSettings.templatePath + "acute.select.htm", - link: function(scope, element, attrs) { - scope.initialise(); - }, - // ************************************************************** - // CONTROLLER - // ************************************************************** - controller: function($scope, $element, $window, $rootScope, $timeout, $filter, navKey, safeApply) { + link: function (scope, element, attrs) { + + scope.settings = acuteSelectService.getSettings(); + + scope.searchText = ""; + scope.longestText = ""; + scope.comboText = ""; + scope.items = []; + scope.allItems = []; // Unfiltered + scope.selectedItem = null; + scope.allDataLoaded = false; + scope.scrollTo = 0; // To change scroll position + scope.scrollPosition = 0; // Reported scroll position + scope.listHeight = 0; + scope.matchFound = false; + + // Check that ac-options and ac-model values are set + var acOptions = attrs.acOptions; + if (acOptions === undefined || attrs.acModel === undefined) { + throw "ac-options and ac-model attributes must be set"; + } - $scope.initialise = function() { - $scope.settings = acuteSelectService.getSettings(); - $scope.previousSearchText = ""; - $scope.searchText = ""; - $scope.longestText = ""; - $scope.comboText = ""; - $scope.items = []; - $scope.allItems = []; // Unfiltered - $scope.selectedItem = null; - $scope.allDataLoaded = false; - $scope.scrollTo = 0; // To change scroll position - $scope.scrollPosition = 0; // Reported scroll position - $scope.listHeight = 0; - $scope.matchFound = false; - - // Check that ac-options and ac-model values are set - if (!$scope.acOptions || $scope.model === undefined) { - throw "ac-model and ac-options attributes must be set"; + if (attrs.acSettings != undefined) { + scope.acSettings = scope.$eval(attrs.acSettings); + if (typeof scope.acSettings === "object") { + // Merge settings with default values + angular.extend(scope.settings, scope.acSettings); } + } - processSettings(); - - // Parse acOptions - - // Value should be in the form "label for value in array" or "for value in array" - var words = $scope.acOptions.split(' '); - var len = words.length; - $scope.textField = null; - $scope.dataFunction = null; - - if (len > 3) { - if (len > 4) { - var label = words[len - 5]; // E.g. colour.name - $scope.textField = label.split(".")[1]; - } - var dataName = words[len - 1]; - - // See if a data load function is specified, i.e. name ends in "()" - if (dataName.indexOf("()") === dataName.length - 2) { - dataName = dataName.substr(0, dataName.length - 2) - // Get a reference to the data function - var dataFunction = $scope.$parent.$eval(dataName); - if (typeof dataFunction === "function") { - $scope.dataFunction = dataFunction; - if ($scope.settings.loadOnCreate) { - // Load initial data (args are callback function, search text and item offset) - $scope.dataFunction($scope.dataCallback, "", 0); - } + // Parse acOptions + + // Value should be in the form "label for value in array" or "for value in array" + var words = acOptions.split(" "); + var len = words.length; + scope.textField = null; + scope.dataFunction = null; + + if (len > 3) { + if (len > 4) { + var label = words[len - 5]; // E.g. colour.name + scope.textField = label.split(".")[1]; + } + var dataName = words[len - 1]; + + // See if a data load function is specified, i.e. name ends in "()" + if (dataName.indexOf("()") === dataName.length - 2) { + dataName = dataName.substr(0, dataName.length - 2) + // Get a reference to the data function + var dataFunction = scope.$parent.$eval(dataName); + if (typeof dataFunction === "function") { + scope.dataFunction = dataFunction; + if (scope.settings.loadOnCreate) { + // Load initial data (args are callback function, search text and item offset) + scope.dataFunction(scope.dataCallback, "", 0); } - else { - throw "Invalid data function: " + dataName; + else if (scope.model && scope.model[scope.textField]) { + scope.confirmedItem = scope.selectedItem = scope.getItemFromDataItem(scope.model, 0); + if (scope.confirmedItem) { + scope.comboText = scope.confirmedItem.text; + } } } else { - // Get the data from the parent $scope - var dataItems = $scope.$parent.$eval(dataName); - loadStaticData(dataItems); - // Watch for any change to the data - $scope.$parent.$watch(dataName, function(newVal, oldVal) { - if (newVal !== oldVal && angular.isArray(newVal)) { - loadStaticData(newVal) - } - }); + throw "Invalid data function: " + dataName; } } - - // Save initial selection, if any - $scope.setInitialSelection(); - }; - - function loadStaticData(dataItems) { - // Create dropdown items - $scope.loadItems(dataItems, $scope.model); - // Save selected item - $scope.confirmedItem = angular.copy($scope.selectedItem); - $scope.allDataLoaded = $scope.items.length > 0; - } - - // If the ac-refresh attribute is set, watch it. If its value gets set to true, re-initialise. - if ($scope.acRefresh !== undefined) { - $scope.$watch("acRefresh", function(newValue, oldValue) { - if (newValue === true) { - $scope.initialise(); - } - }); - } - - // Handle ac-focus-when attribute. When set to true - // give focus to either the combo or search text box - if ($scope.acFocusWhen !== undefined) { - $scope.$watch("acFocusWhen", function(newValue, oldValue) { - if (newValue === true) { - // Set flag to fire the ac-focus directive - if ($scope.settings.comboMode) { - $scope.comboFocus = true; - } - else { - $scope.searchBoxFocus = true; - } - $scope.acFocusWhen = false; - } - }); + else { + // Get the data from the parent scope + var dataItems = scope.$parent.$eval(dataName); + // Create dropdown items + scope.loadItems(dataItems, scope.model); + // Save selected item + scope.confirmedItem = angular.copy(scope.selectedItem); + scope.allDataLoaded = true; + } } + }, - $scope.setInitialSelection = function() { - if ($scope.model) { - $scope.initialSelection = angular.copy($scope.model); - $scope.initialItem = $scope.getItemFromDataItem($scope.model, 0); - $scope.confirmedItem = $scope.selectedItem = $scope.initialItem; - $scope.comboText = $scope.confirmedItem ? $scope.confirmedItem.text : ""; - } - }; + // ************************************************************** + // CONTROLLER + // ************************************************************** + controller: [ + "$scope", "$element", "$attrs", "$window", "$rootScope", "$timeout", "$filter", "navKey", "safeApply", + function ($scope, $element, $attrs, $window, $rootScope, $timeout, $filter, navKey, safeApply) { // Create dropdown items based on the source data items - $scope.loadItems = function(dataItems, selectedDataItem) { + $scope.loadItems = function (dataItems, selectedDataItem) { var itemCount, itemIndex, item, key = $scope.keyField; - if (angular.isArray(dataItems)) { - - var foundSelected = false; itemCount = $scope.items.length; - - angular.forEach(dataItems, function(dataItem, index) { + angular.forEach(dataItems, function (dataItem, index) { itemIndex = itemCount + index; item = $scope.getItemFromDataItem(dataItem, itemIndex); - if (item) { - $scope.items.push(item); - // If not currently filtering - if (!$scope.searchText) { - // Look for a matching item - if (dataItem === selectedDataItem || (key && selectedDataItem && dataItem[key] == selectedDataItem[key])) { - confirmSelection(item); - foundSelected = true; - } - } - else if ($scope.searchText.toLowerCase() === item.text.toLowerCase()) { - // Search text matches item - confirmSelection(item); - } - - if (item.text.length > $scope.longestText.length) { - if ($scope.maxCharacters && item.text.length > $scope.maxCharacters) { - $scope.longestText = item.text.substr(0, $scope.maxCharacters); - } - else { - $scope.longestText = item.text; - } - } + $scope.items.push(item); + if (dataItem === selectedDataItem || (key && key != "" && dataItem[key] == selectedDataItem[key])) { + $scope.selectedItem = item; + $scope.confirmedItem = angular.copy($scope.selectedItem); + $scope.model = $scope.selectedItem.value; + $scope.comboText = $scope.selectedItem.text; } - }); - - // If not currently filtering and there's no selected item, but we have an initial selection - if (!$scope.searchText && $scope.initialSelection && !foundSelected) { - // Create a new item - item = $scope.getItemFromDataItem($scope.initialSelection, 0); - if (item) { - // Add it to the start of the items array - $scope.items.unshift(item); - // Update indexes - angular.forEach($scope.items, function(item, index) { - item.index = index; - }); - - confirmSelection(item); + if (item.text.length > $scope.longestText.length) { + $scope.longestText = item.text; } - } - - // If data is not filtered - if (!$scope.searchText) { - angular.copy($scope.items, $scope.allItems); - } - + }); + angular.copy($scope.items, $scope.allItems); $scope.setListHeight(); - - checkItemCount($scope.items); } }; - function processSettings() { - if ($scope.acSettings) { - var settings = $scope.$eval($scope.acSettings); - if (typeof settings === "object") { - // Merge settings with default values - angular.extend($scope.settings, settings); - } - } - $scope.longestText = $scope.settings.placeholderText; - - $scope.maxTextWidth = ""; - - // If maxWidth is set, limit textbox size, allowing room for dropdown icon - if ($scope.settings.maxWidth) { - var maxWidth = parseInt($scope.settings.maxWidth); - // Set an approximate limit to the number of characters to allow in $scope.longestText - $scope.maxCharacters = Math.round(maxWidth / 6); - $scope.maxTextWidth = (maxWidth - 100) + "px"; - } - } - - function checkItemCount() { - $scope.noItemsFound = $scope.items.length === 0; - $scope.noItemsAdditional = ""; - // If no item is found clear the selected item - if ($scope.noItemsFound) { - $scope.selectedItem = null; - if ($scope.settings.comboMode && $scope.comboText && $scope.settings.allowCustomText) { - $scope.noItemsAdditional = "Press Enter to Add."; - } - } - } - $scope.getItemFromDataItem = function(dataItem, itemIndex) { var item = null; - if (dataItem !== null) { - if ($scope.textField === null || $scope.textField === undefined && typeof dataItem === 'string') { + if (dataItem !== null){ + if ($scope.textField === null) { item = { "text": dataItem, "value": dataItem, "index": itemIndex }; } else if (dataItem[$scope.textField]) { @@ -259,7 +147,7 @@ angular.module("acute.select", []) }; // Set height of list according to number of visible items - $scope.setListHeight = function() { + $scope.setListHeight = function () { var itemCount = $scope.items.length; if (itemCount > $scope.settings.itemsInView) { itemCount = $scope.settings.itemsInView; @@ -268,44 +156,20 @@ angular.module("acute.select", []) $scope.listHeight = $scope.settings.itemHeight * itemCount; }; - $scope.$watch("model", function(newValue, oldValue) { - if ($scope.modelUpdating) { - $scope.modelUpdating = false; - } - else if (!newValue && !oldValue) { - // Do nothing - } - else if (newValue && !oldValue) { - // Model no longer null - $scope.setInitialSelection(); - } - else if (oldValue && !newValue) { - // Model cleared - $scope.setInitialSelection(); - } - else { - // Check that the text is different - if (!$scope.textField || newValue[$scope.textField] !== oldValue[$scope.textField]) { - // Model has been changed in the parent scope - $scope.setInitialSelection(); - } - } - }); - if ($scope.selectedItem) { $scope.comboText = $scope.selectedItem.text; } // Close all instances when user clicks elsewhere - $window.onclick = function(event) { - closeWhenClickingElsewhere(event, function() { + $window.onclick = function (event) { + closeWhenClickingElsewhere(event, function () { $scope.sentBroadcast = false; $rootScope.$broadcast("ac-select-close-all"); }); }; // Keyboard events - $scope.keyHandler = function(event) { + $scope.keyHandler = function (event) { if (!$scope.settings.showSearchBox) { handleCharCodes(event); @@ -313,10 +177,6 @@ angular.module("acute.select", []) var keyCode = event.which || event.keyCode; - if (keyCode === navKey.downArrow) { - $scope.popupVisible = true; - } - if ($scope.popupVisible || keyCode === navKey.del) { var stopPropagation = true; switch (keyCode) { @@ -370,12 +230,7 @@ angular.module("acute.select", []) } // Callback function to receive async data - $scope.dataCallback = function(data, searchText, offset) { - - // Quit if search text has changed since the data function was called - if (searchText !== undefined && searchText !== $scope.searchText) { - return; - } + $scope.dataCallback = function (data, matchingItemTotal) { var selectedDataItem = null; @@ -386,28 +241,21 @@ angular.module("acute.select", []) selectedDataItem = $scope.selectedItem.value; } else { + //selectedDataItem = $scope.getModelObject(); selectedDataItem = $scope.model; } - // Clear all existing items, unless offset > 0, i.e. we're paging and getting additional items - if (!offset || offset == 0) { - $scope.items = []; - } - $scope.loadItems(data, selectedDataItem); - // If not in paging mode - if (!$scope.settings.pageSize) { - // All data is now loaded - $scope.allDataLoaded = true; - // Clear loadOnOpen flag to avoid re-loading when dropdown is next opened - $scope.settings.loadOnOpen = false; - } - else { - - // All data is loaded if fewer than [pageSize] items were returned - $scope.allDataLoaded = data.length < $scope.settings.pageSize; + // If data function takes only one argument, all data is now loaded + $scope.allDataLoaded = $scope.dataFunction.length === 1; + // Clear loadOnOpen flag to avoid re-loading when dropdown next opened + $scope.settings.loadOnOpen = false; + // If a matchingItemTotal value is returned, we are in paging mode + if (matchingItemTotal) { + $scope.paging = true; + $scope.matchingItemTotal = matchingItemTotal; // If user was scrolling down if ($scope.requestedItemIndex) { // Select first of the newly loaded items (if present) @@ -417,49 +265,38 @@ angular.module("acute.select", []) } $scope.requestedItemIndex = null; } + // Show loading message if not all items yet loaded + $scope.showLoadingMessage = $scope.items.length < matchingItemTotal; } - - if ($scope.allDataLoaded) { - $scope.previousSearchText = $scope.searchText; - } - $scope.loading = false; - $scope.loadMessage = "Load more..."; - }; - $scope.findData = function() { + $scope.findData = function () { filterData($scope.searchText); }; - $scope.comboTextChange = function() { + $scope.comboTextChange = function () { $scope.popupVisible = true; $scope.ensureDataLoaded(); $scope.searchText = $scope.comboText; - if ($scope.comboText == '' && $scope.settings.allowClear) { - clearSelection(); - } - filterData($scope.comboText); }; // Show/hide popup - $scope.togglePopup = function() { + $scope.togglePopup = function () { $scope.popupVisible = !$scope.popupVisible; if ($scope.popupVisible) { - // Pop-up opening if ($scope.settings.comboMode) { - $timeout(function() { $scope.comboFocus = true; }); + $timeout(function () { $scope.comboFocus = true; }); } else { - $timeout(function() { $scope.searchBoxFocus = true; }); + $timeout(function () { $scope.searchBoxFocus = true; }); } $scope.ensureDataLoaded(); - clearClientFilter(); } }; - $scope.ensureDataLoaded = function() { + $scope.ensureDataLoaded = function () { if (!$scope.allDataLoaded && $scope.dataFunction && $scope.settings.loadOnOpen) { // Load initial data (args are callback function, search text and item offset) $scope.dataFunction($scope.dataCallback, "", 0); @@ -467,17 +304,17 @@ angular.module("acute.select", []) }; // When clicking on the ac-select-main div - $scope.mainClick = function() { + $scope.mainClick = function () { // Close any other ac-select instances $scope.sentBroadcast = true; $rootScope.$broadcast("ac-select-close-all"); }; - $scope.$on("ac-select-close-all", function() { - if (!$scope.sentBroadcast && $scope.popupVisible) { + $scope.$on("ac-select-close-all", function () { + if (!$scope.sentBroadcast) { $scope.popupVisible = false; safeApply($scope); - // If clear is not allowed and we're in combo mode + // If clear is not allowed and we"re in combo mode if (!$scope.settings.allowClear && $scope.settings.comboMode && $scope.selectedItem) { // Update the combo text to reflect the currently selected item $scope.comboText = $scope.confirmedItem.text; @@ -488,11 +325,12 @@ angular.module("acute.select", []) } }); - $scope.itemClick = function(i) { - confirmSelection($scope.items[i]); + $scope.itemClick = function (i) { + $scope.selectedItem = $scope.items[i]; + selectionConfirmed(); }; - $scope.getItemClass = function(i) { + $scope.getItemClass = function (i) { if ($scope.selectedItem && $scope.items[i].value === $scope.selectedItem.value) { return "ac-select-highlight"; } @@ -501,15 +339,15 @@ angular.module("acute.select", []) } }; - $scope.addButtonClick = function() { + $scope.addButtonClick = function () { if (customAddRequest()) { - confirmSelection(null); + selectionConfirmed(); } }; - $scope.listScrolled = function(scrollPosition) { + $scope.listScrolled = function (scrollPosition) { $scope.scrollPosition = scrollPosition; - if ($scope.settings.pageSize) { + if ($scope.paging) { var totalHeight = $scope.items.length * $scope.settings.itemHeight; // If scrolled past the last item if (scrollPosition > totalHeight - $scope.listHeight) { @@ -519,11 +357,9 @@ angular.module("acute.select", []) }; // Load further data when paging is enabled - $scope.loadMore = function() { - if (!$scope.loading) { + $scope.loadMore = function () { + if (!$scope.loading && $scope.showLoadingMessage) { $scope.loading = true; - $scope.loadMessage = "Loading..."; - var offSet = $scope.items.length; $scope.dataFunction($scope.dataCallback, $scope.searchText, offSet); } @@ -531,18 +367,13 @@ angular.module("acute.select", []) // Private functions - function confirmSelection(item, forceClose) { - - $scope.selectedItem = item; - - var oldConfirmedItem = $scope.confirmedItem; + function selectionConfirmed(forceClose) { var close = false; if ($scope.selectedItem) { $scope.confirmedItem = angular.copy($scope.selectedItem); - $scope.modelUpdating = true; $scope.model = $scope.selectedItem.value; $scope.comboText = $scope.selectedItem.text; - close = true; + close = true } else { // Try adding as a custom item @@ -550,17 +381,7 @@ angular.module("acute.select", []) close = true; } } - - // If the pop-up is visible (i.e. not setting an initial selection) - if ($scope.popupVisible) { - fireChangeEvent(); - } - - // Clear any initial selection - $scope.initialSelection = null; - $scope.initialItem == null; - - if ($scope.popupVisible && (close || forceClose)) { + if (close || forceClose) { $scope.popupVisible = false; $scope.wrapperFocus = true; // If all data is loaded @@ -570,11 +391,9 @@ angular.module("acute.select", []) clearClientFilter(); } } - } - function fireChangeEvent() { // Fire acChange function, if specified - if (typeof $scope.acChange === 'function') { + if (typeof $scope.acChange === "function") { $scope.acChange({ value: $scope.selectedItem ? $scope.selectedItem.value : null }); } } @@ -588,12 +407,6 @@ angular.module("acute.select", []) // Create new data item dataItem = {}; dataItem[$scope.textField] = customText; - - // add the key field if it is defined. - if ($scope.keyField) { - dataItem[$scope.keyField] = customText; - } - $scope.modelUpdating = true; $scope.model = dataItem; $scope.confirmedItem = $scope.selectedItem = { "text": customText, "value": dataItem, "index": -1 }; $scope.items.push($scope.selectedItem); @@ -605,7 +418,7 @@ angular.module("acute.select", []) } function enterKey() { - confirmSelection($scope.selectedItem); + selectionConfirmed(); } function downArrowKey() { @@ -619,7 +432,7 @@ angular.module("acute.select", []) ensureItemVisible($scope.selectedItem); selected = true; } - else if ($scope.settings.pageSize && $scope.items.length >= $scope.settings.pageSize) { + else if ($scope.paging) { $scope.requestedItemIndex = newIndex; $scope.scrollTo += $scope.settings.itemHeight; $scope.loadMore(); @@ -709,15 +522,14 @@ angular.module("acute.select", []) function escapeKey() { // Revert to last confirmed selection $scope.selectedItem = $scope.confirmedItem; - confirmSelection($scope.selectedItem, true); + selectionConfirmed(true); } function deleteKey(event) { if ($scope.settings.allowClear) { var srcElement = angular.element(event.target); - // If in combo textbox, ignore - if (srcElement.hasClass('ac-select-text')) { - event.stopPropagation(); + if (srcElement.hasClass("ac-select-text")) { + event.stopPropagation = true; } else { clearSelection(); @@ -726,18 +538,11 @@ angular.module("acute.select", []) } function clearSelection() { - var oldConfirmedItem = $scope.confirmedItem; $scope.selectedItem = null; $scope.confirmedItem = null; - $scope.modelUpdating = true; $scope.model = null; - $scope.initialSelection = null; $scope.scrollTo = 0; $scope.comboText = ""; - - if (oldConfirmedItem !== null) { - fireChangeEvent(); - } } function ensureItemVisible(item) { @@ -752,83 +557,42 @@ angular.module("acute.select", []) } function filterData() { - - var itemCount = $scope.allItems.length; - - // If search text is blank OR paging is enabled && current number of items is >= pageSize (or zero) - if ($scope.searchText === "" || ($scope.settings.pageSize && (itemCount >= $scope.settings.pageSize || itemCount === 0))) { - // Data needs to be re-loaded. - $scope.allDataLoaded = false; - } - + $scope.showLoadingMessage = false; + //$scope.selectedItem = null; if ($scope.allDataLoaded) { - - var itemsToFilter = $scope.allItems; - - // If search text includes the previous search - if ($scope.previousSearchText && $scope.searchText.indexOf($scope.previousSearchText) != -1) { - // We can refine the filtering, without checking all items - itemsToFilter = $scope.items; - } - if ($scope.settings.filterType == "contains") { - $scope.items = $filter("filter")(itemsToFilter, function(item) { - // Check for match at start of items only - return item.text.toLowerCase().indexOf($scope.searchText.toLowerCase()) > -1; - }); + $scope.items = $filter("filter")($scope.allItems, $scope.searchText); } else { - $scope.items = $filter("filter")(itemsToFilter, function(item) { + $scope.items = $filter("filter")($scope.allItems, function (item) { // Check for match at start of items only return item.text.substr(0, $scope.searchText.length).toLowerCase() === $scope.searchText.toLowerCase(); }); } // Update indexes - angular.forEach($scope.items, function(item, index) { + angular.forEach($scope.items, function (item, index) { item.index = index; }); - - checkItemCount(); } else { // Pass search text to data function (if it takes 2 or more arguments) - if ($scope.dataFunction && $scope.dataFunction.length >= 2) { - // If search text has enough chars (or it's just been cleared) - if ($scope.searchText.length >= $scope.settings.minSearchLength || $scope.searchText == "") { - // Get data - $scope.dataFunction($scope.dataCallback, $scope.searchText, 0); - } + $scope.items = []; + if ($scope.dataFunction && $scope.dataFunction.length >= 2 + && $scope.searchText.length >= $scope.settings.minSearchLength) { + $scope.dataFunction($scope.dataCallback, $scope.searchText, 0); } } - // If narrowed down to one item (and search text isn't a subset of the previous search), select it - if ($scope.items.length === 1 && $scope.previousSearchText.indexOf($scope.searchText) === -1) { + $scope.setListHeight(); + + // If narrowed down to one item, select it + if ($scope.items.length === 1) { $scope.matchFound = true; $scope.selectedItem = $scope.items[0]; } else { - // See if the search text exactly matches one of the items - $scope.matchFound = searchTextMatchesItem(); - } - - $scope.previousSearchText = $scope.searchText - $scope.setListHeight(); - } - - // Look for an item with text that exactly matches the search text - function searchTextMatchesItem() { - var i, valid = false; - if ($scope.searchText.length > 0) { - for (i = 0; i < $scope.items.length; i++) { - if ($scope.searchText.toLowerCase() === $scope.items[i].text.toLowerCase()) { - $scope.selectedItem = $scope.items[i]; - $scope.searchText = $scope.comboText = $scope.selectedItem.text; - valid = true; - break; - } - } + $scope.matchFound = false; } - return valid; } // Remove any client filtering of items @@ -848,7 +612,7 @@ angular.module("acute.select", []) // Check up to 10 levels up the DOM tree for (var i = 0; i < 10 && element && !clickedOnPopup; i++) { var elementClasses = element.classList; - if (elementClasses !== undefined && elementClasses.contains('ac-select-wrapper')) { + if (elementClasses.contains("ac-select-wrapper")) { clickedOnPopup = true; } else { @@ -860,115 +624,93 @@ angular.module("acute.select", []) callbackFn(); } } - - function log(text) { - if (console && console.log && $scope.settings.debug) { - console.log("ac-select:- " + text); - } - } } - }; -}) + ]}; +}]) // Directive to set focus to an element when a specified expression is true -.directive('acFocus', function($timeout, $parse, safeApply) { +.directive("acFocus", ["$timeout", "$parse",function ($timeout, $parse) { return { restrict: "A", - link: function(scope, element, attributes) { + link: function (scope, element, attributes) { var setFocus = $parse(attributes.acFocus); - scope.$watch(setFocus, function(value) { + scope.$watch(setFocus, function (value) { if (value === true) { - $timeout(function() { + $timeout(function () { element[0].focus(); }); } }); - // Set the "setFocus" attribute value to 'false' on blur event + // Set the "setFocus" attribute value to "false" on blur event // using the "assign" method on the function that $parse returns - element.bind('blur', function() { - safeApply(scope, function() { setFocus.assign(scope, false) }); + element.bind("blur", function () { + scope.$apply(setFocus.assign(scope, false)); }); } }; -}) - -.directive('acSelectOnFocus', function() { - return { - restrict: 'A', - scope: { - acSelectOnFocus: "=" - }, - link: function(scope, element, attrs) { - element.bind('focus', function() { - if (scope.acSelectOnFocus !== false && scope.acSelectOnFocus !== 'false') { - element[0].select(); - } - }); - } - }; -}) +}]) // Directive for a scroll container. Set the "ac-scroll-to" attribute to an expression and when its value changes, // the div will scroll to that position -.directive('acScrollTo', function() { +.directive("acScrollTo", [function () { return { restrict: "A", scope: false, - controller: function($scope, $element, $attrs) { + controller: ["$scope","$element","$attrs",function ($scope, $element, $attrs) { var expression = $attrs.acScrollTo; - $scope.$watch(expression, function() { + $scope.$watch(expression, function () { var scrollTop = $scope.$eval(expression); angular.element($element)[0].scrollTop = scrollTop; }); } - }; -}) + ]}; +}]) // Call a function when the element is scrolled -// E.g. ac-on-scroll="listScrolled()" +// E.g. ac-on-scroll="listScrolled()" // N.B. take care not to use the result to directly update an acScrollTo expression // as this will result in an infinite recursion! -.directive('acOnScroll', function() { +.directive("acOnScroll", [function () { return { restrict: "A", - link: function(scope, element, attrs) { + link: function (scope, element, attrs) { var callbackName = attrs.acOnScroll; if (callbackName.indexOf("()") === callbackName.length - 2) { callbackName = callbackName.substr(0, callbackName.length - 2); } var callback = scope[callbackName]; if (typeof callback === "function") { - element.bind("scroll", function() { + element.bind("scroll", function () { callback(element[0].scrollTop); }); } } }; -}) +}]) -.factory('navKey', function() { +.factory("navKey", [function () { return { - 'backspace': 8, - 'tab': 9, - 'enter': 13, - 'escape': 27, - 'pageUp': 33, - 'pageDown': 34, - 'end': 35, - 'home': 36, - 'leftArrow': 37, - 'upArrow': 38, - 'rightArrow': 39, - 'downArrow': 40, - 'del': 46 + "backspace": 8, + "tab": 9, + "enter": 13, + "escape": 27, + "pageUp": 33, + "pageDown": 34, + "end": 35, + "home": 36, + "leftArrow": 37, + "upArrow": 38, + "rightArrow": 39, + "downArrow": 40, + "del": 46 }; -}) +}]) // safeApply service, courtesy Alex Vanston and Andrew Reutter -.factory('safeApply', [function($rootScope) { - return function($scope, fn) { +.factory("safeApply", [function () { + return function ($scope, fn) { var phase = $scope.$root.$$phase; - if (phase == '$apply' || phase == '$digest') { + if (phase === "$apply" || phase === "$digest") { if (fn) { $scope.$eval(fn); } @@ -979,35 +721,30 @@ angular.module("acute.select", []) $scope.$apply(); } } - } + }; }]) // Service to allow host pages to change settings for all instances (in their module.run function) -.factory('acuteSelectService', function() { +.factory("acuteSelectService", [function () { var defaultSettings = { "templatePath": "/acute.select/", "noItemsText": "No items found.", - "placeholderText": "Please select...", "itemHeight": 24, "itemsInView": 10, - "pageSize": null, "minWidth": "100px", - "maxWidth": "", "showSearchBox": true, "comboMode": false, - "comboSelectOnFocus": true, "loadOnCreate": true, - "loadOnOpen": false, // If true, while loadOnCreate is false, the load function will be called when the dropdown opens + "loadOnOpen": false, // If true, while loadOnCreate is false, the load function will be called the when dropdown opens "allowCustomText": false, - "minSearchLength": 0, + "minSearchLength": 1, "filterType": "contains", // or "start" - "allowClear": true, - "debug": false + "allowClear": true }; return { - getSettings: function() { + getSettings: function () { // Add trailing "/" to template path if not present var len = defaultSettings.templatePath.length; if (len > 0 && defaultSettings.templatePath.substr(len - 1, 1) !== "/") { @@ -1015,21 +752,10 @@ angular.module("acute.select", []) } return angular.copy(defaultSettings); }, - - updateSetting: function(settingName, value) { - updateSingleSetting(settingName, value); - }, - - updateSettings: function(settings) { - for (name in settings) { - updateSingleSetting(name, settings[name]); + updateSetting: function (settingName, value) { + if (defaultSettings.hasOwnProperty(settingName)) { + defaultSettings[settingName] = value; } } }; - - function updateSingleSetting(settingName, value) { - if (defaultSettings.hasOwnProperty(settingName)) { - defaultSettings[settingName] = value; - } - } -}); \ No newline at end of file +}]);