@cdc/editor 1.4.0 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cdceditor.js +734 -0
  2. package/example/data-horizontal-filters.json +9 -0
  3. package/example/data-horizontal-multiseries-filters.json +20 -0
  4. package/example/data-horizontal-multiseries.json +7 -0
  5. package/example/data-horizontal.json +5 -0
  6. package/example/data-vertical-filters.json +11 -0
  7. package/example/data-vertical-multiseries-filters.json +20 -0
  8. package/example/data-vertical-multiseries-multirow-filters.json +53 -0
  9. package/example/data-vertical-multiseries-multirow.json +14 -0
  10. package/example/data-vertical-multiseries.json +7 -0
  11. package/example/data-vertical.json +7 -0
  12. package/example/region-map.json +33 -0
  13. package/example/valid-county-data.csv +3048 -0
  14. package/example/valid-county-data.json +3049 -0
  15. package/example/valid-data-chart.csv +6 -0
  16. package/example/valid-data-map.csv +59 -0
  17. package/package.json +14 -12
  18. package/src/CdcEditor.js +117 -0
  19. package/src/assets/icons/dashboard.svg +8 -0
  20. package/src/assets/icons/file-upload-solid.svg +1 -0
  21. package/src/assets/icons/globe-asia-solid.svg +1 -0
  22. package/src/assets/icons/link.svg +1 -0
  23. package/src/assets/icons/upload-solid.svg +1 -0
  24. package/src/components/ChooseTab.js +103 -0
  25. package/src/components/ConfigureTab.js +60 -0
  26. package/src/components/DataImport.js +601 -0
  27. package/src/components/PreviewDataTable.js +266 -0
  28. package/src/components/TabPane.js +5 -0
  29. package/src/components/Tabs.js +62 -0
  30. package/src/components/modal/Confirmation.js +14 -0
  31. package/src/components/modal/Modal.js +51 -0
  32. package/src/components/modal/UseModal.js +10 -0
  33. package/src/context.js +7 -0
  34. package/src/index.html +22 -0
  35. package/src/index.js +17 -0
  36. package/src/scss/_data-table.scss +15 -0
  37. package/src/scss/_variables.scss +27 -0
  38. package/src/scss/choose-vis-tab.scss +70 -0
  39. package/src/scss/configure-tab.scss +19 -0
  40. package/src/scss/data-import.scss +212 -0
  41. package/src/scss/main.scss +166 -0
  42. package/LICENSE +0 -201
@@ -0,0 +1,266 @@
1
+ import React, { useState, useContext, useMemo, useCallback, useEffect, memo } from 'react';
2
+ import {
3
+ useTable,
4
+ useBlockLayout,
5
+ useGlobalFilter,
6
+ useSortBy,
7
+ useResizeColumns,
8
+ usePagination
9
+ } from 'react-table';
10
+ import GlobalState from '../context';
11
+ import { useDebounce } from 'use-debounce';
12
+
13
+ // Core
14
+ import validateFipsCodeLength from '@cdc/core/helpers/validateFipsCodeLength';
15
+
16
+ const TableFilter = memo(({globalFilter, setGlobalFilter, disabled = false}) => {
17
+ const [filterValue, setFilterValue ] = useState(globalFilter);
18
+
19
+ const [ debouncedValue ] = useDebounce(filterValue, 200);
20
+
21
+ useEffect(() => {
22
+ if('string' === typeof debouncedValue && debouncedValue !== globalFilter ) {
23
+ setGlobalFilter(debouncedValue ?? '')
24
+ }
25
+ }, [debouncedValue])
26
+
27
+ const onChange = (e) => {
28
+ setFilterValue(e.target.value);
29
+ }
30
+
31
+ return (
32
+ <input
33
+ className="filter"
34
+ value={filterValue}
35
+ onChange={onChange}
36
+ type="search"
37
+ placeholder='Filter...'
38
+ disabled={disabled}
39
+ />
40
+ )
41
+ });
42
+
43
+ const Header = memo(({ globalFilter, data, setGlobalFilter}) => (
44
+ <header className="data-table-header">
45
+ <h2>Data Preview</h2>
46
+ <TableFilter globalFilter={globalFilter || ''} setGlobalFilter={setGlobalFilter} disabled={null === data} />
47
+ </header>
48
+ ))
49
+
50
+ const Footer = memo(({previousPage, nextPage, canPreviousPage, canNextPage, pageNumber, totalPages}) => (
51
+ <footer className="data-table-pagination">
52
+ <ul>
53
+ <li>
54
+ <button onClick={() => previousPage()} className="btn btn-prev" disabled={!canPreviousPage} title="Previous Page"></button>
55
+ </li>
56
+ <li>
57
+ <button onClick={() => nextPage()} className="btn btn-next" disabled={!canNextPage} title="Next Page"></button>
58
+ </li>
59
+ </ul>
60
+ <span>
61
+ Page{' '} {pageNumber} of {totalPages}
62
+ </span>
63
+ </footer>
64
+ ))
65
+
66
+ const PreviewDataTable = ({ data }) => {
67
+ const [tableData, setTableData] = useState(data ?? []);
68
+ const {setErrors, errorMessages, config} = useContext(GlobalState);
69
+
70
+ const tableColumns = useMemo(() => {
71
+ const columns = tableData.columns ?? [];
72
+ if ( columns.length > 0 && columns.includes("") ) {
73
+ // todo find a way to call the errors. Currently they are in DataImport.js
74
+ // maybe these can be moved to a file? but then we need a way to add settings like size...
75
+ setErrors([errorMessages.emptyCols]);
76
+ }
77
+
78
+ return columns.map((columnName, idx) => {
79
+ const columnConfig = {
80
+ id: `column-${columnName}`,
81
+ accessor: row => row[columnName],
82
+ Header: columnName,
83
+ width: 250
84
+ };
85
+
86
+ return columnConfig
87
+ });
88
+ }, [tableData]);
89
+
90
+ // This adds a columns property just like the D3 function for JSON parsing.
91
+ const generateColumns = useCallback((data) => {
92
+ let columns = []
93
+
94
+ data.forEach( (rowObj) => {
95
+ Object.keys(rowObj).forEach( (columnHeading) => {
96
+ if(false === columns.includes(columnHeading)) {
97
+ columns.push(columnHeading)
98
+ }
99
+ })
100
+ })
101
+
102
+ // D3 uses a weird quirk where it attaches a named property to an array. Replicating here.
103
+ const newData = [...data];
104
+
105
+ if(Array.isArray(newData)) {
106
+ newData.columns = columns;
107
+ return newData;
108
+ }
109
+ }, [])
110
+
111
+ useEffect(() => {
112
+ if(!data) {
113
+ return;
114
+ }
115
+
116
+ let newData = [...data];
117
+
118
+ newData = generateColumns(newData);
119
+ validateFipsCodeLength(newData)
120
+ setTableData(newData)
121
+ }, [data, generateColumns])
122
+
123
+ const {
124
+ getTableProps,
125
+ getTableBodyProps,
126
+ headerGroups,
127
+ state: { pageIndex, globalFilter },
128
+ prepareRow,
129
+ setGlobalFilter,
130
+ page,
131
+ canPreviousPage,
132
+ canNextPage,
133
+ pageOptions,
134
+ nextPage,
135
+ previousPage,
136
+ } = useTable({ columns: tableColumns, data: tableData, initialState: { pageSize: 25 } }, useBlockLayout, useGlobalFilter, useSortBy, useResizeColumns, usePagination);
137
+
138
+ const NoData = () => (
139
+ <section className="no-data-message">
140
+ <section>
141
+ <h3>No Data</h3>
142
+ <p>Import data to preview</p>
143
+ </section>
144
+ </section>
145
+ )
146
+
147
+ const PlaceholderTable = () => {
148
+ return (
149
+ <section className="no-data">
150
+ <NoData />
151
+ <div className="table-container">
152
+ <table className="editor data-table" role="table">
153
+ <thead>
154
+ <tr role="row">
155
+ <th scope="col" colSpan="1" role="columnheader">
156
+ </th>
157
+ <th scope="col" colSpan="1" role="columnheader">
158
+ </th>
159
+ <th scope="col" colSpan="1" role="columnheader">
160
+ </th>
161
+ </tr>
162
+ </thead>
163
+ <tbody>
164
+ <tr role="row">
165
+ <td role="cell"></td>
166
+ <td role="cell"></td>
167
+ <td role="cell"></td>
168
+ </tr>
169
+ <tr role="row">
170
+ <td role="cell"></td>
171
+ <td role="cell"></td>
172
+ <td role="cell"></td>
173
+ </tr>
174
+ <tr role="row">
175
+ <td role="cell"></td>
176
+ <td role="cell"></td>
177
+ <td role="cell"></td>
178
+ </tr>
179
+ <tr role="row">
180
+ <td role="cell"></td>
181
+ <td role="cell"></td>
182
+ <td role="cell"></td>
183
+ </tr>
184
+ <tr role="row">
185
+ <td role="cell"></td>
186
+ <td role="cell"></td>
187
+ <td role="cell"></td>
188
+ </tr>
189
+ <tr role="row">
190
+ <td role="cell"></td>
191
+ <td role="cell"></td>
192
+ <td role="cell"></td>
193
+ </tr>
194
+ <tr role="row">
195
+ <td role="cell"></td>
196
+ <td role="cell"></td>
197
+ <td role="cell"></td>
198
+ </tr>
199
+ <tr role="row">
200
+ <td role="cell"></td>
201
+ <td role="cell"></td>
202
+ <td role="cell"></td>
203
+ </tr>
204
+ <tr role="row">
205
+ <td role="cell"></td>
206
+ <td role="cell"></td>
207
+ <td role="cell"></td>
208
+ </tr>
209
+ <tr role="row">
210
+ <td role="cell"></td>
211
+ <td role="cell"></td>
212
+ <td role="cell"></td>
213
+ </tr>
214
+ </tbody>
215
+ </table>
216
+ </div>
217
+ </section>
218
+ )
219
+ }
220
+
221
+ if(!data) return [<Header key="header" />, <PlaceholderTable key="table" />]
222
+
223
+ const footerProps = {previousPage, nextPage, canPreviousPage, canNextPage, pageNumber: pageIndex + 1, totalPages: pageOptions.length}
224
+
225
+ const Table = () => (
226
+ <>
227
+ <section className="data-table-container">
228
+ <div className="table-container">
229
+ <table className="data-table" {...getTableProps()} aria-hidden="true">
230
+ <thead>
231
+ {headerGroups.map((headerGroup) => (
232
+ <tr {...headerGroup.getHeaderGroupProps()}>
233
+ {headerGroup.headers.map((column) => (
234
+ <th scope="col" {...column.getHeaderProps(column.getSortByToggleProps())} className={column.isSorted ? column.isSortedDesc ? 'sort sort-desc' : 'sort sort-asc' : ''} title={column.Header}>
235
+ {column.render('Header')}
236
+ <div {...column.getResizerProps()} className="resizer" />
237
+ </th>
238
+ ))}
239
+ </tr>
240
+ ))}
241
+ </thead>
242
+ <tbody {...getTableBodyProps()}>
243
+ {page.map((row) => {
244
+ prepareRow(row);
245
+ return (
246
+ <tr {...row.getRowProps()}>
247
+ {row.cells.map((cell) => (
248
+ <td {...cell.getCellProps()} title={cell.value}>
249
+ {cell.render('Cell')}
250
+ </td>
251
+ ))}
252
+ </tr>
253
+ );
254
+ })}
255
+ </tbody>
256
+ </table>
257
+ </div>
258
+ </section>
259
+ <Footer {...footerProps} />
260
+ </>
261
+ )
262
+
263
+ return [<Header key="header" data={data} setGlobalFilter={setGlobalFilter} globalFilter={globalFilter} />, <Table key="table" />]
264
+ };
265
+
266
+ export default PreviewDataTable;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ const TabPane = ({ children, className = '' }) => <div className={`tab-content ${className}`}>{children}</div>;
4
+
5
+ export default TabPane;
@@ -0,0 +1,62 @@
1
+ import React, { useState, useContext, useEffect } from 'react';
2
+
3
+ const Tabs = ({ children, startingTab = 0, className, changeTab = null }) => {
4
+ const [active, setActive] = useState(startingTab);
5
+
6
+ let containerClassName = 'tabs';
7
+
8
+ if(className) {
9
+ containerClassName = `tabs ${className}`;
10
+ }
11
+
12
+ const setActiveTab = (disabled, index) => {
13
+ if(false === disabled) {
14
+ setActive(index)
15
+ }
16
+ }
17
+
18
+ useEffect(() => {
19
+ if(startingTab > -1) {
20
+ setActive(startingTab)
21
+ }
22
+ }, [startingTab])
23
+
24
+ const tabs = children.map(({props}, i) => {
25
+
26
+ let classes = 'nav-item'
27
+
28
+ let disabled = props.disableRule || false
29
+
30
+ if(disabled) {
31
+ classes += ' disabled';
32
+ }
33
+
34
+ if(i === active) {
35
+ classes += ' active';
36
+ }
37
+
38
+ return (
39
+ <li
40
+ onClick={() => setActiveTab(disabled, i)}
41
+ key={props.title}
42
+ className={classes}
43
+ >
44
+ {props.icon}
45
+ {props.title}
46
+ </li>
47
+ )
48
+ })
49
+
50
+ return (
51
+ <>
52
+ <nav className={containerClassName}>
53
+ <ul className="nav nav-fill">
54
+ {tabs}
55
+ </ul>
56
+ </nav>
57
+ {children[active]}
58
+ </>
59
+ );
60
+ };
61
+
62
+ export default Tabs;
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+
3
+ export const ConfirmationModal
4
+ = (props) => {
5
+ return (
6
+ <>
7
+ <p className="message">{props.message}</p>
8
+ <div className="confirmation-buttons">
9
+ <div className="btn btn-inline" onClick={props.onCancel}>No</div>
10
+ <div className="btn btn-inline" onClick={props.onConfirm}>Yes</div>
11
+ </div>
12
+ </>
13
+ );
14
+ };
@@ -0,0 +1,51 @@
1
+ import React, { useEffect } from "react";
2
+ // import FocusLock from "react-focus-lock";
3
+ import ReactDOM from "react-dom";
4
+ import CloseIcon from '../../assets/icons/close.svg';
5
+
6
+ export const Modal = ({
7
+ isShown,
8
+ hide,
9
+ modalContent,
10
+ headerText
11
+ }) => {
12
+ const onKeyDown = (event) => {
13
+ if (event.keyCode === 27 && isShown) {
14
+ hide();
15
+ }
16
+ };
17
+
18
+ useEffect(() => {
19
+ isShown
20
+ ? (document.body.style.overflow = "hidden")
21
+ : (document.body.style.overflow = "unset");
22
+ document.addEventListener("keydown", onKeyDown, false);
23
+ return () => {
24
+ document.removeEventListener("keydown", onKeyDown, false);
25
+ };
26
+ }, [isShown]);
27
+
28
+ const modal = (
29
+ <>
30
+ <div className="modal-backdrop" onClick={hide} />
31
+ {/* <FocusLock> */}
32
+ <div className="modal-wrapper"
33
+ aria-modal
34
+ aria-labelledby={headerText}
35
+ tabIndex={-1}
36
+ role="dialog"
37
+ >
38
+ <div className="modal">
39
+ <div className="modal-header">
40
+ <strong>{headerText}</strong>
41
+ <CloseIcon className="modal-close" onClick={hide} />
42
+ </div>
43
+ <div className="modal-content">{modalContent}</div>
44
+ </div>
45
+ </div>
46
+ {/* </FocusLock> */}
47
+ </>
48
+ );
49
+
50
+ return isShown ? ReactDOM.createPortal(modal, document.body) : null;
51
+ };
@@ -0,0 +1,10 @@
1
+ import { useState } from 'react';
2
+
3
+ export const useModal = () => {
4
+ const [isShown, setIsShown] = useState(false);
5
+ const toggle = () => setIsShown(!isShown);
6
+ return {
7
+ isShown,
8
+ toggle,
9
+ };
10
+ };
package/src/context.js ADDED
@@ -0,0 +1,7 @@
1
+ import { createContext } from 'react';
2
+
3
+ const GlobalState = createContext();
4
+
5
+ GlobalState.displayName = 'GlobalState';
6
+
7
+ export default GlobalState;
package/src/index.html ADDED
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1, shrink-to-fit=no"
8
+ />
9
+ <style type="text/css">
10
+ body {
11
+ margin: 0;
12
+ }
13
+ .react-container {
14
+ min-height: 100vh;
15
+ }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="react-container react--editor"></div>
20
+ <noscript>You need to enable JavaScript to run this app.</noscript>
21
+ </body>
22
+ </html>
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom';
3
+ import CdcEditor from './CdcEditor';
4
+
5
+ // Allow URL query to preselect a tab in standalone mode
6
+ const standaloneParams = new URLSearchParams(window.location.search);
7
+
8
+ let activeTab = Number.parseInt( standaloneParams.get('active') ) - 1 || null;
9
+ const domContainer = document.querySelector('.react-container')
10
+
11
+
12
+ ReactDOM.render(
13
+ <React.StrictMode>
14
+ <CdcEditor startingTab={activeTab} containerEl={domContainer} />
15
+ </React.StrictMode>,
16
+ document.querySelector('.react-container')
17
+ );
@@ -0,0 +1,15 @@
1
+ .data-table-header {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: space-between;
5
+ margin-bottom: 1.5em;
6
+ h2 {
7
+ font-weight: 600;
8
+ font-size: 1.3em;
9
+ }
10
+ .filter {
11
+ align-self: flex-end;
12
+ font-size: 1em;
13
+ width: 30%;
14
+ }
15
+ }
@@ -0,0 +1,27 @@
1
+ @import "~@cdc/core/styles/variables";
2
+
3
+ $xxs: 350px;
4
+ $xs: 576px;
5
+ $sm: 768px;
6
+ $md: 992px;
7
+ $lg: 1200px;
8
+
9
+ $base-size: 16;
10
+
11
+ @function size($target, $context: $base-size) {
12
+ @return calc($target / $context) * 1em;
13
+ }
14
+
15
+ $gbl-padding: size(30);
16
+ $table-padding: size(10);
17
+
18
+ // colors
19
+ $white: #fff;
20
+ $darkest-gray: #2e2e2e;
21
+ $gray: #333;
22
+ $light-gray: #797979;
23
+ $lighter-gray: #ebebeb;
24
+ $lightest-gray: #f5f5f5;
25
+
26
+ $light-yellow: #fde0b5;
27
+ $dark-yellow: #f59a23;
@@ -0,0 +1,70 @@
1
+ @import 'variables';
2
+
3
+ .cdc-editor .choose-vis {
4
+ padding: $gbl-padding;
5
+ .capitalize {
6
+ text-transform: capitalize;
7
+ }
8
+ ul + .heading-2 {
9
+ margin-top: 1em;
10
+ }
11
+ .grid {
12
+ margin-top: 1em;
13
+ list-style: none;
14
+ display: flex;
15
+ li {
16
+ margin-right: 1rem;
17
+ margin-bottom: 1rem;
18
+ width: 165px;
19
+ }
20
+ button {
21
+ background-color: #fff;
22
+ color: $baseColor;
23
+ border: solid 1px;
24
+ border-color: rgb(199,199,199);
25
+ padding: 1.3em $gbl-padding;
26
+ height: 100%;
27
+ align-items: center;
28
+ display: flex;
29
+ border: $lightGray 1px solid;
30
+ margin-right: 1em;
31
+ cursor: pointer;
32
+ transition: .1s all;
33
+ flex-direction: column;
34
+ span {
35
+ text-transform: none;
36
+ font-size: 1em;
37
+ }
38
+ &:hover {
39
+ background: #F2F2F2;
40
+ border-color: #949494;
41
+ transition: .1s all;
42
+ }
43
+ &.active {
44
+ background: #fff;
45
+ border-color: #005eaa;
46
+ color: #005eaa;
47
+ position: relative;
48
+ path {
49
+ fill: #005eaa;
50
+ }
51
+ &:before {
52
+ content: " ";
53
+ width: 5px;
54
+ background: #005eaa;
55
+ left: 0;
56
+ top: 0;
57
+ bottom: 0;
58
+ position: absolute;
59
+ }
60
+ }
61
+ svg {
62
+ display: block;
63
+ margin: 0 auto .5em;
64
+ box-sizing: border-box;
65
+ width: 100px;
66
+ height: 75px;
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,19 @@
1
+ .cdc-editor .configure {
2
+ .editor-panel {
3
+ top: 3em;
4
+ }
5
+ .editor-toggle {
6
+ top: 3.5em;
7
+ }
8
+ .type-dashboard {
9
+ .editor-heading {
10
+ top: 3em;
11
+ }
12
+ .editor-panel, .visualizations-panel {
13
+ top: 6em !important;
14
+ }
15
+ .editor-toggle {
16
+ top: 6.5em !important;
17
+ }
18
+ }
19
+ }