diff --git a/.gitignore b/.gitignore index 2fba55ec..a994a257 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,11 @@ /vendor/ /node_modules/ /composer.lock +/phpspec.yml /etc/build/* !/etc/build/.gitkeep +!/etc/build/.gitignore /tests/Application/yarn.lock diff --git a/composer.json b/composer.json index 9cfc32b5..a076729a 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,9 @@ "sylius-labs/coding-standard": "^3.0", "symfony/debug-bundle": "^4.4 || ^5.4", "symfony/dotenv": "^4.4 || ^5.4", - "symfony/web-profiler-bundle": "^4.4 || ^5.4" + "symfony/web-profiler-bundle": "^4.4 || ^5.4", + "symfony/webpack-encore-bundle": "^1.12", + "symfony/web-server-bundle": "^4.4 || ^5.4" }, "conflict": { "doctrine/dbal": "^3.0" diff --git a/features/creating_order/creating_order_with_address_from_address_book.feature b/features/creating_order/creating_order_with_address_from_address_book.feature new file mode 100644 index 00000000..eb3596d8 --- /dev/null +++ b/features/creating_order/creating_order_with_address_from_address_book.feature @@ -0,0 +1,48 @@ +@admin_order_creation_managing_orders +Feature: Creating order with different billing address + In order to place an order in the name of a Customer + As an Administrator + I want to be able to create an order with different shipping and billing addresses in Admin panel + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Stark Coat" priced at "$100" + And the store has a product "Lannister Banner" priced at "$40" + And the store ships everywhere for free + And the store allows paying with "Cash on Delivery" + And there is a customer account "jon.snow@the-wall.com" + And their default address is "Ankh-Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And this customer has an address "Ned Stark", "Banana Street", "90232", "New York", "United States" in their address book + And I am logged in as an administrator + + @ui @javascript + Scenario: Creating an order with both addresses from address books + When I create a new order for "jon.snow@the-wall.com" and channel "United States" + And I add "Stark Coat" to this order + And I see the address book shipping address select + And I see the address book billing address select + And I select first address in shipping address book with name "Jon Snow" + And I select first address in billing address book with name "Ned Stark" + And I select "Free" shipping method + And I select "Cash on Delivery" payment method + And I place and confirm this order + Then I should be notified that order has been successfully created + And this order shipping address should be "Jon Snow", "Frost Alley", "90210", "Ankh-Morpork", "United States" + And this order billing address should be "Ned Stark", "Banana Street", "90232", "New York", "United States" + And there should be one not paid nor shipped order with channel "United States" for "jon.snow@the-wall.com" in the registry + + @ui @javascript + Scenario: Creating an order with one address from address books + When I create a new order for "jon.snow@the-wall.com" and channel "United States" + And I add "Stark Coat" to this order + And I see the address book shipping address select + And I see the address book billing address select + And I specify this order shipping address as "Ankh-Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I select first address in billing address book with name "Ned Stark" + And I select "Free" shipping method + And I select "Cash on Delivery" payment method + And I place and confirm this order + Then I should be notified that order has been successfully created + And this order billing address should be "Ned Stark", "Banana Street", "90232", "New York", "United States" + And this order shipping address should be "Jon Snow", "Frost Alley", "90210", "Ankh-Morpork", "United States" + And there should be one not paid nor shipped order with channel "United States" for "jon.snow@the-wall.com" in the registry diff --git a/features/creating_order/creating_order_with_different_billing_address.feature b/features/creating_order/creating_order_with_different_billing_address.feature index 3954232e..870d8a4e 100644 --- a/features/creating_order/creating_order_with_different_billing_address.feature +++ b/features/creating_order/creating_order_with_different_billing_address.feature @@ -17,6 +17,8 @@ Feature: Creating order with different billing address Scenario: Creating an order with both addresses specified When I create a new order for "jon.snow@the-wall.com" and channel "United States" And I add "Stark Coat" to this order + And I should not see the address book shipping address select + And I should not see the address book billing address select And I specify this order shipping address as "Ankh-Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" And I specify this order billing address as "Rivendell", "Elm Street", "444", "United States" for "Ned Stark" And I select "Free" shipping method diff --git a/package.json b/package.json new file mode 100644 index 00000000..2e0d8f4a --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "@sylius/admin-order-creation-plugin", + "description": "Admin order creation plugin for Sylius.", + "repository": "https://github.com/Sylius/AdminOrderCreationPlugin.git", + "license": "MIT", + "scripts": { + "dist": "yarn encore production --config-name sylius-plugin-dist" + } +} diff --git a/public/build/sylius/orderCreation/admin/entrypoints.json b/public/build/sylius/orderCreation/admin/entrypoints.json new file mode 100644 index 00000000..9290b879 --- /dev/null +++ b/public/build/sylius/orderCreation/admin/entrypoints.json @@ -0,0 +1,12 @@ +{ + "entrypoints": { + "sylius-orderCreation-admin": { + "css": [ + "/build/sylius/orderCreation/admin/sylius-orderCreation-admin.css" + ], + "js": [ + "/build/sylius/orderCreation/admin/sylius-orderCreation-admin.js" + ] + } + } +} \ No newline at end of file diff --git a/public/build/sylius/orderCreation/admin/manifest.json b/public/build/sylius/orderCreation/admin/manifest.json new file mode 100644 index 00000000..54f14707 --- /dev/null +++ b/public/build/sylius/orderCreation/admin/manifest.json @@ -0,0 +1,4 @@ +{ + "build/sylius/orderCreation/admin/sylius-orderCreation-admin.css": "/build/sylius/orderCreation/admin/sylius-orderCreation-admin.css", + "build/sylius/orderCreation/admin/sylius-orderCreation-admin.js": "/build/sylius/orderCreation/admin/sylius-orderCreation-admin.js" +} \ No newline at end of file diff --git a/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.css b/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.css new file mode 100644 index 00000000..e0b5afdb --- /dev/null +++ b/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.css @@ -0,0 +1,10 @@ +.custom-address-select { + margin-bottom: 16px; +} + +.custom-address-select .dropdown, +.custom-address-select .ui.search.dropdown > input.search { + cursor: pointer; +} + +/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lsaXVzLW9yZGVyQ3JlYXRpb24tYWRtaW4uY3NzIiwibWFwcGluZ3MiOiJBQUFBO0VBQ0U7QUNDRjs7QURDQTs7RUFFRTtBQ0VGLEMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9Ac3lsaXVzL2FkbWluLW9yZGVyLWNyZWF0aW9uLXBsdWdpbi8uL3BhZ2VzL29yZGVyLWNyZWF0aW9uL19hZGRyZXNzU2VsZWN0LnNjc3MiLCJ3ZWJwYWNrOi8vQHN5bGl1cy9hZG1pbi1vcmRlci1jcmVhdGlvbi1wbHVnaW4vLi9tYWluLnNjc3MiXSwic291cmNlc0NvbnRlbnQiOlsiLmN1c3RvbS1hZGRyZXNzLXNlbGVjdCB7XG4gIG1hcmdpbi1ib3R0b206IDE2cHg7XG59XG4uY3VzdG9tLWFkZHJlc3Mtc2VsZWN0IC5kcm9wZG93bixcbi5jdXN0b20tYWRkcmVzcy1zZWxlY3QgLnVpLnNlYXJjaC5kcm9wZG93biA+IGlucHV0LnNlYXJjaCB7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn1cbiIsIi5jdXN0b20tYWRkcmVzcy1zZWxlY3Qge1xuICBtYXJnaW4tYm90dG9tOiAxNnB4O1xufVxuXG4uY3VzdG9tLWFkZHJlc3Mtc2VsZWN0IC5kcm9wZG93bixcbi5jdXN0b20tYWRkcmVzcy1zZWxlY3QgLnVpLnNlYXJjaC5kcm9wZG93biA+IGlucHV0LnNlYXJjaCB7XG4gIGN1cnNvcjogcG9pbnRlcjtcbn0iXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=*/ \ No newline at end of file diff --git a/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.js b/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.js new file mode 100644 index 00000000..7df27159 --- /dev/null +++ b/public/build/sylius/orderCreation/admin/sylius-orderCreation-admin.js @@ -0,0 +1,5284 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "./src/Resources/assets/admin/js/OrderCreateAddressSelect.js": +/*!*******************************************************************!*\ + !*** ./src/Resources/assets/admin/js/OrderCreateAddressSelect.js ***! + \*******************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + +var parseKey = function parseKey(key) { + return key.replace(/(_\w)/g, function (words) { + return words[1].toUpperCase(); + }); +}; + +$.fn.extend({ + addressBook: function addressBook() { + var element = this; + var select = element.parents('.js-address-container').prev().find('> *:first-child'); + + var findByName = function findByName(name) { + return element.find("[name*=\"[".concat(parseKey(name), "]\"]")); + }; + + select.dropdown({ + forceSelection: false, + onChange: function onChange(name, text, choice) { + var _choice$data = choice.data(), + provinceCode = _choice$data.provinceCode, + provinceName = _choice$data.provinceName; + + var provinceContainer = select.parent().find('.province-container').get(0); + element.find('input:not([type="radio"]):not([type="checkbox"]), select').each(function (index, input) { + $(input).val(''); + }); + Object.entries(choice.data()).forEach(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + property = _ref2[0], + value = _ref2[1]; + + var field = findByName(property); + + if (property.indexOf('countryCode') !== -1) { + field.val(value).change(); + var exists = setInterval(function () { + var provinceCodeField = findByName('provinceCode'); + var provinceNameField = findByName('provinceName'); + var provinceIsLoading = $(provinceContainer).attr('data-loading'); + + if (!(typeof provinceIsLoading !== 'undefinded' && provinceIsLoading !== false)) { + if (provinceCodeField.length !== 0 && (provinceCode !== '' || provinceCode != undefined)) { + provinceCodeField.val(provinceCode); + clearInterval(exists); + } else if (provinceNameField.length !== 0 && (provinceName !== '' || provinceName != undefined)) { + provinceNameField.val(provinceName); + clearInterval(exists); + } + } + }, 100); + } else if (field.is('[type="radio"]') || field.is('[type="checkbox"]')) { + field.prop('checked', false); + field.filter("[value=\"".concat(value, "\"]")).prop('checked', true); + } else { + field.val(value); + } + }); + } + }); + } +}); +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (function () { + $('#sylius_admin_order_creation_new_order_shippingAddress').addressBook(); + $('#sylius_admin_order_creation_new_order_billingAddress').addressBook(); +}); + +/***/ }), + +/***/ "./src/Resources/assets/admin/js/index.js": +/*!************************************************!*\ + !*** ./src/Resources/assets/admin/js/index.js ***! + \************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var semantic_ui_css_components_dropdown__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! semantic-ui-css/components/dropdown */ "./tests/Application/node_modules/semantic-ui-css/components/dropdown.js"); +/* harmony import */ var semantic_ui_css_components_dropdown__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(semantic_ui_css_components_dropdown__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var semantic_ui_css_components_transition__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! semantic-ui-css/components/transition */ "./tests/Application/node_modules/semantic-ui-css/components/transition.js"); +/* harmony import */ var semantic_ui_css_components_transition__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(semantic_ui_css_components_transition__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _OrderCreateAddressSelect__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./OrderCreateAddressSelect */ "./src/Resources/assets/admin/js/OrderCreateAddressSelect.js"); + + + +$(function () { + (0,_OrderCreateAddressSelect__WEBPACK_IMPORTED_MODULE_2__["default"])(); +}); + +/***/ }), + +/***/ "./src/Resources/assets/admin/scss/main.scss": +/*!***************************************************!*\ + !*** ./src/Resources/assets/admin/scss/main.scss ***! + \***************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + +/***/ }), + +/***/ "./tests/Application/node_modules/semantic-ui-css/components/dropdown.js": +/*!*******************************************************************************!*\ + !*** ./tests/Application/node_modules/semantic-ui-css/components/dropdown.js ***! + \*******************************************************************************/ +/***/ (() => { + +/*! + * # Semantic UI 2.4.1 - Dropdown + * http://github.com/semantic-org/semantic-ui/ + * + * + * Released under the MIT license + * http://opensource.org/licenses/MIT + * + */ + +;(function ($, window, document, undefined) { + +'use strict'; + +window = (typeof window != 'undefined' && window.Math == Math) + ? window + : (typeof self != 'undefined' && self.Math == Math) + ? self + : Function('return this')() +; + +$.fn.dropdown = function(parameters) { + var + $allModules = $(this), + $document = $(document), + + moduleSelector = $allModules.selector || '', + + hasTouch = ('ontouchstart' in document.documentElement), + time = new Date().getTime(), + performance = [], + + query = arguments[0], + methodInvoked = (typeof query == 'string'), + queryArguments = [].slice.call(arguments, 1), + returnedValue + ; + + $allModules + .each(function(elementIndex) { + var + settings = ( $.isPlainObject(parameters) ) + ? $.extend(true, {}, $.fn.dropdown.settings, parameters) + : $.extend({}, $.fn.dropdown.settings), + + className = settings.className, + message = settings.message, + fields = settings.fields, + keys = settings.keys, + metadata = settings.metadata, + namespace = settings.namespace, + regExp = settings.regExp, + selector = settings.selector, + error = settings.error, + templates = settings.templates, + + eventNamespace = '.' + namespace, + moduleNamespace = 'module-' + namespace, + + $module = $(this), + $context = $(settings.context), + $text = $module.find(selector.text), + $search = $module.find(selector.search), + $sizer = $module.find(selector.sizer), + $input = $module.find(selector.input), + $icon = $module.find(selector.icon), + + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev(), + + $menu = $module.children(selector.menu), + $item = $menu.find(selector.item), + + activated = false, + itemActivated = false, + internalChange = false, + element = this, + instance = $module.data(moduleNamespace), + + initialLoad, + pageLostFocus, + willRefocus, + elementNamespace, + id, + selectObserver, + menuObserver, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing dropdown', settings); + + if( module.is.alreadySetup() ) { + module.setup.reference(); + } + else { + + module.setup.layout(); + + if(settings.values) { + module.change.values(settings.values); + } + + module.refreshData(); + + module.save.defaults(); + module.restore.selected(); + + module.create.id(); + module.bind.events(); + + module.observeChanges(); + module.instantiate(); + } + + }, + + instantiate: function() { + module.verbose('Storing instance of dropdown', module); + instance = module; + $module + .data(moduleNamespace, module) + ; + }, + + destroy: function() { + module.verbose('Destroying previous dropdown', $module); + module.remove.tabbable(); + $module + .off(eventNamespace) + .removeData(moduleNamespace) + ; + $menu + .off(eventNamespace) + ; + $document + .off(elementNamespace) + ; + module.disconnect.menuObserver(); + module.disconnect.selectObserver(); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + selectObserver = new MutationObserver(module.event.select.mutation); + menuObserver = new MutationObserver(module.event.menu.mutation); + module.debug('Setting up mutation observer', selectObserver, menuObserver); + module.observe.select(); + module.observe.menu(); + } + }, + + disconnect: { + menuObserver: function() { + if(menuObserver) { + menuObserver.disconnect(); + } + }, + selectObserver: function() { + if(selectObserver) { + selectObserver.disconnect(); + } + } + }, + observe: { + select: function() { + if(module.has.input()) { + selectObserver.observe($module[0], { + childList : true, + subtree : true + }); + } + }, + menu: function() { + if(module.has.menu()) { + menuObserver.observe($menu[0], { + childList : true, + subtree : true + }); + } + } + }, + + create: { + id: function() { + id = (Math.random().toString(16) + '000000000').substr(2, 8); + elementNamespace = '.' + id; + module.verbose('Creating unique id for element', id); + }, + userChoice: function(values) { + var + $userChoices, + $userChoice, + isUserValue, + html + ; + values = values || module.get.userValues(); + if(!values) { + return false; + } + values = $.isArray(values) + ? values + : [values] + ; + $.each(values, function(index, value) { + if(module.get.item(value) === false) { + html = settings.templates.addition( module.add.variables(message.addResult, value) ); + $userChoice = $('
') + .html(html) + .attr('data-' + metadata.value, value) + .attr('data-' + metadata.text, value) + .addClass(className.addition) + .addClass(className.item) + ; + if(settings.hideAdditions) { + $userChoice.addClass(className.hidden); + } + $userChoices = ($userChoices === undefined) + ? $userChoice + : $userChoices.add($userChoice) + ; + module.verbose('Creating user choices for value', value, $userChoice); + } + }); + return $userChoices; + }, + userLabels: function(value) { + var + userValues = module.get.userValues() + ; + if(userValues) { + module.debug('Adding user labels', userValues); + $.each(userValues, function(index, value) { + module.verbose('Adding custom user value'); + module.add.label(value, value); + }); + } + }, + menu: function() { + $menu = $('') + .addClass(className.menu) + .appendTo($module) + ; + }, + sizer: function() { + $sizer = $('') + .addClass(className.sizer) + .insertAfter($search) + ; + } + }, + + search: function(query) { + query = (query !== undefined) + ? query + : module.get.query() + ; + module.verbose('Searching for query', query); + if(module.has.minCharacters(query)) { + module.filter(query); + } + else { + module.hide(); + } + }, + + select: { + firstUnfiltered: function() { + module.verbose('Selecting first non-filtered element'); + module.remove.selectedItem(); + $item + .not(selector.unselectable) + .not(selector.addition + selector.hidden) + .eq(0) + .addClass(className.selected) + ; + }, + nextAvailable: function($selected) { + $selected = $selected.eq(0); + var + $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0), + $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0), + hasNext = ($nextAvailable.length > 0) + ; + if(hasNext) { + module.verbose('Moving selection to', $nextAvailable); + $nextAvailable.addClass(className.selected); + } + else { + module.verbose('Moving selection to', $prevAvailable); + $prevAvailable.addClass(className.selected); + } + } + }, + + setup: { + api: function() { + var + apiSettings = { + debug : settings.debug, + urlData : { + value : module.get.value(), + query : module.get.query() + }, + on : false + } + ; + module.verbose('First request, initializing API'); + $module + .api(apiSettings) + ; + }, + layout: function() { + if( $module.is('select') ) { + module.setup.select(); + module.setup.returnedObject(); + } + if( !module.has.menu() ) { + module.create.menu(); + } + if( module.is.search() && !module.has.search() ) { + module.verbose('Adding search input'); + $search = $('') + .addClass(className.search) + .prop('autocomplete', 'off') + .insertBefore($text) + ; + } + if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) { + module.create.sizer(); + } + if(settings.allowTab) { + module.set.tabbable(); + } + }, + select: function() { + var + selectValues = module.get.selectValues() + ; + module.debug('Dropdown initialized on a select', selectValues); + if( $module.is('select') ) { + $input = $module; + } + // see if select is placed correctly already + if($input.parent(selector.dropdown).length > 0) { + module.debug('UI dropdown already exists. Creating dropdown menu only'); + $module = $input.closest(selector.dropdown); + if( !module.has.menu() ) { + module.create.menu(); + } + $menu = $module.children(selector.menu); + module.setup.menu(selectValues); + } + else { + module.debug('Creating entire dropdown from select'); + $module = $('') + .attr('class', $input.attr('class') ) + .addClass(className.selection) + .addClass(className.dropdown) + .html( templates.dropdown(selectValues) ) + .insertBefore($input) + ; + if($input.hasClass(className.multiple) && $input.prop('multiple') === false) { + module.error(error.missingMultiple); + $input.prop('multiple', true); + } + if($input.is('[multiple]')) { + module.set.multiple(); + } + if ($input.prop('disabled')) { + module.debug('Disabling dropdown'); + $module.addClass(className.disabled); + } + $input + .removeAttr('class') + .detach() + .prependTo($module) + ; + } + module.refresh(); + }, + menu: function(values) { + $menu.html( templates.menu(values, fields)); + $item = $menu.find(selector.item); + }, + reference: function() { + module.debug('Dropdown behavior was called on select, replacing with closest dropdown'); + // replace module reference + $module = $module.parent(selector.dropdown); + instance = $module.data(moduleNamespace); + element = $module.get(0); + module.refresh(); + module.setup.returnedObject(); + }, + returnedObject: function() { + var + $firstModules = $allModules.slice(0, elementIndex), + $lastModules = $allModules.slice(elementIndex + 1) + ; + // adjust all modules to use correct reference + $allModules = $firstModules.add($module).add($lastModules); + } + }, + + refresh: function() { + module.refreshSelectors(); + module.refreshData(); + }, + + refreshItems: function() { + $item = $menu.find(selector.item); + }, + + refreshSelectors: function() { + module.verbose('Refreshing selector cache'); + $text = $module.find(selector.text); + $search = $module.find(selector.search); + $input = $module.find(selector.input); + $icon = $module.find(selector.icon); + $combo = ($module.prev().find(selector.text).length > 0) + ? $module.prev().find(selector.text) + : $module.prev() + ; + $menu = $module.children(selector.menu); + $item = $menu.find(selector.item); + }, + + refreshData: function() { + module.verbose('Refreshing cached metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + }, + + clearData: function() { + module.verbose('Clearing metadata'); + $item + .removeData(metadata.text) + .removeData(metadata.value) + ; + $module + .removeData(metadata.defaultText) + .removeData(metadata.defaultValue) + .removeData(metadata.placeholderText) + ; + }, + + toggle: function() { + module.verbose('Toggling menu visibility'); + if( !module.is.active() ) { + module.show(); + } + else { + module.hide(); + } + }, + + show: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if(!module.can.show() && module.is.remote()) { + module.debug('No API results retrieved, searching before show'); + module.queryRemote(module.get.query(), module.show); + } + if( module.can.show() && !module.is.active() ) { + module.debug('Showing dropdown'); + if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) { + module.remove.message(); + } + if(module.is.allFiltered()) { + return true; + } + if(settings.onShow.call(element) !== false) { + module.animate.show(function() { + if( module.can.click() ) { + module.bind.intent(); + } + if(module.has.menuSearch()) { + module.focusSearch(); + } + module.set.visible(); + callback.call(element); + }); + } + } + }, + + hide: function(callback) { + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.active() && !module.is.animatingOutward() ) { + module.debug('Hiding dropdown'); + if(settings.onHide.call(element) !== false) { + module.animate.hide(function() { + module.remove.visible(); + callback.call(element); + }); + } + } + }, + + hideOthers: function() { + module.verbose('Finding other dropdowns to hide'); + $allModules + .not($module) + .has(selector.menu + '.' + className.visible) + .dropdown('hide') + ; + }, + + hideMenu: function() { + module.verbose('Hiding menu instantaneously'); + module.remove.active(); + module.remove.visible(); + $menu.transition('hide'); + }, + + hideSubMenus: function() { + var + $subMenus = $menu.children(selector.item).find(selector.menu) + ; + module.verbose('Hiding sub menus', $subMenus); + $subMenus.transition('hide'); + }, + + bind: { + events: function() { + if(hasTouch) { + module.bind.touchEvents(); + } + module.bind.keyboardEvents(); + module.bind.inputEvents(); + module.bind.mouseEvents(); + }, + touchEvents: function() { + module.debug('Touch device detected binding additional touch events'); + if( module.is.searchSelection() ) { + // do nothing special yet + } + else if( module.is.single() ) { + $module + .on('touchstart' + eventNamespace, module.event.test.toggle) + ; + } + $menu + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + ; + }, + keyboardEvents: function() { + module.verbose('Binding keyboard events'); + $module + .on('keydown' + eventNamespace, module.event.keydown) + ; + if( module.has.search() ) { + $module + .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input) + ; + } + if( module.is.multiple() ) { + $document + .on('keydown' + elementNamespace, module.event.document.keydown) + ; + } + }, + inputEvents: function() { + module.verbose('Binding input change events'); + $module + .on('change' + eventNamespace, selector.input, module.event.change) + ; + }, + mouseEvents: function() { + module.verbose('Binding mouse events'); + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + eventNamespace, selector.remove, module.event.remove.click) + ; + } + if( module.is.searchSelection() ) { + $module + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown) + .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup) + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('focus' + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + .on('click' + eventNamespace, selector.text, module.event.text.focus) + ; + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, module.event.click) + ; + } + } + else { + if(settings.on == 'click') { + $module + .on('click' + eventNamespace, module.event.test.toggle) + ; + } + else if(settings.on == 'hover') { + $module + .on('mouseenter' + eventNamespace, module.delay.show) + .on('mouseleave' + eventNamespace, module.delay.hide) + ; + } + else { + $module + .on(settings.on + eventNamespace, module.toggle) + ; + } + $module + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('mousedown' + eventNamespace, module.event.mousedown) + .on('mouseup' + eventNamespace, module.event.mouseup) + .on('focus' + eventNamespace, module.event.focus) + ; + if(module.has.menuSearch() ) { + $module + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + ; + } + else { + $module + .on('blur' + eventNamespace, module.event.blur) + ; + } + } + $menu + .on('mouseenter' + eventNamespace, selector.item, module.event.item.mouseenter) + .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave) + .on('click' + eventNamespace, selector.item, module.event.item.click) + ; + }, + intent: function() { + module.verbose('Binding hide intent event to document'); + if(hasTouch) { + $document + .on('touchstart' + elementNamespace, module.event.test.touch) + .on('touchmove' + elementNamespace, module.event.test.touch) + ; + } + $document + .on('click' + elementNamespace, module.event.test.hide) + ; + } + }, + + unbind: { + intent: function() { + module.verbose('Removing hide intent event from document'); + if(hasTouch) { + $document + .off('touchstart' + elementNamespace) + .off('touchmove' + elementNamespace) + ; + } + $document + .off('click' + elementNamespace) + ; + } + }, + + filter: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + afterFiltered = function() { + if(module.is.multiple()) { + module.filterActive(); + } + if(query || (!query && module.get.activeItem().length == 0)) { + module.select.firstUnfiltered(); + } + if( module.has.allResultsFiltered() ) { + if( settings.onNoResults.call(element, searchTerm) ) { + if(settings.allowAdditions) { + if(settings.hideAdditions) { + module.verbose('User addition with no menu, setting empty style'); + module.set.empty(); + module.hideMenu(); + } + } + else { + module.verbose('All items filtered, showing message', searchTerm); + module.add.message(message.noResults); + } + } + else { + module.verbose('All items filtered, hiding dropdown', searchTerm); + module.hideMenu(); + } + } + else { + module.remove.empty(); + module.remove.message(); + } + if(settings.allowAdditions) { + module.add.userSuggestion(query); + } + if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { + module.show(); + } + } + ; + if(settings.useLabels && module.has.maxSelections()) { + return; + } + if(settings.apiSettings) { + if( module.can.useAPI() ) { + module.queryRemote(searchTerm, function() { + if(settings.filterRemoteData) { + module.filterItems(searchTerm); + } + afterFiltered(); + }); + } + else { + module.error(error.noAPI); + } + } + else { + module.filterItems(searchTerm); + afterFiltered(); + } + }, + + queryRemote: function(query, callback) { + var + apiSettings = { + errorDuration : false, + cache : 'local', + throttle : settings.throttle, + urlData : { + query: query + }, + onError: function() { + module.add.message(message.serverError); + callback(); + }, + onFailure: function() { + module.add.message(message.serverError); + callback(); + }, + onSuccess : function(response) { + var + values = response[fields.remoteValues], + hasRemoteValues = ($.isArray(values) && values.length > 0) + ; + if(hasRemoteValues) { + module.remove.message(); + module.setup.menu({ + values: response[fields.remoteValues] + }); + } + else { + module.add.message(message.noResults); + } + callback(); + } + } + ; + if( !$module.api('get request') ) { + module.setup.api(); + } + apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings); + $module + .api('setting', apiSettings) + .api('query') + ; + }, + + filterItems: function(query) { + var + searchTerm = (query !== undefined) + ? query + : module.get.query(), + results = null, + escapedTerm = module.escape.string(searchTerm), + beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm') + ; + // avoid loop if we're matching nothing + if( module.has.query() ) { + results = []; + + module.verbose('Searching for matching values', searchTerm); + $item + .each(function(){ + var + $choice = $(this), + text, + value + ; + if(settings.match == 'both' || settings.match == 'text') { + text = String(module.get.choiceText($choice, false)); + if(text.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) { + results.push(this); + return true; + } + } + if(settings.match == 'both' || settings.match == 'value') { + value = String(module.get.choiceValue($choice, text)); + if(value.search(beginsWithRegExp) !== -1) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) { + results.push(this); + return true; + } + else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) { + results.push(this); + return true; + } + } + }) + ; + } + module.debug('Showing only matched items', searchTerm); + module.remove.filteredItem(); + if(results) { + $item + .not(results) + .addClass(className.filtered) + ; + } + }, + + fuzzySearch: function(query, term) { + var + termLength = term.length, + queryLength = query.length + ; + query = query.toLowerCase(); + term = term.toLowerCase(); + if(queryLength > termLength) { + return false; + } + if(queryLength === termLength) { + return (query === term); + } + search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) { + var + queryCharacter = query.charCodeAt(characterIndex) + ; + while(nextCharacterIndex < termLength) { + if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) { + continue search; + } + } + return false; + } + return true; + }, + exactSearch: function (query, term) { + query = query.toLowerCase(); + term = term.toLowerCase(); + if(term.indexOf(query) > -1) { + return true; + } + return false; + }, + filterActive: function() { + if(settings.useLabels) { + $item.filter('.' + className.active) + .addClass(className.filtered) + ; + } + }, + + focusSearch: function(skipHandler) { + if( module.has.search() && !module.is.focusedOnSearch() ) { + if(skipHandler) { + $module.off('focus' + eventNamespace, selector.search); + $search.focus(); + $module.on('focus' + eventNamespace, selector.search, module.event.search.focus); + } + else { + $search.focus(); + } + } + }, + + forceSelection: function() { + var + $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0), + $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0), + $selectedItem = ($currentlySelected.length > 0) + ? $currentlySelected + : $activeItem, + hasSelected = ($selectedItem.length > 0) + ; + if(hasSelected && !module.is.multiple()) { + module.debug('Forcing partial selection to selected item', $selectedItem); + module.event.item.click.call($selectedItem, {}, true); + return; + } + else { + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); + } + else { + module.remove.searchTerm(); + } + } + }, + + change: { + values: function(values) { + if(!settings.allowAdditions) { + module.clear(); + } + module.debug('Creating dropdown with specified values', values); + module.setup.menu({values: values}); + $.each(values, function(index, item) { + if(item.selected == true) { + module.debug('Setting initial selection to', item.value); + module.set.selected(item.value); + return true; + } + }); + } + }, + + event: { + change: function() { + if(!internalChange) { + module.debug('Input changed, updating selection'); + module.set.selected(); + } + }, + focus: function() { + if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) { + module.show(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(!activated && !pageLostFocus) { + module.remove.activeLabel(); + module.hide(); + } + }, + mousedown: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = true; + } + else { + // prevents focus callback from occurring on mousedown + activated = true; + } + }, + mouseup: function() { + if(module.is.searchSelection()) { + // prevent menu hiding on immediate re-focus + willRefocus = false; + } + else { + activated = false; + } + }, + click: function(event) { + var + $target = $(event.target) + ; + // focus search + if($target.is($module)) { + if(!module.is.focusedOnSearch()) { + module.focusSearch(); + } + else { + module.show(); + } + } + }, + search: { + focus: function() { + activated = true; + if(module.is.multiple()) { + module.remove.activeLabel(); + } + if(settings.showOnFocus) { + module.search(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(module.is.searchSelection() && !willRefocus) { + if(!itemActivated && !pageLostFocus) { + if(settings.forceSelection) { + module.forceSelection(); + } + module.hide(); + } + } + willRefocus = false; + } + }, + icon: { + click: function(event) { + if($icon.hasClass(className.clear)) { + module.clear(); + } + else if (module.can.click()) { + module.toggle(); + } + } + }, + text: { + focus: function(event) { + activated = true; + module.focusSearch(); + } + }, + input: function(event) { + if(module.is.multiple() || module.is.searchSelection()) { + module.set.filtered(); + } + clearTimeout(module.timer); + module.timer = setTimeout(module.search, settings.delay.search); + }, + label: { + click: function(event) { + var + $label = $(this), + $labels = $module.find(selector.label), + $activeLabels = $labels.filter('.' + className.active), + $nextActive = $label.nextAll('.' + className.active), + $prevActive = $label.prevAll('.' + className.active), + $range = ($nextActive.length > 0) + ? $label.nextUntil($nextActive).add($activeLabels).add($label) + : $label.prevUntil($prevActive).add($activeLabels).add($label) + ; + if(event.shiftKey) { + $activeLabels.removeClass(className.active); + $range.addClass(className.active); + } + else if(event.ctrlKey) { + $label.toggleClass(className.active); + } + else { + $activeLabels.removeClass(className.active); + $label.addClass(className.active); + } + settings.onLabelSelect.apply(this, $labels.filter('.' + className.active)); + } + }, + remove: { + click: function() { + var + $label = $(this).parent() + ; + if( $label.hasClass(className.active) ) { + // remove all selected labels + module.remove.activeLabels(); + } + else { + // remove this label only + module.remove.activeLabels( $label ); + } + } + }, + test: { + toggle: function(event) { + var + toggleBehavior = (module.is.multiple()) + ? module.show + : module.toggle + ; + if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) { + return; + } + if( module.determine.eventOnElement(event, toggleBehavior) ) { + event.preventDefault(); + } + }, + touch: function(event) { + module.determine.eventOnElement(event, function() { + if(event.type == 'touchstart') { + module.timer = setTimeout(function() { + module.hide(); + }, settings.delay.touch); + } + else if(event.type == 'touchmove') { + clearTimeout(module.timer); + } + }); + event.stopPropagation(); + }, + hide: function(event) { + module.determine.eventInModule(event, module.hide); + } + }, + select: { + mutation: function(mutations) { + module.debug('