├── .babelrc
├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── assets
├── 1-preview.png
├── 2-reader.png
├── 3-metadata.png
├── 4-alt-nocontent.png
├── Arial.js
├── BaseballCards.pbix
├── icon.png
├── icon.svg
└── thumbnail.png
├── bin
├── buildOSSReport.js
├── installPrivateSubmodules.js
├── openCert.js
├── packageVisual.js
├── pbiPluginLoader.js
└── startDev.js
├── capabilities.json
├── certs
├── PowerBICustomVisualTest_private.key
└── PowerBICustomVisualTest_public.crt
├── karma.conf.js
├── lib
└── @uncharted
│ └── cards
│ ├── example
│ ├── index.html
│ ├── sampledata.js
│ └── style.css
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── _variables.scss
│ │ ├── card
│ │ │ ├── _card.scss
│ │ │ ├── card.handlebars
│ │ │ └── card.js
│ │ ├── cards
│ │ │ ├── _cards.scss
│ │ │ ├── cards.handlebars
│ │ │ └── cards.js
│ │ ├── constants.js
│ │ ├── headerImage
│ │ │ ├── _headerImage.scss
│ │ │ └── headerImage.js
│ │ ├── inlineCardsView
│ │ │ ├── _inlineCardsView.scss
│ │ │ ├── inlineCardsView.handlebars
│ │ │ └── inlineCardsView.js
│ │ ├── readerContent
│ │ │ ├── _readerContent.scss
│ │ │ ├── readerContent.handlebars
│ │ │ └── readerContent.js
│ │ ├── verticalReader
│ │ │ ├── _verticalReader.scss
│ │ │ ├── verticalReader.handlebars
│ │ │ └── verticalReader.js
│ │ └── wrappedCardsView
│ │ │ ├── _wrappedCardsView.scss
│ │ │ ├── wrappedCardsView.handlebars
│ │ │ └── wrappedCardsView.js
│ ├── handlebarHelper
│ │ ├── cardImages.js
│ │ ├── isImageDataUrl.js
│ │ └── linkify.js
│ ├── index.js
│ ├── main.scss
│ ├── style
│ │ └── _base.scss
│ └── util
│ │ ├── IBindable.js
│ │ ├── images.js
│ │ ├── index.js
│ │ └── scroll.js
│ └── webpack.config.js
├── package.json
├── pbiviz.json
├── src
├── VisualMain.spec.ts
├── VisualMain.ts
├── constants.ts
├── dataConversion.ts
├── loader.handlebars
├── sandboxPolyfill.js
├── test_data
│ ├── colors.js
│ ├── mockdataview.ts
│ ├── table.js
│ ├── testDataUtils.ts
│ └── testHtmlStrings.js
├── types.ts
├── utils.spec.ts
├── utils.ts
└── visual.handlebars
├── style
└── visual.scss
├── tsconfig.json
├── tslint.json
├── types
└── PowerBI-Visuals-2.6.0.d.ts
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [ "env", {
4 | "targets": {
5 | "browsers": ["ie 11"]
6 | },
7 | "modules": false
8 | }]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ develop ]
9 | pull_request:
10 | branches: [ develop ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [10.x, 12.x, 14.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - name: Install Deps
28 | run: yarn
29 | - name: Lint
30 | run: yarn run lint
31 | - name: Build
32 | run: NODE_ENV=production yarn run package
33 | - name: Test
34 | run: yarn test
35 | - name: Archive pbiviz
36 | uses: actions/upload-artifact@v2
37 | with:
38 | name: pbiviz
39 | path: dist/essex.cardbrowser.pbiviz
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .npmrc
3 | .tmp
4 | node_modules
5 | typings
6 | dist
7 | coverage
8 | package-lock.json
9 |
10 | # Logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Runtime data
18 | pids
19 | *.pid
20 | *.seed
21 | *.pid.lock
22 |
23 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation. All rights reserved.
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://github.com/microsoft/PowerBI-visuals-CardBrowser/actions)
3 |
4 | # Card Browser
5 | Browse documents using double-sided cards, and click to view in place.
6 |
7 | Card Browser is a document set viewer featuring flippable, double-sided cards for natural navigation of media collections.
8 |
9 | The Preview face of each card renders the headline image, title, and origin of the story with a text sample, enabling rapid discovery of documents of interest. Flipping the cards reveals the MetaData face, which lists document properties. Clicking on a card expands it in place for detailed reading.
10 |
11 | 
12 |
13 | 
14 | # Contributing
15 |
16 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
17 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
18 | the rights to use your contribution. For details, visit https://cla.microsoft.com.
19 |
20 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
21 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
22 | provided by the bot. You will only need to do this once across all repos using our CLA.
23 |
24 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
25 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
26 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
27 |
28 | ## Debugging
29 |
30 | * Install ssl certificate by running `yarn run install-certificate` and following the steps from: [https://github.com/Microsoft/PowerBI-visuals/blob/master/tools/CertificateSetup.md](https://github.com/Microsoft/PowerBI-visuals/blob/master/tools/CertificateSetup.md)
31 | * Enable Developer Tools in PowerBI: [https://github.com/Microsoft/PowerBI-visuals/blob/master/tools/DebugVisualSetup.md](https://github.com/Microsoft/PowerBI-visuals/blob/master/tools/DebugVisualSetup.md)
32 | * Run `yarn start` to start development.
33 |
34 | ## Building
35 |
36 | * Run `yarn run package` to package the visual.
37 | * `.pbiviz` file will be generated in the `dist` folder
38 |
39 | ## Testing
40 |
41 | * Run `yarn test`
42 |
--------------------------------------------------------------------------------
/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://aka.ms/opensource/security/definition), 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://aka.ms/opensource/security/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://aka.ms/opensource/security/pgpkey).
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://aka.ms/opensource/security/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://aka.ms/opensource/security/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://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/assets/1-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/1-preview.png
--------------------------------------------------------------------------------
/assets/2-reader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/2-reader.png
--------------------------------------------------------------------------------
/assets/3-metadata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/3-metadata.png
--------------------------------------------------------------------------------
/assets/4-alt-nocontent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/4-alt-nocontent.png
--------------------------------------------------------------------------------
/assets/BaseballCards.pbix:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/BaseballCards.pbix
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
--------------------------------------------------------------------------------
/assets/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/PowerBI-visuals-CardBrowser/b58fc4dfc254c86350a4fdf99253c913ebb40bbe/assets/thumbnail.png
--------------------------------------------------------------------------------
/bin/buildOSSReport.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Uncharted Software Inc.
3 | * http://www.uncharted.software/
4 | */
5 |
6 | const path = require('path');
7 | const cp = require('child_process');
8 | const fs = require('fs');
9 |
10 | /**
11 | * Object containing the modules that should always be considered as runtime modules.
12 | *
13 | * @type {Object}
14 | */
15 | const alwaysRuntimeDependencies = {
16 | 'font-awesome': true
17 | };
18 |
19 | /**
20 | * Parses the output of `npm ll` making sure that the private sub-modules are included in the report.
21 | *
22 | * @method parseNPMLL
23 | * @param {Function} cb - Callback to be invoked when the operation is complete.
24 | */
25 | function parseNPMLL(cb) {
26 | /* read the original package.json file and parse it */
27 | const packagePath = path.resolve('./package.json');
28 | const packageJsonContent = fs.readFileSync(packagePath, 'utf8');
29 | const packageJson = JSON.parse(packageJsonContent);
30 |
31 | /* save a version of package.json with the privateSubmodules added to the dependencies object */
32 | const privateSubmodules = packageJson.privateSubmodules;
33 | const dependencies = packageJson.dependencies;
34 | Object.keys(privateSubmodules || {}).forEach(key => {
35 | if (!dependencies.hasOwnProperty(key)) {
36 | dependencies[key] = privateSubmodules[key];
37 | }
38 | });
39 | fs.writeFile(packagePath, JSON.stringify(packageJson), err => {
40 | if (err) throw(err);
41 |
42 | /* process the output of `npm ll` */
43 | let output;
44 | try {
45 | output = cp.execSync('npm ll --json', {env: process.env});
46 | } catch (e) {
47 | output = e.stdout;
48 | }
49 |
50 | /* restore the previous package.json */
51 | fs.writeFile(packagePath, packageJsonContent, err => {
52 | if (err) throw(err);
53 |
54 | /* parse the output and call the callback */
55 | let parsed;
56 | try {
57 | parsed = JSON.parse(output);
58 | } catch (e) {
59 | console.warn('Cannot parse `npm ll` output:', e);
60 | parsed = null;
61 | }
62 | cb(parsed);
63 | });
64 | });
65 | }
66 |
67 | /**
68 | * Converts an object representing a JSON property to a CSV compatible string.
69 | *
70 | * @method readObjectValue
71 | * @param {Object} object - The object to read as a string.
72 | * @returns {String}
73 | */
74 | function readObjectValue(object) {
75 | let result = '';
76 |
77 | if (typeof object === 'string' || object instanceof String) {
78 | result = object;
79 | } else if (typeof object === 'number' || object instanceof Number) {
80 | result = object.toString();
81 | } else if (object) {
82 | Object.keys(object).forEach(key => {
83 | result += key + ': ' + object[key] + '\t';
84 | });
85 | }
86 |
87 | if (!result.length) {
88 | result = 'Not Specified';
89 | }
90 |
91 | return result.replace(new RegExp('"', 'g'), '""');
92 | }
93 |
94 | /**
95 | * Recursively parses the dependencies object withing the given info object and returns them as a CSV string.
96 | *
97 | * @method parseDependencies
98 | * @param {Object} info - An info object containing the `dependencies` property to parse.
99 | * @param {Object} runtimeDependencies - An Object containing the runtime dependencies of this project.
100 | * @param {Boolean} isParentRoot - Flag defining if the parent object of this dependencies is the root of the project.
101 | * @returns {String}
102 | */
103 | function parseDependencies(info, runtimeDependencies, isParentRoot) {
104 | const dependencies = info.dependencies;
105 | const devDependencies = info.devDependencies;
106 | const nodeModulesPath = path.join(path.resolve('./'), 'node_modules/');
107 | let csv = '';
108 |
109 | /* dependency */ /* version */ /* type */ /* usage */ /* included in product */ /* in git repository */ /* licence */ /* URL */ /* description */
110 | Object.keys(dependencies).forEach(key => {
111 | const dependency = dependencies[key];
112 | const name = readObjectValue(dependency.name);
113 | const version = readObjectValue(dependency.version);
114 |
115 | if (dependency.name && dependency.version) {
116 | const type = isParentRoot ? 'dependency' : 'sub-dependency';
117 | const usage = devDependencies.hasOwnProperty(key) ? 'development' : 'runtime';
118 | const included = (!runtimeDependencies.hasOwnProperty(key) || !runtimeDependencies[key]) ? 'No' : 'Yes';
119 | /* TODO: This could be done using `git check-ignore` for better results */
120 | const inRepo = (dependency.link && !dependency.link.startsWith(nodeModulesPath)) ? 'Yes' : 'No';
121 | const license = readObjectValue(dependency.license);
122 | const url = readObjectValue(dependency.homepage || dependency.repository || null);
123 | const description = readObjectValue(dependency.description);
124 | csv += '"' + name + '","' + version + '","' + type + '","' + usage + '","' + included + '","' + inRepo + '","' + license + '","' + url + '","' + description + '"\n';
125 | }
126 |
127 | if (dependency.dependencies) {
128 | csv += parseDependencies(dependency, runtimeDependencies, false);
129 | }
130 | });
131 |
132 | return csv;
133 | }
134 |
135 | /**
136 | * Finds the runtime dependencies in the given webpack modules and adds the dependencies defined in the
137 | * `alwaysRuntimeDependencies` object.
138 | *
139 | * @method findRuntimeDependencies
140 | * @param {Object} webpackModules - An object containing the dependencies as obtaining from compiling the project using webpack.
141 | * @returns {Object}
142 | */
143 | function findRuntimeDependencies(webpackModules) {
144 | const runtimeDependencies = {};
145 | webpackModules.forEach(module => {
146 | const name = module.name;
147 | if (name.startsWith('./~/') || name.startsWith('./node_modules/') || name.startsWith('./lib/')) {
148 | const components = name.split('/');
149 | let i = 2;
150 | let moduleName = '';
151 | let moduleComponent = components[i++];
152 | while (moduleComponent.startsWith('@')) {
153 | moduleName += moduleComponent + '/';
154 | moduleComponent = components[i++];
155 | }
156 | moduleName += moduleComponent;
157 | runtimeDependencies[moduleName] = module.built && module.cacheable;
158 | }
159 | });
160 | return Object.assign({}, runtimeDependencies, alwaysRuntimeDependencies);
161 | }
162 |
163 | /**
164 | * Builds an OSS report for this project taking into account the specified webpack dependencies.
165 | *
166 | * @method buildOSSReport
167 | * @param {Object} webpackModules - An object containing the dependencies as obtaining from compiling the project using webpack.
168 | * @param {Function} cb - A callback function to be invoked qhen the process is complete.
169 | */
170 | function buildOSSReport(webpackModules, cb) {
171 | parseNPMLL(dependencies => {
172 | const runtimeDependencies = findRuntimeDependencies(webpackModules);
173 | const dependenciesCSV = parseDependencies(dependencies, runtimeDependencies, true);
174 | cb('"Dependency","Version","Type","Usage","Included In Product","In Git Repository","License","URL","Description"\n' + dependenciesCSV);
175 | });
176 | }
177 |
178 | module.exports = buildOSSReport;
179 |
--------------------------------------------------------------------------------
/bin/installPrivateSubmodules.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const { execSync } = require('child_process');
3 |
4 | const subModules = require("../package.json").privateSubmodules;
5 |
6 | Object.entries(subModules).forEach(entry => {
7 | const [name, version] = entry;
8 | console.log(`Installing ${name}@${version}...`);
9 | try {
10 | execSync(`yarn add ${name}@${version} --no-lockfile --ignore-scripts`);
11 | fs.ensureDirSync(`lib/${name}`);
12 | fs.moveSync(`node_modules/${name}`, `lib/${name}`, { overwrite: true });
13 | execSync(`yarn remove ${name}`);
14 | } catch (e) {
15 | console.log(e.message);
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/bin/openCert.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Uncharted Software Inc.
3 | * http://www.uncharted.software/
4 | */
5 |
6 | const os = require('os');
7 | const path = require('path');
8 | const exec = require('child_process').execSync;
9 |
10 | function openCertFile() {
11 | const certPath = path.join(process.cwd(), 'certs/PowerBICustomVisualTest_public.crt');
12 | const openCmds = {
13 | linux: 'xdg-open',
14 | darwin: 'open',
15 | win32: 'powershell start'
16 | };
17 | const startCmd = openCmds[os.platform()];
18 | if (startCmd) {
19 | try {
20 | exec(`${startCmd} "${certPath}"`);
21 | } catch (e) {
22 | console.info('Certificate path:', certPath);
23 | }
24 | } else {
25 | console.info('Certificate path:', certPath);
26 | }
27 | }
28 |
29 | openCertFile();
--------------------------------------------------------------------------------
/bin/packageVisual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Uncharted Software Inc.
3 | * http://www.uncharted.software/
4 | */
5 |
6 | "use strict";
7 |
8 | const fs = require('fs');
9 | const zip = require('node-zip')();
10 | const path = require('path');
11 | const sass = require('node-sass');
12 | const CleanCSS = require('clean-css');
13 | const mkdirp = require('mkdirp');
14 | const webpack = require("webpack");
15 | const MemoryFS = require("memory-fs");
16 | const pbivizJson = require('../pbiviz.json');
17 | const packageJson = require('../package.json');
18 | const capabilities = require(path.join('..', pbivizJson.capabilities));
19 | const webpackConfig = require('../webpack.config');
20 | const buildOSSReport = require('./buildOSSReport.js');
21 |
22 | const packagingWebpackConfig = {
23 | output: {
24 | filename: 'visual.js', path: '/'
25 | },
26 | };
27 |
28 | const _buildLegacyPackageJson = () => {
29 | const pack = {
30 | version: packageJson.version,
31 | author: pbivizJson.author,
32 | licenseTerms: packageJson.license,
33 | privacyTerms: packageJson.privacyTerms,
34 | resources: [
35 | {
36 | "resourceId": "rId0",
37 | "sourceType": 5,
38 | "file": `resources/${ pbivizJson.visual.guid }.ts`
39 | },
40 | {
41 | "resourceId": "rId1",
42 | "sourceType": 0,
43 | "file": `resources/${ pbivizJson.visual.guid }.js`
44 | },
45 | {
46 | "resourceId": "rId2",
47 | "sourceType": 1,
48 | "file": `resources/${ pbivizJson.visual.guid }.css`
49 | },
50 | {
51 | "resourceId": "rId3",
52 | "sourceType": 3,
53 | "file": `resources/${path.basename(pbivizJson.assets.icon)}`
54 | },
55 | {
56 | "resourceId": "rId4",
57 | "sourceType": 6,
58 | "file": `resources/${path.basename(pbivizJson.assets.thumbnail)}`
59 | },
60 | {
61 | "resourceId": "rId5",
62 | "sourceType": 2,
63 | "file": `resources/${path.basename(pbivizJson.assets.screenshot)}`
64 | }
65 | ],
66 | visual: Object.assign({ version: packageJson.version }, pbivizJson.visual),
67 | "code": {
68 | "typeScript": {
69 | "resourceId": "rId0"
70 | },
71 | "javaScript": {
72 | "resourceId": "rId1"
73 | },
74 | "css": {
75 | "resourceId": "rId2"
76 | }
77 | },
78 | "images": {
79 | "icon": {
80 | "resourceId": "rId3"
81 | },
82 | "thumbnail": {
83 | "resourceId": "rId4"
84 | },
85 | "screenshots": [
86 | {
87 | "resourceId": "rId5"
88 | }
89 | ]
90 | }
91 | };
92 |
93 | delete pack.visual.visualClassName;
94 |
95 | const date = new Date();
96 | pack.build = date.getUTCFullYear().toString().substring(2) + '.' + (date.getUTCMonth() + 1) + '.' + date.getUTCDate() + '.' + ((date.getUTCHours() * 3600) + (date.getUTCMinutes() * 60) + date.getUTCSeconds());
97 |
98 | return pack;
99 | };
100 |
101 | const _buildPackageJson = () => {
102 | return {
103 | version: packageJson.version,
104 | author: pbivizJson.author,
105 | licenseTerms: packageJson.license,
106 | privacyTerms: packageJson.privacyTerms,
107 | resources: [
108 | {
109 | resourceId: 'rId0',
110 | sourceType: 5,
111 | file: `resources/${ pbivizJson.visual.guid }.pbiviz.json`,
112 | }
113 | ],
114 | visual: Object.assign({ version: packageJson.version }, pbivizJson.visual),
115 | metadata: {
116 | pbivizjson: {
117 | resourceId: 'rId0'
118 | }
119 | }
120 | };
121 | };
122 |
123 | const buildPackageJson = pbivizJson.apiVersion ? _buildPackageJson() : _buildLegacyPackageJson();
124 |
125 | const compileSass = () => {
126 | const sassOutput = sass.renderSync({ file: pbivizJson.style }).css.toString();
127 | const options = {
128 | level: {
129 | 2: {
130 | all: true,
131 | mergeNonAdjacentRules: false,
132 | },
133 | },
134 | };
135 | const cssContent = new CleanCSS(options).minify(sassOutput).styles;
136 | return cssContent;
137 | };
138 |
139 | const compileScripts = (callback) => {
140 | const regex = new RegExp("\\bpowerbi-visuals.d.ts\\b");
141 | const fs = new MemoryFS();
142 | const compiler = webpack(Object.assign(webpackConfig, packagingWebpackConfig));
143 | compiler.outputFileSystem = fs;
144 | compiler.run((err, stats) => {
145 | if (err) throw err;
146 | const jsonStats = stats.toJson(true);
147 | const errors = jsonStats.errors.filter(error => !regex.test(error));
148 | console.info('Time:', jsonStats.time);
149 | console.info('Hash:', jsonStats.hash);
150 | jsonStats.warnings.forEach(warning => console.warn('WARNING:', warning));
151 | errors.forEach(error => !regex.test(error) && console.error('ERROR:', error));
152 | if (errors.length > 0) {
153 | return process.exit(1);
154 | }
155 | const fileContent = fs.readFileSync("/visual.js").toString();
156 | callback(err, fileContent);
157 | });
158 | };
159 |
160 | const _buildLegacyPackage = (fileContent) => {
161 | const icon = fs.readFileSync(pbivizJson.assets.icon);
162 | const thumbnail = fs.readFileSync(pbivizJson.assets.thumbnail);
163 | const screenshot = fs.readFileSync(pbivizJson.assets.screenshot);
164 | const iconType = pbivizJson.assets.icon.indexOf('.svg') >= 0 ? 'svg+xml' : 'png';
165 | const iconBase64 = `data:image/${iconType};base64,` + icon.toString('base64');
166 | const cssContent = compileSass() + `\n.visual-icon.${pbivizJson.visual.guid} {background-image: url(${iconBase64});}`;
167 | zip.file('package.json', JSON.stringify(buildPackageJson, null, 2));
168 | zip.file(`resources/${pbivizJson.visual.guid}.js`, fileContent);
169 | zip.file(`resources/${pbivizJson.visual.guid}.ts`, `/** See ${pbivizJson.visual.guid}.js **/`);
170 | zip.file(`resources/${pbivizJson.visual.guid}.css`, cssContent + `\n`);
171 | zip.file(`resources/${path.basename(pbivizJson.assets.icon)}`, icon);
172 | zip.file(`resources/${path.basename(pbivizJson.assets.thumbnail)}`, thumbnail);
173 | zip.file(`resources/${path.basename(pbivizJson.assets.screenshot)}`, screenshot);
174 | fs.writeFileSync(pbivizJson.output, zip.generate({ base64:false,compression:'DEFLATE' }), 'binary');
175 | };
176 |
177 | const _buildPackage = (fileContent) => {
178 | const jsContent = 'var window = window.document.defaultView;\nvar $ = window.$;\n var _ = window._;\n' + fileContent;
179 | const cssContent = compileSass();
180 | const icon = fs.readFileSync(pbivizJson.assets.icon);
181 | const iconType = pbivizJson.assets.icon.indexOf('.svg') >= 0 ? 'svg+xml' : 'png';
182 | const iconBase64 = `data:image/${iconType};base64,` + icon.toString('base64');
183 |
184 | pbivizJson.capabilities = capabilities;
185 | pbivizJson.content = {
186 | js: jsContent,
187 | css: cssContent,
188 | iconBase64: iconBase64
189 | };
190 | pbivizJson.visual.version = packageJson.version;
191 |
192 | zip.file('package.json', JSON.stringify(buildPackageJson, null, 2));
193 | zip.file(`resources/${pbivizJson.visual.guid}.pbiviz.json`, JSON.stringify(pbivizJson, null, 2));
194 | fs.writeFileSync(pbivizJson.output, zip.generate({ base64:false,compression:'DEFLATE' }), 'binary');
195 | };
196 |
197 | const buildPackage = () => {
198 | mkdirp.sync(path.parse(pbivizJson.output).dir);
199 | compileScripts((err, result) => {
200 | if (err) throw err;
201 |
202 | if (!pbivizJson.apiVersion) {
203 | _buildLegacyPackage(result);
204 | } else {
205 | _buildPackage(result);
206 | }
207 | });
208 | };
209 |
210 | buildPackage();
211 |
--------------------------------------------------------------------------------
/bin/pbiPluginLoader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Uncharted Software Inc.
3 | * http://www.uncharted.software/
4 | */
5 |
6 | 'use strict';
7 |
8 | const path = require('path');
9 | const cp = require('child_process');
10 | const crypto = require('crypto');
11 | const pbiviz = require(path.join(process.cwd(), 'pbiviz.json'));
12 | const packageJson = require(path.join(process.cwd(), 'package.json'));
13 |
14 | const userName = cp.execSync('whoami').toString();
15 | const userHash = crypto.createHash('md5').update(userName).digest('hex');
16 |
17 | function pbivizPluginTemplate(pbiviz) {
18 | return `(function (powerbi) {
19 | var visuals;
20 | (function (visuals) {
21 | var plugins;
22 | (function (plugins) {
23 | /* ESSEX Capabilities Patcher */
24 | plugins['${pbiviz.visual.guid}'] = {
25 | name: '${pbiviz.visual.guid}',
26 | displayName: '${pbiviz.visual.name}',
27 | class: '${pbiviz.visual.visualClassName}',
28 | version: '${packageJson.version}',
29 | apiVersion: '${pbiviz.apiVersion}',
30 | capabilities: '{}',
31 | create: function (/*options*/) {
32 | var instance = Object.create(${pbiviz.visual.visualClassName}.prototype);
33 | ${pbiviz.visual.visualClassName}.apply(instance, arguments);
34 | return instance;
35 | },
36 | custom: true
37 | };
38 |
39 | /* save version number to visual */
40 | ${pbiviz.visual.visualClassName}.__essex_build_info__ = '${packageJson.version} ${Date.now()} [${userHash}]';
41 | Object.defineProperty(${pbiviz.visual.visualClassName}.prototype, '__essex_build_info__', { get: function() { return ${pbiviz.visual.visualClassName}.__essex_build_info__; } } );
42 | })(plugins = visuals.plugins || (visuals.plugins = {}));
43 | })(visuals = powerbi.visuals || (powerbi.visuals = {}));
44 | })(window['powerbi'] || (window['powerbi'] = {}));`;
45 | }
46 |
47 | /**
48 | * Webpack loader function that appends pbiviz plugin code at the end of the provided source
49 | */
50 | function pluginLoader(source, map) {
51 | this.cacheable();
52 | source = source + '\n' + pbivizPluginTemplate(pbiviz);
53 | this.callback(null, source, map);
54 | }
55 |
56 | module.exports = pluginLoader;
57 |
--------------------------------------------------------------------------------
/bin/startDev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2018 Uncharted Software Inc.
3 | * http://www.uncharted.software/
4 | */
5 |
6 | const fs = require('fs');
7 | const path = require('path');
8 | const https = require('https');
9 | const sass = require('node-sass');
10 | const mkdirp = require('mkdirp');
11 | const connect = require('connect');
12 | const webpack = require('webpack');
13 | const chokidar = require('chokidar');
14 | const serveStatic = require('serve-static');
15 | const webpackConfig = require('../webpack.config');
16 | const pbivizJson = require('../pbiviz.json');
17 |
18 | const config = {
19 | pbivizJsonPath: 'pbiviz.json',
20 | capabilitiesJsonPath: pbivizJson.capabilities,
21 | tmpDropDir: '.tmp/drop',
22 | sassEntry: pbivizJson.style,
23 | sassPaths: [
24 | 'lib/@uncharted/strippets/sass',
25 | ],
26 | server: {
27 | cert: 'certs/PowerBICustomVisualTest_public.crt',
28 | key: 'certs/PowerBICustomVisualTest_private.key',
29 | port: 8080
30 | }
31 | };
32 | const pbiResource = {
33 | jsFile: `${ config.tmpDropDir }/visual.js`,
34 | cssFile: `${ config.tmpDropDir }/visual.css`,
35 | pbivizJsonFile: `${ config.tmpDropDir }/pbiviz.json`,
36 | statusFile: `${ config.tmpDropDir }/status`,
37 | };
38 |
39 | const compileSass = () => {
40 | console.info('Building css...');
41 | const cssContent = sass.renderSync({ file: config.sassEntry, includePaths: config.sassPaths }).css;
42 | fs.writeFileSync(pbiResource.cssFile, cssContent);
43 | };
44 |
45 | const emitPbivizJson = () => {
46 | console.info('Updating pbiviz.json...');
47 | const pbiviz = JSON.parse(fs.readFileSync(config.pbivizJsonPath));
48 | const capabilities = JSON.parse(fs.readFileSync(config.capabilitiesJsonPath))
49 | pbiviz.capabilities = capabilities;
50 | fs.writeFileSync(pbiResource.pbivizJsonFile, JSON.stringify(pbiviz, null, 2));
51 | };
52 |
53 | const updateStatus = () => {
54 | fs.writeFileSync(pbiResource.statusFile, Date.now().toString());
55 | console.info('Visual updated.');
56 | };
57 |
58 | const runWatchTask = (task, isSass) => {
59 | try {
60 | task();
61 | } catch (e) {
62 | isSass
63 | ? console.info(`ERROR: ${e.message}\n at ${e.file}:${e.line}:${e.column}`)
64 | : console.info(`ERROR: ${e.message}`);
65 | }
66 | };
67 |
68 | const startWatchers = () => {
69 | // watch script change and re-compile
70 | const compiler = webpack(Object.assign({ output: { filename: pbiResource.jsFile }}, webpackConfig));
71 | compiler.watch({}, (err, stats) => {
72 | let log = stats.toString({
73 | chunks: false,
74 | color: true
75 | });
76 | log = log.split('\n\n').filter(msg => msg.indexOf('node_module') === -1 ).join('\n\n');
77 | console.info(log);
78 | });
79 |
80 | // watch for pbiviz.json or capabilities.json change
81 | chokidar
82 | .watch([config.pbivizJsonPath, config.capabilitiesJsonPath])
83 | .on('change', path => runWatchTask(emitPbivizJson));
84 |
85 | // watch for sass file changes
86 | chokidar
87 | .watch(['**/*.scss', '**/*.sass'])
88 | .on('change', path => runWatchTask(compileSass, true));
89 |
90 | // watch pbi resource change and update status to trigger debug visual update
91 | chokidar
92 | .watch([pbiResource.jsFile, pbiResource.cssFile, pbiResource.pbivizJsonFile])
93 | .on('change', path => runWatchTask(updateStatus));
94 | };
95 |
96 | const startServer = () => {
97 | const options = {
98 | key: fs.readFileSync(config.server.key),
99 | cert: fs.readFileSync(config.server.cert)
100 | };
101 | const app = connect();
102 | app.use((req, res, next) => {
103 | res.setHeader('Access-Control-Allow-Origin', '*');
104 | next();
105 | });
106 | app.use('/assets', serveStatic(config.tmpDropDir));
107 |
108 | https.createServer(options, app).listen(config.server.port, (err) => {
109 | console.info('Server listening on port ', config.server.port + '.');
110 | });
111 | };
112 |
113 | const start = () => {
114 | mkdirp.sync(config.tmpDropDir);
115 | compileSass();
116 | emitPbivizJson();
117 | updateStatus();
118 | startWatchers();
119 | startServer();
120 | };
121 |
122 | start();
123 |
--------------------------------------------------------------------------------
/capabilities.json:
--------------------------------------------------------------------------------
1 | {
2 | "dataRoles": [
3 | {
4 | "name": "id",
5 | "kind": 0,
6 | "displayName": "Document Id",
7 | "description": "Unique ID for the document"
8 | },
9 | {
10 | "name": "title",
11 | "kind": 0,
12 | "displayName": "Title",
13 | "description": "Title of the document; shown on cards and in the reader"
14 | },
15 | {
16 | "name": "summary",
17 | "displayName": "Preview",
18 | "description": "Preview text; only displayed on cards",
19 | "kind": 1
20 | },
21 | {
22 | "name": "content",
23 | "displayName": "Content",
24 | "description": "Full content to display in the reader",
25 | "kind": 1
26 | },
27 | {
28 | "name": "imageUrl",
29 | "displayName": "Title Image (URL)",
30 | "description": "Main image(s) for the document; displayed on cards and in the reader",
31 | "kind": 0
32 | },
33 | {
34 | "name": "subtitle",
35 | "displayName": "Subtitle Fields",
36 | "description": "Text items to display as a subtitle",
37 | "kind": 0
38 | },
39 | {
40 | "name": "sourceImage",
41 | "displayName": "Badge (Image URL)",
42 | "description": "URL for an image to display in the top-right corner of the card",
43 | "kind": 0
44 | },
45 | {
46 | "name": "metadata",
47 | "displayName": "MetaData Fields",
48 | "description": "Key-value pairs for the table on the back of the cards",
49 | "kind": 1
50 | },
51 | {
52 | "name": "topBarColor",
53 | "displayName": "Top Bar Color",
54 | "description": "Color of the bar at the top of the card",
55 | "kind": 0
56 | },
57 | {
58 | "name": "dummySortingField",
59 | "displayName": "Sorting Field",
60 | "description": "Field that can be used to sort the visual's data based on an arbitrary column",
61 | "kind": 2
62 | }
63 | ],
64 | "dataViewMappings": [
65 | {
66 | "conditions": [
67 | {
68 | "id": {
69 | "max": 1
70 | },
71 | "title": {
72 | "max": 10
73 | },
74 | "summary": {
75 | "max": 1
76 | },
77 | "content": {
78 | "max": 1
79 | },
80 | "imageUrl": {
81 | "max": 4
82 | },
83 | "subtitle": {
84 | "max": 6
85 | },
86 | "sourceImage": {
87 | "max": 1
88 | },
89 | "metadata": {
90 | "max": 10
91 | },
92 | "topBarColor": {
93 | "max": 1
94 | },
95 | "dummySortingField": {
96 | "max": 1
97 | }
98 | }
99 | ],
100 | "categorical": {
101 | "categories": {
102 | "for": {
103 | "in": "id"
104 | },
105 | "select": [
106 | {
107 | "bind": {
108 | "to": "title"
109 | }
110 | },
111 | {
112 | "bind": {
113 | "to": "summary"
114 | }
115 | },
116 | {
117 | "bind": {
118 | "to": "content"
119 | }
120 | },
121 | {
122 | "bind": {
123 | "to": "imageUrl"
124 | }
125 | },
126 | {
127 | "bind": {
128 | "to": "subtitle"
129 | }
130 | },
131 | {
132 | "bind": {
133 | "to": "sourceImage"
134 | }
135 | },
136 | {
137 | "bind": {
138 | "to": "metadata"
139 | }
140 | },
141 | {
142 | "bind": {
143 | "to": "topBarColor"
144 | }
145 | }
146 | ],
147 | "dataReductionAlgorithm": {
148 | "window": {
149 | "count": 500
150 | }
151 | }
152 | },
153 | "values": {
154 | "select": [
155 | {
156 | "for": {
157 | "in": "id"
158 | }
159 | }
160 | ],
161 | "dataReductionAlgorithm": { "window": { "count": 20000 } }
162 | },
163 | "rowCount": {
164 | "preferred": {
165 | "min": 500,
166 | "max": 6000
167 | },
168 | "supported": {
169 | "min": 0,
170 | "max": 20000
171 | }
172 | }
173 | }
174 | }
175 | ],
176 | "objects": {
177 | "general": {
178 | "displayName": "General",
179 | "properties": {
180 | "version": {
181 | "displayName": "Version",
182 | "description": "The version of Card Browser",
183 | "type": {
184 | "text": true
185 | }
186 | }
187 | }
188 | },
189 | "presentation": {
190 | "properties": {
191 | "dateFormat": {
192 | "displayName": "Date Format",
193 | "description": "Subtitle date format using tokens; e.g. YYYY MM DD h:mm:ss",
194 | "type": {
195 | "text": "MMM D, YYYY"
196 | }
197 | },
198 | "separator": {
199 | "displayName": "Separator",
200 | "description": "Character to place between subtitle fields",
201 | "type": {
202 | "text": " \u2022 "
203 | }
204 | },
205 | "cardWidth": {
206 | "displayName": "Card Width",
207 | "description": "Width, in pixels, of each card",
208 | "type": {
209 | "numeric": 200
210 | }
211 | },
212 | "cardHeight": {
213 | "displayName": "Card Height",
214 | "description": "Height, in pixels, of each card when using wrapped layout",
215 | "type": {
216 | "numeric": 300
217 | }
218 | },
219 | "filter": {
220 | "type": {
221 | "bool": true
222 | },
223 | "displayName": "Filter",
224 | "description": "If on, selecting a card filters other visuals to that article."
225 | },
226 | "cropImages": {
227 | "displayName": "Crop Images",
228 | "description": "If on, crop and recenter images to fill the top of the card",
229 | "type": {
230 | "bool": true
231 | }
232 | },
233 | "shadow": {
234 | "displayName": "Shadow",
235 | "description": "Control the style of border around cards.",
236 | "type": {
237 | "bool": "true"
238 | }
239 | },
240 | "fade": {
241 | "displayName": "Fade Text",
242 | "description": "Fade text toward the bottom of the card",
243 | "type": {
244 | "bool": "true"
245 | }
246 | },
247 | "showImageOnBack": {
248 | "description": "Whether to show Title Images on the card's MetaData face",
249 | "displayName": "Title Image on MetaData",
250 | "type": {
251 | "bool": "true"
252 | }
253 | }
254 | },
255 | "displayName": "Cards"
256 | },
257 | "reader": {
258 | "properties": {
259 | "headerBackgroundColor": {
260 | "description": "Background color for the reader header",
261 | "displayName": "Header Color",
262 | "type": {
263 | "fill": {
264 | "solid": {
265 | "color": "#000"
266 | }
267 | }
268 | }
269 | },
270 | "width": {
271 | "description": "Expanded width of the inline reader",
272 | "displayName": "Expanded Width",
273 | "type": {
274 | "numeric": 520
275 | }
276 | },
277 | "height": {
278 | "description": "Height of the vertical (wrapped) reader",
279 | "displayName": "Height",
280 | "type": {
281 | "numeric": 500
282 | }
283 | }
284 | },
285 | "displayName": "Reader"
286 | },
287 | "metadata": {
288 | "properties": {
289 | "fontSize": {
290 | "description": "MetaData text size",
291 | "displayName": "Text Size",
292 | "type": {
293 | "formatting": {
294 | "fontSize": true
295 | }
296 | }
297 | },
298 | "titleFontFamily": {
299 | "displayName": "Title font family",
300 | "type": {
301 | "formatting": {
302 | "fontFamily": true
303 | }
304 | }
305 | },
306 | "valueFontFamily": {
307 | "displayName": "Value font family",
308 | "type": {
309 | "formatting": {
310 | "fontFamily": true
311 | }
312 | }
313 | },
314 | "titleColor": {
315 | "description": "Color for the row titles",
316 | "displayName": "Titles Color",
317 | "type": {
318 | "fill": {
319 | "solid": {
320 | "color": "#bbb"
321 | }
322 | }
323 | }
324 | },
325 | "valueColor": {
326 | "description": "Color for the MetaData values",
327 | "displayName": "Values Color",
328 | "type": {
329 | "fill": {
330 | "solid": {
331 | "color": "#000"
332 | }
333 | }
334 | }
335 | }
336 | },
337 | "displayName": "MetaData Format"
338 | },
339 | "flipState": {
340 | "properties": {
341 | "show": {
342 | "description": "Whether to enable card flipping",
343 | "displayName": "Enable Flip",
344 | "type": {
345 | "bool": "true"
346 | }
347 | },
348 | "cardFaceDefault": {
349 | "description": "Which face of the card to show by default",
350 | "displayName": "Card Face",
351 | "type": {
352 | "enumeration": [
353 | { "value": "preview", "displayName": "Preview" },
354 | { "value": "metadata", "displayName": "MetaData" }
355 | ]
356 | }
357 | }
358 | },
359 | "description": "Cards flipping configuration",
360 | "displayName": "Flip"
361 | },
362 | "loadMoreData": {
363 | "properties": {
364 | "show": {
365 | "description": "Whether to load more data than the initial rows",
366 | "displayName": "Enable",
367 | "type": {
368 | "bool": "false"
369 | }
370 | },
371 | "limit": {
372 | "description": "Stop after loading this many Documents",
373 | "displayName": "Limit",
374 | "type": {
375 | "numeric": 500
376 | }
377 | }
378 | },
379 | "description": "Control over data loading",
380 | "displayName": "Load More Data"
381 | }
382 | },
383 | "sorting": {
384 | "default": {}
385 | },
386 | "suppressDefaultTitle": true,
387 | "supportsHighlight": false
388 | }
389 |
--------------------------------------------------------------------------------
/certs/PowerBICustomVisualTest_private.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDxYnqKr9KKwxbP
3 | p/H156sO89OBN88tqbSShKWKrk5ktAja1xAv+6/xnWvoKNJoGLS3V5Z/rlVa/m2Y
4 | rBcCaSQZvcWp28+xYcqrh+SabeQhyDGgjTE1nMmAOTBXLPsy9O5p2/pRn9+j+noi
5 | CL3R8j99Q1/uZDArDBWit00SzYJGuFdUb0DVoWcWb+btAtpJe4Q/SepZKfMVDNOs
6 | LNeM6AQzWDFp5FHFBIOYHHijNwhR4LyROc2TT6oR2DzQgByOk/Ot/zwEn8Vn5528
7 | 57EKoC4RBWws0vCcTIvCVPuWXGwr524dyCOCEiwiK34la45Hs4UMOInbJKSB7Keh
8 | cImp5qE1AgMBAAECggEASp/8bIXg3F9l3Pr59eESZEQDPm6nkWFm3uk9WHt85us1
9 | 1Zopefwgr9zQMGz2Z5JDxG7Cq2SVJNWFwm5piqhAreiahGHdeuRVyOPxS8Dvh3Yh
10 | yZX9AasgLJEBneHdIyrPzlBguf1oIYTdX0o+jPqMoEhFDylQLu7EfZKXtFlujjfz
11 | qoP6FjdIW00G0QA+xmFmEL4TfqBDJXwefFzQYAj22YmUYYdJ7OVNhlx7flyAyxHn
12 | ebn4MDg4mI5z75uC82ivWL8DpPvrXXnqSxioDktJk70mIHZwbEg/pgH5UQB2l/Cg
13 | REBoiW8bT9GTF6x4JSXsKwdgl9GuwhU9+/iqFtGK3QKBgQD6TR6egyyu+5u52hma
14 | zOhsYjg8Qshj1+UnToZBx5cB/sJrxnskIRQFUmEakPdOmUHV1XOMGmDJZvhSXVQJ
15 | g3AHe8yme7nlbyNcZf7gOUxum9SuVZOJgmcuiuEu5PCIStSUNYELRxlYKjL3vJwO
16 | kqTK17Ng7e5ioLB5V5q4x2nfDwKBgQD24WONyaAqAMH64wkxQGdrrt+Q8SYDW9eh
17 | V4gTFnRwXVcqhARLN4PACtopO7Rr4bOHDW32cVWQxA5LCCwhaR/4j3cmYjo1uKPP
18 | qUdC4B5XRLAKwXHoHy58AVDPEZpKmPmUD94ct8LHOIMr1nnpMKQFFNmEQ2r2t2zt
19 | HSL6UmA7ewKBgQDvxI5PM8LLAFUFbIpYgm8m29OYzjRdiEOIKq1rN8FM9PjS+vec
20 | /V7LVkWUiEeO8DpjlywvilkqtMutQp+s2U6orIu28xB5WsQZz86eheTUk8vhEDLb
21 | Z6JlsD3TiRVsyZnnO3WZEwuRLCNUs0UepJTdhlDbyjAwJFPIeQXKeaPOOwKBgQCg
22 | WQfWIazbWx6imy9vQ8toT255r4bnC5HkAvwomZ8LFDT3MkOvruDtrJ7BxTuMDk4S
23 | W5CeTkIrAoveA/LVyHexc00KKyZvmfsbd3EHaJWMTNqiQb5/6zC/7gLUWzSBWxZP
24 | Kncy48+ooXXg1S7dXHBLtJ0KoNcqYzxmTVRYjYRfXQKBgQCtWN60lpb4qf5tCtSj
25 | UIYJ4+BYZ0blW2HpCkUAqh4H860MRlkcEHAwCSKHxU9QjifV7e7cfAvX2VKOPcFQ
26 | TtVrs4ltamA6cRms/SWJQlkboXVRcPjfZt9WRYuqLoDnrODqXX4DF1fouGcuAJ1+
27 | 7N6eq1wXbSPFFsR5K19JV7di1A==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/certs/PowerBICustomVisualTest_public.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDrzCCApegAwIBAgIJAJoHcouCtktvMA0GCSqGSIb3DQEBCwUAMG4xKzApBgNV
3 | BAoMIkRPX05PVF9UUlVTVF9Qb3dlckJJVmlzdWFsVGVzdFJvb3QxKzApBgNVBAsM
4 | IkRPX05PVF9UUlVTVF9Qb3dlckJJVmlzdWFsVGVzdFJvb3QxEjAQBgNVBAMMCWxv
5 | Y2FsaG9zdDAeFw0xNjA1MDkyMzQ2MzBaFw0xODA1MDkyMzQ2MzBaMG4xKzApBgNV
6 | BAoMIkRPX05PVF9UUlVTVF9Qb3dlckJJVmlzdWFsVGVzdFJvb3QxKzApBgNVBAsM
7 | IkRPX05PVF9UUlVTVF9Qb3dlckJJVmlzdWFsVGVzdFJvb3QxEjAQBgNVBAMMCWxv
8 | Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPFieoqv0orD
9 | Fs+n8fXnqw7z04E3zy2ptJKEpYquTmS0CNrXEC/7r/Gda+go0mgYtLdXln+uVVr+
10 | bZisFwJpJBm9xanbz7FhyquH5Jpt5CHIMaCNMTWcyYA5MFcs+zL07mnb+lGf36P6
11 | eiIIvdHyP31DX+5kMCsMFaK3TRLNgka4V1RvQNWhZxZv5u0C2kl7hD9J6lkp8xUM
12 | 06ws14zoBDNYMWnkUcUEg5gceKM3CFHgvJE5zZNPqhHYPNCAHI6T863/PASfxWfn
13 | nbznsQqgLhEFbCzS8JxMi8JU+5ZcbCvnbh3II4ISLCIrfiVrjkezhQw4idskpIHs
14 | p6FwianmoTUCAwEAAaNQME4wHQYDVR0OBBYEFCWXalZgOJfwtYxsfTxZ2Ok8MFMl
15 | MB8GA1UdIwQYMBaAFCWXalZgOJfwtYxsfTxZ2Ok8MFMlMAwGA1UdEwQFMAMBAf8w
16 | DQYJKoZIhvcNAQELBQADggEBAMPC7CahK5lw5BkO991tejQMu8JElkqEOQCj6Vbc
17 | yG+1gs+55sMxLgBg4GBUYqNepK+hMGzBDawG4V3c6lxx3nR8fSdPQdqcxEfTYvgD
18 | W9eKk2QLMcg4iZzHfSAcHeUIZc6RLVlDcf3KzFIxs9wTeVThwI29erDNHwxqRBkD
19 | QKvTMYXB7SeTpIEdTELwOh/xrSe5eq6TUWgCcOMqLO0auYosqlJsivHPKZiA5Z7/
20 | NhOaDpI5BQ5gxHglX7faCXnI0mBU3yRABZZkS4roXf4FgBewwOLeT6Vjkxoy8P+W
21 | kPRQrZlZR4pcKY5E15fxenStD01d8O86wA3oJwYFwLmUthg=
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | 'use strict';
6 |
7 | const isTddMode = process.argv.indexOf("--tdd") > -1;
8 | const webpackConfig = require('./webpack.config');
9 | const webpack = require('webpack');
10 | const path = require('path');
11 |
12 | module.exports = function (config) {
13 | config.set({
14 | basePath: '',
15 | frameworks: ['mocha', 'sinon-chai', 'jquery-1.8.3'],
16 | files: [
17 | 'node_modules/babel-polyfill/dist/polyfill.min.js',
18 | 'src/**/*.spec.js',
19 | 'src/**/*.spec.ts',
20 | ],
21 | exclude: [
22 | ],
23 | preprocessors: {
24 | 'src/**/*.spec.js': ['webpack', 'sourcemap'],
25 | 'src/**/*.spec.ts': ['webpack', 'typescript'],
26 | },
27 | webpackMiddleware: {
28 | stats: 'errors-only',
29 | },
30 | webpack: {
31 | module: {
32 | rules: [
33 | {
34 | test: /\.handlebars$/,
35 | loader: 'handlebars-loader',
36 | query: {
37 | helperDirs: [
38 | path.resolve(__dirname, 'lib/@uncharted/cards/src/handlebarHelper'),
39 | ],
40 | },
41 | },
42 | {
43 | test: /\.js$/,
44 | loader: 'babel-loader',
45 | options: {
46 | presets: [
47 | ['latest', { es2015: { modules: false } }],
48 | ],
49 | },
50 | exclude: /node_modules/,
51 | },
52 | {
53 | test: /\.ts?$/,
54 | loaders: [{
55 | loader: 'babel-loader',
56 | options: {
57 | presets: [
58 | ['latest', {es2015: {modules: false}}],
59 | ],
60 | },
61 | }, 'ts-loader'],
62 | },
63 | ],
64 | },
65 | resolve: webpackConfig.resolve,
66 | externals: [
67 | {
68 | sinon: "sinon",
69 | chai: "chai",
70 | },
71 | ],
72 | plugins: [
73 | new webpack.SourceMapDevToolPlugin({
74 | filename: null, // if no value is provided the sourcemap is inlined
75 | test: /\.(js)($|\?)/i, // process .js files only
76 | }),
77 | ],
78 | },
79 | reporters: ['mocha'],
80 | port: 9876,
81 | colors: true,
82 | logLevel: config.LOG_INFO,
83 | autoWatch: true,
84 | browsers: isTddMode ? ['Chrome'] : ['PhantomJS'],
85 | singleRun: !isTddMode,
86 | concurrency: Infinity,
87 | });
88 | };
89 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cards
5 |
6 |
7 |
8 |
9 |
10 |
31 |
32 |
33 |
34 |
35 |
36 |
52 |
171 |
172 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/example/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | html, body {
18 | margin: 0;
19 | padding: 0;
20 | display: flex;
21 | position: relative;
22 | width: 100%;
23 | height: 100%;
24 | box-sizing: border-box;
25 | }
26 |
27 | .input-panel {
28 | width: 300px;
29 | height: 100%;
30 | position: relative;
31 | border-right: 1px solid #ddd;
32 | display: flex;
33 | flex-direction: column;
34 | }
35 |
36 | .buttons-box {
37 | padding: 5px;
38 | }
39 |
40 | .text-box-container {
41 | flex-grow: 1;
42 | display: flex;
43 | flex-direction: column;
44 | }
45 |
46 | .data-box, .config-box {
47 | display: flex;
48 | flex-direction: column;
49 | flex-grow: 1;
50 | padding: 5px;
51 | }
52 |
53 | .config-box {
54 | height: 300px;
55 | }
56 |
57 | textarea {
58 | width: 100%;
59 | height: 100%;
60 | resize: none;
61 | }
62 |
63 | .cards-panel {
64 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
65 | font-size: 14px;
66 | line-height: 1.42857;
67 | width: calc(100% - 300px);
68 | }
69 | ::-webkit-scrollbar {
70 | width: 8px;
71 | height: 8px;
72 | border-radius: 100px;
73 | background-color: rgba(0, 0, 0, 0);
74 | }
75 |
76 | ::-webkit-scrollbar:hover {
77 | background-color: rgba(0, 0, 0, 0.09);
78 | }
79 |
80 | ::-webkit-scrollbar-thumb {
81 | background: rgba(0, 0, 0, 0.2);
82 | border-radius: 100px;
83 | }
84 |
85 | ::-webkit-scrollbar-thumb:hover,
86 | ::-webkit-scrollbar-thumb:active
87 | {
88 | background: rgba(0, 0, 0, 0.5);
89 | }
--------------------------------------------------------------------------------
/lib/@uncharted/cards/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@uncharted/cards",
3 | "version": "0.13.9",
4 | "main": "dist/uncharted.cards.js",
5 | "scripts": {
6 | "test": "./node_modules/.bin/karma start",
7 | "build": "npm-run-all --parallel build:**",
8 | "build:dev": "webpack",
9 | "build:prod": "cross-env NODE_ENV='production' webpack -p",
10 | "start": "cross-env WEBPACK_ENV='serve' webpack-dev-server --host 0.0.0.0 --port 8090 --content-base ./example --hot --open",
11 | "clean": "rm -rf node_modules"
12 | },
13 | "publishConfig": {
14 | "registry": "https://npm.uncharted.software"
15 | },
16 | "author": "Uncharted Software Inc.",
17 | "description": "",
18 | "dependencies": {
19 | "lodash": "^4.17.11"
20 | },
21 | "devDependencies": {
22 | "babel-core": "^6.26.0",
23 | "babel-loader": "^7.1.2",
24 | "babel-polyfill": "^6.26.0",
25 | "babel-preset-latest": "^6.24.1",
26 | "chai": "^4.1.2",
27 | "clean-webpack-plugin": "^0.1.16",
28 | "cross-env": "^5.0.5",
29 | "css-loader": "^0.28.4",
30 | "eslint": "^5.15.1",
31 | "eslint-loader": "^2.1.2",
32 | "handlebars": "^4.0.11",
33 | "handlebars-loader": "^1.7.0",
34 | "karma": "^4.0.1",
35 | "karma-chrome-launcher": "^2.2.0",
36 | "karma-mocha": "^1.3.0",
37 | "karma-mocha-reporter": "^2.2.5",
38 | "karma-sinon-chai": "^1.3.3",
39 | "karma-sourcemap-loader": "^0.3.7",
40 | "karma-webpack": "^2.0.4",
41 | "mocha": "^5.0.4",
42 | "node-sass": "^4.5.3",
43 | "npm-run-all": "^4.0.2",
44 | "puppeteer": "^1.2.0",
45 | "sass-loader": "^6.0.6",
46 | "sinon": "^4.4.6",
47 | "sinon-chai": "^3.0.0",
48 | "style-loader": "^0.18.2",
49 | "webpack": "^4.29.6",
50 | "webpack-cli": "^3.2.3",
51 | "webpack-dev-server": "^3.2.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/_variables.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | // card
6 | $card-height: 300px !default;
7 | $card-border-width: 1px !default;
8 | $card-border: $card-border-width solid #F2F2F2 !default;
9 | $card-shadow: -1px 0 0 rgba(0,0,0,.12), 0 0 2px rgba(0,0,0,.12), 2px 0 4px rgba(0,0,0,.24) !default;
10 | $card-margin: 5px !default;
11 | $card-expanding-animation: 0.3s ease-out !default;
12 | $card-flipping-animation-duration: 0.3s !default;
13 |
14 | $inline-card-max-height: 400px !default;
15 | $inline-card-min-height: 150px !default;
16 | $inline-card-height: 100% !default;
17 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/card/_card.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | @import '../variables';
6 |
7 | .flip-tag-hidden {
8 | .flip-tag, .back .flip-tag {
9 | display: none;
10 | }
11 | }
12 |
13 | .uncharted-cards-card {
14 | $card-outer: $card-margin + $card-border-width;
15 | height: $card-height;
16 | padding: $card-margin;
17 | flex-shrink: 0;
18 | transition: width $card-expanding-animation;
19 |
20 | &.title-only {
21 | .card-image {
22 | display: none;
23 | }
24 | .card-body h1 {
25 | margin-right: 13px;
26 | }
27 | .metadata-content {
28 | .image {
29 | display: none;
30 | }
31 | }
32 | }
33 |
34 | &.shadow-enabled {
35 | .card {
36 | border: none;
37 | box-shadow: $card-shadow;
38 | }
39 | .card-content-container {
40 | padding-left: $card-margin * 2;
41 | left: -(2 * $card-margin);
42 | &:after {
43 | width: calc(100% - #{$card-margin * 2});
44 | }
45 | }
46 | }
47 |
48 | &.flip-container.shadow-enabled {
49 | .back {
50 | box-shadow: none;
51 | }
52 | .flipped {
53 | .back {
54 | box-shadow: $card-shadow;
55 | }
56 | .front {
57 | box-shadow: none;
58 | }
59 | }
60 | }
61 |
62 | .card {
63 | border: $card-border;
64 | background-color: #fff;
65 | position: relative;
66 | overflow: hidden;
67 | }
68 |
69 | .card-content-container {
70 | position: absolute;
71 | height: 100%;
72 | padding-left: $card-outer * 2;
73 | left: -(2 * $card-outer);
74 | transition: opacity $card-expanding-animation;
75 | opacity: 1;
76 | z-index: 1;
77 | &.card-reader-container {
78 | opacity: 0;
79 | z-index: 0;
80 | }
81 | }
82 |
83 | &.expanded .card-content-container {
84 | opacity: 0;
85 |
86 | &.card-reader-container {
87 | opacity: 1;
88 | z-index: 2;
89 | }
90 | }
91 |
92 | .card-content-container:after {
93 | content: "";
94 | pointer-events: none;
95 | position: absolute;
96 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 85%);
97 | width: calc(100% - #{$card-outer * 2});
98 | height: 50px;
99 | bottom: 0;
100 | }
101 |
102 | .metadata-content-container.has-image:after {
103 | bottom: 50px;
104 | }
105 |
106 | .card-reader-container:after {
107 | display: none;
108 | }
109 |
110 | .card-header {
111 | height: 28px;
112 | }
113 |
114 | .card-image + .card-body {
115 | padding-top: 8px;
116 | }
117 |
118 | .card-content-container {
119 |
120 | h1 {
121 | font-size: 13px;
122 | margin: 0;
123 | cursor: pointer;
124 | }
125 |
126 | .meta-line {
127 | flex-shrink: 0;
128 | }
129 |
130 | .summary {
131 | margin-top: 10px;
132 | font: inherit;
133 | font-size: 12px;
134 | overflow: hidden;
135 | }
136 | }
137 |
138 | // Card body
139 | .card-body {
140 | display: flex;
141 | flex-direction: column;
142 | overflow: hidden;
143 | overflow-wrap: break-word;
144 | padding: 16px 16px 8px 16px;
145 | }
146 |
147 | // Card icon
148 | .card-icon {
149 | height: 28px;
150 | width: 28px;
151 | padding: 6px;
152 | background: #f6f6f6 no-repeat 50% 50% / 16px 16px;
153 | border-radius: 14px;
154 | position: absolute;
155 | top: 2px;
156 | right: 3px;
157 | z-index: 999;
158 | transition: opacity $card-expanding-animation;
159 | }
160 |
161 | &.expanded .card-icon {
162 | opacity: 0;
163 | }
164 |
165 | .meta-data-content {
166 | position: relative;
167 | height: 100%;
168 | $title-line-height: 1.25em;
169 |
170 | .overflow-box {
171 | position: relative;
172 | overflow: hidden;
173 | white-space: nowrap;
174 | }
175 |
176 | .overflow-box.overflow:after {
177 | content: "";
178 | pointer-events: none;
179 | position: absolute;
180 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #fff 85%);
181 | right: 0;
182 | top: 0;
183 | height: 100%;
184 | width: 5em;
185 | }
186 |
187 | .title-box {
188 | font-size: 13px;
189 | max-height: #{$title-line-height * 2};
190 | white-space: normal;
191 |
192 | h1 {
193 | font-size: inherit;
194 | }
195 |
196 | &.overflow:after {
197 | top: $title-line-height;
198 | height: $title-line-height;
199 | }
200 | }
201 |
202 | .subtitle-box {
203 | margin-top: 2px;
204 | .subtitle {
205 | margin: 0;
206 | display: inline;
207 | }
208 | }
209 | }
210 |
211 | .meta-data-table {
212 | margin-top: 10px;
213 | font-size: 12px;
214 | vertical-align: bottom;
215 | table {
216 | table-layout: fixed;
217 | }
218 | .value-box {
219 | &.overflow:after {
220 | width: 15px;
221 | }
222 | }
223 |
224 | .key-box {
225 | max-width: 38.2%;
226 | text-align: right;
227 | padding-right: 2px;
228 | color: #bbb;
229 | }
230 |
231 | .meta-data-box {
232 | overflow: hidden;
233 | .value {
234 | padding-left: 2px;
235 | }
236 |
237 | .value img {
238 | max-height: 1em;
239 | max-width: 100%;
240 | }
241 | }
242 | }
243 |
244 | .meta-data-images-container {
245 | position: absolute;
246 | width: 100%;
247 | height: 50px;
248 | bottom: 0;
249 | background-color: #F2F2F2;
250 | display: flex;
251 | flex-wrap: nowrap;
252 | .image {
253 | width: 50px;
254 | height: 50px;
255 | background-position: 50% 50%;
256 | background-repeat: no-repeat;
257 | background-size: cover;
258 | margin-left: 1px;
259 | flex-shrink: 0;
260 | }
261 | }
262 | }
263 |
264 |
265 | .uncharted-cards-card {
266 | .flipper,
267 | .front,
268 | .back {
269 | width: 100%;
270 | height: 100%;
271 | }
272 | .flipper {
273 | position: relative;
274 | }
275 | }
276 |
277 | // Flipping animation
278 | .uncharted-cards-card.flip-container {
279 | perspective: 1000px;
280 | .flipper {
281 | transition: $card-flipping-animation-duration;
282 | transform-style: preserve-3d;
283 | &.flipped {
284 | transform: rotateY(180deg);
285 | }
286 | }
287 | .front,
288 | .back {
289 | backface-visibility: hidden;
290 | position: absolute;
291 | top: 0;
292 | left: 0;
293 | }
294 | .front {
295 | z-index: 2;
296 | transform: rotateY(0deg);
297 | }
298 | .back {
299 | transform: rotateY(180deg);
300 | }
301 | }
302 |
303 | .uncharted-cards-card.flip-container.expanded {
304 | .flipper {
305 | transition: none;
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/card/card.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
8 |
9 | {{#unless removeFrontCard}}
10 |
11 | {{#if iconUrl}}
12 |
13 | {{/if}}
14 |
15 |
16 |
17 |
18 | {{#if title}}
19 |
{{title}}
20 | {{/if}}
21 | {{#if subtitle.length }}
22 |
23 | {{#each subtitle}}{{#unless @first}}{{../subtitleDelimiter}}{{/unless}}{{this}}{{/each}}
24 |
25 | {{/if}}
26 |
{{{summary}}}
27 |
28 |
29 |
30 |
31 |
32 | {{/unless}}
33 | {{#unless removeBackCard}}
34 |
35 |
36 |
37 |
73 | {{#if imageUrl}}
74 |
79 | {{/if}}
80 |
81 |
82 |
83 |
84 | {{/unless}}
85 |
86 |
87 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/card/card.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import cardTemplate from './card.handlebars';
6 | import ReaderContent from '../readerContent/readerContent';
7 | import HeaderImage from '../headerImage/headerImage';
8 | import { IBindable, createFallbackIconURL } from '../../util';
9 | import { DEFAULT_CONFIG, EVENTS } from '../constants';
10 |
11 | export default class Card extends IBindable {
12 | constructor(spec = {}) {
13 | super();
14 | this.headerImage = new HeaderImage();
15 | this.readerContent = new ReaderContent();
16 | this.forward(this.readerContent);
17 | this.reset(spec);
18 | }
19 |
20 | reset(spec = {}) {
21 | this.$element = undefined;
22 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
23 | this.data = spec.data || {};
24 |
25 | const imageHeight = (this.data.summary && !this._config['card.displayLargeImage']) ?
26 | undefined : this.initialWidth - 10; // card margins from css TODO: move css to config object
27 |
28 | this.headerImage.reset({ imageUrls: this.data.imageUrl, imageHeight: imageHeight, config: this._config });
29 | this.readerContent.reset({ data: this.data, config: this._config });
30 | this.forward(this.readerContent);
31 |
32 | this.isExpanded = false;
33 | this.isFlipped = this._config['card.displayBackCardByDefault'];
34 | return this;
35 | }
36 |
37 | get expandedWidth() {
38 | return this._config['card.expandedWidth'];
39 | }
40 |
41 | get initialWidth() {
42 | return this._config['card.width'];
43 | }
44 |
45 | get height() {
46 | return this._config['card.height'];
47 | }
48 |
49 | _getIconUrl() {
50 | const source = this.data.sourceIconName || this.data.source;
51 | return this.data.sourceImage || (this.data.source && createFallbackIconURL(50, 50, source));
52 | }
53 |
54 | render() {
55 | const noImages = !this.data.imageUrl && (this.data.source || this.data.sourceUrl);
56 | const displayBackCardByDefault = this._config['card.displayBackCardByDefault'];
57 | const disableFlipping = this._config['card.disableFlipping'];
58 | const enableBoxShadow = this._config['card.enableBoxShadow'];
59 | const metaDataFontSize = this._config['card.metadata.fontSize'];
60 | const metaDataTitleColor = this._config['card.metadata.title.color'];
61 | const metaDataTitleFontFamily = this._config['card.metadata.title.fontFamily'];
62 | const metaDataValueColor = this._config['card.metadata.value.color'];
63 | const metaDataValueFontFamily = this._config['card.metadata.value.fontFamily'];
64 |
65 | const data = Object.assign({
66 | titleOnly: noImages,
67 | boxShadow: enableBoxShadow,
68 | width: this.isExpanded ? this.expandedWidth : this.initialWidth,
69 | height: this.height,
70 | expandedWidth: this.expandedWidth,
71 | cardContentWidth: this.initialWidth,
72 | isExpanded: this.isExpanded,
73 | isFlipped: this.isFlipped,
74 | disableFlipping,
75 | subtitleDelimiter: this._config.subtitleDelimiter,
76 | iconUrl: this._getIconUrl(),
77 | removeFrontCard: disableFlipping && displayBackCardByDefault,
78 | removeBackCard: disableFlipping && !displayBackCardByDefault,
79 | tooltip: $('').html(this.data.summary).text(),
80 | metaDataFontSize: metaDataFontSize,
81 | metaDataTitleColor: metaDataTitleColor,
82 | metaDataTitleFontFamily: metaDataTitleFontFamily,
83 | metaDataValueColor: metaDataValueColor,
84 | metaDataValueFontFamily: metaDataValueFontFamily,
85 | }, this.data);
86 |
87 | this.$element = $(cardTemplate(data));
88 |
89 | this._$cardImage = this.$element.find('.card-image');
90 | this.headerImage.hasImages() && this._$cardImage.append(this.headerImage.render());
91 |
92 | this.isExpanded && this._renderReaderContent();
93 |
94 | this._registerDomEvents();
95 | return this.$element;
96 | }
97 |
98 | _renderReaderContent() {
99 | if (this.$element) {
100 | const readerContainerSelector = this.isFlipped ? '.back.card .card-reader-container' : '.front.card .card-reader-container';
101 | this._$readerContent = this.readerContent.render();
102 | this.$element.find(this._config['card.disableFlipping'] ? '.card-reader-container' : readerContainerSelector).html(this._$readerContent);
103 | }
104 | }
105 |
106 | _moveReaderContent() {
107 | if (this.readerContent && this.readerContent.$element && this.readerContent.$element.parent().hasClass('card-reader-container')) {
108 | const readerContainerSelector = this.isFlipped ? '.back.card .card-reader-container' : '.front.card .card-reader-container';
109 | this.$element.find(this._config['card.disableFlipping'] ? '.card-reader-container' : readerContainerSelector).append(this.readerContent.$element);
110 | }
111 | }
112 |
113 | _registerDomEvents() {
114 | this.$element.on('transitionend', event => {
115 | const originalEvent = event.originalEvent;
116 | if (event.target !== this.$element[0]) {
117 | return;
118 | }
119 | if (event.target === this.$element[0] && originalEvent.propertyName === 'width' && !this.isExpanded) {
120 | this.emit(EVENTS.CARD_SHRINK, this);
121 | } else if (event.target === this.$element[0] && originalEvent.propertyName === 'width' && this.isExpanded) {
122 | this.emit(EVENTS.CARD_EXPAND, this);
123 | }
124 | });
125 | this.$element.on('click', '.card', event => {
126 | event.stopImmediatePropagation();
127 | this.emit(EVENTS.CARD_CLICK, this);
128 | });
129 |
130 | this.$element.on('click', '.meta-data-table a', event => {
131 | event.stopImmediatePropagation();
132 | this.emit(EVENTS.CARD_CLICK_LINK, event);
133 | return !this._config['card.disableLinkNavigation'];
134 | });
135 |
136 | this.$element.on('mouseenter', '.overflow-box', event => $(event.currentTarget).attr('title', $(event.currentTarget).find('.overflow-value').text().trim()));
137 | this.$element.on('mouseleave', '.overflow-box', event => $(event.currentTarget).removeAttr('title'));
138 | }
139 |
140 | expand() {
141 | this.isExpanded = true;
142 | if (this.$element) {
143 | this.$element.addClass('expanded');
144 | this.$element.css('width', this.expandedWidth);
145 | }
146 | }
147 |
148 | shrink() {
149 | this.isExpanded = false;
150 | if (this.$element) {
151 | this.$element.removeClass('expanded');
152 | this.$element.css('width', this.initialWidth);
153 | }
154 | }
155 |
156 | flip(state) {
157 | this.isFlipped = state === undefined ? !this.isFlipped : Boolean(state);
158 | if (this.$element) {
159 | this.$element.find('.flipper').toggleClass('flipped', this.isFlipped);
160 | this._moveReaderContent();
161 | }
162 | }
163 |
164 | updateReaderContent(readerContentData = {}) {
165 | this.readerContent.updateData(readerContentData);
166 | this._renderReaderContent();
167 | }
168 |
169 | scaleHeaderImages() {
170 | this.headerImage.hasImages() && this._$cardImage[0] && this.headerImage.scaleImages(this._$cardImage[0].offsetWidth, this._$cardImage[0].offsetHeight);
171 | }
172 | }
173 |
174 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/cards/_cards.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | @import '../variables';
6 |
7 | .uncharted-cards {
8 | width: 100%;
9 | height: 100%;
10 | }
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/cards/cards.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/cards/cards.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import cardsTemplate from './cards.handlebars';
6 | import InlineCardsView from '../inlineCardsView/inlineCardsView';
7 | import WrappedCardsView from '../wrappedCardsView/wrappedCardsView';
8 | import Card from '../card/card';
9 | import { IBindable } from '../../util';
10 | import { DEFAULT_CONFIG } from '../constants';
11 |
12 | export default class Cards extends IBindable {
13 | constructor(config = {}) { // eslint-disable-line
14 | super();
15 | this._initInlineCardsView();
16 | this._initWrappedCardsView();
17 | this.reset(config);
18 | }
19 |
20 | _initInlineCardsView() {
21 | this.inlineCardsView = new InlineCardsView({ config: this._config });
22 | this.inlineCardsView.postRender = () => {
23 | this._fadeOutOverflowingCardMetadataTexts(this.inlineCardsView.cardsInViewPort);
24 | };
25 | this.forward(this.inlineCardsView);
26 | }
27 |
28 | _initWrappedCardsView() {
29 | this.wrappedCardsView = new WrappedCardsView({ config: this._config });
30 | this.wrappedCardsView.postRender = () => {
31 | this._fadeOutOverflowingCardMetadataTexts(this.wrappedCardsView.cardInstances);
32 | };
33 | this.forward(this.wrappedCardsView);
34 | }
35 |
36 | _initMoreCards(cardsData) {
37 | const cardsInstances = cardsData.map(data => {
38 | const card = new Card({ data, config: Object.assign({}, this._config, {
39 | 'card.height': this.inlineMode ? null : this._config['card.height'],
40 | })});
41 | this.forward(card);
42 | return card;
43 | });
44 | this.cardInstances.push(...cardsInstances);
45 | }
46 |
47 | _fadeOutOverflowingCardMetadataTexts(cardInstances) {
48 | const overflowBoxElements = [];
49 | cardInstances.forEach(card => {
50 | const cardEle = card.$element && card.$element[0];
51 | const textBoxElements = cardEle && cardEle.querySelectorAll('.meta-data-content .overflow-box');
52 | [].forEach.call(textBoxElements || [], element => {
53 | const textValueElement = element.children && element.children[0];
54 | if (textValueElement) {
55 | const boxWidth = element.offsetWidth;
56 | const boxHeight = element.offsetHeight;
57 | const valueWidth = textValueElement.offsetWidth;
58 | const valueHeight = textValueElement.offsetHeight;
59 | const isOverflowing = valueWidth > boxWidth || valueHeight > boxHeight;
60 | isOverflowing && overflowBoxElements.push(element);
61 | }
62 | });
63 | });
64 | overflowBoxElements.forEach(element => element.classList.add('overflow'));
65 | }
66 |
67 | reset(config) {
68 | this._config = Object.assign({}, DEFAULT_CONFIG, config);
69 | this.cardInstances = [];
70 | this.inlineMode = Boolean(this._config.inlineMode);
71 | this.inlineCardsView.reset({ config: this._config });
72 | this.wrappedCardsView.reset({ config: this._config });
73 | return this;
74 | }
75 |
76 | render() {
77 | this.$element = $(cardsTemplate({
78 | inlineMode: this.inlineMode,
79 | }));
80 | this.inlineMode
81 | ? this.$element.html(this.inlineCardsView.render())
82 | : this.$element.html(this.wrappedCardsView.render());
83 | return this.$element;
84 | }
85 |
86 | resize() {
87 | if (this.$element) {
88 | this.inlineMode
89 | ? this.inlineCardsView.resize()
90 | : this.wrappedCardsView.resize();
91 | }
92 | }
93 |
94 | clearCards() {
95 | this.cardInstances = [];
96 | this.inlineCardsView.cardInstances = [];
97 | this.wrappedCardsView.cardInstances = [];
98 | this.inlineMode
99 | ? this.$element.html(this.inlineCardsView.render())
100 | : this.$element.html(this.wrappedCardsView.clearCards());
101 | }
102 |
103 | loadData(cardsData) {
104 | this.clearCards();
105 | this._initMoreCards(cardsData);
106 |
107 | this.inlineMode
108 | ? this.inlineCardsView.updateCardInstances(this.cardInstances)
109 | : this.wrappedCardsView.renderMoreCards(this.cardInstances);
110 | }
111 |
112 | loadMoreData(cardsData) {
113 | const numberOfCurrentCards = this.cardInstances.length;
114 | this._initMoreCards(cardsData);
115 | this.inlineMode
116 | ? this.inlineCardsView.updateCardInstances(this.cardInstances)
117 | : this.wrappedCardsView.renderMoreCards(this.cardInstances.slice(numberOfCurrentCards));
118 | }
119 |
120 | findCardById(cardId) {
121 | return this.cardInstances.find(card => card.data.id === cardId);
122 | }
123 |
124 | /**
125 | * Open a reader for the provided card
126 | * @param {Object} card - card instance whose reader will be opened.
127 | */
128 | openReader(card) {
129 | this.inlineMode
130 | ? this.inlineCardsView.openReader(card)
131 | : this.wrappedCardsView.openReader(card);
132 | }
133 |
134 | closeReader() {
135 | this.inlineMode
136 | ? this.inlineCardsView.closeReader()
137 | : this.wrappedCardsView.closeReader();
138 | }
139 |
140 | toggleInlineDisplayMode(state) {
141 | this.closeReader();
142 | this.inlineMode = state === undefined ? !this.inlineMode : state;
143 | this.cardInstances.forEach(card => {
144 | card.$element && card.$element.remove() && (card.$element = undefined);
145 | card._config['card.height'] = this.inlineMode ? null : this._config['card.height'];
146 | });
147 | this.inlineMode
148 | ? this.$element.html(this.inlineCardsView.render()) && this.inlineCardsView.updateCardInstances(this.cardInstances)
149 | : this.$element.html(this.wrappedCardsView.clearCards()) && this.wrappedCardsView.renderMoreCards(this.cardInstances);
150 | }
151 |
152 | updateReaderContent(card, readerContentData) {
153 | this.inlineMode
154 | ? card.updateReaderContent(readerContentData)
155 | : this.wrappedCardsView.verticalReader.updateReaderContent(card, readerContentData);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export const DEFAULT_CONFIG = {
6 | 'inlineMode': true,
7 | 'subtitleDelimiter': ' \u2022 ',
8 | 'scrollToVerticalReaderDuration': 300,
9 | 'inlineCardCenteringDuration': 300,
10 | 'card.width': 200,
11 | 'card.height': 300,
12 | 'card.enableBoxShadow': false,
13 | 'card.expandedWidth': 520,
14 | 'card.disableFlipping': false,
15 | 'card.displayBackCardByDefault': false,
16 | 'card.disableLinkNavigation': false, // set to true if you plan to handle the CARD_CLICK_LINK event
17 | 'card.displayLargeImage': false, // false is actually more like 'auto'
18 | 'card.metadata.fontSize': 10, // pt, to match the PBI fontSize control
19 | 'card.metadata.title.color': '#bbb',
20 | 'card.metadata.title.fontFamily': 'inherit',
21 | 'card.metadata.value.color': '#000',
22 | 'card.metadata.value.fontFamily': 'inherit',
23 | 'verticalReader.height': 500,
24 | 'readerContent.headerBackgroundColor': '#555',
25 | 'readerContent.headerSourceLinkColor': '#fff',
26 | 'readerContent.headerImageMaxWidth': 190,
27 | 'readerContent.disableLinkNavigation': false, // set to true if you plan to handle the READER_CONTENT_CLICK_LINK event
28 | 'readerContent.cropImages': true, // set to false to show entire image, even if portrait mode and tiny
29 | };
30 |
31 | export const EVENTS = {
32 | CARDS_CLICK_BACKGROUND: 'cards:clickBackground',
33 | CARD_CLICK: 'card:click',
34 | CARD_CLICK_LINK: 'card:clickLink',
35 | CARD_EXPAND: 'card:expand',
36 | CARD_SHRINK: 'card:shrink',
37 | INLINE_CARDS_VIEW_SCROLL_END: 'inlineCardsView:scrollEnd',
38 | WRAPPED_CARDS_VIEW_SCROLL_END: 'wrappedCardsView:scrollEnd',
39 | VERTICAL_READER_NAVIGATE_CARD: 'verticalReader:navigateToCard',
40 | VERTICAL_READER_CLICK_BACKGROUND: 'verticalReader:clickBackground',
41 | READER_CONTENT_CLICK_CLOSE: 'readerContent:clickCloseButton',
42 | READER_CONTENT_CLICK_LINK: 'readerContent:clickLink',
43 | };
44 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/headerImage/_headerImage.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | .uncharted-cards-header-image {
6 | height: 100px;
7 | display: flex;
8 | position: relative;
9 | overflow: hidden;
10 | .image {
11 | height: 100%;
12 | position: relative;
13 | background-position: 50% 30%;
14 | background-repeat: no-repeat;
15 | background-color: #fff;
16 | flex-grow: 1;
17 | background-size: cover;
18 | }
19 | .mirror-image-box {
20 | flex-grow: 1;
21 | position: relative;
22 | display: flex;
23 |
24 | .gradient {
25 | position: absolute;
26 | height: 100%;
27 | width: 100%;
28 | z-index: 1;
29 | left: 1px;
30 | }
31 |
32 | .image {
33 | right: 1px;
34 | width: 100%;
35 | transform: scaleX(-1) translate3d(0, 0, 0);
36 | outline: 3.5px solid transparent;
37 | }
38 |
39 | .padding {
40 | border-left: 1px solid;
41 | flex-grow: 1;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/headerImage/headerImage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import { IBindable, loadImage } from '../../util';
6 | import { DEFAULT_CONFIG } from '../constants';
7 |
8 | export default class HeaderImage extends IBindable {
9 | constructor(spec = {}) {
10 | super();
11 | this.reset(spec);
12 | }
13 |
14 | reset(spec) {
15 | this.MAX_NUM_IMAGES = 4;
16 | this.DEFAULT_IMAGE_HEIGHT = 100;
17 | this.DEFAULT_IMAGE_BG_COLOR = '#FFF';
18 |
19 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
20 | this.imageUrls = [].concat(spec.imageUrls || []).slice(0, this.MAX_NUM_IMAGES);
21 | this.loadedImagePromises = this.imageUrls.map(imageUrl => loadImage(imageUrl));
22 | this._mirrorLastImage = Boolean(spec.mirrorLastImage);
23 | this._mirrorImageGradientColor = spec.mirrorImageGradientColor || '#555';
24 | this.imageHeight = spec.imageHeight || this.DEFAULT_IMAGE_HEIGHT;
25 | this.imgMaxWidth = `${spec.imageMaxWidth}px` || 'none';
26 |
27 | this.portraitImageMaxWidth = '100px';
28 | return this;
29 | }
30 |
31 | render() {
32 | const lastImageUrl = this.imageUrls[this.imageUrls.length - 1];
33 | const wrapImages = this.imageHeight > this.DEFAULT_IMAGE_HEIGHT && this.imageUrls.length > 2;
34 | this.$element = $('')
35 | .css({
36 | 'background-color': this._mirrorLastImage ? this._mirrorImageGradientColor : this.DEFAULT_IMAGE_BG_COLOR,
37 | 'height': `${this.imageHeight}px`,
38 | 'flex-wrap': wrapImages ? 'wrap' : 'nowrap',
39 | });
40 | this._$partialImages = this.imageUrls.map(imageUrl => {
41 | const $image = $('')
42 | .css({
43 | 'background-image': `url(${imageUrl})`,
44 | 'max-width': this.imgMaxWidth,
45 | });
46 | return wrapImages ? $image.css({ height: '50%', width: '50%', 'flex-grow': 0 }) : $image;
47 | });
48 | if (this._mirrorLastImage && lastImageUrl) {
49 | this._$partialImages.push($(`
50 |
54 | `));
55 | }
56 | return this.$element.append(this._$partialImages);
57 | }
58 |
59 | hasImages() {
60 | return this.imageUrls.length > 0;
61 | }
62 |
63 | scaleImages(containerWidth, containerHeight) {
64 | const $partialImages = [];
65 | const numberOfImages = this.imageUrls.length;
66 | const partialImageWidth = containerWidth / numberOfImages;
67 | const partialImageHeight = containerHeight / numberOfImages;
68 | const subdivided = numberOfImages > 1;
69 | this.loadedImagePromises.forEach((imagePromise, index) => {
70 | imagePromise.then(img => {
71 | const scale = Math.max(partialImageWidth / img.width, partialImageHeight / img.height);
72 | const scaledWidth = Math.round(img.width * scale);
73 | let sizeType = 'cover';
74 | if ((subdivided && scaledWidth < partialImageWidth) || (!subdivided && scaledWidth > partialImageWidth)) {
75 | sizeType = 'contain';
76 | } else if (scale > 1) {
77 | sizeType = 'auto';
78 | }
79 | this._$partialImages[index].css({
80 | 'background-size': sizeType,
81 | 'background-image': `url(${this.imageUrls[index]})`,
82 | });
83 | });
84 | });
85 | return $partialImages;
86 | }
87 |
88 | fitImages() {
89 | this.loadedImagePromises.forEach((imagePromise, index, array) => {
90 | imagePromise.then(img => {
91 | const aspectRatio = img.width / img.height;
92 | const imageProperty = {
93 | 'max-width': Math.round(this.imageHeight * aspectRatio) + 'px',
94 | 'background-size': 'cover',
95 | };
96 | this._$partialImages[index].css(imageProperty);
97 | if (index === array.length - 1) {
98 | this._$partialImages[array.length].find('.image, .gradient').css(imageProperty);
99 | }
100 | });
101 | });
102 | }
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/inlineCardsView/_inlineCardsView.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | .uncharted-inline-cards-view {
6 | height: 100%;
7 | width: 100%;
8 | overflow: auto;
9 | .cards-container {
10 | height: 100%;
11 | display: flex;
12 | flex-wrap: nowrap;
13 | flex-direction: row;
14 | justify-content: flex-start;
15 | }
16 |
17 | .uncharted-cards-card {
18 | height: $inline-card-height;
19 | max-height: $inline-card-max-height;
20 | min-height: $inline-card-min-height;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/inlineCardsView/inlineCardsView.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/inlineCardsView/inlineCardsView.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import inlineCardsListTemplate from './inlineCardsView.handlebars';
6 | import { IBindable, mapVerticalToHorizontalScroll, delayOnHorizontalToVerticalScrollingTransition } from '../../util';
7 | import { DEFAULT_CONFIG, EVENTS } from '../constants';
8 |
9 | export default class InlineCardsView extends IBindable {
10 | constructor(spec = {}) { // eslint-disable-line
11 | super();
12 | this.postRender = () => { /* this function called after render */ };
13 | this.reset(spec);
14 | }
15 |
16 | reset(spec) {
17 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
18 | this.cardInstances = [];
19 | this.cardsInViewPort = [];
20 | this.cardExpansionState = {};
21 | this.CARD_WIDTH = this._config['card.width'];
22 | this.CARD_EXPANDED_WIDTH = this._config['card.expandedWidth'];
23 | this.cardAnimationDuration = this._config.inlineCardCenteringDuration;
24 | return this;
25 | }
26 |
27 | render() {
28 | this.$element = $(inlineCardsListTemplate());
29 | this._$cardsContainer = this.$element.find('.cards-container');
30 | this._$responsivePadding = this.$element.find('.responsive-padding');
31 | this._registerDomEvents();
32 | this.renderCardsInViewPort();
33 | return this.$element;
34 | }
35 |
36 | resize() {
37 | this.$element && this.renderCardsInViewPort();
38 | }
39 |
40 | _registerDomEvents() {
41 | let ticking = false;
42 |
43 | this.$element.on('click', event => this.emit(EVENTS.CARDS_CLICK_BACKGROUND, this, event.originalEvent));
44 | this.$element.on('scroll', event => {
45 | if (!ticking) {
46 | requestAnimationFrame(() => {
47 | this.$element[0].scrollLeft + this.$element[0].offsetWidth >= this.$element[0].scrollWidth - 1 && this.emit(EVENTS.INLINE_CARDS_VIEW_SCROLL_END, this, event.originalEvent);
48 | this.renderCardsInViewPort();
49 | ticking = false;
50 | });
51 | ticking = true;
52 | }
53 | });
54 | mapVerticalToHorizontalScroll(this.$element);
55 | delayOnHorizontalToVerticalScrollingTransition(this.$element, '.uncharted-cards-reader-content');
56 | }
57 |
58 | updateCardInstances(cardInstances) {
59 | this.cardsInViewPort = [];
60 | this.cardInstances = cardInstances;
61 | this._$cardsContainer.css({ 'min-width': this.CARD_WIDTH * this.cardInstances.length });
62 | this.renderCardsInViewPort();
63 | }
64 |
65 | _getIndexOfFirstCardInViewPort(scrollLeft) {
66 | const {
67 | closingCard,
68 | closingCardIndex,
69 | expandingCard,
70 | expandingCardIndex,
71 | expandedCard,
72 | expandedCardIndex,
73 | } = this.cardExpansionState;
74 | const CARD_WIDTH = this.CARD_WIDTH;
75 | const responsivePaddingWidth = this._$responsivePadding[0].getBoundingClientRect().width;
76 | let index;
77 | if (closingCard && expandingCard && closingCardIndex < expandingCardIndex) {
78 | const closingCardOffSetLeft = closingCardIndex * CARD_WIDTH;
79 | if (closingCardOffSetLeft >= scrollLeft) {
80 | // [|][][][ c ][][|]
81 | index = Math.floor(scrollLeft / CARD_WIDTH);
82 | } else if (closingCardOffSetLeft < scrollLeft && closingCardOffSetLeft + this.CARD_EXPANDED_WIDTH >= scrollLeft) {
83 | // [ c | ][][][]|
84 | index = closingCardIndex;
85 | } else {
86 | // [ c ][]|[][][]|
87 | index = Math.floor((Math.max(scrollLeft - responsivePaddingWidth, 0)) / CARD_WIDTH);
88 | }
89 | } else if (expandedCard && expandedCardIndex * CARD_WIDTH < scrollLeft) {
90 | const expandedReaderLeftPosition = expandedCardIndex * CARD_WIDTH;
91 | const expandedReaderWidth = this.CARD_EXPANDED_WIDTH;
92 | const widthBetweenExpandedCardToScrollLeftPosition = scrollLeft - (expandedReaderLeftPosition + expandedReaderWidth);
93 | if (widthBetweenExpandedCardToScrollLeftPosition > 0) {
94 | const paddingWidth = expandedReaderWidth - CARD_WIDTH;
95 | index = Math.floor(widthBetweenExpandedCardToScrollLeftPosition / CARD_WIDTH) + expandedCardIndex + 1;
96 | this._$cardsContainer.css({ 'min-width': this.CARD_WIDTH * this.cardInstances.length + paddingWidth});
97 | this._$responsivePadding.css({ width: paddingWidth });
98 | // console.log('in left offscreen');
99 | } else {
100 | index = expandedCardIndex;
101 | this._$responsivePadding.css({ width: 0 });
102 | // console.log('in viewport');
103 | }
104 | } else {
105 | // console.log('other cases');
106 | index = Math.floor(scrollLeft / CARD_WIDTH);
107 | this._$responsivePadding.css({ width: 0 });
108 | }
109 | return index;
110 | }
111 |
112 | renderCardsInViewPort() {
113 | const scrollLeft = this.$element[0].scrollLeft;
114 | const viewportWidth = this.$element[0].offsetWidth;
115 | const CARD_WIDTH = this.CARD_WIDTH;
116 | const indexOfFirstCardInViewPort = this._getIndexOfFirstCardInViewPort(scrollLeft);
117 | const paddingLeft = indexOfFirstCardInViewPort * CARD_WIDTH;
118 |
119 | const newItemsInViewPort = [];
120 |
121 | // get cards to render
122 | for (let i = indexOfFirstCardInViewPort, remainingSpaceInViewPort = viewportWidth + (this.CARD_EXPANDED_WIDTH - CARD_WIDTH); remainingSpaceInViewPort > 0; i++) {
123 | const card = this.cardInstances[i];
124 | card && newItemsInViewPort.push(card);
125 | i !== indexOfFirstCardInViewPort && (remainingSpaceInViewPort -= CARD_WIDTH);
126 | }
127 |
128 | if (this.cardsInViewPort[0] !== newItemsInViewPort[0] || this.cardsInViewPort[this.cardsInViewPort.length - 1] !== newItemsInViewPort[newItemsInViewPort.length - 1]) {
129 | this.cardsInViewPort.forEach(card => {
130 | newItemsInViewPort.indexOf(card) < 0 && card.$element && card.$element.remove() && (card.$element = undefined);
131 | });
132 | const fragments = document.createDocumentFragment();
133 | newItemsInViewPort.forEach((card, index) => {
134 | const isCardAlreadyRendered = Boolean(card.$element);
135 | const $cardElement = isCardAlreadyRendered ? card.$element : card.render();
136 | $cardElement.css({
137 | 'order': index + 1,
138 | });
139 | !isCardAlreadyRendered && fragments.appendChild($cardElement[0]);
140 | });
141 | this._$cardsContainer.css({
142 | 'padding-left': paddingLeft,
143 | });
144 | this._$cardsContainer.append(fragments);
145 | this.cardsInViewPort = newItemsInViewPort;
146 | }
147 | this.postRender();
148 | }
149 |
150 | _expandAndCenterCard(card, scrollDuration) {
151 | const cardIndex = this.cardInstances.indexOf(card);
152 | const previouslyExpandedCard = this.cardInstances.filter(cardItem => cardItem.isExpanded)[0];
153 | const previouslyExpandedCardIndex = this.cardInstances.indexOf(previouslyExpandedCard);
154 | const expandedToNormalLeftOffsetDifference = previouslyExpandedCard && previouslyExpandedCardIndex < cardIndex
155 | ? this.CARD_EXPANDED_WIDTH - this.CARD_WIDTH
156 | : 0;
157 | previouslyExpandedCard && previouslyExpandedCard.shrink();
158 | card.expand();
159 | this.centerCard(card, scrollDuration, -expandedToNormalLeftOffsetDifference);
160 |
161 | this.cardExpansionState = {
162 | expandingCard: card,
163 | expandingCardIndex: cardIndex,
164 | closingCard: previouslyExpandedCard,
165 | closingCardIndex: previouslyExpandedCardIndex,
166 | };
167 |
168 | this._$responsivePadding.animate({ width: 0 }, { duration: scrollDuration, complete: () => {
169 | this.cardExpansionState = {
170 | expandedCard: card,
171 | expandedCardIndex: cardIndex,
172 | };
173 | }});
174 | }
175 |
176 | /**
177 | * Open a reader for the provided card
178 | * @param {Object} card - card instance whose reader will be opened.
179 | */
180 | openReader(card) {
181 | if (!card.isExpanded) {
182 | this._expandAndCenterCard(card, this.cardAnimationDuration);
183 | }
184 | }
185 |
186 | closeReader() {
187 | this.cardInstances.forEach(card => card.shrink());
188 | this._$responsivePadding.animate({ width: 0 }, { duration: this.cardAnimationDuration, complete: () => {
189 | this.cardExpansionState = {};
190 | this._$cardsContainer.css({ 'min-width': this.CARD_WIDTH * this.cardInstances.length });
191 | }});
192 | }
193 |
194 | /**
195 | * Scroll to the provided card and centers it.
196 | * @param {Object} card - A card to be centered.
197 | * @param {Number} duration - A number representing the duration of the centering animation in ms.
198 | * @param {Number} offset - Given card will be offset from the center by provided number.
199 | */
200 | centerCard(card, duration, offset) {
201 | const viewportWidth = this.$element[0].offsetWidth;
202 | const targetCardIndex = this.cardInstances.indexOf(card);
203 | const expandedCardIndex = this.cardExpansionState.expandedCardIndex;
204 | const extraLeftOffset = expandedCardIndex !== undefined && expandedCardIndex < targetCardIndex
205 | ? this.CARD_EXPANDED_WIDTH - this.CARD_WIDTH
206 | : 0;
207 | const cardOffsetLeft = Math.max(targetCardIndex, 0) * this.CARD_WIDTH + extraLeftOffset;
208 | const leftMargin = Math.max(viewportWidth - card.expandedWidth, 0) / 2;
209 | const scrollLeft = cardOffsetLeft - leftMargin + (offset || 0);
210 | duration
211 | ? this.$element.animate({ scrollLeft: scrollLeft }, duration)
212 | : this.$element.scrollLeft(scrollLeft);
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/readerContent/_readerContent.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | .uncharted-cards-reader-content {
6 | $header-height: 33px;
7 | width: 100%;
8 | height: 100%;
9 | background: #fff;
10 | position: relative;
11 |
12 | hr {
13 | display: block;
14 | height: 1px;
15 | border: 0;
16 | border-top: 1px solid #ccc;
17 | margin: 1em 0;
18 | padding: 0;
19 | }
20 |
21 | .reader-content-header {
22 | $header-background-color: #f6f6f6;
23 | position: absolute;
24 | z-index: 999;
25 | width: 100%;
26 | display: flex;
27 |
28 | .header-source-link-box {
29 | position: relative;
30 | min-width: 170px;
31 | max-width: 50%;
32 | height: 27px;
33 | padding: 0 15px;
34 | top: 0;
35 | display: flex;
36 | align-items: center;
37 | }
38 |
39 | .header-source-link-background {
40 | position: absolute;
41 | left: 0;
42 | top: 0;
43 | width: 100%;
44 | height: 100%;
45 | background: #555;
46 | opacity: 0.7;
47 | }
48 |
49 | .source-link {
50 | z-index: 1;
51 | flex-grow: 1;
52 | text-decoration: none;
53 | color: #fff;
54 | white-space: nowrap;
55 | text-overflow: ellipsis;
56 | overflow: hidden;
57 | }
58 |
59 | .close-button {
60 | flex-grow: 1;
61 | text-align: right;
62 | height: 30px;
63 | color: rgba(0, 0, 0, 0.4);
64 | align-items: center;
65 | display: flex;
66 | flex-direction: row-reverse;
67 | cursor: pointer;
68 | .icon-box {
69 | height: calc(100% - 2px);
70 | margin-top: 2px;
71 | display: flex;
72 | align-items: center;
73 | }
74 | i {
75 | padding-right: 6px;
76 | }
77 | &:hover {
78 | i {
79 | color: rgba(0, 0, 0, 1);
80 | }
81 | }
82 | }
83 |
84 | .source-icon {
85 | height: 28px;
86 | width: 28px;
87 | padding: 6px;
88 | background: $header-background-color no-repeat 50% 50% / 16px 16px;
89 | border-radius: 14px;
90 | margin-top: 2px;
91 | margin-right: 3px;
92 | }
93 | }
94 |
95 | .reader-content-viewport {
96 | height: 100%;
97 | overflow: auto;
98 | overflow: overlay; // for webkit browser
99 | }
100 |
101 | .reader-content-body {
102 | position: relative;
103 | padding: 8px 32px 22px;
104 | font-size: 12px;
105 | }
106 |
107 | &.no-header-image {
108 | .reader-content-body {
109 | padding-top: 40px;
110 | }
111 | }
112 |
113 | h1.title {
114 | font-size: 1.3em;
115 | }
116 |
117 | .content {
118 | margin-top: 10px;
119 | }
120 |
121 | .reader-content-meta-data-table {
122 | font-size: 12px;
123 |
124 | td {
125 | border: 5px solid transparent;
126 | &.key-column {
127 | border-left: 0;
128 | }
129 | &.value-column {
130 | border-right: 0;
131 | }
132 | &:first-of-type {
133 | border-top: 0;
134 | }
135 | &:last-of-type {
136 | border-bottom: 0;
137 | }
138 | }
139 |
140 | .key-column {
141 | &.value-box {
142 | max-width: 170px;
143 | white-space: nowrap;
144 | overflow: hidden;
145 | text-overflow: ellipsis;
146 | text-align: right;
147 | padding-right: 2px;
148 | color: #bbb;
149 | }
150 | }
151 |
152 | .value-column {
153 | &.value-box {
154 | padding-left: 2px;
155 | }
156 | .value img {
157 | max-height: 1em;
158 | max-width: 100%;
159 | }
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/readerContent/readerContent.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
8 |
24 |
25 |
27 |
28 |
{{title}}
29 | {{#if subtitle}}
30 |
31 | {{#each subtitle}}{{#unless @first}}{{../subtitleDelimiter}}{{/unless}}{{this}}{{/each}}
32 |
33 | {{/if}}
34 |
35 |
36 | {{{content}}}
37 |
38 |
39 | {{#if metadata}}
40 |
41 |
42 | {{#each metadata}}
43 |
44 |
45 | {{@key}}
46 | |
47 |
48 |
49 | {{#if this}}
50 | {{#isImageDataUrl this}}
51 |
52 | {{else}}
53 | {{#linkify this}}{{/linkify}}
54 | {{/isImageDataUrl}}
55 | {{/if}}
56 |
57 | |
58 |
59 | {{/each}}
60 |
61 | {{/if}}
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/readerContent/readerContent.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import readerContentTemplate from './readerContent.handlebars';
6 | import { IBindable, createFallbackIconURL } from '../../util';
7 | import HeaderImage from '../headerImage/headerImage';
8 | import { DEFAULT_CONFIG, EVENTS } from '../constants';
9 |
10 | export default class ReaderContent extends IBindable {
11 | constructor(spec = {}) {
12 | super();
13 | this.headerImage = new HeaderImage();
14 | this.reset(spec);
15 | }
16 |
17 | reset(spec = {}) {
18 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
19 | this.updateData(spec.data);
20 | this.headerImage.reset({
21 | imageUrls: this.data.imageUrl,
22 | mirrorLastImage: true,
23 | mirrorImageGradientColor: this._config['readerContent.headerBackgroundColor'],
24 | imageMaxWidth: this._config['readerContent.headerImageMaxWidth'],
25 | config: this._config,
26 | });
27 | return this;
28 | }
29 |
30 | render() {
31 | const metaDataFontSize = this._config['card.metadata.fontSize'];
32 | const metaDataTitleColor = this._config['card.metadata.title.color'];
33 | const metaDataTitleFontFamily = this._config['card.metadata.title.fontFamily'];
34 | const metaDataValueColor = this._config['card.metadata.value.color'];
35 | const metaDataValueFontFamily = this._config['card.metadata.value.fontFamily'];
36 |
37 | this.$element = $(readerContentTemplate(Object.assign({
38 | iconUrl: this._getIconUrl(),
39 | subtitleDelimiter: this._config.subtitleDelimiter,
40 | headerBackgroundColor: this._config['readerContent.headerBackgroundColor'],
41 | headerSourceLinkColor: this._config['readerContent.headerSourceLinkColor'],
42 | metaDataFontSize: metaDataFontSize,
43 | metaDataTitleColor: metaDataTitleColor,
44 | metaDataTitleFontFamily: metaDataTitleFontFamily,
45 | metaDataValueColor: metaDataValueColor,
46 | metaDataValueFontFamily: metaDataValueFontFamily,
47 | }, this.data)));
48 | this.$headerImageContainer = this.$element.find('.reader-content-header-image');
49 | if (this.headerImage.hasImages()) {
50 | this.$headerImageContainer.append(this.headerImage.render());
51 | if (!this._config['readerContent.cropImages']) {
52 | this.headerImage.fitImages();
53 | }
54 | } else {
55 | this.$element.addClass('no-header-image');
56 | }
57 | this._registerDomEvents();
58 | return this.$element;
59 | }
60 |
61 | updateData(data) {
62 | this.data = data || {};
63 | }
64 |
65 | _registerDomEvents() {
66 | this.$element.on('click', '.close-button', event => {
67 | event.stopPropagation();
68 | this.emit(EVENTS.READER_CONTENT_CLICK_CLOSE, this);
69 | });
70 |
71 | this.$element.on('click', 'a', event => {
72 | var $anchor = $(event.target).closest('a');
73 | var href = $anchor.attr('href');
74 | if (href) {
75 | this.emit(EVENTS.READER_CONTENT_CLICK_LINK, event);
76 | if (!this._config['readerContent.disableLinkNavigation']) {
77 | window.open(href, '_blank');
78 | }
79 | }
80 | return false;
81 | });
82 | }
83 |
84 | _getIconUrl() {
85 | const source = this.data.sourceIconName || this.data.source;
86 | return this.data.sourceImage || (this.data.source && createFallbackIconURL(50, 50, source));
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/verticalReader/_verticalReader.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | $reader-animation-duration: 0.3s;
6 | .uncharted-cards-reader-holder {
7 | width: 100%;
8 | height: 0;
9 | transition: height $reader-animation-duration ease-out;
10 | }
11 |
12 | .uncharted-cards-vertical-reader {
13 | height: 100%;
14 | width: 100%;
15 | position: relative;
16 | padding: 5px 0;
17 | background: #555;
18 | outline: none;
19 | display: flex;
20 | justify-content: center;
21 | align-items: center;
22 |
23 | .marker {
24 | position: absolute;
25 | top: -10px;
26 | left: 50%;
27 | transform: translateX(-50%);
28 | border-bottom: 10px solid #555;
29 | border-left: 10px solid transparent;
30 | border-right: 10px solid transparent;
31 | height: 0;
32 | width: 0;
33 | transition: left $reader-animation-duration ease-out;
34 | }
35 |
36 | .reader-content-container {
37 | height: 100%;
38 | width: 100%;
39 | max-width: 1200px;
40 | }
41 |
42 | .reader-page-button {
43 | margin: 0;
44 | color: #ccc;
45 | font-size: 36px;
46 | i {
47 | padding: 25px;
48 | }
49 | &:hover {
50 | color: white;
51 | cursor: pointer;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/verticalReader/verticalReader.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/verticalReader/verticalReader.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import verticalReaderTemplate from './verticalReader.handlebars';
6 | import { IBindable } from '../../util';
7 | import { DEFAULT_CONFIG, EVENTS } from '../constants';
8 |
9 | export default class VerticalReader extends IBindable {
10 | constructor(spec = {}) {
11 | super();
12 | this.reset(spec);
13 | }
14 |
15 | reset(spec = {}) {
16 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
17 | this._cardInstances = [];
18 | return this;
19 | }
20 |
21 | get $readerHolder() {
22 | return this.$element.closest('.uncharted-cards-reader-holder');
23 | }
24 |
25 | render() {
26 | this.$element = $(verticalReaderTemplate());
27 | this._registerDOMEvents();
28 | return this.$element;
29 | }
30 |
31 | _registerDOMEvents() {
32 | this.$element.on('transitionend', event => {
33 | const originalEvent = event.originalEvent;
34 | if (event.target !== this.$element[0]) {
35 | return;
36 | }
37 | if (event.target === this.$element[0] && originalEvent.propertyName === 'width' && !this.isExpanded) {
38 | this._clearReaderContainer();
39 | this.emit(EVENTS.CARD_SHRINK, this);
40 | } else if (event.target === this.$element[0] && originalEvent.propertyName === 'width' && this.isExpanded) {
41 | this.emit(EVENTS.CARD_EXPAND, this);
42 | }
43 | });
44 | this.$element.on('click', event => {
45 | event.stopImmediatePropagation();
46 | event.target === this.$element[0] && this.emit(EVENTS.VERTICAL_READER_CLICK_BACKGROUND);
47 | });
48 | this.$element.on('click', '.reader-prev-button', () => this._navigate(-1));
49 | this.$element.on('click', '.reader-next-button', () => this._navigate(1));
50 | }
51 |
52 | /**
53 | * Move to a neighbouring card
54 | * @param {Number} offset - +1 to move to the next card; -1 to move to the previous card
55 | * @private
56 | */
57 | _navigate(offset) {
58 | const currentCardIndex = this._cardInstances.findIndex(card => card.data.id === this._markedCard.data.id);
59 | const toIndex = (currentCardIndex + offset) > 0 ? currentCardIndex + offset : 0;
60 |
61 | if (toIndex >= 0 && toIndex < this._cardInstances.length && currentCardIndex !== toIndex) {
62 | const targetCard = this._cardInstances[toIndex];
63 | this.placeUnder(targetCard, true);
64 | this.emit(EVENTS.VERTICAL_READER_NAVIGATE_CARD, targetCard);
65 | }
66 | }
67 |
68 | _placeMarker(card) {
69 | const cardCenterOffSetLeft = card.$element[0].offsetWidth / 2 + (card.$element[0].offsetLeft - this.$element[0].offsetLeft);
70 | const $marker = this.$element.find('.marker');
71 | $marker.css({
72 | left: cardCenterOffSetLeft,
73 | });
74 | this._markedCard = card;
75 | }
76 |
77 | _createNewReaderHolder() {
78 | // The reader holder closes and removes itself when its child reader is removed.
79 | const $readerHolder = $('');
80 |
81 | const observer = new MutationObserver(mutations => {
82 | if (mutations[0].removedNodes.length > 0) {
83 | $readerHolder.css({ height: '0' });
84 | }
85 | });
86 | observer.observe($readerHolder[0], { childList: true });
87 |
88 | $readerHolder[0].addEventListener('transitionend', event => {
89 | if (event.propertyName === 'height' && $readerHolder.height() === 0) {
90 | $readerHolder.remove();
91 | }
92 | });
93 |
94 | return $readerHolder;
95 | }
96 |
97 | resize() {
98 | this._markedCard && this.$readerHolder.height() > 0 && this.placeUnder(this._markedCard, true);
99 | }
100 |
101 | _expandReaderHolder() {
102 | this.$readerHolder.css({ height: `${this._config['verticalReader.height']}px`});
103 | }
104 |
105 | updateCardInstances(cardInstances = []) {
106 | this._cardInstances = cardInstances;
107 | }
108 |
109 | open(card) {
110 | this.placeUnder(card);
111 | requestAnimationFrame(() => {
112 | this._expandReaderHolder();
113 | });
114 | }
115 |
116 | close() {
117 | this.$element && this.$element.detach();
118 | }
119 |
120 | updateReaderContent(card, readerContentData) {
121 | card.readerContent.updateData(readerContentData);
122 | this.$element.find('.reader-content-container').html(card.readerContent.render());
123 | }
124 |
125 | placeUnder(card, stayOpened) {
126 | const targetCardPosition = card.$element.position();
127 | const elementsInSameRow = card.$element.nextAll()
128 | .filter((index, ele) => $(ele).position().top === targetCardPosition.top);
129 | const lastElementInRow = elementsInSameRow[elementsInSameRow.length - 1] || card.$element;
130 |
131 | const $nextElement = $(lastElementInRow).next();
132 | const $prevReaderHolder = this.$readerHolder;
133 | if (!$nextElement.is($prevReaderHolder)) {
134 | const $newReaderHolder = this._createNewReaderHolder();
135 | $newReaderHolder.append(this.$element);
136 | if (stayOpened) {
137 | $prevReaderHolder.remove();
138 | this._expandReaderHolder();
139 | }
140 | $(lastElementInRow).after($newReaderHolder);
141 | }
142 | this._placeMarker(card);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/wrappedCardsView/_wrappedCardsView.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | @import '../variables';
6 |
7 | .uncharted-wrapped-cards-view {
8 | height: 100%;
9 | width: 100%;
10 | overflow: auto;
11 | .cards-container {
12 | display: flex;
13 | flex-wrap: wrap;
14 | justify-content: center;
15 | align-content: flex-start;
16 |
17 | .uncharted-cards-card {
18 | order: unset !important;
19 | }
20 |
21 | .dummy-card {
22 | height: 0;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/wrappedCardsView/wrappedCardsView.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 | --}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/components/wrappedCardsView/wrappedCardsView.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import wrappedCardsViewTemplate from './wrappedCardsView.handlebars';
6 | import { IBindable } from '../../util';
7 | import { DEFAULT_CONFIG, EVENTS } from '../constants';
8 | import VerticalReader from '../verticalReader/verticalReader';
9 |
10 | export default class WrappedCardsView extends IBindable {
11 | constructor(spec = {}) { // eslint-disable-line
12 | super();
13 | this.reset(spec);
14 | this._initVerticalReader();
15 | this.postRender = () => {};
16 | }
17 |
18 | reset(spec = {}) {
19 | this._config = Object.assign({}, DEFAULT_CONFIG, spec.config);
20 | this.cardInstances = [];
21 | this.verticalReader && this.verticalReader.reset(spec);
22 | return this;
23 | }
24 |
25 | _initVerticalReader() {
26 | this.verticalReader = new VerticalReader({ config: this._config });
27 | this.verticalReader.on(EVENTS.VERTICAL_READER_NAVIGATE_CARD, card => this.scrollToVerticalReader(card.$element[0].offsetHeight, 0));
28 | this.forward(this.verticalReader);
29 | }
30 |
31 | _registerDomEvents() {
32 | this.$element.on('click', event => this.emit(EVENTS.CARDS_CLICK_BACKGROUND, this, event));
33 | this.$element.on('scroll', event => {
34 | let ticking = false;
35 | if (!ticking) {
36 | requestAnimationFrame(() => {
37 | this.$element[0].scrollTop + this.$element[0].offsetHeight >= this.$element[0].scrollHeight - 1 && this.emit(EVENTS.WRAPPED_CARDS_VIEW_SCROLL_END, this, event.originalEvent);
38 | ticking = false;
39 | });
40 | ticking = true;
41 | }
42 | });
43 | }
44 |
45 | render() {
46 | this.$element = $(wrappedCardsViewTemplate({
47 | cardWidth: this._config['card.width'],
48 | }));
49 | this._$cardsContainer = this.$element.find('.cards-container');
50 | this._$dummyCards = this.$element.find('.dummy-card');
51 | this.verticalReader.render();
52 | this._registerDomEvents();
53 | return this.$element;
54 | }
55 |
56 | clearCards() {
57 | this.cardInstances = [];
58 | return this.render();
59 | }
60 |
61 | resize() {
62 | this.verticalReader.resize();
63 | }
64 |
65 | renderMoreCards(cardInstances) {
66 | const cardFragments = document.createDocumentFragment();
67 | cardInstances.forEach(card => cardFragments.appendChild(card.render()[0]));
68 | this._$cardsContainer.append(cardFragments).append(this._$dummyCards);
69 | this.cardInstances.push(...cardInstances);
70 | this.verticalReader.updateCardInstances(this.cardInstances);
71 | this.postRender();
72 | }
73 |
74 | scrollToVerticalReader(offset = 0, duration = 0) {
75 | const $vReaderHolders = this.$element.find('.uncharted-cards-reader-holder');
76 | const currentReaderHolderOffsetTop = this.verticalReader.$readerHolder[0].offsetTop;
77 | let scrollTop = currentReaderHolderOffsetTop - offset;
78 | if ($vReaderHolders[0].offsetTop < currentReaderHolderOffsetTop) {
79 | scrollTop -= this._config['verticalReader.height'];
80 | }
81 | this.$element.animate({scrollTop: scrollTop}, duration);
82 | }
83 |
84 | openReader(card) {
85 | this.verticalReader.open(card);
86 | this.scrollToVerticalReader(card.$element[0].offsetHeight, this._config.scrollToVerticalReaderDuration);
87 | }
88 |
89 | closeReader() {
90 | this.verticalReader.close();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/handlebarHelper/cardImages.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export default function(imageUrl, block) {
6 | const imagesUrls = imageUrl ? [].concat(imageUrl) : [];
7 | let output = '';
8 | imagesUrls.forEach(url => {
9 | output += block.fn({ url });
10 | });
11 | return output;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/handlebarHelper/isImageDataUrl.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export default function(value, options) {
6 | const fnTrue = options.fn;
7 | const fnFalse = options.inverse;
8 | const supportedImageTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif'];
9 | const isImageDataUrl = supportedImageTypes.some(imageType => value.indexOf && value.indexOf(`data:${imageType};base64`) === 0);
10 | return isImageDataUrl ? fnTrue(this) : fnFalse(this);
11 | }
12 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/handlebarHelper/linkify.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | // import { Utils, SafeString } from 'handlebars/runtime';
6 | import { Utils, SafeString } from 'handlebars';
7 |
8 | function isUrl(url) {
9 | const regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
10 | return regexp.test(url);
11 | }
12 |
13 | function getHostName(url) {
14 | var link = document.createElement('a');
15 | link.href = url;
16 | return link.hostname;
17 | }
18 |
19 | function getLink(href, text) {
20 | return `${text}`;
21 | }
22 |
23 | function isMailTo(text) {
24 | const regexp = /mailto:*/;
25 | return regexp.test(text);
26 | }
27 |
28 | function linkify(text) {
29 | if (isUrl(text)) {
30 | return getLink(text, getHostName(text).replace('www.', ''));
31 | }
32 | if (isMailTo(text)) {
33 | return getLink(text, text.replace('mailto:', ''));
34 | }
35 | return text;
36 | }
37 |
38 | export default function(text) {
39 | const escapedText = Utils.escapeExpression(text);
40 | return new SafeString(linkify(escapedText));
41 | }
42 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import Cards from './components/cards/cards';
6 |
7 | export default Cards;
8 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/main.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | @import 'style/base';
6 | @import 'components/cards/cards';
7 | .uncharted-cards-style {
8 | @import 'components/inlineCardsView/inlineCardsView';
9 | @import 'components/wrappedCardsView/wrappedCardsView';
10 | @import 'components/card/card';
11 | @import 'components/readerContent/readerContent';
12 | @import 'components/verticalReader/verticalReader';
13 | @import 'components/headerImage/headerImage';
14 | }
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/style/_base.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | .uncharted-cards-style {
6 |
7 | -ms-overflow-style: -ms-autohiding-scrollbar;
8 |
9 | ::-webkit-scrollbar {
10 | width: 8px;
11 | height: 8px;
12 | border-radius: 100px;
13 | background-color: rgba(0, 0, 0, 0);
14 | }
15 |
16 | ::-webkit-scrollbar:hover {
17 | background-color: rgba(0, 0, 0, 0.09);
18 | }
19 |
20 | ::-webkit-scrollbar-thumb {
21 | background: rgba(0, 0, 0, 0.2);
22 | border-radius: 100px;
23 | }
24 |
25 | ::-webkit-scrollbar-thumb:hover,
26 | ::-webkit-scrollbar-thumb:active
27 | {
28 | background: rgba(0, 0, 0, 0.5);
29 | }
30 |
31 | box-sizing: border-box;
32 |
33 | *,
34 | *:before,
35 | *:after {
36 | box-sizing: inherit;
37 | }
38 | /* css reset */
39 | div, span, applet, object, iframe,
40 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
41 | a, abbr, acronym, address, big, cite, code,
42 | del, dfn, em, img, ins, kbd, q, s, samp,
43 | small, strike, strong, sub, sup, tt, var,
44 | b, u, i, center,
45 | dl, dt, dd, ol, ul, li,
46 | fieldset, form, label, legend,
47 | table, caption, tbody, tfoot, thead, tr, th, td,
48 | article, aside, canvas, details, embed,
49 | figure, figcaption, footer, header, hgroup,
50 | menu, nav, output, ruby, section, summary,
51 | time, mark, audio, video {
52 | margin: 0;
53 | padding: 0;
54 | border: 0;
55 | font-size: 100%;
56 | vertical-align: baseline;
57 | }
58 |
59 | // HTML5 display-role reset for older browsers
60 | article, aside, details, figcaption, figure,
61 | footer, header, hgroup, menu, nav, section {
62 | display: block;
63 | }
64 | ol, ul {
65 | list-style: none;
66 | }
67 | blockquote, q {
68 | quotes: none;
69 | }
70 | blockquote:before, blockquote:after,
71 | q:before, q:after {
72 | content: '';
73 | content: none;
74 | }
75 | table {
76 | border-collapse: collapse;
77 | border-spacing: 0;
78 | }
79 |
80 | h1,
81 | h2,
82 | h3,
83 | h4,
84 | h5,
85 | h6 {
86 | font-family: inherit;
87 | font-weight: 500;
88 | line-height: 1.25;
89 | color: inherit;
90 | }
91 | img {
92 | border: 0;
93 | vertical-align: middle;
94 | }
95 | ol,
96 | ul {
97 | margin-top: 0;
98 | margin-bottom: 10px;
99 | }
100 | a {
101 | text-decoration: none;
102 | color: #2C83C0;
103 | }
104 |
105 | .meta-line {
106 | list-style: none;
107 | padding: 0;
108 | overflow: hidden;
109 | font-size: 11px;
110 | color: #777;
111 | margin-top: 2px;
112 | }
113 | }
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/util/IBindable.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | const _sourceFunctionKey = Symbol('SourceFunctionKey');
6 | const _sourceOwnerKey = Symbol('SourceOwnerKey');
7 |
8 | /**
9 | * Base interface class for objects that wish to emit events.
10 | *
11 | * @class IBindable
12 | */
13 | export class IBindable {
14 | /**
15 | * @constructor IBindable
16 | */
17 | constructor() {
18 | this.mHandlers = {};
19 | this.mOmniHandlers = [];
20 | this.mBoundForwardEvent = this._forwardEvent.bind(this);
21 | }
22 |
23 | /**
24 | * Unbinds all events bound to this IBindable instance.
25 | *
26 | * @method destroy
27 | */
28 | destroy() {
29 | Object.keys(this.mHandlers).forEach(key => {
30 | delete this.mHandlers[key];
31 | });
32 | this.mOmniHandlers.length = 0;
33 |
34 | delete this.mHandlers;
35 | delete this.mOmniHandlers;
36 | delete this.mBoundForwardEvent;
37 | }
38 |
39 | /**
40 | * Binds a list of events to the specified callback.
41 | *
42 | * @method on
43 | * @param {String|Array|null} events - A space-separated list or an Array of events to listen for. If null is passed, the callback will be invoked for all events.
44 | * @param {Function} callback - The callback to invoke when the event is triggered. If this callback returns true, event bubbling stops.
45 | */
46 | on(events, callback) {
47 | if (events === null) {
48 | if (this.mOmniHandlers.indexOf(callback) < 0) {
49 | this.mOmniHandlers.push(callback);
50 | }
51 | } else {
52 | const eventArray = events instanceof Array ? events : events.split(' ');
53 | eventArray.forEach(event => {
54 | let handlers = this.mHandlers[event];
55 | if (!handlers) {
56 | handlers = [];
57 | this.mHandlers[event] = handlers;
58 | }
59 | if (handlers.indexOf(callback) < 0) {
60 | handlers.push(callback);
61 | }
62 | });
63 | }
64 | }
65 |
66 | /**
67 | * Unbinds the specified callback from the specified event. If no callback is specified, all callbacks for the specified event are removed.
68 | *
69 | * @method off
70 | * @param {String|Array|null=} events - A space-separated list or an Array of events to listen for. If null is passed the callback will be removed from the all-event handler list.
71 | * @param {Function=} callback - The callback to remove from the event or nothing to completely clear the event callbacks.
72 | * @param {*} owner - The owner of the callback, needed when unregistering callbacks created with `safeBind`.
73 | */
74 | off(events = null, callback = null, owner = null) {
75 | if (events === null) {
76 | if (!callback) {
77 | this.mOmniHandlers.length = 0;
78 | } else {
79 | const index = this.mOmniHandlers.indexOf(callback);
80 | if (index >= 0) {
81 | this.mOmniHandlers.splice(index, 1);
82 | }
83 | }
84 | } else {
85 | const eventArray = events instanceof Array ? events : events.split(' ');
86 | eventArray.forEach(event => {
87 | const handlers = this.mHandlers[event];
88 | if (handlers) {
89 | if (!callback) {
90 | delete this.mHandlers[event];
91 | } else {
92 | for (let i = 0, n = handlers.length; i < n; ++i) {
93 | if (callback === handlers[i] || (callback === handlers[i][_sourceFunctionKey] && owner === handlers[i][_sourceOwnerKey])) {
94 | handlers.splice(i, 1);
95 | break;
96 | }
97 | }
98 | }
99 | }
100 | });
101 | }
102 | }
103 |
104 | /**
105 | * Emits the specified event and forwards all passed parameters.
106 | *
107 | * @method emit
108 | * @param {String} event - The name of the event to emit.
109 | * @param {...*} varArgs - Arguments to forward to the event listener callbacks.
110 | */
111 | emit(event, ...varArgs) {
112 | let i;
113 | let n;
114 |
115 | if (this.mHandlers[event]) {
116 | for (i = 0, n = this.mHandlers[event].length; i < n; ++i) {
117 | if (this.mHandlers[event][i](...varArgs) === true) {
118 | break;
119 | }
120 | }
121 | }
122 |
123 | if (this.mOmniHandlers.length > 0) {
124 | for (i = 0, n = this.mOmniHandlers.length; i < n; ++i) {
125 | if (this.mOmniHandlers[i](...arguments) === true) {
126 | break;
127 | }
128 | }
129 | }
130 | }
131 |
132 | /**
133 | * Forwards the specified events triggered by the given `bindable` as if this object was emitting them. If no events
134 | * are passed, all events are forwarded.
135 | *
136 | * @method forward
137 | * @param {IBindable} bindable - The `IBindable` instance for which all events will be forwarded through this instance.
138 | * @param {String|Array|null=} events - A space-separated list of events to forward or null to forward all the events.
139 | */
140 | forward(bindable, events = null) {
141 | if (events === null) {
142 | bindable.on(null, this.mBoundForwardEvent);
143 | } else {
144 | const eventArray = events instanceof Array ? events : events.split(' ');
145 | eventArray.forEach(event => {
146 | bindable.on(event, this.safeBind(this._forwardEvent, this, event));
147 | });
148 | }
149 | }
150 |
151 | /**
152 | * Stops forwarding the events of the specified `bindable`
153 | *
154 | * @method unforward
155 | * @param {IBindable} bindable - The `IBindable` instance to stop forwarding.
156 | * @param {String|Array|null=} events - A space-separated list of events to stop forwarding or null to stop forwarding all the events.
157 | */
158 | unforward(bindable, events = null) {
159 | if (events === null) {
160 | bindable.off(null, this.mBoundForwardEvent);
161 | } else {
162 | const eventArray = events instanceof Array ? events : events.split(' ');
163 | eventArray.forEach(event => {
164 | bindable.off(event, this._forwardEvent, this);
165 | });
166 | }
167 | }
168 |
169 | /**
170 | * Binds a function so it is safe to unregister it using the `off` method with only the original function as the second argument.
171 | *
172 | * @method safeBind
173 | * @param {Function} func - The function to bind.
174 | * @param {*} owner - The owner of this function.
175 | * @param {...*} varArgs - The arguments used to bind this function.
176 | * @returns {Function}
177 | */
178 | safeBind(func, owner, ...varArgs) {
179 | const boundFunction = func.bind(owner, ...varArgs);
180 | boundFunction[_sourceFunctionKey] = func;
181 | boundFunction[_sourceOwnerKey] = owner;
182 | return boundFunction;
183 | }
184 |
185 | /**
186 | * Internal method used to forward the events from other `IBindable` instances.
187 | *
188 | * @method _forwardEvent
189 | * @param {String} event - The name of the event to emit.
190 | * @param {...*} varArgs - Arguments to forward to the event listener callbacks.
191 | * @private
192 | */
193 | _forwardEvent(/* event */ /* varArgs */) {
194 | this.emit.apply(this, arguments);
195 | }
196 | }
197 |
198 | export default IBindable;
199 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/util/images.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | /**
6 | * Hash a string, such as a domain, into one of 256 shades of gray.
7 | * @param {String} str - arbitrary string to hash into a grey shade
8 | * @param {Number=} min - optional lower bound for the grey value
9 | * @param {Number=} max - optional upper bound for the grey value
10 | * @returns {number|*} A shade of grey in the range [min|0, max|255]
11 | * @static
12 | */
13 | export function grayShadeFromString(str, min, max) {
14 | let hash = 0;
15 | for (let i = 0; i < str.length; i++) {
16 | hash = str.charCodeAt(i) + ((hash << 5) - hash);
17 | }
18 |
19 | const color32bit = (hash & 0xFFFFFF);
20 | let r = (color32bit >> 16) & 255;
21 | let g = (color32bit >> 8) & 255;
22 | let b = color32bit & 255;
23 |
24 | /* clamp the colors */
25 | if (min !== undefined) {
26 | r = Math.max(r, min);
27 | g = Math.max(g, min);
28 | b = Math.max(b, min);
29 | }
30 |
31 | if (max !== undefined) {
32 | r = Math.min(r, max);
33 | g = Math.min(g, max);
34 | b = Math.min(b, max);
35 | }
36 |
37 | return Math.floor((r + g + b) / 3);
38 | }
39 |
40 | /**
41 | * Generate a Data URL encoding a grey single-letter icon.
42 | * @param {Number} width - width of the icon in pixels
43 | * @param {Number} height - height of the icon in pixels
44 | * @param {String} sourceName - string to create an icon for;
45 | * the first character becomes the icon's letter and the string as a whole gets hashed into a grey shade
46 | * @returns {string} Data URL encoding an icon image
47 | * @static
48 | */
49 | export function createFallbackIconURL(width, height, sourceName) {
50 | /* get the gray shade for the background */
51 | let channel = grayShadeFromString(sourceName, 0, 102);
52 |
53 | /* initialize an offscreen canvas */
54 | const canvas = document.createElement('canvas');
55 | canvas.width = width;
56 | canvas.height = height;
57 | const context = canvas.getContext('2d');
58 |
59 | /* draw the background */
60 | context.fillStyle = 'rgb(' + channel + ',' + channel + ',' + channel + ')';
61 | context.fillRect(0, 0, width, height);
62 |
63 | /* make the channel brighter for the text */
64 | channel = Math.floor(channel * 2.5);
65 | context.fillStyle = 'rgb(' + channel + ',' + channel + ',' + channel + ')';
66 |
67 | /* draw the text */
68 | const letter = sourceName[0].toUpperCase();
69 | context.font = Math.round(height * 0.7) + 'px helvetica';
70 | context.fontWeight = 'bolder';
71 | context.textAlign = 'center';
72 | context.textBaseline = 'middle';
73 | context.fillText(letter, width * 0.5, height * 0.5);
74 |
75 | return canvas.toDataURL();
76 | }
77 |
78 | /**
79 | * Load one image from the given URL.
80 | * @param {String} url - Address of the image
81 | * @returns {Promise}
82 | */
83 | export function loadImage(url) {
84 | return new Promise(resolve => {
85 | const img = new Image();
86 | img.onload = () => {
87 | resolve(img);
88 | };
89 | img.src = url;
90 | });
91 | }
92 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/util/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export { default as IBindable } from './IBindable';
6 | export * from './scroll';
7 | export * from './images';
8 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/src/util/scroll.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import debounce from 'lodash/debounce';
6 |
7 | const V_TO_H_MOUSE_WHEEL_EVENT = 'wheel.uncharted.verticalToHorizontal';
8 |
9 | export function mapVerticalToHorizontalScroll($element) {
10 | $element.on(V_TO_H_MOUSE_WHEEL_EVENT, event => {
11 | const { deltaX, deltaY } = event.originalEvent;
12 | const delta = Math.abs(deltaX) > Math.abs(deltaY) ? deltaX : deltaY;
13 | $element.scrollLeft($element.scrollLeft() + delta);
14 | event.preventDefault();
15 | });
16 | }
17 |
18 | export function delayOnHorizontalToVerticalScrollingTransition($element, verticalScrollContainerSelector, delay = 1000) {
19 | let canIScrollVertically = true;
20 | const preventFollowingVerticalScrolling = debounce(function () {
21 | canIScrollVertically = true;
22 | }, delay);
23 |
24 | $element.on(V_TO_H_MOUSE_WHEEL_EVENT, () => {
25 | canIScrollVertically = false;
26 | preventFollowingVerticalScrolling();
27 | });
28 | $element.on(V_TO_H_MOUSE_WHEEL_EVENT, verticalScrollContainerSelector, event => {
29 | event.stopPropagation();
30 | !canIScrollVertically && event.preventDefault();
31 | });
32 | }
33 |
34 | export function unMapVerticalToHorizontalScroll($element) {
35 | $element.off(V_TO_H_MOUSE_WHEEL_EVENT);
36 | }
37 |
--------------------------------------------------------------------------------
/lib/@uncharted/cards/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 |
5 | const OUTPUT_FILE_NAME = 'uncharted.cards';
6 |
7 | const isProduction = (process.env.NODE_ENV === 'production'); // eslint-disable-line
8 | const isServing = (process.env.WEBPACK_ENV === 'serve'); // eslint-disable-line
9 | const outputFileName = isProduction ? `${OUTPUT_FILE_NAME}.min` : OUTPUT_FILE_NAME;
10 |
11 | const HANDLEBAR_RUNTIME_PATH = isProduction ? 'handlebars/dist/handlebars.runtime.min' : 'handlebars/runtime';
12 |
13 | /** Base config */
14 | const config = {
15 | entry: ['./src/main.scss', 'babel-polyfill', './src/index.js'],
16 | output: {
17 | filename: `${outputFileName}.js`,
18 | path: path.resolve(__dirname, 'dist'),
19 | },
20 | resolve: {
21 | alias: {
22 | handlebars: HANDLEBAR_RUNTIME_PATH,
23 | },
24 | },
25 | module: {
26 | rules: [
27 | {
28 | enforce: 'pre',
29 | test: /\.js$/,
30 | exclude: /node_modules/,
31 | loader: 'eslint-loader',
32 | },
33 | {
34 | test: /\.js$/,
35 | loader: 'babel-loader',
36 | options: {
37 | presets: [
38 | ['latest', { es2015: { modules: false } }],
39 | ],
40 | },
41 | exclude: /node_modules/,
42 | },
43 | {
44 | test: /\.scss$|\.css$/,
45 | use: [
46 | {
47 | loader: "style-loader" // creates style nodes from JS strings
48 | },
49 | {
50 | loader: "css-loader" // translates CSS into CommonJS
51 | },
52 | {
53 | loader: "sass-loader" // compiles Sass to CSS
54 | }
55 | ]
56 | },
57 | {
58 | test: /\.handlebars$/,
59 | loader: 'handlebars-loader',
60 |
61 | query: {
62 | helperDirs: [
63 | path.resolve(__dirname, 'src/handlebarHelper'),
64 | ],
65 | runtime: HANDLEBAR_RUNTIME_PATH,
66 | },
67 | },
68 | ],
69 | },
70 |
71 | externals: {
72 | jquery: 'jQuery',
73 | },
74 |
75 | plugins: [
76 | new webpack.optimize.ModuleConcatenationPlugin(),
77 | ],
78 | devServer: {
79 | disableHostCheck: true,
80 | },
81 | };
82 |
83 | if (!isServing) {
84 | config.plugins.push(...[
85 | new CleanWebpackPlugin(['dist']),
86 | ]);
87 | }
88 |
89 | // Environment specific configs
90 | if (isProduction) {
91 | config.devtool = 'source-map';
92 | config.plugins.push(...[
93 | new webpack.DefinePlugin({
94 | 'process.env': {
95 | 'NODE_ENV': JSON.stringify('production'),
96 | },
97 | }),
98 | ]);
99 |
100 | config.mode = 'production';
101 | } else {
102 | config.output.libraryTarget = 'window';
103 | config.output.library = ['Uncharted', 'Cards'];
104 | config.devtool = 'inline-source-map';
105 | config.plugins.push(...[
106 | new webpack.DefinePlugin({
107 | 'process.env': {
108 | 'NODE_ENV': JSON.stringify('dev'),
109 | },
110 | }),
111 | ]);
112 |
113 | config.mode = 'development';
114 | }
115 |
116 | module.exports = config;
117 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "powerbi-visuals-cardbrowser",
3 | "version": "2.0.1",
4 | "description": "Card Browser, a custom visual for PowerBI",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "install-certificate": "node bin/openCert",
8 | "install-private-submodule": "node bin/installPrivateSubmodules",
9 | "start": "yarn run dev",
10 | "tdd": "npm-run-all --parallel dev test:watch",
11 | "dev": "node bin/startDev",
12 | "package": "node bin/packageVisual",
13 | "test": "jest --coverage",
14 | "test:watch": "jest --watch",
15 | "lint": "tslint --project ./tsconfig.json src/**/*.ts",
16 | "clean": "rm -rf node_modules .tmp"
17 | },
18 | "config": {},
19 | "keywords": [],
20 | "author": "Microsoft",
21 | "license": "MIT",
22 | "privacyTerms": "https://privacy.microsoft.com/en-US/privacystatement/",
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/Microsoft/PowerBI-visuals-CardBrowser"
26 | },
27 | "privateSubmodules": {
28 | "@uncharted/cards": "0.13.9"
29 | },
30 | "devDependencies": {
31 | "@types/jest": "^24.0.13",
32 | "@types/jquery": "^3.3.29",
33 | "@types/lodash-es": "^4.17.0",
34 | "@types/node": "^9.4.1",
35 | "babel-core": "^6.26.0",
36 | "babel-loader": "^7.1.2",
37 | "babel-polyfill": "^6.26.0",
38 | "babel-preset-env": "^1.6.1",
39 | "chai": "^4.1.2",
40 | "chokidar": "^2.1.2",
41 | "clean-css": "^4.1.9",
42 | "connect": "^3.6.5",
43 | "eslint": "^5.15.1",
44 | "eslint-loader": "^2.1.2",
45 | "fs-extra": "^7.0.0",
46 | "handlebars": "^4.3.0",
47 | "handlebars-loader": "^1.7.0",
48 | "jest": "^24.8.0",
49 | "jquery": "^3.5.0",
50 | "json": "^9.0.6",
51 | "memory-fs": "^0.4.1",
52 | "mkdirp": "^0.5.1",
53 | "mv": "^2.1.1",
54 | "node-sass": "^4.7.2",
55 | "node-zip": "^1.1.1",
56 | "npm-run-all": "^4.1.5",
57 | "onchange": "^6.0.0",
58 | "serve-static": "^1.13.2",
59 | "sinon": "^7.2.7",
60 | "sinon-chai": "^3.3.0",
61 | "targz": "^1.0.1",
62 | "ts-jest": "^24.0.2",
63 | "ts-loader": "^3.5.0",
64 | "tslint": "^5.9.1",
65 | "tslint-loader": "^3.5.3",
66 | "tslint-microsoft-contrib": "^5.0.3",
67 | "typescript": "^2.7.2",
68 | "upath": "^1.1.1",
69 | "webpack": "^3.12.0"
70 | },
71 | "dependencies": {
72 | "@fortawesome/fontawesome-free": "^5.8.2",
73 | "@uncharted/cards": "./lib/@uncharted/cards",
74 | "lodash": "^4.17.19",
75 | "lodash-es": "^4.17.14",
76 | "moment": "^2.24.0"
77 | },
78 | "engines": {
79 | "node": ">=8"
80 | },
81 | "jest": {
82 | "transform": {
83 | "^.+\\.(j|t)sx?$": "ts-jest"
84 | },
85 | "transformIgnorePatterns": [
86 | "/node_modules/(?!lodash-es/.*)"
87 | ],
88 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
89 | "moduleFileExtensions": [
90 | "ts",
91 | "tsx",
92 | "js",
93 | "jsx",
94 | "json"
95 | ],
96 | "testURL": "http://localhost/"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/pbiviz.json:
--------------------------------------------------------------------------------
1 | {
2 | "visual": {
3 | "name": "CardBrowser",
4 | "displayName": "Card Browser",
5 | "guid": "CardBrowser8D7CFFDA2E7E400C9474F41B9EDBBA58",
6 | "visualClassName": "CardBrowserVisual",
7 | "description": "ESSEX Card Browser",
8 | "supportUrl": "http://community.powerbi.com",
9 | "gitHubUrl": "https://github.com/Microsoft/PowerBI-visuals-CardBrowser"
10 | },
11 | "author": {
12 | "name": "Microsoft",
13 | "email": "msrvizsupport@microsoft.com"
14 | },
15 | "apiVersion": "2.6.0",
16 | "assets": {
17 | "icon": "assets/icon.svg",
18 | "thumbnail": "assets/thumbnail.png",
19 | "screenshot": "assets/1-preview.png"
20 | },
21 | "style": "style/visual.scss",
22 | "capabilities": "capabilities.json",
23 | "output": "dist/essex.cardbrowser.pbiviz"
24 | }
25 |
--------------------------------------------------------------------------------
/src/VisualMain.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | window['powerbi'] = {
6 | visuals: {
7 | }
8 | };
9 |
10 | import * as $ from 'jquery';
11 | import populateData from './test_data/testDataUtils';
12 | import colors from './test_data/colors';
13 | import CardBrowser from './VisualMain';
14 |
15 | jest.mock('../lib/@uncharted/cards/src/index', () => {
16 | // typescript creates a named export called "default". So your mock needs to be an object with a default key
17 | // reference https://github.com/kulshekhar/ts-jest/issues/120
18 | return {
19 | 'default': jest.fn().mockImplementation(() => {
20 | return {
21 | on: jest.fn(),
22 | render: jest.fn(),
23 | };
24 | })
25 | };
26 | });
27 | jest.mock('../lib/@uncharted/cards/src/components/constants', () => {
28 | return {
29 | EVENTS: {}
30 | };
31 | });
32 | jest.mock('./visual.handlebars', () => () => 'visualTemplate
');
33 | jest.mock('./loader.handlebars', () => () => 'loaderTamplate
');
34 |
35 | describe('Card Browser Visual', () => {
36 | let visual;
37 |
38 | beforeAll(function () {
39 | const element = $('');
40 | const dummyHost = {
41 | createSelectionManager: () => ({
42 | hostServices: 'hostService',
43 | registerOnSelectCallback: () => {}
44 | }),
45 | colors: colors,
46 | };
47 | visual = new CardBrowser({
48 | element: element[0],
49 | host: dummyHost,
50 | });
51 | });
52 |
53 | it('exists', () => {
54 | expect(CardBrowser).toBeTruthy();
55 | expect(visual).toBeTruthy();
56 | });
57 |
58 | it('update', () => {
59 | const options = populateData([]);
60 | visual.update(options);
61 | });
62 |
63 | it('enumerateObjectInstances', () => {
64 | const options = {
65 | objectName: 'presentation',
66 | };
67 | const instances = visual.enumerateObjectInstances(options);
68 | expect(instances).toBeTruthy();
69 | expect(instances.length).toBe(1);
70 | const instanceProperties = instances[0].properties;
71 | expect(instanceProperties.shadow).toBe(true);
72 | expect(instanceProperties.fade).toBe(true);
73 | expect(instanceProperties.dateFormat).toBe('MMM D, YYYY');
74 | expect(instanceProperties.separator).toBe(' \u2022 ');
75 | expect(instanceProperties.separator).toBe(' \u2022 ');
76 | expect(instanceProperties.cardWidth).toBe(200);
77 | });
78 |
79 | it('destroy', () => {
80 | visual.destroy();
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 | const packageJSON = require("../package.json");
5 | const isDev = process.env.NODE_ENV !== 'production';
6 |
7 | export const METADATA_FIELDS = ['metadata'];
8 | export const REQUIRED_FIELDS = ['id'];
9 | export const SUMMARY_FIELD = 'summary';
10 | export const CONTENT_FIELD = 'content';
11 | export const IMAGE_FIELD = 'imageUrl';
12 | export const CARD_FACE_METADATA = 'metadata';
13 | export const CARD_FACE_PREVIEW = 'preview';
14 |
15 | /**
16 | * White list of HTML tags allowed in either the content or summary
17 | * @type {string[]}
18 | */
19 | export const HTML_WHITELIST_STANDARD = [
20 | 'A', 'ABBR', 'ACRONYM', 'ADDRESS', 'AREA', 'ARTICLE', 'ASIDE',
21 | 'B', 'BDI', 'BDO', 'BLOCKQUOTE', 'BR',
22 | 'CAPTION', 'CITE', 'CODE', 'COL', 'COLGROUP',
23 | 'DD', 'DEL', 'DETAILS', 'DFN', 'DIV', 'DL', 'DT',
24 | 'EM',
25 | 'FIGCAPTION', 'FIGURE', 'FONT', 'FOOTER',
26 | 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HGROUP', 'HR', 'HTML',
27 | 'I', 'INS',
28 | 'LEGEND', 'LI', 'LINK',
29 | 'MAIN', 'MAP',
30 | // We probably don't want navigation, but it's also probably mostly harmless
31 | // 'NAV',
32 | 'OL',
33 | 'P', 'PRE',
34 | 'SECTION', 'SMALL', 'SOURCE', 'SPAN', 'STRONG', 'STYLE', 'SUB', 'SUMMARY', 'SUP',
35 | 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TIME', 'TR',
36 | 'U', 'UL',
37 | 'VAR',
38 | ];
39 |
40 | /**
41 | * White list of HTML tags, for media, which are allowed only in the content
42 | * @type {string[]}
43 | */
44 | export const HTML_WHITELIST_MEDIA = [
45 | 'IMG',
46 | 'PICTURE',
47 | 'SVG',
48 | 'VIDEO'
49 | ];
50 |
51 | export const HTML_WHITELIST_SUMMARY = HTML_WHITELIST_STANDARD;
52 | export const HTML_WHITELIST_CONTENT = HTML_WHITELIST_STANDARD.concat(HTML_WHITELIST_MEDIA);
53 | export const WRAP_HEIGHT_FACTOR = 1.25;
54 | export const FLIP_ANIMATION_DURATION = 317; // 300 ms from CSS plus one frame
55 | export const INFINITE_SCROLL_DELAY = 50;
56 | export const MIN_CARD_WIDTH = 11;
57 |
58 | /**
59 | * Default visual settings
60 | */
61 | export const DEFAULT_VISUAL_SETTINGS = {
62 | presentation: {
63 | shadow: true,
64 | fade: true,
65 | dateFormat: 'MMM D, YYYY',
66 | separator: ' \u2022 ',
67 | showImageOnBack: true,
68 | cardWidth: 200,
69 | cardHeight: 300,
70 | filter: true,
71 | cropImages: true,
72 | },
73 | reader: {
74 | headerBackgroundColor: {
75 | solid: {
76 | color: '#373737',
77 | }
78 | },
79 | headerTextColor: {
80 | solid: {
81 | color: '#fff',
82 | }
83 | },
84 | width: 520,
85 | height: 500,
86 | },
87 | metadata: {
88 | fontSize: 10,
89 | titleColor: {
90 | solid: {
91 | color: '#bbb',
92 | }
93 | },
94 | valueColor: {
95 | solid: {
96 | color: '#000',
97 | }
98 | },
99 | titleFontFamily: "Default",
100 | valueFontFamily: "Default"
101 | },
102 | flipState: {
103 | show: true,
104 | cardFaceDefault: CARD_FACE_PREVIEW,
105 | },
106 | loadMoreData: {
107 | show: false,
108 | limit: 500
109 | },
110 | general: {
111 | version: `${packageJSON.version}`
112 | }
113 | };
114 |
--------------------------------------------------------------------------------
/src/dataConversion.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 | ///
5 |
6 | import DataView = powerbi.DataView;
7 | import IVisualHost = powerbi.extensibility.IVisualHost;
8 |
9 | import * as utils from './utils';
10 | import * as moment from 'moment';
11 |
12 | import { HTML_WHITELIST_SUMMARY, HTML_WHITELIST_CONTENT } from './constants';
13 | import { HashMap, CardBrowserDocument, CardBrowserDocumentData } from './types';
14 |
15 | function flattenMetaData(metaData) {
16 | const metaDataArray = Array.isArray(metaData) ? metaData : [metaData];
17 | const metaDataObject = {};
18 | for (let i = 0; i < metaDataArray.length; i++) {
19 | metaDataObject[metaDataArray[i].key] = metaDataArray[i].value;
20 | }
21 | return metaDataObject;
22 | }
23 |
24 | function createSelectionId(i, dataView: DataView, host) {
25 | const category = dataView.categorical.categories.find(n => n.source.roles.id);
26 | return host.createSelectionIdBuilder()
27 | .withCategory(category, i)
28 | .withMeasure(dataView.metadata.columns.find((col) => col.roles.id).queryName)
29 | .createSelectionId();
30 | }
31 |
32 | function convertToDocuments(rowObjs: CardBrowserDocument[], dataView, host): CardBrowserDocumentData {
33 | const documents: HashMap = {};
34 | const documentList: CardBrowserDocument[] = [];
35 | let obj: CardBrowserDocument;
36 | let docId: string | number;
37 | let i: number;
38 | const rowCount = rowObjs.length;
39 |
40 | for (i = 0; i < rowCount; i++) {
41 | obj = rowObjs[i];
42 | docId = obj.id;
43 | if (docId !== null && !documents[docId]) {
44 | obj.selectionId = createSelectionId(i, dataView, host);
45 | documents[docId] = obj;
46 | documentList.push(documents[docId]);
47 | }
48 | }
49 |
50 | return { documents, documentList };
51 | }
52 |
53 | function assignRole(rowObj, role, columnValue, roles, idx) {
54 | if (roles && roles.ordering) {
55 | const roleOrdering = roles.ordering[role];
56 | const index = roleOrdering.indexOf(idx);
57 | if (index < 0 || rowObj[role][index] !== undefined) {
58 | // TODO: fix the bug that causes this to happen
59 | rowObj[role].push(columnValue);
60 | }
61 | else {
62 | rowObj[role][index] = columnValue;
63 | }
64 | }
65 | else {
66 | rowObj[role].push(columnValue);
67 | }
68 | }
69 |
70 | function assignValue(role, displayName, columnValue) {
71 | switch (role) {
72 | case 'metadata':
73 | return {
74 | key: displayName,
75 | value: columnValue,
76 | };
77 | case 'summary':
78 | return utils.sanitizeHTML(columnValue, HTML_WHITELIST_SUMMARY);
79 | default:
80 | return columnValue;
81 | }
82 | }
83 |
84 | function convertToRowObjs(dataView: DataView, settings, roles = null): CardBrowserDocument[] {
85 | const result = [];
86 | // const columns = dataView.metadata.columns;
87 | let rowObj: any;
88 | let firstRoleIndexMap = [];
89 | const categorical =
90 | dataView &&
91 | dataView.categorical;
92 | const categories =
93 | categorical &&
94 | dataView.categorical.categories;
95 | const columnValues =
96 | categorical &&
97 | dataView.categorical.values;
98 | function parseColumn(column: powerbi.DataViewMetadataColumn, colValue: any, colIdx: number) {
99 | const colRoles = Object.keys(column.roles);
100 | const columnValue = colValue && (column.type.dateTime ?
101 | moment(colValue as any).format(settings.presentation.dateFormat) : colValue);
102 | colRoles.forEach((role) => {
103 | if (rowObj[role] === undefined) {
104 | rowObj[role] = assignValue(role, column.displayName, columnValue);
105 | firstRoleIndexMap[role] = colIdx;
106 | return;
107 | }
108 | if (!Array.isArray(rowObj[role])) {
109 | const firstRoleValue = rowObj[role];
110 | rowObj[role] = [];
111 | assignRole(rowObj, role, firstRoleValue, roles, firstRoleIndexMap[role]);
112 | }
113 | assignRole(rowObj, role, assignValue(role, column.displayName, columnValue), roles, colIdx);
114 | });
115 | }
116 | if (categories && categories.length > 0 && categories[0].values && categories[0].values.length > 0) {
117 |
118 | // We can get duplicated categories if the user passes in the same column for multiple fields
119 | const uniqueCategories = dedupeCategories(categories);
120 | const idValues = categories[0].values;
121 | for (let rowIdx = 0; rowIdx < idValues.length; rowIdx++) {
122 | rowObj = {
123 | index: rowIdx,
124 | };
125 |
126 | for (const cat of uniqueCategories) {
127 | parseColumn(cat.source, cat.values[rowIdx], 0);
128 | }
129 |
130 | if (columnValues && columnValues.length > 0) {
131 | columnValues.forEach((valueCol, colIdx) => {
132 | const column = valueCol.source;
133 | const colValue = valueCol.values[rowIdx];
134 | parseColumn(column, colValue, colIdx + 1);
135 | });
136 | }
137 |
138 | if (rowObj.metadata) {
139 | rowObj.metadata = flattenMetaData(rowObj.metadata);
140 | }
141 |
142 | if (rowObj.subtitle) {
143 | if (Array.isArray(rowObj.subtitle)) {
144 | rowObj.subtitle = rowObj.subtitle.filter(item => item);
145 | }
146 | else {
147 | rowObj.subtitle = [rowObj.subtitle];
148 | }
149 | }
150 |
151 | if (rowObj.imageUrl && Array.isArray(rowObj.imageUrl)) {
152 | const cleanArray = [];
153 | for (let i = 0; i < rowObj.imageUrl.length; i++) {
154 | if (rowObj.imageUrl[i]) {
155 | cleanArray.push(rowObj.imageUrl[i]);
156 | }
157 | }
158 | rowObj.imageUrl = cleanArray;
159 | }
160 |
161 | if (rowObj.content) {
162 | rowObj.content = utils.sanitizeHTML(rowObj.content, HTML_WHITELIST_CONTENT);
163 | if (!rowObj.summary) {
164 | rowObj.summary = utils.sanitizeHTML(rowObj.content, HTML_WHITELIST_SUMMARY);
165 | }
166 | }
167 |
168 | if (rowObj.title && Array.isArray(rowObj.title)) {
169 | rowObj.title = rowObj.title.join(' ');
170 | }
171 |
172 | result.push(rowObj);
173 | }
174 | }
175 | return result;
176 | }
177 |
178 | function dedupeCategories(categories: powerbi.DataViewCategoryColumn[]) {
179 | const uniqueCategoryMap = categories.reduce((map, category, i) => {
180 | map[category.source.queryName || `category_${i}`] = category;
181 | return map;
182 | }, {});
183 | return Object.keys(uniqueCategoryMap).map(n => uniqueCategoryMap[n]);
184 | }
185 |
186 | function convertToDocumentData(dataView: DataView, settings, roles, host: IVisualHost) {
187 | const rowObjs = convertToRowObjs(dataView, settings, roles);
188 | return convertToDocuments(rowObjs, dataView, host);
189 | }
190 |
191 | function countDocuments(dataView: DataView) {
192 | const categories =
193 | dataView &&
194 | dataView.categorical &&
195 | dataView.categorical.categories;
196 | if (!categories || categories.length === 0) {
197 | return 0;
198 | }
199 |
200 | const idCat = categories.find(n => n.source.roles.id);
201 | if (idCat && idCat.values) {
202 | return Object.keys(idCat.values.reduce((map, item) => {
203 | map[`${item}`] = true;
204 | return map;
205 | }, {})).length;
206 | } else {
207 | return 1;
208 | }
209 | }
210 |
211 | export {
212 | convertToDocumentData,
213 | countDocuments,
214 | };
215 |
--------------------------------------------------------------------------------
/src/loader.handlebars:
--------------------------------------------------------------------------------
1 | {{!--
2 | /*
3 | * Copyright 2018 Uncharted Software Inc.
4 | */
5 |
6 | --}}
7 | Loading ...
--------------------------------------------------------------------------------
/src/sandboxPolyfill.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | // If, and only if, we are sandboxed, load babel-polyfill
6 | if (window.parent !== window) {
7 | "use strict";
8 |
9 | require("core-js/shim");
10 |
11 | require("regenerator-runtime/runtime");
12 |
13 | require("core-js/fn/regexp/escape");
14 |
15 | if (global._babelPolyfill) {
16 | throw new Error("only one instance of babel-polyfill is allowed");
17 | }
18 | global._babelPolyfill = true;
19 |
20 | var DEFINE_PROPERTY = "defineProperty";
21 | function define(O, key, value) {
22 | O[key] || Object[DEFINE_PROPERTY](O, key, {
23 | writable: true,
24 | configurable: true,
25 | value: value
26 | });
27 | }
28 |
29 | define(String.prototype, "padLeft", "".padStart);
30 | define(String.prototype, "padRight", "".padEnd);
31 |
32 | "pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach(function (key) {
33 | [][key] && define(Array, key, Function.call.bind([][key]));
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/test_data/table.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export default {
6 | "rows": [
7 | [
8 | 4423,
9 | "Comments",
10 | 0,
11 | "Now is the time",
12 | 2935,
13 | 5,
14 | "2017-01-01 20:43:39"
15 | ],
16 | [
17 | 4423,
18 | "Comments",
19 | 2,
20 | "From who manufactured the narrative.",
21 | 10019,
22 | 4,
23 | "2017-01-01 20:43:39"
24 | ],
25 | [
26 | 4423,
27 | "Comments",
28 | 4,
29 | "demonstrates that students",
30 | 9415,
31 | 2,
32 | "2017-01-01 20:43:39"
33 | ],
34 | [
35 | 15223,
36 | "Free Market",
37 | 0,
38 | "Free Market for Education",
39 | 8883,
40 | 2,
41 | "2017-01-02 01:57:41"
42 | ],
43 | [
44 | 211289,
45 | "Democrats",
46 | 0,
47 | "Washington",
48 | 1272,
49 | 64,
50 | "2017-01-02 05:45:33"
51 | ],
52 | [
53 | 211289,
54 | "Democrats",
55 | 2,
56 | "WASHINGTON",
57 | 5787,
58 | 50,
59 | "2017-01-02 05:45:33"
60 | ],
61 | [
62 | 211289,
63 | "Democrats",
64 | 4,
65 | "Such delays",
66 | 4476,
67 | 41,
68 | "2017-01-02 05:45:33"
69 | ],
70 | [
71 | 211289,
72 | "Democrats",
73 | 6,
74 | "Incoming",
75 | 4475,
76 | 43,
77 | "2017-01-02 05:45:33"
78 | ],
79 | [
80 | 211289,
81 | "Democrats",
82 | 8,
83 | "the focus of Democratic attacks",
84 | 4479,
85 | 43,
86 | "2017-01-02 05:45:33"
87 | ],
88 | [
89 | 211289,
90 | "Democrats",
91 | 10,
92 | "President-elect nominees have made billions off the industries they'd be tasked with regulating",
93 | 7778,
94 | 46,
95 | "2017-01-02 05:45:33"
96 | ],
97 | [
98 | 211696,
99 | "Sheltering",
100 | 0,
101 | "Sheltering in place",
102 | 13478,
103 | 2,
104 | "2017-01-01 23:49:00"
105 | ]
106 | ]
107 | };
--------------------------------------------------------------------------------
/src/test_data/testDataUtils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | import cloneDeep from 'lodash-es/cloneDeep';
6 | import mockDataView from './mockdataview';
7 | import table from './table';
8 |
9 | // pbi wraps the categories with a "wrapCtor" that has the actual data accessors
10 | function wrapCtor(category, values) {
11 |
12 | this.source = category.source;
13 | this.identity = category.values.map(v => 'fakeId' + v);
14 | this.identityFields = [];
15 | this.values = values || [];
16 | }
17 |
18 | export default function populateData(data, highlights = null) {
19 | const options = cloneDeep(mockDataView);
20 |
21 | let dataView = options.dataViews[0];
22 |
23 | dataView.categorical.categories = dataView.categorical.categories.map(function (category, index) {
24 | return new wrapCtor(category, data && data[index]);
25 | });
26 |
27 | if (highlights) {
28 | dataView.categorical.values[0]['highlights'] = highlights;
29 | }
30 |
31 | dataView.table = table;
32 | return options;
33 | }
--------------------------------------------------------------------------------
/src/test_data/testHtmlStrings.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | */
4 |
5 | export default {
6 | testArticle: ' Lorem ipsumLorem ipsum dolor sit amet, consectetur adipiscing elit. Nam congue erat nulla, at lobortis velit efficitur eget. Pellentesque sit amet ante mattis, dignissim nisi et, efficitur nisi. Nunc vitae sapien eget arcu egestas viverra eu vitae metus. Cras et tincidunt nunc. Suspendisse vitae feugiat justo, sed malesuada est. Morbi enim leo, euismod porttitor risus nec, auctor pellentesque leo. Mauris volutpat commodo nisi eu rutrum. Etiam molestie congue nibh id rhoncus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas ut dolor posuere, tempor dolor nec, mattis ex. Pellentesque lobortis leo ac eros sagittis, ac commodo velit feugiat. Sed lectus nulla, suscipit et faucibus et, congue a tellus. Fusce suscipit odio dui. Maecenas blandit est a mauris interdum, id pellentesque velit feugiat. Morbi non volutpat magna. In vitae ipsum eget nibh fringilla auctor vitae rutrum lorem. <\/html>\n'
7 | };
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | export interface CardBrowserDocument {
4 | id: string | number;
5 | selectionId: powerbi.visuals.ISelectionId;
6 | }
7 |
8 | export interface HashMap {
9 | [key: string]: T;
10 | }
11 |
12 | export interface CardBrowserDocumentData {
13 | documents: HashMap;
14 | documentList: CardBrowserDocument[];
15 | }
--------------------------------------------------------------------------------
/src/utils.spec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Uncharted Software Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn;
18 |
19 | import * as $ from 'jquery';
20 | import * as utils from './utils';
21 | import mockDataView from './test_data/mockdataview';
22 | import { HTML_WHITELIST_CONTENT } from './constants';
23 | import testHtmlStrings from './test_data/testHtmlStrings.js';
24 | import cloneDeep from 'lodash-es/cloneDeep';
25 |
26 | describe('utils', () => {
27 | it('findColumn', () => {
28 | let options = cloneDeep(mockDataView);
29 | let dataView = options.dataViews[0];
30 | const result = utils.findColumn(dataView, 'document', false);
31 | expect(result).toEqual({
32 | "roles": {
33 | "id": true,
34 | "document": true
35 | },
36 | "type": {
37 | "underlyingType": 260,
38 | "category": null
39 | },
40 | "displayName": "documentID",
41 | "queryName": "betsydevos_lsh_strippets_browser.documentID",
42 | "expr": {
43 | "_kind": 2,
44 | "source": {
45 | "_kind": 0,
46 | "entity": "betsydevos_lsh_strippets_browser"
47 | },
48 | "ref": "documentID"
49 | }
50 | });
51 | });
52 |
53 | it('hasColumns', () => {
54 | let options = cloneDeep(mockDataView);
55 | let dataView = options.dataViews[0];
56 | expect(utils.hasColumns(dataView, ['document'])).toBe(true);
57 | });
58 |
59 | it('hasRole', () => {
60 | const column = mockDataView.dataViews[0].metadata.columns[0];
61 | expect(utils.hasRole(column, 'title')).toBe(false);
62 | expect(utils.hasRole(column, 'id')).toBe(true);
63 | expect(utils.hasRole(column, 'document')).toBe(true);
64 | });
65 |
66 | it('removeScriptAttributes', () => {
67 | const element = $('')[0];
68 | expect([].find.call(element.attributes, (element, index, array) => element.nodeName === 'onerror')).toBeTruthy();
69 | utils.removeScriptAttributes(element);
70 | expect([].find.call(element.attributes, (element, index, array) => element.nodeName === 'onerror')).toBeUndefined();
71 | });
72 |
73 | it('sanitizes HTML', function () {
74 | const sanitized = utils.sanitizeHTML(testHtmlStrings.testArticle, HTML_WHITELIST_CONTENT);
75 | expect(sanitized).toBeTruthy();
76 | expect(sanitized.indexOf('