Adds SFRA 6.0

This commit is contained in:
Isaac Vallee 2021-12-21 10:57:31 -08:00
parent d04eb5dd16
commit 823c7608c3
1257 changed files with 137087 additions and 0 deletions

View File

@ -0,0 +1,17 @@
orbs:
node: circleci/node@1.1
jobs:
build:
working_directory: ~/build_only
executor:
name: node/default
tag: '12.21'
steps:
- checkout
- run: npm install
- run: npm run lint
- run: npm run test
- run: npm run compile:js
- run: npm run compile:scss
version: 2.1

View File

@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,6 @@
cartridges/app_storefront_base/cartridge/static/
coverage/
doc/
bin/
codecept.conf.js
cartridges/bm_app_storefront_base/cartridge/static/

View File

@ -0,0 +1,15 @@
{
"root": true,
"extends": "airbnb-base/legacy",
"rules": {
"import/no-unresolved": "off",
"indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1 }],
"func-names": "off",
"require-jsdoc": "error",
"valid-jsdoc": ["error", { "preferType": { "Boolean": "boolean", "Number": "number", "object": "Object", "String": "string" }, "requireReturn": false}],
"vars-on-top": "off",
"global-require": "off",
"no-shadow": ["error", { "allow": ["err", "callback"]}],
"max-len": "off"
}
}

View File

@ -0,0 +1,43 @@
node_modules/
cartridges/app_storefront_base/cartridge/static/default/css/
cartridges/app_storefront_base/cartridge/static/default/js/
cartridges/app_storefront_base/cartridge/static/default/fonts/
cartridges/app_storefront_base/cartridge/static/fr_FR/css/
cartridges/app_storefront_base/cartridge/static/fr_FR/js/
cartridges/app_storefront_base/cartridge/static/fr_FR/fonts/
cartridges/app_storefront_base/cartridge/static/it_IT/css/
cartridges/app_storefront_base/cartridge/static/it_IT/js/
cartridges/app_storefront_base/cartridge/static/it_IT/fonts/
cartridges/app_storefront_base/cartridge/static/ja_JP/css/
cartridges/app_storefront_base/cartridge/static/ja_JP/js/
cartridges/app_storefront_base/cartridge/static/ja_JP/fonts/
cartridges/app_storefront_base/cartridge/static/zh_CN/css/
cartridges/app_storefront_base/cartridge/static/zh_CN/js/
cartridges/app_storefront_base/cartridge/static/zh_CN/fonts/
cartridges/app_storefront_base/cartridge/static/en_GB/css/
cartridges/app_storefront_base/cartridge/static/en_GB/js/
cartridges/app_storefront_base/cartridge/static/en_GB/fonts/
coverage/
npm-debug.log
cartridges.zip
.idea/
dw.json
sitegenesisdata/
mobilefirstdata/
storefrontdata/
demo_data_sfra/
demo_data_sfra.zip
.DS_Store
test/appium/webdriver/config.json
.vscode
.history
*.iml
.idea
test/acceptance/report
test/integration/config.json

View File

@ -0,0 +1,16 @@
instrumentation:
root: .
extensions:
- .js
default-excludes: true
excludes: [
"**/static/**", # Those are pre-processed client-side scripts
"**/js/**", # Those are client-side scripts
"**/controllers/**", # We can't test controllers without too much mocking
"**/server/EventEmitter.js", # Third-party library
"**/modules/*.js", # Those are just wrappers around modules
"bin/*", # Those are task files
"**/scripts/payment/processor/*", # Those are payment processor files, we don't test them
"webpack.config.js" # This is webpack config for javascript
]
include-all-sources: true

View File

@ -0,0 +1,17 @@
{
"extends": "stylelint-config-standard",
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": [true, { "ignoreAtRules": ["include", "each", "mixin", "if", "else", "content"] } ],
"indentation": 4,
"scss/at-import-no-partial-leading-underscore": true,
"scss/at-import-partial-extension-blacklist": ["scss"],
"scss/dollar-variable-no-missing-interpolation": true,
"scss/media-feature-value-dollar-variable": "always",
"scss/selector-no-redundant-nesting-selector": true,
"at-rule-empty-line-before": [ "always", { "ignoreAtRules": ["else"], "ignore": ["blockless-after-same-name-blockless", "inside-block"] } ],
"block-closing-brace-newline-after": [ "always", { "ignoreAtRules": ["if", "else"] } ]
}
}

View File

@ -0,0 +1,106 @@
# Table of contents
- [Conventions for branch names and commit messages ](#conventions-for-branch-names-and-commit-messages)
- [Submitting your first pull request ](#submitting-your-first-pull-request)
- [Submitting a pull request ](#submitting-a-pull-request)
- [What to expect](#what-to-expect)
- [Community contributors](#community-contributors)
- [Contributor License Agreement (CLA)](#contributor-license-agreement)
- [Commit signing](#commit-signing)
- [Back to README](./README.md)
# Contributing to SFRA
To contribute to the SFRA base cartridge, follow the guidelines below. This helps us address your pull request in a more timely manner.
## Conventions for branch names and commit messages
### Branch names
To name a branch, use the following pattern: `yourusername-description`
In this pattern, `description` is dash-delimited.
For example: jdoe-unify-shipping-isml
### Commit messages
To create a commit message, use the following pattern: `action-term: short-description`
In this pattern, `action-term` is one of the following:
* Bug
* Doc
* Chore
* Update
* Breaking
* New
After `action-term,` add a colon, and then write a short description. You can optionally include a GUS ticket number in parentheses.
For example: "Breaking: Unify the single- and multi-ship shipping isml templates (W-999999)."
## Submitting your first pull request
If this is your first pull request, follow these steps:
1. Create a fork of the SFRA repository
2. Download the forked repository
3. Checkout the integration branch
4. Apply your code fix
5. Create a pull request against the integration branch
## Submitting a pull request
1. Create a branch off the integration branch.
* To reduce merge conflicts, rebase your branch before submitting your pull request.
* If applicable, reference the issue number in the comments of your pull request.
2. In your pull request, include:
* A brief description of the problem and your solution
* (optional) Screen shots
* (optional) Error logs
* (optional) Steps to reproduce
3. Grant SFRA team members access to your fork so we can run an automated test on your pull request prior to merging it into our integration branch.
* From within your forked repository, find the 'Settings' link (see the site navigation on left of the page).
* Under the settings menu, click 'User and group access'.
* Add the new user to the input field under the heading 'Users' and give the new user write access.
4. Indicate if there is any data that needs to be included with your code submission.
5. Your code should pass the automation process.
* Lint your code:
`npm run lint`
* Run and pass the unit test:
`npm run test`
* Run and pass the unit/intergration test:
`npm run test:integration`
## What to expect
After you submit your pull request, we'll look it over and consider it for merging.
As long as your submission has met the above guidelines, we should merge it in a timely manner.
Our sprints run for about two weeks; in that period of time, we typically review all pull requests, give feedback, and merge the request (depending on our current sprint priorities).
## Community contributors
To speed up the process of reviewing and merging your pull request, grant the following team members access to your fork:
* SFRA team
## Contributor License Agreement
All external contributors must sign our Contributor License Agreement (CLA).
## Commit signing
All contributors must set up [commit signing](https://help.github.com/en/github/authenticating-to-github/signing-commits).

View File

@ -0,0 +1,34 @@
## Applicable to all contributors to this repository:
### All pull requests:
* [ ] Have you considered security (e.g., XSS)?
* [ ] Have you considered desktop, mobile, and tablet form-factors?
* [ ] Have you considered [accessibility best practices](https://www.w3.org/WAI/standards-guidelines/wcag/)?
### Code
* [ ] Are the commits squashed into one commit?
* [ ] Is the [branch name and commit message](CONTRIBUTING.md#conventions-for-branch-names-and-commit-messages) following team convention?
* [ ] Is the code linted?
* [ ] Are all open issues, questions, and concerns by reviewers answered, resolved, and reviewed?
### Quality assurance
* [ ] If applicable, are unit tests written and are they passing?
* [ ] If applicable, are integration tests written and are they passing?
* [ ] Have checks linked in your pull request passed?
### Documentation
* [ ] Are there meaningful code comments included?
## Applicable to community contributors:
* [ ] Have you granted SFRA team access to your fork? [Grant access](CONTRIBUTING.md#community-contributors)
## Applicable to SFRA team members:
### Documentation
* [ ] If applicable, is the change log updated?
* [ ] If applicable, have any UI text changes been reviewed by Documentation?
* [ ] If applicable, have any UI implementations been reviewed by the UX team?
### Security
* [ ] If applicable, has Security reviewed this code?
* [ ] If applicable, is a 3PP request submitted?

View File

@ -0,0 +1,101 @@
# Storefront Reference Architecture (SFRA)
This is a repository for the Storefront Reference Architecture reference application.
Storefront Reference Architecture has a base cartridge (`app_storefront_base`) provided by Commerce Cloud that is never directly customized or edited. Instead, customization cartridges are layered on top of the base cartridge. This change is intended to allow for easier adoption of new features and bug fixes.
Storefront Reference Architecture supplies an [plugin_applepay](https://github.com/SalesforceCommerceCloud/plugin-applepay) plugin cartridge to demonstrate how to layer customizations for the reference application.
Your feedback on the ease-of-use and limitations of this new architecture is invaluable during the developer preview. Particularly, feedback on any issues you encounter or workarounds you develop for efficiently customizing the base cartridge without editing it directly.
# The latest version
The latest version of SFRA is 6.0.0
# Getting Started
1. Clone this repository.
2. Run `npm install` to install all of the local dependencies (SFRA has been tested with v12.21.0 and is recommended)
3. Run `npm run compile:js` from the command line that would compile all client-side JS files. Run `npm run compile:scss` and `npm run compile:fonts` that would do the same for css and fonts.
4. Create `dw.json` file in the root of the project:
```json
{
"hostname": "your-sandbox-hostname.demandware.net",
"username": "yourlogin",
"password": "yourpwd",
"code-version": "version_to_upload_to"
}
```
5. Run `npm run uploadCartridge`. It will upload `app_storefront_base`, `modules` and `bm_app_storefront_base` cartridges to the sandbox you specified in `dw.json` file.
6. Use https://github.com/SalesforceCommerceCloud/storefrontdata to zip and import site data on your sandbox.
7. Add the `app_storefront_base` cartridge to your cartridge path in _Administration > Sites > Manage Sites > RefArch - Settings_ (Note: This should already be populated by the sample data in Step 6).
8. You should now be ready to navigate to and use your site.
# NPM scripts
Use the provided NPM scripts to compile and upload changes to your Sandbox.
## Compiling your application
* `npm run compile:scss` - Compiles all .scss files into CSS.
* `npm run compile:js` - Compiles all .js files and aggregates them.
* `npm run compile:fonts` - Copies all needed font files. Usually, this only has to be run once.
If you are having an issue compiling scss files, try running 'npm rebuild node-sass' from within your local repo.
## Linting your code
`npm run lint` - Execute linting for all JavaScript and SCSS files in the project. You should run this command before committing your code.
## Watching for changes and uploading
`npm run watch` - Watches everything and recompiles (if necessary) and uploads to the sandbox. Requires a valid `dw.json` file at the root that is configured for the sandbox to upload.
## Uploading
`npm run uploadCartridge` - Will upload `app_storefront_base`, `modules` and `bm_app_storefront_base` to the server. Requires a valid `dw.json` file at the root that is configured for the sandbox to upload.
`npm run upload <filepath>` - Will upload a given file to the server. Requires a valid `dw.json` file.
# Testing
## Running unit tests
You can run `npm test` to execute all unit tests in the project. Run `npm run cover` to get coverage information. Coverage will be available in `coverage` folder under root directory.
* UNIT test code coverage:
1. Open a terminal and navigate to the root directory of the mfsg repository.
2. Enter the command: `npm run cover`.
3. Examine the report that is generated. For example: `Writing coverage reports at [/Users/yourusername/SCC/sfra/coverage]`
3. Navigate to this directory on your local machine, open up the index.html file. This file contains a detailed report.
## Running integration tests
Integration tests are located in the `storefront-reference-architecture/test/integration` directory.
To run integration tests you can use the following command:
```
npm run test:integration
```
**Note:** Please note that short form of this command will try to locate URL of your sandbox by reading `dw.json` file in the root directory of your project. If you don't have `dw.json` file, integration tests will fail.
sample `dw.json` file (this file needs to be in the root of your project)
{
"hostname": "devxx-sitegenesis-dw.demandware.net"
}
You can also supply URL of the sandbox on the command line:
```
npm run test:integration -- --baseUrl devxx-sitegenesis-dw.demandware.net
```
# [Contributing to SFRA](./CONTRIBUTING.md)
#Page Designer Components for Storefront Reference Architecture
See: [Page Designer Components](./page-designer-components.md)

View File

@ -0,0 +1,140 @@
'use strict';
/* global cat, cd, cp, echo, exec, exit, find, ls, mkdir, pwd, rm, target, test */
require('shelljs/make');
var chalk = require('chalk'),
path = require('path'),
spawn = require('child_process').spawn,
fs = require('fs'),
shell = require('shelljs');
function getSandboxUrl() {
if (test('-f', path.join(process.cwd(), 'dw.json'))) {
var config = cat(path.join(process.cwd(), 'dw.json'));
var parsedConfig = JSON.parse(config);
return '' + parsedConfig.hostname;
}
return '';
}
function getOptions(defaults, args) {
var params = {};
var i = 0;
while (i < args.length) {
var item = args[i];
if (item.indexOf('--') === 0) {
if (i + 1 < args.length && args[i + 1].indexOf('--') < 0) {
var value = args[i + 1];
value = value.replace(/\/+$/, "");
params[item.substr(2)] = value;
i += 2;
} else {
params[item.substr(2)] = true;
i++;
}
} else {
params[item] = true;
i++;
}
}
var options = Object.assign({}, defaults, params);
return options;
}
function getOptionsString(options) {
if (!options.baseUrl) {
console.error(chalk.red('Could not find baseUrl parameter.'));
process.exit();
}
var optionsString = '';
Object.keys(options).forEach(function (key) {
if (options[key] === true) {
optionsString += key + ' ';
} else {
optionsString += '--' + key + ' ' + options[key] + ' ';
}
});
return optionsString;
}
target.compileFonts = function () {
var fontsDir = 'cartridges/app_storefront_base/cartridge/static/default/fonts';
mkdir('-p', fontsDir);
cp('-r', 'node_modules/font-awesome/fonts/', 'cartridges/app_storefront_base/cartridge/static/default');
cp('-r', 'node_modules/flag-icon-css/flags', fontsDir + '/flags');
};
target.functional = function (args) {
var defaults = {
baseUrl: 'https://' + getSandboxUrl() + '/s/RefArch',
client: 'chrome'
};
var configFile = 'test/functional/webdriver/wdio.conf.js';
if(args.indexOf('appium') > -1) {
args.splice(args.indexOf('appium'), 1);
configFile = 'test/functional/webdriver/wdio.appium.js';
defaults = {
baseUrl: 'https://' + getSandboxUrl() + '/s/RefArch'
}
}
var options = getOptions(defaults, args);
var optionsString = getOptionsString(options);
console.log(chalk.green('Installing selenium'));
exec('node_modules/.bin/selenium-standalone install', { silent: true });
console.log(chalk.green('Selenium Server started'));
var selenium = exec('node_modules/.bin/selenium-standalone start', { async: true, silent: true });
console.log(chalk.green('Running functional tests'));
var tests = spawn('./node_modules/.bin/wdio ' + configFile + ' ' + optionsString, { stdio: 'inherit', shell: true });
tests.on('exit', function (code) {
selenium.kill();
console.log(chalk.green('Stopping Selenium Server'));
process.exit(code);
});
};
target.release = function (args) {
if (!args) {
console.log('No version type provided. Please specify release type patch/minor/major');
return;
}
var type = args[0].replace(/"/g, '');
if (['patch', 'minor', 'major'].indexOf(type) >= 0) {
console.log('Updating package.json version with ' + args[0] + ' release.');
var version = spawn('npm version ' + args[0], { stdio: 'inherit', shell: true });
var propertiesFileName = path.resolve('./cartridges/app_storefront_base/cartridge/templates/resources/version.properties')
version.on('exit', function (code) {
if (code === 0) {
var versionNumber = JSON.parse(fs.readFileSync('./package.json').toString()).version;
//modify version.properties file
var propertiesFile = fs.readFileSync(propertiesFileName).toString();
var propertiesLines = propertiesFile.split('\n');
var newLines = propertiesLines.map(function (line) {
if (line.indexOf('global.version.number=') === 0) {
line = 'global.version.number=' + versionNumber;
}
return line;
});
fs.writeFileSync(propertiesFileName, newLines.join('\n'));
shell.exec('git add -A');
shell.exec('git commit -m "Release ' + versionNumber + '"');
console.log('Version updated to ' + versionNumber);
console.log('Please do not forget to push your changes to the integration branch');
}
});
} else {
console.log('Could not release new version. Please specify version type (patch/minor/major).');
}
}

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
HUB_RUNNING=$(docker inspect --format="{{ .State.Running }}" selenium-hub 2> /dev/null)
if [ $? -eq 1 ]; then
echo "Selenium Hub does not exist. Attempting to run it..."
docker run -d -p 4444:4444 --name selenium-hub --restart always selenium/hub:2.53.0
sleep 2
fi
if [ "$HUB_RUNNING" == "false" ]; then
echo "Selenium Hub is not running. Attempting to start it..."
docker start selenium-hub
sleep 2
fi
CONTAINER_NAME="sg-functional-test-chrome-$JOB_NAME"
# remove any pre-existing container
if [ $(docker ps -a | grep $CONTAINER_NAME | awk '{print $NF}' | wc -l) -gt 0 ]; then
docker rm -f $CONTAINER_NAME 1>/dev/null
fi
# if a debug flag is passed in, use the debug image and open vnc screen sharing
if [[ $@ == *"--debug"* ]]; then
ip=$(grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' <<< "$@")
docker run -d --link selenium-hub:hub --name $CONTAINER_NAME -p 5900:5900 -v /dev/shm:/dev/shm selenium/node-chrome-debug:2.53.0 1>/dev/null
sleep 2 # wait a bit for container to start
open vnc://:secret@"$ip":5900
else
docker run -d --link selenium-hub:hub --name $CONTAINER_NAME -v /dev/shm:/dev/shm selenium/node-chrome:2.53.0 1>/dev/null
sleep 2
fi
# run actual test command in a subshell to be able to rm docker container afterwards
# this command is for Jenkins job to by pass the Makefile.js
(
./node_modules/.bin/wdio test/functional/webdriver/wdio.conf.js "$@" 2> /dev/null
)
# save exit code of subshell
testresult=$?
docker stop $CONTAINER_NAME 1>/dev/null && docker rm $CONTAINER_NAME 1>/dev/null
exit $testresult

View File

@ -0,0 +1,20 @@
# This is a sample build configuration for Javascript.
# Check our guides at https://confluence.atlassian.com/x/VYk8Lw for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: node:6.9.2
pipelines:
default:
- step:
script: # Modify the commands below to build your repository.
- npm install
- npm run lint
- npm test
- npm run compile:js
- npm run compile:scss
- npm run compile:fonts
- node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/app_storefront_base
- node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/modules
- npm run test:integration -- --baseUrl https://${HOSTNAME}/on/demandware.store/Sites-MobileFirst-Site/en_US "test/integration/*"

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app_storefront_base</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.demandware.studio.core.beehiveElementBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.demandware.studio.core.beehiveNature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,14 @@
{
"ecmaVersion": 5,
"plugins": {
"guess-types": {
},
"outline": {
},
"demandware": {
}
}
}

View File

@ -0,0 +1,5 @@
# Welcome to Storefront Reference Architecture (SFRA)
The Storefront Reference Architecture is fully compliant with standard JavaScript. It uses [Controllers]{@tutorial Controllers} to handle incoming requests. It provides a layer of JSON objects through the [Model-Views]{@tutorial Models}. All scripts are [Common JS modules](http://www.commonjs.org) with defined and documented exports to avoid polluting the global namespace.
This documentation is meant to serve as a reference to quickly look up supported functionality and is fully based on the comments in the code. You can continue to maintain these [JSDoc comments](http://usejsdoc.org/) to generate a similar documentation for your own project.

View File

@ -0,0 +1,4 @@
## cartridge.properties for cartridge app_storefront_base
#Thu Jun 09 11:30:40 EDT 2016
demandware.cartridges.app_storefront_base.multipleLanguageStorefront=true
demandware.cartridges.app_storefront_base.id=app_storefront_base

View File

@ -0,0 +1,10 @@
{
"env": {
"jquery": true
},
"rules": {
"global-require": "off",
"no-var": "off",
"prefer-const": "off"
}
}

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./addressBook/addressBook'));
});

View File

@ -0,0 +1,105 @@
'use strict';
var formValidation = require('../components/formValidation');
var url;
var isDefault;
/**
* Create an alert to display the error message
* @param {Object} message - Error message to display
*/
function createErrorNotification(message) {
var errorHtml = '<div class="alert alert-danger alert-dismissible valid-cart-error ' +
'fade show" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">&times;</span>' +
'</button>' + message + '</div>';
$('.error-messaging').append(errorHtml);
}
module.exports = {
removeAddress: function () {
$('.remove-address').on('click', function (e) {
e.preventDefault();
isDefault = $(this).data('default');
if (isDefault) {
url = $(this).data('url')
+ '?addressId='
+ $(this).data('id')
+ '&isDefault='
+ isDefault;
} else {
url = $(this).data('url') + '?addressId=' + $(this).data('id');
}
$('.product-to-remove').empty().append($(this).data('id'));
});
},
removeAddressConfirmation: function () {
$('.delete-confirmation-btn').click(function (e) {
e.preventDefault();
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
$('#uuid-' + data.UUID).remove();
if (isDefault) {
var addressId = $('.card .address-heading').first().text();
var addressHeading = addressId + ' (' + data.defaultMsg + ')';
$('.card .address-heading').first().text(addressHeading);
$('.card .card-make-default-link').first().remove();
$('.remove-address').data('default', true);
if (data.message) {
var toInsert = '<div><h3>' +
data.message +
'</h3><div>';
$('.addressList').after(toInsert);
}
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
}
$.spinner().stop();
}
});
});
},
submitAddress: function () {
$('form.address-form').submit(function (e) {
var $form = $(this);
e.preventDefault();
url = $form.attr('action');
$form.spinner().start();
$('form.address-form').trigger('address:submit', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: $form.serialize(),
success: function (data) {
$form.spinner().stop();
if (!data.success) {
formValidation($form, data);
} else {
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$form.spinner().stop();
}
});
return false;
});
}
};

View File

@ -0,0 +1,17 @@
'use strict';
$(document).ready(function () {
if (window.resetCampaignBannerSessionToken) {
window.sessionStorage.removeItem('hide_campaign_banner');
}
var campaignBannerStatus = window.sessionStorage.getItem('hide_campaign_banner');
$('.campaign-banner .close').on('click', function () {
$('.campaign-banner').addClass('d-none');
window.sessionStorage.setItem('hide_campaign_banner', '1');
});
if (!campaignBannerStatus || campaignBannerStatus < 0) {
$('.campaign-banner').removeClass('d-none');
}
});

View File

@ -0,0 +1,174 @@
'use strict';
var debounce = require('lodash/debounce');
/**
* Get display information related to screen size
* @param {jQuery} element - the current carousel that is being used
* @returns {Object} an object with display information
*/
function screenSize(element) {
var result = {
itemsToDisplay: null,
sufficientSlides: true
};
var viewSize = $(window).width();
var extraSmallDisplay = element.data('xs');
var smallDisplay = element.data('sm');
var mediumDisplay = element.data('md');
var numberOfSlides = element.data('number-of-slides');
if (viewSize <= 575.98) {
result.itemsToDisplay = extraSmallDisplay;
} else if ((viewSize >= 576) && (viewSize <= 768.98)) {
result.itemsToDisplay = smallDisplay;
} else if (viewSize >= 769) {
result.itemsToDisplay = mediumDisplay;
}
if (result.itemsToDisplay && numberOfSlides <= result.itemsToDisplay) {
result.sufficientSlides = false;
}
return result;
}
/**
* Makes the next element to be displayed next unreachable for screen readers and keyboard nav
* @param {jQuery} element - the current carousel that is being used
*/
function hiddenSlides(element) {
var carousel;
if (element) {
carousel = element;
} else {
carousel = $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel');
}
var screenSizeInfo = screenSize(carousel);
var lastDisplayedElement;
var elementToBeDisplayed;
switch (screenSizeInfo.itemsToDisplay) {
case 2:
lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item');
elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item');
break;
case 3:
lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item');
elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item');
break;
case 4:
lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item');
elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item');
break;
case 6:
lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item');
elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item');
break;
default:
break;
}
carousel.find('.active.carousel-item').removeAttr('tabindex').removeAttr('aria-hidden');
carousel.find('.active.carousel-item').find('a, button, details, input, textarea, select')
.removeAttr('tabindex')
.removeAttr('aria-hidden');
if (lastDisplayedElement) {
lastDisplayedElement.removeAttr('tabindex').removeAttr('aria-hidden');
lastDisplayedElement.find('a, button, details, input, textarea, select')
.removeAttr('tabindex')
.removeAttr('aria-hidden');
}
if (elementToBeDisplayed) {
elementToBeDisplayed.attr('tabindex', -1).attr('aria-hidden', true);
elementToBeDisplayed.find('a, button, details, input, textarea, select')
.attr('tabindex', -1)
.attr('aria-hidden', true);
}
}
$(document).ready(function () {
hiddenSlides();
$(window).on('resize', debounce(function () {
hiddenSlides();
}, 500));
$('body').on('carousel:setup', function () {
hiddenSlides();
});
$('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('touchstart', function (touchStartEvent) {
var screenSizeInfo = screenSize($(this));
if (screenSizeInfo.sufficientSlides) {
var xClick = touchStartEvent.originalEvent.touches[0].pageX;
$(this).one('touchmove', function (touchMoveEvent) {
var xMove = touchMoveEvent.originalEvent.touches[0].pageX;
if (Math.floor(xClick - xMove) > 5) {
$(this).carousel('next');
} else if (Math.floor(xClick - xMove) < -5) {
$(this).carousel('prev');
}
});
$('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('touchend', function () {
$(this).off('touchmove');
});
}
});
$('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('slide.bs.carousel', function (e) {
var activeCarouselPosition = $(e.relatedTarget).data('position');
$(this).find('.pd-carousel-indicators .active').removeClass('active');
$(this).find(".pd-carousel-indicators [data-position='" + activeCarouselPosition + "']").addClass('active');
var extraSmallDisplay = $(this).data('xs');
var smallDisplay = $(this).data('sm');
var mediumDisplay = $(this).data('md');
var arrayOfSlidesToDisplay = [];
if (!$(this).hasClass('insufficient-xs-slides')) {
arrayOfSlidesToDisplay.push(extraSmallDisplay);
}
if (!$(this).hasClass('insufficient-sm-slides')) {
arrayOfSlidesToDisplay.push(smallDisplay);
}
if (!$(this).hasClass('insufficient-md-slides')) {
arrayOfSlidesToDisplay.push(mediumDisplay);
}
var itemsToDisplay = Math.max.apply(Math, arrayOfSlidesToDisplay);
var elementIndex = $(e.relatedTarget).index();
var numberOfSlides = $('.carousel-item', this).length;
var carouselInner = $(this).find('.carousel-inner');
var carouselItem;
if (elementIndex >= numberOfSlides - (itemsToDisplay - 1)) {
var it = itemsToDisplay - (numberOfSlides - elementIndex);
for (var i = 0; i < it; i++) {
// append slides to end
if (e.direction === 'left') {
carouselItem = $('.carousel-item', this).eq(i);
$(carouselItem).appendTo($(carouselInner));
} else {
carouselItem = $('.carousel-item', this).eq(0);
$(carouselItem).appendTo($(carouselInner));
}
}
}
});
$('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('slid.bs.carousel', function () {
hiddenSlides($(this));
});
});

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./cart/cart'));
});

View File

@ -0,0 +1,766 @@
'use strict';
var base = require('../product/base');
var focusHelper = require('../components/focus');
/**
* appends params to a url
* @param {string} url - Original url
* @param {Object} params - Parameters to append
* @returns {string} result url with appended parameters
*/
function appendToUrl(url, params) {
var newUrl = url;
newUrl += (newUrl.indexOf('?') !== -1 ? '&' : '?') + Object.keys(params).map(function (key) {
return key + '=' + encodeURIComponent(params[key]);
}).join('&');
return newUrl;
}
/**
* Checks whether the basket is valid. if invalid displays error message and disables
* checkout button
* @param {Object} data - AJAX response from the server
*/
function validateBasket(data) {
if (data.valid.error) {
if (data.valid.message) {
var errorHtml = '<div class="alert alert-danger alert-dismissible valid-cart-error ' +
'fade show" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">&times;</span>' +
'</button>' + data.valid.message + '</div>';
$('.cart-error').append(errorHtml);
} else {
$('.cart').empty().append('<div class="row"> ' +
'<div class="col-12 text-center"> ' +
'<h1>' + data.resources.emptyCartMsg + '</h1> ' +
'</div> ' +
'</div>'
);
$('.number-of-items').empty().append(data.resources.numberOfItems);
$('.minicart-quantity').empty().append(data.numItems);
$('.minicart-link').attr({
'aria-label': data.resources.minicartCountOfItems,
title: data.resources.minicartCountOfItems
});
$('.minicart .popover').empty();
$('.minicart .popover').removeClass('show');
}
$('.checkout-btn').addClass('disabled');
} else {
$('.checkout-btn').removeClass('disabled');
}
}
/**
* re-renders the order totals and the number of items in the cart
* @param {Object} data - AJAX response from the server
*/
function updateCartTotals(data) {
$('.number-of-items').empty().append(data.resources.numberOfItems);
$('.shipping-cost').empty().append(data.totals.totalShippingCost);
$('.tax-total').empty().append(data.totals.totalTax);
$('.grand-total').empty().append(data.totals.grandTotal);
$('.sub-total').empty().append(data.totals.subTotal);
$('.minicart-quantity').empty().append(data.numItems);
$('.minicart-link').attr({
'aria-label': data.resources.minicartCountOfItems,
title: data.resources.minicartCountOfItems
});
if (data.totals.orderLevelDiscountTotal.value > 0) {
$('.order-discount').removeClass('hide-order-discount');
$('.order-discount-total').empty()
.append('- ' + data.totals.orderLevelDiscountTotal.formatted);
} else {
$('.order-discount').addClass('hide-order-discount');
}
if (data.totals.shippingLevelDiscountTotal.value > 0) {
$('.shipping-discount').removeClass('hide-shipping-discount');
$('.shipping-discount-total').empty().append('- ' +
data.totals.shippingLevelDiscountTotal.formatted);
} else {
$('.shipping-discount').addClass('hide-shipping-discount');
}
data.items.forEach(function (item) {
if (data.totals.orderLevelDiscountTotal.value > 0) {
$('.coupons-and-promos').empty().append(data.totals.discountsHtml);
}
if (item.renderedPromotions) {
$('.item-' + item.UUID).empty().append(item.renderedPromotions);
} else {
$('.item-' + item.UUID).empty();
}
$('.uuid-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice);
$('.line-item-price-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice);
$('.item-total-' + item.UUID).empty().append(item.priceTotal.renderedPrice);
});
}
/**
* re-renders the order totals and the number of items in the cart
* @param {Object} message - Error message to display
*/
function createErrorNotification(message) {
var errorHtml = '<div class="alert alert-danger alert-dismissible valid-cart-error ' +
'fade show" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">&times;</span>' +
'</button>' + message + '</div>';
$('.cart-error').append(errorHtml);
}
/**
* re-renders the approaching discount messages
* @param {Object} approachingDiscounts - updated approaching discounts for the cart
*/
function updateApproachingDiscounts(approachingDiscounts) {
var html = '';
$('.approaching-discounts').empty();
if (approachingDiscounts.length > 0) {
approachingDiscounts.forEach(function (item) {
html += '<div class="single-approaching-discount text-center">'
+ item.discountMsg + '</div>';
});
}
$('.approaching-discounts').append(html);
}
/**
* Updates the availability of a product line item
* @param {Object} data - AJAX response from the server
* @param {string} uuid - The uuid of the product line item to update
*/
function updateAvailability(data, uuid) {
var lineItem;
var messages = '';
for (var i = 0; i < data.items.length; i++) {
if (data.items[i].UUID === uuid) {
lineItem = data.items[i];
break;
}
}
if (lineItem != null) {
$('.availability-' + lineItem.UUID).empty();
if (lineItem.availability) {
if (lineItem.availability.messages) {
lineItem.availability.messages.forEach(function (message) {
messages += '<p class="line-item-attributes">' + message + '</p>';
});
}
if (lineItem.availability.inStockDate) {
messages += '<p class="line-item-attributes line-item-instock-date">'
+ lineItem.availability.inStockDate
+ '</p>';
}
}
$('.availability-' + lineItem.UUID).html(messages);
}
}
/**
* Finds an element in the array that matches search parameter
* @param {array} array - array of items to search
* @param {function} match - function that takes an element and returns a boolean indicating if the match is made
* @returns {Object|null} - returns an element of the array that matched the query.
*/
function findItem(array, match) { // eslint-disable-line no-unused-vars
for (var i = 0, l = array.length; i < l; i++) {
if (match.call(this, array[i])) {
return array[i];
}
}
return null;
}
/**
* Updates details of a product line item
* @param {Object} data - AJAX response from the server
* @param {string} uuid - The uuid of the product line item to update
*/
function updateProductDetails(data, uuid) {
$('.card.product-info.uuid-' + uuid).replaceWith(data.renderedTemplate);
}
/**
* Generates the modal window on the first call.
*
*/
function getModalHtmlElement() {
if ($('#editProductModal').length !== 0) {
$('#editProductModal').remove();
}
var htmlString = '<!-- Modal -->'
+ '<div class="modal fade" id="editProductModal" tabindex="-1" role="dialog">'
+ '<span class="enter-message sr-only" ></span>'
+ '<div class="modal-dialog quick-view-dialog">'
+ '<!-- Modal content-->'
+ '<div class="modal-content">'
+ '<div class="modal-header">'
+ ' <button type="button" class="close pull-right" data-dismiss="modal">'
+ ' <span aria-hidden="true">&times;</span>'
+ ' <span class="sr-only"> </span>'
+ ' </button>'
+ '</div>'
+ '<div class="modal-body"></div>'
+ '<div class="modal-footer"></div>'
+ '</div>'
+ '</div>'
+ '</div>';
$('body').append(htmlString);
}
/**
* Parses the html for a modal window
* @param {string} html - representing the body and footer of the modal window
*
* @return {Object} - Object with properties body and footer.
*/
function parseHtml(html) {
var $html = $('<div>').append($.parseHTML(html));
var body = $html.find('.product-quickview');
var footer = $html.find('.modal-footer').children();
return { body: body, footer: footer };
}
/**
* replaces the content in the modal window for product variation to be edited.
* @param {string} editProductUrl - url to be used to retrieve a new product model
*/
function fillModalElement(editProductUrl) {
$('.modal-body').spinner().start();
$.ajax({
url: editProductUrl,
method: 'GET',
dataType: 'json',
success: function (data) {
var parsedHtml = parseHtml(data.renderedTemplate);
$('#editProductModal .modal-body').empty();
$('#editProductModal .modal-body').html(parsedHtml.body);
$('#editProductModal .modal-footer').html(parsedHtml.footer);
$('#editProductModal .modal-header .close .sr-only').text(data.closeButtonText);
$('#editProductModal .enter-message').text(data.enterDialogMessage);
$('#editProductModal').modal('show');
$('body').trigger('editproductmodal:ready');
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
}
/**
* replace content of modal
* @param {string} actionUrl - url to be used to remove product
* @param {string} productID - pid
* @param {string} productName - product name
* @param {string} uuid - uuid
*/
function confirmDelete(actionUrl, productID, productName, uuid) {
var $deleteConfirmBtn = $('.cart-delete-confirmation-btn');
var $productToRemoveSpan = $('.product-to-remove');
$deleteConfirmBtn.data('pid', productID);
$deleteConfirmBtn.data('action', actionUrl);
$deleteConfirmBtn.data('uuid', uuid);
$productToRemoveSpan.empty().append(productName);
}
module.exports = function () {
$('body').on('click', '.remove-product', function (e) {
e.preventDefault();
var actionUrl = $(this).data('action');
var productID = $(this).data('pid');
var productName = $(this).data('name');
var uuid = $(this).data('uuid');
confirmDelete(actionUrl, productID, productName, uuid);
});
$('body').on('afterRemoveFromCart', function (e, data) {
e.preventDefault();
confirmDelete(data.actionUrl, data.productID, data.productName, data.uuid);
});
$('.optional-promo').click(function (e) {
e.preventDefault();
$('.promo-code-form').toggle();
});
$('body').on('click', '.cart-delete-confirmation-btn', function (e) {
e.preventDefault();
var productID = $(this).data('pid');
var url = $(this).data('action');
var uuid = $(this).data('uuid');
var urlParams = {
pid: productID,
uuid: uuid
};
url = appendToUrl(url, urlParams);
$('body > .modal-backdrop').remove();
$.spinner().start();
$('body').trigger('cart:beforeUpdate');
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
if (data.basket.items.length === 0) {
$('.cart').empty().append('<div class="row"> ' +
'<div class="col-12 text-center"> ' +
'<h1>' + data.basket.resources.emptyCartMsg + '</h1> ' +
'</div> ' +
'</div>'
);
$('.number-of-items').empty().append(data.basket.resources.numberOfItems);
$('.minicart-quantity').empty().append(data.basket.numItems);
$('.minicart-link').attr({
'aria-label': data.basket.resources.minicartCountOfItems,
title: data.basket.resources.minicartCountOfItems
});
$('.minicart .popover').empty();
$('.minicart .popover').removeClass('show');
$('body').removeClass('modal-open');
$('html').removeClass('veiled');
} else {
if (data.toBeDeletedUUIDs && data.toBeDeletedUUIDs.length > 0) {
for (var i = 0; i < data.toBeDeletedUUIDs.length; i++) {
$('.uuid-' + data.toBeDeletedUUIDs[i]).remove();
}
}
$('.uuid-' + uuid).remove();
if (!data.basket.hasBonusProduct) {
$('.bonus-product').remove();
}
$('.coupons-and-promos').empty().append(data.basket.totals.discountsHtml);
updateCartTotals(data.basket);
updateApproachingDiscounts(data.basket.approachingDiscounts);
$('body').trigger('setShippingMethodSelection', data.basket);
validateBasket(data.basket);
}
$('body').trigger('cart:update', data);
$.spinner().stop();
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
$.spinner().stop();
}
}
});
});
$('body').on('change', '.quantity-form > .quantity', function () {
var preSelectQty = $(this).data('pre-select-qty');
var quantity = $(this).val();
var productID = $(this).data('pid');
var url = $(this).data('action');
var uuid = $(this).data('uuid');
var urlParams = {
pid: productID,
quantity: quantity,
uuid: uuid
};
url = appendToUrl(url, urlParams);
$(this).parents('.card').spinner().start();
$('body').trigger('cart:beforeUpdate');
$.ajax({
url: url,
type: 'get',
context: this,
dataType: 'json',
success: function (data) {
$('.quantity[data-uuid="' + uuid + '"]').val(quantity);
$('.coupons-and-promos').empty().append(data.totals.discountsHtml);
updateCartTotals(data);
updateApproachingDiscounts(data.approachingDiscounts);
updateAvailability(data, uuid);
validateBasket(data);
$(this).data('pre-select-qty', quantity);
$('body').trigger('cart:update', data);
$.spinner().stop();
if ($(this).parents('.product-info').hasClass('bonus-product-line-item') && $('.cart-page').length) {
location.reload();
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
$(this).val(parseInt(preSelectQty, 10));
$.spinner().stop();
}
}
});
});
$('.shippingMethods').change(function () {
var url = $(this).attr('data-actionUrl');
var urlParams = {
methodID: $(this).find(':selected').attr('data-shipping-id')
};
// url = appendToUrl(url, urlParams);
$('.totals').spinner().start();
$('body').trigger('cart:beforeShippingMethodSelected');
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: urlParams,
success: function (data) {
if (data.error) {
window.location.href = data.redirectUrl;
} else {
$('.coupons-and-promos').empty().append(data.totals.discountsHtml);
updateCartTotals(data);
updateApproachingDiscounts(data.approachingDiscounts);
validateBasket(data);
}
$('body').trigger('cart:shippingMethodSelected', data);
$.spinner().stop();
},
error: function (err) {
if (err.redirectUrl) {
window.location.href = err.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
$.spinner().stop();
}
}
});
});
$('.promo-code-form').submit(function (e) {
e.preventDefault();
$.spinner().start();
$('.coupon-missing-error').hide();
$('.coupon-error-message').empty();
if (!$('.coupon-code-field').val()) {
$('.promo-code-form .form-control').addClass('is-invalid');
$('.promo-code-form .form-control').attr('aria-describedby', 'missingCouponCode');
$('.coupon-missing-error').show();
$.spinner().stop();
return false;
}
var $form = $('.promo-code-form');
$('.promo-code-form .form-control').removeClass('is-invalid');
$('.coupon-error-message').empty();
$('body').trigger('promotion:beforeUpdate');
$.ajax({
url: $form.attr('action'),
type: 'GET',
dataType: 'json',
data: $form.serialize(),
success: function (data) {
if (data.error) {
$('.promo-code-form .form-control').addClass('is-invalid');
$('.promo-code-form .form-control').attr('aria-describedby', 'invalidCouponCode');
$('.coupon-error-message').empty().append(data.errorMessage);
$('body').trigger('promotion:error', data);
} else {
$('.coupons-and-promos').empty().append(data.totals.discountsHtml);
updateCartTotals(data);
updateApproachingDiscounts(data.approachingDiscounts);
validateBasket(data);
$('body').trigger('promotion:success', data);
}
$('.coupon-code-field').val('');
$.spinner().stop();
},
error: function (err) {
$('body').trigger('promotion:error', err);
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.errorMessage);
$.spinner().stop();
}
}
});
return false;
});
$('body').on('click', '.remove-coupon', function (e) {
e.preventDefault();
var couponCode = $(this).data('code');
var uuid = $(this).data('uuid');
var $deleteConfirmBtn = $('.delete-coupon-confirmation-btn');
var $productToRemoveSpan = $('.coupon-to-remove');
$deleteConfirmBtn.data('uuid', uuid);
$deleteConfirmBtn.data('code', couponCode);
$productToRemoveSpan.empty().append(couponCode);
});
$('body').on('click', '.delete-coupon-confirmation-btn', function (e) {
e.preventDefault();
var url = $(this).data('action');
var uuid = $(this).data('uuid');
var couponCode = $(this).data('code');
var urlParams = {
code: couponCode,
uuid: uuid
};
url = appendToUrl(url, urlParams);
$('body > .modal-backdrop').remove();
$.spinner().start();
$('body').trigger('promotion:beforeUpdate');
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
$('.coupon-uuid-' + uuid).remove();
updateCartTotals(data);
updateApproachingDiscounts(data.approachingDiscounts);
validateBasket(data);
$.spinner().stop();
$('body').trigger('promotion:success', data);
},
error: function (err) {
$('body').trigger('promotion:error', err);
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
$.spinner().stop();
}
}
});
});
$('body').on('click', '.cart-page .bonus-product-button', function () {
$.spinner().start();
$(this).addClass('launched-modal');
$.ajax({
url: $(this).data('url'),
method: 'GET',
dataType: 'json',
success: function (data) {
base.methods.editBonusProducts(data);
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
});
$('body').on('hidden.bs.modal', '#chooseBonusProductModal', function () {
$('#chooseBonusProductModal').remove();
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
if ($('.cart-page').length) {
$('.launched-modal .btn-outline-primary').trigger('focus');
$('.launched-modal').removeClass('launched-modal');
} else {
$('.product-detail .add-to-cart').focus();
}
});
$('body').on('click', '.cart-page .product-edit .edit, .cart-page .bundle-edit .edit', function (e) {
e.preventDefault();
var editProductUrl = $(this).attr('href');
getModalHtmlElement();
fillModalElement(editProductUrl);
});
$('body').on('shown.bs.modal', '#editProductModal', function () {
$('#editProductModal').siblings().attr('aria-hidden', 'true');
$('#editProductModal .close').focus();
});
$('body').on('hidden.bs.modal', '#editProductModal', function () {
$('#editProductModal').siblings().attr('aria-hidden', 'false');
});
$('body').on('keydown', '#editProductModal', function (e) {
var focusParams = {
event: e,
containerSelector: '#editProductModal',
firstElementSelector: '.close',
lastElementSelector: '.update-cart-product-global',
nextToLastElementSelector: '.modal-footer .quantity-select'
};
focusHelper.setTabNextFocus(focusParams);
});
$('body').on('product:updateAddToCart', function (e, response) {
// update global add to cart (single products, bundles)
var dialog = $(response.$productContainer)
.closest('.quick-view-dialog');
$('.update-cart-product-global', dialog).attr('disabled',
!$('.global-availability', dialog).data('ready-to-order')
|| !$('.global-availability', dialog).data('available')
);
});
$('body').on('product:updateAvailability', function (e, response) {
// bundle individual products
$('.product-availability', response.$productContainer)
.data('ready-to-order', response.product.readyToOrder)
.data('available', response.product.available)
.find('.availability-msg')
.empty()
.html(response.message);
var dialog = $(response.$productContainer)
.closest('.quick-view-dialog');
if ($('.product-availability', dialog).length) {
// bundle all products
var allAvailable = $('.product-availability', dialog).toArray()
.every(function (item) { return $(item).data('available'); });
var allReady = $('.product-availability', dialog).toArray()
.every(function (item) { return $(item).data('ready-to-order'); });
$('.global-availability', dialog)
.data('ready-to-order', allReady)
.data('available', allAvailable);
$('.global-availability .availability-msg', dialog).empty()
.html(allReady ? response.message : response.resources.info_selectforstock);
} else {
// single product
$('.global-availability', dialog)
.data('ready-to-order', response.product.readyToOrder)
.data('available', response.product.available)
.find('.availability-msg')
.empty()
.html(response.message);
}
});
$('body').on('product:afterAttributeSelect', function (e, response) {
if ($('.modal.show .product-quickview .bundle-items').length) {
$('.modal.show').find(response.container).data('pid', response.data.product.id);
$('.modal.show').find(response.container).find('.product-id').text(response.data.product.id);
} else {
$('.modal.show .product-quickview').data('pid', response.data.product.id);
}
});
$('body').on('change', '.quantity-select', function () {
var selectedQuantity = $(this).val();
$('.modal.show .update-cart-url').data('selected-quantity', selectedQuantity);
});
$('body').on('change', '.options-select', function () {
var selectedOptionValueId = $(this).children('option:selected').data('value-id');
$('.modal.show .update-cart-url').data('selected-option', selectedOptionValueId);
});
$('body').on('click', '.update-cart-product-global', function (e) {
e.preventDefault();
var updateProductUrl = $(this).closest('.cart-and-ipay').find('.update-cart-url').val();
var selectedQuantity = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('selected-quantity');
var selectedOptionValueId = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('selected-option');
var uuid = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('uuid');
var form = {
uuid: uuid,
pid: base.getPidValue($(this)),
quantity: selectedQuantity,
selectedOptionValueId: selectedOptionValueId
};
$(this).parents('.card').spinner().start();
$('body').trigger('cart:beforeUpdate');
if (updateProductUrl) {
$.ajax({
url: updateProductUrl,
type: 'post',
context: this,
data: form,
dataType: 'json',
success: function (data) {
$('#editProductModal').modal('hide');
$('.coupons-and-promos').empty().append(data.cartModel.totals.discountsHtml);
updateCartTotals(data.cartModel);
updateApproachingDiscounts(data.cartModel.approachingDiscounts);
updateAvailability(data.cartModel, uuid);
updateProductDetails(data, uuid);
if (data.uuidToBeDeleted) {
$('.uuid-' + data.uuidToBeDeleted).remove();
}
validateBasket(data.cartModel);
$('body').trigger('cart:update', data);
$.spinner().stop();
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification(err.responseJSON.errorMessage);
$.spinner().stop();
}
}
});
}
});
base.selectAttribute();
base.colorAttribute();
base.removeBonusProduct();
base.selectBonusProduct();
base.enableBonusProductSelection();
base.showMoreBonusProducts();
base.addBonusProductsToCart();
base.focusChooseBonusProductModal();
base.trapChooseBonusProductModalFocus();
base.onClosingChooseBonusProductModal();
};

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./checkout/checkout'));
});

View File

@ -0,0 +1,187 @@
'use strict';
/**
* Populate the Billing Address Summary View
* @param {string} parentSelector - the top level DOM selector for a unique address summary
* @param {Object} address - the address data
*/
function populateAddressSummary(parentSelector, address) {
$.each(address, function (attr) {
var val = address[attr];
$('.' + attr, parentSelector).text(val || '');
});
}
/**
* returns a formed <option /> element
* @param {Object} shipping - the shipping object (shipment model)
* @param {boolean} selected - current shipping is selected (for PLI)
* @param {order} order - the Order model
* @param {Object} [options] - options
* @returns {Object} - the jQuery / DOMElement
*/
function optionValueForAddress(shipping, selected, order, options) {
var safeOptions = options || {};
var isBilling = safeOptions.type && safeOptions.type === 'billing';
var className = safeOptions.className || '';
var isSelected = selected;
var isNew = !shipping;
if (typeof shipping === 'string') {
return $('<option class="' + className + '" disabled>' + shipping + '</option>');
}
var safeShipping = shipping || {};
var shippingAddress = safeShipping.shippingAddress || {};
if (isBilling && isNew && !order.billing.matchingAddressId) {
shippingAddress = order.billing.billingAddress.address || {};
isNew = false;
isSelected = true;
safeShipping.UUID = 'manual-entry';
}
var uuid = safeShipping.UUID ? safeShipping.UUID : 'new';
var optionEl = $('<option class="' + className + '" />');
optionEl.val(uuid);
var title;
if (isNew) {
title = order.resources.addNewAddress;
} else {
title = [];
if (shippingAddress.firstName) {
title.push(shippingAddress.firstName);
}
if (shippingAddress.lastName) {
title.push(shippingAddress.lastName);
}
if (shippingAddress.address1) {
title.push(shippingAddress.address1);
}
if (shippingAddress.address2) {
title.push(shippingAddress.address2);
}
if (shippingAddress.city) {
if (shippingAddress.state) {
title.push(shippingAddress.city + ',');
} else {
title.push(shippingAddress.city);
}
}
if (shippingAddress.stateCode) {
title.push(shippingAddress.stateCode);
}
if (shippingAddress.postalCode) {
title.push(shippingAddress.postalCode);
}
if (!isBilling && safeShipping.selectedShippingMethod) {
title.push('-');
title.push(safeShipping.selectedShippingMethod.displayName);
}
if (title.length > 2) {
title = title.join(' ');
} else {
title = order.resources.newAddress;
}
}
optionEl.text(title);
var keyMap = {
'data-first-name': 'firstName',
'data-last-name': 'lastName',
'data-address1': 'address1',
'data-address2': 'address2',
'data-city': 'city',
'data-state-code': 'stateCode',
'data-postal-code': 'postalCode',
'data-country-code': 'countryCode',
'data-phone': 'phone'
};
$.each(keyMap, function (key) {
var mappedKey = keyMap[key];
var mappedValue = shippingAddress[mappedKey];
// In case of country code
if (mappedValue && typeof mappedValue === 'object') {
mappedValue = mappedValue.value;
}
optionEl.attr(key, mappedValue || '');
});
var giftObj = {
'data-is-gift': 'isGift',
'data-gift-message': 'giftMessage'
};
$.each(giftObj, function (key) {
var mappedKey = giftObj[key];
var mappedValue = safeShipping[mappedKey];
optionEl.attr(key, mappedValue || '');
});
if (isSelected) {
optionEl.attr('selected', true);
}
return optionEl;
}
/**
* returns address properties from a UI form
* @param {Form} form - the Form element
* @returns {Object} - a JSON object with all values
*/
function getAddressFieldsFromUI(form) {
var address = {
firstName: $('input[name$=_firstName]', form).val(),
lastName: $('input[name$=_lastName]', form).val(),
address1: $('input[name$=_address1]', form).val(),
address2: $('input[name$=_address2]', form).val(),
city: $('input[name$=_city]', form).val(),
postalCode: $('input[name$=_postalCode]', form).val(),
stateCode: $('select[name$=_stateCode],input[name$=_stateCode]', form).val(),
countryCode: $('select[name$=_country]', form).val(),
phone: $('input[name$=_phone]', form).val()
};
return address;
}
module.exports = {
methods: {
populateAddressSummary: populateAddressSummary,
optionValueForAddress: optionValueForAddress,
getAddressFieldsFromUI: getAddressFieldsFromUI
},
showDetails: function () {
$('.btn-show-details').on('click', function () {
var form = $(this).closest('form');
form.attr('data-address-mode', 'details');
form.find('.multi-ship-address-actions').removeClass('d-none');
form.find('.multi-ship-action-buttons .col-12.btn-save-multi-ship').addClass('d-none');
});
},
addNewAddress: function () {
$('.btn-add-new').on('click', function () {
var $el = $(this);
if ($el.parents('#dwfrm_billing').length > 0) {
// Handle billing address case
$('body').trigger('checkout:clearBillingForm');
var $option = $($el.parents('form').find('.addressSelector option')[0]);
$option.attr('value', 'new');
var $newTitle = $('#dwfrm_billing input[name=localizedNewAddressTitle]').val();
$option.text($newTitle);
$option.prop('selected', 'selected');
$el.parents('[data-address-mode]').attr('data-address-mode', 'new');
} else {
// Handle shipping address case
var $newEl = $el.parents('form').find('.addressSelector option[value=new]');
$newEl.prop('selected', 'selected');
$newEl.parent().trigger('change');
}
});
}
};

View File

@ -0,0 +1,327 @@
'use strict';
var addressHelpers = require('./address');
var cleave = require('../components/cleave');
/**
* updates the billing address selector within billing forms
* @param {Object} order - the order model
* @param {Object} customer - the customer model
*/
function updateBillingAddressSelector(order, customer) {
var shippings = order.shipping;
var form = $('form[name$=billing]')[0];
var $billingAddressSelector = $('.addressSelector', form);
var hasSelectedAddress = false;
if ($billingAddressSelector && $billingAddressSelector.length === 1) {
$billingAddressSelector.empty();
// Add New Address option
$billingAddressSelector.append(addressHelpers.methods.optionValueForAddress(
null,
false,
order,
{ type: 'billing' }));
// Separator -
$billingAddressSelector.append(addressHelpers.methods.optionValueForAddress(
order.resources.shippingAddresses, false, order, {
// className: 'multi-shipping',
type: 'billing'
}
));
shippings.forEach(function (aShipping) {
var isSelected = order.billing.matchingAddressId === aShipping.UUID;
hasSelectedAddress = hasSelectedAddress || isSelected;
// Shipping Address option
$billingAddressSelector.append(
addressHelpers.methods.optionValueForAddress(aShipping, isSelected, order,
{
// className: 'multi-shipping',
type: 'billing'
}
)
);
});
if (customer.addresses && customer.addresses.length > 0) {
$billingAddressSelector.append(addressHelpers.methods.optionValueForAddress(
order.resources.accountAddresses, false, order));
customer.addresses.forEach(function (address) {
var isSelected = order.billing.matchingAddressId === address.ID;
hasSelectedAddress = hasSelectedAddress || isSelected;
// Customer Address option
$billingAddressSelector.append(
addressHelpers.methods.optionValueForAddress({
UUID: 'ab_' + address.ID,
shippingAddress: address
}, isSelected, order, { type: 'billing' })
);
});
}
}
if (hasSelectedAddress
|| (!order.billing.matchingAddressId && order.billing.billingAddress.address)) {
// show
$(form).attr('data-address-mode', 'edit');
} else {
$(form).attr('data-address-mode', 'new');
}
$billingAddressSelector.show();
}
/**
* Updates the billing address form values within payment forms without any payment instrument validation
* @param {Object} order - the order model
*/
function updateBillingAddress(order) {
var billing = order.billing;
if (!billing.billingAddress || !billing.billingAddress.address) return;
var form = $('form[name=dwfrm_billing]');
if (!form) return;
$('input[name$=_firstName]', form).val(billing.billingAddress.address.firstName);
$('input[name$=_lastName]', form).val(billing.billingAddress.address.lastName);
$('input[name$=_address1]', form).val(billing.billingAddress.address.address1);
$('input[name$=_address2]', form).val(billing.billingAddress.address.address2);
$('input[name$=_city]', form).val(billing.billingAddress.address.city);
$('input[name$=_postalCode]', form).val(billing.billingAddress.address.postalCode);
$('select[name$=_stateCode],input[name$=_stateCode]', form)
.val(billing.billingAddress.address.stateCode);
$('select[name$=_country]', form).val(billing.billingAddress.address.countryCode.value);
$('input[name$=_phone]', form).val(billing.billingAddress.address.phone);
$('input[name$=_email]', form).val(order.orderEmail);
}
/**
* Validate and update payment instrument form fields
* @param {Object} order - the order model
*/
function validateAndUpdateBillingPaymentInstrument(order) {
var billing = order.billing;
if (!billing.payment || !billing.payment.selectedPaymentInstruments
|| billing.payment.selectedPaymentInstruments.length <= 0) return;
var form = $('form[name=dwfrm_billing]');
if (!form) return;
var instrument = billing.payment.selectedPaymentInstruments[0];
$('select[name$=expirationMonth]', form).val(instrument.expirationMonth);
$('select[name$=expirationYear]', form).val(instrument.expirationYear);
// Force security code and card number clear
$('input[name$=securityCode]', form).val('');
$('input[name$=cardNumber]').data('cleave').setRawValue('');
}
/**
* Updates the billing address form values within payment forms
* @param {Object} order - the order model
*/
function updateBillingAddressFormValues(order) {
module.exports.methods.updateBillingAddress(order);
module.exports.methods.validateAndUpdateBillingPaymentInstrument(order);
}
/**
* clears the billing address form values
*/
function clearBillingAddressFormValues() {
updateBillingAddressFormValues({
billing: {
billingAddress: {
address: {
countryCode: {}
}
}
}
});
}
/**
* update billing address summary and contact information
* @param {Object} order - checkout model to use as basis of new truth
*/
function updateBillingAddressSummary(order) {
// update billing address summary
addressHelpers.methods.populateAddressSummary('.billing .address-summary',
order.billing.billingAddress.address);
// update billing parts of order summary
$('.order-summary-email').text(order.orderEmail);
if (order.billing.billingAddress.address) {
$('.order-summary-phone').text(order.billing.billingAddress.address.phone);
}
}
/**
* Updates the billing information in checkout, based on the supplied order model
* @param {Object} order - checkout model to use as basis of new truth
* @param {Object} customer - customer model to use as basis of new truth
* @param {Object} [options] - options
*/
function updateBillingInformation(order, customer) {
updateBillingAddressSelector(order, customer);
// update billing address form
updateBillingAddressFormValues(order);
// update billing address summary and billing parts of order summary
updateBillingAddressSummary(order);
}
/**
* Updates the payment information in checkout, based on the supplied order model
* @param {Object} order - checkout model to use as basis of new truth
*/
function updatePaymentInformation(order) {
// update payment details
var $paymentSummary = $('.payment-details');
var htmlToAppend = '';
if (order.billing.payment && order.billing.payment.selectedPaymentInstruments
&& order.billing.payment.selectedPaymentInstruments.length > 0) {
htmlToAppend += '<span>' + order.resources.cardType + ' '
+ order.billing.payment.selectedPaymentInstruments[0].type
+ '</span><div>'
+ order.billing.payment.selectedPaymentInstruments[0].maskedCreditCardNumber
+ '</div><div><span>'
+ order.resources.cardEnding + ' '
+ order.billing.payment.selectedPaymentInstruments[0].expirationMonth
+ '/' + order.billing.payment.selectedPaymentInstruments[0].expirationYear
+ '</span></div>';
}
$paymentSummary.empty().append(htmlToAppend);
}
/**
* clears the credit card form
*/
function clearCreditCardForm() {
$('input[name$="_cardNumber"]').data('cleave').setRawValue('');
$('select[name$="_expirationMonth"]').val('');
$('select[name$="_expirationYear"]').val('');
$('input[name$="_securityCode"]').val('');
}
module.exports = {
methods: {
updateBillingAddressSelector: updateBillingAddressSelector,
updateBillingAddressFormValues: updateBillingAddressFormValues,
clearBillingAddressFormValues: clearBillingAddressFormValues,
updateBillingInformation: updateBillingInformation,
updatePaymentInformation: updatePaymentInformation,
clearCreditCardForm: clearCreditCardForm,
updateBillingAddress: updateBillingAddress,
validateAndUpdateBillingPaymentInstrument: validateAndUpdateBillingPaymentInstrument,
updateBillingAddressSummary: updateBillingAddressSummary
},
showBillingDetails: function () {
$('.btn-show-billing-details').on('click', function () {
$(this).parents('[data-address-mode]').attr('data-address-mode', 'new');
});
},
hideBillingDetails: function () {
$('.btn-hide-billing-details').on('click', function () {
$(this).parents('[data-address-mode]').attr('data-address-mode', 'shipment');
});
},
selectBillingAddress: function () {
$('.payment-form .addressSelector').on('change', function () {
var form = $(this).parents('form')[0];
var selectedOption = $('option:selected', this);
var optionID = selectedOption[0].value;
if (optionID === 'new') {
// Show Address
$(form).attr('data-address-mode', 'new');
} else {
// Hide Address
$(form).attr('data-address-mode', 'shipment');
}
// Copy fields
var attrs = selectedOption.data();
var element;
Object.keys(attrs).forEach(function (attr) {
element = attr === 'countryCode' ? 'country' : attr;
if (element === 'cardNumber') {
$('.cardNumber').data('cleave').setRawValue(attrs[attr]);
} else {
$('[name$=' + element + ']', form).val(attrs[attr]);
}
});
});
},
handleCreditCardNumber: function () {
cleave.handleCreditCardNumber('.cardNumber', '#cardType');
},
santitizeForm: function () {
$('body').on('checkout:serializeBilling', function (e, data) {
var serializedForm = cleave.serializeData(data.form);
data.callback(serializedForm);
});
},
selectSavedPaymentInstrument: function () {
$(document).on('click', '.saved-payment-instrument', function (e) {
e.preventDefault();
$('.saved-payment-security-code').val('');
$('.saved-payment-instrument').removeClass('selected-payment');
$(this).addClass('selected-payment');
$('.saved-payment-instrument .card-image').removeClass('checkout-hidden');
$('.saved-payment-instrument .security-code-input').addClass('checkout-hidden');
$('.saved-payment-instrument.selected-payment' +
' .card-image').addClass('checkout-hidden');
$('.saved-payment-instrument.selected-payment ' +
'.security-code-input').removeClass('checkout-hidden');
});
},
addNewPaymentInstrument: function () {
$('.btn.add-payment').on('click', function (e) {
e.preventDefault();
$('.payment-information').data('is-new-payment', true);
clearCreditCardForm();
$('.credit-card-form').removeClass('checkout-hidden');
$('.user-payment-instruments').addClass('checkout-hidden');
});
},
cancelNewPayment: function () {
$('.cancel-new-payment').on('click', function (e) {
e.preventDefault();
$('.payment-information').data('is-new-payment', false);
clearCreditCardForm();
$('.user-payment-instruments').removeClass('checkout-hidden');
$('.credit-card-form').addClass('checkout-hidden');
});
},
clearBillingForm: function () {
$('body').on('checkout:clearBillingForm', function () {
clearBillingAddressFormValues();
});
},
paymentTabs: function () {
$('.payment-options .nav-item').on('click', function (e) {
e.preventDefault();
var methodID = $(this).data('method-id');
$('.payment-information').data('payment-method-id', methodID);
});
}
};

View File

@ -0,0 +1,633 @@
'use strict';
var customerHelpers = require('./customer');
var addressHelpers = require('./address');
var shippingHelpers = require('./shipping');
var billingHelpers = require('./billing');
var summaryHelpers = require('./summary');
var formHelpers = require('./formErrors');
var scrollAnimate = require('../components/scrollAnimate');
/**
* Create the jQuery Checkout Plugin.
*
* This jQuery plugin will be registered on the dom element in checkout.isml with the
* id of "checkout-main".
*
* The checkout plugin will handle the different state the user interface is in as the user
* progresses through the varying forms such as shipping and payment.
*
* Billing info and payment info are used a bit synonymously in this code.
*
*/
(function ($) {
$.fn.checkout = function () { // eslint-disable-line
var plugin = this;
//
// Collect form data from user input
//
var formData = {
// Customer Data
customer: {},
// Shipping Address
shipping: {},
// Billing Address
billing: {},
// Payment
payment: {},
// Gift Codes
giftCode: {}
};
//
// The different states/stages of checkout
//
var checkoutStages = [
'customer',
'shipping',
'payment',
'placeOrder',
'submitted'
];
/**
* Updates the URL to determine stage
* @param {number} currentStage - The current stage the user is currently on in the checkout
*/
function updateUrl(currentStage) {
history.pushState(
checkoutStages[currentStage],
document.title,
location.pathname
+ '?stage='
+ checkoutStages[currentStage]
+ '#'
+ checkoutStages[currentStage]
);
}
//
// Local member methods of the Checkout plugin
//
var members = {
// initialize the currentStage variable for the first time
currentStage: 0,
/**
* Set or update the checkout stage (AKA the shipping, billing, payment, etc... steps)
* @returns {Object} a promise
*/
updateStage: function () {
var stage = checkoutStages[members.currentStage];
var defer = $.Deferred(); // eslint-disable-line
if (stage === 'customer') {
//
// Clear Previous Errors
//
customerHelpers.methods.clearErrors();
//
// Submit the Customer Form
//
var customerFormSelector = customerHelpers.methods.isGuestFormActive() ? customerHelpers.vars.GUEST_FORM : customerHelpers.vars.REGISTERED_FORM;
var customerForm = $(customerFormSelector);
$.ajax({
url: customerForm.attr('action'),
type: 'post',
data: customerForm.serialize(),
success: function (data) {
if (data.redirectUrl) {
window.location.href = data.redirectUrl;
} else {
customerHelpers.methods.customerFormResponse(defer, data);
}
},
error: function (err) {
if (err.responseJSON && err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
// Server error submitting form
defer.reject(err.responseJSON);
}
});
return defer;
} else if (stage === 'shipping') {
//
// Clear Previous Errors
//
formHelpers.clearPreviousErrors('.shipping-form');
//
// Submit the Shipping Address Form
//
var isMultiShip = $('#checkout-main').hasClass('multi-ship');
var formSelector = isMultiShip ?
'.multi-shipping .active form' : '.single-shipping .shipping-form';
var form = $(formSelector);
if (isMultiShip && form.length === 0) {
// disable the next:Payment button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
// in case the multi ship form is already submitted
var url = $('#checkout-main').attr('data-checkout-get-url');
$.ajax({
url: url,
method: 'GET',
success: function (data) {
// enable the next:Payment button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
if (!data.error) {
$('body').trigger('checkout:updateCheckoutView',
{ order: data.order, customer: data.customer });
defer.resolve();
} else if (data.message && $('.shipping-error .alert-danger').length < 1) {
var errorMsg = data.message;
var errorHtml = '<div class="alert alert-danger alert-dismissible valid-cart-error ' +
'fade show" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">&times;</span>' +
'</button>' + errorMsg + '</div>';
$('.shipping-error').append(errorHtml);
scrollAnimate($('.shipping-error'));
defer.reject();
} else if (data.redirectUrl) {
window.location.href = data.redirectUrl;
}
},
error: function () {
// enable the next:Payment button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
// Server error submitting form
defer.reject();
}
});
} else {
var shippingFormData = form.serialize();
$('body').trigger('checkout:serializeShipping', {
form: form,
data: shippingFormData,
callback: function (data) {
shippingFormData = data;
}
});
// disable the next:Payment button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
$.ajax({
url: form.attr('action'),
type: 'post',
data: shippingFormData,
success: function (data) {
// enable the next:Payment button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
shippingHelpers.methods.shippingFormResponse(defer, data);
},
error: function (err) {
// enable the next:Payment button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
if (err.responseJSON && err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
// Server error submitting form
defer.reject(err.responseJSON);
}
});
}
return defer;
} else if (stage === 'payment') {
//
// Submit the Billing Address Form
//
formHelpers.clearPreviousErrors('.payment-form');
var billingAddressForm = $('#dwfrm_billing .billing-address-block :input').serialize();
$('body').trigger('checkout:serializeBilling', {
form: $('#dwfrm_billing .billing-address-block'),
data: billingAddressForm,
callback: function (data) {
if (data) {
billingAddressForm = data;
}
}
});
var contactInfoForm = $('#dwfrm_billing .contact-info-block :input').serialize();
$('body').trigger('checkout:serializeBilling', {
form: $('#dwfrm_billing .contact-info-block'),
data: contactInfoForm,
callback: function (data) {
if (data) {
contactInfoForm = data;
}
}
});
var activeTabId = $('.tab-pane.active').attr('id');
var paymentInfoSelector = '#dwfrm_billing .' + activeTabId + ' .payment-form-fields :input';
var paymentInfoForm = $(paymentInfoSelector).serialize();
$('body').trigger('checkout:serializeBilling', {
form: $(paymentInfoSelector),
data: paymentInfoForm,
callback: function (data) {
if (data) {
paymentInfoForm = data;
}
}
});
var paymentForm = billingAddressForm + '&' + contactInfoForm + '&' + paymentInfoForm;
if ($('.data-checkout-stage').data('customer-type') === 'registered') {
// if payment method is credit card
if ($('.payment-information').data('payment-method-id') === 'CREDIT_CARD') {
if (!($('.payment-information').data('is-new-payment'))) {
var cvvCode = $('.saved-payment-instrument.' +
'selected-payment .saved-payment-security-code').val();
if (cvvCode === '') {
var cvvElement = $('.saved-payment-instrument.' +
'selected-payment ' +
'.form-control');
cvvElement.addClass('is-invalid');
scrollAnimate(cvvElement);
defer.reject();
return defer;
}
var $savedPaymentInstrument = $('.saved-payment-instrument' +
'.selected-payment'
);
paymentForm += '&storedPaymentUUID=' +
$savedPaymentInstrument.data('uuid');
paymentForm += '&securityCode=' + cvvCode;
}
}
}
// disable the next:Place Order button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
$.ajax({
url: $('#dwfrm_billing').attr('action'),
method: 'POST',
data: paymentForm,
success: function (data) {
// enable the next:Place Order button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
// look for field validation errors
if (data.error) {
if (data.fieldErrors.length) {
data.fieldErrors.forEach(function (error) {
if (Object.keys(error).length) {
formHelpers.loadFormErrors('.payment-form', error);
}
});
}
if (data.serverErrors.length) {
data.serverErrors.forEach(function (error) {
$('.error-message').show();
$('.error-message-text').text(error);
scrollAnimate($('.error-message'));
});
}
if (data.cartError) {
window.location.href = data.redirectUrl;
}
defer.reject();
} else {
//
// Populate the Address Summary
//
$('body').trigger('checkout:updateCheckoutView',
{ order: data.order, customer: data.customer });
if (data.renderedPaymentInstruments) {
$('.stored-payments').empty().html(
data.renderedPaymentInstruments
);
}
if (data.customer.registeredUser
&& data.customer.customerPaymentInstruments.length
) {
$('.cancel-new-payment').removeClass('checkout-hidden');
}
scrollAnimate();
defer.resolve(data);
}
},
error: function (err) {
// enable the next:Place Order button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
if (err.responseJSON && err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
}
});
return defer;
} else if (stage === 'placeOrder') {
// disable the placeOrder button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
$.ajax({
url: $('.place-order').data('action'),
method: 'POST',
success: function (data) {
// enable the placeOrder button here
$('body').trigger('checkout:enableButton', '.next-step-button button');
if (data.error) {
if (data.cartError) {
window.location.href = data.redirectUrl;
defer.reject();
} else {
// go to appropriate stage and display error message
defer.reject(data);
}
} else {
var redirect = $('<form>')
.appendTo(document.body)
.attr({
method: 'POST',
action: data.continueUrl
});
$('<input>')
.appendTo(redirect)
.attr({
name: 'orderID',
value: data.orderID
});
$('<input>')
.appendTo(redirect)
.attr({
name: 'orderToken',
value: data.orderToken
});
redirect.submit();
defer.resolve(data);
}
},
error: function () {
// enable the placeOrder button here
$('body').trigger('checkout:enableButton', $('.next-step-button button'));
}
});
return defer;
}
var p = $('<div>').promise(); // eslint-disable-line
setTimeout(function () {
p.done(); // eslint-disable-line
}, 500);
return p; // eslint-disable-line
},
/**
* Initialize the checkout stage.
*
* TODO: update this to allow stage to be set from server?
*/
initialize: function () {
// set the initial state of checkout
members.currentStage = checkoutStages
.indexOf($('.data-checkout-stage').data('checkout-stage'));
$(plugin).attr('data-checkout-stage', checkoutStages[members.currentStage]);
$('body').on('click', '.submit-customer-login', function (e) {
e.preventDefault();
members.nextStage();
});
$('body').on('click', '.submit-customer', function (e) {
e.preventDefault();
members.nextStage();
});
//
// Handle Payment option selection
//
$('input[name$="paymentMethod"]', plugin).on('change', function () {
$('.credit-card-form').toggle($(this).val() === 'CREDIT_CARD');
});
//
// Handle Next State button click
//
$(plugin).on('click', '.next-step-button button', function () {
members.nextStage();
});
//
// Handle Edit buttons on shipping and payment summary cards
//
$('.customer-summary .edit-button', plugin).on('click', function () {
members.gotoStage('customer');
});
$('.shipping-summary .edit-button', plugin).on('click', function () {
if (!$('#checkout-main').hasClass('multi-ship')) {
$('body').trigger('shipping:selectSingleShipping');
}
members.gotoStage('shipping');
});
$('.payment-summary .edit-button', plugin).on('click', function () {
members.gotoStage('payment');
});
//
// remember stage (e.g. shipping)
//
updateUrl(members.currentStage);
//
// Listen for foward/back button press and move to correct checkout-stage
//
$(window).on('popstate', function (e) {
//
// Back button when event state less than current state in ordered
// checkoutStages array.
//
if (e.state === null ||
checkoutStages.indexOf(e.state) < members.currentStage) {
members.handlePrevStage(false);
} else if (checkoutStages.indexOf(e.state) > members.currentStage) {
// Forward button pressed
members.handleNextStage(false);
}
});
//
// Set the form data
//
plugin.data('formData', formData);
},
/**
* The next checkout state step updates the css for showing correct buttons etc...
*/
nextStage: function () {
var promise = members.updateStage();
promise.done(function () {
// Update UI with new stage
$('.error-message').hide();
members.handleNextStage(true);
});
promise.fail(function (data) {
// show errors
if (data) {
if (data.errorStage) {
members.gotoStage(data.errorStage.stage);
if (data.errorStage.step === 'billingAddress') {
var $billingAddressSameAsShipping = $(
'input[name$="_shippingAddressUseAsBillingAddress"]'
);
if ($billingAddressSameAsShipping.is(':checked')) {
$billingAddressSameAsShipping.prop('checked', false);
}
}
}
if (data.errorMessage) {
$('.error-message').show();
$('.error-message-text').text(data.errorMessage);
}
}
});
},
/**
* The next checkout state step updates the css for showing correct buttons etc...
*
* @param {boolean} bPushState - boolean when true pushes state using the history api.
*/
handleNextStage: function (bPushState) {
if (members.currentStage < checkoutStages.length - 1) {
// move stage forward
members.currentStage++;
//
// show new stage in url (e.g.payment)
//
if (bPushState) {
updateUrl(members.currentStage);
}
}
// Set the next stage on the DOM
$(plugin).attr('data-checkout-stage', checkoutStages[members.currentStage]);
},
/**
* Previous State
*/
handlePrevStage: function () {
if (members.currentStage > 0) {
// move state back
members.currentStage--;
updateUrl(members.currentStage);
}
$(plugin).attr('data-checkout-stage', checkoutStages[members.currentStage]);
},
/**
* Use window history to go to a checkout stage
* @param {string} stageName - the checkout state to goto
*/
gotoStage: function (stageName) {
members.currentStage = checkoutStages.indexOf(stageName);
updateUrl(members.currentStage);
$(plugin).attr('data-checkout-stage', checkoutStages[members.currentStage]);
}
};
//
// Initialize the checkout
//
members.initialize();
return this;
};
}(jQuery));
var exports = {
initialize: function () {
$('#checkout-main').checkout();
},
updateCheckoutView: function () {
$('body').on('checkout:updateCheckoutView', function (e, data) {
if (data.csrfToken) {
$("input[name*='csrf_token']").val(data.csrfToken);
}
customerHelpers.methods.updateCustomerInformation(data.customer, data.order);
shippingHelpers.methods.updateMultiShipInformation(data.order);
summaryHelpers.updateTotals(data.order.totals);
data.order.shipping.forEach(function (shipping) {
shippingHelpers.methods.updateShippingInformation(
shipping,
data.order,
data.customer,
data.options
);
});
billingHelpers.methods.updateBillingInformation(
data.order,
data.customer,
data.options
);
billingHelpers.methods.updatePaymentInformation(data.order, data.options);
summaryHelpers.updateOrderProductSummaryInformation(data.order, data.options);
});
},
disableButton: function () {
$('body').on('checkout:disableButton', function (e, button) {
$(button).prop('disabled', true);
});
},
enableButton: function () {
$('body').on('checkout:enableButton', function (e, button) {
$(button).prop('disabled', false);
});
}
};
[customerHelpers, billingHelpers, shippingHelpers, addressHelpers].forEach(function (library) {
Object.keys(library).forEach(function (item) {
if (typeof library[item] === 'object') {
exports[item] = $.extend({}, exports[item], library[item]);
} else {
exports[item] = library[item];
}
});
});
module.exports = exports;

View File

@ -0,0 +1,143 @@
'use strict';
var formHelpers = require('./formErrors');
var scrollAnimate = require('../components/scrollAnimate');
var createErrorNotification = require('../components/errorNotification');
var GUEST_FORM = '#guest-customer';
var REGISTERED_FORM = '#registered-customer';
var ERROR_SECTION = '.customer-error';
/**
* @returns {boolean} If guest is active, registered is not visible
*/
function isGuestFormActive() {
return $(REGISTERED_FORM).hasClass('d-none');
}
/**
* Clear any previous errors in the customer form.
*/
function clearErrors() {
$(ERROR_SECTION).children().remove();
formHelpers.clearPreviousErrors('.customer-information-block');
}
/**
* @param {Object} customerData - data includes checkout related customer information
* @param {Object} orderData - data includes checkout related order information
*/
function updateCustomerInformation(customerData, orderData) {
var $container = $('.customer-summary');
var $summaryDetails = $container.find('.summary-details');
var email = customerData.profile && customerData.profile.email ? customerData.profile.email : orderData.orderEmail;
$summaryDetails.find('.customer-summary-email').text(email);
if (customerData.registeredUser) {
$container.find('.edit-button').hide();
} else {
$container.find('.edit-button').show();
}
}
/**
* Handle response from the server for valid or invalid form fields.
* @param {Object} defer - the deferred object which will resolve on success or reject.
* @param {Object} data - the response data with the invalid form fields or
* valid model data.
*/
function customerFormResponse(defer, data) {
var parentForm = isGuestFormActive() ? GUEST_FORM : REGISTERED_FORM;
var formSelector = '.customer-section ' + parentForm;
// highlight fields with errors
if (data.error) {
if (data.fieldErrors.length) {
data.fieldErrors.forEach(function (error) {
if (Object.keys(error).length) {
formHelpers.loadFormErrors(formSelector, error);
}
});
}
if (data.customerErrorMessage) {
createErrorNotification(ERROR_SECTION, data.customerErrorMessage);
}
if (data.fieldErrors.length || data.customerErrorMessage || (data.serverErrors && data.serverErrors.length)) {
defer.reject(data);
}
if (data.cartError) {
window.location.href = data.redirectUrl;
defer.reject();
}
} else {
// Populate the Address Summary
$('body').trigger('checkout:updateCheckoutView', {
order: data.order,
customer: data.customer,
csrfToken: data.csrfToken
});
scrollAnimate($('.shipping-form'));
defer.resolve(data);
}
}
/**
*
* @param {boolean} registered - wether a registered login block will be used
*/
function chooseLoginBlock(registered) {
$(ERROR_SECTION).find('.alert').remove();
$('#password').val('');
if (registered) {
$(REGISTERED_FORM).removeClass('d-none');
$(GUEST_FORM).addClass('d-none');
$('#email').val($('#email-guest').val());
} else {
$(REGISTERED_FORM).addClass('d-none');
$(GUEST_FORM).removeClass('d-none');
$('#email').val('');
}
}
module.exports = {
/**
* Listeners for customer form
*/
initListeners: function () {
// 1. password
var customerLogin = '.js-login-customer';
var cancelLogin = '.js-cancel-login';
var registered;
if (customerLogin.length !== 0) {
$('body').on('click', customerLogin, function (e) {
registered = true;
e.preventDefault();
chooseLoginBlock(registered);
});
}
if (cancelLogin.length !== 0) {
$('body').on('click', cancelLogin, function (e) {
registered = false;
e.preventDefault();
chooseLoginBlock(registered);
});
}
},
methods: {
clearErrors: clearErrors,
updateCustomerInformation: updateCustomerInformation,
customerFormResponse: customerFormResponse,
isGuestFormActive: isGuestFormActive
},
vars: {
GUEST_FORM: GUEST_FORM,
REGISTERED_FORM: REGISTERED_FORM
}
};

View File

@ -0,0 +1,34 @@
'use strict';
var scrollAnimate = require('../components/scrollAnimate');
/**
* Display error messages and highlight form fields with errors.
* @param {string} parentSelector - the form which contains the fields
* @param {Object} fieldErrors - the fields with errors
*/
function loadFormErrors(parentSelector, fieldErrors) { // eslint-disable-line
// Display error messages and highlight form fields with errors.
$.each(fieldErrors, function (attr) {
$('*[name=' + attr + ']', parentSelector)
.addClass('is-invalid')
.siblings('.invalid-feedback')
.html(fieldErrors[attr]);
});
// Animate to top of form that has errors
scrollAnimate($(parentSelector));
}
/**
* Clear the form errors.
* @param {string} parentSelector - the parent form selector.
*/
function clearPreviousErrors(parentSelector) {
$(parentSelector).find('.form-control.is-invalid').removeClass('is-invalid');
$('.error-message').hide();
}
module.exports = {
loadFormErrors: loadFormErrors,
clearPreviousErrors: clearPreviousErrors
};

View File

@ -0,0 +1,146 @@
'use strict';
/**
* updates the totals summary
* @param {Array} totals - the totals data
*/
function updateTotals(totals) {
$('.shipping-total-cost').text(totals.totalShippingCost);
$('.tax-total').text(totals.totalTax);
$('.sub-total').text(totals.subTotal);
$('.grand-total-sum').text(totals.grandTotal);
if (totals.orderLevelDiscountTotal.value > 0) {
$('.order-discount').removeClass('hide-order-discount');
$('.order-discount-total').text('- ' + totals.orderLevelDiscountTotal.formatted);
} else {
$('.order-discount').addClass('hide-order-discount');
}
if (totals.shippingLevelDiscountTotal.value > 0) {
$('.shipping-discount').removeClass('hide-shipping-discount');
$('.shipping-discount-total').text('- ' +
totals.shippingLevelDiscountTotal.formatted);
} else {
$('.shipping-discount').addClass('hide-shipping-discount');
}
}
/**
* updates the order product shipping summary for an order model
* @param {Object} order - the order model
*/
function updateOrderProductSummaryInformation(order) {
var $productSummary = $('<div />');
order.shipping.forEach(function (shipping) {
shipping.productLineItems.items.forEach(function (lineItem) {
var pli = $('[data-product-line-item=' + lineItem.UUID + ']');
$productSummary.append(pli);
});
var address = shipping.shippingAddress || {};
var selectedMethod = shipping.selectedShippingMethod;
var nameLine = address.firstName ? address.firstName + ' ' : '';
if (address.lastName) nameLine += address.lastName;
var address1Line = address.address1;
var address2Line = address.address2;
var phoneLine = address.phone;
var shippingCost = selectedMethod ? selectedMethod.shippingCost : '';
var methodNameLine = selectedMethod ? selectedMethod.displayName : '';
var methodArrivalTime = selectedMethod && selectedMethod.estimatedArrivalTime
? '( ' + selectedMethod.estimatedArrivalTime + ' )'
: '';
var tmpl = $('#pli-shipping-summary-template').clone();
if (shipping.productLineItems.items && shipping.productLineItems.items.length > 1) {
$('h5 > span').text(' - ' + shipping.productLineItems.items.length + ' '
+ order.resources.items);
} else {
$('h5 > span').text('');
}
var stateRequiredAttr = $('#shippingState').attr('required');
var isRequired = stateRequiredAttr !== undefined && stateRequiredAttr !== false;
var stateExists = (shipping.shippingAddress && shipping.shippingAddress.stateCode)
? shipping.shippingAddress.stateCode
: false;
var stateBoolean = false;
if ((isRequired && stateExists) || (!isRequired)) {
stateBoolean = true;
}
var shippingForm = $('.multi-shipping input[name="shipmentUUID"][value="' + shipping.UUID + '"]').parent();
if (shipping.shippingAddress
&& shipping.shippingAddress.firstName
&& shipping.shippingAddress.address1
&& shipping.shippingAddress.city
&& stateBoolean
&& shipping.shippingAddress.countryCode
&& (shipping.shippingAddress.phone || shipping.productLineItems.items[0].fromStoreId)) {
$('.ship-to-name', tmpl).text(nameLine);
$('.ship-to-address1', tmpl).text(address1Line);
$('.ship-to-address2', tmpl).text(address2Line);
$('.ship-to-city', tmpl).text(address.city);
if (address.stateCode) {
$('.ship-to-st', tmpl).text(address.stateCode);
}
$('.ship-to-zip', tmpl).text(address.postalCode);
$('.ship-to-phone', tmpl).text(phoneLine);
if (!address2Line) {
$('.ship-to-address2', tmpl).hide();
}
if (!phoneLine) {
$('.ship-to-phone', tmpl).hide();
}
shippingForm.find('.ship-to-message').text('');
} else {
shippingForm.find('.ship-to-message').text(order.resources.addressIncomplete);
}
if (shipping.isGift) {
$('.gift-message-summary', tmpl).text(shipping.giftMessage);
} else {
$('.gift-summary', tmpl).addClass('d-none');
}
// checking h5 title shipping to or pickup
var $shippingAddressLabel = $('.shipping-header-text', tmpl);
$('body').trigger('shipping:updateAddressLabelText',
{ selectedShippingMethod: selectedMethod, resources: order.resources, shippingAddressLabel: $shippingAddressLabel });
if (shipping.selectedShippingMethod) {
$('.display-name', tmpl).text(methodNameLine);
$('.arrival-time', tmpl).text(methodArrivalTime);
$('.price', tmpl).text(shippingCost);
}
var $shippingSummary = $('<div class="multi-shipping" data-shipment-summary="'
+ shipping.UUID + '" />');
$shippingSummary.html(tmpl.html());
$productSummary.append($shippingSummary);
});
$('.product-summary-block').html($productSummary.html());
// Also update the line item prices, as they might have been altered
$('.grand-total-price').text(order.totals.subTotal);
order.items.items.forEach(function (item) {
if (item.priceTotal && item.priceTotal.renderedPrice) {
$('.item-total-' + item.UUID).empty().append(item.priceTotal.renderedPrice);
}
});
}
module.exports = {
updateTotals: updateTotals,
updateOrderProductSummaryInformation: updateOrderProductSummaryInformation
};

View File

@ -0,0 +1,33 @@
'use strict';
var formValidation = require('./components/formValidation');
$(document).ready(function () {
$('form.checkout-registration').submit(function (e) {
var form = $(this);
e.preventDefault();
var url = form.attr('action');
form.spinner().start();
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: form.serialize(),
success: function (data) {
form.spinner().stop();
if (!data.success) {
formValidation(form, data);
} else {
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
form.spinner().stop();
}
});
return false;
});
});

View File

@ -0,0 +1,45 @@
'use strict';
var Cleave = require('cleave.js').default;
module.exports = {
handleCreditCardNumber: function (cardFieldSelector, cardTypeSelector) {
var cleave = new Cleave(cardFieldSelector, {
creditCard: true,
onCreditCardTypeChanged: function (type) {
var creditCardTypes = {
visa: 'Visa',
mastercard: 'Master Card',
amex: 'Amex',
discover: 'Discover',
unknown: 'Unknown'
};
var cardType = creditCardTypes[Object.keys(creditCardTypes).indexOf(type) > -1
? type
: 'unknown'];
$(cardTypeSelector).val(cardType);
$('.card-number-wrapper').attr('data-type', type);
if (type === 'visa' || type === 'mastercard' || type === 'discover') {
$('#securityCode').attr('maxlength', 3);
} else {
$('#securityCode').attr('maxlength', 4);
}
}
});
$(cardFieldSelector).data('cleave', cleave);
},
serializeData: function (form) {
var serializedArray = form.serializeArray();
serializedArray.forEach(function (item) {
if (item.name.indexOf('cardNumber') > -1) {
item.value = $('#cardNumber').data('cleave').getRawValue(); // eslint-disable-line
}
});
return $.param(serializedArray);
}
};

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Validate whole form. Requires `this` to be set to form object
* @param {jQuery.event} event - Event to be canceled if form is invalid.
* @returns {boolean} - Flag to indicate if form is valid
*/
function validateForm(event) {
var valid = true;
if (this.checkValidity && !this.checkValidity()) {
// safari
valid = false;
if (event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
$(this).find('input, select').each(function () {
if (!this.validity.valid) {
$(this).trigger('invalid', this.validity);
}
});
}
return valid;
}
/**
* Remove all validation. Should be called every time before revalidating form
* @param {element} form - Form to be cleared
* @returns {void}
*/
function clearForm(form) {
$(form).find('.form-control.is-invalid').removeClass('is-invalid');
}
module.exports = {
invalid: function () {
$('form input, form select').on('invalid', function (e) {
e.preventDefault();
this.setCustomValidity('');
if (!this.validity.valid) {
var validationMessage = this.validationMessage;
$(this).addClass('is-invalid');
if (this.validity.patternMismatch && $(this).data('pattern-mismatch')) {
validationMessage = $(this).data('pattern-mismatch');
}
if ((this.validity.rangeOverflow || this.validity.rangeUnderflow)
&& $(this).data('range-error')) {
validationMessage = $(this).data('range-error');
}
if ((this.validity.tooLong || this.validity.tooShort)
&& $(this).data('range-error')) {
validationMessage = $(this).data('range-error');
}
if (this.validity.valueMissing && $(this).data('missing-error')) {
validationMessage = $(this).data('missing-error');
}
$(this).parents('.form-group').find('.invalid-feedback')
.text(validationMessage);
}
});
},
submit: function () {
$('form').on('submit', function (e) {
return validateForm.call(this, e);
});
},
buttonClick: function () {
$('form button[type="submit"], form input[type="submit"]').on('click', function () {
// clear all errors when trying to submit the form
clearForm($(this).parents('form'));
});
},
functions: {
validateForm: function (form, event) {
validateForm.call($(form), event || null);
},
clearForm: clearForm
}
};

View File

@ -0,0 +1,18 @@
'use strict';
module.exports = function () {
var sizes = ['xs', 'sm', 'md', 'lg', 'xl'];
sizes.forEach(function (size) {
var selector = '.collapsible-' + size + ' .title';
$('body').on('click', selector, function (e) {
e.preventDefault();
$(this).parents('.collapsible-' + size).toggleClass('active');
if ($(this).parents('.collapsible-' + size).hasClass('active')) {
$(this).attr('aria-expanded', true);
} else {
$(this).attr('aria-expanded', false);
}
});
});
};

View File

@ -0,0 +1,107 @@
'use strict';
var focusHelper = require('../components/focus');
/**
* Renders a modal window that will track the users consenting to accepting site tracking policy
*/
function showConsentModal() {
if (!$('.tracking-consent').data('caonline')) {
return;
}
var urlContent = $('.tracking-consent').data('url');
var urlAccept = $('.tracking-consent').data('accept');
var urlReject = $('.tracking-consent').data('reject');
var textYes = $('.tracking-consent').data('accepttext');
var textNo = $('.tracking-consent').data('rejecttext');
var textHeader = $('.tracking-consent').data('heading');
var htmlString = '<!-- Modal -->'
+ '<div class="modal show" id="consent-tracking" aria-modal="true" role="dialog" style="display: block;">'
+ '<div class="modal-dialog">'
+ '<!-- Modal content-->'
+ '<div class="modal-content">'
+ '<div class="modal-header">'
+ textHeader
+ '</div>'
+ '<div class="modal-body"></div>'
+ '<div class="modal-footer">'
+ '<div class="button-wrapper">'
+ '<button class="affirm btn btn-primary" data-url="' + urlAccept + '" autofocus data-dismiss="modal">'
+ textYes
+ '</button>'
+ '<button class="decline btn btn-primary" data-url="' + urlReject + '" data-dismiss="modal" >'
+ textNo
+ '</button>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>';
$.spinner().start();
$('body').append(htmlString);
$.ajax({
url: urlContent,
type: 'get',
dataType: 'html',
success: function (response) {
$('.modal-body').html(response);
$('#consent-tracking').modal('show');
},
error: function () {
$('#consent-tracking').remove();
}
});
$('#consent-tracking .button-wrapper button').click(function (e) {
e.preventDefault();
var url = $(this).data('url');
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function () {
$('#consent-tracking').remove();
$.spinner().stop();
},
error: function () {
$('#consent-tracking').remove();
$.spinner().stop();
}
});
});
}
module.exports = function () {
if ($('.consented').length === 0 && $('.tracking-consent').hasClass('api-true')) {
showConsentModal();
}
if ($('.tracking-consent').hasClass('api-true')) {
$('.tracking-consent').click(function () {
showConsentModal();
});
}
$('body').on('shown.bs.modal', '#consent-tracking', function () {
$('#consent-tracking').siblings().attr('aria-hidden', 'true');
$('#consent-tracking .close').focus();
});
$('body').on('hidden.bs.modal', '#consent-tracking', function () {
$('#consent-tracking').siblings().attr('aria-hidden', 'false');
});
$('body').on('keydown', '#consent-tracking', function (e) {
var focusParams = {
event: e,
containerSelector: '#consent-tracking',
firstElementSelector: '.affirm',
lastElementSelector: '.decline',
nextToLastElementSelector: '.affirm'
};
focusHelper.setTabNextFocus(focusParams);
});
};

View File

@ -0,0 +1,44 @@
'use strict';
/**
* Get cookie value by cookie name from browser
* @param {string} cookieName - name of the cookie
* @returns {string} cookie value of the found cookie name
*/
function getCookie(cookieName) {
var name = cookieName + '=';
var decodedCookie = decodeURIComponent(document.cookie);
var cookieArray = decodedCookie.split(';');
for (var i = 0; i < cookieArray.length; i++) {
var cookieItem = cookieArray[i];
while (cookieItem.charAt(0) === ' ') {
cookieItem = cookieItem.substring(1);
}
if (cookieItem.indexOf(name) === 0) {
return cookieItem.substring(name.length, cookieItem.length);
}
}
return '';
}
module.exports = function () {
if ($('.valid-cookie-warning').length > 0) {
var previousSessionID = window.localStorage.getItem('previousSid');
var currentSessionID = getCookie('sid');
if (!previousSessionID && currentSessionID) {
// When a user first time visit the home page,
// set the previousSessionID to currentSessionID
// and Show the cookie alert
previousSessionID = currentSessionID;
window.localStorage.setItem('previousSid', previousSessionID);
$('.cookie-warning-messaging').show();
} else if (previousSessionID && previousSessionID === currentSessionID) {
// Hide the cookie alert if user is in the same session
$('.cookie-warning-messaging').hide();
} else {
// Clear the previousSessionID from localStorage
// when user session is changed or expired
window.localStorage.removeItem('previousSid');
}
}
};

View File

@ -0,0 +1,72 @@
'use strict';
var keyboardAccessibility = require('./keyboardAccessibility');
module.exports = function () {
$('.country-selector a').click(function (e) {
e.preventDefault();
var action = $('.page').data('action');
var localeCode = $(this).data('locale');
var localeCurrencyCode = $(this).data('currencycode');
var queryString = $('.page').data('querystring');
var url = $('.country-selector').data('url');
$.ajax({
url: url,
type: 'get',
dataType: 'json',
data: {
code: localeCode,
queryString: queryString,
CurrencyCode: localeCurrencyCode,
action: action
},
success: function (response) {
$.spinner().stop();
if (response && response.redirectUrl) {
window.location.href = response.redirectUrl;
}
},
error: function () {
$.spinner().stop();
}
});
});
keyboardAccessibility('.navbar-header .country-selector',
{
40: function ($countryOptions) { // down
if ($(this).is(':focus')) {
$countryOptions.first().focus();
} else {
$(':focus').next().focus();
}
},
38: function ($countryOptions) { // up
if ($countryOptions.first().is(':focus') || $(this).is(':focus')) {
$(this).focus();
$(this).removeClass('show');
} else {
$(':focus').prev().focus();
}
},
27: function () { // escape
$(this).focus();
$(this).removeClass('show').children('.dropdown-menu').removeClass('show');
},
9: function () { // tab
$(this).removeClass('show').children('.dropdown-menu').removeClass('show');
}
},
function () {
if (!($(this).hasClass('show'))) {
$(this).addClass('show');
}
return $(this).find('.dropdown-country-selector').children('a');
}
);
$('.navbar-header .country-selector').on('focusin', function () {
$(this).addClass('show').children('.dropdown-menu').addClass('show');
});
};

View File

@ -0,0 +1,11 @@
'use strict';
module.exports = function (element, message) {
var errorHtml = '<div class="alert alert-danger alert-dismissible ' +
'fade show" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">&times;</span>' +
'</button>' + message + '</div>';
$(element).append(errorHtml);
};

View File

@ -0,0 +1,35 @@
'use strict';
module.exports = {
setTabNextFocus: function (focusParams) {
var KEYCODE_TAB = 9;
var isTabPressed = (focusParams.event.key === 'Tab' || focusParams.event.keyCode === KEYCODE_TAB);
if (!isTabPressed) {
return;
}
var firstFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.firstElementSelector);
var lastFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.lastElementSelector);
if ($(focusParams.containerSelector + ' ' + focusParams.lastElementSelector).is(':disabled')) {
lastFocusableEl = $(focusParams.containerSelector + ' ' + focusParams.nextToLastElementSelector);
if ($('.product-quickview.product-set').length > 0) {
var linkElements = $(focusParams.containerSelector + ' a#fa-link.share-icons');
lastFocusableEl = linkElements[linkElements.length - 1];
}
}
if (focusParams.event.shiftKey) /* shift + tab */ {
if ($(':focus').is(firstFocusableEl)) {
lastFocusableEl.focus();
focusParams.event.preventDefault();
}
} else /* tab */ {
if ($(':focus').is(lastFocusableEl)) { // eslint-disable-line
firstFocusableEl.focus();
focusParams.event.preventDefault();
}
}
}
};

View File

@ -0,0 +1,60 @@
'use strict';
var scrollAnimate = require('./scrollAnimate');
/**
* appends params to a url
* @param {string} data - data returned from the server's ajax call
* @param {Object} button - button that was clicked for email sign-up
*/
function displayMessage(data, button) {
$.spinner().stop();
var status;
if (data.success) {
status = 'alert-success';
} else {
status = 'alert-danger';
}
if ($('.email-signup-message').length === 0) {
$('body').append(
'<div class="email-signup-message"></div>'
);
}
$('.email-signup-message')
.append('<div class="email-signup-alert text-center ' + status + '">' + data.msg + '</div>');
setTimeout(function () {
$('.email-signup-message').remove();
button.removeAttr('disabled');
}, 3000);
}
module.exports = function () {
$('.back-to-top').click(function () {
scrollAnimate();
});
$('.subscribe-email').on('click', function (e) {
e.preventDefault();
var url = $(this).data('href');
var button = $(this);
var emailId = $('input[name=hpEmailSignUp]').val();
$.spinner().start();
$(this).attr('disabled', true);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: {
emailId: emailId
},
success: function (data) {
displayMessage(data, button);
},
error: function (err) {
displayMessage(err, button);
}
});
});
};

View File

@ -0,0 +1,43 @@
'use strict';
/**
* Remove all validation. Should be called every time before revalidating form
* @param {element} form - Form to be cleared
* @returns {void}
*/
function clearFormErrors(form) {
$(form).find('.form-control.is-invalid').removeClass('is-invalid');
}
module.exports = function (formElement, payload) {
// clear form validation first
clearFormErrors(formElement);
$('.alert', formElement).remove();
if (typeof payload === 'object' && payload.fields) {
Object.keys(payload.fields).forEach(function (key) {
if (payload.fields[key]) {
var feedbackElement = $(formElement).find('[name="' + key + '"]')
.parent()
.children('.invalid-feedback');
if (feedbackElement.length > 0) {
if (Array.isArray(payload[key])) {
feedbackElement.html(payload.fields[key].join('<br/>'));
} else {
feedbackElement.html(payload.fields[key]);
}
feedbackElement.siblings('.form-control').addClass('is-invalid');
}
}
});
}
if (payload && payload.error) {
var form = $(formElement).prop('tagName') === 'FORM'
? $(formElement)
: $(formElement).parents('form');
form.prepend('<div class="alert alert-danger" role="alert">'
+ payload.error.join('<br/>') + '</div>');
}
};

View File

@ -0,0 +1,15 @@
'use strict';
module.exports = function (selector, keyFunctions, preFunction) {
$(selector).on('keydown', function (e) {
var key = e.which;
var supportedKeyCodes = [37, 38, 39, 40, 27];
if (supportedKeyCodes.indexOf(key) >= 0) {
e.preventDefault();
}
var returnedScope = preFunction.call(this);
if (keyFunctions[key]) {
keyFunctions[key].call(this, returnedScope);
}
});
};

View File

@ -0,0 +1,252 @@
'use strict';
var keyboardAccessibility = require('./keyboardAccessibility');
var clearSelection = function (element) {
$(element).closest('.dropdown').children('.dropdown-menu').children('.top-category')
.detach();
$(element).closest('.dropdown.show').children('.nav-link').attr('aria-expanded', 'false');
$(element).closest('.dropdown.show').children('.dropdown-menu').attr('aria-hidden', 'true');
$(element).closest('.dropdown.show').removeClass('show');
$('div.menu-group > ul.nav.navbar-nav > li.nav-item > a').attr('aria-hidden', 'false');
$(element).closest('li').detach();
};
module.exports = function () {
var isDesktop = function (element) {
return $(element).parents('.menu-toggleable-left').css('position') !== 'fixed';
};
var headerBannerStatus = window.sessionStorage.getItem('hide_header_banner');
$('.header-banner .close').on('click', function () {
$('.header-banner').addClass('d-none');
window.sessionStorage.setItem('hide_header_banner', '1');
});
if (!headerBannerStatus || headerBannerStatus < 0) {
$('.header-banner').removeClass('d-none');
}
keyboardAccessibility('.main-menu .nav-link, .main-menu .dropdown-link',
{
40: function (menuItem) { // down
if (menuItem.hasClass('nav-item')) { // top level
$('.navbar-nav .show').removeClass('show')
.children('.dropdown-menu')
.removeClass('show');
menuItem.addClass('show').children('.dropdown-menu').addClass('show');
menuItem.find('ul > li > a')
.first()
.focus();
} else {
menuItem.removeClass('show').children('.dropdown-menu').removeClass('show');
if (!(menuItem.next().length > 0)) { // if this is the last menuItem
menuItem.parent().parent().find('li > a') // set focus to the first menuitem
.first()
.focus();
} else {
menuItem.next().children().first().focus();
}
}
},
39: function (menuItem) { // right
if (menuItem.hasClass('nav-item')) { // top level
menuItem.removeClass('show').children('.dropdown-menu').removeClass('show');
$(this).attr('aria-expanded', 'false');
menuItem.next().children().first().focus();
} else if (menuItem.hasClass('dropdown')) {
menuItem.addClass('show').children('.dropdown-menu').addClass('show');
$(this).attr('aria-expanded', 'true');
menuItem.find('ul > li > a')
.first()
.focus();
}
},
38: function (menuItem) { // up
if (menuItem.hasClass('nav-item')) { // top level
menuItem.removeClass('show').children('.dropdown-menu').removeClass('show');
} else if (menuItem.prev().length === 0) { // first menuItem
menuItem.parent().parent().removeClass('show')
.children('.nav-link')
.attr('aria-expanded', 'false');
menuItem.parent().children().last().children() // set the focus to the last menuItem
.first()
.focus();
} else {
menuItem.prev().children().first().focus();
}
},
37: function (menuItem) { // left
if (menuItem.hasClass('nav-item')) { // top level
menuItem.removeClass('show').children('.dropdown-menu').removeClass('show');
$(this).attr('aria-expanded', 'false');
menuItem.prev().children().first().focus();
} else {
menuItem.closest('.show').removeClass('show')
.closest('li.show').removeClass('show')
.children()
.first()
.focus()
.attr('aria-expanded', 'false');
}
},
27: function (menuItem) { // escape
var parentMenu = menuItem.hasClass('show')
? menuItem
: menuItem.closest('li.show');
parentMenu.children('.show').removeClass('show');
parentMenu.removeClass('show').children('.nav-link')
.attr('aria-expanded', 'false');
parentMenu.children().first().focus();
}
},
function () {
return $(this).parent();
}
);
$('.dropdown:not(.disabled) [data-toggle="dropdown"]')
.on('click', function (e) {
if (!isDesktop(this)) {
$('.modal-background').show();
// copy parent element into current UL
var li = $('<li class="dropdown-item top-category" role="button"></li>');
var link = $(this).clone().removeClass('dropdown-toggle')
.removeAttr('data-toggle')
.removeAttr('aria-expanded')
.attr('aria-haspopup', 'false');
li.append(link);
var closeMenu = $('<li class="nav-menu"></li>');
closeMenu.append($('.close-menu').first().clone());
$(this).parent().children('.dropdown-menu')
.prepend(li)
.prepend(closeMenu)
.attr('aria-hidden', 'false');
// copy navigation menu into view
$(this).parent().addClass('show');
$(this).attr('aria-expanded', 'true');
$(link).focus();
$('div.menu-group > ul.nav.navbar-nav > li.nav-item > a').attr('aria-hidden', 'true');
e.preventDefault();
}
})
.on('mouseenter', function () {
if (isDesktop(this)) {
var eventElement = this;
$('.navbar-nav > li').each(function () {
if (!$.contains(this, eventElement)) {
$(this).find('.show').each(function () {
clearSelection(this);
});
if ($(this).hasClass('show')) {
$(this).removeClass('show');
$(this).children('ul.dropdown-menu').removeClass('show');
$(this).children('.nav-link').attr('aria-expanded', 'false');
}
}
});
// need to close all the dropdowns that are not direct parent of current dropdown
$(this).parent().addClass('show');
$(this).siblings('.dropdown-menu').addClass('show');
$(this).attr('aria-expanded', 'true');
}
})
.parent()
.on('mouseleave', function () {
if (isDesktop(this)) {
$(this).removeClass('show');
$(this).children('.dropdown-menu').removeClass('show');
$(this).children('.nav-link').attr('aria-expanded', 'false');
}
});
$('.navbar>.close-menu>.close-button').on('click', function (e) {
e.preventDefault();
$('.menu-toggleable-left').removeClass('in');
$('.modal-background').hide();
$('.navbar-toggler').focus();
$('.main-menu').attr('aria-hidden', 'true');
$('.main-menu').siblings().attr('aria-hidden', 'false');
$('header').siblings().attr('aria-hidden', 'false');
});
$('.navbar-nav').on('click', '.back', function (e) {
e.preventDefault();
clearSelection(this);
});
$('.navbar-nav').on('click', '.close-button', function (e) {
e.preventDefault();
$('.navbar-nav').find('.top-category').detach();
$('.navbar-nav').find('.nav-menu').detach();
$('.navbar-nav').find('.show').removeClass('show');
$('.menu-toggleable-left').removeClass('in');
$('.main-menu').siblings().attr('aria-hidden', 'false');
$('header').siblings().attr('aria-hidden', 'false');
$('.modal-background').hide();
});
$('.navbar-toggler').click(function (e) {
e.preventDefault();
$('.main-menu').toggleClass('in');
$('.modal-background').show();
$('.main-menu').removeClass('d-none');
$('.main-menu').attr('aria-hidden', 'false');
$('.main-menu').siblings().attr('aria-hidden', 'true');
$('header').siblings().attr('aria-hidden', 'true');
$('.main-menu .nav.navbar-nav .nav-link').first().focus();
});
keyboardAccessibility('.navbar-header .user',
{
40: function ($popover) { // down
if ($popover.children('a').first().is(':focus')) {
$popover.next().children().first().focus();
} else {
$popover.children('a').first().focus();
}
},
38: function ($popover) { // up
if ($popover.children('a').first().is(':focus')) {
$(this).focus();
$popover.removeClass('show');
} else {
$popover.children('a').first().focus();
}
},
27: function () { // escape
$('.navbar-header .user .popover').removeClass('show');
$('.user').attr('aria-expanded', 'false');
},
9: function () { // tab
$('.navbar-header .user .popover').removeClass('show');
$('.user').attr('aria-expanded', 'false');
}
},
function () {
var $popover = $('.user .popover li.nav-item');
return $popover;
}
);
$('.navbar-header .user').on('mouseenter focusin', function () {
if ($('.navbar-header .user .popover').length > 0) {
$('.navbar-header .user .popover').addClass('show');
$('.user').attr('aria-expanded', 'true');
}
});
$('.navbar-header .user').on('mouseleave', function () {
$('.navbar-header .user .popover').removeClass('show');
$('.user').attr('aria-expanded', 'false');
});
$('body').on('click', '#myaccount', function () {
event.preventDefault();
});
};

View File

@ -0,0 +1,68 @@
'use strict';
var cart = require('../cart/cart');
var updateMiniCart = true;
module.exports = function () {
cart();
$('.minicart').on('count:update', function (event, count) {
if (count && $.isNumeric(count.quantityTotal)) {
$('.minicart .minicart-quantity').text(count.quantityTotal);
$('.minicart .minicart-link').attr({
'aria-label': count.minicartCountOfItems,
title: count.minicartCountOfItems
});
}
});
$('.minicart').on('mouseenter focusin touchstart', function () {
if ($('.search:visible').length === 0) {
return;
}
var url = $('.minicart').data('action-url');
var count = parseInt($('.minicart .minicart-quantity').text(), 10);
if (count !== 0 && $('.minicart .popover.show').length === 0) {
if (!updateMiniCart) {
$('.minicart .popover').addClass('show');
return;
}
$('.minicart .popover').addClass('show');
$('.minicart .popover').spinner().start();
$.get(url, function (data) {
$('.minicart .popover').empty();
$('.minicart .popover').append(data);
updateMiniCart = false;
$.spinner().stop();
});
}
});
$('body').on('touchstart click', function (e) {
if ($('.minicart').has(e.target).length <= 0) {
$('.minicart .popover').removeClass('show');
}
});
$('.minicart').on('mouseleave focusout', function (event) {
if ((event.type === 'focusout' && $('.minicart').has(event.target).length > 0)
|| (event.type === 'mouseleave' && $(event.target).is('.minicart .quantity'))
|| $('body').hasClass('modal-open')) {
event.stopPropagation();
return;
}
$('.minicart .popover').removeClass('show');
});
$('body').on('change', '.minicart .quantity', function () {
if ($(this).parents('.bonus-product-line-item').length && $('.cart-page').length) {
location.reload();
}
});
$('body').on('product:afterAddToCart', function () {
updateMiniCart = true;
});
$('body').on('cart:update', function () {
updateMiniCart = true;
});
};

View File

@ -0,0 +1,11 @@
'use strict';
module.exports = function (element) {
var position = element && element.length ? element.offset().top : 0;
$('html, body').animate({
scrollTop: position
}, 500);
if (!element) {
$('.logo-home').focus();
}
};

View File

@ -0,0 +1,269 @@
'use strict';
var debounce = require('lodash/debounce');
var endpoint = $('.suggestions-wrapper').data('url');
var minChars = 1;
var UP_KEY = 38;
var DOWN_KEY = 40;
var DIRECTION_DOWN = 1;
var DIRECTION_UP = -1;
/**
* Retrieves Suggestions element relative to scope
*
* @param {Object} scope - Search input field DOM element
* @return {JQuery} - .suggestions-wrapper element
*/
function getSuggestionsWrapper(scope) {
return $(scope).siblings('.suggestions-wrapper');
}
/**
* Determines whether DOM element is inside the .search-mobile class
*
* @param {Object} scope - DOM element, usually the input.search-field element
* @return {boolean} - Whether DOM element is inside div.search-mobile
*/
function isMobileSearch(scope) {
return !!$(scope).closest('.search-mobile').length;
}
/**
* Remove modal classes needed for mobile suggestions
*
*/
function clearModals() {
$('body').removeClass('modal-open');
$('header').siblings().attr('aria-hidden', 'false');
$('.suggestions').removeClass('modal');
}
/**
* Apply modal classes needed for mobile suggestions
*
* @param {Object} scope - Search input field DOM element
*/
function applyModals(scope) {
if (isMobileSearch(scope)) {
$('body').addClass('modal-open');
$('header').siblings().attr('aria-hidden', 'true');
getSuggestionsWrapper(scope).find('.suggestions').addClass('modal');
}
}
/**
* Tear down Suggestions panel
*/
function tearDownSuggestions() {
$('input.search-field').val('');
clearModals();
$('.search-mobile .suggestions').unbind('scroll');
$('.suggestions-wrapper').empty();
}
/**
* Toggle search field icon from search to close and vice-versa
*
* @param {string} action - Action to toggle to
*/
function toggleSuggestionsIcon(action) {
var mobileSearchIcon = '.search-mobile button.';
var iconSearch = 'fa-search';
var iconSearchClose = 'fa-close';
if (action === 'close') {
$(mobileSearchIcon + iconSearch).removeClass(iconSearch).addClass(iconSearchClose).attr('type', 'button');
} else {
$(mobileSearchIcon + iconSearchClose).removeClass(iconSearchClose).addClass(iconSearch).attr('type', 'submit');
}
}
/**
* Determines whether the "More Content Below" icon should be displayed
*
* @param {Object} scope - DOM element, usually the input.search-field element
*/
function handleMoreContentBelowIcon(scope) {
if (($(scope).scrollTop() + $(scope).innerHeight()) >= $(scope)[0].scrollHeight) {
$('.more-below').fadeOut();
} else {
$('.more-below').fadeIn();
}
}
/**
* Positions Suggestions panel on page
*
* @param {Object} scope - DOM element, usually the input.search-field element
*/
function positionSuggestions(scope) {
var outerHeight;
var $scope;
var $suggestions;
var top;
if (isMobileSearch(scope)) {
$scope = $(scope);
top = $scope.offset().top;
outerHeight = $scope.outerHeight();
$suggestions = getSuggestionsWrapper(scope).find('.suggestions');
$suggestions.css('top', top + outerHeight);
handleMoreContentBelowIcon(scope);
// Unfortunately, we have to bind this dynamically, as the live scroll event was not
// properly detecting dynamic suggestions element's scroll event
$suggestions.scroll(function () {
handleMoreContentBelowIcon(this);
});
}
}
/**
* Process Ajax response for SearchServices-GetSuggestions
*
* @param {Object|string} response - Empty object literal if null response or string with rendered
* suggestions template contents
*/
function processResponse(response) {
var $suggestionsWrapper = getSuggestionsWrapper(this).empty();
$.spinner().stop();
if (typeof (response) !== 'object') {
$suggestionsWrapper.append(response).show();
$(this).siblings('.reset-button').addClass('d-sm-block');
positionSuggestions(this);
if (isMobileSearch(this)) {
toggleSuggestionsIcon('close');
applyModals(this);
}
// Trigger screen reader by setting aria-describedby with the new suggestion message.
var suggestionsList = $('.suggestions .item');
if ($(suggestionsList).length) {
$('input.search-field').attr('aria-describedby', 'search-result-count');
} else {
$('input.search-field').removeAttr('aria-describedby');
}
} else {
$suggestionsWrapper.hide();
}
}
/**
* Retrieve suggestions
*
* @param {Object} scope - Search field DOM element
*/
function getSuggestions(scope) {
if ($(scope).val().length >= minChars) {
$.spinner().start();
$.ajax({
context: scope,
url: endpoint + encodeURIComponent($(scope).val()),
method: 'GET',
success: processResponse,
error: function () {
$.spinner().stop();
}
});
} else {
toggleSuggestionsIcon('search');
$(scope).siblings('.reset-button').removeClass('d-sm-block');
clearModals();
getSuggestionsWrapper(scope).empty();
}
}
/**
* Handle Search Suggestion Keyboard Arrow Keys
*
* @param {Integer} direction takes positive or negative number constant, DIRECTION_UP (-1) or DIRECTION_DOWN (+1)
*/
function handleArrow(direction) {
// get all li elements in the suggestions list
var suggestionsList = $('.suggestions .item');
if (suggestionsList.filter('.selected').length === 0) {
suggestionsList.first().addClass('selected');
$('input.search-field').each(function () {
$(this).attr('aria-activedescendant', suggestionsList.first()[0].id);
});
} else {
suggestionsList.each(function (index) {
var idx = index + direction;
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
$(this).removeAttr('aria-selected');
if (suggestionsList.eq(idx).length !== 0) {
suggestionsList.eq(idx).addClass('selected');
suggestionsList.eq(idx).attr('aria-selected', true);
$(this).removeProp('aria-selected');
$('input.search-field').each(function () {
$(this).attr('aria-activedescendant', suggestionsList.eq(idx)[0].id);
});
} else {
suggestionsList.first().addClass('selected');
suggestionsList.first().attr('aria-selected', true);
$('input.search-field').each(function () {
$(this).attr('aria-activedescendant', suggestionsList.first()[0].id);
});
}
return false;
}
return true;
});
}
}
module.exports = function () {
$('form[name="simpleSearch"]').submit(function (e) {
var suggestionsList = $('.suggestions .item');
if (suggestionsList.filter('.selected').length !== 0) {
e.preventDefault();
suggestionsList.filter('.selected').find('a')[0].click();
}
});
$('input.search-field').each(function () {
/**
* Use debounce to avoid making an Ajax call on every single key press by waiting a few
* hundred milliseconds before making the request. Without debounce, the user sees the
* browser blink with every key press.
*/
var debounceSuggestions = debounce(getSuggestions, 300);
$(this).on('keyup focus', function (e) {
// Capture Down/Up Arrow Key Events
switch (e.which) {
case DOWN_KEY:
handleArrow(DIRECTION_DOWN);
e.preventDefault(); // prevent moving the cursor
break;
case UP_KEY:
handleArrow(DIRECTION_UP);
e.preventDefault(); // prevent moving the cursor
break;
default:
debounceSuggestions(this, e);
}
});
});
$('body').on('click', function (e) {
if (!$('.suggestions').has(e.target).length && !$(e.target).hasClass('search-field')) {
$('.suggestions').hide();
}
});
$('body').on('click touchend', '.search-mobile button.fa-close', function (e) {
e.preventDefault();
$('.suggestions').hide();
toggleSuggestionsIcon('search');
tearDownSuggestions();
});
$('.site-search .reset-button').on('click', function () {
$(this).removeClass('d-sm-block');
});
};

View File

@ -0,0 +1,75 @@
'use strict';
/**
* Show a spinner inside a given element
* @param {element} $target - Element to block by the veil and spinner.
* Pass body to block the whole page.
*/
function addSpinner($target) {
var $veil = $('<div class="veil"><div class="underlay"></div></div>');
$veil.append('<div class="spinner"><div class="dot1"></div><div class="dot2"></div></div>');
if ($target.get(0).tagName === 'IMG') {
$target.after($veil);
$veil.css({ width: $target.width(), height: $target.height() });
if ($target.parent().css('position') === 'static') {
$target.parent().css('position', 'relative');
}
} else {
$target.append($veil);
if ($target.css('position') === 'static') {
$target.parent().css('position', 'relative');
$target.parent().addClass('veiled');
}
if ($target.get(0).tagName === 'BODY') {
$veil.find('.spinner').css('position', 'fixed');
}
}
$veil.click(function (e) {
e.stopPropagation();
});
}
/**
* Remove existing spinner
* @param {element} $veil - jQuery pointer to the veil element
*/
function removeSpinner($veil) {
if ($veil.parent().hasClass('veiled')) {
$veil.parent().css('position', '');
$veil.parent().removeClass('veiled');
}
$veil.off('click');
$veil.remove();
}
// element level spinner:
$.fn.spinner = function () {
var $element = $(this);
var Fn = function () {
this.start = function () {
if ($element.length) {
addSpinner($element);
}
};
this.stop = function () {
if ($element.length) {
var $veil = $('.veil');
removeSpinner($veil);
}
};
};
return new Fn();
};
// page-level spinner:
$.spinner = function () {
var Fn = function () {
this.start = function () {
addSpinner($('body'));
};
this.stop = function () {
removeSpinner($('.veil'));
};
};
return new Fn();
};

View File

@ -0,0 +1,11 @@
'use strict';
module.exports = function () {
$('.info-icon').on('mouseenter focusin', function () {
$(this).find('.tooltip').removeClass('d-none');
});
$('.info-icon').on('mouseleave focusout', function () {
$(this).find('.tooltip').addClass('d-none');
});
};

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./contactUs/contactUs'));
});

View File

@ -0,0 +1,58 @@
'use strict';
/**
* Display the returned message.
* @param {string} data - data returned from the server's ajax call
* @param {Object} button - button that was clicked for contact us sign-up
*/
function displayMessage(data, button) {
$.spinner().stop();
var status;
if (data.success) {
status = 'alert-success';
} else {
status = 'alert-danger';
}
if ($('.contact-us-signup-message').length === 0) {
$('body').append(
'<div class="contact-us-signup-message"></div>'
);
}
$('.contact-us-signup-message')
.append('<div class="contact-us-signup-alert text-center ' + status + '" role="alert">' + data.msg + '</div>');
setTimeout(function () {
$('.contact-us-signup-message').remove();
button.removeAttr('disabled');
}, 3000);
}
module.exports = {
subscribeContact: function () {
$('form.contact-us').submit(function (e) {
e.preventDefault();
var form = $(this);
var button = $('.subscribe-contact-us');
var url = form.attr('action');
$.spinner().start();
button.attr('disabled', true);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: form.serialize(),
success: function (data) {
displayMessage(data, button);
if (data.success) {
$('.contact-us').trigger('reset');
}
},
error: function (err) {
displayMessage(err, button);
}
});
});
}
};

View File

@ -0,0 +1,170 @@
'use strict';
/**
* Validates and Return the cquotient namespace provided by the commerce cloud platform
* @returns {Object} - einsteinUtils or null
*/
function getEinsteinUtils() {
var einsteinUtils = window.CQuotient;
if (einsteinUtils && (typeof einsteinUtils.getCQUserId === 'function') && (typeof einsteinUtils.getCQCookieId === 'function')) {
return einsteinUtils;
}
return null;
}
/**
* Renders the einstein response into a given dom element
* @param {jQuery} $parentElement parent element where recommendations will show.
*/
function showControls($parentElement) {
var $liTemplate = $parentElement.find('.hidden-indicators-template li');
var $carouselItems = $parentElement.find('.carousel-item');
$carouselItems.each(function (index) {
var $newIndiator = $liTemplate.clone();
if (index === 0) {
$parentElement.find('.pd-carousel-indicators').append($newIndiator);
} else {
$newIndiator.removeClass('active');
$parentElement.find('.pd-carousel-indicators').append($newIndiator);
}
$parentElement.find('.pd-carousel-indicators li').last().attr('data-position', index);
$parentElement.removeClass('hide-indicators');
});
}
/**
* fills in the carousel with product tile html objects
* @param {string} einsteinResponse string html for product tiles
* @param {jQuery} $parentElement parent element where recommendations will show.
*/
function fillDomElement(einsteinResponse, $parentElement) {
var recommender = $parentElement.data('recommender');
var recommendedProducts = einsteinResponse[recommender].recs;
if (recommendedProducts && recommendedProducts.length > 0) {
var template = $parentElement.data('template');
var swatches = $parentElement.data('swatches');
var displayRatings = $parentElement.data('displayratings');
var components = [];
components = recommendedProducts.map(function (recommendedProduct) {
var tiledefinition = {};
tiledefinition.classxs = $parentElement.data('bsxs');
tiledefinition.classsm = $parentElement.data('bssm');
tiledefinition.classmd = $parentElement.data('bsmd');
tiledefinition.template = template;
tiledefinition.swatches = swatches;
tiledefinition.displayratings = displayRatings;
tiledefinition.model = {
type: 'product',
id: recommendedProduct.id
};
return tiledefinition;
});
var url = new URL($parentElement.data('product-load-url'));
url.searchParams.append('components', JSON.stringify(components));
url.searchParams.append('limit', $parentElement.data('limit'));
url.searchParams.append('recommender', recommender);
$.ajax({
url: url.href,
type: 'get',
dataType: 'html',
success: function (html) {
$parentElement.find('.carousel-inner').html(html);
showControls($parentElement);
$('body').trigger('carousel:setup', {});
},
error: function () {
$parentElement.spinner().stop();
}
});
}
}
/**
* Processes a recommendation tile, with an already initialized category specific anchors array
* @param {jQuery} $parentElement parent element where recommendations will show.
* @param {Object} einsteinUtils cquotient object
* @param {Array} anchorsArray array of objects representing anchors
*/
function processRecommendationsTile($parentElement, einsteinUtils, anchorsArray) {
var recommender = $parentElement.data('recommender');
var params = {
userId: einsteinUtils.getCQUserId(),
cookieId: einsteinUtils.getCQCookieId(),
ccver: '1.01'
};
if (anchorsArray) {
params.anchors = anchorsArray;
}
/**
* Processes a recommendation responses
* @param {Object} einsteinResponse cquotient object
*/
function recommendationsReceived(einsteinResponse) {
fillDomElement(einsteinResponse, $parentElement);
$parentElement.spinner().stop();
}
if (einsteinUtils.getRecs) {
einsteinUtils.getRecs(einsteinUtils.clientId, recommender, params, recommendationsReceived);
} else {
einsteinUtils.widgets = einsteinUtils.widgets || []; // eslint-disable-line no-param-reassign
einsteinUtils.widgets.push({
recommenderName: recommender,
parameters: params,
callback: recommendationsReceived
});
}
}
/**
* Processes a recommendation tile, with an already initialized product specific anchors array
* @param {jQuery} $parentElement parent element where recommendations will show.
* @returns {Array} - containing an anchor object
*/
function createProductAnchor($parentElement) {
return [{
id: $parentElement.data('primaryProductId'),
sku: $parentElement.data('secondaryProductId'),
type: $parentElement.data('alternativeGroupType'),
alt_id: $parentElement.data('alternativeGroupId')
}];
}
/**
* Rerieves data attributes from parent element and converts to gretel compatible recommenders array
* @param {jQuery} $parentElement parent element where recommendations will show.
* @returns {Array} - containing an anchor object
*/
function createCategoryAnchor($parentElement) {
return [{ id: $parentElement.data('categoryId') }];
}
/**
* Gets all placeholder elements, which hold einstein recommendations queries the details from the
* einstein engine and feeds them back to the dom element
*/
function loadRecommendations() {
var einsteinUtils = getEinsteinUtils();
if (einsteinUtils) {
var $recommendationTiles = $('.einstein-carousel');
$recommendationTiles.each(function () {
var $parentElement = $(this);
$parentElement.spinner().start();
if ($(this).closest('.experience-einstein-einsteinCarouselProduct').length) {
return processRecommendationsTile($parentElement, einsteinUtils, createProductAnchor($parentElement));
} else if ($(this).closest('.experience-einstein-einsteinCarouselCategory').length) {
return processRecommendationsTile($parentElement, einsteinUtils, createCategoryAnchor($parentElement));
}
return processRecommendationsTile($parentElement, einsteinUtils);
});
}
}
$(document).ready(function () {
loadRecommendations();
});

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./login/login'));
});

View File

@ -0,0 +1,125 @@
'use strict';
var formValidation = require('../components/formValidation');
var createErrorNotification = require('../components/errorNotification');
module.exports = {
login: function () {
$('form.login').submit(function (e) {
var form = $(this);
e.preventDefault();
var url = form.attr('action');
form.spinner().start();
$('form.login').trigger('login:submit', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: form.serialize(),
success: function (data) {
form.spinner().stop();
if (!data.success) {
formValidation(form, data);
$('form.login').trigger('login:error', data);
} else {
$('form.login').trigger('login:success', data);
location.href = data.redirectUrl;
}
},
error: function (data) {
if (data.responseJSON.redirectUrl) {
window.location.href = data.responseJSON.redirectUrl;
} else {
$('form.login').trigger('login:error', data);
form.spinner().stop();
}
}
});
return false;
});
},
register: function () {
$('form.registration').submit(function (e) {
var form = $(this);
e.preventDefault();
var url = form.attr('action');
form.spinner().start();
$('form.registration').trigger('login:register', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: form.serialize(),
success: function (data) {
form.spinner().stop();
if (!data.success) {
$('form.registration').trigger('login:register:error', data);
formValidation(form, data);
} else {
$('form.registration').trigger('login:register:success', data);
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
} else {
createErrorNotification($('.error-messaging'), err.responseJSON.errorMessage);
}
form.spinner().stop();
}
});
return false;
});
},
resetPassword: function () {
$('.reset-password-form').submit(function (e) {
var form = $(this);
e.preventDefault();
var url = form.attr('action');
form.spinner().start();
$('.reset-password-form').trigger('login:register', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: form.serialize(),
success: function (data) {
form.spinner().stop();
if (!data.success) {
formValidation(form, data);
} else {
$('.request-password-title').text(data.receivedMsgHeading);
$('.request-password-body').empty()
.append('<p>' + data.receivedMsgBody + '</p>');
if (!data.mobile) {
$('#submitEmailButton').text(data.buttonText)
.attr('data-dismiss', 'modal');
} else {
$('.send-email-btn').empty()
.html('<a href="'
+ data.returnUrl
+ '" class="btn btn-primary btn-block">'
+ data.buttonText + '</a>'
);
}
}
},
error: function () {
form.spinner().stop();
}
});
return false;
});
},
clearResetForm: function () {
$('#login .modal').on('hidden.bs.modal', function () {
$('#reset-password-email').val('');
$('.modal-dialog .form-control.is-invalid').removeClass('is-invalid');
});
}
};

View File

@ -0,0 +1,18 @@
window.jQuery = window.$ = require('jquery');
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./components/menu'));
processInclude(require('./components/cookie'));
processInclude(require('./components/consentTracking'));
processInclude(require('./components/footer'));
processInclude(require('./components/miniCart'));
processInclude(require('./components/collapsibleItem'));
processInclude(require('./components/search'));
processInclude(require('./components/clientSideValidation'));
processInclude(require('./components/countrySelector'));
processInclude(require('./components/toolTip'));
});
require('./thirdParty/bootstrap');
require('./components/spinner');

View File

@ -0,0 +1,13 @@
'use strict';
$(document).ready(function () {
$('body').on('click', '.show-more-button', function (e) {
e.preventDefault();
var $set2Element = $(this).closest('.look-book-layout').find('.look-book-set2');
$set2Element.removeClass('hide-set');
var $showMoreElement = $(this).closest('.look-book-layout').find('.show-more');
$showMoreElement.addClass('d-none');
});
});

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./orderHistory/orderHistory'));
});

View File

@ -0,0 +1,24 @@
'use strict';
module.exports = function () {
$('body').on('change', '.order-history-select', function (e) {
var $ordersContainer = $('.order-list-container');
$ordersContainer.empty();
$.spinner().start();
$('.order-history-select').trigger('orderHistory:sort', e);
$.ajax({
url: e.currentTarget.value,
method: 'GET',
success: function (data) {
$ordersContainer.html(data);
$.spinner().stop();
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$.spinner().stop();
}
});
});
};

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./paymentInstruments/paymentInstruments'));
});

View File

@ -0,0 +1,81 @@
'use strict';
var formValidation = require('../components/formValidation');
var cleave = require('../components/cleave');
var url;
module.exports = {
removePayment: function () {
$('.remove-payment').on('click', function (e) {
e.preventDefault();
url = $(this).data('url') + '?UUID=' + $(this).data('id');
$('.payment-to-remove').empty().append($(this).data('card'));
$('.delete-confirmation-btn').click(function (f) {
f.preventDefault();
$('.remove-payment').trigger('payment:remove', f);
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
$('#uuid-' + data.UUID).remove();
if (data.message) {
var toInsert = '<div class="row justify-content-center h3 no-saved-payments"><p>' +
data.message +
'</p></div>';
$('.paymentInstruments').empty().append(toInsert);
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$.spinner().stop();
}
});
});
});
},
submitPayment: function () {
$('form.payment-form').submit(function (e) {
var $form = $(this);
e.preventDefault();
url = $form.attr('action');
$form.spinner().start();
$('form.payment-form').trigger('payment:submit', e);
var formData = cleave.serializeData($form);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: formData,
success: function (data) {
$form.spinner().stop();
if (!data.success) {
formValidation($form, data);
} else {
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$form.spinner().stop();
}
});
return false;
});
},
handleCreditCardNumber: function () {
if ($('#cardNumber').length && $('#cardType').length) {
cleave.handleCreditCardNumber('#cardNumber', '#cardType');
}
}
};

View File

@ -0,0 +1,848 @@
'use strict';
var focusHelper = require('../components/focus');
/**
* Retrieves the relevant pid value
* @param {jquery} $el - DOM container for a given add to cart button
* @return {string} - value to be used when adding product to cart
*/
function getPidValue($el) {
var pid;
if ($('#quickViewModal').hasClass('show') && !$('.product-set').length) {
pid = $($el).closest('.modal-content').find('.product-quickview').data('pid');
} else if ($('.product-set-detail').length || $('.product-set').length) {
pid = $($el).closest('.product-detail').find('.product-id').text();
} else {
pid = $('.product-detail:not(".bundle-item")').data('pid');
}
return pid;
}
/**
* Retrieve contextual quantity selector
* @param {jquery} $el - DOM container for the relevant quantity
* @return {jquery} - quantity selector DOM container
*/
function getQuantitySelector($el) {
var quantitySelected;
if ($el && $('.set-items').length) {
quantitySelected = $($el).closest('.product-detail').find('.quantity-select');
} else if ($el && $('.product-bundle').length) {
var quantitySelectedModal = $($el).closest('.modal-footer').find('.quantity-select');
var quantitySelectedPDP = $($el).closest('.bundle-footer').find('.quantity-select');
if (quantitySelectedModal.val() === undefined) {
quantitySelected = quantitySelectedPDP;
} else {
quantitySelected = quantitySelectedModal;
}
} else {
quantitySelected = $('.quantity-select');
}
return quantitySelected;
}
/**
* Retrieves the value associated with the Quantity pull-down menu
* @param {jquery} $el - DOM container for the relevant quantity
* @return {string} - value found in the quantity input
*/
function getQuantitySelected($el) {
return getQuantitySelector($el).val();
}
/**
* Process the attribute values for an attribute that has image swatches
*
* @param {Object} attr - Attribute
* @param {string} attr.id - Attribute ID
* @param {Object[]} attr.values - Array of attribute value objects
* @param {string} attr.values.value - Attribute coded value
* @param {string} attr.values.url - URL to de/select an attribute value of the product
* @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be
* selected. If there is no variant that corresponds to a specific combination of attribute
* values, an attribute may be disabled in the Product Detail Page
* @param {jQuery} $productContainer - DOM container for a given product
* @param {Object} msgs - object containing resource messages
*/
function processSwatchValues(attr, $productContainer, msgs) {
attr.values.forEach(function (attrValue) {
var $attrValue = $productContainer.find('[data-attr="' + attr.id + '"] [data-attr-value="' +
attrValue.value + '"]');
var $swatchButton = $attrValue.parent();
if (attrValue.selected) {
$attrValue.addClass('selected');
$attrValue.siblings('.selected-assistive-text').text(msgs.assistiveSelectedText);
} else {
$attrValue.removeClass('selected');
$attrValue.siblings('.selected-assistive-text').empty();
}
if (attrValue.url) {
$swatchButton.attr('data-url', attrValue.url);
} else {
$swatchButton.removeAttr('data-url');
}
// Disable if not selectable
$attrValue.removeClass('selectable unselectable');
$attrValue.addClass(attrValue.selectable ? 'selectable' : 'unselectable');
});
}
/**
* Process attribute values associated with an attribute that does not have image swatches
*
* @param {Object} attr - Attribute
* @param {string} attr.id - Attribute ID
* @param {Object[]} attr.values - Array of attribute value objects
* @param {string} attr.values.value - Attribute coded value
* @param {string} attr.values.url - URL to de/select an attribute value of the product
* @param {boolean} attr.values.isSelectable - Flag as to whether an attribute value can be
* selected. If there is no variant that corresponds to a specific combination of attribute
* values, an attribute may be disabled in the Product Detail Page
* @param {jQuery} $productContainer - DOM container for a given product
*/
function processNonSwatchValues(attr, $productContainer) {
var $attr = '[data-attr="' + attr.id + '"]';
var $defaultOption = $productContainer.find($attr + ' .select-' + attr.id + ' option:first');
$defaultOption.attr('value', attr.resetUrl);
attr.values.forEach(function (attrValue) {
var $attrValue = $productContainer
.find($attr + ' [data-attr-value="' + attrValue.value + '"]');
$attrValue.attr('value', attrValue.url)
.removeAttr('disabled');
if (!attrValue.selectable) {
$attrValue.attr('disabled', true);
}
});
}
/**
* Routes the handling of attribute processing depending on whether the attribute has image
* swatches or not
*
* @param {Object} attrs - Attribute
* @param {string} attr.id - Attribute ID
* @param {jQuery} $productContainer - DOM element for a given product
* @param {Object} msgs - object containing resource messages
*/
function updateAttrs(attrs, $productContainer, msgs) {
// Currently, the only attribute type that has image swatches is Color.
var attrsWithSwatches = ['color'];
attrs.forEach(function (attr) {
if (attrsWithSwatches.indexOf(attr.id) > -1) {
processSwatchValues(attr, $productContainer, msgs);
} else {
processNonSwatchValues(attr, $productContainer);
}
});
}
/**
* Updates the availability status in the Product Detail Page
*
* @param {Object} response - Ajax response object after an
* attribute value has been [de]selected
* @param {jQuery} $productContainer - DOM element for a given product
*/
function updateAvailability(response, $productContainer) {
var availabilityValue = '';
var availabilityMessages = response.product.availability.messages;
if (!response.product.readyToOrder) {
availabilityValue = '<li><div>' + response.resources.info_selectforstock + '</div></li>';
} else {
availabilityMessages.forEach(function (message) {
availabilityValue += '<li><div>' + message + '</div></li>';
});
}
$($productContainer).trigger('product:updateAvailability', {
product: response.product,
$productContainer: $productContainer,
message: availabilityValue,
resources: response.resources
});
}
/**
* Generates html for product attributes section
*
* @param {array} attributes - list of attributes
* @return {string} - Compiled HTML
*/
function getAttributesHtml(attributes) {
if (!attributes) {
return '';
}
var html = '';
attributes.forEach(function (attributeGroup) {
if (attributeGroup.ID === 'mainAttributes') {
attributeGroup.attributes.forEach(function (attribute) {
html += '<div class="attribute-values">' + attribute.label + ': '
+ attribute.value + '</div>';
});
}
});
return html;
}
/**
* @typedef UpdatedOptionValue
* @type Object
* @property {string} id - Option value ID for look up
* @property {string} url - Updated option value selection URL
*/
/**
* @typedef OptionSelectionResponse
* @type Object
* @property {string} priceHtml - Updated price HTML code
* @property {Object} options - Updated Options
* @property {string} options.id - Option ID
* @property {UpdatedOptionValue[]} options.values - Option values
*/
/**
* Updates DOM using post-option selection Ajax response
*
* @param {OptionSelectionResponse} optionsHtml - Ajax response optionsHtml from selecting a product option
* @param {jQuery} $productContainer - DOM element for current product
*/
function updateOptions(optionsHtml, $productContainer) {
// Update options
$productContainer.find('.product-options').empty().html(optionsHtml);
}
/**
* Dynamically creates Bootstrap carousel from response containing images
* @param {Object[]} imgs - Array of large product images,along with related information
* @param {jQuery} $productContainer - DOM element for a given product
*/
function createCarousel(imgs, $productContainer) {
var carousel = $productContainer.find('.carousel');
$(carousel).carousel('dispose');
var carouselId = $(carousel).attr('id');
$(carousel).empty().append('<ol class="carousel-indicators"></ol><div class="carousel-inner" role="listbox"></div><a class="carousel-control-prev" href="#' + carouselId + '" role="button" data-slide="prev"><span class="fa icon-prev" aria-hidden="true"></span><span class="sr-only">' + $(carousel).data('prev') + '</span></a><a class="carousel-control-next" href="#' + carouselId + '" role="button" data-slide="next"><span class="fa icon-next" aria-hidden="true"></span><span class="sr-only">' + $(carousel).data('next') + '</span></a>');
for (var i = 0; i < imgs.length; i++) {
$('<div class="carousel-item"><img src="' + imgs[i].url + '" class="d-block img-fluid" alt="' + imgs[i].alt + ' image number ' + parseInt(imgs[i].index, 10) + '" title="' + imgs[i].title + '" itemprop="image" /></div>').appendTo($(carousel).find('.carousel-inner'));
$('<li data-target="#' + carouselId + '" data-slide-to="' + i + '" class=""></li>').appendTo($(carousel).find('.carousel-indicators'));
}
$($(carousel).find('.carousel-item')).first().addClass('active');
$($(carousel).find('.carousel-indicators > li')).first().addClass('active');
if (imgs.length === 1) {
$($(carousel).find('.carousel-indicators, a[class^="carousel-control-"]')).detach();
}
$(carousel).carousel();
$($(carousel).find('.carousel-indicators')).attr('aria-hidden', true);
}
/**
* Parses JSON from Ajax call made whenever an attribute value is [de]selected
* @param {Object} response - response from Ajax call
* @param {Object} response.product - Product object
* @param {string} response.product.id - Product ID
* @param {Object[]} response.product.variationAttributes - Product attributes
* @param {Object[]} response.product.images - Product images
* @param {boolean} response.product.hasRequiredAttrsSelected - Flag as to whether all required
* attributes have been selected. Used partially to
* determine whether the Add to Cart button can be enabled
* @param {jQuery} $productContainer - DOM element for a given product.
*/
function handleVariantResponse(response, $productContainer) {
var isChoiceOfBonusProducts =
$productContainer.parents('.choose-bonus-product-dialog').length > 0;
var isVaraint;
if (response.product.variationAttributes) {
updateAttrs(response.product.variationAttributes, $productContainer, response.resources);
isVaraint = response.product.productType === 'variant';
if (isChoiceOfBonusProducts && isVaraint) {
$productContainer.parent('.bonus-product-item')
.data('pid', response.product.id);
$productContainer.parent('.bonus-product-item')
.data('ready-to-order', response.product.readyToOrder);
}
}
// Update primary images
var primaryImageUrls = response.product.images.large;
createCarousel(primaryImageUrls, $productContainer);
// Update pricing
if (!isChoiceOfBonusProducts) {
var $priceSelector = $('.prices .price', $productContainer).length
? $('.prices .price', $productContainer)
: $('.prices .price');
$priceSelector.replaceWith(response.product.price.html);
}
// Update promotions
$productContainer.find('.promotions').empty().html(response.product.promotionsHtml);
updateAvailability(response, $productContainer);
if (isChoiceOfBonusProducts) {
var $selectButton = $productContainer.find('.select-bonus-product');
$selectButton.trigger('bonusproduct:updateSelectButton', {
product: response.product, $productContainer: $productContainer
});
} else {
// Enable "Add to Cart" button if all required attributes have been selected
$('button.add-to-cart, button.add-to-cart-global, button.update-cart-product-global').trigger('product:updateAddToCart', {
product: response.product, $productContainer: $productContainer
}).trigger('product:statusUpdate', response.product);
}
// Update attributes
$productContainer.find('.main-attributes').empty()
.html(getAttributesHtml(response.product.attributes));
}
/**
* @typespec UpdatedQuantity
* @type Object
* @property {boolean} selected - Whether the quantity has been selected
* @property {string} value - The number of products to purchase
* @property {string} url - Compiled URL that specifies variation attributes, product ID, options,
* etc.
*/
/**
* Updates the quantity DOM elements post Ajax call
* @param {UpdatedQuantity[]} quantities -
* @param {jQuery} $productContainer - DOM container for a given product
*/
function updateQuantities(quantities, $productContainer) {
if ($productContainer.parent('.bonus-product-item').length <= 0) {
var optionsHtml = quantities.map(function (quantity) {
var selected = quantity.selected ? ' selected ' : '';
return '<option value="' + quantity.value + '" data-url="' + quantity.url + '"' +
selected + '>' + quantity.value + '</option>';
}).join('');
getQuantitySelector($productContainer).empty().html(optionsHtml);
}
}
/**
* updates the product view when a product attribute is selected or deselected or when
* changing quantity
* @param {string} selectedValueUrl - the Url for the selected variation value
* @param {jQuery} $productContainer - DOM element for current product
*/
function attributeSelect(selectedValueUrl, $productContainer) {
if (selectedValueUrl) {
$('body').trigger('product:beforeAttributeSelect',
{ url: selectedValueUrl, container: $productContainer });
$.ajax({
url: selectedValueUrl,
method: 'GET',
success: function (data) {
handleVariantResponse(data, $productContainer);
updateOptions(data.product.optionsHtml, $productContainer);
updateQuantities(data.product.quantities, $productContainer);
$('body').trigger('product:afterAttributeSelect',
{ data: data, container: $productContainer });
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
}
}
/**
* Retrieves url to use when adding a product to the cart
*
* @return {string} - The provided URL to use when adding a product to the cart
*/
function getAddToCartUrl() {
return $('.add-to-cart-url').val();
}
/**
* Parses the html for a modal window
* @param {string} html - representing the body and footer of the modal window
*
* @return {Object} - Object with properties body and footer.
*/
function parseHtml(html) {
var $html = $('<div>').append($.parseHTML(html));
var body = $html.find('.choice-of-bonus-product');
var footer = $html.find('.modal-footer').children();
return { body: body, footer: footer };
}
/**
* Retrieves url to use when adding a product to the cart
*
* @param {Object} data - data object used to fill in dynamic portions of the html
*/
function chooseBonusProducts(data) {
$('.modal-body').spinner().start();
if ($('#chooseBonusProductModal').length !== 0) {
$('#chooseBonusProductModal').remove();
}
var bonusUrl;
if (data.bonusChoiceRuleBased) {
bonusUrl = data.showProductsUrlRuleBased;
} else {
bonusUrl = data.showProductsUrlListBased;
}
var htmlString = '<!-- Modal -->'
+ '<div class="modal fade" id="chooseBonusProductModal" tabindex="-1" role="dialog">'
+ '<span class="enter-message sr-only" ></span>'
+ '<div class="modal-dialog choose-bonus-product-dialog" '
+ 'data-total-qty="' + data.maxBonusItems + '"'
+ 'data-UUID="' + data.uuid + '"'
+ 'data-pliUUID="' + data.pliUUID + '"'
+ 'data-addToCartUrl="' + data.addToCartUrl + '"'
+ 'data-pageStart="0"'
+ 'data-pageSize="' + data.pageSize + '"'
+ 'data-moreURL="' + data.showProductsUrlRuleBased + '"'
+ 'data-bonusChoiceRuleBased="' + data.bonusChoiceRuleBased + '">'
+ '<!-- Modal content-->'
+ '<div class="modal-content">'
+ '<div class="modal-header">'
+ ' <span class="">' + data.labels.selectprods + '</span>'
+ ' <button type="button" class="close pull-right" data-dismiss="modal">'
+ ' <span aria-hidden="true">&times;</span>'
+ ' <span class="sr-only"> </span>'
+ ' </button>'
+ '</div>'
+ '<div class="modal-body"></div>'
+ '<div class="modal-footer"></div>'
+ '</div>'
+ '</div>'
+ '</div>';
$('body').append(htmlString);
$('.modal-body').spinner().start();
$.ajax({
url: bonusUrl,
method: 'GET',
dataType: 'json',
success: function (response) {
var parsedHtml = parseHtml(response.renderedTemplate);
$('#chooseBonusProductModal .modal-body').empty();
$('#chooseBonusProductModal .enter-message').text(response.enterDialogMessage);
$('#chooseBonusProductModal .modal-header .close .sr-only').text(response.closeButtonText);
$('#chooseBonusProductModal .modal-body').html(parsedHtml.body);
$('#chooseBonusProductModal .modal-footer').html(parsedHtml.footer);
$('#chooseBonusProductModal').modal('show');
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
}
/**
* Updates the Mini-Cart quantity value after the customer has pressed the "Add to Cart" button
* @param {string} response - ajax response from clicking the add to cart button
*/
function handlePostCartAdd(response) {
$('.minicart').trigger('count:update', response);
var messageType = response.error ? 'alert-danger' : 'alert-success';
// show add to cart toast
if (response.newBonusDiscountLineItem
&& Object.keys(response.newBonusDiscountLineItem).length !== 0) {
chooseBonusProducts(response.newBonusDiscountLineItem);
} else {
if ($('.add-to-cart-messages').length === 0) {
$('body').append(
'<div class="add-to-cart-messages"></div>'
);
}
$('.add-to-cart-messages').append(
'<div class="alert ' + messageType + ' add-to-basket-alert text-center" role="alert">'
+ response.message
+ '</div>'
);
setTimeout(function () {
$('.add-to-basket-alert').remove();
}, 5000);
}
}
/**
* Retrieves the bundle product item ID's for the Controller to replace bundle master product
* items with their selected variants
*
* @return {string[]} - List of selected bundle product item ID's
*/
function getChildProducts() {
var childProducts = [];
$('.bundle-item').each(function () {
childProducts.push({
pid: $(this).find('.product-id').text(),
quantity: parseInt($(this).find('label.quantity').data('quantity'), 10)
});
});
return childProducts.length ? JSON.stringify(childProducts) : [];
}
/**
* Retrieve product options
*
* @param {jQuery} $productContainer - DOM element for current product
* @return {string} - Product options and their selected values
*/
function getOptions($productContainer) {
var options = $productContainer
.find('.product-option')
.map(function () {
var $elOption = $(this).find('.options-select');
var urlValue = $elOption.val();
var selectedValueId = $elOption.find('option[value="' + urlValue + '"]')
.data('value-id');
return {
optionId: $(this).data('option-id'),
selectedValueId: selectedValueId
};
}).toArray();
return JSON.stringify(options);
}
/**
* Makes a call to the server to report the event of adding an item to the cart
*
* @param {string | boolean} url - a string representing the end point to hit so that the event can be recorded, or false
*/
function miniCartReportingUrl(url) {
if (url) {
$.ajax({
url: url,
method: 'GET',
success: function () {
// reporting urls hit on the server
},
error: function () {
// no reporting urls hit on the server
}
});
}
}
module.exports = {
attributeSelect: attributeSelect,
methods: {
editBonusProducts: function (data) {
chooseBonusProducts(data);
}
},
focusChooseBonusProductModal: function () {
$('body').on('shown.bs.modal', '#chooseBonusProductModal', function () {
$('#chooseBonusProductModal').siblings().attr('aria-hidden', 'true');
$('#chooseBonusProductModal .close').focus();
});
},
onClosingChooseBonusProductModal: function () {
$('body').on('hidden.bs.modal', '#chooseBonusProductModal', function () {
$('#chooseBonusProductModal').siblings().attr('aria-hidden', 'false');
});
},
trapChooseBonusProductModalFocus: function () {
$('body').on('keydown', '#chooseBonusProductModal', function (e) {
var focusParams = {
event: e,
containerSelector: '#chooseBonusProductModal',
firstElementSelector: '.close',
lastElementSelector: '.add-bonus-products'
};
focusHelper.setTabNextFocus(focusParams);
});
},
colorAttribute: function () {
$(document).on('click', '[data-attr="color"] button', function (e) {
e.preventDefault();
if ($(this).attr('disabled')) {
return;
}
var $productContainer = $(this).closest('.set-item');
if (!$productContainer.length) {
$productContainer = $(this).closest('.product-detail');
}
attributeSelect($(this).attr('data-url'), $productContainer);
});
},
selectAttribute: function () {
$(document).on('change', 'select[class*="select-"], .options-select', function (e) {
e.preventDefault();
var $productContainer = $(this).closest('.set-item');
if (!$productContainer.length) {
$productContainer = $(this).closest('.product-detail');
}
attributeSelect(e.currentTarget.value, $productContainer);
});
},
availability: function () {
$(document).on('change', '.quantity-select', function (e) {
e.preventDefault();
var $productContainer = $(this).closest('.product-detail');
if (!$productContainer.length) {
$productContainer = $(this).closest('.modal-content').find('.product-quickview');
}
if ($('.bundle-items', $productContainer).length === 0) {
attributeSelect($(e.currentTarget).find('option:selected').data('url'),
$productContainer);
}
});
},
addToCart: function () {
$(document).on('click', 'button.add-to-cart, button.add-to-cart-global', function () {
var addToCartUrl;
var pid;
var pidsObj;
var setPids;
$('body').trigger('product:beforeAddToCart', this);
if ($('.set-items').length && $(this).hasClass('add-to-cart-global')) {
setPids = [];
$('.product-detail').each(function () {
if (!$(this).hasClass('product-set-detail')) {
setPids.push({
pid: $(this).find('.product-id').text(),
qty: $(this).find('.quantity-select').val(),
options: getOptions($(this))
});
}
});
pidsObj = JSON.stringify(setPids);
}
pid = getPidValue($(this));
var $productContainer = $(this).closest('.product-detail');
if (!$productContainer.length) {
$productContainer = $(this).closest('.quick-view-dialog').find('.product-detail');
}
addToCartUrl = getAddToCartUrl();
var form = {
pid: pid,
pidsObj: pidsObj,
childProducts: getChildProducts(),
quantity: getQuantitySelected($(this))
};
if (!$('.bundle-item').length) {
form.options = getOptions($productContainer);
}
$(this).trigger('updateAddToCartFormData', form);
if (addToCartUrl) {
$.ajax({
url: addToCartUrl,
method: 'POST',
data: form,
success: function (data) {
handlePostCartAdd(data);
$('body').trigger('product:afterAddToCart', data);
$.spinner().stop();
miniCartReportingUrl(data.reportingURL);
},
error: function () {
$.spinner().stop();
}
});
}
});
},
selectBonusProduct: function () {
$(document).on('click', '.select-bonus-product', function () {
var $choiceOfBonusProduct = $(this).parents('.choice-of-bonus-product');
var pid = $(this).data('pid');
var maxPids = $('.choose-bonus-product-dialog').data('total-qty');
var submittedQty = parseInt($choiceOfBonusProduct.find('.bonus-quantity-select').val(), 10);
var totalQty = 0;
$.each($('#chooseBonusProductModal .selected-bonus-products .selected-pid'), function () {
totalQty += $(this).data('qty');
});
totalQty += submittedQty;
var optionID = $choiceOfBonusProduct.find('.product-option').data('option-id');
var valueId = $choiceOfBonusProduct.find('.options-select option:selected').data('valueId');
if (totalQty <= maxPids) {
var selectedBonusProductHtml = ''
+ '<div class="selected-pid row" '
+ 'data-pid="' + pid + '"'
+ 'data-qty="' + submittedQty + '"'
+ 'data-optionID="' + (optionID || '') + '"'
+ 'data-option-selected-value="' + (valueId || '') + '"'
+ '>'
+ '<div class="col-sm-11 col-9 bonus-product-name" >'
+ $choiceOfBonusProduct.find('.product-name').html()
+ '</div>'
+ '<div class="col-1"><i class="fa fa-times" aria-hidden="true"></i></div>'
+ '</div>'
;
$('#chooseBonusProductModal .selected-bonus-products').append(selectedBonusProductHtml);
$('.pre-cart-products').html(totalQty);
$('.selected-bonus-products .bonus-summary').removeClass('alert-danger');
} else {
$('.selected-bonus-products .bonus-summary').addClass('alert-danger');
}
});
},
removeBonusProduct: function () {
$(document).on('click', '.selected-pid', function () {
$(this).remove();
var $selected = $('#chooseBonusProductModal .selected-bonus-products .selected-pid');
var count = 0;
if ($selected.length) {
$selected.each(function () {
count += parseInt($(this).data('qty'), 10);
});
}
$('.pre-cart-products').html(count);
$('.selected-bonus-products .bonus-summary').removeClass('alert-danger');
});
},
enableBonusProductSelection: function () {
$('body').on('bonusproduct:updateSelectButton', function (e, response) {
$('button.select-bonus-product', response.$productContainer).attr('disabled',
(!response.product.readyToOrder || !response.product.available));
var pid = response.product.id;
$('button.select-bonus-product', response.$productContainer).data('pid', pid);
});
},
showMoreBonusProducts: function () {
$(document).on('click', '.show-more-bonus-products', function () {
var url = $(this).data('url');
$('.modal-content').spinner().start();
$.ajax({
url: url,
method: 'GET',
success: function (html) {
var parsedHtml = parseHtml(html);
$('.modal-body').append(parsedHtml.body);
$('.show-more-bonus-products:first').remove();
$('.modal-content').spinner().stop();
},
error: function () {
$('.modal-content').spinner().stop();
}
});
});
},
addBonusProductsToCart: function () {
$(document).on('click', '.add-bonus-products', function () {
var $readyToOrderBonusProducts = $('.choose-bonus-product-dialog .selected-pid');
var queryString = '?pids=';
var url = $('.choose-bonus-product-dialog').data('addtocarturl');
var pidsObject = {
bonusProducts: []
};
$.each($readyToOrderBonusProducts, function () {
var qtyOption =
parseInt($(this)
.data('qty'), 10);
var option = null;
if (qtyOption > 0) {
if ($(this).data('optionid') && $(this).data('option-selected-value')) {
option = {};
option.optionId = $(this).data('optionid');
option.productId = $(this).data('pid');
option.selectedValueId = $(this).data('option-selected-value');
}
pidsObject.bonusProducts.push({
pid: $(this).data('pid'),
qty: qtyOption,
options: [option]
});
pidsObject.totalQty = parseInt($('.pre-cart-products').html(), 10);
}
});
queryString += JSON.stringify(pidsObject);
queryString = queryString + '&uuid=' + $('.choose-bonus-product-dialog').data('uuid');
queryString = queryString + '&pliuuid=' + $('.choose-bonus-product-dialog').data('pliuuid');
$.spinner().start();
$.ajax({
url: url + queryString,
method: 'POST',
success: function (data) {
$.spinner().stop();
if (data.error) {
$('#chooseBonusProductModal').modal('hide');
if ($('.add-to-cart-messages').length === 0) {
$('body').append('<div class="add-to-cart-messages"></div>');
}
$('.add-to-cart-messages').append(
'<div class="alert alert-danger add-to-basket-alert text-center"'
+ ' role="alert">'
+ data.errorMessage + '</div>'
);
setTimeout(function () {
$('.add-to-basket-alert').remove();
}, 3000);
} else {
$('.configure-bonus-product-attributes').html(data);
$('.bonus-products-step2').removeClass('hidden-xl-down');
$('#chooseBonusProductModal').modal('hide');
if ($('.add-to-cart-messages').length === 0) {
$('body').append('<div class="add-to-cart-messages"></div>');
}
$('.minicart-quantity').html(data.totalQty);
$('.add-to-cart-messages').append(
'<div class="alert alert-success add-to-basket-alert text-center"'
+ ' role="alert">'
+ data.msgSuccess + '</div>'
);
setTimeout(function () {
$('.add-to-basket-alert').remove();
if ($('.cart-page').length) {
location.reload();
}
}, 1500);
}
},
error: function () {
$.spinner().stop();
}
});
});
},
getPidValue: getPidValue,
getQuantitySelected: getQuantitySelected,
miniCartReportingUrl: miniCartReportingUrl
};

View File

@ -0,0 +1,151 @@
'use strict';
var base = require('./base');
/**
* Enable/disable UI elements
* @param {boolean} enableOrDisable - true or false
*/
function updateAddToCartEnableDisableOtherElements(enableOrDisable) {
$('button.add-to-cart-global').attr('disabled', enableOrDisable);
}
module.exports = {
methods: {
updateAddToCartEnableDisableOtherElements: updateAddToCartEnableDisableOtherElements
},
availability: base.availability,
addToCart: base.addToCart,
updateAttributesAndDetails: function () {
$('body').on('product:statusUpdate', function (e, data) {
var $productContainer = $('.product-detail[data-pid="' + data.id + '"]');
$productContainer.find('.description-and-detail .product-attributes')
.empty()
.html(data.attributesHtml);
if (data.shortDescription) {
$productContainer.find('.description-and-detail .description')
.removeClass('hidden-xl-down');
$productContainer.find('.description-and-detail .description .content')
.empty()
.html(data.shortDescription);
} else {
$productContainer.find('.description-and-detail .description')
.addClass('hidden-xl-down');
}
if (data.longDescription) {
$productContainer.find('.description-and-detail .details')
.removeClass('hidden-xl-down');
$productContainer.find('.description-and-detail .details .content')
.empty()
.html(data.longDescription);
} else {
$productContainer.find('.description-and-detail .details')
.addClass('hidden-xl-down');
}
});
},
showSpinner: function () {
$('body').on('product:beforeAddToCart product:beforeAttributeSelect', function () {
$.spinner().start();
});
},
updateAttribute: function () {
$('body').on('product:afterAttributeSelect', function (e, response) {
if ($('.product-detail>.bundle-items').length) {
response.container.data('pid', response.data.product.id);
response.container.find('.product-id').text(response.data.product.id);
} else if ($('.product-set-detail').eq(0)) {
response.container.data('pid', response.data.product.id);
response.container.find('.product-id').text(response.data.product.id);
} else {
$('.product-id').text(response.data.product.id);
$('.product-detail:not(".bundle-item")').data('pid', response.data.product.id);
}
});
},
updateAddToCart: function () {
$('body').on('product:updateAddToCart', function (e, response) {
// update local add to cart (for sets)
$('button.add-to-cart', response.$productContainer).attr('disabled',
(!response.product.readyToOrder || !response.product.available));
var enable = $('.product-availability').toArray().every(function (item) {
return $(item).data('available') && $(item).data('ready-to-order');
});
module.exports.methods.updateAddToCartEnableDisableOtherElements(!enable);
});
},
updateAvailability: function () {
$('body').on('product:updateAvailability', function (e, response) {
$('div.availability', response.$productContainer)
.data('ready-to-order', response.product.readyToOrder)
.data('available', response.product.available);
$('.availability-msg', response.$productContainer)
.empty().html(response.message);
if ($('.global-availability').length) {
var allAvailable = $('.product-availability').toArray()
.every(function (item) { return $(item).data('available'); });
var allReady = $('.product-availability').toArray()
.every(function (item) { return $(item).data('ready-to-order'); });
$('.global-availability')
.data('ready-to-order', allReady)
.data('available', allAvailable);
$('.global-availability .availability-msg').empty()
.html(allReady ? response.message : response.resources.info_selectforstock);
}
});
},
sizeChart: function () {
$('.size-chart a').on('click', function (e) {
e.preventDefault();
var url = $(this).attr('href');
var $prodSizeChart = $(this).closest('.size-chart').find('.size-chart-collapsible');
if ($prodSizeChart.is(':empty')) {
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
$prodSizeChart.append(data.content);
}
});
}
$prodSizeChart.toggleClass('active');
});
var $sizeChart = $('.size-chart-collapsible');
$('body').on('click touchstart', function (e) {
if ($('.size-chart').has(e.target).length <= 0) {
$sizeChart.removeClass('active');
}
});
},
copyProductLink: function () {
$('body').on('click', '#fa-link', function () {
event.preventDefault();
var $temp = $('<input>');
$('body').append($temp);
$temp.val($('#shareUrl').val()).select();
document.execCommand('copy');
$temp.remove();
$('.copy-link-message').attr('role', 'alert');
$('.copy-link-message').removeClass('d-none');
setTimeout(function () {
$('.copy-link-message').addClass('d-none');
}, 3000);
});
},
focusChooseBonusProductModal: base.focusChooseBonusProductModal()
};

View File

@ -0,0 +1,201 @@
'use strict';
var base = require('./base');
var focusHelper = require('../components/focus');
/**
* Generates the modal window on the first call.
*
*/
function getModalHtmlElement() {
if ($('#quickViewModal').length !== 0) {
$('#quickViewModal').remove();
}
var htmlString = '<!-- Modal -->'
+ '<div class="modal fade" id="quickViewModal" role="dialog">'
+ '<span class="enter-message sr-only" ></span>'
+ '<div class="modal-dialog quick-view-dialog">'
+ '<!-- Modal content-->'
+ '<div class="modal-content">'
+ '<div class="modal-header">'
+ ' <a class="full-pdp-link" href=""></a>'
+ ' <button type="button" class="close pull-right" data-dismiss="modal">'
+ ' <span aria-hidden="true">&times;</span>'
+ ' <span class="sr-only"> </span>'
+ ' </button>'
+ '</div>'
+ '<div class="modal-body"></div>'
+ '<div class="modal-footer"></div>'
+ '</div>'
+ '</div>'
+ '</div>';
$('body').append(htmlString);
}
/**
* @typedef {Object} QuickViewHtml
* @property {string} body - Main Quick View body
* @property {string} footer - Quick View footer content
*/
/**
* Parse HTML code in Ajax response
*
* @param {string} html - Rendered HTML from quickview template
* @return {QuickViewHtml} - QuickView content components
*/
function parseHtml(html) {
var $html = $('<div>').append($.parseHTML(html));
var body = $html.find('.product-quickview');
var footer = $html.find('.modal-footer').children();
return { body: body, footer: footer };
}
/**
* replaces the content in the modal window on for the selected product variation.
* @param {string} selectedValueUrl - url to be used to retrieve a new product model
*/
function fillModalElement(selectedValueUrl) {
$('.modal-body').spinner().start();
$.ajax({
url: selectedValueUrl,
method: 'GET',
dataType: 'json',
success: function (data) {
var parsedHtml = parseHtml(data.renderedTemplate);
$('.modal-body').empty();
$('.modal-body').html(parsedHtml.body);
$('.modal-footer').html(parsedHtml.footer);
$('.full-pdp-link').text(data.quickViewFullDetailMsg);
$('#quickViewModal .full-pdp-link').attr('href', data.productUrl);
$('#quickViewModal .size-chart').attr('href', data.productUrl);
$('#quickViewModal .modal-header .close .sr-only').text(data.closeButtonText);
$('#quickViewModal .enter-message').text(data.enterDialogMessage);
$('#quickViewModal').modal('show');
$('body').trigger('quickview:ready');
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
}
module.exports = {
showQuickview: function () {
$('body').on('click', '.quickview', function (e) {
e.preventDefault();
var selectedValueUrl = $(this).closest('a.quickview').attr('href');
$(e.target).trigger('quickview:show');
getModalHtmlElement();
fillModalElement(selectedValueUrl);
});
},
focusQuickview: function () {
$('body').on('shown.bs.modal', '#quickViewModal', function () {
$('#quickViewModal .close').focus();
});
},
trapQuickviewFocus: function () {
$('body').on('keydown', '#quickViewModal', function (e) {
var focusParams = {
event: e,
containerSelector: '#quickViewModal',
firstElementSelector: '.full-pdp-link',
lastElementSelector: '.add-to-cart-global',
nextToLastElementSelector: '.modal-footer .quantity-select'
};
focusHelper.setTabNextFocus(focusParams);
});
},
availability: base.availability,
addToCart: base.addToCart,
showSpinner: function () {
$('body').on('product:beforeAddToCart', function (e, data) {
$(data).closest('.modal-content').spinner().start();
});
},
hideDialog: function () {
$('body').on('product:afterAddToCart', function () {
$('#quickViewModal').modal('hide');
});
},
beforeUpdateAttribute: function () {
$('body').on('product:beforeAttributeSelect', function () {
$('.modal.show .modal-content').spinner().start();
});
},
updateAttribute: function () {
$('body').on('product:afterAttributeSelect', function (e, response) {
if ($('.modal.show .product-quickview>.bundle-items').length) {
$('.modal.show').find(response.container).data('pid', response.data.product.id);
$('.modal.show').find(response.container)
.find('.product-id').text(response.data.product.id);
} else if ($('.set-items').length) {
response.container.find('.product-id').text(response.data.product.id);
} else {
$('.modal.show .product-quickview').data('pid', response.data.product.id);
$('.modal.show .full-pdp-link')
.attr('href', response.data.product.selectedProductUrl);
}
});
},
updateAddToCart: function () {
$('body').on('product:updateAddToCart', function (e, response) {
// update local add to cart (for sets)
$('button.add-to-cart', response.$productContainer).attr('disabled',
(!response.product.readyToOrder || !response.product.available));
// update global add to cart (single products, bundles)
var dialog = $(response.$productContainer)
.closest('.quick-view-dialog');
$('.add-to-cart-global', dialog).attr('disabled',
!$('.global-availability', dialog).data('ready-to-order')
|| !$('.global-availability', dialog).data('available')
);
});
},
updateAvailability: function () {
$('body').on('product:updateAvailability', function (e, response) {
// bundle individual products
$('.product-availability', response.$productContainer)
.data('ready-to-order', response.product.readyToOrder)
.data('available', response.product.available)
.find('.availability-msg')
.empty()
.html(response.message);
var dialog = $(response.$productContainer)
.closest('.quick-view-dialog');
if ($('.product-availability', dialog).length) {
// bundle all products
var allAvailable = $('.product-availability', dialog).toArray()
.every(function (item) { return $(item).data('available'); });
var allReady = $('.product-availability', dialog).toArray()
.every(function (item) { return $(item).data('ready-to-order'); });
$('.global-availability', dialog)
.data('ready-to-order', allReady)
.data('available', allAvailable);
$('.global-availability .availability-msg', dialog).empty()
.html(allReady ? response.message : response.resources.info_selectforstock);
} else {
// single product
$('.global-availability', dialog)
.data('ready-to-order', response.product.readyToOrder)
.data('available', response.product.available)
.find('.availability-msg')
.empty()
.html(response.message);
}
});
}
};

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./product/detail'));
});

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./product/quickView'));
});

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./profile/profile'));
});

View File

@ -0,0 +1,67 @@
'use strict';
var formValidation = require('../components/formValidation');
module.exports = {
submitProfile: function () {
$('form.edit-profile-form').submit(function (e) {
var $form = $(this);
e.preventDefault();
var url = $form.attr('action');
$form.spinner().start();
$('form.edit-profile-form').trigger('profile:edit', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: $form.serialize(),
success: function (data) {
$form.spinner().stop();
if (!data.success) {
formValidation($form, data);
} else {
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$form.spinner().stop();
}
});
return false;
});
},
submitPassword: function () {
$('form.change-password-form').submit(function (e) {
var $form = $(this);
e.preventDefault();
var url = $form.attr('action');
$form.spinner().start();
$('form.change-password-form').trigger('password:edit', e);
$.ajax({
url: url,
type: 'post',
dataType: 'json',
data: $form.serialize(),
success: function (data) {
$form.spinner().stop();
if (!data.success) {
formValidation($form, data);
} else {
location.href = data.redirectUrl;
}
},
error: function (err) {
if (err.responseJSON.redirectUrl) {
window.location.href = err.responseJSON.redirectUrl;
}
$form.spinner().stop();
}
});
return false;
});
}
};

View File

@ -0,0 +1,8 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./search/search'));
processInclude(require('./product/quickView'));
});

View File

@ -0,0 +1,223 @@
'use strict';
/**
* Update DOM elements with Ajax results
*
* @param {Object} $results - jQuery DOM element
* @param {string} selector - DOM element to look up in the $results
* @return {undefined}
*/
function updateDom($results, selector) {
var $updates = $results.find(selector);
$(selector).empty().html($updates.html());
}
/**
* Keep refinement panes expanded/collapsed after Ajax refresh
*
* @param {Object} $results - jQuery DOM element
* @return {undefined}
*/
function handleRefinements($results) {
$('.refinement.active').each(function () {
$(this).removeClass('active');
var activeDiv = $results.find('.' + $(this)[0].className.replace(/ /g, '.'));
activeDiv.addClass('active');
activeDiv.find('button.title').attr('aria-expanded', 'true');
});
updateDom($results, '.refinements');
}
/**
* Parse Ajax results and updated select DOM elements
*
* @param {string} response - Ajax response HTML code
* @return {undefined}
*/
function parseResults(response) {
var $results = $(response);
var specialHandlers = {
'.refinements': handleRefinements
};
// Update DOM elements that do not require special handling
[
'.grid-header',
'.header-bar',
'.header.page-title',
'.product-grid',
'.show-more',
'.filter-bar'
].forEach(function (selector) {
updateDom($results, selector);
});
Object.keys(specialHandlers).forEach(function (selector) {
specialHandlers[selector]($results);
});
}
/**
* This function retrieves another page of content to display in the content search grid
* @param {JQuery} $element - the jquery element that has the click event attached
* @param {JQuery} $target - the jquery element that will receive the response
* @return {undefined}
*/
function getContent($element, $target) {
var showMoreUrl = $element.data('url');
$.spinner().start();
$.ajax({
url: showMoreUrl,
method: 'GET',
success: function (response) {
$target.append(response);
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
}
/**
* Update sort option URLs from Ajax response
*
* @param {string} response - Ajax response HTML code
* @return {undefined}
*/
function updateSortOptions(response) {
var $tempDom = $('<div>').append($(response));
var sortOptions = $tempDom.find('.grid-footer').data('sort-options').options;
sortOptions.forEach(function (option) {
$('option.' + option.id).val(option.url);
});
}
module.exports = {
filter: function () {
// Display refinements bar when Menu icon clicked
$('.container').on('click', 'button.filter-results', function () {
$('.refinement-bar, .modal-background').show();
$('.refinement-bar').siblings().attr('aria-hidden', true);
$('.refinement-bar').closest('.row').siblings().attr('aria-hidden', true);
$('.refinement-bar').closest('.tab-pane.active').siblings().attr('aria-hidden', true);
$('.refinement-bar').closest('.container.search-results').siblings().attr('aria-hidden', true);
$('.refinement-bar .close').focus();
});
},
closeRefinements: function () {
// Refinements close button
$('.container').on('click', '.refinement-bar button.close, .modal-background', function () {
$('.refinement-bar, .modal-background').hide();
$('.refinement-bar').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.row').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.tab-pane.active').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.container.search-results').siblings().attr('aria-hidden', false);
$('.btn.filter-results').focus();
});
},
resize: function () {
// Close refinement bar and hide modal background if user resizes browser
$(window).resize(function () {
$('.refinement-bar, .modal-background').hide();
$('.refinement-bar').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.row').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.tab-pane.active').siblings().attr('aria-hidden', false);
$('.refinement-bar').closest('.container.search-results').siblings().attr('aria-hidden', false);
});
},
sort: function () {
// Handle sort order menu selection
$('.container').on('change', '[name=sort-order]', function (e) {
e.preventDefault();
$.spinner().start();
$(this).trigger('search:sort', this.value);
$.ajax({
url: this.value,
data: { selectedUrl: this.value },
method: 'GET',
success: function (response) {
$('.product-grid').empty().html(response);
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
});
},
showMore: function () {
// Show more products
$('.container').on('click', '.show-more button', function (e) {
e.stopPropagation();
var showMoreUrl = $(this).data('url');
e.preventDefault();
$.spinner().start();
$(this).trigger('search:showMore', e);
$.ajax({
url: showMoreUrl,
data: { selectedUrl: showMoreUrl },
method: 'GET',
success: function (response) {
$('.grid-footer').replaceWith(response);
updateSortOptions(response);
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
});
},
applyFilter: function () {
// Handle refinement value selection and reset click
$('.container').on(
'click',
'.refinements li button, .refinement-bar button.reset, .filter-value button, .swatch-filter button',
function (e) {
e.preventDefault();
e.stopPropagation();
$.spinner().start();
$(this).trigger('search:filter', e);
$.ajax({
url: $(this).data('href'),
data: {
page: $('.grid-footer').data('page-number'),
selectedUrl: $(this).data('href')
},
method: 'GET',
success: function (response) {
parseResults(response);
$.spinner().stop();
},
error: function () {
$.spinner().stop();
}
});
});
},
showContentTab: function () {
// Display content results from the search
$('.container').on('click', '.content-search', function () {
if ($('#content-search-results').html() === '') {
getContent($(this), $('#content-search-results'));
}
});
// Display the next page of content results from the search
$('.container').on('click', '.show-more-content button', function () {
getContent($(this), $('#content-search-results'));
$('.show-more-content').remove();
});
}
};

View File

@ -0,0 +1,7 @@
'use strict';
var processInclude = require('./util');
$(document).ready(function () {
processInclude(require('./storeLocator/storeLocator'));
});

View File

@ -0,0 +1,264 @@
/* globals google */
'use strict';
/**
* appends params to a url
* @param {string} url - Original url
* @param {Object} params - Parameters to append
* @returns {string} result url with appended parameters
*/
function appendToUrl(url, params) {
var newUrl = url;
newUrl += (newUrl.indexOf('?') !== -1 ? '&' : '?') + Object.keys(params).map(function (key) {
return key + '=' + encodeURIComponent(params[key]);
}).join('&');
return newUrl;
}
/**
* Uses google maps api to render a map
*/
function maps() {
var map;
var infowindow = new google.maps.InfoWindow();
// Init U.S. Map in the center of the viewport
var latlng = new google.maps.LatLng(37.09024, -95.712891);
var mapOptions = {
scrollwheel: false,
zoom: 4,
center: latlng
};
map = new google.maps.Map($('.map-canvas')[0], mapOptions);
var mapdiv = $('.map-canvas').attr('data-locations');
mapdiv = JSON.parse(mapdiv);
var bounds = new google.maps.LatLngBounds();
// Customized google map marker icon with svg format
var markerImg = {
path: 'M13.5,30.1460153 L16.8554555,25.5 L20.0024287,25.5 C23.039087,25.5 25.5,' +
'23.0388955 25.5,20.0024287 L25.5,5.99757128 C25.5,2.96091298 23.0388955,0.5 ' +
'20.0024287,0.5 L5.99757128,0.5 C2.96091298,0.5 0.5,2.96110446 0.5,5.99757128 ' +
'L0.5,20.0024287 C0.5,23.039087 2.96110446,25.5 5.99757128,25.5 L10.1445445,' +
'25.5 L13.5,30.1460153 Z',
fillColor: '#0070d2',
fillOpacity: 1,
scale: 1.1,
strokeColor: 'white',
strokeWeight: 1,
anchor: new google.maps.Point(13, 30),
labelOrigin: new google.maps.Point(12, 12)
};
Object.keys(mapdiv).forEach(function (key) {
var item = mapdiv[key];
var lable = parseInt(key, 10) + 1;
var storeLocation = new google.maps.LatLng(item.latitude, item.longitude);
var marker = new google.maps.Marker({
position: storeLocation,
map: map,
title: item.name,
icon: markerImg,
label: { text: lable.toString(), color: 'white', fontSize: '16px' }
});
marker.addListener('click', function () {
infowindow.setOptions({
content: item.infoWindowHtml
});
infowindow.open(map, marker);
});
// Create a minimum bound based on a set of storeLocations
bounds.extend(marker.position);
});
// Fit the all the store marks in the center of a minimum bounds when any store has been found.
if (mapdiv && mapdiv.length !== 0) {
map.fitBounds(bounds);
}
}
/**
* Renders the results of the search and updates the map
* @param {Object} data - Response from the server
*/
function updateStoresResults(data) {
var $resultsDiv = $('.results');
var $mapDiv = $('.map-canvas');
var hasResults = data.stores.length > 0;
if (!hasResults) {
$('.store-locator-no-results').show();
} else {
$('.store-locator-no-results').hide();
}
$resultsDiv.empty()
.data('has-results', hasResults)
.data('radius', data.radius)
.data('search-key', data.searchKey);
$mapDiv.attr('data-locations', data.locations);
if ($mapDiv.data('has-google-api')) {
maps();
} else {
$('.store-locator-no-apiKey').show();
}
if (data.storesResultsHtml) {
$resultsDiv.append(data.storesResultsHtml);
}
}
/**
* Search for stores with new zip code
* @param {HTMLElement} element - the target html element
* @returns {boolean} false to prevent default event
*/
function search(element) {
var dialog = element.closest('.in-store-inventory-dialog');
var spinner = dialog.length ? dialog.spinner() : $.spinner();
spinner.start();
var $form = element.closest('.store-locator');
var radius = $('.results').data('radius');
var url = $form.attr('action');
var urlParams = { radius: radius };
var payload = $form.is('form') ? $form.serialize() : { postalCode: $form.find('[name="postalCode"]').val() };
url = appendToUrl(url, urlParams);
$.ajax({
url: url,
type: $form.attr('method'),
data: payload,
dataType: 'json',
success: function (data) {
spinner.stop();
updateStoresResults(data);
$('.select-store').prop('disabled', true);
}
});
return false;
}
module.exports = {
init: function () {
if ($('.map-canvas').data('has-google-api')) {
maps();
} else {
$('.store-locator-no-apiKey').show();
}
if (!$('.results').data('has-results')) {
$('.store-locator-no-results').show();
}
},
detectLocation: function () {
// clicking on detect location.
$('.detect-location').on('click', function () {
$.spinner().start();
if (!navigator.geolocation) {
$.spinner().stop();
return;
}
navigator.geolocation.getCurrentPosition(function (position) {
var $detectLocationButton = $('.detect-location');
var url = $detectLocationButton.data('action');
var radius = $('.results').data('radius');
var urlParams = {
radius: radius,
lat: position.coords.latitude,
long: position.coords.longitude
};
url = appendToUrl(url, urlParams);
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
$.spinner().stop();
updateStoresResults(data);
$('.select-store').prop('disabled', true);
}
});
});
});
},
search: function () {
$('.store-locator-container form.store-locator').submit(function (e) {
e.preventDefault();
search($(this));
});
$('.store-locator-container .btn-storelocator-search[type="button"]').click(function (e) {
e.preventDefault();
search($(this));
});
},
changeRadius: function () {
$('.store-locator-container .radius').change(function () {
var radius = $(this).val();
var searchKeys = $('.results').data('search-key');
var url = $(this).data('action-url');
var urlParams = {};
if (searchKeys.postalCode) {
urlParams = {
radius: radius,
postalCode: searchKeys.postalCode
};
} else if (searchKeys.lat && searchKeys.long) {
urlParams = {
radius: radius,
lat: searchKeys.lat,
long: searchKeys.long
};
}
url = appendToUrl(url, urlParams);
var dialog = $(this).closest('.in-store-inventory-dialog');
var spinner = dialog.length ? dialog.spinner() : $.spinner();
spinner.start();
$.ajax({
url: url,
type: 'get',
dataType: 'json',
success: function (data) {
spinner.stop();
updateStoresResults(data);
$('.select-store').prop('disabled', true);
}
});
});
},
selectStore: function () {
$('.store-locator-container').on('click', '.select-store', (function (e) {
e.preventDefault();
var selectedStore = $(':checked', '.results-card .results');
var data = {
storeID: selectedStore.val(),
searchRadius: $('#radius').val(),
searchPostalCode: $('.results').data('search-key').postalCode,
storeDetailsHtml: selectedStore.siblings('label').find('.store-details').html(),
event: e
};
$('body').trigger('store:selected', data);
}));
},
updateSelectStoreButton: function () {
$('body').on('change', '.select-store-input', (function () {
$('.select-store').prop('disabled', false);
}));
}
};

View File

@ -0,0 +1,8 @@
{
"env": {
"es6": true
},
"parserOptions": {
"sourceType": "module"
}
}

View File

@ -0,0 +1,11 @@
require('bootstrap/js/src/util.js');
require('bootstrap/js/src/alert.js');
// require('bootstrap/js/src/button.js');
require('bootstrap/js/src/carousel.js');
require('bootstrap/js/src/collapse.js');
// require('bootstrap/js/src/dropdown.js');
require('bootstrap/js/src/modal.js');
require('bootstrap/js/src/scrollspy.js');
require('bootstrap/js/src/tab.js');
// require('bootstrap/js/src/tooltip.js');
// require('bootstrap/js/src/popover.js');

View File

@ -0,0 +1,13 @@
'use strict';
module.exports = function (include) {
if (typeof include === 'function') {
include();
} else if (typeof include === 'object') {
Object.keys(include).forEach(function (key) {
if (typeof include[key] === 'function') {
include[key]();
}
});
}
};

View File

@ -0,0 +1,35 @@
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/root";
@import "bootstrap/scss/reboot";
@import "bootstrap/scss/type";
@import "bootstrap/scss/images";
@import "bootstrap/scss/code";
@import "bootstrap/scss/grid";
@import "bootstrap/scss/tables";
@import "bootstrap/scss/forms";
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/transitions";
@import "bootstrap/scss/dropdown";
@import "bootstrap/scss/button-group";
@import "bootstrap/scss/input-group";
@import "bootstrap/scss/custom-forms";
@import "bootstrap/scss/nav";
@import "bootstrap/scss/navbar";
@import "bootstrap/scss/card";
@import "bootstrap/scss/breadcrumb";
@import "bootstrap/scss/pagination";
@import "bootstrap/scss/badge";
@import "bootstrap/scss/jumbotron";
@import "bootstrap/scss/alert";
@import "bootstrap/scss/progress";
@import "bootstrap/scss/media";
@import "bootstrap/scss/list-group";
@import "bootstrap/scss/close";
@import "bootstrap/scss/modal";
@import "bootstrap/scss/tooltip";
@import "bootstrap/scss/popover";
@import "bootstrap/scss/carousel";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/print";

View File

@ -0,0 +1,86 @@
.carousel {
.icon-prev,
.icon-next {
background-color: $white;
font-size: 1.875em;
// width and height here need to use rem units because the font size used here is 30px
height: 3rem;
padding-top: 0.24em;
width: 3rem;
&::before {
color: black;
font-family: 'FontAwesome';
}
}
.icon-prev {
&::before {
content: '\f104';
}
}
.icon-next {
&::before {
content: '\f105';
}
}
.carousel-control-prev {
justify-content: flex-start;
}
.carousel-control-next {
justify-content: flex-end;
}
}
.nav-tabs {
border-bottom: $border-width solid $grey3;
.nav-link {
font-size: 1.1rem;
color: $nav-tabs-link-hover-border-color;
&.active {
border-bottom: 0.188em solid #{var(--skin-primary-color-1)};
}
}
}
.card {
margin-bottom: 1em;
}
.card-header h4 {
margin-bottom: 0;
}
.modal .modal-body {
flex: 0 0 auto;
}
dt {
color: $gray-700;
font-weight: normal;
}
.custom-checkbox .custom-control-label::before {
border: 1px solid black;
background: $grey1; /* For browsers that do not support gradients */
background: linear-gradient($grey1, $grey3); /* Standard syntax */
}
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
background-image: $svg-check;
}
.custom-radio .custom-control-label::before {
border: 1px solid black;
background: $grey3; /* For browsers that do not support gradients */
background: linear-gradient($grey3, $grey5); /* Standard syntax */
}
.form-control.is-invalid {
background-image: none;
}

View File

@ -0,0 +1,116 @@
@import "variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "productCard";
.minicart {
position: relative;
h1 {
font-size: 1rem;
}
.cart {
padding-top: 0.625em;
padding-bottom: 0.625em;
background-color: $body-bg;
}
.remove-btn {
color: $slightly-darker-gray;
float: right;
background-color: white;
border: none;
font-size: 1.625em;
margin-top: -0.313em;
padding: 0;
}
.product-summary {
margin-right: -0.938em;
max-height: 21.875em;
overflow-y: auto;
overflow-x: hidden;
padding-right: 0.938em;
}
.card-body {
padding: 0.625em;
}
.quantity-label {
font-size: 0.813em;
}
.quantity {
width: 100%;
}
.popover {
top: 100%;
left: auto;
right: 0;
min-width: 23.44rem;
max-width: 23.44rem;
min-height: 22.7rem;
display: none;
&::before {
left: auto;
right: 15px;
}
&::after {
left: auto;
right: 16px;
}
&.show {
display: block;
}
}
.minicart-footer {
border-top: 1px solid $grey3;
}
.estimated-total {
margin-top: 0.625em;
}
.sub-total-label {
font-size: 1em;
font-weight: 600;
}
.sub-total {
font-size: 1em;
font-weight: 600;
}
.line-item-divider {
margin: 0.625em -0.625em 0.625em -1.325em;
}
.line-item-name {
width: 90%;
}
}
.hide-link-med {
@include media-breakpoint-only(sm) {
display: none;
}
}
.hide-no-link {
@include media-breakpoint-up(md) {
display: none;
}
@include media-breakpoint-down(xs) {
display: none;
}
}

View File

@ -0,0 +1,108 @@
@import "variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
.item-attributes {
vertical-align: top;
padding-left: 0;
}
.line-item-attributes,
.line-item-option {
font-size: 0.813rem;
margin: 0;
}
.line-item-name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: $darker-gray;
font-size: 1em;
font-weight: bold;
margin-bottom: 0.313em;
}
.line-item-pricing-info {
margin-bottom: 0;
+ .price {
font-size: 1em;
font-weight: bolder;
}
+ .unit-price .price {
font-size: 1em;
font-weight: bolder;
}
}
.line-item-price-quantity-info {
margin-top: 0.625em;
border-top: 1px solid $horizontal-rule-grey;
}
.line-item-total-text {
font-size: 0.813em;
}
.pricing {
font-size: 1em;
font-weight: bolder;
}
.item-image {
height: 5.625em;
width: 5.625em;
margin-right: 0.938em;
flex-grow: 0;
flex-shrink: 0;
img.product-image {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
max-height: 5.625em;
}
}
.non-adjusted-price {
display: none;
}
.line-item-promo {
color: $success;
font-size: 0.813em;
}
.line-item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.bundled-line-item + .bundled-line-item {
margin-top: 0.625em;
}
.bundle-includes {
font-size: 0.813em;
margin-bottom: 0.625em;
}
.line-item-divider {
margin: 0.625em -1.225em 0.625em -1.325em;
}
.line-dotted {
border-top: 0.063em dashed #ccc;
}
.line-item-availability {
font-size: 0.813rem;
}
.product-line-item-details {
overflow-y: auto;
}

View File

@ -0,0 +1,83 @@
@import "bootstrap/scss/functions";
// font-awesome font file locations in relation to target location of the css file.
$fa-font-path: "../fonts" !default;
// flag icons location in relation to target location of the css file.
$flag-icon-css-path: "../fonts/flags/" !default;
$white: #fff !default;
$black: #000 !default;
$blue: #0070d2 !default;
$green: #008827 !default;
// primary is replaced by css variable --skin-primary-color-1
$primary: #00a1e0 !default;
$red: #c00 !default;
$success: $green !default;
$danger: $red !default;
$light-blue: #7ed0ee !default;
// Consolidated values
$grey1: #f9f9f9 !default;
$grey2: #eee !default;
$grey3: #ccc !default;
$grey4: #999 !default;
$grey5: #666 !default;
$grey6: #444 !default;
$grey7: #222 !default;
$grey8: #333 !default;
$grey-transparent-1: rgba(0, 0, 0, 0.65) !default;
$grey-transparent-2: rgba(0, 0, 0, 0.25) !default;
$light-gray: $grey1 !default;
$slightly-darker-gray: $grey4 !default;
$dark-gray: $grey6 !default;
$darker-gray: $grey7 !default;
$horizontal-rule-grey: $grey3 !default;
$product-number-grey: $grey3 !default;
$horizontal-border-grey: $grey4 !default;
$menu-link: $grey6 !default;
$close-menu-bg: $grey2 !default;
$link-color: $dark-gray !default;
$hr-border-color: $grey3 !default;
$grid-breakpoints: (
xs: 0,
sm: 544px,
md: 769px,
lg: 992px,
xl: 1200px
) !default;
$container-max-widths: (
md: 720px,
lg: 940px,
xl: 1140px
) !default;
$border-radius: 0.1875rem !default;
$border-radius-lg: $border-radius !default;
$border-radius-sm: $border-radius !default;
$state-danger-text: #fff !default;
$alert-success-text: #fff !default;
// Font sizes
$base16-14px: 0.875em !default;
// Bootstrap overrides
$body-bg: $grey1 !default;
$card-cap-bg: $white !default;
// Tabs
$nav-tabs-border-width: 0 !default;
$nav-tabs-border-radius: 0 !default;
$nav-tabs-active-link-hover-bg: transparent !default;
// breadcrumb
$breadcrumb-bg: transparent !default;
// table border
$table-border-color: $grey3 !default;
$svg-check: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E") !default;

View File

@ -0,0 +1,62 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "../components/formFields";
.account-image {
background-image: url(../../images/account.jpg);
background-position-y: 40%;
}
.card-footer > a {
color: #{var(--skin-primary-color-1)};
text-decoration: underline;
}
.card-header > a {
color: #{var(--skin-primary-color-1)};
float: right;
text-decoration: underline;
}
.card-info-group {
p {
margin-bottom: 0;
}
div {
margin-bottom: 1rem;
}
div:last-child {
margin-bottom: 0;
}
}
.order-history-control,
.order-history {
h2 {
font-weight: bold;
margin-top: 0.5rem;
}
}
.dashboard-order-card-image {
width: 7rem;
padding-right: 1rem;
}
.dashboard-order-card-footer-columns:last-child {
text-align: right;
}
.dashboard-order-card-footer-value {
font-weight: bold;
font-size: 1rem;
}
.card-header h2,
.card-header h3 {
font-size: 1.5rem;
margin-bottom: 0;
}

View File

@ -0,0 +1,19 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "account";
@import "../utilities/deleteCardButton";
.card-body-positioning {
position: relative;
}
.card-make-default-link {
margin-top: 0.625em; /* 10/16 */
}
.remove-btn {
@include delete-card-button();
width: 3rem;
}

View File

@ -0,0 +1,44 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "account";
.card-footer-border {
border-top: 1px dashed $horizontal-border-grey;
}
.card-make-default-link {
margin-top: 1rem;
}
.dashboard-cards-block-title {
font-weight: bold;
}
.dashboard-order-card-status {
text-transform: capitalize;
}
.dashboard-order-card-image {
width: 7rem;
}
.account-landing-ordercard {
padding: 0;
height: 1rem;
margin-left: 1.25rem;
border-bottom: 0 none;
margin-top: 1rem;
h4 {
font-size: 1.2rem;
}
}
div.order-number {
padding-top: 0.2rem;
}
.dashboard-cards-block-title {
margin-bottom: 1em;
}

View File

@ -0,0 +1,14 @@
@import "../variables";
@import "../checkout/checkoutComponents";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
.section-label {
font-size: $receipt-font-size;
font-weight: bolder;
}
.my-account {
text-align: center;
margin-bottom: 0.938em;
}

View File

@ -0,0 +1,36 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "account";
@import "../utilities/deleteCardButton";
@import "../components/creditCardField";
.back-to-account-link {
margin-bottom: 0;
}
.card-body-positioning {
position: relative;
}
.make-default-payment {
margin-bottom: 1rem;
}
.masked-card-number {
margin-top: 1rem;
}
.payment-to-remove {
font-weight: bold;
}
.remove-btn {
@include delete-card-button();
width: 3rem;
}
.no-saved-payments {
font-family: var(--skin-header-font), sans-serif;
}

View File

@ -0,0 +1,17 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "account";
.profile-back-to-account-link {
color: #{var(--skin-primary-color-1)};
text-decoration: underline;
display: block;
text-align: center;
}
.tracking-consent {
color: #{var(--skin-link-color-1)};
text-decoration: underline;
cursor: pointer;
}

View File

@ -0,0 +1,314 @@
@import "variables";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "utilities/deleteCardButton";
@import "components/quickView";
$spacer: 0.625em;
$negative-spacer: -0.625rem;
.single-approaching-discount {
border: 1px solid rgba(0, 0, 0, 0.125);
background-color: $white;
color: $success;
margin-bottom: 0.3125rem;
}
.checkout-continue {
position: fixed;
bottom: 0;
z-index: 1;
padding-right: 0;
padding-left: 0;
@include media-breakpoint-down(xs) {
background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
div {
padding: $spacer;
}
}
@include media-breakpoint-up(sm) {
position: static;
padding-right: 0.938em;
padding-left: 0.938em;
}
}
.edit {
margin-right: 0.625em;
}
.product-edit {
margin-top: auto;
a {
font-size: 0.813em;
}
}
.line-item-attributes {
font-size: 0.813rem;
margin: 0;
}
.item-attributes {
display: inline-block;
vertical-align: top;
}
.line-item-divider {
margin: $spacer $negative-spacer $spacer $negative-spacer;
}
.line-item-name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 1em;
color: $darker-gray;
font-weight: bold;
margin-bottom: $spacer;
width: 90%;
}
.line-item-price {
font-size: 1em;
color: $darker-gray;
font-weight: bold;
}
.line-item-price-info {
font-size: 0.75em;
margin-bottom: 0.5rem;
}
.no-margin-top {
margin-top: 0;
}
.number-of-items {
font-size: 1.25rem;
margin-top: 1rem;
@include media-breakpoint-up(sm) {
margin-top: 0;
}
}
.optional-promo {
color: #{var(--skin-primary-color-1)};
}
.product-info {
margin-bottom: 0.313em;
padding: $spacer;
@include media-breakpoint-up(md) {
height: auto;
}
}
.product-to-remove {
font-weight: bold;
}
.item-image {
height: 5.625em;
width: 5.625em;
margin-right: 0.938em;
flex-grow: 0;
flex-shrink: 0;
img.product-image {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
max-height: 5.625em;
}
}
.promo-code-form {
display: none;
@include media-breakpoint-up(sm) {
display: block;
}
}
.promo-code-submit {
padding-left: 0;
}
.quantity-form {
margin-bottom: 0;
margin-top: -0.313em;
}
.product-info {
.remove-btn {
color: $slightly-darker-gray;
font-size: 1.625em;
padding: 0;
position: absolute;
top: $negative-spacer;
right: 0.25rem;
border: none;
background-color: $white;
@include media-breakpoint-up(lg) {
top: 0;
bottom: 0;
right: $negative-spacer;
margin-top: $negative-spacer;
margin-bottom: $negative-spacer;
}
}
.remove-btn-lg {
@include delete-card-button();
padding-left: 0.3125rem;
padding-right: 0.3125rem;
z-index: 1;
}
}
.bonus-product {
display: block;
text-align: center;
}
.remove-line-item {
position: relative;
}
.remove-coupon {
border: none;
background: transparent;
}
.sub-total {
font-weight: bold;
}
.grand-total {
font-size: 1em;
font-weight: 600;
}
.coupon-price-adjustment + .coupon-price-adjustment {
margin-top: 0.625rem;
}
.coupon-price-adjustment {
background-color: $white;
padding: 0.625em;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0.1875rem;
}
.coupon-promotion-relationship {
font-size: 0.813em;
padding-left: 1rem;
margin-bottom: 0;
}
.coupons-and-promos {
margin-bottom: 0.625rem;
padding-right: 0;
padding-left: 0;
}
.coupon-code {
font-size: 1.125em;
}
.coupon-applied {
color: $success;
font-size: 0.813em;
}
.coupon-not-applied {
color: $danger;
font-size: 0.813em;
}
.coupon-error {
color: $danger;
margin-top: 0.25rem;
}
.coupon-missing-error {
display: none;
}
.applied-promotion-discount {
color: $success;
float: right;
}
.promotion-information {
margin-bottom: 0.625rem;
margin-top: 0.625rem;
}
.line-item-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.bundle-includes {
font-size: 0.813em;
margin-bottom: 0.625em;
}
.cart-page .bundled-line-item + .bundled-line-item::before,
.cart-page .bonus-line-item-row + .bonus-line-item-msg::before {
content: "";
display: block;
border-bottom: 0.063em dashed $horizontal-rule-grey;
margin: 0.625em -0.625em;
}
.quantity-label {
font-size: 0.813em;
}
.quantity {
width: 100%;
min-width: 5em;
}
.bundle-misc {
font-size: 0.813rem;
@include media-breakpoint-down(md) {
margin-bottom: 0.625em;
}
}
.cart-error-messaging.cart-error {
position: fixed;
top: 0;
width: 100%;
z-index: 2;
}
.valid-cart-error {
min-height: 6.5rem;
}
.bundled-line-item {
.item-attributes {
margin-left: 0;
}
}
.bonus-product-button {
margin-right: 1.5em;
}

View File

@ -0,0 +1,225 @@
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
$checkout-font-weight: 600;
$receipt-font-size: 0.875rem;
$receipt-spacing: 0.625em;
$stored-payment-spacing: 1rem;
.page {
background-color: $light-gray;
}
.checkout-card-header {
font-size: 1.5rem;
}
.grand-total-price {
float: right;
font-weight: $checkout-font-weight;
}
.grand-total-label {
font-weight: $checkout-font-weight;
}
.grand-total {
font-size: 1.125rem !important;
font-weight: 600;
}
.order-receipt-label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.product-divider {
margin-left: -$receipt-spacing;
margin-right: -$receipt-spacing;
hr {
border-top: dashed 0.063em;
}
}
.product-line-item + .product-line-item::before,
.multi-shipping + .product-line-item::before {
content: "";
display: block;
border-bottom: 0.063em dashed $horizontal-rule-grey;
margin: $receipt-spacing -0.625em;
@include media-breakpoint-up(lg) {
margin: $receipt-spacing -1.225em;
}
}
.shipment-block + .shipment-block::before {
content: "";
display: block;
border-bottom: 0.063em dashed $horizontal-rule-grey;
margin: $receipt-spacing -0.625em;
@include media-breakpoint-up(lg) {
margin: $receipt-spacing -1.225em;
}
}
.shipping-method {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.hero-confirmation {
background-image: url('../../images/thankyou.jpg');
background-position-y: -8.125em;
}
.product-summary-block {
margin: 1em 0;
h3 {
font-size: 1.25rem;
}
}
.leading-lines {
overflow: hidden;
margin: 0;
label {
background-color: white;
}
.start-lines {
padding: 1px;
span {
position: relative;
background-color: white;
z-index: 2;
}
&::before {
float: left;
width: 0;
white-space: nowrap;
content: ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
z-index: 1;
color: #adadad;
}
}
.end-lines {
padding: 1px;
span {
position: relative;
background-color: white;
z-index: 2;
}
}
}
.summary-details {
font-size: 0.938em;
margin-bottom: 1em;
}
.summary-details .address-summary {
margin-bottom: 0.5em;
}
.summary-section-label {
font-size: 1em;
font-weight: $checkout-font-weight;
@include media-breakpoint-up(lg) {
font-weight: 500;
}
}
.add-payment {
margin-top: $stored-payment-spacing;
}
.selected-payment {
background-color: $gray-200;
}
.saved-security-code {
margin-top: $stored-payment-spacing;
}
.saved-credit-card-type {
font-weight: 600;
}
.saved-payment-information {
margin-top: $stored-payment-spacing;
margin-bottom: $stored-payment-spacing;
}
.payment-information {
margin-bottom: $stored-payment-spacing;
}
.checkout-hidden {
display: none;
}
.card-image {
margin-top: 0.5rem;
width: 100%;
}
.cancel-new-payment {
margin-top: $stored-payment-spacing;
}
.form-check.start-lines {
padding-left: 1.5rem;
}
.multi-ship .single-shipping .shipping-content {
display: none;
}
.multi-ship .shipping-summary .single-shipping {
display: none;
}
.gift-message-block {
padding-bottom: 1em;
padding-top: 1em;
}
.single-shipping .summary-section-label {
margin-bottom: 0;
}
.confirm-details .shipping-method,
.confirm-details .shipping-method-price {
margin-bottom: 0;
}
.multi-ship .confirm-details .single-shipping {
display: none;
}
.multi-shipping {
display: none;
}
.contact-info-block {
border-bottom: 0.063em dashed $horizontal-rule-grey;
}
.view-address-block h3,
.shipping-method-block h3 {
font-size: 1.25rem;
}

View File

@ -0,0 +1,339 @@
@import "../variables";
@import "bootstrap/scss/variables";
@import "checkoutComponents";
@import "bootstrap/scss/mixins/breakpoints";
@import "../components/formFields";
@import "../components/creditCardField";
$checkout-font-weight: 600;
.card.ghost {
opacity: 0.5;
}
.arrival-time {
white-space: pre;
}
.billing-address {
display: block;
}
.checkout-checkbox {
font-size: 0.875em;
}
.customer-information-block .btn-link {
color: #{var(--skin-link-color-2)};
padding: 0;
vertical-align: baseline;
}
.edit-button,
.btn-show-details,
.btn-add-new {
color: #{var(--skin-primary-color-1)};
float: right;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.edit-button {
border: none;
padding: 0;
background: none;
}
.error-message {
display: none;
}
.next-step-button {
position: fixed;
bottom: 0;
z-index: 3;
padding-right: 0;
padding-left: 0;
@include media-breakpoint-down(xs) {
background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
div {
padding: $spacer;
}
}
@include media-breakpoint-up(sm) {
position: static;
padding-right: 0.938em;
padding-left: 0.938em;
}
}
.shipping-methods {
font-size: 1.125em;
font-weight: $checkout-font-weight;
}
.shipping-method-option {
font-size: 0.938em;
}
.shipping-method-pricing {
font-weight: bolder;
}
.multi-ship .multi-shipping {
display: block;
}
span.ship-to-name,
span.ship-to-address1,
span.ship-to-address2,
span.ship-to-phone,
span.ship-to-city-st-zip {
display: block;
}
.data-checkout-stage {
// Initial states ------------------------
&[data-checkout-stage] {
.card.payment-summary,
.shipping-summary {
display: none;
}
button.place-order {
display: none;
}
button.submit-payment {
display: none;
}
button.submit-shipping {
display: none;
}
}
// Customer ------------------------------
&[data-checkout-stage=customer] {
.card.ghost.customer {
display: none;
}
.card.customer-summary {
display: none;
}
.card.shipping-section {
display: none;
}
.card.payment-form {
display: none;
}
button.submit-customer {
display: block;
}
}
// Shipping ------------------------------
&[data-checkout-stage=shipping] {
.card.customer-section {
display: none;
}
button.submit-customer {
display: none;
}
.card.ghost.customer {
display: none;
}
.card.ghost {
display: none;
}
&.multi-ship .order-product-summary {
display: none;
}
.card.payment-form {
display: none;
}
button.submit-shipping {
display: block;
}
.shipment-selector-block {
.btn-show-details,
.btn-add-new {
border: none;
}
}
[data-address-mode=customer] {
.shipping-address-block {
display: none;
}
}
[data-address-mode=shipment] {
.shipping-address-form {
display: none;
}
}
[data-address-mode=edit] {
.shipping-address-block {
display: none;
}
}
[data-address-mode=new] {
.btn-show-details,
.btn-add-new {
display: none;
}
}
}
// Payment -------------------------------
&[data-checkout-stage=payment] {
button.submit-customer {
display: none;
}
.customer-section,
.shipping-section,
.card.ghost {
display: none;
}
.card.payment-form,
.shipping-summary {
display: block;
}
button.submit-payment {
display: block;
}
.address-selector-block {
.btn-show-details,
.btn-add-new {
border: none;
}
}
[data-address-mode=customer] {
.billing-address {
display: none;
}
}
[data-address-mode=shipment] {
.billing-address {
display: none;
}
}
[data-address-mode=edit] {
.billing-address {
display: none;
}
}
[data-address-mode=new] {
.btn-show-details,
.btn-add-new {
display: none;
}
}
[data-address-mode=details] {
.btn-show-details,
.btn-add-new {
display: none;
}
}
}
// Place Order -----------------------------
&[data-checkout-stage=placeOrder] {
button.submit-customer,
.customer-section,
.shipping-section,
.card.payment-form,
.card.ghost {
display: none;
}
.card.payment-summary,
.shipping-summary {
display: block;
}
button.place-order {
display: block;
}
}
&[data-checkout-stage=submitted] {
.shipping-form,
.card.payment-form,
button.submit-customer,
.card.ghost {
display: none;
}
.summary-section-label.shipping-addr-label {
display: none;
}
.card.payment-summary,
.shipping-summary {
display: block;
}
button.place-order {
display: none;
}
}
option[value=new] {
display: none;
}
h5 > span {
font-size: 0.8em;
}
}
[data-customer-type=guest] .single-shipping .shipment-selector-block {
display: none;
}
.single-shipping .multi-ship-action-buttons {
display: none;
}
.single-shipping .view-address-block {
display: none;
}
.btn-show-details {
padding-top: 0;
}
.multi-ship-address-actions .btn-save-multi-ship {
margin-left: 10px;
}

View File

@ -0,0 +1,19 @@
@import "../variables";
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins/breakpoints";
@import "../components/formFields";
.login-oauth {
margin-top: 1rem;
}
.total-items-label {
font-weight: bold;
margin-bottom: 0.25rem;
}
.total-price {
font-weight: bold;
margin-bottom: 0.25rem;
}

View File

@ -0,0 +1,4 @@
.container .breadcrumb {
border-radius: 0;
border-bottom: $border-width solid $grey3;
}

View File

@ -0,0 +1,20 @@
.category-tile {
position: relative;
h1,
h2 {
font-size: 1.75rem;
position: absolute;
bottom: 1.875rem;
left: 1.875rem;
color: $white;
}
&::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
background-image: linear-gradient(to bottom, transparent 60%, rgba(0, 0, 0, 0.5) 100%);
}
}

View File

@ -0,0 +1,46 @@
@each $size in map-keys($grid-breakpoints) {
@include media-breakpoint-down($size) {
.collapsible-#{$size} {
.title {
line-height: 2.5rem; /* 40/16 */
@include clearfix;
&::after {
float: right;
content: "\f078";
font-family: "FontAwesome";
}
}
.content,
.card-body {
display: none;
}
&.active {
.title::after {
content: "\f077";
margin-top: -0.125em; /* 2/16 */
}
.content,
.card-body {
display: block;
}
}
}
}
.container div.collapsible-#{$size} button.title {
color: $black;
text-decoration: none;
border: none;
background-color: transparent;
&:hover {
text-decoration: none;
}
}
}

View File

@ -0,0 +1,86 @@
.modal-background {
background-color: $black;
display: none;
height: 100%;
position: fixed;
opacity: 0.5;
width: 100%;
top: 0;
left: 0;
}
input[placeholder] {
text-overflow: ellipsis;
}
header ~ #maincontent .container a:not(.btn-primary, .btn-outline-primary) {
color: #{var(--skin-link-color-2)};
}
.hide-order-discount {
display: none;
}
.hide-shipping-discount {
display: none;
}
.order-discount {
color: $success;
}
.shipping-discount {
color: $success;
}
.error-messaging {
position: fixed;
top: 0;
width: 100%;
z-index: 1;
}
.error-hero {
background-image: url('../images/storelocator.jpg');
margin-bottom: 0.625em;
}
.error-message {
margin-top: 3.125rem;
margin-bottom: 3.125rem;
}
.error.continue-shopping {
margin-bottom: 6.25em;
}
.error-unassigned-category {
color: $red;
}
.skip {
position: absolute;
left: 0;
top: -4.2em;
overflow: hidden;
padding: 1em 1.5em;
background: $white;
transition: all 0.2s ease-in-out;
}
a.skip:active,
a.skip:focus,
a.skip:hover {
left: 0;
top: 0;
width: auto;
height: auto;
z-index: 10000000;
background: $white;
transition: all 0.2s ease-in-out;
}
.card-header-custom {
font-size: 1.5rem;
margin-bottom: 0;
}

View File

@ -0,0 +1,39 @@
.card-number-wrapper {
position: relative;
&::after {
content: '';
position: absolute;
right: 3px;
background-repeat: no-repeat;
background-image: url('../../images/credit.png');
background-size: contain;
width: 48px;
height: 30px;
top: 5px;
}
&[data-type="visa"]::after {
background-image: url('../../images/payment-types.png');
background-size: auto;
background-position: -162px -110px;
}
&[data-type="mastercard"]::after {
background-image: url('../../images/payment-types.png');
background-size: auto;
background-position: -295px -110px;
}
&[data-type="amex"]::after {
background-image: url('../../images/payment-types.png');
background-size: auto;
background-position: -230px -15px;
}
&[data-type="discover"]::after {
background-image: url('../../images/payment-types.png');
background-size: auto;
background-position: -95px -110px;
}
}

View File

@ -0,0 +1,145 @@
@import "collapsibleItem";
@import "toastMessage";
footer {
background-color: $gray-200;
padding-top: 1.25em; /* 20/16 */
padding-bottom: 1.25em;
h2 {
font-size: $font-size-base;
margin-bottom: 0;
line-height: 2.5em; /* 40/16 */
}
ul {
list-style: none;
padding-left: 0;
}
.social {
h2 {
margin-top: 0;
}
@include clearfix;
}
.copyright,
.social {
margin-top: 1.25em;
}
.footer-container .footer-item.collapsible-xs button {
font-family: 'Dosis', sans-serif;
padding: 0;
}
.social-links {
@include clearfix;
@include media-breakpoint-down(xs) {
width: 80%;
}
float: left;
li {
float: left;
margin: 0.313em;
@include media-breakpoint-down(xs) {
width: 20%;
text-align: center;
}
}
a {
font-size: 2.25em;
&:hover {
text-decoration: none;
}
}
}
.store {
@include media-breakpoint-down(xs) {
border-bottom: 1px solid $dark-gray;
.content {
display: none;
}
h2 {
@include clearfix;
&::after {
font-family: "FontAwesome";
float: right;
content: "\f041";
}
}
}
}
.content {
font-size: 0.875em;
li {
height: 1.875rem;
}
}
.copyright,
.postscript {
font-size: 0.8125em;
}
.copyright {
margin-bottom: 0.625em;
}
.back-to-top {
margin: 0.1em 0.313em;
padding: 0;
background-color: transparent;
border: 0;
-webkit-appearance: none;
i {
&.fa-arrow-up {
color: rgba(0, 0, 0, 0.7);
}
&.fa-circle {
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
}
}
}
.footer-item {
@include media-breakpoint-down(xs) {
border-bottom: 1px solid $dark-gray;
}
}
#consent-tracking {
.button-wrapper {
button {
margin: 0.5em;
}
}
}
.email-signup-message {
@include toast-message();
}
.email-signup-alert {
@include toast-alert();
}

View File

@ -0,0 +1,7 @@
.form-group {
&.required .form-control-label::before {
content: "*";
color: $danger;
}
}

View File

@ -0,0 +1,197 @@
@import "menu";
$banner-padding: 0.3125em;
$menu-padding: 0.5em;
$menu-item-margin: 0.625em;
.header {
position: relative;
}
.navbar-header {
height: 4.375em; /* 70/16 */
.user,
.country-selector,
.search,
.minicart {
display: inline-block;
margin: 1.125em 0 0 0.5em;
}
.user,
.country-selector,
.minicart,
.navbar-toggler {
line-height: 2.25em; /* 36/16 */
height: auto;
}
.navbar-toggler {
font-size: 1.6em;
width: auto;
}
.user {
position: relative;
.popover {
position: absolute;
display: none;
padding: 1em;
top: 85%;
left: 0;
a {
white-space: nowrap;
margin-bottom: 0.5em;
}
&::before {
left: 1.5rem;
}
&::after {
left: 1.5rem;
}
&.show {
display: block;
}
}
}
}
.brand {
position: absolute;
left: 50%;
display: block;
text-align: center;
img {
width: 100%;
}
@include media-breakpoint-up(lg) {
width: 14.125em; /* 226/16 */
margin-left: -7.0625em; /* 113/16 */
padding-top: 0.5em;
}
@include media-breakpoint-down(md) {
width: 4em; /* 64/16 */
margin-left: -2em;
padding-top: 0.8em;
}
}
.main-menu {
background-color: $dark-gray;
.navbar .close-menu button,
.navbar .close-button button {
background-color: transparent;
border: 0;
-webkit-appearance: none;
}
}
.header-banner {
background-color: $darker-gray;
text-align: center;
color: $white;
.close-button {
width: 1.5em + $banner-padding * 2;
.close {
opacity: 1;
color: $white;
width: 100%;
height: 100%;
background-color: #{var(--skin-primary-color-1)};
}
}
.content {
margin-right: 1.5em;
padding-top: $banner-padding;
padding-bottom: $banner-padding;
}
}
.minicart {
margin-top: 0.1875em; /* 3/16 */
vertical-align: top;
.minicart-icon {
font-size: 1.5em;
}
a.minicart-link {
&:hover {
text-decoration: none;
}
}
.minicart-quantity {
background-color: #{var(--skin-primary-color-1)};
border-radius: 50%;
width: 1.25em; /* 20/16 */
height: 1.25em; /* 20/16 */
line-height: normal;
display: inline-block;
text-align: center;
font-size: 0.8125em; /* 13/16 */
position: relative;
top: -0.9375em; /* 15/16 */
left: -0.9375em; /* 15/16 */
color: $white;
}
}
a.normal {
color: #{var(--skin-primary-color-1)};
text-decoration: underline;
}
.slide-up {
transition-duration: 0.5s;
transition-timing-function: ease-in;
max-height: 100px;
overflow: hidden;
&.hide {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
}
.dropdown-country-selector {
margin-top: -0.0625em;
}
.cookie-warning-messaging.cookie-warning {
position: fixed;
bottom: 0;
left: 50%;
transform: translate(-50%, 0);
text-align: center;
display: none;
}
.valid-cookie-warning {
background-color: #{var(--skin-link-color-1)};
color: $white;
white-space: nowrap;
p {
margin-top: 0;
margin-bottom: 0.2em;
padding-right: 2em;
}
}

View File

@ -0,0 +1,149 @@
@import "../utilities/swatch";
@import "../variables";
.site-search {
position: relative;
height: 2.5em; /* 40/16 */
@include media-breakpoint-up(sm) {
margin-right: 0.5em; /* 20/16 */
}
@include media-breakpoint-up(xl) {
width: 20em; /* 320/16 */
}
@include media-breakpoint-only(md) {
width: 14.0625em; /* 225/16 */
}
@include media-breakpoint-only(sm) {
width: 12.5em; /* 200/16 */
}
.fa-close,
.fa-search {
position: absolute;
border: none;
top: 0.5625em; /* 9/16 */
right: 0.5625em; /* 9/16 */
padding: 0;
background-color: transparent;
}
.reset-button {
position: absolute;
border: none;
top: 0.5625em; /* 9/16 */
right: 2em; /* 32/16 */
padding: 0;
background-color: transparent;
}
input {
padding-right: 2rem;
height: 100%;
}
}
@include media-breakpoint-down(xs) {
.header-search {
.site-search {
display: none;
}
}
}
.suggestions-wrapper {
position: relative;
}
.suggestions {
display: block;
position: absolute;
border: 1px solid $grey3;
background-color: $white;
top: 0;
right: 0;
width: 21.875rem;
z-index: 3;
@include media-breakpoint-only(xs) {
display: flex;
position: fixed;
width: 100%;
}
.swatch-circle {
@include swatch(2.5em, $white);
}
.header {
color: $grey4;
font-size: 0.875em;
padding-top: 0.625em;
&:not(:first-child) {
border-top: 1px solid $grey3;
}
}
.items {
padding: 0.313em 0;
}
.item {
padding-bottom: 0.625em;
.name {
margin-top: 0.313em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@include media-breakpoint-down(xs) {
padding-bottom: 0.938em;
}
}
.category-parent {
color: $grey4;
font-size: 0.875em;
}
.selected {
background-color: $grey2;
}
.container {
list-style-type: none;
}
}
.more-below {
-moz-border-radius: 1.25em;
background: $grey3;
border: 0.063em solid rgba(0, 0, 0, 0.1);
border-radius: 1.25em;
bottom: 1.875em;
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.3);
display: none;
height: 2.5em;
position: fixed;
right: 1.875em;
width: 2.5em;
i.fa-long-arrow-down {
border-radius: 50%;
color: $white;
display: table-caption;
height: 0.75em;
font-size: 1.5rem;
left: 0.57em;
line-height: 0.8em;
position: absolute;
top: 0.4em;
width: 0.8em;
}
}

View File

@ -0,0 +1,8 @@
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Dosis', sans-serif;
}

Some files were not shown because too many files have changed in this diff Show More