Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

484 строки
17 KiB

  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. optionTierPricesBlocksSelector: '#option-tier-prices-{1} [data-role="selection-tier-prices"]',
  30. isOptionsInitialized: false
  31. };
  32. $.widget('mage.priceBundle', {
  33. options: globalOptions,
  34. /**
  35. * @private
  36. */
  37. _init: function initPriceBundle() {
  38. var form = this.element,
  39. options = $(this.options.productBundleSelector, form);
  40. options.trigger('change');
  41. },
  42. /**
  43. * @private
  44. */
  45. _create: function createPriceBundle() {
  46. var form = this.element,
  47. options = $(this.options.productBundleSelector, form),
  48. priceBox = $(this.options.priceBoxSelector, form),
  49. qty = $(this.options.qtyFieldSelector, form);
  50. this._updatePriceBox();
  51. priceBox.on('price-box-initialized', this._updatePriceBox.bind(this));
  52. options.on('change', this._onBundleOptionChanged.bind(this));
  53. qty.on('change', this._onQtyFieldChanged.bind(this));
  54. },
  55. /**
  56. * Update price box config with bundle option prices
  57. * @private
  58. */
  59. _updatePriceBox: function () {
  60. var form = this.element,
  61. options = $(this.options.productBundleSelector, form),
  62. priceBox = $(this.options.priceBoxSelector, form);
  63. if (!this.options.isOptionsInitialized) {
  64. if (priceBox.data('magePriceBox') &&
  65. priceBox.priceBox('option') &&
  66. priceBox.priceBox('option').priceConfig
  67. ) {
  68. if (priceBox.priceBox('option').priceConfig.optionTemplate) { //eslint-disable-line max-depth
  69. this._setOption('optionTemplate', priceBox.priceBox('option').priceConfig.optionTemplate);
  70. }
  71. this._setOption('priceFormat', priceBox.priceBox('option').priceConfig.priceFormat);
  72. priceBox.priceBox('setDefault', this.options.optionConfig.prices);
  73. this.options.isOptionsInitialized = true;
  74. }
  75. this._applyOptionNodeFix(options);
  76. }
  77. return this;
  78. },
  79. /**
  80. * Handle change on bundle option inputs
  81. * @param {jQuery.Event} event
  82. * @private
  83. */
  84. _onBundleOptionChanged: function onBundleOptionChanged(event) {
  85. var changes,
  86. bundleOption = $(event.target),
  87. priceBox = $(this.options.priceBoxSelector, this.element),
  88. handler = this.options.optionHandlers[bundleOption.data('role')];
  89. bundleOption.data('optionContainer', bundleOption.closest(this.options.controlContainer));
  90. bundleOption.data('qtyField', bundleOption.data('optionContainer').find(this.options.qtyFieldSelector));
  91. if (handler && handler instanceof Function) {
  92. changes = handler(bundleOption, this.options.optionConfig, this);
  93. } else {
  94. changes = defaultGetOptionValue(bundleOption, this.options.optionConfig);//eslint-disable-line
  95. }
  96. // eslint-disable-next-line no-use-before-define
  97. if (isValidQty(bundleOption)) {
  98. if (changes) {
  99. priceBox.trigger('updatePrice', changes);
  100. }
  101. this._displayTierPriceBlock(bundleOption);
  102. this.updateProductSummary();
  103. }
  104. },
  105. /**
  106. * Handle change on qty inputs near bundle option
  107. * @param {jQuery.Event} event
  108. * @private
  109. */
  110. _onQtyFieldChanged: function onQtyFieldChanged(event) {
  111. var field = $(event.target),
  112. optionInstance,
  113. optionConfig;
  114. if (field.data('optionId') && field.data('optionValueId')) {
  115. optionInstance = field.data('option');
  116. optionConfig = this.options.optionConfig
  117. .options[field.data('optionId')]
  118. .selections[field.data('optionValueId')];
  119. optionConfig.qty = field.val();
  120. // eslint-disable-next-line no-use-before-define
  121. if (isValidQty(optionInstance)) {
  122. optionInstance.trigger('change');
  123. }
  124. }
  125. },
  126. /**
  127. * Helper to fix backend behavior:
  128. * - if default qty large than 1 then backend multiply price in config
  129. *
  130. * @deprecated
  131. * @private
  132. */
  133. _applyQtyFix: function applyQtyFix() {
  134. var config = this.options.optionConfig;
  135. if (config.isFixedPrice) {
  136. _.each(config.options, function (option) {
  137. _.each(option.selections, function (item) {
  138. if (item.qty && item.qty !== 1) {
  139. _.each(item.prices, function (price) {
  140. price.amount /= item.qty;
  141. });
  142. }
  143. });
  144. });
  145. }
  146. },
  147. /**
  148. * Helper to fix issue with option nodes:
  149. * - you can't place any html in option ->
  150. * so you can't style it via CSS
  151. * @param {jQuery} options
  152. * @private
  153. */
  154. _applyOptionNodeFix: function applyOptionNodeFix(options) {
  155. var config = this.options,
  156. format = config.priceFormat,
  157. template = config.optionTemplate;
  158. template = mageTemplate(template);
  159. options.filter('select').each(function (index, element) {
  160. var $element = $(element),
  161. optionId = utils.findOptionId($element),
  162. optionConfig = config.optionConfig && config.optionConfig.options[optionId].selections,
  163. value;
  164. $element.find('option').each(function (idx, option) {
  165. var $option,
  166. optionValue,
  167. toTemplate,
  168. prices;
  169. $option = $(option);
  170. optionValue = $option.val();
  171. if (!optionValue && optionValue !== 0) {
  172. return;
  173. }
  174. toTemplate = {
  175. data: {
  176. label: optionConfig[optionValue] && optionConfig[optionValue].name
  177. }
  178. };
  179. prices = optionConfig[optionValue].prices;
  180. _.each(prices, function (price, type) {
  181. value = +price.amount;
  182. value += _.reduce(price.adjustments, function (sum, x) {//eslint-disable-line
  183. return sum + x;
  184. }, 0);
  185. toTemplate.data[type] = {
  186. value: value,
  187. formatted: utils.formatPriceLocale(value, format)
  188. };
  189. });
  190. $option.html(template(toTemplate));
  191. });
  192. });
  193. },
  194. /**
  195. * Custom behavior on getting options:
  196. * now widget able to deep merge accepted configuration with instance options.
  197. * @param {Object} options
  198. * @return {$.Widget}
  199. */
  200. _setOptions: function setOptions(options) {
  201. $.extend(true, this.options, options);
  202. this._super(options);
  203. return this;
  204. },
  205. /**
  206. * Show or hide option tier prices block
  207. *
  208. * @param {Object} optionElement
  209. * @private
  210. */
  211. _displayTierPriceBlock: function (optionElement) {
  212. var optionType = optionElement.prop('type'),
  213. optionId,
  214. optionValue,
  215. optionTierPricesElements;
  216. if (optionType === 'select-one') {
  217. optionId = utils.findOptionId(optionElement[0]);
  218. optionValue = optionElement.val() || null;
  219. optionTierPricesElements = $(this.options.optionTierPricesBlocksSelector.replace('{1}', optionId));
  220. _.each(optionTierPricesElements, function (tierPriceElement) {
  221. var selectionId = $(tierPriceElement).data('selection-id') + '';
  222. if (selectionId === optionValue) {
  223. $(tierPriceElement).show();
  224. } else {
  225. $(tierPriceElement).hide();
  226. }
  227. });
  228. }
  229. },
  230. /**
  231. * Handler to update productSummary box
  232. */
  233. updateProductSummary: function updateProductSummary() {
  234. this.element.trigger('updateProductSummary', {
  235. config: this.options.optionConfig
  236. });
  237. }
  238. });
  239. return $.mage.priceBundle;
  240. /**
  241. * Converts option value to priceBox object
  242. *
  243. * @param {jQuery} element
  244. * @param {Object} config
  245. * @returns {Object|null} - priceBox object with additional prices
  246. */
  247. function defaultGetOptionValue(element, config) {
  248. var changes = {},
  249. optionHash,
  250. tempChanges,
  251. qtyField,
  252. optionId = utils.findOptionId(element[0]),
  253. optionValue = element.val() || null,
  254. optionName = element.prop('name'),
  255. optionType = element.prop('type'),
  256. optionConfig = config.options[optionId].selections,
  257. optionQty = 0,
  258. canQtyCustomize = false,
  259. selectedIds = config.selected;
  260. switch (optionType) {
  261. case 'radio':
  262. case 'select-one':
  263. if (optionType === 'radio' && !element.is(':checked')) {
  264. return null;
  265. }
  266. qtyField = element.data('qtyField');
  267. qtyField.data('option', element);
  268. if (optionValue) {
  269. optionQty = optionConfig[optionValue].qty || 0;
  270. canQtyCustomize = optionConfig[optionValue].customQty === '1';
  271. toggleQtyField(qtyField, optionQty, optionId, optionValue, canQtyCustomize);//eslint-disable-line
  272. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  273. tempChanges = applyTierPrice(//eslint-disable-line
  274. tempChanges,
  275. optionQty,
  276. optionConfig[optionValue]
  277. );
  278. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  279. } else {
  280. tempChanges = {};
  281. toggleQtyField(qtyField, '0', optionId, optionValue, false);//eslint-disable-line
  282. }
  283. optionHash = 'bundle-option-' + optionName;
  284. changes[optionHash] = tempChanges;
  285. selectedIds[optionId] = [optionValue];
  286. break;
  287. case 'select-multiple':
  288. optionValue = _.compact(optionValue);
  289. _.each(optionConfig, function (row, optionValueCode) {
  290. optionHash = 'bundle-option-' + optionName + '##' + optionValueCode;
  291. optionQty = row.qty || 0;
  292. tempChanges = utils.deepClone(row.prices);
  293. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  294. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  295. changes[optionHash] = _.contains(optionValue, optionValueCode) ? tempChanges : {};
  296. });
  297. selectedIds[optionId] = optionValue || [];
  298. break;
  299. case 'checkbox':
  300. optionHash = 'bundle-option-' + optionName + '##' + optionValue;
  301. optionQty = optionConfig[optionValue].qty || 0;
  302. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  303. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  304. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  305. changes[optionHash] = element.is(':checked') ? tempChanges : {};
  306. selectedIds[optionId] = selectedIds[optionId] || [];
  307. if (!_.contains(selectedIds[optionId], optionValue) && element.is(':checked')) {
  308. selectedIds[optionId].push(optionValue);
  309. } else if (!element.is(':checked')) {
  310. selectedIds[optionId] = _.without(selectedIds[optionId], optionValue);
  311. }
  312. break;
  313. case 'hidden':
  314. optionHash = 'bundle-option-' + optionName + '##' + optionValue;
  315. optionQty = optionConfig[optionValue].qty || 0;
  316. canQtyCustomize = optionConfig[optionValue].customQty === '1';
  317. qtyField = element.data('qtyField');
  318. qtyField.data('option', element);
  319. toggleQtyField(qtyField, optionQty, optionId, optionValue, canQtyCustomize);//eslint-disable-line
  320. tempChanges = utils.deepClone(optionConfig[optionValue].prices);
  321. tempChanges = applyTierPrice(tempChanges, optionQty, optionConfig);//eslint-disable-line
  322. tempChanges = applyQty(tempChanges, optionQty);//eslint-disable-line
  323. optionHash = 'bundle-option-' + optionName;
  324. changes[optionHash] = tempChanges;
  325. selectedIds[optionId] = [optionValue];
  326. break;
  327. }
  328. return changes;
  329. }
  330. /**
  331. * Check the quantity field if negative value occurs.
  332. *
  333. * @param {Object} bundleOption
  334. */
  335. function isValidQty(bundleOption) {
  336. var isValid = true,
  337. qtyElem = bundleOption.data('qtyField'),
  338. bundleOptionType = bundleOption.prop('type'),
  339. qtyValidator = qtyElem.data('validate') &&
  340. typeof qtyElem.data('validate')['validate-item-quantity'] === 'object' ?
  341. qtyElem.data('validate')['validate-item-quantity'] : null;
  342. if (['radio', 'select-one'].includes(bundleOptionType) &&
  343. qtyValidator &&
  344. (qtyElem.val() < qtyValidator.minAllowed || qtyElem.val() > qtyValidator.maxAllowed)
  345. ) {
  346. isValid = false;
  347. }
  348. return isValid;
  349. }
  350. /**
  351. * Helper to toggle qty field
  352. * @param {jQuery} element
  353. * @param {String|Number} value
  354. * @param {String|Number} optionId
  355. * @param {String|Number} optionValueId
  356. * @param {Boolean} canEdit
  357. */
  358. function toggleQtyField(element, value, optionId, optionValueId, canEdit) {
  359. element
  360. .val(value)
  361. .data('optionId', optionId)
  362. .data('optionValueId', optionValueId)
  363. .attr('disabled', !canEdit);
  364. if (canEdit) {
  365. element.removeClass('qty-disabled');
  366. } else {
  367. element.addClass('qty-disabled');
  368. }
  369. }
  370. /**
  371. * Helper to multiply on qty
  372. *
  373. * @param {Object} prices
  374. * @param {Number} qty
  375. * @returns {Object}
  376. */
  377. function applyQty(prices, qty) {
  378. _.each(prices, function (everyPrice) {
  379. everyPrice.amount *= qty;
  380. _.each(everyPrice.adjustments, function (el, index) {
  381. everyPrice.adjustments[index] *= qty;
  382. });
  383. });
  384. return prices;
  385. }
  386. /**
  387. * Helper to limit price with tier price
  388. *
  389. * @param {Object} oneItemPrice
  390. * @param {Number} qty
  391. * @param {Object} optionConfig
  392. * @returns {Object}
  393. */
  394. function applyTierPrice(oneItemPrice, qty, optionConfig) {
  395. var tiers = optionConfig.tierPrice,
  396. magicKey = _.keys(oneItemPrice)[0],
  397. tiersFirstKey = _.keys(optionConfig)[0],
  398. lowest = false;
  399. if (!tiers) {//tiers is undefined when options has only one option
  400. tiers = optionConfig[tiersFirstKey].tierPrice;
  401. }
  402. tiers.sort(function (a, b) {//sorting based on "price_qty"
  403. return a['price_qty'] - b['price_qty'];
  404. });
  405. _.each(tiers, function (tier, index) {
  406. if (tier['price_qty'] > qty) {
  407. return;
  408. }
  409. if (tier.prices[magicKey].amount < oneItemPrice[magicKey].amount) {
  410. lowest = index;
  411. }
  412. });
  413. if (lowest !== false) {
  414. oneItemPrice = utils.deepClone(tiers[lowest].prices);
  415. }
  416. return oneItemPrice;
  417. }
  418. });