@performant-software/semantic-components 1.0.1 → 1.0.2-beta.1

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 (36) hide show
  1. package/build/index.js +1 -1
  2. package/build/index.js.map +1 -1
  3. package/build/main.css +26 -0
  4. package/package.json +6 -3
  5. package/src/components/BibliographyList.css +3 -0
  6. package/src/components/BibliographyList.js +286 -0
  7. package/src/components/BibliographyModal.js +125 -0
  8. package/src/components/BibliographySearchInput.css +13 -0
  9. package/src/components/BibliographySearchInput.js +86 -0
  10. package/src/components/Citation.js +78 -0
  11. package/src/components/CreatorField.css +11 -0
  12. package/src/components/CreatorField.js +137 -0
  13. package/src/components/Creators.js +97 -0
  14. package/src/components/List.js +7 -2
  15. package/src/components/SortSelector.js +91 -0
  16. package/src/components/StyleSelector.js +75 -0
  17. package/src/constants/Sort.js +2 -0
  18. package/src/context/ZoteroTranslateContext.js +7 -0
  19. package/src/i18n/en.json +29 -0
  20. package/src/index.js +1 -0
  21. package/src/resources/BibliographyTypes.json +117 -0
  22. package/src/resources/CitationStyles.json +25 -0
  23. package/src/utils/Bibliography.js +191 -0
  24. package/types/components/BibliographyList.js.flow +286 -0
  25. package/types/components/BibliographyModal.js.flow +125 -0
  26. package/types/components/BibliographySearchInput.js.flow +86 -0
  27. package/types/components/Citation.js.flow +78 -0
  28. package/types/components/CreatorField.js.flow +137 -0
  29. package/types/components/Creators.js.flow +97 -0
  30. package/types/components/List.js.flow +7 -2
  31. package/types/components/SortSelector.js.flow +91 -0
  32. package/types/components/StyleSelector.js.flow +75 -0
  33. package/types/constants/Sort.js.flow +2 -0
  34. package/types/context/ZoteroTranslateContext.js.flow +7 -0
  35. package/types/index.js.flow +1 -0
  36. package/types/utils/Bibliography.js.flow +191 -0
@@ -0,0 +1,137 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, useEffect, useMemo } from 'react';
4
+ import { Dropdown, Form } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import i18n from '../i18n/i18n';
7
+ import './CreatorField.css';
8
+
9
+ const NameTypes = {
10
+ single: 0,
11
+ full: 1
12
+ };
13
+
14
+ type Props = {
15
+ allowDelete?: boolean,
16
+ creatorTypes: Array<any>,
17
+ creator: any,
18
+ onAdd: () => void,
19
+ onDelete: () => void,
20
+ onUpdate: (props: any) => void
21
+ };
22
+
23
+ const CreatorField = (props: Props) => {
24
+ /**
25
+ * Updates the passed attribute for the current creator.
26
+ *
27
+ * @type {function(string, ?Event, *): *}
28
+ */
29
+ const onUpdate = useCallback((attribute: string, e: ?Event, { value }: any) => (
30
+ props.onUpdate({ ...props.creator || {}, [attribute]: value })
31
+ ), [props.creator, props.onUpdate]);
32
+
33
+ /**
34
+ * Updates the passed attributes for the current creator.
35
+ *
36
+ * @type {function(*): *}
37
+ */
38
+ const onUpdateAttributes = useCallback((attributes) => (
39
+ props.onUpdate({ ...props.creator || {}, ...attributes })
40
+ ), [props.creator, props.onUpdate]);
41
+
42
+ /**
43
+ * Sets the label attribute as a dropdown of the passed creatorTypes prop.
44
+ *
45
+ * @type {unknown}
46
+ */
47
+ const label = useMemo(() => (
48
+ // eslint-disable-next-line jsx-a11y/label-has-associated-control
49
+ <label>
50
+ <Dropdown
51
+ onChange={onUpdate.bind(this, 'creatorType')}
52
+ options={_.map(props.creatorTypes, (ct) => ({
53
+ key: ct.value,
54
+ value: ct.value,
55
+ text: ct.label
56
+ }))}
57
+ value={props.creator.creatorType}
58
+ />
59
+ </label>
60
+ ), [props.creator.creatorType, props.creatorTypes]);
61
+
62
+ /**
63
+ * Default the nameType property to full (first name/last name).
64
+ */
65
+ useEffect(() => {
66
+ if (!_.has(props.creator, 'nameType')) {
67
+ onUpdateAttributes({ nameType: NameTypes.full });
68
+ }
69
+ }, [props.creator, onUpdateAttributes]);
70
+
71
+ /**
72
+ * Convert between firstName/lastName and name attributes depending on the name type.
73
+ */
74
+ useEffect(() => {
75
+ if (props.creator.name && props.creator.nameType === NameTypes.full) {
76
+ const [firstName, lastName] = props.creator.name.split(' ');
77
+ onUpdateAttributes({ name: null, firstName, lastName });
78
+ } else if ((props.creator.firstName || props.creator.lastName) && props.creator.nameType === NameTypes.single) {
79
+ const name = _.compact([props.creator.firstName, props.creator.lastName]).join(' ');
80
+ onUpdateAttributes({ firstName: null, lastName: null, name });
81
+ }
82
+ }, [props.creator.nameType]);
83
+
84
+ return (
85
+ <Form.Group
86
+ className='creator-field'
87
+ >
88
+ { props.creator.nameType === NameTypes.full && (
89
+ <>
90
+ <Form.Input
91
+ className='flex'
92
+ label={label}
93
+ onChange={onUpdate.bind(this, 'firstName')}
94
+ placeholder={i18n.t('CreatorField.labels.firstName')}
95
+ value={props.creator.firstName}
96
+ />
97
+ <Form.Input
98
+ className='flex'
99
+ onChange={onUpdate.bind(this, 'lastName')}
100
+ placeholder={i18n.t('CreatorField.labels.lastName')}
101
+ value={props.creator.lastName}
102
+ />
103
+ <Form.Button
104
+ icon='exchange'
105
+ onClick={() => onUpdateAttributes({ nameType: NameTypes.single })}
106
+ />
107
+ </>
108
+ )}
109
+ { props.creator.nameType === NameTypes.single && (
110
+ <>
111
+ <Form.Input
112
+ className='flex'
113
+ label={label}
114
+ onChange={onUpdate.bind(this, 'name')}
115
+ placeholder={i18n.t('CreatorField.labels.name')}
116
+ value={props.creator.name}
117
+ />
118
+ <Form.Button
119
+ icon='exchange'
120
+ onClick={() => onUpdateAttributes({ nameType: NameTypes.full })}
121
+ />
122
+ </>
123
+ )}
124
+ <Form.Button
125
+ icon='plus'
126
+ onClick={props.onAdd}
127
+ />
128
+ <Form.Button
129
+ disabled={!props.allowDelete}
130
+ icon='minus'
131
+ onClick={props.onDelete}
132
+ />
133
+ </Form.Group>
134
+ );
135
+ };
136
+
137
+ export default CreatorField;
@@ -0,0 +1,97 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, useEffect, useMemo } from 'react';
4
+ import _ from 'underscore';
5
+ import CreatorField from './CreatorField';
6
+
7
+ type Props = {
8
+ creatorTypes: Array<any>,
9
+ onChange: (creators: Array<any>) => void,
10
+ value: Array<any>
11
+ };
12
+
13
+ const Creators = (props: Props) => {
14
+ /**
15
+ * Sets the default creator type.
16
+ *
17
+ * @type {unknown}
18
+ */
19
+ const defaultCreatorType = useMemo(() => (
20
+ props.creatorTypes && props.creatorTypes.length && props.creatorTypes[0].value
21
+ ), [props.creatorTypes]);
22
+
23
+ /**
24
+ * Adds a new creator record with the default type.
25
+ *
26
+ * @type {function(): *}
27
+ */
28
+ const onAddCreator = useCallback(() => props.onChange([
29
+ ...(props.value || []),
30
+ { creatorType: defaultCreatorType }
31
+ ]), [props.onChange, props.value]);
32
+
33
+ /**
34
+ * Deletes the creator at the passed index.
35
+ *
36
+ * @type {function(*): *}
37
+ */
38
+ const onDeleteCreator = useCallback((index) => props.onChange(
39
+ _.filter(props.value, (v, i) => i !== index)
40
+ ), [props.onChange, props.value]);
41
+
42
+ /**
43
+ * Updates the creator at the passed index.
44
+ *
45
+ * @type {function(*, *): *}
46
+ */
47
+ const onUpdateCreator = useCallback((index, value) => props.onChange(
48
+ _.map(props.value, (v, i) => (i === index ? value : v))
49
+ ), [props.onChange, props.value]);
50
+
51
+ /**
52
+ * Updates the creator type on the items if the selected value is not longer valid.
53
+ *
54
+ * @type {(function(*): (*))|*}
55
+ */
56
+ const onUpdateCreatorType = useCallback((item) => {
57
+ const creatorType = _.findWhere(props.creatorTypes, { value: item.creatorType });
58
+ if (creatorType) {
59
+ return item;
60
+ }
61
+
62
+ return {
63
+ ...item,
64
+ creatorType: defaultCreatorType
65
+ };
66
+ }, [props.creatorTypes, defaultCreatorType]);
67
+
68
+ /**
69
+ * Add the first creator when the component is mounted.
70
+ */
71
+ useEffect(() => {
72
+ onAddCreator();
73
+ }, []);
74
+
75
+ /**
76
+ * If the list of creator types changes, reset any invalid creator types to the default value.
77
+ */
78
+ useEffect(() => {
79
+ if (props.value && props.value.length) {
80
+ props.onChange(_.map(props.value, onUpdateCreatorType));
81
+ }
82
+ }, [onUpdateCreatorType, props.creatorTypes]);
83
+
84
+ return _.map(props.value, (creator, index) => (
85
+ <CreatorField
86
+ allowDelete={props.value.length > 1}
87
+ creator={creator}
88
+ creatorTypes={props.creatorTypes}
89
+ key={index}
90
+ onAdd={() => onAddCreator()}
91
+ onDelete={() => onDeleteCreator(index)}
92
+ onUpdate={(value) => onUpdateCreator(index, value)}
93
+ />
94
+ ));
95
+ };
96
+
97
+ export default Creators;
@@ -41,8 +41,11 @@ type Props = {
41
41
  addButton: {
42
42
  basic: boolean,
43
43
  color: string,
44
+ content?: string,
45
+ inverted?: boolean,
44
46
  location: string,
45
- onClick?: () => void
47
+ onClick?: () => void,
48
+ secondary?: boolean
46
49
  },
47
50
  buttons: Array<ListButton>,
48
51
  count: number,
@@ -378,10 +381,12 @@ const useList = (WrappedComponent: ComponentType<any>) => (
378
381
  basic={this.props.addButton.basic !== false}
379
382
  color={this.props.addButton.color}
380
383
  key={BUTTON_KEY_ADD}
384
+ inverted={this.props.addButton.inverted}
381
385
  onClick={this.onAddButton.bind(this)}
386
+ secondary={this.props.addButton.secondary}
382
387
  >
383
388
  <Icon name='plus' />
384
- { i18n.t('List.buttons.add') }
389
+ { this.props.addButton.content || i18n.t('List.buttons.add') }
385
390
  </Button>
386
391
  );
387
392
  }
@@ -0,0 +1,91 @@
1
+ // @flow
2
+
3
+ import React, { useCallback, useEffect, useRef } from 'react';
4
+ import { Button, Dropdown } from 'semantic-ui-react';
5
+ import _ from 'underscore';
6
+ import { SORT_ASCENDING, SORT_DESCENDING } from '../constants/Sort';
7
+
8
+ type Option = {
9
+ key: string,
10
+ value: string,
11
+ text: string
12
+ };
13
+
14
+ type Sort = Option & {
15
+ direction?: string
16
+ };
17
+
18
+ type Props = {
19
+ defaultValue?: string,
20
+ direction: string,
21
+ onChange: (sort: Sort) => void,
22
+ options: Array<Option>,
23
+ text: string,
24
+ value: string
25
+ };
26
+
27
+ // TODO: Add this to ItemsToggle component
28
+ const SortSelector = (props: Props) => {
29
+ const sortRef = useRef();
30
+
31
+ /**
32
+ * Calls the onChange prop with the direction, text, and value of the selected sort.
33
+ *
34
+ * @type {(function(*): void)|*}
35
+ */
36
+ const onSelection = useCallback((option) => {
37
+ const direction = props.value === option.value && props.direction === SORT_ASCENDING
38
+ ? SORT_DESCENDING
39
+ : SORT_ASCENDING;
40
+
41
+ props.onChange({ ...option, direction });
42
+ }, [props.direction, props.onChange, props.value]);
43
+
44
+ /**
45
+ * Set the default sort to the passed default value or the first option in the list.
46
+ */
47
+ useEffect(() => {
48
+ if (!props.value) {
49
+ let defaultSort;
50
+
51
+ if (props.defaultValue) {
52
+ defaultSort = _.findWhere(props.options, { value: props.defaultValue });
53
+ } else {
54
+ defaultSort = _.first(props.options);
55
+ }
56
+
57
+ onSelection(defaultSort);
58
+ }
59
+ }, []);
60
+
61
+ return (
62
+ <Button.Group
63
+ basic
64
+ className='sort-selector'
65
+ style={{
66
+ fontSize: 'inherit'
67
+ }}
68
+ >
69
+ <Button
70
+ aria-label='Sort by'
71
+ content={props.text}
72
+ icon={props.direction === SORT_ASCENDING ? 'sort alphabet up' : 'sort alphabet down'}
73
+ onClick={(e) => sortRef.current.handleClick(e)}
74
+ />
75
+ <Dropdown
76
+ aria-label='Sort'
77
+ className='button icon'
78
+ floating
79
+ options={_.map(props.options, (option) => ({
80
+ ...option,
81
+ onClick: () => onSelection(option)
82
+ }))}
83
+ ref={sortRef}
84
+ trigger={<></>}
85
+ value={props.value}
86
+ />
87
+ </Button.Group>
88
+ );
89
+ };
90
+
91
+ export default SortSelector;
@@ -0,0 +1,75 @@
1
+ // @flow
2
+
3
+ import React, {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useState
8
+ } from 'react';
9
+ import { Dropdown } from 'semantic-ui-react';
10
+ import _ from 'underscore';
11
+ import CitationStyles from '../resources/CitationStyles.json';
12
+
13
+ type Props = {
14
+ onChange: (name: string, xml: string) => void,
15
+ value: ?string
16
+ };
17
+
18
+ const StyleSelector = (props: Props) => {
19
+ const [stylesCache, setStylesCache] = useState({});
20
+
21
+ /**
22
+ * Build the list of available style options.
23
+ *
24
+ * @type {{}}
25
+ */
26
+ const styles = useMemo(() => CitationStyles.coreCitationStyles, []);
27
+
28
+ /**
29
+ * Sets the style to the selected value. Styles are cached on the state so they do not need to be fetched
30
+ * more than once per render.
31
+ *
32
+ * @type {(function(*, {value: *}): void)|*}
33
+ */
34
+ const onChange = useCallback((e, { value }) => {
35
+ if (_.has(stylesCache, value)) {
36
+ props.onChange(value, stylesCache[value]);
37
+ } else {
38
+ fetch(`https://www.zotero.org/styles/${value}`)
39
+ .then((response) => response.text())
40
+ .then((xml) => {
41
+ props.onChange(value, xml);
42
+ setStylesCache((prevCache) => ({ ...prevCache, [value]: xml }));
43
+ });
44
+ }
45
+ }, [stylesCache]);
46
+
47
+ /**
48
+ * Default the selected style on component mount.
49
+ */
50
+ useEffect(() => {
51
+ const { name } = _.findWhere(styles, { isDefault: true });
52
+ onChange(null, { value: name });
53
+ }, []);
54
+
55
+ return (
56
+ <Dropdown
57
+ onChange={onChange}
58
+ options={_.map(styles, (style) => ({
59
+ key: style.name,
60
+ value: style.name,
61
+ text: style.title
62
+ }))}
63
+ search
64
+ searchInput={{
65
+ 'aria-label': 'Search styles',
66
+ }}
67
+ selectOnBlur={false}
68
+ selection
69
+ text={_.findWhere(styles, { name: props.value })?.title}
70
+ value={props.value}
71
+ />
72
+ );
73
+ };
74
+
75
+ export default StyleSelector;
@@ -0,0 +1,2 @@
1
+ export const SORT_ASCENDING = 'ascending';
2
+ export const SORT_DESCENDING = 'descending';
@@ -0,0 +1,7 @@
1
+ // @flow
2
+
3
+ import { createContext } from 'react';
4
+
5
+ const ZoteroTranslateContext = createContext();
6
+
7
+ export default ZoteroTranslateContext;
package/src/i18n/en.json CHANGED
@@ -16,6 +16,26 @@
16
16
  }
17
17
  }
18
18
  },
19
+ "BibliographyList": {
20
+ "sort": {
21
+ "author": "Author",
22
+ "date": "Date",
23
+ "title": "Title"
24
+ }
25
+ },
26
+ "BibliographyModal": {
27
+ "title": "Add Entry"
28
+ },
29
+ "BibliographySearchInput": {
30
+ "labels": {
31
+ "placeholder": "Enter a URL, ISBN, DOI, PMID, arXiv ID"
32
+ }
33
+ },
34
+ "Citation": {
35
+ "labels": {
36
+ "untitled": "Untitled"
37
+ }
38
+ },
19
39
  "Common": {
20
40
  "buttons": {
21
41
  "add": "Add",
@@ -28,6 +48,7 @@
28
48
  "ok": "OK",
29
49
  "open": "Open",
30
50
  "save": "Save",
51
+ "search": "Search",
31
52
  "upload": "Upload"
32
53
  },
33
54
  "errors": {
@@ -41,12 +62,20 @@
41
62
  "header": "Oops!"
42
63
  },
43
64
  "loading": "Loading",
65
+ "noResults": "No results found.",
44
66
  "save": {
45
67
  "content": "Your changes have been saved.",
46
68
  "header": "Success!"
47
69
  }
48
70
  }
49
71
  },
72
+ "CreatorField": {
73
+ "labels": {
74
+ "firstName": "First name",
75
+ "lastName": "Last name",
76
+ "name": "Name"
77
+ }
78
+ },
50
79
  "EditContainer": {
51
80
  "errors": {
52
81
  "general": "Something went wrong while saving the record. Please make sure all required fields are filled out.",
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ export { default as AccordionSelector } from './components/AccordionSelector';
10
10
  export { default as ArrowButtons } from './components/ArrowButtons';
11
11
  export { default as AssociatedDropdown } from './components/AssociatedDropdown';
12
12
  export { default as AudioPlayer } from './components/AudioPlayer';
13
+ export { default as BibliographyList } from './components/BibliographyList';
13
14
  export { default as BooleanIcon } from './components/BooleanIcon';
14
15
  export { default as CancelButton } from './components/CancelButton';
15
16
  export { default as ColorButton } from './components/ColorButton';
@@ -0,0 +1,117 @@
1
+ {
2
+ "artwork": {
3
+ "medium": "artworkMedium"
4
+ },
5
+ "audioRecording": {
6
+ "medium": "audioRecordingFormat",
7
+ "publisher": "label"
8
+ },
9
+ "bill": {
10
+ "number": "billNumber",
11
+ "volume": "codeVolume",
12
+ "pages": "codePages"
13
+ },
14
+ "blogPost": {
15
+ "publicationTitle": "blogTitle",
16
+ "type": "websiteType"
17
+ },
18
+ "bookSection": {
19
+ "publicationTitle": "bookTitle"
20
+ },
21
+ "case": {
22
+ "title": "caseName",
23
+ "date": "dateDecided",
24
+ "number": "docketNumber",
25
+ "volume": "reporterVolume",
26
+ "pages": "firstPage"
27
+ },
28
+ "computerProgram": {
29
+ "publisher": "company"
30
+ },
31
+ "conferencePaper": {
32
+ "publicationTitle": "proceedingsTitle"
33
+ },
34
+ "dictionaryEntry": {
35
+ "publicationTitle": "dictionaryTitle"
36
+ },
37
+ "email": {
38
+ "title": "subject"
39
+ },
40
+ "encyclopediaArticle": {
41
+ "publicationTitle": "encyclopediaTitle"
42
+ },
43
+ "film": {
44
+ "publisher": "distributor",
45
+ "type": "genre",
46
+ "medium": "videoRecordingFormat"
47
+ },
48
+ "forumPost": {
49
+ "publicationTitle": "forumTitle",
50
+ "type": "postType"
51
+ },
52
+ "hearing": {
53
+ "number": "documentNumber"
54
+ },
55
+ "interview": {
56
+ "medium": "interviewMedium"
57
+ },
58
+ "letter": {
59
+ "type": "letterType"
60
+ },
61
+ "manuscript": {
62
+ "type": "manuscriptType"
63
+ },
64
+ "map": {
65
+ "type": "mapType"
66
+ },
67
+ "patent": {
68
+ "number": "patentNumber",
69
+ "date": "issueDate"
70
+ },
71
+ "podcast": {
72
+ "number": "episodeNumber",
73
+ "medium": "audioFileType"
74
+ },
75
+ "preprint": {
76
+ "type": "genre",
77
+ "publisher": "repository",
78
+ "number": "archiveID"
79
+ },
80
+ "presentation": {
81
+ "type": "presentationType"
82
+ },
83
+ "radioBroadcast": {
84
+ "publicationTitle": "programTitle",
85
+ "number": "episodeNumber",
86
+ "medium": "audioRecordingFormat",
87
+ "publisher": "network"
88
+ },
89
+ "report": {
90
+ "number": "reportNumber",
91
+ "type": "reportType",
92
+ "publisher": "institution"
93
+ },
94
+ "statute": {
95
+ "title": "nameOfAct",
96
+ "number": "publicLawNumber",
97
+ "date": "dateEnacted"
98
+ },
99
+ "thesis": {
100
+ "type": "thesisType",
101
+ "publisher": "university"
102
+ },
103
+ "tvBroadcast": {
104
+ "publicationTitle": "programTitle",
105
+ "number": "episodeNumber",
106
+ "medium": "videoRecordingFormat",
107
+ "publisher": "network"
108
+ },
109
+ "videoRecording": {
110
+ "medium": "videoRecordingFormat",
111
+ "publisher": "studio"
112
+ },
113
+ "webpage": {
114
+ "publicationTitle": "websiteTitle",
115
+ "type": "websiteType"
116
+ }
117
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "coreCitationStyles": [
3
+ {
4
+ "isDefault": false,
5
+ "name": "apa",
6
+ "title": "American Psychological Association 7th edition"
7
+ },
8
+ {
9
+ "isDefault": false,
10
+ "name": "chicago-note-bibliography",
11
+ "title": "Chicago Manual of Style 17th edition (note)"
12
+ },
13
+ {
14
+ "isDefault": true,
15
+ "name": "modern-language-association",
16
+ "title": "Modern Language Association 9th edition"
17
+ },
18
+ {
19
+ "isDefault": false,
20
+ "name": "turabian-fullnote-bibliography",
21
+ "title": "Turabian 8th edition (full note)"
22
+ }
23
+ ],
24
+ "citationStylesCount": 10349
25
+ }