frontend.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. (function ($) {
  2. 'use strict';
  3. $(document).ready(function () {
  4. var $uploadWrap = $('.studiou-fpp-upload-wrap');
  5. if (!$uploadWrap.length) {
  6. return;
  7. }
  8. var productId = $uploadWrap.data('product-id');
  9. var maxFileSize = parseInt($uploadWrap.data('max-file-size'), 10) || 50;
  10. var chunkSize = parseInt(studiouWcfppFront.chunkSize, 10) || 1048576;
  11. var uploadedAttachmentId = null;
  12. var uploadedFileRecordId = null;
  13. var isUploading = false;
  14. var originalGalleryImages = {};
  15. // Broad selectors for add-to-cart elements
  16. var addToCartSelectors = [
  17. '.single_add_to_cart_button',
  18. '.add_to_cart_button',
  19. 'button[name="add-to-cart"]',
  20. 'input[name="add-to-cart"]',
  21. 'a[data-product_id]',
  22. 'button[data-product_id]',
  23. 'a[href*="add-to-cart"]',
  24. '.product form.cart button[type="submit"]',
  25. '.product form.cart input[type="submit"]'
  26. ].join(', ');
  27. // Upload area elements
  28. var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone');
  29. var $fileInput = $uploadWrap.find('#studiou-fpp-file-input');
  30. var $progress = $uploadWrap.find('.studiou-fpp-progress');
  31. var $progressFill = $uploadWrap.find('.studiou-fpp-progress-fill');
  32. var $progressText = $uploadWrap.find('.studiou-fpp-progress-text');
  33. var $preview = $uploadWrap.find('.studiou-fpp-preview');
  34. var $previewImg = $preview.find('img');
  35. var $previewName = $uploadWrap.find('.studiou-fpp-preview-name');
  36. var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file');
  37. var $messages = $uploadWrap.find('.studiou-fpp-messages');
  38. // ==========================================
  39. // Disable/enable add-to-cart buttons
  40. // ==========================================
  41. function disableAddToCart() {
  42. $(addToCartSelectors).each(function () {
  43. var $el = $(this);
  44. $el.addClass('studiou-fpp-disabled');
  45. if ($el.is('a')) {
  46. if (!$el.data('studiou-fpp-href')) {
  47. $el.data('studiou-fpp-href', $el.attr('href'));
  48. }
  49. $el.attr('href', '#');
  50. } else {
  51. $el.prop('disabled', true);
  52. }
  53. });
  54. }
  55. function enableAddToCart() {
  56. $(addToCartSelectors).each(function () {
  57. var $el = $(this);
  58. $el.removeClass('studiou-fpp-disabled');
  59. if ($el.is('a')) {
  60. var origHref = $el.data('studiou-fpp-href');
  61. if (origHref) {
  62. $el.attr('href', origHref);
  63. }
  64. } else {
  65. $el.prop('disabled', false);
  66. }
  67. });
  68. }
  69. // Fallback: intercept clicks on disabled elements (capture phase)
  70. document.addEventListener('click', function (e) {
  71. if (!uploadedAttachmentId) {
  72. var el = e.target.closest(addToCartSelectors.replace(/,\s*/g, ', '));
  73. if (el) {
  74. e.preventDefault();
  75. e.stopImmediatePropagation();
  76. showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
  77. $('html, body').animate({ scrollTop: $uploadWrap.offset().top - 100 }, 400);
  78. return false;
  79. }
  80. }
  81. }, true); // capture phase - fires before bubble
  82. // Disable buttons on page load
  83. disableAddToCart();
  84. // Also watch for dynamically added buttons (e.g. AJAX-loaded variations)
  85. var observer = new MutationObserver(function () {
  86. if (!uploadedAttachmentId) {
  87. disableAddToCart();
  88. }
  89. });
  90. observer.observe(document.querySelector('.product') || document.body, {
  91. childList: true, subtree: true
  92. });
  93. // ==========================================
  94. // Restore session state on page load
  95. // ==========================================
  96. if (typeof studiouWcfppSession !== 'undefined' && studiouWcfppSession.attachment_id) {
  97. uploadedAttachmentId = studiouWcfppSession.attachment_id;
  98. uploadedFileRecordId = studiouWcfppSession.file_record_id;
  99. showPreview(studiouWcfppSession.thumbnail_url, studiouWcfppSession.file_name);
  100. updateProductGallery(studiouWcfppSession.preview_url || studiouWcfppSession.thumbnail_url);
  101. enableAddToCart();
  102. }
  103. // ==========================================
  104. // Drag and drop
  105. // ==========================================
  106. $dropzone.on('dragover dragenter', function (e) {
  107. e.preventDefault();
  108. e.stopPropagation();
  109. $(this).addClass('studiou-fpp-dragover');
  110. });
  111. $dropzone.on('dragleave drop', function (e) {
  112. e.preventDefault();
  113. e.stopPropagation();
  114. $(this).removeClass('studiou-fpp-dragover');
  115. });
  116. $dropzone.on('drop', function (e) {
  117. var files = e.originalEvent.dataTransfer.files;
  118. if (files.length > 0) {
  119. handleFile(files[0]);
  120. }
  121. });
  122. $dropzone.on('click', function () {
  123. if (!isUploading) {
  124. $fileInput.trigger('click');
  125. }
  126. });
  127. $fileInput.on('change', function () {
  128. if (this.files.length > 0) {
  129. handleFile(this.files[0]);
  130. }
  131. });
  132. // Remove file
  133. $removeBtn.on('click', function () {
  134. removeUploadedFile();
  135. });
  136. // ==========================================
  137. // Upload logic
  138. // ==========================================
  139. function handleFile(file) {
  140. if (isUploading) return;
  141. var maxBytes = maxFileSize * 1024 * 1024;
  142. if (file.size > maxBytes) {
  143. showMessage(studiouWcfppFront.i18n.fileTooLarge.replace('%s', maxFileSize), 'error');
  144. return;
  145. }
  146. clearMessages();
  147. startChunkedUpload(file);
  148. }
  149. function startChunkedUpload(file) {
  150. isUploading = true;
  151. var totalChunks = Math.ceil(file.size / chunkSize);
  152. var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  153. var currentChunk = 0;
  154. $dropzone.hide();
  155. $progress.show();
  156. function uploadNextChunk() {
  157. var start = currentChunk * chunkSize;
  158. var end = Math.min(start + chunkSize, file.size);
  159. var blob = file.slice(start, end);
  160. var formData = new FormData();
  161. formData.append('action', 'studiou_wcfpp_upload_chunk');
  162. formData.append('nonce', studiouWcfppFront.nonce);
  163. formData.append('product_id', productId);
  164. formData.append('upload_id', uploadId);
  165. formData.append('chunk_index', currentChunk);
  166. formData.append('total_chunks', totalChunks);
  167. formData.append('file_name', file.name);
  168. formData.append('file_size', file.size);
  169. formData.append('chunk', blob, 'chunk');
  170. var isLastChunk = (currentChunk === totalChunks - 1);
  171. if (isLastChunk) {
  172. $progressFill.css('width', '100%');
  173. $progressText.text(studiouWcfppFront.i18n.processing || 'Processing...');
  174. }
  175. $.ajax({
  176. url: studiouWcfppFront.ajaxUrl,
  177. type: 'POST',
  178. data: formData,
  179. processData: false,
  180. contentType: false,
  181. timeout: isLastChunk ? 120000 : 30000,
  182. success: function (response) {
  183. if (response.success) {
  184. if (!isLastChunk) {
  185. currentChunk++;
  186. var percent = Math.round((currentChunk / totalChunks) * 100);
  187. $progressFill.css('width', percent + '%');
  188. $progressText.text(percent + '%');
  189. }
  190. if (response.data.complete) {
  191. uploadedAttachmentId = response.data.attachment_id;
  192. uploadedFileRecordId = response.data.file_record_id;
  193. storeInSession(
  194. response.data.attachment_id,
  195. response.data.file_record_id,
  196. response.data.file_name
  197. );
  198. showPreview(response.data.thumbnail_url, response.data.file_name);
  199. updateProductGallery(response.data.preview_url || response.data.thumbnail_url);
  200. enableAddToCart();
  201. $progress.hide();
  202. isUploading = false;
  203. } else {
  204. uploadNextChunk();
  205. }
  206. } else {
  207. uploadFailed(response.data ? response.data.message : studiouWcfppFront.i18n.uploadError);
  208. }
  209. },
  210. error: function () {
  211. uploadFailed(studiouWcfppFront.i18n.uploadError);
  212. }
  213. });
  214. }
  215. uploadNextChunk();
  216. }
  217. // ==========================================
  218. // Session management
  219. // ==========================================
  220. function storeInSession(attachmentId, fileRecordId, fileName) {
  221. $.ajax({
  222. url: studiouWcfppFront.ajaxUrl,
  223. type: 'POST',
  224. data: {
  225. action: 'studiou_wcfpp_set_upload_session',
  226. nonce: studiouWcfppFront.nonce,
  227. attachment_id: attachmentId,
  228. file_record_id: fileRecordId,
  229. file_name: fileName
  230. }
  231. });
  232. }
  233. function clearSession() {
  234. $.ajax({
  235. url: studiouWcfppFront.ajaxUrl,
  236. type: 'POST',
  237. data: {
  238. action: 'studiou_wcfpp_clear_upload_session',
  239. nonce: studiouWcfppFront.nonce
  240. }
  241. });
  242. }
  243. // ==========================================
  244. // Product gallery image replacement
  245. // ==========================================
  246. function updateProductGallery(imageUrl) {
  247. if (!imageUrl) return;
  248. var selectors = [
  249. '.woocommerce-product-gallery__image img',
  250. '.product .wp-post-image',
  251. '.product-image img',
  252. '.product-thumbnail img',
  253. '.entry-summary img.attachment-woocommerce_thumbnail',
  254. '.product-gallery img:first'
  255. ];
  256. var $img = null;
  257. for (var i = 0; i < selectors.length; i++) {
  258. $img = $(selectors[i]).first();
  259. if ($img.length) break;
  260. }
  261. if (!$img || !$img.length) return;
  262. if (!originalGalleryImages.src) {
  263. originalGalleryImages.src = $img.attr('src');
  264. originalGalleryImages.srcset = $img.attr('srcset') || '';
  265. originalGalleryImages.sizes = $img.attr('sizes') || '';
  266. var $parentLink = $img.closest('a');
  267. if ($parentLink.length) {
  268. originalGalleryImages.href = $parentLink.attr('href');
  269. }
  270. }
  271. $img.attr('src', imageUrl);
  272. $img.removeAttr('srcset');
  273. $img.removeAttr('sizes');
  274. var $parentLink = $img.closest('a');
  275. if ($parentLink.length) {
  276. $parentLink.attr('href', imageUrl);
  277. }
  278. }
  279. function restoreProductGallery() {
  280. if (!originalGalleryImages.src) return;
  281. var selectors = [
  282. '.woocommerce-product-gallery__image img',
  283. '.product .wp-post-image',
  284. '.product-image img',
  285. '.product-thumbnail img',
  286. '.entry-summary img.attachment-woocommerce_thumbnail',
  287. '.product-gallery img:first'
  288. ];
  289. var $img = null;
  290. for (var i = 0; i < selectors.length; i++) {
  291. $img = $(selectors[i]).first();
  292. if ($img.length) break;
  293. }
  294. if (!$img || !$img.length) return;
  295. $img.attr('src', originalGalleryImages.src);
  296. if (originalGalleryImages.srcset) $img.attr('srcset', originalGalleryImages.srcset);
  297. if (originalGalleryImages.sizes) $img.attr('sizes', originalGalleryImages.sizes);
  298. if (originalGalleryImages.href) {
  299. $img.closest('a').attr('href', originalGalleryImages.href);
  300. }
  301. originalGalleryImages = {};
  302. }
  303. // ==========================================
  304. // UI helpers
  305. // ==========================================
  306. function uploadFailed(message) {
  307. isUploading = false;
  308. $progress.hide();
  309. $dropzone.show();
  310. showMessage(message, 'error');
  311. $fileInput.val('');
  312. }
  313. function showPreview(thumbnailUrl, fileName) {
  314. if (thumbnailUrl) {
  315. $previewImg.attr('src', thumbnailUrl).show();
  316. } else {
  317. $previewImg.hide();
  318. }
  319. $previewName.text(fileName);
  320. $preview.show();
  321. $dropzone.hide();
  322. }
  323. function removeUploadedFile() {
  324. if (!uploadedFileRecordId) return;
  325. $.ajax({
  326. url: studiouWcfppFront.ajaxUrl,
  327. type: 'POST',
  328. data: {
  329. action: 'studiou_wcfpp_remove_upload',
  330. nonce: studiouWcfppFront.nonce,
  331. file_record_id: uploadedFileRecordId
  332. },
  333. success: function () {
  334. resetUpload();
  335. }
  336. });
  337. }
  338. function resetUpload() {
  339. uploadedAttachmentId = null;
  340. uploadedFileRecordId = null;
  341. clearSession();
  342. restoreProductGallery();
  343. disableAddToCart();
  344. $preview.hide();
  345. $previewImg.attr('src', '');
  346. $previewName.text('');
  347. $dropzone.show();
  348. $fileInput.val('');
  349. clearMessages();
  350. }
  351. function showMessage(text, type) {
  352. var cls = type === 'error' ? 'studiou-fpp-msg-error' : 'studiou-fpp-msg-success';
  353. $messages.html('<div class="studiou-fpp-msg ' + cls + '">' + escHtml(text) + '</div>');
  354. }
  355. function clearMessages() {
  356. $messages.empty();
  357. }
  358. function escHtml(str) {
  359. var div = document.createElement('div');
  360. div.appendChild(document.createTextNode(str));
  361. return div.innerHTML;
  362. }
  363. });
  364. })(jQuery);