superdesk-ui-framework 2.4.12 → 2.4.17

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.
@@ -60,9 +60,8 @@ function sdModal($document, $rootScope) {
60
60
  };
61
61
 
62
62
  $document.bind('keydown', (evt) => {
63
- evt.preventDefault();
64
-
65
63
  if (evt.which === 27 && $rootScope.modals > 0) {
64
+ evt.preventDefault();
66
65
  closeModal();
67
66
  }
68
67
  });
@@ -73,6 +73,7 @@
73
73
  @import 'form-elements/checkbox';
74
74
  @import 'form-elements/radio';
75
75
  @import 'form-elements/autocomplete';
76
+ @import 'form-elements/select-grid';
76
77
 
77
78
  // Menus
78
79
  @import 'menus/sd-sidebar-menu';
@@ -0,0 +1,77 @@
1
+ .select-grid__overlay-panel {
2
+ z-index: 1500 !important;
3
+ margin-top: 1px;
4
+ }
5
+
6
+ .select-grid__panel {
7
+ width: 450px;
8
+ max-height: 550px;
9
+ background-color: var(--color-bg-00);
10
+ display: flex;
11
+ flex-direction: column;
12
+ }
13
+
14
+ .select-grid__header {
15
+ padding: 1rem;
16
+ box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
17
+ }
18
+
19
+ .select-grid__body {
20
+ overflow-y: auto;
21
+ margin: 0;
22
+ padding: 1rem;
23
+ position: relative;
24
+ }
25
+
26
+ .select-grid__item {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ color: $sd-text;
31
+
32
+ &:hover, &:focus {
33
+ color: $sd-blue;
34
+ cursor: pointer;
35
+
36
+ i {
37
+ color: $sd-blue;
38
+ }
39
+ }
40
+
41
+ &:hover {
42
+ background: $sd-colour-background__menu-item--hover;
43
+ }
44
+
45
+ &:focus {
46
+ outline: 2px solid $sd-colour-interactive__base;
47
+ outline-offset: -2px;
48
+ }
49
+ }
50
+
51
+ .sd-input--grid-select {
52
+ .btn {
53
+ grid-row: 2/3;
54
+ grid-column: 2/4;
55
+ background-color: var(--color-input-bg);
56
+ border-bottom: 1px solid var(--color-input-border);
57
+ width: 3.2rem;
58
+ }
59
+
60
+ .btn:hover {
61
+ background-color: var(--color-input-bg--hover);
62
+ border-color: var(--color-input-border-hover);
63
+ }
64
+
65
+ .btn:focus {
66
+ background-color: var(--color-input-bg--focus);
67
+ box-shadow: 0 1px 0 0 $sd-blue;
68
+ }
69
+ }
70
+
71
+ .form__item--auto-width {
72
+ .sd-input--grid-select {
73
+ .btn {
74
+ margin-top: 0;
75
+ }
76
+ }
77
+ }
@@ -158,7 +158,6 @@ export const Dropdown = ({
158
158
  if (toggleRef && menuRef) {
159
159
  createPopper(toggleRef, menuRef, {
160
160
  placement: checkAlign() ? 'bottom-end' : 'bottom-start',
161
- strategy: 'fixed',
162
161
  });
163
162
  menuRef.style.display = 'block';
164
163
  }
@@ -0,0 +1,277 @@
1
+ import * as React from 'react';
2
+ // @ts-ignore
3
+ import * as iconFont from '../../app/styles/_icon-font.scss';
4
+ import { Button } from './Button';
5
+ import { Icon } from './Icon';
6
+ import { IItem, SelectGrid } from "./SelectGrid";
7
+
8
+ interface IProps {
9
+ label?: string;
10
+ filterPlaceholder?: string;
11
+ translateFunction?: (text: string) => string;
12
+ value: string;
13
+ onChange(icon: string): void;
14
+ }
15
+
16
+ interface IState {
17
+ icons: Array<IItem>;
18
+ }
19
+
20
+ export class IconPicker extends React.PureComponent<IProps, IState> {
21
+
22
+ constructor(props: IProps) {
23
+ super(props);
24
+ this.state = { icons: [] };
25
+ }
26
+
27
+ componentDidMount() {
28
+ const translateFunction = this.props.translateFunction ?
29
+ this.props.translateFunction : (text: string): string => text;
30
+ this.setState({
31
+ icons: getIcons(translateFunction),
32
+ });
33
+ }
34
+
35
+ getItems = (searchString: string | null): Promise<Array<IItem>> => {
36
+ return new Promise((resolve) => {
37
+ let icons = [...this.state.icons];
38
+
39
+ if (searchString) {
40
+ icons = icons.filter(
41
+ (icon) => (
42
+ icon.value.toLowerCase().includes(searchString) ||
43
+ icon.label.toLowerCase().includes(searchString)
44
+ ),
45
+ );
46
+ }
47
+ resolve(icons);
48
+ });
49
+ }
50
+
51
+ onChange = (item: IItem) => {
52
+ this.props.onChange(item.value);
53
+ }
54
+
55
+ triggerTemplate = (props: any) => <Button
56
+ icon={this.props.value}
57
+ text={this.props.value}
58
+ onClick={(e) => { props.onClick(e); }}
59
+ iconOnly={true} />
60
+
61
+ itemTemplate = ({ item }: { item: IItem | null }) => item ?
62
+ (<>
63
+ <Icon name={item.value} />
64
+ <span className="sd-text__normal sd-padding-t--1">
65
+ {item.label}
66
+ </span>
67
+ </>) : null
68
+
69
+ render() {
70
+ return (
71
+ <SelectGrid
72
+ label={this.props.label || "Icon"}
73
+ filterPlaceholder={this.props.filterPlaceholder || "Search..."}
74
+ getItems={this.getItems}
75
+ onChange={this.onChange}
76
+ itemTemplate={this.itemTemplate}
77
+ triggerTemplate={this.triggerTemplate}
78
+ />
79
+ );
80
+ }
81
+ }
82
+
83
+ const getIcons = (translateFunction: (text: string) => string): Array<IItem> => {
84
+ const translatedIconNameMap: any = {
85
+ 'add-gallery': 'Add Gallery',
86
+ 'add-image': 'Add Image',
87
+ 'adjust': 'Adjust',
88
+ 'align-center': 'Align Center',
89
+ 'align-justify': 'Align Justify',
90
+ 'align-left': 'Align Left',
91
+ 'align-right': 'Align Right',
92
+ 'amp': 'AMP',
93
+ 'analytics': 'Analytics',
94
+ 'archive': 'Archive',
95
+ 'arrow-left': 'Arrow Left',
96
+ 'arrow-right': 'Arrow Right',
97
+ 'arrow-small': 'Arrow Small',
98
+ 'ascending': 'Ascending',
99
+ 'assign': 'Assign',
100
+ 'attachment': 'Attachment',
101
+ 'attachment-large': 'Attachment Large',
102
+ 'audio': 'Audio',
103
+ 'backward-thin': 'Backward Thin',
104
+ 'ban-circle': 'Ban Circle',
105
+ 'bell': 'Bell',
106
+ 'bold': 'Bold',
107
+ 'broadcast': 'Broadcast',
108
+ 'broadcast-create': 'Broadcast Create',
109
+ 'business': 'Business',
110
+ 'calendar': 'Calendar',
111
+ 'calendar-list': 'Calendar List',
112
+ 'chevron-down-thin': 'Chevron Down Thin',
113
+ 'chevron-left-thin': 'Chevron Left Thin',
114
+ 'chevron-right-thin': 'Chevron Right Thin',
115
+ 'chevron-up-thin': 'Chevron Up Thin',
116
+ 'clear-all': 'Clear All',
117
+ 'clear-format': 'Clear Format',
118
+ 'close-small': 'Close Small',
119
+ 'close-thick': 'Close Thick',
120
+ 'code': 'Code',
121
+ 'collapse': 'Collapse',
122
+ 'comment': 'Comment',
123
+ 'composite': 'Composite',
124
+ 'copy': 'Copy',
125
+ 'crop': 'Crop',
126
+ 'cut': 'Cut',
127
+ 'descending': 'Descending',
128
+ 'dots': 'Dots',
129
+ 'dots-vertical': 'Dots Vertical',
130
+ 'download': 'Download',
131
+ 'download-alt': 'Download Alternate',
132
+ 'edit-line': 'Edit Line',
133
+ 'envelope': 'Envelope',
134
+ 'event': 'Event',
135
+ 'exclamation-sign': 'Exclamation Sign',
136
+ 'expand': 'Expand',
137
+ 'expand-thin': 'Expand Thin',
138
+ 'external': 'External',
139
+ 'eye-open': 'Eye Open',
140
+ 'facebook': 'Facebook',
141
+ 'facebook-circle': 'Facebook Circle',
142
+ 'fast_forward': 'Fast Forward',
143
+ 'fast_rewind': 'Fast Rewind',
144
+ 'fetch-as': 'Fetch As',
145
+ 'file': 'File',
146
+ 'filter-large': 'Filter Large',
147
+ 'flip-horizontal': 'Flip Horizontal',
148
+ 'flip-vertical': 'Flip Vertical',
149
+ 'folder-close': 'Folder Close',
150
+ 'folder-open': 'Folder Open',
151
+ 'font': 'Font',
152
+ 'forward-thin': 'Forward Thin',
153
+ 'fullscreen': 'Fullscreen',
154
+ 'globe': 'Globe',
155
+ 'graphic': 'Graphic',
156
+ 'grid-view': 'Grid View',
157
+ 'grid-view-large': 'Grid View Large',
158
+ 'heading-1': 'Heading 1',
159
+ 'heading-2': 'Heading 2',
160
+ 'heading-3': 'Heading 3',
161
+ 'heading-4': 'Heading 4',
162
+ 'heading-5': 'Heading 5',
163
+ 'heading-6': 'Heading 6',
164
+ 'heart': 'Heart',
165
+ 'help-large': 'Help Large',
166
+ 'highlight-package': 'Highlight Package',
167
+ 'home': 'Home',
168
+ 'indent-left': 'Indent Left',
169
+ 'indent-right': 'Indent Right',
170
+ 'info-large': 'Info Large',
171
+ 'info-sign': 'Info Sign',
172
+ 'ingest': 'Ingest',
173
+ 'instagram': 'Instagram',
174
+ 'italic': 'Italic',
175
+ 'kanban-view': 'Kanban View',
176
+ 'kill': 'Kill',
177
+ 'link': 'Link',
178
+ 'linked-in': 'LinkedIn',
179
+ 'linked-in-circle': 'LinkedIn Circle',
180
+ 'list-alt': 'List Alternate',
181
+ 'list-menu': 'List Menu',
182
+ 'list-plus': 'List Plus',
183
+ 'list-view': 'List View',
184
+ 'lock': 'Lock',
185
+ 'map-marker': 'Map Marker',
186
+ 'minus-sign': 'Minus Sign',
187
+ 'minus-small': 'Minus Small',
188
+ 'mobile': 'Mobile',
189
+ 'move': 'Move',
190
+ 'multi-star': 'Multi Start',
191
+ 'multiedit': 'Multi Edit',
192
+ 'new-doc': 'New Document',
193
+ 'ok': 'Okay',
194
+ 'ordered-list': 'Ordered List',
195
+ 'package-create': 'Package Create',
196
+ 'package-plus': 'Package Plus',
197
+ 'paragraph': 'Paragraph',
198
+ 'paste': 'Paste',
199
+ 'pause': 'Pause',
200
+ 'paywall': 'Paywall',
201
+ 'pencil': 'Pencil',
202
+ 'phone': 'Phone',
203
+ 'photo': 'Photo',
204
+ 'pick': 'Pick',
205
+ 'picture': 'Picture',
206
+ 'pin': 'Pin',
207
+ 'play': 'Play',
208
+ 'plus-large': 'Plus Large',
209
+ 'plus-sign': 'Plus Sign',
210
+ 'plus-small': 'Plus Small',
211
+ 'post': 'Post',
212
+ 'preformatted': 'Preformatted',
213
+ 'preview-mode': 'Preview Mode',
214
+ 'print': 'Print',
215
+ 'question-sign': 'Question Sign',
216
+ 'quote': 'Quote',
217
+ 'random': 'Random',
218
+ 'recurring': 'Recurring',
219
+ 'redo': 'Redo',
220
+ 'refresh': 'Refresh',
221
+ 'remove-sign': 'Remove Sign',
222
+ 'repeat': 'Repeat',
223
+ 'retweet': 'Retweet',
224
+ 'revert': 'Revert',
225
+ 'rotate-left': 'Rotate Left',
226
+ 'rotate-right': 'Rotate Right',
227
+ 'search': 'Search',
228
+ 'settings': 'Settings',
229
+ 'share-alt': 'Share Alternate',
230
+ 'signal': 'Signal',
231
+ 'skip_next': 'Skip Next',
232
+ 'skip_previous': 'Skip Previous',
233
+ 'slideshow': 'Slideshow',
234
+ 'star': 'Star',
235
+ 'star-empty': 'Star Empty',
236
+ 'stop': 'Stop',
237
+ 'stream': 'Stream',
238
+ 'strikethrough': 'Strikethrough',
239
+ 'subscript': 'Subscript',
240
+ 'suggestion': 'Suggestion',
241
+ 'superscript': 'Superscript',
242
+ 'switches': 'Switches',
243
+ 'table': 'Table',
244
+ 'takes-package': 'Takes Package',
245
+ 'tasks': 'Tasks',
246
+ 'text': 'Text',
247
+ 'text-format': 'Text Format',
248
+ 'th': 'Table Header',
249
+ 'th-large': 'Table Header Large',
250
+ 'th-list': 'Table Header List',
251
+ 'time': 'Time',
252
+ 'to-lowercase': 'To Lowercase',
253
+ 'to-uppercase': 'To Uppercase',
254
+ 'trash': 'Trash',
255
+ 'twitter': 'Twitter',
256
+ 'twitter-circle': 'Twitter Circle',
257
+ 'underline': 'Underline',
258
+ 'undo': 'Undo',
259
+ 'unlocked': 'Unlocked',
260
+ 'unordered-list': 'Unordered List',
261
+ 'unspike': 'Unspike',
262
+ 'upload': 'Upload',
263
+ 'user': 'User',
264
+ 'video': 'Video',
265
+ 'warning-sign': 'Warning Sign',
266
+ 'zoom-in': 'Zoom In',
267
+ 'zoom-out': 'Zoom Out',
268
+ };
269
+
270
+ return iconFont.icon
271
+ .split(', ')
272
+ .sort()
273
+ .map((icon: string) => ({
274
+ value: icon,
275
+ label: translatedIconNameMap[icon] ? translateFunction(translatedIconNameMap[icon]) : icon,
276
+ }));
277
+ };
@@ -0,0 +1,233 @@
1
+ import React from 'react';
2
+ import nextId from "react-id-generator";
3
+ import { OverlayPanel } from '@superdesk/primereact/overlaypanel';
4
+ import { Loader } from "./Loader";
5
+
6
+ /**
7
+ * @ngdoc react
8
+ * @name SelectGrid
9
+ * @description searchable select component with grid view of items
10
+ */
11
+
12
+ export interface IItem {
13
+ value: string;
14
+ label: string;
15
+ [extra: string]: any;
16
+ }
17
+
18
+ interface IProps {
19
+ getItems(searchString: string | null): Promise<Array<IItem>>;
20
+ onChange(value: IItem): void;
21
+ itemTemplate: React.ComponentType<{ item: IItem | null }>;
22
+ triggerTemplate: React.ComponentType<any>;
23
+ label: string;
24
+ filterPlaceholder?: string;
25
+ }
26
+
27
+ interface IState {
28
+ items: Array<IItem>;
29
+ loading: boolean;
30
+ isOpen: boolean;
31
+ }
32
+
33
+ export class SelectGrid extends React.PureComponent<IProps, IState> {
34
+ htmlId = nextId();
35
+ buttonContainer: React.RefObject<HTMLDivElement>;
36
+ overlayPanel: React.RefObject<OverlayPanel>;
37
+ searchInput: React.RefObject<HTMLInputElement>;
38
+ gridContainer: React.RefObject<HTMLDivElement>;
39
+
40
+ constructor(props: IProps) {
41
+ super(props);
42
+
43
+ this.state = { items: [], loading: true, isOpen: false };
44
+
45
+ this.buttonContainer = React.createRef();
46
+ this.overlayPanel = React.createRef();
47
+ this.searchInput = React.createRef();
48
+ this.gridContainer = React.createRef();
49
+ }
50
+
51
+ componentDidMount() {
52
+ this.props.getItems(null).then((items) => {
53
+ this.setState({ items, loading: false });
54
+ });
55
+ }
56
+
57
+ componentWillUnmount() {
58
+ document.removeEventListener('keydown', this.handleKeydown);
59
+ }
60
+
61
+ togglePopup = (event?: React.SyntheticEvent) => {
62
+ if (!event) {
63
+ // @ts-ignore
64
+ this.overlayPanel.current.hide();
65
+ } else {
66
+ // @ts-ignore
67
+ this.overlayPanel.current.toggle(event);
68
+ }
69
+
70
+ setTimeout(() => {
71
+ // Now that the popup has (dis)appeared handle items and events
72
+
73
+ if (this.state.isOpen) {
74
+ document.removeEventListener('keydown', this.handleKeydown);
75
+ // @ts-ignore
76
+ this.buttonContainer.current.querySelector('button')?.focus();
77
+ } else {
78
+ document.addEventListener('keydown', this.handleKeydown);
79
+ this.loadItems();
80
+ // @ts-ignore
81
+ this.searchInput.current.focus();
82
+ }
83
+
84
+ this.setState({ isOpen: !this.state.isOpen });
85
+ });
86
+
87
+ }
88
+
89
+ search = (event: React.ChangeEvent<HTMLInputElement>) => {
90
+ const searchString: string = event.target.value.toLowerCase();
91
+ this.loadItems(searchString);
92
+ }
93
+
94
+ loadItems = (searchString: string | null = null) => {
95
+ this.setState({ loading: true });
96
+ this.props.getItems(searchString).then((items) => {
97
+ this.setState({ items, loading: false });
98
+ });
99
+ }
100
+
101
+ select = (item: IItem) => {
102
+ this.props.onChange(item);
103
+ this.togglePopup();
104
+ }
105
+
106
+ getItemElement = (index: number): HTMLDivElement | null | undefined => {
107
+ return this.gridContainer.current?.querySelector(`[data-item-index="${index}"]`);
108
+ }
109
+
110
+ handleKeydown = (event: KeyboardEvent) => {
111
+ const navKeys = [
112
+ "Enter",
113
+ "ArrowRight",
114
+ "ArrowLeft",
115
+ "ArrowUp",
116
+ "ArrowDown",
117
+ "PageDown",
118
+ "PageUp",
119
+ ];
120
+ const activeElement = document.activeElement;
121
+
122
+ if (event.code === "Escape") {
123
+ event.preventDefault();
124
+ event.stopPropagation();
125
+ this.togglePopup();
126
+ } else if (activeElement === this.searchInput?.current) {
127
+ if (event.code === "ArrowDown") {
128
+ event.preventDefault();
129
+ this.getItemElement(0)?.focus();
130
+ } else if (event.code === "Enter" && this.state.items.length === 1) {
131
+ event.preventDefault();
132
+ this.select(this.state.items[0]);
133
+ }
134
+ // @ts-ignore
135
+ } else if (document.activeElement.getAttribute('data-item-index') && navKeys.includes(event.code)) {
136
+ // @ts-ignore
137
+ let itemIndex = parseInt(activeElement.getAttribute('data-item-index'), 10);
138
+
139
+ // Prevent default behaviour, such as scrolling
140
+ event.preventDefault();
141
+ if (event.code === "Enter") {
142
+ this.select(this.state.items[itemIndex]);
143
+ return;
144
+ } else if (event.code === "ArrowRight") {
145
+ itemIndex += 1;
146
+ } else if (event.code === "ArrowLeft") {
147
+ itemIndex -= 1;
148
+ } else if (event.code === "ArrowDown") {
149
+ itemIndex += 4;
150
+ } else if (event.code === "ArrowUp") {
151
+ if (itemIndex === 0) {
152
+ this.searchInput?.current?.focus();
153
+ return;
154
+ }
155
+ itemIndex -= 4;
156
+ } else if (event.code === "PageDown") {
157
+ itemIndex += 16;
158
+ } else if (event.code === "PageUp") {
159
+ itemIndex -= 16;
160
+ }
161
+
162
+ if (itemIndex < 0) {
163
+ itemIndex = 0;
164
+ } else if (itemIndex >= this.state.items.length) {
165
+ itemIndex = this.state.items.length - 1;
166
+ }
167
+
168
+ this.getItemElement(itemIndex)?.focus();
169
+ }
170
+ }
171
+
172
+ render() {
173
+ const ItemTemplate = this.props.itemTemplate;
174
+ const TriggerTemplate = this.props.triggerTemplate;
175
+
176
+ return (
177
+ <React.Fragment>
178
+ <div
179
+ className="sd-input sd-input--grid-select"
180
+ ref={this.buttonContainer}
181
+ aria-label={this.props.label}
182
+ >
183
+ <label className="sd-input__label">
184
+ {this.props.label}
185
+ </label>
186
+ <TriggerTemplate onClick={this.togglePopup} />
187
+
188
+ </div>
189
+ <OverlayPanel
190
+ ref={this.overlayPanel}
191
+ dismissable={true}
192
+ className="select-grid__overlay-panel"
193
+ appendTo={document.body} // making it work inside `overflow:hidden`
194
+ >
195
+ <div className="sd-shadow--z3 select-grid__panel">
196
+ <div className="select-grid__header">
197
+ <div className="sd-searchbar sd-searchbar--boxed">
198
+ <label className="sd-searchbar__icon" />
199
+ <input
200
+ className="sd-searchbar__input"
201
+ placeholder={this.props.filterPlaceholder || 'Search...'}
202
+ type="text"
203
+ onChange={this.search}
204
+ ref={this.searchInput}
205
+ />
206
+ </div>
207
+ </div>
208
+ <div
209
+ className="select-grid__body flex-grid flex-grid--wrap-items flex-grid--small-4 flex-grid--boxed"
210
+ ref={this.gridContainer}
211
+
212
+ >
213
+ <Loader overlay={this.state.loading} />
214
+ {this.state.items.map((item, index) => (
215
+ <div
216
+ key={this.htmlId + item.label}
217
+ data-item-index={index}
218
+ className="flex-grid__item select-grid__item sd-padding-y--2"
219
+ tabIndex={0}
220
+ role="button"
221
+ aria-label={item.name}
222
+ onClick={() => this.select(item)}
223
+ >
224
+ <ItemTemplate item={item} />
225
+ </div>
226
+ ))}
227
+ </div>
228
+ </div>
229
+ </OverlayPanel>
230
+ </React.Fragment>
231
+ );
232
+ }
233
+ }
@@ -63,6 +63,8 @@ export { GridItem, GridItemContent, GridItemMedia, GridItemFooter, GridItemConte
63
63
  export { toasted } from './components/Toast';
64
64
  export { Menu } from './components/Menu';
65
65
  export { ToggleBox } from './components/Togglebox';
66
+ export { SelectGrid } from './components/SelectGrid';
67
+ export { IconPicker } from './components/IconPicker';
66
68
 
67
69
  export { Skeleton } from './components/Skeleton';
68
70
  export { ListItemLoader } from './components/ListItemLoader';