pages_core 3.7.0 → 3.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/Rakefile +37 -0
  4. data/app/assets/builds/pages_core/admin-dist.js +55 -0
  5. data/app/assets/stylesheets/pages/admin/components/image_editor.scss +1 -0
  6. data/app/assets/stylesheets/pages/admin/components/image_grid.scss +33 -5
  7. data/app/assets/stylesheets/pages/admin/components/layout.scss +2 -1
  8. data/app/assets/stylesheets/pages/admin/components/login.scss +6 -0
  9. data/app/assets/stylesheets/pages/admin/components/tabs.scss +5 -0
  10. data/app/assets/stylesheets/pages/admin/components/tag_editor.scss +13 -7
  11. data/app/assets/stylesheets/pages/admin/controllers/pages.scss +13 -5
  12. data/app/assets/stylesheets/pages/admin.scss +0 -1
  13. data/app/controller_dummies/admin/admin_controller.rb +1 -1
  14. data/app/controller_dummies/application_controller.rb +1 -1
  15. data/app/controller_dummies/attachments_controller.rb +1 -1
  16. data/app/controller_dummies/frontend_controller.rb +1 -1
  17. data/app/controller_dummies/images_controller.rb +1 -1
  18. data/app/controller_dummies/page_files_controller.rb +1 -1
  19. data/app/controller_dummies/pages_controller.rb +1 -1
  20. data/app/controller_dummies/sitemaps_controller.rb +1 -1
  21. data/app/controllers/admin/attachments_controller.rb +1 -1
  22. data/app/controllers/admin/images_controller.rb +11 -8
  23. data/app/controllers/concerns/pages_core/authentication.rb +9 -4
  24. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +5 -0
  25. data/app/controllers/pages_core/frontend/pages_controller.rb +5 -2
  26. data/app/controllers/sessions_controller.rb +1 -1
  27. data/app/formatters/pages_core/link_renderer.rb +2 -2
  28. data/app/helpers/application_helper.rb +1 -1
  29. data/app/helpers/frontend_helper.rb +1 -1
  30. data/app/helpers/pages_core/admin/content_tabs_helper.rb +5 -2
  31. data/app/helpers/pages_core/admin/image_uploads_helper.rb +2 -3
  32. data/app/helpers/pages_core/admin/tag_editor_helper.rb +9 -39
  33. data/app/helpers/pages_core/head_tags_helper.rb +11 -20
  34. data/app/helpers/pages_core/open_graph_tags_helper.rb +1 -1
  35. data/app/javascript/admin-dist.js +2 -0
  36. data/app/javascript/components/Attachments/Attachment.jsx +121 -0
  37. data/app/javascript/components/Attachments/AttachmentEditor.jsx +116 -0
  38. data/app/javascript/components/Attachments/Placeholder.jsx +10 -0
  39. data/app/javascript/components/Attachments.jsx +165 -0
  40. data/app/{assets/javascripts/pages/admin/components/date_range_select.jsx → javascript/components/DateRangeSelect.jsx} +16 -5
  41. data/app/javascript/components/EditableImage.jsx +61 -0
  42. data/app/javascript/components/FileUploadButton.jsx +47 -0
  43. data/app/{assets/javascripts/pages/admin/components/focal_point.jsx → javascript/components/ImageCropper/FocalPoint.jsx} +12 -1
  44. data/app/javascript/components/ImageCropper/Image.jsx +65 -0
  45. data/app/javascript/components/ImageCropper/Toolbar.jsx +73 -0
  46. data/app/javascript/components/ImageCropper/useCrop.js +199 -0
  47. data/app/javascript/components/ImageCropper.jsx +90 -0
  48. data/app/javascript/components/ImageEditor/Form.jsx +98 -0
  49. data/app/javascript/components/ImageEditor.jsx +62 -0
  50. data/app/javascript/components/ImageGrid/DragElement.jsx +30 -0
  51. data/app/javascript/components/ImageGrid/FilePlaceholder.jsx +9 -0
  52. data/app/javascript/components/ImageGrid/GridImage.jsx +103 -0
  53. data/app/javascript/components/ImageGrid/Placeholder.jsx +23 -0
  54. data/app/javascript/components/ImageGrid.jsx +257 -0
  55. data/app/javascript/components/ImageUploader.jsx +171 -0
  56. data/app/{assets/javascripts/pages/admin/components/modal.jsx → javascript/components/Modal.jsx} +13 -2
  57. data/app/{assets/javascripts/pages/admin/components/page_dates.jsx → javascript/components/PageDates.jsx} +11 -1
  58. data/app/{assets/javascripts/pages/admin/components/page_files.jsx → javascript/components/PageFiles.jsx} +11 -2
  59. data/app/{assets/javascripts/pages/admin/components/page_images.jsx → javascript/components/PageImages.jsx} +11 -2
  60. data/app/{assets/javascripts/pages/admin/components/page_tree_store.jsx → javascript/components/PageTree.jsx} +127 -137
  61. data/app/{assets/javascripts/pages/admin/components/page_tree.jsx → javascript/components/PageTreeDraggable.jsx} +35 -29
  62. data/app/{assets/javascripts/pages/admin/components/page_tree_node.jsx → javascript/components/PageTreeNode.jsx} +35 -20
  63. data/app/javascript/components/RichTextArea.jsx +213 -0
  64. data/app/javascript/components/RichTextToolbarButton.jsx +20 -0
  65. data/app/javascript/components/TagEditor/AddTagForm.jsx +42 -0
  66. data/app/javascript/components/TagEditor/Tag.jsx +32 -0
  67. data/app/javascript/components/TagEditor.jsx +61 -0
  68. data/app/javascript/components/Toast.jsx +72 -0
  69. data/app/javascript/components/drag/draggedOrder.js +51 -0
  70. data/app/javascript/components/drag/useDragCollection.js +84 -0
  71. data/app/javascript/components/drag/useDragUploader.js +112 -0
  72. data/app/javascript/components/drag/useDraggable.js +17 -0
  73. data/app/javascript/components/drag.js +6 -0
  74. data/app/javascript/components.js +15 -0
  75. data/app/javascript/controllers/EditPageController.js +20 -0
  76. data/app/javascript/controllers/LoginController.js +29 -0
  77. data/app/javascript/controllers/MainController.js +65 -0
  78. data/app/javascript/controllers/PageOptionsController.js +62 -0
  79. data/app/javascript/features/RichText.jsx +34 -0
  80. data/app/javascript/hooks.js +2 -0
  81. data/app/javascript/index.js +38 -0
  82. data/app/{assets/javascripts/pages/admin/lib/tree.jsx → javascript/lib/Tree.js} +55 -54
  83. data/app/javascript/lib/copyToClipboard.js +13 -0
  84. data/app/javascript/lib/readyHandler.js +22 -0
  85. data/app/javascript/lib/request.js +36 -0
  86. data/app/javascript/stores/ModalStore.jsx +12 -0
  87. data/app/javascript/stores/ToastStore.jsx +14 -0
  88. data/app/javascript/stores.js +2 -0
  89. data/app/models/concerns/pages_core/page_model/images.rb +3 -1
  90. data/app/models/concerns/pages_core/page_model/searchable.rb +19 -0
  91. data/app/models/concerns/pages_core/searchable_document.rb +71 -0
  92. data/app/models/concerns/pages_core/taggable.rb +27 -12
  93. data/app/models/page.rb +2 -0
  94. data/app/models/page_exporter.rb +2 -2
  95. data/app/models/page_image.rb +0 -2
  96. data/app/models/role.rb +1 -1
  97. data/app/models/search_document.rb +72 -0
  98. data/app/models/tag.rb +1 -0
  99. data/app/models/user.rb +1 -1
  100. data/app/{serializers/admin/attachment_serializer.rb → resources/admin/attachment_resource.rb} +6 -5
  101. data/app/{serializers/admin/image_serializer.rb → resources/admin/image_resource.rb} +9 -9
  102. data/app/resources/admin/page_file_resource.rb +10 -0
  103. data/app/{serializers/admin/page_image_serializer.rb → resources/admin/page_image_resource.rb} +4 -2
  104. data/app/resources/export/attachment_resource.rb +10 -0
  105. data/app/resources/export/page_image_resource.rb +45 -0
  106. data/app/resources/export/page_resource.rb +42 -0
  107. data/app/{serializers/page_image_serializer.rb → resources/page_image_resource.rb} +8 -16
  108. data/app/resources/page_resource.rb +33 -0
  109. data/app/services/pages_core/destroy_invite_service.rb +2 -2
  110. data/app/services/pages_core/invite_service.rb +2 -2
  111. data/app/views/admin/pages/_edit_content.html.erb +1 -1
  112. data/app/views/admin/pages/_edit_files.html.erb +1 -5
  113. data/app/views/admin/pages/_edit_images.html.erb +1 -5
  114. data/app/views/admin/pages/_edit_options.html.erb +74 -55
  115. data/app/views/admin/pages/_form.html.erb +19 -0
  116. data/app/views/admin/pages/edit.html.erb +35 -61
  117. data/app/views/admin/pages/index.html.erb +0 -1
  118. data/app/views/admin/pages/new.html.erb +32 -32
  119. data/app/views/admin/users/_access_control.html.erb +5 -1
  120. data/app/views/admin/users/login.html.erb +12 -4
  121. data/app/views/feeds/pages.rss.builder +1 -2
  122. data/app/views/layouts/admin/_header.html.erb +1 -1
  123. data/app/views/layouts/admin/_page_header.html.erb +33 -0
  124. data/app/views/layouts/admin.html.erb +23 -42
  125. data/app/views/pages_core/_google_analytics.html.erb +8 -0
  126. data/db/migrate/20180625154059_enable_search_extensions.rb +10 -0
  127. data/db/migrate/20210209151400_create_search_configurations.rb +35 -0
  128. data/db/migrate/20210210235200_create_search_documents.rb +74 -0
  129. data/lib/pages_core/engine.rb +1 -5
  130. data/lib/pages_core/templates/block_configuration.rb +1 -1
  131. data/lib/pages_core/templates/configuration_handler.rb +1 -1
  132. data/lib/pages_core/version.rb +3 -1
  133. data/lib/pages_core.rb +3 -5
  134. data/lib/rails/generators/pages_core/frontend/frontend_generator.rb +0 -7
  135. data/lib/rails/generators/pages_core/install/templates/page_templates_initializer.rb +2 -2
  136. metadata +116 -115
  137. data/app/assets/javascripts/pages/admin/components/attachment.jsx +0 -130
  138. data/app/assets/javascripts/pages/admin/components/attachment_editor.jsx +0 -131
  139. data/app/assets/javascripts/pages/admin/components/attachments.jsx +0 -211
  140. data/app/assets/javascripts/pages/admin/components/drag_uploader.jsx +0 -174
  141. data/app/assets/javascripts/pages/admin/components/editable_image.jsx +0 -57
  142. data/app/assets/javascripts/pages/admin/components/file_upload_button.jsx +0 -44
  143. data/app/assets/javascripts/pages/admin/components/grid_image.jsx +0 -124
  144. data/app/assets/javascripts/pages/admin/components/image_editor.jsx +0 -496
  145. data/app/assets/javascripts/pages/admin/components/image_grid.jsx +0 -306
  146. data/app/assets/javascripts/pages/admin/components/image_uploader.jsx +0 -176
  147. data/app/assets/javascripts/pages/admin/components/modal_store.jsx +0 -20
  148. data/app/assets/javascripts/pages/admin/components/rich_text_area.jsx +0 -64
  149. data/app/assets/javascripts/pages/admin/components/rich_text_toolbar.jsx +0 -91
  150. data/app/assets/javascripts/pages/admin/components/toast.jsx +0 -34
  151. data/app/assets/javascripts/pages/admin/components/toast_store.jsx +0 -52
  152. data/app/assets/javascripts/pages/admin/components.jsx +0 -2
  153. data/app/assets/javascripts/pages/admin/features/content_tabs.jsx +0 -72
  154. data/app/assets/javascripts/pages/admin/features/edit_page.jsx +0 -97
  155. data/app/assets/javascripts/pages/admin/features/rich_text.jsx +0 -14
  156. data/app/assets/javascripts/pages/admin/features/tag_editor.jsx +0 -160
  157. data/app/assets/javascripts/pages/admin.jsx +0 -17
  158. data/app/assets/javascripts/pages/login_form.jsx +0 -21
  159. data/app/serializers/admin/page_file_serializer.rb +0 -8
  160. data/app/serializers/page_export_serializer.rb +0 -32
  161. data/app/serializers/page_file_export_serializer.rb +0 -6
  162. data/app/serializers/page_image_export_serializer.rb +0 -42
  163. data/app/serializers/page_serializer.rb +0 -23
  164. data/app/views/layouts/admin/_analytics.html.erb +0 -16
  165. data/lib/rails/generators/pages_core/frontend/templates/application.js.erb +0 -15
  166. data/vendor/assets/javascripts/ReactCrop.min.js +0 -1
  167. data/vendor/assets/javascripts/reflux.min.js +0 -1
@@ -0,0 +1,213 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import RichTextToolbarButton from "./RichTextToolbarButton";
4
+
5
+ export default class RichTextArea extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ this.state = {
9
+ value: props.value || "",
10
+ rows: props.rows || 5
11
+ };
12
+ this.inputRef = React.createRef();
13
+ this.handleChange = this.handleChange.bind(this);
14
+ this.handleKeyPress = this.handleKeyPress.bind(this);
15
+ this.getSelection = this.getSelection.bind(this);
16
+ this.link = this.link.bind(this);
17
+ this.replaceSelection = this.replaceSelection.bind(this);
18
+ }
19
+
20
+ actions() {
21
+ const simple = [
22
+ {
23
+ name: "bold",
24
+ className: "bold",
25
+ hotkey: "b",
26
+ fn: (str) => ["<b>", str, "</b>"]
27
+ },
28
+ { name: "italic",
29
+ className: "italic",
30
+ hotkey: "i",
31
+ fn: (str) => ["<i>", str, "</i>"]
32
+ },
33
+ ];
34
+
35
+ const advanced = [
36
+ {
37
+ name: "Heading 2",
38
+ className: "header h2",
39
+ fn: (str) => ["h2. ", str, ""]
40
+ },
41
+ {
42
+ name: "Heading 3",
43
+ className: "header h3",
44
+ fn: (str) => ["h3. ", str, ""]
45
+ },
46
+ {
47
+ name: "Heading 4",
48
+ className: "header h4",
49
+ fn: (str) => ["h4. ", str, ""]
50
+ },
51
+ {
52
+ name: "Blockquote",
53
+ className: "quote-left",
54
+ fn: (str) => ["bq. ", str, ""]
55
+ },
56
+ {
57
+ name: "List",
58
+ className: "list-ul",
59
+ fn: (str) => ["", this.strToList(str, "*"), ""]
60
+ },
61
+ {
62
+ name: "Ordered list",
63
+ className: "list-ol",
64
+ fn: (str) => ["", this.strToList(str, "#"), ""]
65
+ },
66
+ {
67
+ name: "Link",
68
+ className: "link",
69
+ fn: this.link
70
+ },
71
+ {
72
+ name: "Email link",
73
+ className: "envelope",
74
+ fn: this.emailLink
75
+ },
76
+ ];
77
+
78
+ return this.props.simple ? simple : [...simple, ...advanced];
79
+ }
80
+
81
+ applyAction(fn) {
82
+ let [prefix, replacement, postfix] = fn(this.getSelection());
83
+ this.replaceSelection(prefix, replacement, postfix);
84
+ }
85
+
86
+ emailLink(selection) {
87
+ var address = prompt("Enter email address", "");
88
+ let name = selection.length > 0 ? selection : address;
89
+ return ["\"", name, `":mailto:${address}`];
90
+ }
91
+
92
+ getSelection() {
93
+ let { selectionStart, selectionEnd, value } = this.inputRef.current;
94
+ return value.substr(selectionStart, (selectionEnd - selectionStart));
95
+ }
96
+
97
+ handleChange(evt) {
98
+ this.setState({ value: evt.target.value });
99
+ }
100
+
101
+ handleKeyPress(evt) {
102
+ let key;
103
+ if (evt.which >= 65 && evt.which <= 90) {
104
+ key = String.fromCharCode(evt.keyCode).toLowerCase();
105
+ } else if (evt.keyCode === 13) {
106
+ key = "enter";
107
+ }
108
+
109
+ let hotkeys = {};
110
+ this.actions().forEach(a => {
111
+ if (a.hotkey) {
112
+ hotkeys[a.hotkey] = a.fn;
113
+ }
114
+ });
115
+
116
+ if ((evt.metaKey || evt.ctrlKey) && Object.prototype.hasOwnProperty.call(hotkeys, key)) {
117
+ evt.preventDefault();
118
+ this.applyAction(hotkeys[key]);
119
+ }
120
+ }
121
+
122
+ link(selection) {
123
+ let name = selection.length > 0 ? selection : "Link text";
124
+ var url = prompt("Enter link URL", "");
125
+ if (url) {
126
+ return ["\"", name, `":${this.relativeUrl(url)}`];
127
+ } else {
128
+ return ["", name, ""];
129
+ }
130
+ }
131
+
132
+ relativeUrl(str) {
133
+ let url = null;
134
+
135
+ if (!str.match(/^https:\/\//) || !document || !document.location) {
136
+ return str;
137
+ }
138
+
139
+ try {
140
+ url = new URL(str);
141
+ } catch (error) {
142
+ console.log("Error parsing URL: ", error);
143
+ }
144
+
145
+ if (url &&
146
+ url.hostname == document.location.hostname &&
147
+ (document.location.port || "80") == (url.port || "80")) {
148
+ return url.pathname;
149
+ }
150
+ return str;
151
+ }
152
+
153
+ render() {
154
+ let { value, rows } = this.state;
155
+ let { id, name } = this.props;
156
+
157
+ const clickHandler = (fn) => (evt) => {
158
+ evt.preventDefault();
159
+ this.applyAction(fn);
160
+ };
161
+
162
+ return (
163
+ <div className="rich-text-area">
164
+ <div className="rich-text toolbar">
165
+ {this.actions().map(a =>
166
+ <RichTextToolbarButton
167
+ key={a.name}
168
+ name={a.name}
169
+ className={a.className}
170
+ onClick={clickHandler(a.fn)} />)}
171
+ </div>
172
+ <textarea
173
+ className="rich"
174
+ ref={this.inputRef}
175
+ id={id}
176
+ name={name}
177
+ value={value}
178
+ rows={rows}
179
+ onChange={this.handleChange}
180
+ onKeyDown={this.handleKeyPress} />
181
+ </div>
182
+ );
183
+ }
184
+
185
+ replaceSelection(prefix, replacement, postfix) {
186
+ let textarea = this.inputRef.current;
187
+ let { selectionStart, selectionEnd, value } = textarea;
188
+
189
+ textarea.value =
190
+ value.substr(0, selectionStart) +
191
+ prefix + replacement + postfix +
192
+ value.substr(selectionEnd, value.length);
193
+
194
+ textarea.focus({ preventScroll: true });
195
+ textarea.setSelectionRange(
196
+ selectionStart + prefix.length,
197
+ selectionStart + prefix.length + replacement.length
198
+ );
199
+ this.setState({ value: textarea.value });
200
+ }
201
+
202
+ strToList(str, prefix) {
203
+ return str.split("\n").map(l => prefix + " " + l).join("\n");
204
+ }
205
+ }
206
+
207
+ RichTextArea.propTypes = {
208
+ id: PropTypes.string,
209
+ name: PropTypes.string,
210
+ value: PropTypes.string,
211
+ rows: PropTypes.number,
212
+ simple: PropTypes.bool
213
+ };
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default class RichTextToolbarButton extends React.Component {
5
+ render() {
6
+ return (
7
+ <a title={this.props.name}
8
+ className={"button " + this.props.className}
9
+ onClick={this.props.onClick}>
10
+ <i className={"fa fa-" + this.props.className} />
11
+ </a>
12
+ );
13
+ }
14
+ }
15
+
16
+ RichTextToolbarButton.propTypes = {
17
+ className: PropTypes.string,
18
+ name: PropTypes.string,
19
+ onClick: PropTypes.func
20
+ };
@@ -0,0 +1,42 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function AddTagForm(props) {
5
+ const [tag, setTag] = useState("");
6
+
7
+ const submit = (evt) => {
8
+ evt.preventDefault();
9
+ props.addTag(tag);
10
+ setTag("");
11
+ };
12
+
13
+ const handleKeyDown = (evt) => {
14
+ if (evt.which === 13) {
15
+ submit(evt);
16
+ }
17
+ };
18
+
19
+ const handleChange = (evt) => {
20
+ setTag(evt.target.value);
21
+ };
22
+
23
+ return (
24
+ <div className="add-tag-form">
25
+ <input name="add-tag"
26
+ type="text"
27
+ className="add-tag"
28
+ value={tag}
29
+ onKeyDown={handleKeyDown}
30
+ onChange={handleChange}
31
+ placeholder="Add tag..." />
32
+ <button onClick={submit}
33
+ disabled={!tag}>
34
+ Add
35
+ </button>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ AddTagForm.propTypes = {
41
+ addTag: PropTypes.func
42
+ };
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ export default function Tag(props) {
5
+ const handleChange = () => {
6
+ props.toggleEnabled(props.tag);
7
+ };
8
+
9
+ let classes = ["tag"];
10
+ if (props.enabled) {
11
+ classes.push("enabled");
12
+ }
13
+
14
+ return (
15
+ <span className={classes.join(" ")}>
16
+ <label className="check-box">
17
+ <input type="checkbox"
18
+ name={"tag-" + props.tag}
19
+ value="1"
20
+ checked={props.enabled}
21
+ onChange={handleChange} />
22
+ <span className="name">{props.tag}</span>
23
+ </label>
24
+ </span>
25
+ );
26
+ }
27
+
28
+ Tag.propTypes = {
29
+ enabled: PropTypes.bool,
30
+ tag: PropTypes.string,
31
+ toggleEnabled: PropTypes.func
32
+ };
@@ -0,0 +1,61 @@
1
+ import React, { useState } from "react";
2
+ import PropTypes from "prop-types";
3
+
4
+ import AddTagForm from "./TagEditor/AddTagForm";
5
+ import Tag from "./TagEditor/Tag";
6
+
7
+ function onlyUnique(value, index, self) {
8
+ return self.indexOf(value) === index;
9
+ }
10
+
11
+ export default function TagEditor(props) {
12
+ const [tags, setTags] = useState(props.tags);
13
+ const [enabled, setEnabled] = useState(props.enabled);
14
+
15
+ const tagList = [...tags, ...enabled].filter(onlyUnique);
16
+
17
+ const normalize = (tag) => {
18
+ return (
19
+ tagList.filter(t => t.toLowerCase() == tag.toLowerCase())[0] || tag
20
+ );
21
+ };
22
+
23
+ const tagEnabled = (tag) => {
24
+ return enabled.map(t => t.toLowerCase()).indexOf(tag.toLowerCase()) !== -1;
25
+ };
26
+
27
+ const toggleEnabled = (tag) => {
28
+ const normalized = normalize(tag);
29
+
30
+ if (tagEnabled(normalized)) {
31
+ setEnabled(enabled.filter((t) => t !== normalized));
32
+ } else {
33
+ setEnabled([...enabled, normalized]);
34
+ }
35
+ };
36
+
37
+ const addTag = (tag) => {
38
+ const normalized = normalize(tag);
39
+
40
+ setTags([...tags, normalized].filter(onlyUnique));
41
+ setEnabled([...enabled, normalized].filter(onlyUnique));
42
+ };
43
+
44
+ return (
45
+ <div className="tag-editor clearfix">
46
+ <input type="hidden" name={props.name} value={JSON.stringify(enabled)} />
47
+ {tagList.map((t) =>
48
+ <Tag key={t}
49
+ tag={t}
50
+ enabled={tagEnabled(t)}
51
+ toggleEnabled={toggleEnabled} />)}
52
+ <AddTagForm addTag={addTag} />
53
+ </div>
54
+ );
55
+ }
56
+
57
+ TagEditor.propTypes = {
58
+ name: PropTypes.string,
59
+ enabled: PropTypes.array,
60
+ tags: PropTypes.array
61
+ };
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import PropTypes from "prop-types";
3
+ import ToastStore from "../stores/ToastStore";
4
+
5
+ export default class Toast extends React.Component {
6
+ constructor(props) {
7
+ super(props);
8
+ this.state = { toast: undefined,
9
+ fadeout: false };
10
+ this.store = ToastStore;
11
+ this.timer = undefined;
12
+ this.handleChange = this.handleChange.bind(this);
13
+ }
14
+
15
+ componentDidMount() {
16
+ this.unsubscribe = this.store.subscribe(this.handleChange);
17
+ if (this.props.error) {
18
+ this.store.dispatch({ type: "ERROR", message: this.props.error });
19
+ }
20
+ if (this.props.notice) {
21
+ this.store.dispatch({ type: "NOTICE", message: this.props.notice });
22
+ }
23
+ }
24
+
25
+ componentWillUnmount() {
26
+ this.unsubscribe();
27
+ if (this.timer) {
28
+ clearTimeout(this.timer);
29
+ }
30
+ }
31
+
32
+ handleChange() {
33
+ this.setState({ toast: this.store.getState()[0], fadeout: false });
34
+ if (!this.timer) {
35
+ this.timer = setTimeout(() => {
36
+ this.setState({ fadeout: true });
37
+ this.timer = setTimeout(() => {
38
+ this.timer = undefined;
39
+ this.setState({ fadeout: false });
40
+ this.store.dispatch({ type: "NEXT" });
41
+ }, 500);
42
+ }, 4000);
43
+ }
44
+ }
45
+
46
+ render() {
47
+ let toast = this.state.toast;
48
+ let classNames = ["toast"];
49
+
50
+ if (toast) {
51
+ classNames.push(toast.type);
52
+ if (this.state.fadeout) {
53
+ classNames.push("fadeout");
54
+ }
55
+ }
56
+
57
+ return (
58
+ <div className="toast-wrapper">
59
+ {toast && (
60
+ <div className={classNames.join(" ")}>
61
+ {toast.message}
62
+ </div>
63
+ )}
64
+ </div>
65
+ );
66
+ }
67
+ }
68
+
69
+ Toast.propTypes = {
70
+ notice: PropTypes.string,
71
+ error: PropTypes.string
72
+ };
@@ -0,0 +1,51 @@
1
+ function hovering(dragState, target) {
2
+ let { x, y } = dragState;
3
+ var rect;
4
+ if (target.rect) {
5
+ rect = target.rect;
6
+ } else if (target.current) {
7
+ rect = target.current.getBoundingClientRect();
8
+ } else {
9
+ return false;
10
+ }
11
+ return (x >= rect.left && x <= rect.right &&
12
+ y >= rect.top && y <= rect.bottom);
13
+ }
14
+
15
+ export function collectionOrder(collection, dragState) {
16
+ const { draggables, ref } = collection;
17
+ const { dragging } = dragState;
18
+
19
+ if (!dragging) {
20
+ return draggables;
21
+ }
22
+
23
+ let ordered = draggables.filter(d => d.handle !== dragging.handle);
24
+ if (hovering(dragState, ref)) {
25
+ const hovered = ordered.filter(d => hovering(dragState, d))[0];
26
+ if (hovered) {
27
+ const index = ordered.indexOf(hovered);
28
+ ordered = [...ordered.slice(0, index),
29
+ dragging,
30
+ ...ordered.slice(index)];
31
+ } else {
32
+ ordered = [...ordered, dragging];
33
+ }
34
+ }
35
+
36
+ return ordered;
37
+ }
38
+
39
+ export default function draggedOrder(collection, dragState) {
40
+ let ordered = collectionOrder(collection, dragState);
41
+
42
+ if (dragState.dragging && ordered.indexOf(dragState.dragging) === -1) {
43
+ if (dragState.y < collection.ref.current.getBoundingClientRect().top) {
44
+ ordered = [dragState.dragging, ...ordered];
45
+ } else {
46
+ ordered.push(dragState.dragging);
47
+ }
48
+ }
49
+
50
+ return ordered;
51
+ }
@@ -0,0 +1,84 @@
1
+ import React, { useEffect, useReducer, useRef } from "react";
2
+ import uniqueId from "lodash/uniqueId";
3
+
4
+ function getPosition(draggable) {
5
+ if (draggable.ref.current) {
6
+ return draggable.ref.current.getBoundingClientRect();
7
+ } else {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ function hideDraggable(draggable, callback) {
13
+ if (draggable && draggable.ref && draggable.ref.current) {
14
+ const prevDisplay = draggable.ref.current.style.display;
15
+ draggable.ref.current.style.display = "none";
16
+ const result = callback();
17
+ draggable.ref.current.style.display = prevDisplay;
18
+ return result;
19
+ }
20
+ else {
21
+ return callback();
22
+ }
23
+ }
24
+
25
+ function dragCollectionReducer(state, action) {
26
+ switch (action.type) {
27
+ case "append":
28
+ return [...state, ...action.payload];
29
+ case "prepend":
30
+ return [...action.payload, ...state];
31
+ case "insertFiles":
32
+ var index = state.indexOf("Files");
33
+
34
+ if (index === -1 || !action.payload) {
35
+ return state;
36
+ } else {
37
+ return [...state.slice(0, index),
38
+ ...action.payload,
39
+ ...state.slice(index + 1)];
40
+ }
41
+ case "update":
42
+ return state.map(d => {
43
+ return (d.handle === action.payload.handle) ? action.payload : d;
44
+ });
45
+ case "updatePositions":
46
+ return hideDraggable(action.payload, () => {
47
+ return state.map(d => {
48
+ return { ...d, rect: getPosition(d) };
49
+ });
50
+ });
51
+ case "remove":
52
+ return state.filter(d => d.handle !== action.payload.handle);
53
+ case "replace":
54
+ return action.payload;
55
+ case "reorder":
56
+ return action.payload;
57
+ default:
58
+ return state;
59
+ }
60
+ }
61
+
62
+ export function createDraggable(record) {
63
+ return { record: record,
64
+ rect: null,
65
+ ref: React.createRef(),
66
+ handle: uniqueId("draggable") };
67
+ }
68
+
69
+ export default function useDragCollection(records) {
70
+ const containerRef = useRef();
71
+ const [draggables, dispatch] = useReducer(
72
+ dragCollectionReducer,
73
+ [],
74
+ () => records.map(r => createDraggable(r))
75
+ );
76
+
77
+ useEffect(() => {
78
+ dispatch({ type: "updatePositions" });
79
+ }, []);
80
+
81
+ return { ref: containerRef,
82
+ draggables: draggables,
83
+ dispatch: dispatch };
84
+ }
@@ -0,0 +1,112 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ function containsFiles(evt) {
4
+ if (!evt.dataTransfer || !evt.dataTransfer.types) {
5
+ return false;
6
+ }
7
+ const types = evt.dataTransfer.types;
8
+ for (var i = 0; i < types.length; i++) {
9
+ if (types[i] === "Files" || types[i] === "application/x-moz-file") {
10
+ return true;
11
+ }
12
+ }
13
+ return false;
14
+ }
15
+
16
+ function getFiles(dt) {
17
+ var files = [];
18
+ if (dt.items) {
19
+ for (let i = 0; i < dt.items.length; i++) {
20
+ if (dt.items[i].kind == "file") {
21
+ files.push(dt.items[i].getAsFile());
22
+ }
23
+ }
24
+ } else {
25
+ for (let i = 0; i < dt.files.length; i++) {
26
+ files.push(dt.files[i]);
27
+ }
28
+ }
29
+ return files;
30
+ }
31
+
32
+ function mousePosition(evt) {
33
+ var x, y;
34
+ if (evt.type == "touchmove") {
35
+ x = evt.touches[0].clientX;
36
+ y = evt.touches[0].clientY;
37
+ } else {
38
+ x = evt.clientX;
39
+ y = evt.clientY;
40
+ }
41
+ return { x: x, y: y };
42
+ }
43
+
44
+ export default function useDragUploader(collections, onDragEnd) {
45
+ const [dragState, setDragState] = useState({
46
+ dragging: false,
47
+ x: null, y: null
48
+ });
49
+
50
+ const updatePositions = (dragging) => {
51
+ collections.forEach(c => {
52
+ c.dispatch({ type: "updatePositions", payload: dragging });
53
+ });
54
+ };
55
+
56
+ const startDrag = (evt, draggable) => {
57
+ updatePositions(draggable);
58
+ setDragState({ dragging: draggable, ...mousePosition(evt) });
59
+ };
60
+
61
+ const drag = (evt) => {
62
+ if (dragState.dragging) {
63
+ evt.stopPropagation();
64
+ evt.preventDefault();
65
+ setDragState({ ...dragState, ...mousePosition(evt) });
66
+ } else {
67
+ if (containsFiles(evt)) {
68
+ startDrag(evt, "Files");
69
+ }
70
+ }
71
+ };
72
+
73
+ const dragEnd = (evt) => {
74
+ if (dragState.dragging) {
75
+ const prevDragState = dragState;
76
+ var files = [];
77
+ evt.preventDefault();
78
+ evt.stopPropagation();
79
+ if (dragState.dragging == "Files") {
80
+ files = getFiles(evt.dataTransfer);
81
+ }
82
+ setDragState({ dragging: false, x: null, y: null });
83
+ onDragEnd(prevDragState, files);
84
+ updatePositions();
85
+ }
86
+ };
87
+
88
+ const dragLeave = (evt) => {
89
+ if (dragState.dragging === "Files") {
90
+ evt.preventDefault();
91
+ evt.stopPropagation();
92
+ setDragState({ dragging: false, x: null, y: null });
93
+ }
94
+ };
95
+
96
+ useEffect(() => {
97
+ window.addEventListener("mousemove", drag);
98
+ window.addEventListener("touchmove", drag);
99
+ window.addEventListener("mouseup", dragEnd);
100
+ window.addEventListener("touchend", dragEnd);
101
+ window.addEventListener("mouseout", dragLeave);
102
+ return function cleanup() {
103
+ window.removeEventListener("mousemove", drag);
104
+ window.removeEventListener("touchmove", drag);
105
+ window.removeEventListener("mouseup", dragEnd);
106
+ window.removeEventListener("touchend", dragEnd);
107
+ window.removeEventListener("mouseout", dragLeave);
108
+ };
109
+ });
110
+
111
+ return [dragState, startDrag, { onDragOver: drag, onDrop: dragEnd }];
112
+ }
@@ -0,0 +1,17 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export default function useDraggable(draggable, startDrag) {
4
+ const ref = useRef();
5
+
6
+ const handleDrag = (evt) => {
7
+ evt.preventDefault();
8
+ evt.stopPropagation();
9
+ startDrag(evt, draggable);
10
+ };
11
+
12
+ useEffect(() => {
13
+ draggable.ref.current = ref.current;
14
+ }, []);
15
+
16
+ return { ref: ref, onDragStart: handleDrag, draggable: true };
17
+ }