Adds SFRA 6.0
This commit is contained in:
parent
d04eb5dd16
commit
823c7608c3
17
storefront-reference-architecture/.circleci/config.yml
Normal file
17
storefront-reference-architecture/.circleci/config.yml
Normal 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
|
14
storefront-reference-architecture/.editorconfig
Normal file
14
storefront-reference-architecture/.editorconfig
Normal 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
|
6
storefront-reference-architecture/.eslintignore
Normal file
6
storefront-reference-architecture/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
cartridges/app_storefront_base/cartridge/static/
|
||||
coverage/
|
||||
doc/
|
||||
bin/
|
||||
codecept.conf.js
|
||||
cartridges/bm_app_storefront_base/cartridge/static/
|
15
storefront-reference-architecture/.eslintrc.json
Normal file
15
storefront-reference-architecture/.eslintrc.json
Normal 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"
|
||||
}
|
||||
}
|
43
storefront-reference-architecture/.gitignore
vendored
Normal file
43
storefront-reference-architecture/.gitignore
vendored
Normal 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
|
16
storefront-reference-architecture/.istanbul.yml
Normal file
16
storefront-reference-architecture/.istanbul.yml
Normal 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
|
17
storefront-reference-architecture/.stylelintrc.json
Normal file
17
storefront-reference-architecture/.stylelintrc.json
Normal 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"] } ]
|
||||
}
|
||||
}
|
106
storefront-reference-architecture/CONTRIBUTING.md
Normal file
106
storefront-reference-architecture/CONTRIBUTING.md
Normal 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).
|
||||
|
||||
|
||||
|
34
storefront-reference-architecture/PULL_REQUEST_TEMPLATE.md
Normal file
34
storefront-reference-architecture/PULL_REQUEST_TEMPLATE.md
Normal 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?
|
101
storefront-reference-architecture/README.md
Normal file
101
storefront-reference-architecture/README.md
Normal 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)
|
140
storefront-reference-architecture/bin/Makefile.js
Normal file
140
storefront-reference-architecture/bin/Makefile.js
Normal 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).');
|
||||
}
|
||||
}
|
46
storefront-reference-architecture/bin/test-functional-docker.sh
Executable file
46
storefront-reference-architecture/bin/test-functional-docker.sh
Executable 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
|
20
storefront-reference-architecture/bitbucket-pipelines.yml
Normal file
20
storefront-reference-architecture/bitbucket-pipelines.yml
Normal 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/*"
|
@ -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>
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"ecmaVersion": 5,
|
||||
"plugins": {
|
||||
"guess-types": {
|
||||
|
||||
},
|
||||
"outline": {
|
||||
|
||||
},
|
||||
"demandware": {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
@ -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
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"jquery": true
|
||||
},
|
||||
"rules": {
|
||||
"global-require": "off",
|
||||
"no-var": "off",
|
||||
"prefer-const": "off"
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./addressBook/addressBook'));
|
||||
});
|
@ -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">×</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;
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
}
|
||||
});
|
@ -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));
|
||||
});
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./cart/cart'));
|
||||
});
|
@ -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">×</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">×</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">×</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();
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./checkout/checkout'));
|
||||
});
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
@ -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">×</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;
|
@ -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
|
||||
}
|
||||
|
||||
};
|
@ -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
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
};
|
@ -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;
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
};
|
@ -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
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
};
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
};
|
@ -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">×</span>' +
|
||||
'</button>' + message + '</div>';
|
||||
|
||||
$(element).append(errorHtml);
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
@ -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>');
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
};
|
@ -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;
|
||||
});
|
||||
};
|
@ -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();
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
};
|
@ -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();
|
||||
};
|
@ -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');
|
||||
});
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./contactUs/contactUs'));
|
||||
});
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
@ -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();
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./login/login'));
|
||||
});
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
@ -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');
|
||||
});
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./orderHistory/orderHistory'));
|
||||
});
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./paymentInstruments/paymentInstruments'));
|
||||
});
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
@ -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">×</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
|
||||
};
|
@ -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()
|
||||
};
|
@ -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">×</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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./product/detail'));
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./product/quickView'));
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./profile/profile'));
|
||||
});
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./search/search'));
|
||||
processInclude(require('./product/quickView'));
|
||||
});
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var processInclude = require('./util');
|
||||
|
||||
$(document).ready(function () {
|
||||
processInclude(require('./storeLocator/storeLocator'));
|
||||
});
|
@ -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);
|
||||
}));
|
||||
}
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
@ -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');
|
@ -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]();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -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";
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
.container .breadcrumb {
|
||||
border-radius: 0;
|
||||
border-bottom: $border-width solid $grey3;
|
||||
}
|
@ -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%);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
|
||||
.form-group {
|
||||
&.required .form-control-label::before {
|
||||
content: "*";
|
||||
color: $danger;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user