frontend.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. (function ($) {
  2. 'use strict';
  3. $(document).ready(function () {
  4. var $wrap = $('.studiou-fpp-upload-wrap');
  5. if (!$wrap.length) {
  6. return;
  7. }
  8. // ==========================================
  9. // Config + data
  10. // ==========================================
  11. var cfg = {
  12. productId: parseInt($wrap.data('product-id'), 10) || 0,
  13. maxFileSize: parseInt($wrap.data('max-file-size'), 10) || 50,
  14. maxUploads: parseInt($wrap.data('max-uploads'), 10) || 0,
  15. chunkSize: parseInt(studiouWcfppFront.chunkSize, 10) || 1048576
  16. };
  17. var i18n = studiouWcfppFront.i18n || {};
  18. var data = window.studiouFppCardData || {variants: [], batchFiles: []};
  19. var variants = data.variants || [];
  20. var $dropzone = $('#studiou-fpp-dropzone');
  21. var $fileInput = $('#studiou-fpp-file-input');
  22. var $cards = $('#studiou-fpp-cards');
  23. var $messages = $wrap.find('.studiou-fpp-messages');
  24. var originalGallery = {};
  25. // ==========================================
  26. // Bootstrap
  27. // ==========================================
  28. initExistingCards();
  29. initDropzone();
  30. initCardDelegation();
  31. initOverviewDelegation();
  32. applyHeroImage(data.heroPreviewUrl);
  33. // ==========================================
  34. // Card construction
  35. // ==========================================
  36. function initExistingCards() {
  37. (data.batchFiles || []).forEach(function (f) {
  38. var $card = buildCard({
  39. fileRecordId: f.file_record_id,
  40. attachmentId: f.attachment_id,
  41. fileName: f.file_name,
  42. thumbUrl: f.thumb_url,
  43. variationId: f.variation_id || 0,
  44. uploading: false
  45. });
  46. $cards.append($card);
  47. });
  48. refreshMaxReachedState();
  49. }
  50. function buildCard(opts) {
  51. var $card = $('<div class="studiou-fpp-card"></div>');
  52. $card.attr('data-file-record-id', opts.fileRecordId || '');
  53. $card.attr('data-attachment-id', opts.attachmentId || '');
  54. if (opts.uploading) $card.addClass('studiou-fpp-card-uploading');
  55. // Remove-upload button (×)
  56. $card.append('<button type="button" class="studiou-fpp-card-remove" title="' +
  57. escAttr(i18n.removeFile || 'Remove') + '" aria-label="' +
  58. escAttr(i18n.removeFile || 'Remove') + '">&times;</button>');
  59. // Thumb column
  60. var $thumb = $('<div class="studiou-fpp-card-thumb"></div>');
  61. if (opts.thumbUrl) {
  62. $thumb.append($('<img>').attr('src', opts.thumbUrl).attr('alt', opts.fileName || ''));
  63. } else {
  64. $thumb.append('<div class="studiou-fpp-card-thumb-placeholder"></div>');
  65. }
  66. var $progress = $('<div class="studiou-fpp-card-progress"></div>');
  67. $progress.append('<div class="studiou-fpp-card-progress-fill"></div>');
  68. $progress.append('<span class="studiou-fpp-card-progress-text">0%</span>');
  69. $thumb.append($progress);
  70. $card.append($thumb);
  71. // Body column
  72. var $body = $('<div class="studiou-fpp-card-body"></div>');
  73. $body.append($('<div class="studiou-fpp-card-name"></div>').text(opts.fileName || ''));
  74. var $controls = $('<div class="studiou-fpp-card-controls"></div>');
  75. $controls.append(buildVariantSelect(opts.variationId || 0));
  76. $controls.append(buildQtyStepper());
  77. $body.append($controls);
  78. var $priceRow = $('<div class="studiou-fpp-card-price-row"></div>');
  79. $priceRow.append('<span class="studiou-fpp-card-price">&mdash;</span>');
  80. $priceRow.append('<span class="studiou-fpp-card-badge" style="display:none;"></span>');
  81. $priceRow.append('<button type="button" class="button studiou-fpp-card-addtocart">' +
  82. escHtml(i18n.addToCart || 'Add to cart') + '</button>');
  83. $body.append($priceRow);
  84. $card.append($body);
  85. if (!opts.uploading) {
  86. updateCardPrice($card);
  87. }
  88. return $card;
  89. }
  90. function buildVariantSelect(initialVariationId) {
  91. var $select = $('<select class="studiou-fpp-card-variant"></select>');
  92. $select.append($('<option></option>').val(0).text(i18n.selectVariant || 'Select variant'));
  93. for (var i = 0; i < variants.length; i++) {
  94. var v = variants[i];
  95. // <option> text nodes cannot render HTML, so use a plain-text price formatter
  96. var priceLabel = v.base_price ? ' — ' + formatPricePlain(v.base_price) : '';
  97. var label = v.label + priceLabel;
  98. if (!v.in_stock) {
  99. label += ' — ' + (i18n.outOfStock || 'out of stock');
  100. }
  101. var $opt = $('<option></option>')
  102. .val(v.id)
  103. .text(label)
  104. .attr('data-base-price', v.base_price);
  105. if (!v.in_stock) {
  106. $opt.prop('disabled', true);
  107. }
  108. $select.append($opt);
  109. }
  110. if (initialVariationId) {
  111. $select.val(initialVariationId);
  112. }
  113. return $select;
  114. }
  115. function buildQtyStepper() {
  116. var $stepper = $('<div class="studiou-fpp-card-qty"></div>');
  117. $stepper.append('<button type="button" class="studiou-fpp-qty-btn studiou-fpp-qty-minus">&minus;</button>');
  118. $stepper.append('<input type="number" class="studiou-fpp-qty-input" min="1" step="1" value="1">');
  119. $stepper.append('<button type="button" class="studiou-fpp-qty-btn studiou-fpp-qty-plus">+</button>');
  120. return $stepper;
  121. }
  122. // ==========================================
  123. // Card live updates
  124. // ==========================================
  125. function initCardDelegation() {
  126. $cards.on('change', '.studiou-fpp-card-variant', function () {
  127. updateCardPrice($(this).closest('.studiou-fpp-card'));
  128. });
  129. $cards.on('input change keyup', '.studiou-fpp-qty-input', function () {
  130. updateCardPrice($(this).closest('.studiou-fpp-card'));
  131. });
  132. $cards.on('click', '.studiou-fpp-qty-plus', function () {
  133. var $input = $(this).closest('.studiou-fpp-card-qty').find('.studiou-fpp-qty-input');
  134. $input.val((parseInt($input.val(), 10) || 1) + 1).trigger('change');
  135. });
  136. $cards.on('click', '.studiou-fpp-qty-minus', function () {
  137. var $input = $(this).closest('.studiou-fpp-card-qty').find('.studiou-fpp-qty-input');
  138. var v = Math.max(1, (parseInt($input.val(), 10) || 1) - 1);
  139. $input.val(v).trigger('change');
  140. });
  141. $cards.on('click', '.studiou-fpp-card-addtocart', function () {
  142. onAddToCartClick($(this).closest('.studiou-fpp-card'));
  143. });
  144. $cards.on('click', '.studiou-fpp-card-remove', function () {
  145. onRemoveUploadClick($(this).closest('.studiou-fpp-card'));
  146. });
  147. }
  148. function findVariant(variationId) {
  149. variationId = parseInt(variationId, 10) || 0;
  150. if (!variationId) return null;
  151. for (var i = 0; i < variants.length; i++) {
  152. if (variants[i].id === variationId) return variants[i];
  153. }
  154. return null;
  155. }
  156. function resolveTier(tiers, qty) {
  157. if (!tiers || !tiers.length) return null;
  158. var match = null;
  159. for (var i = 0; i < tiers.length; i++) {
  160. if (tiers[i].from_qty <= qty) match = tiers[i];
  161. else break;
  162. }
  163. return match;
  164. }
  165. function updateCardPrice($card) {
  166. var $price = $card.find('.studiou-fpp-card-price');
  167. var $badge = $card.find('.studiou-fpp-card-badge');
  168. var $btn = $card.find('.studiou-fpp-card-addtocart');
  169. var variationId = parseInt($card.find('.studiou-fpp-card-variant').val(), 10) || 0;
  170. var qty = Math.max(1, parseInt($card.find('.studiou-fpp-qty-input').val(), 10) || 1);
  171. var variant = findVariant(variationId);
  172. if (!variant || !variant.base_price) {
  173. $price.html('&mdash;');
  174. $badge.hide();
  175. $btn.prop('disabled', true);
  176. return;
  177. }
  178. var tier = resolveTier(variant.tiers || [], qty);
  179. var unit = variant.base_price;
  180. if (tier && tier.percent > 0) {
  181. unit = variant.base_price * (1 - (tier.percent / 100));
  182. $badge.show().text('−' + formatPercent(tier.percent));
  183. } else {
  184. $badge.hide();
  185. }
  186. var lineTotal = unit * qty;
  187. $price.html(formatPriceHtml(lineTotal));
  188. var uploading = $card.hasClass('studiou-fpp-card-uploading');
  189. $btn.prop('disabled', uploading);
  190. }
  191. // ==========================================
  192. // Upload dropzone
  193. // ==========================================
  194. function initDropzone() {
  195. $dropzone.on('dragover dragenter', function (e) {
  196. e.preventDefault(); e.stopPropagation();
  197. $(this).addClass('studiou-fpp-dragover');
  198. });
  199. $dropzone.on('dragleave drop', function (e) {
  200. e.preventDefault(); e.stopPropagation();
  201. $(this).removeClass('studiou-fpp-dragover');
  202. });
  203. $dropzone.on('drop', function (e) {
  204. var files = e.originalEvent.dataTransfer.files;
  205. enqueueFiles(files);
  206. });
  207. $dropzone.on('click', function () {
  208. if ($dropzone.hasClass('studiou-fpp-dropzone-disabled')) return;
  209. $fileInput.trigger('click');
  210. });
  211. $fileInput.on('change', function () {
  212. enqueueFiles(this.files);
  213. $fileInput.val('');
  214. });
  215. }
  216. var uploadQueue = [];
  217. var activeUpload = false;
  218. function enqueueFiles(fileList) {
  219. if (!fileList || !fileList.length) return;
  220. clearMessages();
  221. if (cfg.maxUploads > 0) {
  222. var existing = $cards.children('.studiou-fpp-card').length;
  223. var available = Math.max(0, cfg.maxUploads - existing - uploadQueue.length);
  224. if (available <= 0) {
  225. showMessage(formatI18n(i18n.maxUploadsReached, cfg.maxUploads), 'error');
  226. return;
  227. }
  228. if (fileList.length > available) {
  229. showMessage(formatI18n(i18n.maxUploadsReached, cfg.maxUploads), 'error');
  230. }
  231. for (var i = 0; i < Math.min(fileList.length, available); i++) {
  232. pushFile(fileList[i]);
  233. }
  234. } else {
  235. for (var j = 0; j < fileList.length; j++) {
  236. pushFile(fileList[j]);
  237. }
  238. }
  239. refreshMaxReachedState();
  240. drainQueue();
  241. }
  242. function pushFile(file) {
  243. var maxBytes = cfg.maxFileSize * 1024 * 1024;
  244. if (file.size > maxBytes) {
  245. showMessage(formatI18n(i18n.fileTooLarge, cfg.maxFileSize), 'error');
  246. return;
  247. }
  248. var $card = buildCard({
  249. fileRecordId: 0,
  250. attachmentId: 0,
  251. fileName: file.name,
  252. thumbUrl: '',
  253. variationId: 0,
  254. uploading: true
  255. });
  256. $cards.append($card);
  257. uploadQueue.push({file: file, $card: $card});
  258. }
  259. function drainQueue() {
  260. if (activeUpload || !uploadQueue.length) return;
  261. activeUpload = true;
  262. var next = uploadQueue.shift();
  263. uploadOne(next.file, next.$card, function () {
  264. activeUpload = false;
  265. drainQueue();
  266. });
  267. }
  268. function uploadOne(file, $card, done) {
  269. var totalChunks = Math.ceil(file.size / cfg.chunkSize);
  270. var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
  271. var currentChunk = 0;
  272. var $fill = $card.find('.studiou-fpp-card-progress-fill');
  273. var $text = $card.find('.studiou-fpp-card-progress-text');
  274. $card.find('.studiou-fpp-card-progress').show();
  275. function next() {
  276. var start = currentChunk * cfg.chunkSize;
  277. var end = Math.min(start + cfg.chunkSize, file.size);
  278. var blob = file.slice(start, end);
  279. var form = new FormData();
  280. form.append('action', 'studiou_wcfpp_upload_chunk');
  281. form.append('nonce', studiouWcfppFront.nonce);
  282. form.append('product_id', cfg.productId);
  283. form.append('upload_id', uploadId);
  284. form.append('chunk_index', currentChunk);
  285. form.append('total_chunks', totalChunks);
  286. form.append('file_name', file.name);
  287. form.append('file_size', file.size);
  288. form.append('chunk', blob, 'chunk');
  289. var isLast = (currentChunk === totalChunks - 1);
  290. if (isLast) {
  291. $fill.css('width', '100%');
  292. $text.text(i18n.processing || 'Processing...');
  293. }
  294. $.ajax({
  295. url: studiouWcfppFront.ajaxUrl,
  296. type: 'POST',
  297. data: form,
  298. processData: false,
  299. contentType: false,
  300. timeout: isLast ? 120000 : 30000,
  301. success: function (response) {
  302. if (!response.success) {
  303. failUpload($card, response.data ? response.data.message : (i18n.uploadError || 'Upload failed.'));
  304. done();
  305. return;
  306. }
  307. if (!response.data.complete) {
  308. currentChunk++;
  309. var pct = Math.round((currentChunk / totalChunks) * 100);
  310. $fill.css('width', pct + '%');
  311. $text.text(pct + '%');
  312. next();
  313. } else {
  314. finishUpload($card, response.data);
  315. done();
  316. }
  317. },
  318. error: function () {
  319. failUpload($card, i18n.uploadError || 'Upload failed.');
  320. done();
  321. }
  322. });
  323. }
  324. next();
  325. }
  326. function finishUpload($card, res) {
  327. $card.removeClass('studiou-fpp-card-uploading');
  328. $card.find('.studiou-fpp-card-progress').hide();
  329. $card.attr('data-file-record-id', res.file_record_id);
  330. $card.attr('data-attachment-id', res.attachment_id);
  331. var $img = $card.find('.studiou-fpp-card-thumb img');
  332. if ($img.length) {
  333. $img.attr('src', res.thumbnail_url);
  334. } else {
  335. $card.find('.studiou-fpp-card-thumb-placeholder').replaceWith(
  336. $('<img>').attr('src', res.thumbnail_url).attr('alt', res.file_name)
  337. );
  338. }
  339. updateCardPrice($card);
  340. refreshMaxReachedState();
  341. // If this is now the first/only card, swap the hero gallery image
  342. if ($cards.children('.studiou-fpp-card').index($card) === 0) {
  343. applyHeroImage(res.preview_url || res.thumbnail_url);
  344. }
  345. }
  346. function failUpload($card, message) {
  347. $card.remove();
  348. showMessage(message, 'error');
  349. refreshMaxReachedState();
  350. }
  351. function refreshMaxReachedState() {
  352. if (cfg.maxUploads <= 0) return;
  353. var reached = $cards.children('.studiou-fpp-card').length >= cfg.maxUploads;
  354. $dropzone.toggleClass('studiou-fpp-dropzone-disabled', reached);
  355. }
  356. // ==========================================
  357. // Remove upload (per-card × button)
  358. // ==========================================
  359. function onRemoveUploadClick($card) {
  360. var fileRecordId = parseInt($card.attr('data-file-record-id'), 10) || 0;
  361. if (!fileRecordId) {
  362. // Still uploading — just cancel locally
  363. $card.remove();
  364. refreshMaxReachedState();
  365. return;
  366. }
  367. $.ajax({
  368. url: studiouWcfppFront.ajaxUrl,
  369. type: 'POST',
  370. data: {
  371. action: 'studiou_wcfpp_remove_upload',
  372. nonce: studiouWcfppFront.nonce,
  373. file_record_id: fileRecordId
  374. },
  375. success: function (response) {
  376. if (response.success) {
  377. var wasFirst = ($cards.children('.studiou-fpp-card').index($card) === 0);
  378. $card.remove();
  379. refreshMaxReachedState();
  380. if (wasFirst) {
  381. var $nextFirst = $cards.children('.studiou-fpp-card').first();
  382. if ($nextFirst.length) {
  383. var nextImg = $nextFirst.find('.studiou-fpp-card-thumb img').attr('src');
  384. applyHeroImage(nextImg || '');
  385. } else {
  386. restoreHeroImage();
  387. }
  388. }
  389. } else {
  390. showMessage(response.data ? response.data.message : (i18n.error || 'Error'), 'error');
  391. }
  392. },
  393. error: function () {
  394. showMessage(i18n.error || 'Error', 'error');
  395. }
  396. });
  397. }
  398. // ==========================================
  399. // Add to cart
  400. // ==========================================
  401. function onAddToCartClick($card) {
  402. var fileRecordId = parseInt($card.attr('data-file-record-id'), 10) || 0;
  403. var variationId = parseInt($card.find('.studiou-fpp-card-variant').val(), 10) || 0;
  404. var qty = Math.max(1, parseInt($card.find('.studiou-fpp-qty-input').val(), 10) || 1);
  405. if (!fileRecordId) {
  406. showMessage(i18n.uploadFile || 'Please upload a file.', 'error');
  407. return;
  408. }
  409. if (!variationId) {
  410. showMessage(i18n.selectVariant || 'Please select a variant.', 'error');
  411. return;
  412. }
  413. var $btn = $card.find('.studiou-fpp-card-addtocart');
  414. $btn.prop('disabled', true);
  415. $.ajax({
  416. url: studiouWcfppFront.ajaxUrl,
  417. type: 'POST',
  418. data: {
  419. action: 'studiou_wcfpp_add_file_to_cart',
  420. nonce: studiouWcfppFront.nonce,
  421. file_record_id: fileRecordId,
  422. variation_id: variationId,
  423. quantity: qty
  424. },
  425. success: function (response) {
  426. if (response.success) {
  427. showMessage(i18n.addedToCart || 'Added to cart.', 'success');
  428. if (response.data && response.data.overview_html) {
  429. updateOverview(response.data.overview_html);
  430. }
  431. } else {
  432. showMessage(response.data ? response.data.message : (i18n.addToCartError || 'Could not add to cart.'), 'error');
  433. }
  434. },
  435. error: function () {
  436. showMessage(i18n.addToCartError || 'Could not add to cart.', 'error');
  437. },
  438. complete: function () {
  439. $btn.prop('disabled', false);
  440. }
  441. });
  442. }
  443. // ==========================================
  444. // Order overview
  445. // ==========================================
  446. function initOverviewDelegation() {
  447. // Remove-line on overview panel (cart-only, upload stays)
  448. $(document).on('click', '.studiou-fpp-overview-remove', function (e) {
  449. var $link = $(this);
  450. var href = $link.attr('href');
  451. // Only intercept if the link has a nonce arg — otherwise let browser navigate
  452. if (!href || href.indexOf('studiou-fpp-remove-line') < 0) return;
  453. e.preventDefault();
  454. var $line = $link.closest('.studiou-fpp-overview-line');
  455. var key = $line.data('cart-item-key');
  456. if (!key) return;
  457. $.ajax({
  458. url: studiouWcfppFront.ajaxUrl,
  459. type: 'POST',
  460. data: {
  461. action: 'studiou_wcfpp_remove_cart_line',
  462. nonce: studiouWcfppFront.nonce,
  463. cart_item_key: key
  464. },
  465. success: function (response) {
  466. if (response.success && response.data && response.data.overview_html) {
  467. updateOverview(response.data.overview_html);
  468. } else if (!response.success) {
  469. showMessage(response.data ? response.data.message : (i18n.error || 'Error'), 'error');
  470. }
  471. },
  472. error: function () {
  473. showMessage(i18n.error || 'Error', 'error');
  474. }
  475. });
  476. });
  477. }
  478. function updateOverview(html) {
  479. var $new = $(html);
  480. var $existing = $('#studiou-fpp-overview');
  481. if ($existing.length) {
  482. $existing.replaceWith($new);
  483. } else {
  484. $wrap.after($new);
  485. }
  486. $(document).trigger('studiou-fpp:cart-updated');
  487. }
  488. // ==========================================
  489. // Hero image
  490. // ==========================================
  491. function applyHeroImage(url) {
  492. if (!url) return;
  493. var selectors = [
  494. '.woocommerce-product-gallery__image img',
  495. '.product .wp-post-image',
  496. '.product-image img',
  497. '.product-thumbnail img',
  498. '.entry-summary img.attachment-woocommerce_thumbnail',
  499. '.product-gallery img:first'
  500. ];
  501. var $img = null;
  502. for (var i = 0; i < selectors.length; i++) {
  503. $img = $(selectors[i]).first();
  504. if ($img.length) break;
  505. }
  506. if (!$img || !$img.length) return;
  507. if (!originalGallery.src) {
  508. originalGallery.src = $img.attr('src');
  509. originalGallery.srcset = $img.attr('srcset') || '';
  510. originalGallery.sizes = $img.attr('sizes') || '';
  511. var $link = $img.closest('a');
  512. if ($link.length) originalGallery.href = $link.attr('href');
  513. }
  514. $img.attr('src', url).removeAttr('srcset').removeAttr('sizes');
  515. var $a = $img.closest('a');
  516. if ($a.length) $a.attr('href', url);
  517. }
  518. function restoreHeroImage() {
  519. if (!originalGallery.src) return;
  520. var selectors = [
  521. '.woocommerce-product-gallery__image img',
  522. '.product .wp-post-image',
  523. '.product-image img',
  524. '.product-thumbnail img',
  525. '.entry-summary img.attachment-woocommerce_thumbnail',
  526. '.product-gallery img:first'
  527. ];
  528. var $img = null;
  529. for (var i = 0; i < selectors.length; i++) {
  530. $img = $(selectors[i]).first();
  531. if ($img.length) break;
  532. }
  533. if (!$img || !$img.length) return;
  534. $img.attr('src', originalGallery.src);
  535. if (originalGallery.srcset) $img.attr('srcset', originalGallery.srcset);
  536. if (originalGallery.sizes) $img.attr('sizes', originalGallery.sizes);
  537. if (originalGallery.href) $img.closest('a').attr('href', originalGallery.href);
  538. originalGallery = {};
  539. }
  540. // ==========================================
  541. // Helpers
  542. // ==========================================
  543. function showMessage(text, type) {
  544. var cls = type === 'error' ? 'studiou-fpp-msg-error'
  545. : type === 'success' ? 'studiou-fpp-msg-success'
  546. : 'studiou-fpp-msg-info';
  547. $messages.html('<div class="studiou-fpp-msg ' + cls + '">' + escHtml(text) + '</div>');
  548. }
  549. function clearMessages() { $messages.empty(); }
  550. function escHtml(str) {
  551. var div = document.createElement('div');
  552. div.appendChild(document.createTextNode(str == null ? '' : String(str)));
  553. return div.innerHTML;
  554. }
  555. function escAttr(str) {
  556. return String(str == null ? '' : str).replace(/"/g, '&quot;');
  557. }
  558. function formatI18n(tpl, val) {
  559. if (!tpl) return String(val);
  560. return tpl.replace('%d', val).replace('%s', val);
  561. }
  562. function formatPercent(p) {
  563. var n = parseFloat(p) || 0;
  564. var str = n.toFixed(2).replace(/\.?0+$/, '');
  565. return str + ' %';
  566. }
  567. function formatPrice(amount) {
  568. return formatPriceHtml(amount);
  569. }
  570. // Plain-text price (no HTML) — used for <option> labels where spans aren't rendered
  571. function formatPricePlain(amount) {
  572. var c = studiouWcfppFront.currency || {};
  573. var decimals = parseInt(c.decimals, 10); if (isNaN(decimals)) decimals = 2;
  574. var decSep = c.decimalSeparator || '.';
  575. var thouSep = c.thousandSeparator || ',';
  576. var fmt = c.priceFormat || '%1$s%2$s';
  577. var symbol = c.symbol || '';
  578. var fixed = parseFloat(amount).toFixed(decimals);
  579. var parts = fixed.split('.');
  580. var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep);
  581. var decPart = parts.length > 1 ? parts[1] : '';
  582. var numStr = decPart ? intPart + decSep + decPart : intPart;
  583. // strip any HTML tags that might arrive from priceFormat (defensive)
  584. return fmt.replace('%1$s', symbol).replace('%2$s', numStr).replace(/<[^>]*>/g, '');
  585. }
  586. function formatPriceHtml(amount) {
  587. var c = studiouWcfppFront.currency || {};
  588. var decimals = parseInt(c.decimals, 10); if (isNaN(decimals)) decimals = 2;
  589. var decSep = c.decimalSeparator || '.';
  590. var thouSep = c.thousandSeparator || ',';
  591. var fmt = c.priceFormat || '%1$s%2$s';
  592. var symbol = c.symbol || '';
  593. var fixed = parseFloat(amount).toFixed(decimals);
  594. var parts = fixed.split('.');
  595. var intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouSep);
  596. var decPart = parts.length > 1 ? parts[1] : '';
  597. var numStr = decPart ? intPart + decSep + decPart : intPart;
  598. var sym = '<span class="woocommerce-Price-currencySymbol">' + symbol + '</span>';
  599. return '<span class="woocommerce-Price-amount amount">' +
  600. fmt.replace('%1$s', sym).replace('%2$s', numStr) +
  601. '</span>';
  602. }
  603. });
  604. })(jQuery);