price-bundle.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @api
  7. */
  8. define([
  9. 'jquery',
  10. 'underscore',
  11. 'mage/template',
  12. 'priceUtils',
  13. 'priceBox'
  14. ], function ($, _, mageTemplate, utils) {
  15. 'use strict';
  16. var globalOptions = {
  17. optionConfig: null,
  18. productBundleSelector: 'input.bundle.option, select.bundle.option, textarea.bundle.option',
  19. qtyFieldSelector: 'input.qty',
  20. priceBoxSelector: '.price-box',
  21. optionHandlers: {},
  22. optionTemplate: '<%- data.label %>' +
  23. '<% if (data.finalPrice.value) { %>' +
  24. ' +<%- data.finalPrice.formatted %>' +
  25. '<% } %>',
  26. controlContainer: 'dd', // should be eliminated
  27. priceFormat: {},
  28. isFixedPrice: false
  29. };
  30. $.widget('mage.priceBundle', {
  31. options: globalOptions,
  32. /**
  33. * @private
  34. */
  35. _init: function initPriceBundle() {
  36. var form = this.element,
  37. options = $(this.options.productBundleSelector, form);
  38. options.trigger('change');
  39. },
  40. /**
  41. * @private
  42. */
  43. _create: function createPriceBundle() {
  44. var form = this.element,
  45. options = $(this.options.productBundleSelector, form),
  46. priceBox = $(this.options.priceBoxSelector, form),
  47. qty = $(this.options.qtyFieldSelector, form);
  48. if (priceBox.data('magePriceBox') &&
  49. priceBox.priceBox('option') &&
  50. priceBox.priceBox('option').priceConfig
  51. ) {
  52. if (priceBox.priceBox('option').priceConfig.optionTemplate) {
  53. this._setOption('optionTemplate', priceBox.priceBox('option').priceConfig.optionTemplate);
  54. }
  55. this._setOption('priceFormat', priceBox.priceBox('option').priceConfig.priceFormat);
  56. priceBox.priceBox('setDefault', this.options.optionConfig.prices);
  57. }
  58. this._applyOptionNodeFix(options);
  59. options.on('change', this._onBundleOptionChanged.bind(this));
  60. qty.on('change', this._onQtyFieldChanged.bind(this));
  61. },
  62. /**
  63. * Handle change on bundle option inputs
  64. * @param {jQuery.Event} event
  65. * @private
  66. */
  67. _onBundleOptionChanged: function onBundleOptionChanged(event) {
  68. var changes,
  69. bundleOption = $(event.target),
  70. priceBox = $(this.options.priceBoxSelector, this.element),
  71. handler = this.options.optionHandlers[bundleOption.data('role')];
  72. bundleOption.data('optionContainer', bundleOption.closest(this.options.controlContainer));
  73. bundleOption.data('qtyField', bundleOption.data('optionContainer').find(this.options.qtyFieldSelector));
  74. if (handler && handler instanceof Function) {
  75. changes = handler(bundleOption, this.options.optionConfig, this);
  76. } else {
  77. changes = defaultGetOptionValue(bundleOption, this.options.optionConfig);//eslint-disable-line
  78. }
  79. if (changes) {
  80. priceBox.trigger('updatePrice', changes);
  81. }
  82. this.updateProductSummary();
  83. },
  84. /**
  85. * Handle change on qty inputs near bundle option
  86. * @param {jQuery.Event} event
  87. * @private
  88. */
  89. _onQtyFieldChanged: function onQtyFieldChanged(event) {
  90. var field = $(event.target),
  91. optionInstance,
  92. optionConfig;
  93. if (field.data('optionId') && field.data('optionValueId')) {
  94. optionInstance = field.data('option');
  95. optionConfig = this.options.optionConfig
  96. .options[field.data('optionId')]
  97. .selections[field.data('optionValueId')];
  98. optionConfig.qty = field.val();
  99. optionInstance.trigger('change');
  100. }
  101. },
  102. /**
  103. * Helper to fix backend behavior:
  104. * - if default qty large than 1 then backend multiply price in config
  105. *
  106. * @deprecated
  107. * @private
  108. */
  109. _applyQtyFix: function applyQtyFix() {
  110. var config = this.options.optionConfig;
  111. if (config.isFixedPrice) {
  112. _.each(config.options, function (option) {
  113. _.each(option.selections, function (item) {
  114. if (item.qty && item.qty !== 1) {
  115. _.each(item.prices, function (price) {
  116. price.amount /= item.qty;
  117. });
  118. }
  119. });
  120. });
  121. }
  122. },
  123. /**
  124. * Helper to fix issue with option nodes:
  125. * - you can't place any html in option ->
  126. * so you can't style it via CSS
  127. * @param {jQuery} options
  128. * @private
  129. */
  130. _applyOptionNodeFix: function applyOptionNodeFix(options) {
  131. var config = this.options,
  132. format = config.priceFormat,
  133. template = config.optionTemplate;
  134. template = mageTemplate(template);
  135. options.filter('select').each(function (index, element) {
  136. var $element = $(element),
  137. optionId = utils.findOptionId($element),
  138. optionConfig = config.optionConfig && config.optionConfig.options[optionId].selections,
  139. value;
  140. $element.find('option').each(function (idx, option) {
  141. var $option,
  142. optionValue,
  143. toTemplate,
  144. prices;
  145. $option = $(option);
  146. optionValue = $option.val();
  147. if (!optionValue && optionValue !== 0) {
  148. return;
  149. }
  150. toTemplate = {
  151. data: {
  152. label: optionConfig[optionValue] && optionConfig[optionValue].name
  153. }
  154. };
  155. prices = optionConfig[optionValue].prices;
  156. _.each(prices, function (price, type) {
  157. value = +price.amount;
  158. value += _.reduce(price.adjustments, function (sum, x) {//eslint-disable-line
  159. return sum + x;
  160. }, 0);
  161. toTemplate.data[type] = {
  162. value: value,
  163. formatted: utils.formatPrice(value, format)
  164. };
  165. });
  166. $option.html(template(toTemplate));
  167. });
  168. });
  169. },
  170. /**
  171. * Custom behavior on getting options:
  172. * now widget able to deep merge accepted configuration with instance options.
  173. * @param {Object} options
  174. * @return {$.Widget}
  175. */
  176. _setOptions: function setOptions(options) {
  177. $.extend(true, this.options, options);
  178. this._super(options);
  179. return this;
  180. },
  181. /**
  182. * Handler to update productSummary box
  183. */
  184. updateProductSummary: function updateProductSummary() {
  185. this.element.trigger('updateProductSummary', {
  186. config: this.options.optionConfig
  187. });
  188. }
  189. });
  190. return $.mage.priceBundle;
  191. /**
  192. * Converts option value to priceBox object
  193. *
  194. * @param {jQuery} element
  195. * @param {Object} config
  196. * @returns {Object|null} - priceBox object with additional prices
  197. */
  198. function defaultGetOptionValue(element, config) {
  199. var changes = {},
  200. optionHash,
  201. tempChanges,
  202. qtyField,
  203. optionId = utils.findOptionId(element[0]),
  204. optionValue = element.val() || null,
  205. optionName = element.prop('name'),
  206. optionType = element.prop('type'),
  207. optionConfig = config.options[optionId].selections,
  208. optionQty = 0,
  209. canQtyCustomize = false,
  210. selectedIds = config.selected;
  211. switch (optionType) {
  212. case 'radio':
  213. case 'select-one':
  214. if (optionType === 'radio' && !element.is(':checked')) {
  215. return null;
  216. }
  217. qtyField = element.data('qtyField');
  218. qtyField.data('option', element);
  219. if (optionValue) {
  220. optionQty = optionConfig[optionValue].qty || 0;
  221. canQtyCustomize = optionConfig[optionValue].customQty === '1';
  222. toggleQtyField(qtyField, optionQty, optionId, optionValue, canQtyCustomize);//eslint-disable-line
  223. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  224. tempChanges = applyTierPrice(//eslint-disable-line
  225. tempChanges,
  226. optionQty,
  227. optionConfig[optionValue]
  228. );
  229. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  230. } else {
  231. tempChanges = {};
  232. toggleQtyField(qtyField, '0', optionId, optionValue, false);//eslint-disable-line
  233. }
  234. optionHash = 'bundle-option-' + optionName;
  235. changes[optionHash] = tempChanges;
  236. selectedIds[optionId] = [optionValue];
  237. break;
  238. case 'select-multiple':
  239. optionValue = _.compact(optionValue);
  240. _.each(optionConfig, function (row, optionValueCode) {
  241. optionHash = 'bundle-option-' + optionName + '##' + optionValueCode;
  242. optionQty = row.qty || 0;
  243. tempChanges = utils.deepClone(row.prices);
  244. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  245. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  246. changes[optionHash] = _.contains(optionValue, optionValueCode) ? tempChanges : {};
  247. });
  248. selectedIds[optionId] = optionValue || [];
  249. break;
  250. case 'checkbox':
  251. optionHash = 'bundle-option-' + optionName + '##' + optionValue;
  252. optionQty = optionConfig[optionValue].qty || 0;
  253. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  254. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  255. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  256. changes[optionHash] = element.is(':checked') ? tempChanges : {};
  257. selectedIds[optionId] = selectedIds[optionId] || [];
  258. if (!_.contains(selectedIds[optionId], optionValue) && element.is(':checked')) {
  259. selectedIds[optionId].push(optionValue);
  260. } else if (!element.is(':checked')) {
  261. selectedIds[optionId] = _.without(selectedIds[optionId], optionValue);
  262. }
  263. break;
  264. case 'hidden':
  265. optionHash = 'bundle-option-' + optionName + '##' + optionValue;
  266. optionQty = optionConfig[optionValue].qty || 0;
  267. canQtyCustomize = optionConfig[optionValue].customQty === '1';
  268. qtyField = element.data('qtyField');
  269. qtyField.data('option', element);
  270. toggleQtyField(qtyField, optionQty, optionId, optionValue, canQtyCustomize);//eslint-disable-line
  271. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  272. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  273. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  274. optionHash = 'bundle-option-' + optionName;
  275. changes[optionHash] = tempChanges;
  276. selectedIds[optionId] = [optionValue];
  277. break;
  278. }
  279. return changes;
  280. }
  281. /**
  282. * Helper to toggle qty field
  283. * @param {jQuery} element
  284. * @param {String|Number} value
  285. * @param {String|Number} optionId
  286. * @param {String|Number} optionValueId
  287. * @param {Boolean} canEdit
  288. */
  289. function toggleQtyField(element, value, optionId, optionValueId, canEdit) {
  290. element
  291. .val(value)
  292. .data('optionId', optionId)
  293. .data('optionValueId', optionValueId)
  294. .attr('disabled', !canEdit);
  295. if (canEdit) {
  296. element.removeClass('qty-disabled');
  297. } else {
  298. element.addClass('qty-disabled');
  299. }
  300. }
  301. /**
  302. * Helper to multiply on qty
  303. *
  304. * @param {Object} prices
  305. * @param {Number} qty
  306. * @returns {Object}
  307. */
  308. function applyQty(prices, qty) {
  309. _.each(prices, function (everyPrice) {
  310. everyPrice.amount *= qty;
  311. _.each(everyPrice.adjustments, function (el, index) {
  312. everyPrice.adjustments[index] *= qty;
  313. });
  314. });
  315. return prices;
  316. }
  317. /**
  318. * Helper to limit price with tier price
  319. *
  320. * @param {Object} oneItemPrice
  321. * @param {Number} qty
  322. * @param {Object} optionConfig
  323. * @returns {Object}
  324. */
  325. function applyTierPrice(oneItemPrice, qty, optionConfig) {
  326. var tiers = optionConfig.tierPrice,
  327. magicKey = _.keys(oneItemPrice)[0],
  328. lowest = false;
  329. _.each(tiers, function (tier, index) {
  330. if (tier['price_qty'] > qty) {
  331. return;
  332. }
  333. if (tier.prices[magicKey].amount < oneItemPrice[magicKey].amount) {
  334. lowest = index;
  335. }
  336. });
  337. if (lowest !== false) {
  338. oneItemPrice = utils.deepClone(tiers[lowest].prices);
  339. }
  340. return oneItemPrice;
  341. }
  342. });