├── postcss.config.js
├── src
├── declarations.d.ts
├── interfaces
│ ├── CookieCategoriesPreferences.ts
│ ├── CookieCategories.ts
│ └── Options.ts
├── htmlTools.ts
├── language-list.const.ts
├── themes
│ ├── _theme.scss
│ ├── theme-styles.ts
│ └── themesController.ts
├── index-accessibility.test.ts
├── index-direction.test.ts
├── index-button.test.ts
├── styles.scss
├── index.ts
├── preferencesControl.ts
├── index-constructor.test.ts
└── index.test.ts
├── webpack.dev.js
├── tsconfig.json
├── CODE_OF_CONDUCT.md
├── .github
└── workflows
│ ├── publish-workflow.yaml
│ └── build.yaml
├── azure-pipelines.yml
├── dist
├── index.html
└── consent-banner.d.ts
├── webpack.prod.js
├── styleMock.js
├── jest.config.js
├── LICENSE
├── package.json
├── webpack.common.js
├── .gitignore
├── tslint.json
├── SECURITY.md
└── README.md
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.scss";
2 | declare module "style-loader/dist/runtime/injectStylesIntoStyleTag";
3 |
--------------------------------------------------------------------------------
/src/interfaces/CookieCategoriesPreferences.ts:
--------------------------------------------------------------------------------
1 | export interface ICookieCategoriesPreferences {
2 | [key: string]: boolean | undefined;
3 | }
4 |
--------------------------------------------------------------------------------
/src/interfaces/CookieCategories.ts:
--------------------------------------------------------------------------------
1 | export interface ICookieCategory {
2 | id: string;
3 | name: string;
4 | descHtml: string;
5 | isUnswitchable?: boolean; // optional, prevents toggling the category. True only for categories like Essential cookies.
6 | }
7 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require("webpack-merge");
2 | const common = require("./webpack.common.js");
3 |
4 | module.exports = merge(common, {
5 | devServer: {
6 | contentBase: "./dist",
7 | watchContentBase: true,
8 | headers: {
9 | "Content-Security-Policy": "style-src 'nonce-test1'"
10 | }
11 | }
12 | });
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "strict": true,
6 | "noImplicitReturns": true,
7 | "noImplicitAny": true,
8 | "module": "esnext",
9 | "moduleResolution": "node",
10 | "target": "es5",
11 | "allowJs": true,
12 | },
13 | "include": [
14 | "./src/**/*"
15 | ]
16 | }
--------------------------------------------------------------------------------
/src/htmlTools.ts:
--------------------------------------------------------------------------------
1 | export class HtmlTools {
2 | public static escapeHtml(s: string | undefined): string {
3 | if (s) {
4 | return s.replace(/&/g, "&")
5 | .replace(//g, ">")
7 | .replace(/"/g, """)
8 | .replace(/'/g, "'");
9 | }
10 | else {
11 | return "";
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/.github/workflows/publish-workflow.yaml:
--------------------------------------------------------------------------------
1 | name: Node.js Package
2 | on:
3 | release:
4 | types: [created]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | # Setup .npmrc file to publish to npm
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: '12.x'
14 | registry-url: 'https://registry.npmjs.org'
15 | - run: npm install
16 | - run: npm publish
17 | env:
18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_NEW }}
19 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Node.js
2 | # Build a general Node.js project with npm.
3 | # Add steps that analyze code, save build artifacts, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'ubuntu-latest'
11 |
12 | steps:
13 | - task: NodeTool@0
14 | inputs:
15 | versionSpec: '12.x'
16 | displayName: 'Install Node.js'
17 |
18 | - script: |
19 | npm install
20 | npm run build-prod
21 | npm run lint
22 | npm run test-ci
23 | displayName: 'npm install and build'
24 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test page
6 |
7 |
8 |
9 |
10 |
11 | aaabcdef
12 |
13 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/language-list.const.ts:
--------------------------------------------------------------------------------
1 | // culture can be just language "en" or language-country "en-us".
2 | // Based on language RTL should be applied (https://www.w3.org/International/questions/qa-scripts.en)
3 | // Locale IDs are from
4 | // https://help.bing.microsoft.com/#apex/18/en-US/10004/-1
5 | // https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a
6 | export const RTL_LANGUAGE: string[] = [
7 | 'ar',
8 | 'he',
9 | 'ps',
10 | 'ur',
11 | 'fa',
12 | 'pa',
13 | 'sd',
14 | 'tk',
15 | 'ug',
16 | 'yi',
17 | 'syr',
18 | 'ks-arab' // Kashmiri (Arabic) is rtl
19 | ];
20 |
21 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | let mergedConfig = common;
5 |
6 | let targetRule = mergedConfig.module.rules[1];
7 | let targetRegex = /\.scss$/;
8 | if (targetRule.test.toString() !== targetRegex.toString()) {
9 | targetRule = mergedConfig.module.rules.find(element => element.test.toString() === targetRegex.toString());
10 | }
11 |
12 | let targetLoader = targetRule.use[1];
13 | if (targetLoader.loader !== "css-loader") {
14 | targetLoader = targetRule.use.find(element => element.loader === "css-loader");
15 | }
16 |
17 | targetLoader.options.modules.localIdentName = "[hash:base64]";
18 | module.exports = mergedConfig;
19 |
--------------------------------------------------------------------------------
/styleMock.js:
--------------------------------------------------------------------------------
1 | // Reference: identity-obj-proxy
2 |
3 | /* eslint-disable no-var, comma-dangle */
4 | var Reflect; // eslint-disable-line no-unused-vars
5 | var idObj;
6 |
7 | function checkIsNodeV6OrAbove() {
8 | if (typeof process === 'undefined') {
9 | return false;
10 | }
11 |
12 | return parseInt(process.versions.node.split('.')[0], 10) >= 6;
13 | }
14 |
15 | if (!checkIsNodeV6OrAbove()) {
16 | Reflect = require('harmony-reflect'); // eslint-disable-line global-require
17 | }
18 |
19 | idObj = new Proxy({}, {
20 | get: function getter(target, key) {
21 | if (key === '__esModule') {
22 | return false;
23 | }
24 | else if (key === 'locals') {
25 | return idObj;
26 | }
27 | return key;
28 | }
29 | });
30 |
31 | module.exports = idObj;
32 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | clearMocks: true,
6 | collectCoverageFrom: [
7 | "src/**/*.{js,jsx,ts,tsx}",
8 | "!*.d.ts"
9 | ],
10 | coverageReporters: [
11 | "html"
12 | ],
13 | moduleNameMapper: {
14 | "^.+\\.(css|scss)$": "/styleMock.js"
15 | },
16 | resolver: "jest-pnp-resolver",
17 | testMatch: [
18 | "/src/**/*.test.ts?(x)"
19 | ],
20 | testEnvironment: "jsdom",
21 | testURL: "http://localhost",
22 | transform: {
23 | "^.+\\.tsx?$": "/node_modules/ts-jest",
24 | },
25 | transformIgnorePatterns: [
26 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$"
27 | ],
28 | testPathIgnorePatterns: [
29 | ],
30 | //testResultsProcessor: "/node_modules/jest-junit-reporter",
31 | };
32 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ master ]
10 | pull_request:
11 | branches: [ master ]
12 |
13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
14 | jobs:
15 | # This workflow contains a single job called "build"
16 | build:
17 | # The type of runner that the job will run on
18 | runs-on: ubuntu-latest
19 | # Steps represent a sequence of tasks that will be executed as part of the job
20 | steps:
21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
22 | - uses: actions/checkout@v2
23 | # Setup .npmrc file to publish to npm
24 | - uses: actions/setup-node@v1
25 | with:
26 | node-version: '12.x'
27 | registry-url: 'https://registry.npmjs.org'
28 | # Runs a single command using the runners shell
29 | - run: npm install
30 | - run: npm run build
31 | - run: npm run test-ci
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "consent-banner",
3 | "version": "2.2.0",
4 | "description": "The library which will generate a banner at the specified position for asking the cookie preferences.",
5 | "main": "dist/consent-banner.js",
6 | "types": "dist/consent-banner.d.ts",
7 | "keywords": [
8 | "cookie preferences",
9 | "banner"
10 | ],
11 | "homepage": "https://github.com/microsoft/consent-banner",
12 | "author": "Microsoft",
13 | "license": "MIT",
14 | "files": [
15 | "dist/consent-banner.js",
16 | "dist/consent-banner.d.ts"
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/microsoft/consent-banner.git"
21 | },
22 | "scripts": {
23 | "prepare": "npm run build-prod",
24 | "build": "webpack -d --mode development --config ./webpack.dev.js",
25 | "build-prod": "webpack -p --mode production --config ./webpack.prod.js",
26 | "start": "webpack-dev-server --open --config ./webpack.dev.js",
27 | "test": "jest --watchAll --config ./jest.config.js",
28 | "test-coverage": "jest --coverage --config ./jest.config.js",
29 | "test-ci": "jest --config ./jest.config.js",
30 | "lint": "tslint -p tsconfig.json -t stylish"
31 | },
32 | "dependencies": {
33 | "style-loader": "^1.1.4"
34 | },
35 | "devDependencies": {
36 | "@types/jest": "^25.2.1",
37 | "@types/node-sass": "^4.11.0",
38 | "awesome-typescript-loader": "^5.2.1",
39 | "css-loader": "^3.5.2",
40 | "identity-obj-proxy": "^3.0.0",
41 | "jest": "^25.3.0",
42 | "jest-junit-reporter": "^1.1.0",
43 | "jest-pnp-resolver": "^1.2.1",
44 | "node-sass": "^4.14.1",
45 | "postcss-loader": "^3.0.0",
46 | "postcss-preset-env": "^6.7.0",
47 | "sass-loader": "^8.0.2",
48 | "ts-jest": "^25.3.1",
49 | "tslint": "^6.1.1",
50 | "tslint-microsoft-contrib": "^6.2.0",
51 | "typescript": "^3.8.3",
52 | "webpack": "^4.42.1",
53 | "webpack-cli": "^3.3.11",
54 | "webpack-dev-server": "^3.11.0",
55 | "webpack-merge": "^5.1.4"
56 | },
57 | "browserslist": [
58 | ">0.2%",
59 | "not ie <= 9",
60 | "not dead",
61 | "not op_mini all"
62 | ]
63 | }
64 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 |
4 | const config = {
5 | entry: "./src/index.ts",
6 | output: {
7 | path: path.resolve(__dirname, "dist"),
8 | filename: "consent-banner.js",
9 | libraryTarget: "umd",
10 | library: "ConsentControl"
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.css$/,
16 | exclude: /\.module\.css$/,
17 | use: [
18 | "style-loader",
19 | {
20 | loader: "css-loader",
21 | options: {
22 | importLoaders: 1,
23 | modules: true
24 | }
25 | },
26 | {
27 | loader: "postcss-loader", options: {
28 | ident: "postcss",
29 | plugins: () => [
30 | postcssPresetEnv({
31 | autoprefixer: {
32 | flexbox: "no-2009",
33 | },
34 | stage: 2,
35 | })
36 | ]
37 | }
38 | }
39 | ],
40 | },
41 | {
42 | test: /\.scss$/,
43 | use: [
44 | {
45 | loader: "css-loader",
46 | options: {
47 | importLoaders: 1,
48 | modules: {
49 | exportGlobals: true,
50 | localIdentName: "[path][name]__[local]--[hash:base64:5]" // use '[hash:base64]' for production
51 | }
52 | }
53 | },
54 | {
55 | loader: "postcss-loader", options: {
56 | ident: "postcss",
57 | plugins: () => [
58 | require("postcss-preset-env")({
59 | autoprefixer: {
60 | flexbox: "no-2009",
61 | },
62 | stage: 2
63 | })
64 | ]
65 | }
66 | },
67 | "sass-loader"
68 | ]
69 | },
70 | {
71 | test: /\.ts(x)?$/,
72 | exclude: /node_modules/,
73 | use: [
74 | "awesome-typescript-loader"
75 | ]
76 | }
77 | ]
78 | },
79 | resolve: {
80 | extensions: [
81 | ".ts",
82 | ".js"
83 | ]
84 | }
85 | };
86 |
87 | module.exports = config;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # Host html file
48 | !dist/index.html
49 |
50 | # TypeScript v2 declaration files
51 | !dist/consent-banner.d.ts
52 |
53 | # TypeScript cache
54 | *.tsbuildinfo
55 |
56 | # Optional npm cache directory
57 | .npm
58 |
59 | # Optional eslint cache
60 | .eslintcache
61 |
62 | # Microbundle cache
63 | .rpt2_cache/
64 | .rts2_cache_cjs/
65 | .rts2_cache_es/
66 | .rts2_cache_umd/
67 |
68 | # Optional REPL history
69 | .node_repl_history
70 |
71 | # Output of 'npm pack'
72 | *.tgz
73 |
74 | # Yarn Integrity file
75 | .yarn-integrity
76 |
77 | # dotenv environment variables file
78 | .env
79 | .env.test
80 |
81 | # parcel-bundler cache (https://parceljs.org/)
82 | .cache
83 |
84 | # Next.js build output
85 | .next
86 |
87 | # Nuxt.js build / generate output
88 | .nuxt
89 | dist
90 |
91 | # Gatsby files
92 | .cache/
93 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
94 | # https://nextjs.org/blog/next-9-1#public-directory-support
95 | # public
96 |
97 | # vuepress build output
98 | .vuepress/dist
99 |
100 | # Serverless directories
101 | .serverless/
102 |
103 | # FuseBox cache
104 | .fusebox/
105 |
106 | # DynamoDB Local files
107 | .dynamodb/
108 |
109 | # TernJS port file
110 | .tern-port
111 |
112 | # OS generated files
113 | .DS_Store
114 | .DS_Store?
115 | ._*
116 | .Spotlight-V100
117 | .Trashes
118 | ehthumbs.db
119 | Thumbs.db
120 |
--------------------------------------------------------------------------------
/src/interfaces/Options.ts:
--------------------------------------------------------------------------------
1 | export interface IOptions {
2 | textResources?: ITextResources;
3 | themes?: IThemes;
4 | initialTheme?: string;
5 | stylesNonce?: string;
6 | }
7 |
8 | export interface IThemes {
9 | [key: string]: ITheme | undefined;
10 |
11 | light?: ITheme;
12 | dark?: ITheme;
13 | "high-contrast"?: ITheme;
14 | }
15 |
16 | export interface ITheme {
17 | "close-button-color": string;
18 | "secondary-button-disabled-opacity": string;
19 | "secondary-button-hover-shadow": string;
20 | "primary-button-disabled-opacity": string;
21 | "primary-button-hover-border": string;
22 | "primary-button-disabled-border": string;
23 | "primary-button-hover-shadow": string;
24 |
25 | "banner-background-color": string;
26 | "dialog-background-color": string;
27 | "primary-button-color": string;
28 | "text-color": string;
29 | "secondary-button-color": string;
30 | "secondary-button-disabled-color": string;
31 | "secondary-button-border": string;
32 |
33 | "background-color-between-page-and-dialog"?: string;
34 | "dialog-border-color"?: string;
35 | "hyperlink-font-color"?: string;
36 | "secondary-button-hover-color"?: string;
37 | "secondary-button-hover-border"?: string;
38 | "secondary-button-disabled-border"?: string;
39 | "secondary-button-focus-border-color"?: string;
40 | "secondary-button-text-color"?: string;
41 | "secondary-button-disabled-text-color"?: string;
42 | "primary-button-hover-color"?: string;
43 | "primary-button-disabled-color"?: string;
44 | "primary-button-border"?: string;
45 | "primary-button-focus-border-color"?: string;
46 | "primary-button-text-color"?: string;
47 | "primary-button-disabled-text-color"?: string;
48 | "radio-button-border-color"?: string;
49 | "radio-button-checked-background-color"?: string;
50 | "radio-button-hover-border-color"?: string;
51 | "radio-button-hover-background-color"?: string;
52 | "radio-button-disabled-color"?: string;
53 | "radio-button-disabled-border-color"?: string;
54 | }
55 |
56 | export interface ITextResources {
57 | bannerMessageHtml?: string;
58 | acceptAllLabel?: string;
59 | rejectAllLabel?: string;
60 | moreInfoLabel?: string;
61 | preferencesDialogCloseLabel?: string;
62 | preferencesDialogTitle?: string;
63 | preferencesDialogDescHtml?: string;
64 | acceptLabel?: string;
65 | rejectLabel?: string;
66 | saveLabel?: string;
67 | resetLabel?: string;
68 | }
69 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-microsoft-contrib"],
3 | "rules": {
4 | // coding style
5 | "align": [true, "elements", "members", "statements"],
6 | "ban-types": [true, ["Object", "Strong typing preferred"], ["AnyAction"]],
7 | "function-name": [true, {
8 | "static-method-regex": "^[a-z][\\w\\d]+$"
9 | }],
10 | "quotemark": [true, "double"],
11 | "linebreak-style": false,
12 | "max-func-body-length": false,
13 | "max-line-length": false,
14 | "member-ordering": [true, {"order": [
15 | "public-static-field",
16 | "public-instance-field",
17 | "protected-static-field",
18 | "protected-instance-field",
19 | "private-static-field",
20 | "private-instance-field",
21 | "public-constructor",
22 | "protected-constructor",
23 | "private-constructor",
24 | "public-static-method",
25 | "public-instance-method",
26 | "protected-static-method",
27 | "private-static-method",
28 | "protected-instance-method",
29 | "private-instance-method"
30 | ] }],
31 | "newline-before-return": false,
32 | "newline-per-chained-call": false,
33 | "no-consecutive-blank-lines": [true, 2],
34 | "typedef": [true, "parameter", "property-declaration", "member-variable-declaration", "array-destructuring"],
35 | "variable-name": [true, "allow-pascal-case", "ban-keywords"],
36 |
37 | // modules
38 | "export-name": false,
39 | "import-name": false,
40 | "no-submodule-imports": false,
41 | "no-relative-imports": false,
42 | "no-default-export": false,
43 | "no-import-side-effect": [true, {"ignore-module": "(\\.png|\\.jpg|\\.svg|\\.css|\\.scss)$"}],
44 | "no-implicit-dependencies": [true, "dev"],
45 | "ordered-imports": false,
46 |
47 | // documentation
48 | "completed-docs": false,
49 | "missing-jsdoc": false,
50 |
51 | // best practices
52 | "no-floating-promises": true,
53 | "no-increment-decrement": false,
54 | "no-null-keyword": false,
55 | "no-parameter-reassignment": false,
56 | "no-unsafe-any": false,
57 | "no-unused-expression": [true, "allow-fast-null-checks", "allow-new"],
58 | "no-void-expression": [true, "ignore-arrow-function-shorthand"],
59 | "jsx-no-lambda": false,
60 | "jsx-no-multiline-js": false,
61 | "strict-boolean-expressions": false,
62 | "underscore-consistent-invocation": false,
63 | "use-simple-attributes": false,
64 | "no-console": false,
65 |
66 | // tests
67 | "mocha-no-side-effect-code": false
68 | },
69 | "linterOptions": {
70 | "exclude": [
71 | "*.js",
72 | "node_modules/**/*.ts"
73 | ]
74 | }
75 | }
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
40 |
41 |
--------------------------------------------------------------------------------
/src/themes/_theme.scss:
--------------------------------------------------------------------------------
1 | $secondary-button-disabled-opacity: 1;
2 | $secondary-button-hover-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25);
3 | $primary-button-disabled-opacity: 1;
4 | $primary-button-hover-border: none;
5 | $primary-button-disabled-border: none;
6 | $primary-button-hover-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25);
7 |
8 | $primary-button-color: #0067B8;
9 | $secondary-button-color: #EBEBEB;
10 | $secondary-button-disabled-color: rgba(0, 0, 0, 0.2);
11 | $secondary-button-border: none;
12 |
13 | $text-color: #000000;
14 | $hyperlink-font-color: #0067B8;
15 |
16 | $secondary-button-hover-color: #DBDBDB;
17 | $secondary-button-hover-border: none;
18 | $secondary-button-disabled-border: none;
19 | $secondary-button-focus-border-color: #000000;
20 | $secondary-button-text-color: #000000;
21 | $secondary-button-disabled-text-color: rgba(0, 0, 0, 0.2);
22 |
23 | $primary-button-hover-color: #0067B8;
24 | $primary-button-disabled-color: rgba(0, 120, 215, 0.2);
25 | $primary-button-border: none;
26 | $primary-button-focus-border-color: #000000;
27 | $primary-button-text-color: #FFFFFF;
28 | $primary-button-disabled-text-color: rgba(0, 0, 0, 0.2);
29 |
30 | .textColorTheme {
31 | color: $text-color;
32 | }
33 |
34 | .hyperLinkTheme a {
35 | color: $hyperlink-font-color;
36 | }
37 |
38 | .secondaryButtonTheme {
39 | background-color: $secondary-button-color;
40 | border: $secondary-button-border;
41 | color: $secondary-button-text-color;
42 |
43 | &:enabled:hover {
44 | color: $secondary-button-text-color;
45 | background-color: $secondary-button-hover-color;
46 | box-shadow: $secondary-button-hover-shadow;
47 | border: $secondary-button-hover-border;
48 | }
49 |
50 | &:enabled:focus {
51 | background-color: $secondary-button-hover-color;
52 | box-shadow: $secondary-button-hover-shadow;
53 | border: 2px solid $secondary-button-focus-border-color;
54 | }
55 |
56 | &:disabled {
57 | opacity: $secondary-button-disabled-opacity;
58 | color: $secondary-button-disabled-text-color;
59 | background-color: $secondary-button-disabled-color;
60 | border: $secondary-button-disabled-border;
61 | }
62 | }
63 |
64 | .primaryButtonTheme {
65 | border: $primary-button-border;
66 | background-color: $primary-button-color;
67 | color: $primary-button-text-color;
68 |
69 | &:enabled:hover {
70 | color: $primary-button-text-color;
71 | background-color: $primary-button-hover-color;
72 | box-shadow: $primary-button-hover-shadow;
73 | border: $primary-button-hover-border;
74 | }
75 |
76 | &:enabled:focus {
77 | background-color: $primary-button-hover-color;
78 | box-shadow: $primary-button-hover-shadow;
79 | border: 2px solid $primary-button-focus-border-color;
80 | }
81 |
82 | &:disabled {
83 | opacity: $primary-button-disabled-opacity;
84 | color: $primary-button-disabled-text-color;
85 | background-color: $primary-button-disabled-color;
86 | border: $primary-button-disabled-border;
87 | }
88 | }
--------------------------------------------------------------------------------
/src/themes/theme-styles.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_THEMES = {
2 |
3 | lightTheme: {
4 | "close-button-color": "#666666",
5 | "secondary-button-disabled-opacity": "1",
6 | "secondary-button-hover-shadow": "0px 4px 10px rgba(0, 0, 0, 0.25)",
7 | "primary-button-disabled-opacity": "1",
8 | "primary-button-hover-border": "none",
9 | "primary-button-disabled-border": "none",
10 | "primary-button-hover-shadow": "0px 4px 10px rgba(0, 0, 0, 0.25)",
11 | "banner-background-color": "#F2F2F2",
12 | "dialog-background-color": "#FFFFFF",
13 | "primary-button-color": "#0067B8",
14 | "text-color": "#000000",
15 | "secondary-button-color": "#EBEBEB",
16 | "secondary-button-disabled-color": "rgba(0, 0, 0, 0.2)",
17 | "secondary-button-border": "none",
18 | "background-color-between-page-and-dialog": "rgba(255, 255, 255, 0.6)",
19 | "dialog-border-color": "#0067B8",
20 | "hyperlink-font-color": "#0067B8",
21 | "secondary-button-hover-color": "#DBDBDB",
22 | "secondary-button-hover-border": "none",
23 | "secondary-button-disabled-border": "none",
24 | "secondary-button-focus-border-color": "#000000",
25 | "secondary-button-text-color": "#000000",
26 | "secondary-button-disabled-text-color": "rgba(0, 0, 0, 0.2)",
27 | "primary-button-hover-color": "#0067B8",
28 | "primary-button-disabled-color": "rgba(0, 120, 215, 0.2)",
29 | "primary-button-border": "none",
30 | "primary-button-focus-border-color": "#000000",
31 | "primary-button-text-color": "#FFFFFF",
32 | "primary-button-disabled-text-color": "rgba(0, 0, 0, 0.2)",
33 | "radio-button-border-color": "#000000",
34 | "radio-button-checked-background-color": "#000000",
35 | "radio-button-hover-border-color": "#0067B8",
36 | "radio-button-hover-background-color": "rgba(0, 0, 0, 0.8)",
37 | "radio-button-disabled-color": "rgba(0, 0, 0, 0.2)",
38 | "radio-button-disabled-border-color": "rgba(0, 0, 0, 0.2)"
39 | },
40 |
41 | darkTheme: {
42 | "close-button-color": "#E3E3E3",
43 | "secondary-button-disabled-opacity": "0.5",
44 | "secondary-button-hover-shadow": "none",
45 | "primary-button-disabled-opacity": "0.5",
46 | "primary-button-hover-border": "1px solid rgba(0, 0, 0, 0)",
47 | "primary-button-disabled-border": "1px solid rgba(255, 255, 255, 0)",
48 | "primary-button-hover-shadow": "none",
49 | "banner-background-color": "#242424",
50 | "dialog-background-color": "#171717",
51 | "primary-button-color": "#4DB2FF",
52 | "text-color": "#E3E3E3",
53 | "secondary-button-color": "#171717",
54 | "secondary-button-disabled-color": "#2E2E2E",
55 | "secondary-button-border": "1px solid #C7C7C7",
56 | "background-color-between-page-and-dialog": "rgba(23, 23, 23, 0.6)",
57 | "dialog-border-color": "#4DB2FF",
58 | "hyperlink-font-color": "#4DB2FF",
59 | "secondary-button-hover-color": "#2E2E2E",
60 | "secondary-button-hover-border": "1px solid #C7C7C7",
61 | "secondary-button-disabled-border": "1px solid #242424",
62 | "secondary-button-focus-border-color": "#C7C7C7",
63 | "secondary-button-text-color": "#E3E3E3",
64 | "secondary-button-disabled-text-color": "#E3E3E3",
65 | "primary-button-hover-color": "#0091FF",
66 | "primary-button-disabled-color": "#4DB2FF",
67 | "primary-button-border": "1px solid #4DB2FF",
68 | "primary-button-focus-border-color": "#4DB2FF",
69 | "primary-button-text-color": "black",
70 | "primary-button-disabled-text-color": "black",
71 | "radio-button-border-color": "#E3E3E3",
72 | "radio-button-checked-background-color": "#E3E3E3",
73 | "radio-button-hover-border-color": "#4DB2FF",
74 | "radio-button-hover-background-color": "rgba(227, 227, 227, 0.8)",
75 | "radio-button-disabled-color": "rgba(227, 227, 227, 0.2)",
76 | "radio-button-disabled-border-color": "rgba(227, 227, 227, 0.2)"
77 | },
78 |
79 | highContrast: {
80 | "close-button-color": "#E3E3E3",
81 | "secondary-button-disabled-opacity": "0.5",
82 | "secondary-button-hover-shadow": "none",
83 | "primary-button-disabled-opacity": "0.5",
84 | "primary-button-hover-border": "1px solid yellow",
85 | "primary-button-disabled-border": "1px solid white",
86 | "primary-button-hover-shadow": "none",
87 | "banner-background-color": "black",
88 | "dialog-background-color": "black",
89 | "primary-button-color": "yellow",
90 | "text-color": "white",
91 | "secondary-button-color": "black",
92 | "secondary-button-disabled-color": "black",
93 | "secondary-button-border": "1px solid white",
94 | "background-color-between-page-and-dialog": "rgba(0, 0, 0, 0.6)",
95 | "dialog-border-color": "yellow",
96 | "hyperlink-font-color": "yellow",
97 | "secondary-button-hover-color": "black",
98 | "secondary-button-hover-border": "1px solid yellow",
99 | "secondary-button-disabled-border": "1px solid black",
100 | "secondary-button-focus-border-color": "white",
101 | "secondary-button-text-color": "white",
102 | "secondary-button-disabled-text-color": "white",
103 | "primary-button-hover-color": "#FFFF33",
104 | "primary-button-disabled-color": "yellow",
105 | "primary-button-border": "1px solid yellow",
106 | "primary-button-focus-border-color": "yellow",
107 | "primary-button-text-color": "black",
108 | "primary-button-disabled-text-color": "black",
109 | "radio-button-border-color": "white",
110 | "radio-button-checked-background-color": "white",
111 | "radio-button-hover-border-color": "yellow",
112 | "radio-button-hover-background-color": "rgba(255, 255, 255, 0.8)",
113 | "radio-button-disabled-color": "rgba(255, 255, 255, 0.2)",
114 | "radio-button-disabled-border-color": "rgba(255, 255, 255, 0.2)"
115 | }
116 |
117 | };
118 |
--------------------------------------------------------------------------------
/dist/consent-banner.d.ts:
--------------------------------------------------------------------------------
1 | declare interface ITheme {
2 | "close-button-color": string;
3 | "secondary-button-disabled-opacity": string;
4 | "secondary-button-hover-shadow": string;
5 | "primary-button-disabled-opacity": string;
6 | "primary-button-hover-border": string;
7 | "primary-button-disabled-border": string;
8 | "primary-button-hover-shadow": string;
9 | "banner-background-color": string;
10 | "dialog-background-color": string;
11 | "primary-button-color": string;
12 | "text-color": string;
13 | "secondary-button-color": string;
14 | "secondary-button-disabled-color": string;
15 | "secondary-button-border": string;
16 | "background-color-between-page-and-dialog"?: string;
17 | "dialog-border-color"?: string;
18 | "hyperlink-font-color"?: string;
19 | "secondary-button-hover-color"?: string;
20 | "secondary-button-hover-border"?: string;
21 | "secondary-button-disabled-border"?: string;
22 | "secondary-button-focus-border-color"?: string;
23 | "secondary-button-text-color"?: string;
24 | "secondary-button-disabled-text-color"?: string;
25 | "primary-button-hover-color"?: string;
26 | "primary-button-disabled-color"?: string;
27 | "primary-button-border"?: string;
28 | "primary-button-focus-border-color"?: string;
29 | "primary-button-text-color"?: string;
30 | "primary-button-disabled-text-color"?: string;
31 | "radio-button-border-color"?: string;
32 | "radio-button-checked-background-color"?: string;
33 | "radio-button-hover-border-color"?: string;
34 | "radio-button-hover-background-color"?: string;
35 | "radio-button-disabled-color"?: string;
36 | "radio-button-disabled-border-color"?: string;
37 | }
38 | declare interface IThemes {
39 | [key: string]: ITheme | undefined;
40 | light?: ITheme;
41 | dark?: ITheme;
42 | "high-contrast"?: ITheme;
43 | }
44 | declare interface ITextResources {
45 | bannerMessageHtml?: string;
46 | acceptAllLabel?: string;
47 | rejectAllLabel?: string;
48 | moreInfoLabel?: string;
49 | preferencesDialogCloseLabel?: string;
50 | preferencesDialogTitle?: string;
51 | preferencesDialogDescHtml?: string;
52 | acceptLabel?: string;
53 | rejectLabel?: string;
54 | saveLabel?: string;
55 | resetLabel?: string;
56 | }
57 | declare interface IOptions {
58 | textResources?: ITextResources;
59 | themes?: IThemes;
60 | initialTheme?: string;
61 | stylesNonce?: string;
62 | }
63 | declare interface ICookieCategory {
64 | id: string;
65 | name: string;
66 | descHtml: string;
67 | isUnswitchable?: boolean;
68 | }
69 | declare interface ICookieCategoriesPreferences {
70 | [key: string]: boolean | undefined;
71 | }
72 |
73 | declare class PreferencesControl { }
74 |
75 | export declare class ConsentControl {
76 | private containerElement;
77 | culture: string;
78 | onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void;
79 | cookieCategories: ICookieCategory[];
80 | textResources: ITextResources;
81 | themes: IThemes;
82 | preferencesCtrl: PreferencesControl | null;
83 | private direction;
84 | private isDirty;
85 | defaultCookieCategories: ICookieCategory[];
86 | defaultTextResources: ITextResources;
87 | constructor(containerElementOrId: string | HTMLElement, culture: string, onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void, cookieCategories?: ICookieCategory[], options?: IOptions);
88 | /**
89 | * Set the text resources for the banner to display the text in each area
90 | *
91 | * @param {ITextResources} textResources the text want to be displayed
92 | */
93 | setTextResources(textResources: ITextResources): void;
94 | /**
95 | * Use the passed theme to set the theme property
96 | *
97 | * @param {string} name the theme property that we want to set
98 | * @param {ITheme} theme the passed theme that we want to display
99 | */
100 | createTheme(name: string, theme: ITheme): void;
101 | /**
102 | * Apply the theme and change banner and preferences dialog's color
103 | *
104 | * @param {string} themeName theme that will be applied
105 | */
106 | applyTheme(themeName: string): void;
107 | /**
108 | * Insert all necessary HTML code and shows the banner.
109 | * Until this method is called there should be no HTML elements of the Consent Control anywhere in the DOM
110 | *
111 | * @param {ICookieCategoriesPreferences} cookieCategoriesPreferences object that indicates cookie categories preferences
112 | */
113 | showBanner(cookieCategoriesPreferences: ICookieCategoriesPreferences): void;
114 | /**
115 | * Hides the banner and the Preferences Dialog.
116 | * Removes all HTML elements of the Consent Control from the DOM
117 | */
118 | hideBanner(): void;
119 | /**
120 | * Shows Preferences Dialog. Leaves banner state unchanged
121 | *
122 | * @param {ICookieCategoriesPreferences} cookieCategoriesPreferences object that indicates cookie categories preferences
123 | */
124 | showPreferences(cookieCategoriesPreferences: ICookieCategoriesPreferences): void;
125 | /**
126 | * Hides Preferences Dialog.
127 | * Removes all HTML elements of the Preferences Dialog from the DOM. Leaves banner state unchanged
128 | */
129 | hidePreferences(): void;
130 | /**
131 | * Set the container that will be used for the banner
132 | *
133 | * @param {string | HTMLElement} containerElementOrId here the banner will be inserted
134 | */
135 | setContainerElement(containerElementOrId: string | HTMLElement): void;
136 | /**
137 | * Return the container that is used for the banner
138 | */
139 | getContainerElement(): HTMLElement | null;
140 | /**
141 | * Set the direction by passing the parameter or by checking the culture property
142 | *
143 | * @param {string} dir direction for the web, ltr or rtl
144 | */
145 | setDirection(dir?: string): void;
146 | /**
147 | * Return the direction
148 | */
149 | getDirection(): string;
150 | }
151 |
--------------------------------------------------------------------------------
/src/index-accessibility.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsentControl } from "./index";
2 | import * as styles from "./styles.scss";
3 |
4 | describe("Test accessibility when closing event occurs", () => {
5 | let testId: string = "app";
6 | let testElementString = `
7 | `;
10 |
11 | beforeEach(() => {
12 | let newDiv = document.createElement("div");
13 | newDiv.setAttribute("id", testId);
14 | document.body.appendChild(newDiv);
15 | });
16 |
17 | afterEach(() => {
18 | let child = document.getElementById(testId);
19 | if (child) {
20 | let parent = child.parentNode;
21 |
22 | if (parent) {
23 | parent.removeChild(child);
24 | }
25 | else {
26 | throw new Error("Parent not found error");
27 | }
28 | }
29 | });
30 |
31 | test("Focus should be on more info button after we click on close button", () => {
32 | let callBack = function () { return; };
33 | let cc = new ConsentControl("app", "en", callBack);
34 | cc.showBanner({});
35 |
36 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2];
37 | cookieInfo.focus();
38 | cookieInfo.click();
39 |
40 | let closeModalIcon = document.getElementsByClassName(styles.closeModalIcon)[0];
41 | closeModalIcon.click();
42 |
43 | expect(document.activeElement?.innerHTML).toBe("More info");
44 | });
45 |
46 | test("Call showBanner() and showPreferences(). Focus should be on anchor element after we click on close button", () => {
47 | document.body.innerHTML += testElementString;
48 |
49 | let callBack = function () { return; };
50 | let cc = new ConsentControl("app", "en", callBack);
51 |
52 | cc.showBanner({});
53 |
54 | let testElement = document.getElementById("testElementString")!;
55 | testElement.addEventListener("click", () => {
56 | cc.showPreferences({});
57 | });
58 |
59 | testElement.focus();
60 | testElement.click();
61 |
62 | let closeModalIcon = document.getElementsByClassName(styles.closeModalIcon)[0];
63 | closeModalIcon.click();
64 |
65 | expect(document.activeElement?.innerHTML).toBe("Click me");
66 | });
67 | });
68 |
69 | describe("Test accessibility when radio buttons are clicked", () => {
70 | let testId: string = "app";
71 |
72 | function testRadioBtnOutline(current: HTMLInputElement, previous?: HTMLInputElement): void {
73 | let currentParentElement = current.parentElement!;
74 |
75 | current.focus();
76 | current.click();
77 |
78 | expect(currentParentElement.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) !== -1).toBeTruthy();
79 |
80 | if (previous) {
81 | let previousParentElement = previous.parentElement!;
82 | expect(previousParentElement.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy();
83 | }
84 | }
85 |
86 | beforeEach(() => {
87 | let newDiv = document.createElement("div");
88 | newDiv.setAttribute("id", testId);
89 | document.body.appendChild(newDiv);
90 | });
91 |
92 | afterEach(() => {
93 | let child = document.getElementById(testId);
94 | if (child) {
95 | let parent = child.parentNode;
96 |
97 | if (parent) {
98 | parent.removeChild(child);
99 | }
100 | else {
101 | throw new Error("Parent not found error");
102 | }
103 | }
104 | });
105 |
106 | test("The outline of radio button appears when radio button is clicked", () => {
107 | let callBack = function () { return; };
108 | let cc = new ConsentControl("app", "en", callBack);
109 | cc.showBanner({});
110 |
111 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2];
112 | cookieInfo.click();
113 |
114 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
115 |
116 | testRadioBtnOutline(cookieItemRadioBtn[0]);
117 | testRadioBtnOutline(cookieItemRadioBtn[1]);
118 | testRadioBtnOutline(cookieItemRadioBtn[2]);
119 | testRadioBtnOutline(cookieItemRadioBtn[3]);
120 | testRadioBtnOutline(cookieItemRadioBtn[4]);
121 | testRadioBtnOutline(cookieItemRadioBtn[5]);
122 | });
123 |
124 | test("The outline of previous clicked radio button is removed when another radio button is clicked", () => {
125 | let callBack = function () { return; };
126 | let cc = new ConsentControl("app", "en", callBack);
127 | cc.showBanner({});
128 |
129 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2];
130 | cookieInfo.click();
131 |
132 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
133 |
134 | cookieItemRadioBtn[0].focus();
135 | cookieItemRadioBtn[0].click();
136 |
137 | testRadioBtnOutline(cookieItemRadioBtn[0], cookieItemRadioBtn[1]);
138 | testRadioBtnOutline(cookieItemRadioBtn[1], cookieItemRadioBtn[2]);
139 | });
140 |
141 | test("The outline of previous clicked radio button is removed when 'Reset all' button is clicked", () => {
142 | let callBack = function () { return; };
143 | let cc = new ConsentControl("app", "en", callBack);
144 | cc.showBanner({});
145 |
146 | let cookieInfo = document.getElementsByClassName(styles.bannerButton)[2];
147 | cookieInfo.click();
148 |
149 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
150 |
151 | cookieItemRadioBtn[0].focus();
152 | cookieItemRadioBtn[0].click();
153 | cookieItemRadioBtn[3].focus();
154 | cookieItemRadioBtn[3].click();
155 |
156 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0];
157 | resetAllBtn.focus();
158 | resetAllBtn.click();
159 |
160 | expect(cookieItemRadioBtn[0].parentElement!.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy();
161 | expect(cookieItemRadioBtn[3].parentElement!.className.indexOf(styles.cookieItemRadioBtnCtrlOutline) === -1).toBeTruthy();
162 | });
163 | });
--------------------------------------------------------------------------------
/src/index-direction.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsentControl } from "./index";
2 |
3 | describe("Test language direction", () => {
4 | let testId: string = "app";
5 |
6 | beforeEach(() => {
7 | let newDiv = document.createElement("div");
8 | newDiv.setAttribute("id", testId);
9 | document.body.appendChild(newDiv);
10 | });
11 |
12 | afterEach(() => {
13 | let child = document.getElementById(testId);
14 | if (child) {
15 | let parent = child.parentNode;
16 |
17 | if (parent) {
18 | parent.removeChild(child);
19 | }
20 | else {
21 | throw new Error("Parent not found error");
22 | }
23 | }
24 | });
25 |
26 | test("Language is ms (ltr). No cookieCategories, no textResources", () => {
27 | let callBack = function() { return; };
28 | let cc = new ConsentControl(testId, "ms", callBack);
29 | expect(cc.getDirection()).toBe("ltr");
30 | });
31 |
32 | test("Language is ms (ltr). Set direction to rtl. No cookieCategories, no textResources", () => {
33 | let callBack = function() { return; };
34 | let cc = new ConsentControl(testId, "ms", callBack);
35 | cc.setDirection("rtl");
36 | expect(cc.getDirection()).toBe("rtl");
37 | });
38 |
39 | test("Language is ar (rtl). No cookieCategories, no textResources", () => {
40 | let callBack = function() { return; };
41 | let cc = new ConsentControl(testId, "ar", callBack);
42 | expect(cc.getDirection()).toBe("rtl");
43 | });
44 |
45 | test("Language is he (rtl). No cookieCategories, no textResources", () => {
46 | let callBack = function() { return; };
47 | let cc = new ConsentControl(testId, "he", callBack);
48 | expect(cc.getDirection()).toBe("rtl");
49 | });
50 |
51 | test("Language is ps (rtl). No cookieCategories, no textResources", () => {
52 | let callBack = function() { return; };
53 | let cc = new ConsentControl(testId, "ps", callBack);
54 | expect(cc.getDirection()).toBe("rtl");
55 | });
56 |
57 | test("Language is ur (rtl). No cookieCategories, no textResources", () => {
58 | let callBack = function() { return; };
59 | let cc = new ConsentControl(testId, "ur", callBack);
60 | expect(cc.getDirection()).toBe("rtl");
61 | });
62 |
63 | test("Language is fa (rtl). No cookieCategories, no textResources", () => {
64 | let callBack = function() { return; };
65 | let cc = new ConsentControl(testId, "fa", callBack);
66 | expect(cc.getDirection()).toBe("rtl");
67 | });
68 |
69 | test("Language is pa (rtl). No cookieCategories, no textResources", () => {
70 | let callBack = function() { return; };
71 | let cc = new ConsentControl(testId, "pa", callBack);
72 | expect(cc.getDirection()).toBe("rtl");
73 | });
74 |
75 | test("Language is sd (rtl). No cookieCategories, no textResources", () => {
76 | let callBack = function() { return; };
77 | let cc = new ConsentControl(testId, "sd", callBack);
78 | expect(cc.getDirection()).toBe("rtl");
79 | });
80 |
81 | test("Language is tk (rtl). No cookieCategories, no textResources", () => {
82 | let callBack = function() { return; };
83 | let cc = new ConsentControl(testId, "tk", callBack);
84 | expect(cc.getDirection()).toBe("rtl");
85 | });
86 |
87 | test("Language is ug (rtl). No cookieCategories, no textResources", () => {
88 | let callBack = function() { return; };
89 | let cc = new ConsentControl(testId, "ug", callBack);
90 | expect(cc.getDirection()).toBe("rtl");
91 | });
92 |
93 | test("Language is yi (rtl). No cookieCategories, no textResources", () => {
94 | let callBack = function() { return; };
95 | let cc = new ConsentControl(testId, "yi", callBack);
96 | expect(cc.getDirection()).toBe("rtl");
97 | });
98 |
99 | test("Language is syr (rtl). No cookieCategories, no textResources", () => {
100 | let callBack = function() { return; };
101 | let cc = new ConsentControl(testId, "syr", callBack);
102 | expect(cc.getDirection()).toBe("rtl");
103 | });
104 |
105 | test("Language is ks-arab (rtl). No cookieCategories, no textResources", () => {
106 | let callBack = function() { return; };
107 | let cc = new ConsentControl(testId, "ks-arab", callBack);
108 | expect(cc.getDirection()).toBe("rtl");
109 | });
110 |
111 | test("Language is en-US (ltr). No cookieCategories, no textResources", () => {
112 | let callBack = function() { return; };
113 | let cc = new ConsentControl(testId, "en-US", callBack);
114 | expect(cc.getDirection()).toBe("ltr");
115 | });
116 |
117 | test("Language is ar-SA (rtl). No cookieCategories, no textResources", () => {
118 | let callBack = function() { return; };
119 | let cc = new ConsentControl(testId, "ar-SA", callBack);
120 | expect(cc.getDirection()).toBe("rtl");
121 | });
122 | });
123 |
124 | describe("Test html and body direction", () => {
125 | let testId: string = "app";
126 |
127 | let htmlDir: string | null;
128 | let bodyDir: string | null;
129 |
130 | beforeAll(() => {
131 | htmlDir = document.getElementsByTagName("html")[0].getAttribute("dir");
132 | bodyDir = document.getElementsByTagName("body")[0].getAttribute("dir");
133 | });
134 |
135 | beforeEach(() => {
136 | let newDiv = document.createElement("div");
137 | newDiv.setAttribute("id", testId);
138 | document.body.appendChild(newDiv);
139 | });
140 |
141 | afterEach(() => {
142 | if (htmlDir) {
143 | document.getElementsByTagName("html")[0].setAttribute("dir", htmlDir);
144 | }
145 | else {
146 | document.getElementsByTagName("html")[0].removeAttribute("dir");
147 | }
148 |
149 | if (bodyDir) {
150 | document.getElementsByTagName("body")[0].setAttribute("dir", bodyDir);
151 | }
152 | else {
153 | document.getElementsByTagName("body")[0].removeAttribute("dir");
154 | }
155 |
156 | let child = document.getElementById(testId);
157 | if (child) {
158 | let parent = child.parentNode;
159 |
160 | if (parent) {
161 | parent.removeChild(child);
162 | }
163 | else {
164 | throw new Error("Parent not found error");
165 | }
166 | }
167 | });
168 |
169 | test("Language is en (ltr). Html dir is rtl. No cookieCategories, no textResources", () => {
170 | document.getElementsByTagName("html")[0].setAttribute("dir", "rtl");
171 |
172 | let callBack = function() { return; };
173 | let cc = new ConsentControl(testId, "en", callBack);
174 | expect(cc.getDirection()).toBe("rtl");
175 | });
176 |
177 | test("Language is ar (rtl). Html dir is ltr. No cookieCategories, no textResources", () => {
178 | document.getElementsByTagName("html")[0].setAttribute("dir", "ltr");
179 |
180 | let callBack = function() { return; };
181 | let cc = new ConsentControl(testId, "ar", callBack);
182 | expect(cc.getDirection()).toBe("ltr");
183 | });
184 |
185 | test("Language is en (ltr). Body dir is rtl. No cookieCategories, no textResources", () => {
186 | document.getElementsByTagName("body")[0].setAttribute("dir", "rtl");
187 |
188 | let callBack = function() { return; };
189 | let cc = new ConsentControl(testId, "en", callBack);
190 | expect(cc.getDirection()).toBe("rtl");
191 | });
192 |
193 | test("Language is ar (rtl). Body dir is ltr. No cookieCategories, no textResources", () => {
194 | document.getElementsByTagName("body")[0].setAttribute("dir", "ltr");
195 |
196 | let callBack = function() { return; };
197 | let cc = new ConsentControl(testId, "ar", callBack);
198 | expect(cc.getDirection()).toBe("ltr");
199 | });
200 | });
--------------------------------------------------------------------------------
/src/index-button.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsentControl } from "./index";
2 | import { ICookieCategoriesPreferences } from "./interfaces/CookieCategoriesPreferences";
3 | import * as styles from "./styles.scss";
4 |
5 | describe("Test radio buttons and 'Reset all' button", () => {
6 | let testId: string = "app";
7 |
8 | function testRadioBtnState(radioButtons: String[]): void {
9 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
10 | for (let i = 0; i < radioButtons.length; i++) {
11 | if (radioButtons[i] === "checked") {
12 | expect(cookieItemRadioBtn[i].checked).toBeTruthy();
13 | }
14 | else {
15 | expect(cookieItemRadioBtn[i].checked).toBeFalsy();
16 | }
17 | }
18 | }
19 |
20 | beforeEach(() => {
21 | let newDiv = document.createElement("div");
22 | newDiv.setAttribute("id", testId);
23 | document.body.appendChild(newDiv);
24 | });
25 |
26 | afterEach(() => {
27 | let child = document.getElementById(testId);
28 | if (child) {
29 | let parent = child.parentNode;
30 |
31 | if (parent) {
32 | parent.removeChild(child);
33 | }
34 | else {
35 | throw new Error("Parent not found error");
36 | }
37 | }
38 | });
39 |
40 | test("Call showPreferences(...) and then click radio buttons. All cookieCategoriePreferences will be reset to undefined when 'Reset all' is clicked", () => {
41 | let callBack = function() { return; };
42 | let cc = new ConsentControl(testId, "en", callBack);
43 |
44 | let cookieCategoriePreferences = { "c1": true, "c2": false, "c3": undefined };
45 | cc.showPreferences(cookieCategoriePreferences);
46 |
47 | expect(cc.preferencesCtrl).toBeTruthy();
48 |
49 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
50 | cookieItemRadioBtn[1].click();
51 | cookieItemRadioBtn[2].click();
52 | cookieItemRadioBtn[4].click();
53 |
54 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]);
55 |
56 | if (cc.preferencesCtrl) {
57 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": false, "c2": true, "c3": true });
58 | }
59 | else {
60 | throw new Error("Preference dialog not found error");
61 | }
62 |
63 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0];
64 | resetAllBtn.click();
65 |
66 | testRadioBtnState(["unchecked", "unchecked", "unchecked", "unchecked", "unchecked", "unchecked"]);
67 |
68 | if (cc.preferencesCtrl) {
69 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": undefined, "c2": undefined, "c3": undefined });
70 | }
71 | else {
72 | throw new Error("Preference dialog not found error");
73 | }
74 | });
75 |
76 | test("Call showPreferences(...) with unswitchable id and click radio buttons. All cookiePreferences will be reset to undefined when 'Reset all' is clicked", () => {
77 | let callBack = function() { return; };
78 | let cc = new ConsentControl(testId, "en", callBack);
79 |
80 | let cookieCategoriePreferences = { "c0": true, "c1": true, "c2": false, "c3": undefined };
81 | cc.showPreferences(cookieCategoriePreferences);
82 |
83 | expect(cc.preferencesCtrl).toBeTruthy();
84 |
85 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
86 | cookieItemRadioBtn[1].click();
87 | cookieItemRadioBtn[2].click();
88 | cookieItemRadioBtn[4].click();
89 |
90 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]);
91 |
92 | if (cc.preferencesCtrl) {
93 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c0": true, "c1": false, "c2": true, "c3": true });
94 | }
95 | else {
96 | throw new Error("Preference dialog not found error");
97 | }
98 |
99 | let resetAllBtn = document.getElementsByClassName(styles.modalButtonReset)[0];
100 | resetAllBtn.click();
101 |
102 | testRadioBtnState(["unchecked", "unchecked", "unchecked", "unchecked", "unchecked", "unchecked"]);
103 |
104 | if (cc.preferencesCtrl) {
105 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c0": true, "c1": undefined, "c2": undefined, "c3": undefined });
106 | }
107 | else {
108 | throw new Error("Preference dialog not found error");
109 | }
110 | });
111 |
112 | test("Call showPreferences(...) and then click radio buttons. cookieCategoriePreferences will be set", () => {
113 | let callBack = function() { return; };
114 | let cc = new ConsentControl(testId, "en", callBack);
115 |
116 | let cookieCategoriePreferences = { "c1": true, "c2": false, "c3": undefined };
117 | cc.showPreferences(cookieCategoriePreferences);
118 |
119 | expect(cc.preferencesCtrl).toBeTruthy();
120 |
121 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
122 | cookieItemRadioBtn[0].click();
123 | cookieItemRadioBtn[3].click();
124 | cookieItemRadioBtn[5].click();
125 |
126 | testRadioBtnState(["checked", "unchecked", "unchecked", "checked", "unchecked", "checked"]);
127 |
128 | if (cc.preferencesCtrl) {
129 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": true, "c2": false, "c3": false });
130 | }
131 | else {
132 | throw new Error("Preference dialog not found error");
133 | }
134 |
135 | cookieItemRadioBtn[1].click();
136 | cookieItemRadioBtn[2].click();
137 | cookieItemRadioBtn[4].click();
138 |
139 | testRadioBtnState(["unchecked", "checked", "checked", "unchecked", "checked", "unchecked"]);
140 |
141 | if (cc.preferencesCtrl) {
142 | expect(cc.preferencesCtrl.cookieCategoriesPreferences).toEqual({ "c1": false, "c2": true, "c3": true });
143 | }
144 | else {
145 | throw new Error("Preference dialog not found error");
146 | }
147 | });
148 | });
149 |
150 | describe("Test 'Save changes' button", () => {
151 | let testId: string = "app";
152 |
153 | beforeEach(() => {
154 | let newDiv = document.createElement("div");
155 | newDiv.setAttribute("id", testId);
156 | document.body.appendChild(newDiv);
157 | });
158 |
159 | afterEach(() => {
160 | let child = document.getElementById(testId);
161 | if (child) {
162 | let parent = child.parentNode;
163 |
164 | if (parent) {
165 | parent.removeChild(child);
166 | }
167 | else {
168 | throw new Error("Parent not found error");
169 | }
170 | }
171 | });
172 |
173 | test("Call showPreferences(...), click any unchecked radio buttons, and close the dialog. Open dialog and 'Save changes' button will be enabled", () => {
174 | let callBack = function() { return; };
175 | let cc = new ConsentControl(testId, "en", callBack);
176 |
177 | let cookieCategoriePreferences = { "c2": undefined, "c3": false };
178 | cc.showPreferences(cookieCategoriePreferences);
179 |
180 | let cookieItemRadioBtn: HTMLInputElement[] = [].slice.call(document.getElementsByClassName(styles.cookieItemRadioBtn));
181 | cookieItemRadioBtn[0].click();
182 | cookieItemRadioBtn[3].click();
183 |
184 | cc.hidePreferences();
185 |
186 | cc.showPreferences(cookieCategoriePreferences);
187 | expect(cookieCategoriePreferences).toEqual({ "c1": true, "c2": false, "c3": false });
188 |
189 | let saveChangesBtn = document.getElementsByClassName(styles.modalButtonSave)[0];
190 | expect(saveChangesBtn.disabled).toBeFalsy();
191 | });
192 | });
193 |
194 | describe("Test 'Accept all' button", () => {
195 | let testId: string = "app";
196 |
197 | beforeEach(() => {
198 | let newDiv = document.createElement("div");
199 | newDiv.setAttribute("id", testId);
200 | document.body.appendChild(newDiv);
201 | });
202 |
203 | afterEach(() => {
204 | let child = document.getElementById(testId);
205 | if (child) {
206 | let parent = child.parentNode;
207 |
208 | if (parent) {
209 | parent.removeChild(child);
210 | }
211 | else {
212 | throw new Error("Parent not found error");
213 | }
214 | }
215 | });
216 |
217 | test("Click 'Accept all' button and all cookieCategoriePreferences will be set to 'true'", () => {
218 | let callBack = function() { return; };
219 | let cc = new ConsentControl(testId, "en", callBack);
220 |
221 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c2": undefined, "c3": false };
222 | cc.showBanner(cookieCategoriePreferences);
223 |
224 | let acceptAllBtn = document.getElementsByClassName(styles.bannerButton)[0];
225 | acceptAllBtn.click();
226 |
227 | for (let cookieCategory of cc.cookieCategories) {
228 | if (!cookieCategory.isUnswitchable) {
229 | let id = cookieCategory.id;
230 | expect(cookieCategoriePreferences[id]).toBeTruthy();
231 | }
232 | }
233 | });
234 |
235 | test("Initialize cookieCategoriesPreferences with unswitchable id and click 'Accept all' button. All cookieCategoriePreferences will be set to 'true'", () => {
236 | let callBack = function() { return; };
237 | let cc = new ConsentControl(testId, "en", callBack);
238 |
239 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c0": true, "c2": undefined, "c3": false };
240 | cc.showBanner(cookieCategoriePreferences);
241 |
242 | let acceptAllBtn = document.getElementsByClassName(styles.bannerButton)[0];
243 | acceptAllBtn.click();
244 |
245 | for (let cookieCategory of cc.cookieCategories) {
246 | if (!cookieCategory.isUnswitchable) {
247 | let id = cookieCategory.id;
248 | expect(cookieCategoriePreferences[id]).toBeTruthy();
249 | }
250 | }
251 | });
252 | });
253 |
254 | describe("Test 'Reject all' button", () => {
255 | let testId: string = "app";
256 |
257 | beforeEach(() => {
258 | let newDiv = document.createElement("div");
259 | newDiv.setAttribute("id", testId);
260 | document.body.appendChild(newDiv);
261 | });
262 |
263 | afterEach(() => {
264 | let child = document.getElementById(testId);
265 | if (child) {
266 | let parent = child.parentNode;
267 |
268 | if (parent) {
269 | parent.removeChild(child);
270 | }
271 | else {
272 | throw new Error("Parent not found error");
273 | }
274 | }
275 | });
276 |
277 | test("Click 'Reject all' button and all cookieCategoriePreferences will be set to 'false'", () => {
278 | let callBack = function() { return; };
279 | let cc = new ConsentControl(testId, "en", callBack);
280 |
281 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c2": undefined, "c3": false };
282 | cc.showBanner(cookieCategoriePreferences);
283 |
284 | let rejectAllBtn = document.getElementsByClassName(styles.bannerButton)[1];
285 | rejectAllBtn.click();
286 |
287 | for (let cookieCategory of cc.cookieCategories) {
288 | if (!cookieCategory.isUnswitchable) {
289 | let id = cookieCategory.id;
290 | expect(cookieCategoriePreferences[id]).toBeFalsy();
291 | }
292 | }
293 | });
294 |
295 | test("Initialize cookieCategoriesPreferences with unswitchable id and click 'Reject all' button. All cookieCategoriePreferences will be set to 'false'", () => {
296 | let callBack = function() { return; };
297 | let cc = new ConsentControl(testId, "en", callBack);
298 |
299 | let cookieCategoriePreferences: ICookieCategoriesPreferences = { "c0": true, "c2": undefined, "c3": false };
300 | cc.showBanner(cookieCategoriePreferences);
301 |
302 | let rejectAllBtn = document.getElementsByClassName(styles.bannerButton)[1];
303 | rejectAllBtn.click();
304 |
305 | for (let cookieCategory of cc.cookieCategories) {
306 | if (!cookieCategory.isUnswitchable) {
307 | let id = cookieCategory.id;
308 | expect(cookieCategoriePreferences[id]).toBeFalsy();
309 | }
310 | }
311 | });
312 | });
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import "./themes/theme";
2 |
3 | // Banner button minimum size, can be changed
4 | $bannerBtnWidth: 150px;
5 | $bannerBtnHeight: 36px;
6 |
7 | /// Theme.
8 | /// Default: Light
9 | $close-button-color: #666666;
10 |
11 | $banner-background-color: #F2F2F2;
12 | $dialog-background-color: #FFFFFF;
13 |
14 | $background-color-between-page-and-dialog: rgba(255, 255, 255, 0.6);
15 | $dialog-border-color: #0067B8;
16 |
17 | $radio-button-border-color: #000000;
18 | $radio-button-checked-background-color: #000000;
19 | $radio-button-hover-border-color: #0067B8;
20 | $radio-button-hover-background-color: rgba(0, 0, 0, 0.8);
21 | $radio-button-disabled-color: rgba(0, 0, 0, 0.2);
22 | $radio-button-disabled-border-color: rgba(0, 0, 0, 0.2);
23 |
24 | @mixin bannerFont($weight, $size, $style: normal) {
25 | font : {
26 | family: Segoe UI, SegoeUI, Arial, sans-serif;
27 | style: $style;
28 | weight: $weight;
29 | size: $size;
30 | }
31 | }
32 |
33 | // For right-to-left direction
34 | @mixin rtlDesign($margin, $padding, $float: none) {
35 | div[dir="rtl"] & {
36 | margin: $margin;
37 | padding: $padding;
38 | float: $float;
39 | }
40 | }
41 |
42 | // For right-to-left direction (position)
43 | @mixin rtlDesignPosition($left, $right) {
44 | div[dir="rtl"] & {
45 | left: $left;
46 | right: $right;
47 | }
48 | }
49 |
50 | .bannerBody {
51 | position: relative;
52 | display: flex;
53 | z-index: 9999; /* on top of the page */
54 | width: 100%;
55 | background-color: $banner-background-color;
56 |
57 | justify-content: space-between;
58 | text-align: left;
59 |
60 | @at-root div[dir="rtl"]#{ & } {
61 | text-align: right;
62 | }
63 | }
64 |
65 | .bannerInform {
66 | margin: 0;
67 | padding : {
68 | left: 5%;
69 | top: 8px;
70 | bottom: 8px;
71 | }
72 |
73 | @include rtlDesign(0, 8px 5% 8px 0);
74 | }
75 |
76 | .bannerBody svg {
77 | fill: none;
78 | max-width: none;
79 | max-height: none;
80 | }
81 |
82 | .infoIcon {
83 | display: table-cell;
84 | padding: 12px;
85 | width: 24px;
86 | height: 24px;
87 |
88 | @include bannerFont(normal, 24px);
89 |
90 | /* identical to box height */
91 | line-height: 0;
92 | }
93 |
94 | .bannerInformBody {
95 | display: table-cell;
96 | vertical-align: middle;
97 | padding: 0;
98 |
99 | @include bannerFont(normal, 13px);
100 | line-height: 16px;
101 |
102 | /* Add styles to hyperlinks in case websites close the default styles for hyperlinks */
103 | & a {
104 | text-decoration: underline;
105 | }
106 | }
107 |
108 | .buttonGroup {
109 | display: inline-block;
110 | margin : {
111 | left: 5%;
112 | right: 5%;
113 | }
114 |
115 | min-width: 40%;
116 | min-width: calc((#{ $bannerBtnWidth } + 3 * 4px) * 2 + #{ $bannerBtnWidth });
117 | min-width: fit-content;
118 |
119 | align-self: center;
120 | position: relative;
121 | }
122 |
123 | .bannerButton {
124 | margin: 4px;
125 | padding: 5px;
126 |
127 | min-width: $bannerBtnWidth;
128 | min-height: $bannerBtnHeight;
129 | vertical-align: top;
130 |
131 | cursor: pointer;
132 | @include bannerFont(normal, 15px);
133 | line-height: 20px;
134 | text-align: center;
135 |
136 | &:focus {
137 | box-sizing: border-box;
138 | }
139 |
140 | &:disabled {
141 | cursor: not-allowed;
142 | }
143 | }
144 |
145 | .cookieModal {
146 | display: block;
147 | position: fixed;
148 | z-index: 10000; /* on top of the page */
149 | top: 0;
150 | left: 0;
151 | width: 100%;
152 | height: 100%;
153 | background-color: $background-color-between-page-and-dialog;
154 | overflow: auto;
155 | text-align: left;
156 |
157 | @at-root div[dir="rtl"]#{ & } {
158 | text-align: right;
159 | }
160 | @include rtlDesignPosition(auto, 0);
161 | }
162 |
163 | .modalContainer {
164 | position: relative;
165 | top: 8%;
166 | margin : {
167 | bottom: 40px;
168 | left: auto;
169 | right: auto;
170 | }
171 |
172 | box-sizing: border-box;
173 | width: 640px;
174 |
175 | background-color: $dialog-background-color;
176 | border: 1px solid $dialog-border-color;
177 | }
178 |
179 | .closeModalIcon {
180 | float: right;
181 | z-index: 1;
182 | margin: 2px;
183 | padding: 12px;
184 | border: none;
185 |
186 | cursor: pointer;
187 | @include bannerFont(normal, 13px);
188 | line-height: 13px;
189 |
190 | display: flex;
191 | align-items: center;
192 | text-align: center;
193 | color: $close-button-color;
194 | background-color: $dialog-background-color;
195 |
196 | @include rtlDesign(2px, 12px, left);
197 | }
198 |
199 | .modalBody {
200 | position: static;
201 | margin : {
202 | top: 36px;
203 | left: 36px;
204 | right: 36px;
205 | }
206 | }
207 |
208 | .modalTitle {
209 | margin : {
210 | top: 0;
211 | bottom: 12px;
212 | }
213 |
214 | @include bannerFont(600, 20px);
215 | line-height: 24px;
216 | text-transform: none;
217 | }
218 |
219 | .modalContent {
220 | height: 446px;
221 | overflow: auto;
222 | }
223 |
224 | .cookieStatement {
225 | margin : {
226 | top: 0;
227 | }
228 |
229 | @include bannerFont(normal, 15px);
230 | line-height: 20px;
231 |
232 | /* Add styles to hyperlinks in case websites close the default styles for hyperlinks */
233 | & a {
234 | text-decoration: underline;
235 | }
236 | }
237 |
238 | dl.cookieOrderedList {
239 | margin : {
240 | top: 36px;
241 | bottom: 0;
242 | }
243 | padding: 0;
244 |
245 | /* Close the default styles which adds decimal numbers in front of list items */
246 | list-style: none;
247 | text-transform: none;
248 | }
249 |
250 | dt.cookieListItem {
251 | margin : {
252 | top: 20px;
253 | }
254 | float: none;
255 |
256 | @include bannerFont(600, 18px);
257 | line-height: 24px;
258 |
259 | /* Close the default styles which adds decimal numbers in front of list items */
260 | list-style: none;
261 | }
262 |
263 | .cookieListItemGroup {
264 | margin: 0;
265 | padding: 0;
266 | border: none;
267 | }
268 |
269 | .cookieListItemTitle {
270 | margin: 0;
271 | padding: 0;
272 | border-bottom: none;
273 |
274 | @include bannerFont(600, 18px);
275 | line-height: 24px;
276 | text-transform: none;
277 | }
278 |
279 | .cookieListItemDescription {
280 | display: inline-block;
281 | margin : {
282 | top: 0;
283 | bottom: 13px;
284 | }
285 |
286 | @include bannerFont(normal, 15px);
287 | line-height: 20px;
288 | }
289 |
290 | .cookieItemRadioBtnGroup {
291 | display: block;
292 | }
293 |
294 | .cookieItemRadioBtnCtrl {
295 | display: inline-block;
296 | position: relative;
297 | left: 5px;
298 | margin : {
299 | bottom: 13px;
300 | right: 34px;
301 | }
302 | padding: 3px;
303 |
304 | @include rtlDesign(0 0 13px 34px, 3px);
305 | @include rtlDesignPosition(auto, 5px);
306 | }
307 |
308 | .bannerBody *::before, .cookieModal *::before, .bannerBody *::after, .cookieModal *::after {
309 | box-sizing: inherit;
310 | }
311 |
312 | .cookieItemRadioBtnCtrlOutline {
313 | outline: 2px solid $radio-button-hover-background-color;
314 | }
315 |
316 | @mixin defineRaioInput {
317 | & + label::before {
318 | display: block;
319 | position: absolute;
320 | top: 5px;
321 | left: 3px;
322 | height: 19px;
323 | width: 19px;
324 | content: "";
325 | border-radius: 50%;
326 | border: 1px solid $radio-button-border-color;
327 | background-color: $dialog-background-color;
328 |
329 | @include rtlDesignPosition(auto, 3px);
330 | }
331 |
332 | &:not(:disabled) + label {
333 | &:hover {
334 | &::before {
335 | border: 1px solid $radio-button-hover-border-color;
336 | }
337 |
338 | &::after {
339 | display: block;
340 | position: absolute;
341 | top: 10px;
342 | left: 8px;
343 | height: 9px;
344 | width: 9px;
345 | content: "";
346 | border-radius: 50%;
347 | background-color: $radio-button-hover-background-color;
348 |
349 | @include rtlDesignPosition(auto, 8px);
350 | }
351 | }
352 |
353 | &:focus {
354 | &::before {
355 | border: 1px solid $radio-button-hover-border-color;
356 | }
357 |
358 | &::after {
359 | display: block;
360 | position: absolute;
361 | top: 10px;
362 | left: 8px;
363 | height: 9px;
364 | width: 9px;
365 | content: "";
366 | border-radius: 50%;
367 | background-color: $radio-button-checked-background-color;
368 |
369 | @include rtlDesignPosition(auto, 8px);
370 | }
371 | }
372 | }
373 |
374 | &:checked + label::after {
375 | display: block;
376 | position: absolute;
377 | top: 10px;
378 | left: 8px;
379 | height: 9px;
380 | width: 9px;
381 | content: "";
382 | border-radius: 50%;
383 | background-color: $radio-button-checked-background-color;
384 |
385 | @include rtlDesignPosition(auto, 8px);
386 | }
387 |
388 | &:disabled + label {
389 | cursor: not-allowed;
390 |
391 | &::before {
392 | border: 1px solid $radio-button-disabled-border-color;
393 | background-color: $radio-button-disabled-color;
394 | }
395 | }
396 | }
397 |
398 | input[type="radio"].cookieItemRadioBtn {
399 | display: inline-block;
400 | position: relative; /* Adjust the position */
401 | margin : {
402 | top: 0;
403 | left: 0;
404 | right: 0;
405 | }
406 |
407 | height: 0;
408 | width: 0;
409 | border-radius: 0;
410 |
411 | cursor: pointer;
412 | outline: none;
413 | box-sizing: border-box;
414 |
415 | appearance: none;
416 |
417 | /* Define our own radio input in case websites close the default styles for input type radio */
418 | @include defineRaioInput;
419 | }
420 |
421 | .cookieItemRadioBtnLabel {
422 | display: block;
423 | position: static;
424 | float: right;
425 | margin : {
426 | top: 0;
427 | bottom: 0;
428 | left: 19px;
429 | right: 0;
430 | }
431 | padding : {
432 | top: 0;
433 | bottom: 0;
434 | left: 8px;
435 | right: 0;
436 | }
437 | width: 80%; /* If "calc()" is not supported */
438 |
439 | width: calc(100% - 19px);
440 |
441 | @include bannerFont(normal, 15px);
442 | line-height: 20px;
443 | /* identical to box height, or 133% */
444 |
445 | text-transform: none;
446 | cursor: pointer;
447 | box-sizing: border-box;
448 |
449 | @include rtlDesign(0 19px 0 0, 0 8px 0 0, left);
450 | }
451 |
452 | .modalButtonGroup {
453 | margin : {
454 | top: 20px;
455 | bottom: 48px;
456 | }
457 | }
458 |
459 | .modalButtonReset {
460 | padding: 0;
461 |
462 | width: 278px;
463 | height: 36px;
464 |
465 | cursor: pointer;
466 | @include bannerFont(normal, 15px);
467 |
468 | line-height: 20px;
469 | /* identical to box height, or 133% */
470 | text-align: center;
471 |
472 | &:focus {
473 | box-sizing: border-box;
474 | }
475 |
476 | &:disabled {
477 | cursor: not-allowed;
478 | }
479 | }
480 |
481 | .modalButtonSave {
482 | float: right;
483 | padding: 0;
484 |
485 | width: 278px;
486 | height: 36px;
487 |
488 | cursor: pointer;
489 | @include bannerFont(normal, 15px);
490 |
491 | line-height: 20px;
492 | /* identical to box height, or 133% */
493 | text-align: center;
494 |
495 | &:focus {
496 | box-sizing: border-box;
497 | }
498 |
499 | &:disabled {
500 | cursor: not-allowed;
501 | }
502 |
503 | @include rtlDesign(0, 0, left);
504 | }
505 |
506 | @media only screen and (max-width: 768px) {
507 | /* For mobile phones: */
508 |
509 | .buttonGroup, .bannerInform {
510 | padding : {
511 | top: 8px;
512 | bottom: 12px;
513 | left: 3.75%;
514 | right: 3.75%;
515 | }
516 | margin: 0;
517 |
518 | width: 92.5%;
519 | }
520 |
521 | .bannerBody {
522 | display:block;
523 | }
524 |
525 | .bannerButton {
526 | margin : {
527 | bottom: 8px;
528 | left: 0;
529 | right: 0;
530 | }
531 |
532 | width: 100%;
533 | }
534 |
535 | .cookieModal {
536 | overflow: hidden;
537 | }
538 |
539 | .modalContainer {
540 | top: 1.8%;
541 |
542 | width: 93.33%;
543 | height: 96.4%;
544 |
545 | overflow: hidden;
546 | }
547 |
548 | .modalBody {
549 | margin : {
550 | top: 24px;
551 | left: 24px;
552 | right: 24px;
553 | }
554 |
555 | height: 100%;
556 | }
557 |
558 | .modalContent {
559 | height: 62%; /* If "calc()" is not supported */
560 |
561 | height: calc(100% - 188px);
562 | min-height: 50%;
563 | }
564 |
565 | .modalButtonReset {
566 | width: 100%;
567 | }
568 |
569 | .modalButtonSave {
570 | margin : {
571 | bottom: 12px;
572 | left: 0;
573 | }
574 |
575 | width: 100%;
576 |
577 | @include rtlDesign(0 0 12px 0, 0);
578 | }
579 | }
580 |
581 | /* For landscape orientation: */
582 | @media only screen and (max-width: 768px) and (orientation: landscape), only screen and (max-height: 260px), only screen and (max-width: 340px) {
583 | .modalContainer {
584 | overflow: auto;
585 | }
586 | }
587 |
588 | /* For large zoom in: */
589 | @media only screen and (max-height: 260px), only screen and (max-width: 340px) {
590 |
591 | .bannerButton {
592 | min-width: 0;
593 | }
594 |
595 | .closeModalIcon {
596 | padding: 3%;
597 | }
598 |
599 | .modalBody {
600 | margin : {
601 | top: 3%;
602 | left: 3%;
603 | right: 3%;
604 | }
605 | }
606 |
607 | .modalTitle {
608 | margin: {
609 | bottom: 3%;
610 | }
611 | }
612 |
613 | .modalContent {
614 | height: calc(79% - 64px);
615 | }
616 |
617 | .modalButtonGroup {
618 | margin: {
619 | top: 5%;
620 | bottom: 10%;
621 | }
622 | }
623 |
624 | .modalButtonSave {
625 | margin : {
626 | bottom: 3%;
627 | }
628 |
629 | @include rtlDesign(0 0 3% 0, 0);
630 | }
631 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Consent Banner
2 |
3 | ## Overview
4 |
5 | Consent banner is the library which will generate a banner at the specified position for asking the cookie preferences.
6 |
7 | It contains three buttons, `accept all`, `reject all` and `more info`. If the user clicks `more info` button, A dialog will pop up so that the user can set the cookie categories that he/she wants to share with us.
8 |
9 | ## Building and running on localhost
10 |
11 | First install dependencies:
12 |
13 | ```sh
14 | npm i
15 | ```
16 |
17 | To create a production build:
18 |
19 | ```sh
20 | npm run build-prod
21 | ```
22 |
23 | To create a development build:
24 |
25 | ```sh
26 | npm run build
27 | ```
28 |
29 | ## Running
30 |
31 | ```sh
32 | npm run start
33 | ```
34 |
35 | ## Testing
36 |
37 | ```sh
38 | npm run test
39 | ```
40 |
41 | ## Example use
42 |
43 | This is part of your `html` page.
44 |
45 | ```HTML
46 |
47 | ...
48 |
49 | ...
50 |
51 | ```
52 |
53 | If you want to insert a banner into the ``, you can use the following example.
54 |
55 | ```TypeScript
56 |
57 | let cookieCategories: ICookieCategory[] =
58 | [
59 | {
60 | id: "c0",
61 | name: "1. Essential cookies",
62 | descHtml: "We use this cookie, read more here.",
63 | isUnswitchable: true
64 | },
65 | {
66 | id: "c1",
67 | name: "2. Performance & analytics",
68 | descHtml: "We use this cookie, read more here."
69 | },
70 | {
71 | id: "c2",
72 | name: "3. Advertising/Marketing",
73 | descHtml: "Blah"
74 | },
75 | {
76 | id: "c3",
77 | name: "4. Targeting/personalization",
78 | descHtml: "Blah"
79 | }
80 | ];
81 |
82 | let textResources: ITextResources = {
83 | bannerMessageHtml: "We use optional cookies to provide... read here.",
84 | acceptAllLabel: "Accept all",
85 | rejectAllLabel: "Reject all",
86 | moreInfoLabel: "More info",
87 | preferencesDialogCloseLabel: "Close",
88 | preferencesDialogTitle: "Manage cookie preferences",
89 | preferencesDialogDescHtml: "Most Microsoft sites...",
90 | acceptLabel: "Accept",
91 | rejectLabel: "Reject",
92 | saveLabel: "Save changes",
93 | resetLabel: "Reset all"
94 | };
95 |
96 | let callBack = function(obj: any) { console.log(obj); };
97 |
98 | // If you want to set 'nonce: test1' in style tag, add "stylesNonce: 'test1'"
99 | let options: IOptions = {
100 | textResources: textResources,
101 | initialTheme: "dark",
102 | stylesNonce: "test1"
103 | };
104 |
105 | let cc = new ConsentControl("app", "en", callBack, cookieCategories, options);
106 | let cookieCategoriePreferences: ICookieCategoriesPreferences = {
107 | "c1": undefined,
108 | "c2": false,
109 | "c3": true
110 | };
111 |
112 | // Show a banner with dark theme
113 | cc.showBanner(cookieCategoriePreferences);
114 |
115 | let theme: ITheme = {
116 | "close-button-color": "#000080",
117 | "secondary-button-disabled-opacity": "1",
118 | "secondary-button-hover-shadow": "none",
119 | "primary-button-disabled-opacity": "0.65",
120 | "primary-button-hover-border": "none",
121 | "primary-button-disabled-border": "1px double #3CB371",
122 | "primary-button-hover-shadow": "2px 5px 7px #A12A29",
123 | "banner-background-color": "#BA5583",
124 | "dialog-background-color": "#32CD32",
125 | "primary-button-color": "#4B30AE",
126 | "text-color": "#800000",
127 | "secondary-button-color": "#00FA9A",
128 | "secondary-button-disabled-color": "#7B68EE",
129 | "secondary-button-border": "1px dashed #969696",
130 | }
131 |
132 | // Create a new theme, named "medium"
133 | cc.createTheme("medium", theme);
134 |
135 | // Apply the "medium" theme
136 | cc.applyTheme("medium");
137 | ```
138 |
139 | ## Developer Guide
140 |
141 | `ConsentControl` consists of 2 main elements: **Banner** and **Preferences Dialog**. Use `containerElementOrId`, `culture`, `onPreferencesChanged`, `cookieCategories`, `options` to create an instance. `culture` will be used to determine the direction of the banner and preferences dialog.
142 |
143 | ```JavaScript
144 | var cc = new ConsentControl(
145 | // here the banner will be inserted, can be HTMLElement or element id
146 | containerElementOrId: string | HTMLElement,
147 |
148 | // culture can be just language "en" or language-country "en-us".
149 | // Based on language RTL should be applied (https://www.w3.org/International/questions/qa-scripts.en)
150 | culture: string,
151 |
152 | // callback function, called on preferences changes (via "Accept All", "Reject All" or "Save changes"),
153 | // must pass cookieCategoriePreferences, see ICookieCategoriesPreferences in Data types
154 | onPreferencesChanged: (cookieCategoriesPreferences: ICookieCategoriesPreferences) => void,
155 |
156 | // optional, see ICookieCategory in Data types
157 | cookieCategories?: ICookieCategory[],
158 |
159 | // optional, see IOptions in Data types
160 | options?: IOptions
161 | );
162 | ```
163 |
164 | + `setTextResources(textResources: ITextResources)` can be used to set the texts.
165 |
166 | + `createTheme(name: string, theme: ITheme)` can be used to create the new theme. `name` is the name of this new theme, which can be used by `applyTheme(themeName: string)`. `theme` is the new theme object.
167 |
168 | `ConsentControl` consists of three built in themes, `light`, `dark`, and `high-contrast`. `dark` and `high-contrast` themes are from [Microsoft Docs](https://docs.microsoft.com/en-us/).
169 |
170 | + You can apply the theme manually by using `applyTheme(themeName: string)`. `themeName` is the name of the theme which you want to apply.
171 |
172 | If you want to apply a new theme, you need to call `createTheme(name: string, theme: ITheme)` first, and then call `applyTheme(themeName: string)`
173 |
174 | ```JavaScript
175 | let theme: ITheme = {
176 | "close-button-color": "#000080",
177 | "secondary-button-disabled-opacity": "1",
178 | "secondary-button-hover-shadow": "none",
179 | "primary-button-disabled-opacity": "0.65",
180 | "primary-button-hover-border": "none",
181 | "primary-button-disabled-border": "1px double #3CB371",
182 | "primary-button-hover-shadow": "2px 5px 7px #A12A29",
183 | "banner-background-color": "#BA5583",
184 | "dialog-background-color": "#32CD32",
185 | "primary-button-color": "#4B30AE",
186 | "text-color": "#800000",
187 | "secondary-button-color": "#00FA9A",
188 | "secondary-button-disabled-color": "#7B68EE",
189 | "secondary-button-border": "1px dashed #969696",
190 | }
191 |
192 | // Create a new theme, named "medium"
193 | cc.createTheme("medium", theme);
194 |
195 | // Apply the "medium" theme
196 | cc.applyTheme("medium");
197 | ```
198 |
199 | If the name of theme is not in the themes collections, it will throw an error. `new Error("Theme not found error")`
200 |
201 | + `setContainerElement(containerElementOrId: string | HTMLElement)` can be used to set the container element for the banner and preferences dialog.
202 |
203 | If you pass `HTMLElement`, it will be used as the container. If you pass `string`, the method will use it as the `id` to find the container element.
204 |
205 | ```JavaScript
206 | cc.setContainerElement("app");
207 | ```
208 |
209 | or
210 |
211 | ```JavaScript
212 | let container = document.getElementById('app');
213 | cc.setContainerElement(container);
214 | ```
215 |
216 | If the container can not be found, it will throw an error. `new Error("Container not found error")`
217 |
218 | + `getContainerElement()` will return the current container element.
219 |
220 | + You can set the direction manually by using `setDirection(dir?: string)`. `dir` can be `"ltr"` or `"rtl"`. If `dir` is not passed, it will determine the direction by `dir` attribute in `html`, `body` or `culture`.
221 |
222 | ```JavaScript
223 | // There are 3 cases. dir="rtl", dir="ltr", empty
224 |
225 | // Set direction to rtl (right-to-left)
226 | cc.setDirection("rtl");
227 |
228 | // Set direction to ltr (left-to-right)
229 | cc.setDirection("ltr");
230 |
231 | // It will use "dir" attribute in "html" or "body"
232 | // If the "dir" attribute is not specified, it will apply the direction based on "culture"
233 | cc.setDirection();
234 | ```
235 |
236 | + `getDirection()` will return the current direction.
237 |
238 | ### Data types
239 |
240 | There are six data types: `ICookieCategory`, `ITextResources`, `ICookieCategoriesPreferences`, `IOptions`, `IThemes`, and `ITheme`.
241 |
242 | + `ICookieCategory` is used to create cookie categories that will be showed in the preferences dialog.
243 |
244 | + `ITextResources` is the texts that will be used in the banner and preferences dialog.
245 |
246 | + `ICookieCategoriesPreferences` is used to store the preferences in each cookie categories.
247 |
248 | + `IOptions` is the options for the banner. It contains four parts, `textResources`, `themes`, `initialTheme`, and `stylesNonce`.
249 |
250 | + `textResources` is the initial text resources for texts.
251 |
252 | + `themes` is a collections of themes that can be applied to the banner and preferences dialog.
253 |
254 | + `initialTheme` is the initial theme that you want to applied before you call `applyTheme(themeName: string)`.
255 |
256 | + `stylesNonce` is the `nonce` attribute for `