├── .eslintignore ├── cartridges ├── int_twicpics │ ├── package.json │ ├── cartridge │ │ ├── templates │ │ │ └── default │ │ │ │ ├── components │ │ │ │ └── header │ │ │ │ │ └── twicpicsHeader.isml │ │ │ │ └── twicpics.isml │ │ ├── int_twicpics.properties │ │ ├── scripts │ │ │ ├── hooks │ │ │ │ └── htmlhead.js │ │ │ └── helpers │ │ │ │ ├── twicpicsHelpers.js │ │ │ │ └── ImageTwicpics.js │ │ ├── controllers │ │ │ └── Twicpics.js │ │ └── models │ │ │ └── twicImg.js │ ├── hooks.json │ └── .project ├── int_twicpics_sfra │ ├── cartridge │ │ ├── int_twicpics_sfra.properties │ │ ├── client │ │ │ └── default │ │ │ │ ├── js │ │ │ │ ├── productDetail.js │ │ │ │ ├── productTile.js │ │ │ │ ├── search.js │ │ │ │ └── product │ │ │ │ │ └── base.js │ │ │ │ └── scss │ │ │ │ └── twicpics.scss │ │ ├── controllers │ │ │ └── Product.js │ │ └── models │ │ │ └── product │ │ │ └── productImages.js │ └── .project └── twicpics_sfra_changes │ ├── cartridge │ ├── twicpics_sfra_changes.properties │ └── templates │ │ └── default │ │ ├── experience │ │ └── components │ │ │ └── commerce_assets │ │ │ ├── shopTheLook │ │ │ └── imageFile.isml │ │ │ ├── campaignBanner.isml │ │ │ ├── photoTile.isml │ │ │ ├── mainBanner.isml │ │ │ ├── productListTile.isml │ │ │ ├── imageAndText.isml │ │ │ └── product │ │ │ └── productTile.isml │ │ ├── product │ │ └── components │ │ │ ├── productTileImage.isml │ │ │ ├── productTileSwatch.isml │ │ │ ├── imageCarousel.isml │ │ │ └── choiceOfBonusProducts │ │ │ └── chooseBonusProduct.isml │ │ ├── cart │ │ └── productCard │ │ │ ├── cartProductCardBundledItems.isml │ │ │ ├── uncategorizedCartProductCard.isml │ │ │ ├── cartNestedBonusProductCard.isml │ │ │ └── cartProductCard.isml │ │ ├── checkout │ │ ├── productCard │ │ │ ├── productShippingCard.isml │ │ │ ├── uncategorizedProductCard.isml │ │ │ ├── embeddedBonusProduct.isml │ │ │ ├── productCard.isml │ │ │ └── bonusProductCard.isml │ │ └── billing │ │ │ └── storedPaymentInstruments.isml │ │ ├── slots │ │ └── category │ │ │ └── categoryTile.isml │ │ ├── account │ │ └── order │ │ │ └── orderHistoryCard.isml │ │ ├── common │ │ └── htmlHead.isml │ │ └── search │ │ └── suggestions.isml │ └── .project ├── .stylelintrc.json ├── documentation ├── Twicpics SFRA Documentation.docx ├── Twicpics SFRA Documentation.pdf └── ~$icpics SFRA Documentation.docx ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── package.json ├── webpack.config.js ├── README.md └── metadata └── system-objecttype-extensions.xml /.eslintignore: -------------------------------------------------------------------------------- 1 | documentation/ -------------------------------------------------------------------------------- /cartridges/int_twicpics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": "./hooks.json" 3 | } 4 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "plugins": [ 4 | "stylelint-scss" 5 | ] 6 | } -------------------------------------------------------------------------------- /documentation/Twicpics SFRA Documentation.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwicPics/sfcc/HEAD/documentation/Twicpics SFRA Documentation.docx -------------------------------------------------------------------------------- /documentation/Twicpics SFRA Documentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwicPics/sfcc/HEAD/documentation/Twicpics SFRA Documentation.pdf -------------------------------------------------------------------------------- /documentation/~$icpics SFRA Documentation.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwicPics/sfcc/HEAD/documentation/~$icpics SFRA Documentation.docx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/static/default/css/ 3 | **/static/default/js/ 4 | *.code-workspace 5 | dw.json 6 | .DS_Store 7 | .vscode/ 8 | package-lock.json 9 | *.tern-project -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/templates/default/components/header/twicpicsHeader.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": [ 3 | { 4 | "name": "app.template.htmlHead", 5 | "script": "./cartridge/scripts/hooks/htmlhead.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/int_twicpics.properties: -------------------------------------------------------------------------------- 1 | ## cartridge.properties for cartridge int_twicpics 2 | #Fri May 18 15:38:12 CEST 2021 3 | demandware.cartridges.int_twicpics.multipleLanguageStorefront=true 4 | demandware.cartridges.int_twicpics.id=int_twicpics -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/int_twicpics_sfra.properties: -------------------------------------------------------------------------------- 1 | ## cartridge.properties for cartridge twicpics 2 | #Wed May 2 11:0:0 CEST 2021 3 | demandware.cartridges.twicpics.multipleLanguageStorefront=true 4 | demandware.cartridges.twicpics.id=int_twicpics_sfra -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/twicpics_sfra_changes.properties: -------------------------------------------------------------------------------- 1 | ## cartridge.properties for cartridge twicpics_sfra_changes 2 | #Tue Jul 17 15:25:4 CEST 2021 3 | demandware.cartridges.twicpics_sfra_changes.multipleLanguageStorefront=true 4 | demandware.cartridges.twicpics_sfra_changes.id=twicpics_sfra_changes -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/client/default/js/productDetail.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 'use strict'; 3 | 4 | var processInclude = require('base/util'); 5 | 6 | $(document).ready(function () { 7 | processInclude(require('base/product/detail')); 8 | processInclude(require('./product/base')); 9 | }); 10 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/client/default/js/productTile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 'use strict'; 3 | 4 | var processInclude = require('base/util'); 5 | 6 | $(document).ready(function () { 7 | processInclude(require('base/product/quickView')); 8 | processInclude(require('./product/base')); 9 | }); 10 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/client/default/js/search.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 'use strict'; 3 | 4 | var processInclude = require('base/util'); 5 | 6 | $(document).ready(function () { 7 | processInclude(require('base/search/search')); 8 | processInclude(require('base/product/quickView')); 9 | processInclude(require('./product/base')); 10 | }); 11 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/shopTheLook/imageFile.isml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | int_twicpics 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | int_twicpics_sfra 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | twicpics_sfra_changes 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base/legacy", 4 | "rules": { 5 | "import/no-unresolved": "off", 6 | "indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1 }], 7 | "func-names": "off", 8 | "require-jsdoc": "error", 9 | "valid-jsdoc": ["error", { "preferType": { "Boolean": "boolean", "Number": "number", "object": "Object", "String": "string" }, "requireReturn": false}], 10 | "vars-on-top": "off", 11 | "global-require": "off", 12 | "no-shadow": ["error", { "allow": ["err", "callback"]}], 13 | "max-len": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/client/default/scss/twicpics.scss: -------------------------------------------------------------------------------- 1 | .twic-img { 2 | position: relative; 3 | width: 100%; 4 | background-size: cover; 5 | background-position: center; 6 | background-repeat: no-repeat; 7 | padding-top: 100%; 8 | } 9 | 10 | .twic-img--fade > img { 11 | transition-property: opacity; 12 | will-change: opacity; 13 | opacity: 0; 14 | } 15 | .twic-img--fade > img.twic-done { 16 | opacity: 1; 17 | } 18 | 19 | .twic-img > img { 20 | position: absolute; 21 | display: block; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | object-fit: cover; 27 | } 28 | 29 | .swatches div { 30 | display : inherit; 31 | } -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/scripts/hooks/htmlhead.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ISML = require('dw/template/ISML'); 4 | 5 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 6 | 7 | /** 8 | * custom hook SFRA 9 | */ 10 | function htmlHead() { 11 | if ( 12 | twicpicsHelpers.isTwicpicsEnabled() !== 'advanced' 13 | && twicpicsHelpers.isTwicpicsEnabled() !== 'expert' 14 | ) { 15 | return; 16 | } 17 | 18 | var externalJS = twicpicsHelpers.twicpicsExternalJS(); 19 | 20 | ISML.renderTemplate('components/header/twicpicsHeader', { 21 | externalJS: externalJS 22 | }); 23 | } 24 | 25 | module.exports = { 26 | htmlHead: htmlHead 27 | }; 28 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/campaignBanner.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 |
8 |
9 | 16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/controllers/Twicpics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('server'); 4 | 5 | server.get('Image', function (req, res, next) { 6 | var TwicImgModel = require('*/cartridge/models/twicImg'); 7 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 8 | var ImageTwicpics = require('*/cartridge/scripts/helpers/ImageTwicpics'); 9 | var version = twicpicsHelpers.isTwicpicsEnabled(); 10 | var imgObj = req.querystring; 11 | 12 | if (!Object.hasOwnProperty.call(imgObj, 'src')) { 13 | throw new Error('Src field is required.'); 14 | } 15 | 16 | if (version === 'expert') { 17 | imgObj = new TwicImgModel(imgObj); 18 | } else if (version && !imgObj.src.includes(twicpicsHelpers.domain)) { 19 | var imgTwic = new ImageTwicpics(imgObj, imgObj.imageType, 'large'); 20 | imgObj.src = imgTwic.url; 21 | } 22 | 23 | res.render('twicpics', { 24 | twicpicsImg: imgObj, 25 | version: version 26 | }); 27 | next(); 28 | }); 29 | 30 | module.exports = server.exports(); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ecocea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/product/components/productTileImage.isml: -------------------------------------------------------------------------------- 1 |
2 | 3 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/photoTile.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addCss('/css/experience/components/commerceAssets/photoTile.css'); 4 | 5 |
6 |
7 |
8 |
9 | 10 | 11 | 12 | 24 | 25 |
26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/cart/productCard/cartProductCardBundledItems.isml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | ${bundledLineItem.productName} 5 |
6 |
7 |
8 | 16 |
17 |
18 | 19 |

${attribute.displayName}: ${attribute.displayValue}

20 |
21 |
22 | 23 |
${option}
24 |
25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "int_twicpics_sfra", 3 | "version": "21.06.1", 4 | "description": "New overlay cartridge", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "sgmf-scripts --lint js", 8 | "upload": "sgmf-scripts --upload -- ", 9 | "uploadCartridge": "sgmf-scripts --uploadCartridge int_twicpics && sgmf-scripts --uploadCartridge int_twicpics_sfra", 10 | "compile:js": "sgmf-scripts --compile js", 11 | "compile:scss": "sgmf-scripts --compile css", 12 | "watch": "sgmf-scripts --watch", 13 | "test": "sgmf-scripts --test test/unit/**/*.js" 14 | }, 15 | "dependencies": { 16 | "cleave.js": "~1.4.2" 17 | }, 18 | "devDependencies": { 19 | "chai": "^3.5.0", 20 | "css-loader": "^0.28.11", 21 | "eslint": "^3.19.0", 22 | "eslint-config-airbnb-base": "^5.0.3", 23 | "eslint-plugin-import": "^1.16.0", 24 | "istanbul": "^0.4.5", 25 | "mocha": "^5.2.0", 26 | "node-sass": "^4.12.0", 27 | "postcss-loader": "^2.1.6", 28 | "properties-parser": "^0.3.1", 29 | "proxyquire": "1.7.4", 30 | "sass-loader": "^7.3.1", 31 | "sgmf-scripts": "^2.4.1", 32 | "sinon": "^1.17.7", 33 | "stylelint": "^8.4.0", 34 | "stylelint-config-standard": "^17.0.0", 35 | "stylelint-scss": "^2.5.0" 36 | }, 37 | "paths": { 38 | "base": "../storefront-reference-architecture/cartridges/app_storefront_base" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var ExtractTextPlugin = require('sgmf-scripts')['extract-text-webpack-plugin']; 3 | var sgmfScripts = require('sgmf-scripts'); 4 | 5 | module.exports = [ 6 | { 7 | mode: 'development', 8 | name: 'js', 9 | entry: sgmfScripts.createJsPath(), 10 | output: { 11 | path: path.resolve('./cartridges/int_twicpics_sfra/cartridge/static'), 12 | filename: '[name].js' 13 | } 14 | }, 15 | { 16 | mode: 'none', 17 | name: 'scss', 18 | entry: sgmfScripts.createScssPath(), 19 | output: { 20 | path: path.resolve('./cartridges/int_twicpics_sfra/cartridge/static'), 21 | filename: '[name].css' 22 | }, 23 | module: { 24 | rules: [{ 25 | test: /\.scss$/, 26 | use: ExtractTextPlugin.extract({ 27 | use: [{ 28 | loader: 'css-loader', 29 | options: { 30 | url: false 31 | } 32 | }, { 33 | loader: 'postcss-loader', 34 | options: { 35 | plugins: [ 36 | require('autoprefixer')() 37 | ] 38 | } 39 | }, { 40 | loader: 'sass-loader' 41 | }] 42 | }) 43 | }] 44 | }, 45 | plugins: [ 46 | new ExtractTextPlugin({ filename: '[name].css' }) 47 | ] 48 | } 49 | ]; 50 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/controllers/Product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('server'); 4 | var page = module.superModule; 5 | 6 | server.extend(page); 7 | 8 | var cache = require('*/cartridge/scripts/middleware/cache'); 9 | 10 | server.append('ShowQuickView', cache.applyPromotionSensitiveCache, function (req, res, next) { 11 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 12 | var viewData = res.getViewData(); 13 | var product = viewData.product; 14 | var images = product.images; 15 | var version = twicpicsHelpers.isTwicpicsEnabled(); 16 | 17 | Object.keys(images).forEach(function (viewType) { 18 | for (var i = 0; i < images[viewType].length; i++) { 19 | var image = images[viewType][i]; 20 | image.twicpicsImg = { 21 | class: 'd-block img-fluid', 22 | title: image.title, 23 | src: image.url, 24 | urlImg: image.url, 25 | alt: image.alt + ' image number ' + image.index, 26 | itemprop: 'image', 27 | bgStyle: image.twicImg ? image.twicImg.bgStyle : undefined, 28 | transition: image.twicImg ? image.twicImg.transition : undefined, 29 | twicSrc: image.twicImg ? image.twicImg.twicSrc.replace('data-twic-src=', '') : undefined, 30 | apiRatio: image.twicImg ? image.twicImg.apiRatio : undefined 31 | }; 32 | } 33 | }); 34 | res.setViewData({ 35 | product: product, 36 | version: version, 37 | isShowQuickView: true 38 | }); 39 | next(); 40 | }); 41 | 42 | module.exports = server.exports(); 43 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/productCard/productShippingCard.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | 7 |
${Resource.msg('msg.bundle.includes', 'cart', null)}
8 | 9 | 10 | 11 |
12 |
13 | 21 |
22 |
23 | 24 |

${attribute.displayName}: ${attribute.displayValue}

25 |
26 | 27 |
28 | 29 |
${option.displayName}
30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TwicPics Cartridge for Salesforce Commerce Cloud 2 | 3 | TwicPics is a real-time image processing service that enables businesses of all sizes to deliver high performing, rich visual content with an easy setup. 4 | 5 | ## What is TwicPics? 6 | Websites are heavier than ever and the main culprits are images. They eat up network bandwidth and increase the time visitors spend waiting for pages to load. because every passing tenth of a second reduces your website's overall conversion rate, this dramatically impacts revenue. 7 | 8 | With all kinds of devices, screen sizes and pixel densities out there, tailoring images is extremely challenging. Depending on the approach, it can complicate server-side architectures by an order of magnitude, makes for convoluted deployment strategies or just kill client-side performance. This is where TwicPics comes in. 9 | 10 | TwicPics is a Responsive Image Service Solution (SaaS). It offers on-demand responsive image generation combined with a smart and unobtrusive javascript library, all based around a no-nonsense, testable, URL-based API. 11 | 12 | Through TwicPics, developers always point to the biggest, most detailed version of the media. Yet, end-users never see this master image. Instead, an optimized, perfectly sized, device-adapted version is delivered from a server closest to them. 13 | 14 | ## Create your Twicpics Account 15 | 16 | If you don't have a Twicpics account, sign up for free account on [Twicpics site](https://www.twicpics.com). 17 | 18 | ## Configuring the cartridges 19 | 20 | Follow the [integration guide](documentation). 21 | 22 | ## Link 23 | 24 | * [Twicpics site](https://www.twicpics.com/) 25 | * [Documentation](https://www.twicpics.com/documentation/overview/) 26 | 27 | ## Compatibility 28 | 29 | SFRA 6.0.0 30 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/slots/category/categoryTile.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Render a tile of the category Name and the Category Landing Slot Image field 4 | 5 |
6 | 7 |

8 | 9 |

10 |
11 | 12 |
13 | 14 | 15 | 31 | 32 | 33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/product/components/productTileSwatch.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | ... 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/productCard/uncategorizedProductCard.isml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 |
${Resource.msg('msg.bundle.includes','cart',null)}
13 | 14 | 15 | 16 |
17 |
18 | 26 |
27 |
28 | ${Resource.msg('error.removed.online.catalog', 'cart', null)} 29 |
30 |
31 |
32 |

33 | 34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 |
-------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/cart/productCard/uncategorizedCartProductCard.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 | 23 |
24 |
25 | ${Resource.msg('error.removed.online.catalog', 'cart', null)} 26 |
27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 |
-------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/models/product/productImages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var collections = require('*/cartridge/scripts/util/collections'); 4 | var ImageTwicpics = require('*/cartridge/scripts/helpers/ImageTwicpics'); 5 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 6 | 7 | /** 8 | * @constructor 9 | * @classdesc Returns images for a given product 10 | * @param {dw.catalog.Product} product - product to return images for 11 | * @param {Object} imageConfig - configuration object with image types 12 | */ 13 | function Images(product, imageConfig) { 14 | imageConfig.types.forEach(function (type, index) { 15 | var result = {}; 16 | 17 | if (imageConfig.quantity === 'single') { 18 | var firstImage = new ImageTwicpics(product, twicpicsHelpers.IMAGE_TYPE.CATALOG, type, index); 19 | if (firstImage) { 20 | result = [{ 21 | alt: firstImage.alt, 22 | url: firstImage.url, 23 | title: firstImage.title, 24 | index: firstImage.index 25 | }]; 26 | } 27 | } else { 28 | var images = ImageTwicpics.getImages(product, twicpicsHelpers.IMAGE_TYPE.CATALOG, type, index); 29 | result = collections.map(images, function (image) { 30 | return { 31 | alt: image.alt, 32 | url: image.url, 33 | title: image.title, 34 | index: image.index, 35 | twicImg: image.expertMode ? { 36 | twicSrc: image.twicSrc ? 'data-twic-src=' + image.twicSrc : '', 37 | twicFocus: image.twicFocus ? 'data-twic-focus=' + image.twicFocus : '', 38 | twicStep: image.twicStep ? 'data-twic-step=' + image.twicStep : '', 39 | bgStyle: image.bgStyle, 40 | apiRatio: image.apiRatio, 41 | transition: image.transition ? 'twic-img--fade' : '', 42 | imgStyle: image.imgStyle 43 | } : null 44 | }; 45 | }); 46 | } 47 | this[type] = result; 48 | }, this); 49 | } 50 | 51 | module.exports = Images; 52 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/templates/default/twicpics.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | style="${twicpicsImg.imgStyle}" 9 | alt="${!twicpicsImg.alt ? twicpicsImg.src.split(/[?#]/).shift().split('/').pop().split('.').shift() : twicpicsImg.alt}" 10 | src="${twicpicsImg.urlImg}?twic=v1/cover=${twicpicsImg.apiRatio}" 11 | width="${twicpicsImg.width}" 12 | height="${twicpicsImg.height}" 13 | class="${twicpicsImg.class}" 14 | title="${twicpicsImg.title}" 15 | data-index="${twicpicsImg.dataIndex}" 16 | itemprop="${twicpicsImg.itemprop}" 17 | data-twic-src="${twicpicsImg.twicSrc}" 18 | data-twic-focus="${twicpicsImg.twicFocus}" 19 | data-twic-step="${twicpicsImg.twicStep}" 20 | > 21 |
22 | 23 | style="${twicpicsImg.style}"
26 | alt="${twicpicsImg.alt}" 27 | height="${twicpicsImg.height}" 28 | width="${twicpicsImg.width}" 29 | class="${twicpicsImg.class}" 30 | title="${twicpicsImg.title}" 31 | data-index="${twicpicsImg.dataIndex}" 32 | itemprop="${twicpicsImg.itemprop}" 33 | > 34 | 35 | 36 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/mainBanner.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addCss('/css/experience/components/commerceAssets/imageAndTextCommon.css'); 4 | 5 | 6 | 46 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/productListTile.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addCss('/css/experience/components/commerceAssets/imageAndTextCommon.css'); 4 | 5 | 6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/product/components/imageCarousel.isml: -------------------------------------------------------------------------------- 1 |
2 | 42 |
-------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/imageAndText.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addCss('/css/experience/components/commerceAssets/imageAndTextCommon.css'); 4 | 5 | 6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/billing/storedPaymentInstruments.isml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 12 |
13 | 14 | 15 | 22 | 23 | 25 |
${Resource.msg('error.message.security.code.required', 'checkout', null)}
26 |
27 |
28 |
29 |
30 | 31 | ${Resource.msg('msg.payment.type.credit', 'confirmation', null)} 32 | ${paymentInstrument.creditCardType} 33 | 34 |
35 |
36 | ${paymentInstrument.maskedCreditCardNumber} 37 |
38 |
39 | 40 | ${Resource.msg('msg.card.type.ending', 'confirmation', null)} 41 | ${paymentInstrument.creditCardExpirationMonth}/${paymentInstrument.creditCardExpirationYear} 42 | 43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/product/components/choiceOfBonusProducts/chooseBonusProduct.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 |
9 |
10 |
11 | 19 |
20 |
21 |
${potentialProduct.productName}

23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | 56 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/cart/productCard/cartNestedBonusProductCard.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 |
${productLineItem.productName}
4 |
5 |
6 |
7 |
8 |
9 |
10 | 18 |
19 |
20 | 21 |

${attribute.displayName}: ${attribute.displayValue}

22 |
23 |
24 | 25 |
${option.displayName}
26 |
27 |
28 |
29 |
30 |
31 |
32 |

33 | 55 |
56 |
-------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/cart/productCard/cartProductCard.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 | 22 |
23 |
24 | 25 |

${attribute.displayName}: ${attribute.displayValue}

26 |
27 | 28 | 29 |
30 |

${option.displayName}

31 |
32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 |

42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/experience/components/commerce_assets/product/productTile.isml: -------------------------------------------------------------------------------- 1 | 2 | var assets = require('*/cartridge/scripts/assets.js'); 3 | assets.addCss('/css/experience/components/commerceAssets/productTile.css'); 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 | 13 | 32 | 33 | 34 |
35 |
36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 | 59 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/account/order/orderHistoryCard.isml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

${Resource.msg('label.orderhistory','account',null)}

5 | ${Resource.msg('link.view','account',null)} 6 |
7 | 8 |
9 |

${Resource.msg('label.orderhistory.orderno','account',null)} ${order.orderNumber}

10 | ${Resource.msg('link.view','account',null)} 11 |
12 |
13 |
14 |
15 | 16 |
17 | ${Resource.msg('label.orderhistory.mostrecentorder','account',null)} 18 |
19 |
20 |
21 | 29 |
30 |
31 | 32 |

${Resource.msg('label.orderhistory.ordernumber','account',null)}

33 |
34 |

${Resource.msg('label.orderhistory.dateordered','account',null)}

35 |

${Resource.msg('label.orderhistory.orderstatus','account',null)} ${order.orderStatus}

36 | 37 |
38 |
39 |
40 | 56 |
57 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/productCard/embeddedBonusProduct.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | ${nestedLineItem.productName} 6 |
7 |
8 |
9 |
10 |
11 | 19 |
20 |
21 | 22 |

${attribute.displayName}: ${attribute.displayValue}

23 |
24 | 25 |
26 | 27 |
${option.displayName}
28 |
29 |
30 |
31 |
32 |
33 | 34 |
35 | 36 |

37 | 38 | ${Resource.msg('label.each.item.price','cart',null)} 39 |
40 |

41 | 42 | 43 | ${nestedLineItem.bonusUnitPrice} 44 | 45 | 46 |
47 |

48 |
49 |
50 | 51 |
52 |

53 | 54 | ${Resource.msg('field.selectquantity','cart',null)} 55 |
56 | 57 |

58 |
59 | 60 |
61 |

62 | 63 | ${Resource.msg('label.total.price','cart',null)} 64 | 65 |

66 |
67 |
${nestedLineItem.priceTotal.price}
68 |
69 |
70 |
71 |
-------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/common/htmlHead.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page meta data will be replaced by rule based content if it's available 8 | 9 | ${pdict.CurrentPageMetaData.title} | ${Resource.msg('global.site.name', 'version', null)} | ${Resource.msg('global.version.number', 'version', null)} 10 | 11 | <isprint value="${pdict.CurrentPageMetaData.title}" encoding="htmlcontent" /> 12 | 13 | 14 | 15 | 16 | 17 | Additional Rule based page meta tags 18 | 19 | 20 | " content=""> 21 | 22 | " content=""> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | integrity="${style.integrity}" crossorigin="anonymous" /> 36 | 37 | 38 | 39 | " /> 40 | 41 | 42 | 43 | hook for Marketing Cloud connector & other integration which need to inject 44 | logic or markup into the page header 45 | IMPORTANT: Note that this hook will be called to cached as well as uncached pages 46 | Refrain from calling processing intensive logic in here 47 | do not include shopper specific information. 48 | 49 | ${dw.system.HookMgr.callHook('app.template.htmlHead', 'htmlHead', pdict) || ''} 50 | 51 | Module to overlay skinning css 52 | IMPORTANT: Note that this module will be called to cached as well as uncached pages 53 | Refrain from calling processing intensive logic in here 54 | do not include shopper specific information. 55 | This hook will be executed with encoding disabled. Please sanitize output in the hook. 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/productCard/productCard.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 |
${Resource.msg('msg.bundle.includes','cart',null)}
12 | 13 | 14 | 15 |
16 |
17 | 25 |
26 |
27 | 28 |

${attribute.displayName}: ${attribute.displayValue}

29 |
30 | 31 |
32 | 33 |
${option.displayName}
34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 |

45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 |

53 | 54 | ${Resource.msg('label.each.item.price','cart',null)} 55 |
56 | 57 |

58 | 59 |
60 |

61 |
62 |
63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 |
73 |

74 | 75 | ${Resource.msg('label.total.price','cart',null)} 76 | 77 |

78 |
79 | 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/checkout/productCard/bonusProductCard.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 |
${Resource.msg('msg.bundle.includes','cart',null)}
12 | 13 | 14 | 15 |
16 |
17 | 25 |
26 |
27 | 28 |

${attribute.displayName}: ${attribute.displayValue}

29 |
30 | 31 |
32 | 33 |
${option}
34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 |

45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 |

53 | 54 | ${Resource.msg('label.each.item.price','cart',null)} 55 |
56 | 57 | 58 |

59 |
60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 |
71 |

72 | 73 | ${Resource.msg('label.total.price','cart',null)} 74 | 75 |

76 |
77 | 78 |
79 |
80 |
81 | 82 |
83 | 84 |
85 |
86 | 87 | 88 | 89 | ${Resource.msg('text.lineitem.bonus.msg', 'cart', null)}: 90 | 91 | 92 | 93 | 94 |
95 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/scripts/helpers/twicpicsHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global request */ 4 | 5 | var Site = require('dw/system/Site'); 6 | var File = require('dw/io/File'); 7 | var domain = Site.current.getCustomPreferenceValue('TWICHostname'); 8 | 9 | var IMAGE_TYPE = { 10 | CATALOG: 'CATALOG', 11 | SITE: 'SITE', 12 | CONTENT: 'CONTENT', 13 | STATIC: 'STATIC' 14 | }; 15 | 16 | /** 17 | * Get if Twicpics is enabled in standard, advanced or expert mode 18 | * @returns {string} standard, advanced, expert or empty 19 | */ 20 | function isTwicpicsEnabled() { 21 | var isEnabled = Site.current.getCustomPreferenceValue('TWICEnabled'); 22 | 23 | if (isEnabled) return isEnabled.value; 24 | 25 | return ''; 26 | } 27 | 28 | /** 29 | * Get the Twicpics external JS link 30 | * @returns {string} external JS link 31 | */ 32 | function twicpicsExternalJS() { 33 | return Site.current.getCustomPreferenceValue('TWICExternalJS'); 34 | } 35 | 36 | /** 37 | * Get the path of the original image 38 | * Find the locale into the image URL, split it and get the path after the Akamai cache 39 | * /on/demandware.static/-/Sites/default/dwfd0554ac/images/medium/PG.10254489.JJ3WCXX.PZ.jpg 40 | * give images/medium/PG.10254489.JJ3WCXX.PZ.jpg 41 | * @param {string} src url of the image without simplification 42 | * @param {string} type image type (CATALOG, STATIC, CONTENT, SITE) 43 | * @return {string} clean image path 44 | */ 45 | function getOriginalImage(src, type) { 46 | var locale = request.locale; 47 | var localeWithoutCountry = locale.split('_')[0]; 48 | var split = ''; 49 | 50 | if (src.indexOf(File.SEPARATOR + locale + File.SEPARATOR) !== -1) { 51 | split = File.SEPARATOR + locale + File.SEPARATOR; 52 | } else if (src.indexOf(File.SEPARATOR + localeWithoutCountry + File.SEPARATOR) !== -1) { 53 | split = File.SEPARATOR + localeWithoutCountry + File.SEPARATOR; 54 | locale = localeWithoutCountry; 55 | } else if (src.indexOf('/default/') !== -1) { 56 | split = '/default/'; 57 | locale = 'default'; 58 | } else { 59 | return ''; 60 | } 61 | 62 | var splitImage = src.split(split)[1].split(File.SEPARATOR); 63 | 64 | splitImage.shift(); 65 | 66 | if (type !== IMAGE_TYPE.CATALOG) { 67 | splitImage.unshift(locale); 68 | } 69 | 70 | return splitImage.join(File.SEPARATOR); 71 | } 72 | 73 | 74 | /** 75 | * Get the Twicpics catalog path 76 | * @param {string} src original image url 77 | * @returns {string} catalog path 78 | */ 79 | function getCatalogPath(src) { 80 | var catalogsPath = Site.current.getCustomPreferenceValue('TWICCatalogPath'); 81 | var catalogPath = null; 82 | 83 | if (!catalogsPath || catalogsPath.length === 0) throw new Error('The field TWICCatalogPath is empty'); 84 | 85 | catalogsPath.forEach(function (catalog) { 86 | if (src.indexOf(catalog) !== -1) { 87 | catalogPath = catalog; 88 | } 89 | }); 90 | 91 | return catalogPath; 92 | } 93 | 94 | /** 95 | * Get the Twicpics content path 96 | * @returns {string} content path 97 | */ 98 | function getContentPath() { 99 | var contentPath = Site.current.getCustomPreferenceValue('TWICContentPath'); 100 | 101 | if (!contentPath) throw new Error('The field TWICContentPath is empty'); 102 | 103 | return contentPath; 104 | } 105 | 106 | /** 107 | * Get the Twicpics static path 108 | * @returns {string} static path 109 | */ 110 | function getStaticPath() { 111 | var staticPath = Site.current.getCustomPreferenceValue('TWICStaticPath'); 112 | 113 | if (!staticPath) throw new Error('The field TWICStaticPath is empty'); 114 | 115 | return staticPath; 116 | } 117 | 118 | /** 119 | * Get the Twicpics static path 120 | * @returns {string} static path 121 | */ 122 | function getSitePath() { 123 | var sitePath = Site.current.getCustomPreferenceValue('TWICSitePath'); 124 | 125 | if (!sitePath) throw new Error('The field TWICSitePath is empty'); 126 | 127 | return sitePath; 128 | } 129 | 130 | /** 131 | * Get Image Path 132 | * @param {string} imageType image type (CONTENT/SITE/STATIC/CATALOG) 133 | * @param {string} src original image url 134 | * @returns {string} image path 135 | */ 136 | function getImagePath(imageType, src) { 137 | var path; 138 | 139 | switch (imageType) { 140 | case IMAGE_TYPE.SITE : 141 | path = getSitePath(); 142 | break; 143 | case IMAGE_TYPE.CONTENT : 144 | path = getContentPath(); 145 | break; 146 | case IMAGE_TYPE.STATIC : 147 | path = getStaticPath(); 148 | break; 149 | case IMAGE_TYPE.CATALOG : 150 | path = getCatalogPath(src); 151 | break; 152 | default : path = ''; 153 | } 154 | 155 | return path; 156 | } 157 | 158 | /** 159 | * Set an URL string on the Twicpics format 160 | * Example : /on/demandware.static/-/Sites/default/dwfd0554ac/images/medium/PG.10254489.JJ3WCXX.PZ.jpg 161 | * gives https://[host]/catalogs/images/medium/PG.10254489.JJ3WCXX.PZ.jpg 162 | * @param {string} src original image url 163 | * @param {string} imageType image type (CONTENT/SITE/STATIC/CATALOG) 164 | * @returns {string} Twicpics URL 165 | */ 166 | function getTwicpicsImageUrl(src, imageType) { 167 | if (!domain) throw new Error('The field TWICHostname is empty'); 168 | 169 | if (src.includes(domain)) { return src; } 170 | 171 | var imagePath = getImagePath(imageType, src); 172 | var originalImage = getOriginalImage(src, imageType); 173 | 174 | return [domain, imagePath, originalImage].join(File.SEPARATOR); 175 | } 176 | 177 | 178 | /** 179 | * getTwicSrcImage gets the content of tag data-twic-src 180 | * Example: catalogs/images/medium/90011212_001_0.jpg 181 | * @param {string} src inital image src 182 | * @returns {string} src complients with tag data-twic-src 183 | */ 184 | function getTwicSrcImage(src) { 185 | if (!domain) throw new Error('The field TWICHostname is empty'); 186 | return src.replace(domain, ''); 187 | } 188 | 189 | module.exports = { 190 | isTwicpicsEnabled: isTwicpicsEnabled, 191 | twicpicsExternalJS: twicpicsExternalJS, 192 | getOriginalImage: getOriginalImage, 193 | getTwicpicsImageUrl: getTwicpicsImageUrl, 194 | getTwicSrcImage: getTwicSrcImage, 195 | domain: domain, 196 | IMAGE_TYPE: IMAGE_TYPE 197 | }; 198 | -------------------------------------------------------------------------------- /metadata/system-objecttype-extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Twicpics Catalog Path 7 | Path for the catalog mapping on Twicpics 8 | set-of-string 9 | false 10 | false 11 | 12 | 13 | Twicpics Content Path 14 | Path for the content mapping on Twicpics 15 | string 16 | false 17 | false 18 | 0 19 | 20 | 21 | Twicpics Enabled 22 | Enable or disable the Twicpics cartridge 23 | enum-of-string 24 | false 25 | false 26 | 27 | 28 | Standard 29 | standard 30 | 31 | 32 | Advanced 33 | advanced 34 | 35 | 36 | Expert 37 | expert 38 | 39 | 40 | 41 | 42 | Twicpics Hostname 43 | The hostname to make the Twicpics URL with HTTP protocol 44 | string 45 | true 46 | false 47 | 0 48 | 49 | 50 | Twicpics Script Version 51 | Refer to https://www.twicpics.com/documentation/script-setup/ 52 | Example: v1 53 | string 54 | true 55 | false 56 | 0 57 | 58 | 59 | Twicpics Site Path 60 | Path for the site mapping on Twicpics (example : image into site preferences) 61 | string 62 | false 63 | false 64 | 0 65 | 66 | 67 | Twicpics Static Path 68 | Path for the static mapping on Twicpics 69 | string 70 | false 71 | false 72 | 0 73 | 74 | 75 | Twicpics Transformations 76 | JSON configuration to render the Twicpics images 77 | text 78 | false 79 | false 80 | 81 | 82 | Twicpics External JS Link 83 | https://{subdomain}.twic.pics/?v1 84 | string 85 | false 86 | false 87 | 0 88 | 89 | 90 | 91 | 92 | Twicpics 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/models/twicImg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var Logger = require('dw/system/Logger').getLogger('Twicpics', 'Twicpics'); 5 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 6 | var URLUtils = require('dw/web/URLUtils'); 7 | 8 | /** 9 | * Model TwicImg 10 | * @param {Object} args arguments to create Twicpics Image object 11 | */ 12 | function TwicImg(args) { 13 | try { 14 | this.src = this.initSrc(args.src); 15 | this.imageType = this.initImageType(args.imageType); 16 | this.alt = this.initAlt(args.alt); 17 | this.width = this.initWidth(args.width); 18 | this.height = this.initHeight(args.height); 19 | this.placeholder = this.initPlaceholder(args.placeholder); 20 | this.ratio = this.initRatio(args.ratio); 21 | this.step = this.initStep(args.step); 22 | this.focus = this.initFocus(args.focus); 23 | this.transition = this.initTransition(args.transition); 24 | this.class = this.initClass(args.class); 25 | this.title = this.initTitle(args.title); 26 | this.itemprop = this.initItemprop(args.itemprop); 27 | this.dataIndex = this.initDataIndex(args.dataIndex); 28 | this.transitionDelay = this.initTransitionDelay(args.transitionDelay); 29 | this.transitionDuration = this.initTransitionDuration(args.transitionDuration); 30 | this.transitionTimingFunction = this.initTransitionTimingFunction(args.transitionTimingFunction); 31 | this.style = this.initStyle(args.style); 32 | this.first = this.initFirst(args.first); 33 | this.last = this.initLast(args.last); 34 | this.urlImg = this.urlImg(); 35 | this.apiRatio = this.apiRatio(); 36 | this.apiOutput = this.apiOutput(); 37 | this.apiFocus = this.focus; 38 | this.twicStep = this.step; 39 | this.twicFocus = this.focus; 40 | this.twicSrc = this.initTwicSrc(); 41 | this.imgStyle = this.imgStyle(); 42 | this.paddingRatio = this.paddingRatio(); 43 | this.bgStyle = this.bgStyle(); 44 | } catch (e) { 45 | this.src = URLUtils.staticURL('images/noimagelarge.png').toString(); 46 | 47 | Logger.error('An error occured on the Twicpics cartridge : {0}', e.message); 48 | } 49 | } 50 | 51 | TwicImg.prototype.initSrc = function (src) { 52 | if (!src || typeof src !== 'string') { 53 | Logger.error('Wrong \'src\' format. \'src\' is required and must contain an absolute or relative path to an image.'); 54 | throw new Error('Wrong \'src\' format. \'src\' is required and must contain an absolute or relative path to an image.'); 55 | } 56 | return src; 57 | }; 58 | 59 | TwicImg.prototype.initImageType = function (type) { 60 | if (!type || typeof type !== 'string' || [twicpicsHelpers.IMAGE_TYPE.SITE, twicpicsHelpers.IMAGE_TYPE.STATIC, twicpicsHelpers.IMAGE_TYPE.CONTENT, twicpicsHelpers.IMAGE_TYPE.CATALOG].includes(type)) { 61 | Logger.error('Wrong image type, it must be SITE, STATIC, CONTENT or CATALOG'); 62 | } 63 | return type; 64 | }; 65 | 66 | TwicImg.prototype.initAlt = function (alt) { 67 | return typeof alt === 'string' ? alt : undefined; 68 | }; 69 | 70 | TwicImg.prototype.initPlaceholder = function (placeholder) { 71 | return ['preview', 'meancolor', 'maincolor', 'none'].includes(placeholder) ? placeholder : 'preview'; 72 | }; 73 | 74 | TwicImg.prototype.initWidth = function (width) { 75 | return (typeof width === 'string' || typeof width === Number) && /\d+/.test(width) ? width : undefined; 76 | }; 77 | 78 | TwicImg.prototype.initHeight = function (height) { 79 | return (typeof height === 'string' || typeof height === Number) && /\d+/.test(height) ? height : undefined; 80 | }; 81 | 82 | TwicImg.prototype.initRatio = function (ratio) { 83 | return typeof ratio === 'string' && /\d+\/\d+/.test(ratio) ? ratio : undefined; 84 | }; 85 | 86 | TwicImg.prototype.initFocus = function (focus) { 87 | return typeof focus === 'string' ? focus : undefined; 88 | }; 89 | 90 | TwicImg.prototype.initStep = function (step) { 91 | return (typeof step === 'string' || typeof step === Number) && /\d+/.test(step) ? step : undefined; 92 | }; 93 | 94 | TwicImg.prototype.initTransition = function (transition) { 95 | return typeof transition === Boolean ? transition : true; 96 | }; 97 | 98 | TwicImg.prototype.initTransitionDuration = function (transitionDuration) { 99 | return typeof transitionDuration === 'string' ? transitionDuration : '400ms'; 100 | }; 101 | 102 | TwicImg.prototype.initTransitionTimingFunction = function (transitionTimingFunction) { 103 | return typeof transitionTimingFunction === 'string' ? transitionTimingFunction : 'ease'; 104 | }; 105 | 106 | TwicImg.prototype.initTransitionDelay = function (transitionDelay) { 107 | return typeof transitionDelay === 'string' ? transitionDelay : '0ms'; 108 | }; 109 | 110 | TwicImg.prototype.initClass = function (argClass) { 111 | return typeof argClass === 'string' ? argClass : ''; 112 | }; 113 | 114 | TwicImg.prototype.initTitle = function (title) { 115 | return typeof title === 'string' ? title : undefined; 116 | }; 117 | 118 | TwicImg.prototype.initItemprop = function (itemprop) { 119 | return typeof itemprop === 'string' ? itemprop : undefined; 120 | }; 121 | 122 | TwicImg.prototype.initDataIndex = function (dataIndex) { 123 | return typeof dataIndex === 'string' ? dataIndex : undefined; 124 | }; 125 | 126 | TwicImg.prototype.initStyle = function (style) { 127 | return typeof style === 'string' ? style : undefined; 128 | }; 129 | 130 | TwicImg.prototype.initTwicSrc = function () { 131 | return 'image:' + twicpicsHelpers.getTwicSrcImage(this.urlImg); 132 | }; 133 | 134 | TwicImg.prototype.initFirst = function (first) { 135 | return first === 'true'; 136 | }; 137 | 138 | TwicImg.prototype.initLast = function (last) { 139 | return last === 'true'; 140 | }; 141 | 142 | TwicImg.prototype.apiRatio = function () { 143 | if (this.ratio) { 144 | // Use `ratio` if provided. 145 | return this.ratio.replace('/', ':'); 146 | } else if (this.width && this.height) { 147 | // Use 'width' and 'height' if no 'ratio'. 148 | return this.width + ':' + this.height; 149 | } 150 | // Fallback to square. 151 | return '1:1'; 152 | }; 153 | 154 | TwicImg.prototype.apiOutput = function () { 155 | return (this.placeholder !== 'none') ? this.placeholder : false; 156 | }; 157 | 158 | TwicImg.prototype.paddingRatio = function () { 159 | var r = []; 160 | if (this.ratio) { 161 | r = this.ratio.split('/'); 162 | } else { 163 | r.push(this.width || 1); 164 | r.push(this.height || 1); 165 | } 166 | return Number.parseFloat((r[1] / r[0]) * 100).toFixed(2); 167 | }; 168 | 169 | TwicImg.prototype.urlImg = function () { 170 | return twicpicsHelpers.getTwicpicsImageUrl(this.src, this.imageType); 171 | }; 172 | 173 | TwicImg.prototype.bgStyle = function () { 174 | var styles = ''; 175 | if (this.paddingRatio) { 176 | styles += 'padding-top: ' + this.paddingRatio.toString() + '%;'; 177 | } 178 | // Only provide a background image if the user asks for a placeholder. 179 | if (this.apiOutput) { 180 | var params = []; 181 | if (this.apiFocus) { params.push({ k: 'focus', v: this.apiFocus }); } 182 | if (this.apiRatio) { params.push({ k: 'cover', v: this.apiRatio }); } 183 | if (this.apiOutput) { params.push({ k: 'output', v: this.apiOutput }); } 184 | var apiParams = ''; 185 | var slash = false; 186 | params.forEach(function (element) { 187 | if (slash) { apiParams += '/'; } 188 | apiParams += element.k + '=' + element.v; 189 | slash = true; 190 | }); 191 | styles += ' background-image: url(' + this.urlImg + '?twic=v1/' + apiParams + ');'; 192 | } 193 | return styles; 194 | }; 195 | 196 | TwicImg.prototype.imgStyle = function () { 197 | if (this.transition) { 198 | var imgStyle = 'transition-duration: ' + this.transitionDuration + '; transition-timing-function: ' + this.transitionTimingFunction + '; transition-delay: ' + this.transitionDelay + ';'; 199 | return this.style ? imgStyle + ' ' + this.style + ';' : imgStyle; 200 | } 201 | return undefined; 202 | }; 203 | 204 | module.exports = TwicImg; 205 | -------------------------------------------------------------------------------- /cartridges/twicpics_sfra_changes/cartridge/templates/default/search/suggestions.isml: -------------------------------------------------------------------------------- 1 |
2 |
    3 | 4 | 5 | 6 |
  • 7 |
    ${Resource.msg('label.suggest.doyoumean', 'search', null)}
    8 |
  • 9 |
  • 10 |
    11 |
    12 | 17 |
    18 |
    19 |
  • 20 |
    21 |
    22 |
    23 | 24 |
  • 25 |
    ${Resource.msg('label.suggest.products', 'search', null)}
    26 |
  • 27 |
  • 28 | 29 | 30 | 44 | 45 | 46 |
  • 47 |
    48 | 49 |
  • 50 |
    51 | ${Resource.msg('label.suggest.categories', 'search', null)} 52 |
    53 |
  • 54 |
  • 55 | 56 | 79 | 80 |
  • 81 |
    82 | 83 |
  • 84 |
    85 | ${Resource.msg('label.suggest.recent', 'search', null)} 86 |
    87 |
  • 88 |
  • 89 | 90 |
    91 |
    92 |
    93 | ${phrase.value} 94 |
    95 |
    96 |
    97 |
    98 |
  • 99 |
    100 | 101 |
  • 102 |
    103 | ${Resource.msg('label.suggest.popular', 'search', null)} 104 |
    105 |
  • 106 |
  • 107 | 108 |
    109 |
    110 |
    111 | ${phrase.value} 112 |
    113 |
    114 |
    115 |
    116 |
  • 117 |
    118 | 119 |
  • 120 |
    121 | ${Resource.msg('label.suggest.brand', 'search', null)} 122 |
    123 |
  • 124 |
  • 125 | 126 |
    127 |
    128 |
    129 | ${phrase.value} 130 |
    131 |
    132 |
    133 |
    134 |
  • 135 |
    136 | 137 |
  • 138 |
    139 | ${Resource.msg('label.suggest.content', 'search', null)} 140 |
    141 |
  • 142 |
  • 143 | 144 |
    145 |
    146 |
    147 | ${content.name} 148 |
    149 |
    150 |
    151 |
    152 |
  • 153 |
    154 |
  • 155 | 156 |
  • 157 |
158 | 159 | ${pdict.suggestions.message} 160 | 161 | 162 | ${Resource.msg('label.header.search.assistive_msg', 'common', null)} 163 | 164 |
165 | -------------------------------------------------------------------------------- /cartridges/int_twicpics/cartridge/scripts/helpers/ImageTwicpics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 'use strict'; 3 | 4 | var URLUtils = require('dw/web/URLUtils'); 5 | var Site = require('dw/system/Site'); 6 | var ProductVariationAttributeValue = require('dw/catalog/ProductVariationAttributeValue'); 7 | var ProductVariationModel = require('dw/catalog/ProductVariationModel'); 8 | var Product = require('dw/catalog/Product'); 9 | var File = require('dw/io/File'); 10 | var Logger = require('dw/system/Logger'); 11 | var ArrayList = require('dw/util/ArrayList'); 12 | var collections = require('*/cartridge/scripts/util/collections'); 13 | var twicpicsHelpers = require('*/cartridge/scripts/helpers/twicpicsHelpers'); 14 | 15 | /** 16 | * Initializes the ImageTwicpics wrapper object 17 | * 18 | * @param {dw.catalog.Product} imageObject Product or ProductVariationAttributeValue (required) 19 | * @param {string} imageType image type to get : catalog / content / static (required) 20 | * @param {string} viewType type of view (resolution) that should be generated 21 | * @param {number} index Number position of the image in the list of images for the view type. Defaults to 0 if not provided 22 | */ 23 | function ImageTwicpics(imageObject, imageType, viewType, index) { 24 | if (!imageObject || !imageType) { 25 | return; 26 | } 27 | 28 | this.referenceType = null; 29 | this.imageURL = null; 30 | this.url = null; 31 | this.alt = null; 32 | this.title = null; 33 | this.scalableViewType = null; 34 | this.viewType = viewType; 35 | this.imageType = imageType; 36 | this.imageObject = imageObject; 37 | this.index = index || 0; 38 | this.transformations = {}; 39 | this.image = this.getImage(); 40 | 41 | if (twicpicsHelpers.isTwicpicsEnabled() !== 'standard' && twicpicsHelpers.isTwicpicsEnabled() !== 'advanced') { 42 | return; 43 | } 44 | 45 | try { 46 | this.imageURL = this.setImageURL(); 47 | this.url = this.imageURL; 48 | this.transformations = this.getTransformations(); 49 | this.imageVersion = this.setImageScriptVersion(); 50 | this.imageTransformations = this.setImageTransformations(); 51 | this.url = this.imageURL + '?' + this.imageVersion; 52 | 53 | if (this.imageTransformations) { 54 | this.url += File.SEPARATOR + this.imageTransformations; 55 | } 56 | if (this.imageType === twicpicsHelpers.IMAGE_TYPE.CATALOG) { 57 | this.alt = this.getAlt(); 58 | this.title = this.getTitle(); 59 | } 60 | } catch (e) { 61 | this.url = URLUtils.staticURL('images/noimagelarge.png').toString(); 62 | 63 | this.Logger.error('You have an error on the Twicpics cartridge : {0}', e.message); 64 | } 65 | } 66 | 67 | 68 | ImageTwicpics.prototype.scriptVersion = 'twic'; 69 | ImageTwicpics.prototype.Logger = Logger.getLogger('Twicpics', 'Twicpics'); 70 | 71 | /** 72 | * Get the Twicpics script version 73 | * @returns {string} script version 74 | */ 75 | ImageTwicpics.prototype.getScriptVersion = function () { 76 | var scriptVersion = Site.current.getCustomPreferenceValue('TWICScriptVersion'); 77 | 78 | if (!scriptVersion) throw new Error('The field TWICScriptVersion is empty'); 79 | 80 | return scriptVersion; 81 | }; 82 | 83 | /** 84 | * Parse the JSON of the organization preference TWICTransformations 85 | * @returns {Object} JSON parse 86 | */ 87 | ImageTwicpics.prototype.getTransformations = function () { 88 | var transformations = Site.current.getCustomPreferenceValue('TWICTransformations'); 89 | 90 | if (!transformations) return ''; 91 | 92 | return JSON.parse(transformations); 93 | }; 94 | 95 | ImageTwicpics.prototype.getImage = function () { 96 | var img = this.imageObject; 97 | 98 | if (this.imageType !== twicpicsHelpers.IMAGE_TYPE.CATALOG) { 99 | this.url = this.imageObject; 100 | } else { 101 | this.referenceType = 'Product'; 102 | var getImage = false; 103 | 104 | if (this.imageObject instanceof ProductVariationAttributeValue) { 105 | this.referenceType = 'ProductVariationAttributeValue'; 106 | getImage = true; 107 | } else if (this.imageObject instanceof ProductVariationModel) { 108 | this.imageObject = this.imageObject.selectedVariants.length > 0 ? this.imageObject.selectedVariants[0] : this.imageObject.master; 109 | getImage = true; 110 | } else if (this.imageObject instanceof Product) { 111 | getImage = true; 112 | } 113 | 114 | if (getImage) { 115 | img = this.imageObject.getImage(this.viewType, this.index); 116 | } 117 | 118 | 119 | if (img) { 120 | if (getImage) { 121 | this.url = img.getURL(); 122 | this.alt = img.getAlt(); 123 | this.title = img.getTitle(); 124 | } else { 125 | this.url = Object.hasOwnProperty.call(img, 'src') ? img.src : ''; 126 | this.alt = Object.hasOwnProperty.call(img, 'alt') ? img.alt : ''; 127 | this.title = Object.hasOwnProperty.call(img, 'title') ? img.title : ''; 128 | } 129 | } 130 | } 131 | 132 | return img; 133 | }; 134 | 135 | 136 | /** 137 | * Set an URL string on the Twicpics format 138 | * Example : https://[host]/catalogs/images/medium/90011212_001_0.jpg 139 | * @returns {string} Twicpics URL 140 | */ 141 | ImageTwicpics.prototype.setImageURL = function () { 142 | if (!this.image) return ''; 143 | 144 | var image = null; 145 | 146 | if (Object.hasOwnProperty.call(this.image, 'src')) { 147 | image = this.image.src; 148 | } else if (!Object.hasOwnProperty.call(this.image, 'getURL')) { 149 | image = this.image.toString(); 150 | } else { 151 | image = this.image.getURL().toString(); 152 | } 153 | 154 | return twicpicsHelpers.getTwicpicsImageUrl(image, this.imageType); 155 | }; 156 | 157 | /** 158 | * Set a string with the script version into TWICScriptVersion 159 | * Example : twic=v1 160 | * @returns {string} Twicpics format version 161 | */ 162 | ImageTwicpics.prototype.setImageScriptVersion = function () { 163 | var scriptVersion = this.getScriptVersion(); 164 | 165 | return this.scriptVersion + '=' + scriptVersion; 166 | }; 167 | 168 | /** 169 | * Set a string with all parameters into the JSON TWICTransformations 170 | * Example : resize=50p/focus=20x10 171 | * @returns {string} Twicpics format paramters 172 | */ 173 | ImageTwicpics.prototype.setImageTransformations = function () { 174 | var tab = []; 175 | var transformations = null; 176 | 177 | if (!this.transformations) return ''; 178 | 179 | if ( 180 | Object.prototype.hasOwnProperty.call(this.transformations, 'viewTypeMapping') 181 | && this.transformations.viewTypeMapping[this.viewType] 182 | ) { 183 | this.scalableViewType = this.transformations.viewTypeMapping[this.viewType]; 184 | } 185 | 186 | if (!this.scalableViewType) return ''; 187 | 188 | transformations = this.transformations[this.scalableViewType]; 189 | 190 | if (!transformations) { return ''; } 191 | 192 | Object.keys(transformations).forEach(function (transformation) { 193 | tab.push(transformation + '=' + transformations[transformation]); 194 | }); 195 | 196 | return tab.join(File.SEPARATOR); 197 | }; 198 | 199 | /** 200 | * Get text for title 201 | * @returns {string} title text 202 | */ 203 | ImageTwicpics.prototype.getTitle = function () { 204 | if (this.imageObject === null) { 205 | return ''; 206 | } 207 | 208 | if (this.referenceType === 'ProductVariationAttributeValue' && this.viewType === 'swatch') { 209 | return this.imageObject.displayValue; 210 | } 211 | 212 | if (!this.image || !this.image.title) { 213 | if (this.referenceType === 'Product') { 214 | return this.imageObject.name; 215 | } else if ( 216 | this.transformations 217 | && this.transformations.missingImageTitle 218 | ) { 219 | return this.transformations.missingImageTitle; 220 | } 221 | 222 | return this.imageObject.displayValue; 223 | } 224 | 225 | return this.image.title; 226 | }; 227 | 228 | 229 | /** 230 | * Get text for alt 231 | * @returns {string} alt text 232 | */ 233 | ImageTwicpics.prototype.getAlt = function () { 234 | if (this.imageObject === null) { 235 | return ''; 236 | } 237 | 238 | if (this.referenceType === 'ProductVariationAttributeValue' && this.viewType === 'swatch') { 239 | return this.imageObject.displayValue; 240 | } 241 | 242 | if (!this.image || !this.image.alt) { 243 | if (this.referenceType === 'Product') { 244 | return this.imageObject.name; 245 | } else if ( 246 | this.transformations 247 | && this.transformations.missingImageAlt 248 | ) { 249 | return this.transformations.missingImageAlt; 250 | } 251 | 252 | return this.imageObject.displayValue; 253 | } 254 | 255 | return this.image.alt; 256 | }; 257 | 258 | /** 259 | * Gets a all images for a given image object 260 | * @param {dw.catalog.Product} imageObject Product or ProductVariationAttributeValue (required) 261 | * @param {string} imageType image type to get : catalog / content / static (required) 262 | * @param {string} viewType type of view (resolution) that should be generated (required) 263 | * @returns {Collecion} Collection of images associated with the image object and the view type 264 | */ 265 | ImageTwicpics.getImages = function (imageObject, imageType, viewType) { 266 | var TwicImgModel = require('*/cartridge/models/twicImg'); 267 | var arrayList = new ArrayList(); 268 | var cpt = 0; 269 | 270 | if (!imageObject || !imageType || !viewType) { 271 | return imageObject.getImages(viewType); 272 | } 273 | 274 | collections.forEach(imageObject.getImages(viewType), function (imageObj) { 275 | var imageTwicObj = {}; 276 | if (!twicpicsHelpers.isTwicpicsEnabled()) { 277 | imageTwicObj = { 278 | alt: imageObj.alt, 279 | url: imageObj.URL.toString(), 280 | title: imageObj.title, 281 | absURL: imageObj.absURL.toString() 282 | }; 283 | } else if (twicpicsHelpers.isTwicpicsEnabled() !== 'expert') { 284 | imageTwicObj = new ImageTwicpics(imageObject, imageType, viewType, cpt); 285 | imageTwicObj.expertMode = false; 286 | } else { 287 | imageTwicObj = new TwicImgModel( 288 | { 289 | src: imageObj.url.toString(), 290 | alt: imageObj.alt, 291 | title: imageObj.title, 292 | imageType: imageType 293 | } 294 | ); 295 | imageTwicObj.url = imageTwicObj.urlImg; 296 | imageTwicObj.expertMode = true; 297 | } 298 | imageTwicObj.index = cpt; 299 | arrayList.push(imageTwicObj); 300 | cpt++; 301 | }); 302 | 303 | return arrayList; 304 | }; 305 | 306 | module.exports = ImageTwicpics; 307 | -------------------------------------------------------------------------------- /cartridges/int_twicpics_sfra/cartridge/client/default/js/product/base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 'use strict'; 3 | 4 | var base = require('base/product/base'); 5 | 6 | /** 7 | * Process attribute values associated with an attribute that does not have image swatches 8 | * 9 | * @param {Object} attr - Attribute 10 | * @param {string} attr.id - Attribute ID 11 | * @param {Object[]} attr.values - Array of attribute value objects 12 | * @param {string} attr.values.value - Attribute coded value 13 | * @param {string} attr.values.url - URL to de/select an attribute value of the product 14 | * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be 15 | * selected. If there is no variant that corresponds to a specific combination of attribute 16 | * values, an attribute may be disabled in the Product Detail Page 17 | * @param {jQuery} $productContainer - DOM container for a given product 18 | */ 19 | function processNonSwatchValues(attr, $productContainer) { 20 | var $attr = '[data-attr="' + attr.id + '"]'; 21 | var $defaultOption = $productContainer.find($attr + ' .select-' + attr.id + ' option:first'); 22 | $defaultOption.attr('value', attr.resetUrl); 23 | 24 | attr.values.forEach(function (attrValue) { 25 | var $attrValue = $productContainer 26 | .find($attr + ' [data-attr-value="' + attrValue.value + '"]'); 27 | $attrValue.attr('value', attrValue.url) 28 | .removeAttr('disabled'); 29 | 30 | if (!attrValue.selectable) { 31 | $attrValue.attr('disabled', true); 32 | } 33 | }); 34 | } 35 | 36 | /** 37 | * Process the attribute values for an attribute that has image swatches 38 | * 39 | * @param {Object} attr - Attribute 40 | * @param {string} attr.id - Attribute ID 41 | * @param {Object[]} attr.values - Array of attribute value objects 42 | * @param {string} attr.values.value - Attribute coded value 43 | * @param {string} attr.values.url - URL to de/select an attribute value of the product 44 | * @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be 45 | * selected. If there is no variant that corresponds to a specific combination of attribute 46 | * values, an attribute may be disabled in the Product Detail Page 47 | * @param {jQuery} $productContainer - DOM container for a given product 48 | * @param {Object} msgs - object containing resource messages 49 | */ 50 | function processSwatchValues(attr, $productContainer, msgs) { 51 | attr.values.forEach(function (attrValue) { 52 | var $attrValue = $productContainer.find('[data-attr="' + attr.id + '"] [data-attr-value="' + 53 | attrValue.value + '"]'); 54 | var $swatchButton = $attrValue.parent(); 55 | 56 | if (attrValue.selected) { 57 | $attrValue.addClass('selected'); 58 | $attrValue.siblings('.selected-assistive-text').text(msgs.assistiveSelectedText); 59 | } else { 60 | $attrValue.removeClass('selected'); 61 | $attrValue.siblings('.selected-assistive-text').empty(); 62 | } 63 | 64 | if (attrValue.url) { 65 | $swatchButton.attr('data-url', attrValue.url); 66 | } else { 67 | $swatchButton.removeAttr('data-url'); 68 | } 69 | 70 | // Disable if not selectable 71 | $attrValue.removeClass('selectable unselectable'); 72 | 73 | $attrValue.addClass(attrValue.selectable ? 'selectable' : 'unselectable'); 74 | }); 75 | } 76 | 77 | /** 78 | * Retrieve contextual quantity selector 79 | * @param {jquery} $el - DOM container for the relevant quantity 80 | * @return {jquery} - quantity selector DOM container 81 | */ 82 | function getQuantitySelector($el) { 83 | var quantitySelected; 84 | if ($el && $('.set-items').length) { 85 | quantitySelected = $($el).closest('.product-detail').find('.quantity-select'); 86 | } else if ($el && $('.product-bundle').length) { 87 | var quantitySelectedModal = $($el).closest('.modal-footer').find('.quantity-select'); 88 | var quantitySelectedPDP = $($el).closest('.bundle-footer').find('.quantity-select'); 89 | if (quantitySelectedModal.val() === undefined) { 90 | quantitySelected = quantitySelectedPDP; 91 | } else { 92 | quantitySelected = quantitySelectedModal; 93 | } 94 | } else { 95 | quantitySelected = $('.quantity-select'); 96 | } 97 | return quantitySelected; 98 | } 99 | 100 | /** 101 | * Routes the handling of attribute processing depending on whether the attribute has image 102 | * swatches or not 103 | * 104 | * @param {Object} attrs - Attribute 105 | * @param {string} attr.id - Attribute ID 106 | * @param {jQuery} $productContainer - DOM element for a given product 107 | * @param {Object} msgs - object containing resource messages 108 | */ 109 | function updateAttrs(attrs, $productContainer, msgs) { 110 | // Currently, the only attribute type that has image swatches is Color. 111 | var attrsWithSwatches = ['color']; 112 | 113 | attrs.forEach(function (attr) { 114 | if (attrsWithSwatches.indexOf(attr.id) > -1) { 115 | processSwatchValues(attr, $productContainer, msgs); 116 | } else { 117 | processNonSwatchValues(attr, $productContainer); 118 | } 119 | }); 120 | } 121 | 122 | /** 123 | * Dynamically creates Bootstrap carousel from response containing images 124 | * @param {Object[]} imgs - Array of large product images,along with related information 125 | * @param {jQuery} $productContainer - DOM element for a given product 126 | */ 127 | function createCarousel(imgs, $productContainer) { 128 | var carousel = $productContainer.find('.carousel'); 129 | $(carousel).carousel('dispose'); 130 | var carouselId = $(carousel).attr('id'); 131 | $(carousel).empty().append('' + $(carousel).data('prev') + '' + $(carousel).data('next') + ''); 132 | for (var i = 0; i < imgs.length; i++) { 133 | if (imgs[i].twicImg) { 134 | $('').appendTo($(carousel).find('.carousel-inner')); 136 | } else { 137 | $('').appendTo($(carousel).find('.carousel-inner')); 138 | } 139 | $('
  • ').appendTo($(carousel).find('.carousel-indicators')); 140 | } 141 | $($(carousel).find('.carousel-item')).first().addClass('active'); 142 | $($(carousel).find('.carousel-indicators > li')).first().addClass('active'); 143 | if (imgs.length === 1) { 144 | $($(carousel).find('.carousel-indicators, a[class^="carousel-control-"]')).detach(); 145 | } 146 | $(carousel).carousel(); 147 | $($(carousel).find('.carousel-indicators')).attr('aria-hidden', true); 148 | } 149 | 150 | /** 151 | * Updates the availability status in the Product Detail Page 152 | * 153 | * @param {Object} response - Ajax response object after an 154 | * attribute value has been [de]selected 155 | * @param {jQuery} $productContainer - DOM element for a given product 156 | */ 157 | function updateAvailability(response, $productContainer) { 158 | var availabilityValue = ''; 159 | var availabilityMessages = response.product.availability.messages; 160 | if (!response.product.readyToOrder) { 161 | availabilityValue = '
  • ' + response.resources.info_selectforstock + '
  • '; 162 | } else { 163 | availabilityMessages.forEach(function (message) { 164 | availabilityValue += '
  • ' + message + '
  • '; 165 | }); 166 | } 167 | 168 | $($productContainer).trigger('product:updateAvailability', { 169 | product: response.product, 170 | $productContainer: $productContainer, 171 | message: availabilityValue, 172 | resources: response.resources 173 | }); 174 | } 175 | 176 | /** 177 | * Generates html for product attributes section 178 | * 179 | * @param {array} attributes - list of attributes 180 | * @return {string} - Compiled HTML 181 | */ 182 | function getAttributesHtml(attributes) { 183 | if (!attributes) { 184 | return ''; 185 | } 186 | 187 | var html = ''; 188 | 189 | attributes.forEach(function (attributeGroup) { 190 | if (attributeGroup.ID === 'mainAttributes') { 191 | attributeGroup.attributes.forEach(function (attribute) { 192 | html += '
    ' + attribute.label + ': ' 193 | + attribute.value + '
    '; 194 | }); 195 | } 196 | }); 197 | 198 | return html; 199 | } 200 | 201 | /** 202 | * Updates the quantity DOM elements post Ajax call 203 | * @param {UpdatedQuantity[]} quantities - 204 | * @param {jQuery} $productContainer - DOM container for a given product 205 | */ 206 | function updateQuantities(quantities, $productContainer) { 207 | if ($productContainer.parent('.bonus-product-item').length <= 0) { 208 | var optionsHtml = quantities.map(function (quantity) { 209 | var selected = quantity.selected ? ' selected ' : ''; 210 | return ''; 212 | }).join(''); 213 | getQuantitySelector($productContainer).empty().html(optionsHtml); 214 | } 215 | } 216 | 217 | /** 218 | * Parses JSON from Ajax call made whenever an attribute value is [de]selected 219 | * @param {Object} response - response from Ajax call 220 | * @param {Object} response.product - Product object 221 | * @param {string} response.product.id - Product ID 222 | * @param {Object[]} response.product.variationAttributes - Product attributes 223 | * @param {Object[]} response.product.images - Product images 224 | * @param {boolean} response.product.hasRequiredAttrsSelected - Flag as to whether all required 225 | * attributes have been selected. Used partially to 226 | * determine whether the Add to Cart button can be enabled 227 | * @param {jQuery} $productContainer - DOM element for a given product. 228 | */ 229 | function handleVariantResponse(response, $productContainer) { 230 | var isChoiceOfBonusProducts = 231 | $productContainer.parents('.choose-bonus-product-dialog').length > 0; 232 | var isVaraint; 233 | if (response.product.variationAttributes) { 234 | updateAttrs(response.product.variationAttributes, $productContainer, response.resources); 235 | isVaraint = response.product.productType === 'variant'; 236 | if (isChoiceOfBonusProducts && isVaraint) { 237 | $productContainer.parent('.bonus-product-item') 238 | .data('pid', response.product.id); 239 | 240 | $productContainer.parent('.bonus-product-item') 241 | .data('ready-to-order', response.product.readyToOrder); 242 | } 243 | } 244 | 245 | // Update primary images 246 | var primaryImageUrls = response.product.images.large; 247 | createCarousel(primaryImageUrls, $productContainer); 248 | 249 | // Update pricing 250 | if (!isChoiceOfBonusProducts) { 251 | var $priceSelector = $('.prices .price', $productContainer).length 252 | ? $('.prices .price', $productContainer) 253 | : $('.prices .price'); 254 | $priceSelector.replaceWith(response.product.price.html); 255 | } 256 | 257 | // Update promotions 258 | $productContainer.find('.promotions').empty().html(response.product.promotionsHtml); 259 | 260 | updateAvailability(response, $productContainer); 261 | 262 | if (isChoiceOfBonusProducts) { 263 | var $selectButton = $productContainer.find('.select-bonus-product'); 264 | $selectButton.trigger('bonusproduct:updateSelectButton', { 265 | product: response.product, $productContainer: $productContainer 266 | }); 267 | } else { 268 | // Enable "Add to Cart" button if all required attributes have been selected 269 | $('button.add-to-cart, button.add-to-cart-global, button.update-cart-product-global').trigger('product:updateAddToCart', { 270 | product: response.product, $productContainer: $productContainer 271 | }).trigger('product:statusUpdate', response.product); 272 | } 273 | 274 | // Update attributes 275 | $productContainer.find('.main-attributes').empty() 276 | .html(getAttributesHtml(response.product.attributes)); 277 | } 278 | 279 | /** 280 | * Updates DOM using post-option selection Ajax response 281 | * 282 | * @param {OptionSelectionResponse} optionsHtml - Ajax response optionsHtml from selecting a product option 283 | * @param {jQuery} $productContainer - DOM element for current product 284 | */ 285 | function updateOptions(optionsHtml, $productContainer) { 286 | // Update options 287 | $productContainer.find('.product-options').empty().html(optionsHtml); 288 | } 289 | 290 | /** 291 | * updates the product view when a product attribute is selected or deselected or when 292 | * changing quantity 293 | * @param {string} selectedValueUrl - the Url for the selected variation value 294 | * @param {jQuery} $productContainer - DOM element for current product 295 | */ 296 | function attributeSelect(selectedValueUrl, $productContainer) { 297 | if (selectedValueUrl) { 298 | $('body').trigger('product:beforeAttributeSelect', 299 | { url: selectedValueUrl, container: $productContainer }); 300 | 301 | $.ajax({ 302 | url: selectedValueUrl, 303 | method: 'GET', 304 | success: function (data) { 305 | handleVariantResponse(data, $productContainer); 306 | updateOptions(data.product.optionsHtml, $productContainer); 307 | updateQuantities(data.product.quantities, $productContainer); 308 | $('body').trigger('product:afterAttributeSelect', 309 | { data: data, container: $productContainer }); 310 | $.spinner().stop(); 311 | }, 312 | error: function () { 313 | $.spinner().stop(); 314 | } 315 | }); 316 | } 317 | } 318 | 319 | /** 320 | * Retrieve product options 321 | * 322 | * @param {jQuery} $productContainer - DOM element for current product 323 | * @return {string} - Product options and their selected values 324 | */ 325 | function getOptions($productContainer) { 326 | var options = $productContainer 327 | .find('.product-option') 328 | .map(function () { 329 | var $elOption = $(this).find('.options-select'); 330 | var urlValue = $elOption.val(); 331 | var selectedValueId = $elOption.find('option[value="' + urlValue + '"]') 332 | .data('value-id'); 333 | return { 334 | optionId: $(this).data('option-id'), 335 | selectedValueId: selectedValueId 336 | }; 337 | }).toArray(); 338 | 339 | return JSON.stringify(options); 340 | } 341 | 342 | /** 343 | * Retrieves url to use when adding a product to the cart 344 | * 345 | * @return {string} - The provided URL to use when adding a product to the cart 346 | */ 347 | function getAddToCartUrl() { 348 | return $('.add-to-cart-url').val(); 349 | } 350 | 351 | /** 352 | * Retrieves the bundle product item ID's for the Controller to replace bundle master product 353 | * items with their selected variants 354 | * 355 | * @return {string[]} - List of selected bundle product item ID's 356 | */ 357 | function getChildProducts() { 358 | var childProducts = []; 359 | $('.bundle-item').each(function () { 360 | childProducts.push({ 361 | pid: $(this).find('.product-id').text(), 362 | quantity: parseInt($(this).find('label.quantity').data('quantity'), 10) 363 | }); 364 | }); 365 | 366 | return childProducts.length ? JSON.stringify(childProducts) : []; 367 | } 368 | 369 | /** 370 | * Updates the Mini-Cart quantity value after the customer has pressed the "Add to Cart" button 371 | * @param {string} response - ajax response from clicking the add to cart button 372 | */ 373 | function handlePostCartAdd(response) { 374 | $('.minicart').trigger('count:update', response); 375 | var messageType = response.error ? 'alert-danger' : 'alert-success'; 376 | // show add to cart toast 377 | if (response.newBonusDiscountLineItem 378 | && Object.keys(response.newBonusDiscountLineItem).length !== 0) { 379 | chooseBonusProducts(response.newBonusDiscountLineItem); 380 | } else { 381 | if ($('.add-to-cart-messages').length === 0) { 382 | $('body').append( 383 | '
    ' 384 | ); 385 | } 386 | 387 | $('.add-to-cart-messages').append( 388 | '' 391 | ); 392 | 393 | setTimeout(function () { 394 | $('.add-to-basket-alert').remove(); 395 | }, 5000); 396 | } 397 | } 398 | 399 | /** 400 | * Makes a call to the server to report the event of adding an item to the cart 401 | * 402 | * @param {string | boolean} url - a string representing the end point to hit so that the event can be recorded, or false 403 | */ 404 | function miniCartReportingUrl(url) { 405 | if (url) { 406 | $.ajax({ 407 | url: url, 408 | method: 'GET', 409 | success: function () { 410 | // reporting urls hit on the server 411 | }, 412 | error: function () { 413 | // no reporting urls hit on the server 414 | } 415 | }); 416 | } 417 | } 418 | 419 | var exportData = $.extend( 420 | {}, 421 | base, 422 | { 423 | attributeSelect: attributeSelect, 424 | colorAttribute: function () { 425 | $(document).on('click', '[data-attr="color"] button', function (e) { 426 | e.preventDefault(); 427 | if ($(this).attr('disabled')) { 428 | return; 429 | } 430 | var $productContainer = $(this).closest('.set-item'); 431 | if (!$productContainer.length) { 432 | $productContainer = $(this).closest('.product-detail'); 433 | } 434 | attributeSelect($(this).attr('data-url'), $productContainer); 435 | }); 436 | }, 437 | 438 | selectAttribute: function () { 439 | $(document).on('change', 'select[class*="select-"], .options-select', function (e) { 440 | e.preventDefault(); 441 | var $productContainer = $(this).closest('.set-item'); 442 | if (!$productContainer.length) { 443 | $productContainer = $(this).closest('.product-detail'); 444 | } 445 | attributeSelect(e.currentTarget.value, $productContainer); 446 | }); 447 | }, 448 | 449 | availability: function () { 450 | $(document).on('change', '.quantity-select', function (e) { 451 | e.preventDefault(); 452 | 453 | var $productContainer = $(this).closest('.product-detail'); 454 | if (!$productContainer.length) { 455 | $productContainer = $(this).closest('.modal-content').find('.product-quickview'); 456 | } 457 | 458 | if ($('.bundle-items', $productContainer).length === 0) { 459 | attributeSelect($(e.currentTarget).find('option:selected').data('url'), 460 | $productContainer); 461 | } 462 | }); 463 | }, 464 | 465 | addToCart: function () { 466 | return; 467 | } 468 | } 469 | ); 470 | 471 | module.exports = exportData; 472 | --------------------------------------------------------------------------------