katalyst-koi 4.0.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +23 -0
- data/Upgrade.md +6 -0
- data/app/assets/builds/koi/admin.css +1 -0
- data/app/assets/builds/koi/nav_items.css +1 -0
- data/app/assets/config/koi.js +10 -0
- data/app/assets/images/koi/application/chevron-right.svg +10 -0
- data/app/assets/images/koi/application/glyphicons-halflings-white.png +0 -0
- data/app/assets/images/koi/application/glyphicons-halflings.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-down.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-up.png +0 -0
- data/app/assets/images/koi/application/icon-file-doc.png +0 -0
- data/app/assets/images/koi/application/icon-file-img.png +0 -0
- data/app/assets/images/koi/application/icon-file-pdf.png +0 -0
- data/app/assets/images/koi/application/icon-file-ppt.png +0 -0
- data/app/assets/images/koi/application/icon-file-unknown.png +0 -0
- data/app/assets/images/koi/application/icon-file-xls.png +0 -0
- data/app/assets/images/koi/application/icon-file-zip.png +0 -0
- data/app/assets/images/koi/application/icon-form-date-picker.png +0 -0
- data/app/assets/images/koi/application/icon-form-error.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-ascending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-descending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort.png +0 -0
- data/app/assets/images/koi/application/icon-index-sortable.png +0 -0
- data/app/assets/images/koi/application/icon-menu-cursor.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-add.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-close.png +0 -0
- data/app/assets/images/koi/application/icon-sortable.png +0 -0
- data/app/assets/images/koi/application/jcrop.gif +0 -0
- data/app/assets/images/koi/application/loading.gif +0 -0
- data/app/assets/images/koi/application/select-arrow.svg +3 -0
- data/app/assets/images/koi/application/select_arrow.png +0 -0
- data/app/assets/images/koi/application/sort-ascending.png +0 -0
- data/app/assets/images/koi/application/sort-descending.png +0 -0
- data/app/assets/javascripts/koi/admin.js +4 -0
- data/app/assets/javascripts/koi/controllers/application.js +11 -0
- data/app/assets/javascripts/koi/controllers/document_field_controller.js +26 -0
- data/app/assets/javascripts/koi/controllers/file_field_controller.js +143 -0
- data/app/assets/javascripts/koi/controllers/flash_controller.js +12 -0
- data/app/assets/javascripts/koi/controllers/form_request_submit_controller.js +11 -0
- data/app/assets/javascripts/koi/controllers/image_field_controller.js +24 -0
- data/app/assets/javascripts/koi/controllers/index.js +6 -0
- data/app/assets/javascripts/koi/controllers/index_actions_controller.js +61 -0
- data/app/assets/javascripts/koi/controllers/keyboard_controller.js +149 -0
- data/app/assets/javascripts/koi/controllers/navigation_controller.js +84 -0
- data/app/assets/javascripts/koi/controllers/navigation_toggle_controller.js +7 -0
- data/app/assets/javascripts/koi/controllers/show_hide_controller.js +25 -0
- data/app/assets/javascripts/koi/controllers/sluggable_controller.js +30 -0
- data/app/assets/javascripts/koi/controllers/webauthn_authentication_controller.js +23 -0
- data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +30 -0
- data/app/assets/javascripts/koi/utils/transition.js +220 -0
- data/app/assets/stylesheets/koi/admin.scss +27 -0
- data/app/assets/stylesheets/koi/base/_button.scss +122 -0
- data/app/assets/stylesheets/koi/base/_icon.scss +29 -0
- data/app/assets/stylesheets/koi/base/_index.scss +18 -0
- data/app/assets/stylesheets/koi/base/_input.scss +13 -0
- data/app/assets/stylesheets/koi/base/_link.scss +26 -0
- data/app/assets/stylesheets/koi/base/_list.scss +11 -0
- data/app/assets/stylesheets/koi/base/_typography.scss +160 -0
- data/app/assets/stylesheets/koi/components/_actions-group.scss +7 -0
- data/app/assets/stylesheets/koi/components/_image-field.scss +33 -0
- data/app/assets/stylesheets/koi/components/_index-actions.scss +69 -0
- data/app/assets/stylesheets/koi/components/_index-table.scss +91 -0
- data/app/assets/stylesheets/koi/components/_index.scss +6 -0
- data/app/assets/stylesheets/koi/components/_item-table.scss +33 -0
- data/app/assets/stylesheets/koi/components/_pagy.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_banner.scss +7 -0
- data/app/assets/stylesheets/koi/layouts/_content.scss +40 -0
- data/app/assets/stylesheets/koi/layouts/_flash.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_header.scss +62 -0
- data/app/assets/stylesheets/koi/layouts/_index.scss +48 -0
- data/app/assets/stylesheets/koi/layouts/_main.scss +23 -0
- data/app/assets/stylesheets/koi/layouts/_navigation.scss +156 -0
- data/app/assets/stylesheets/koi/layouts/_stack.scss +13 -0
- data/app/assets/stylesheets/koi/pages/_index.scss +1 -0
- data/app/assets/stylesheets/koi/pages/_login.scss +40 -0
- data/app/assets/stylesheets/koi/themes/_content.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_govuk.scss +52 -0
- data/app/assets/stylesheets/koi/themes/_index.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_kpop.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_navigation.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_trix.scss +32 -0
- data/app/assets/stylesheets/koi/utils/_breakpoints.scss +13 -0
- data/app/assets/stylesheets/koi/utils/_hide.scss +11 -0
- data/app/assets/stylesheets/koi/utils/_index.scss +2 -0
- data/app/assets/stylesheets/koi/utils/_typography.scss +24 -0
- data/app/components/koi/header/edit_component.rb +58 -0
- data/app/components/koi/header/index_component.rb +23 -0
- data/app/components/koi/header/new_component.rb +40 -0
- data/app/components/koi/header/show_component.rb +51 -0
- data/app/components/koi/header_component.html.erb +16 -0
- data/app/components/koi/header_component.rb +28 -0
- data/app/components/koi/index_table_component.rb +21 -0
- data/app/controllers/admin/admin_users_controller.rb +88 -0
- data/app/controllers/admin/application_controller.rb +9 -0
- data/app/controllers/admin/caches_controller.rb +11 -0
- data/app/controllers/admin/credentials_controller.rb +64 -0
- data/app/controllers/admin/dashboards_controller.rb +7 -0
- data/app/controllers/admin/sessions_controller.rb +78 -0
- data/app/controllers/admin/url_rewrites_controller.rb +87 -0
- data/app/controllers/concerns/koi/controller/has_admin_users.rb +49 -0
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +45 -0
- data/app/controllers/concerns/koi/controller/is_admin_controller.rb +52 -0
- data/app/helpers/katalyst/content/editor/errors.rb +21 -0
- data/app/helpers/katalyst/navigation/editor/errors.rb +21 -0
- data/app/helpers/koi/application_helper.rb +7 -0
- data/app/helpers/koi/date_helper.rb +36 -0
- data/app/helpers/koi/definition_list_helper.rb +92 -0
- data/app/helpers/koi/index_actions_helper.rb +99 -0
- data/app/jobs/koi/application_job.rb +6 -0
- data/app/mailers/koi/application_mailer.rb +8 -0
- data/app/models/admin/credential.rb +14 -0
- data/app/models/admin/user.rb +51 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/concerns/koi/model/archivable.rb +55 -0
- data/app/models/url_rewrite.rb +25 -0
- data/app/views/admin/admin_users/_admin.html+row.erb +4 -0
- data/app/views/admin/admin_users/_authentication.html.erb +15 -0
- data/app/views/admin/admin_users/_fields.html.erb +4 -0
- data/app/views/admin/admin_users/edit.html.erb +11 -0
- data/app/views/admin/admin_users/index.html.erb +9 -0
- data/app/views/admin/admin_users/new.html.erb +11 -0
- data/app/views/admin/admin_users/show.html.erb +22 -0
- data/app/views/admin/credentials/new.html.erb +14 -0
- data/app/views/admin/dashboards/show.html.erb +1 -0
- data/app/views/admin/sessions/new.html.erb +19 -0
- data/app/views/admin/shared/icons/_close.html.erb +8 -0
- data/app/views/admin/shared/icons/_cross.html.erb +3 -0
- data/app/views/admin/shared/icons/_menu.html.erb +3 -0
- data/app/views/admin/shared/icons/_refresh.html.erb +8 -0
- data/app/views/admin/url_rewrites/_form_fields.html.erb +3 -0
- data/app/views/admin/url_rewrites/_url_rewrite.html+row.erb +7 -0
- data/app/views/admin/url_rewrites/edit.html.erb +12 -0
- data/app/views/admin/url_rewrites/index.html.erb +10 -0
- data/app/views/admin/url_rewrites/new.html.erb +11 -0
- data/app/views/admin/url_rewrites/show.html.erb +16 -0
- data/app/views/katalyst/content/asides/_aside.html+form.erb +18 -0
- data/app/views/katalyst/content/columns/_column.html+form.erb +18 -0
- data/app/views/katalyst/content/contents/_content.html+form.erb +20 -0
- data/app/views/katalyst/content/figures/_figure.html+form.erb +17 -0
- data/app/views/katalyst/content/groups/_group.html+form.erb +18 -0
- data/app/views/katalyst/content/items/_item.html+form.erb +18 -0
- data/app/views/katalyst/content/sections/_section.html+form.erb +18 -0
- data/app/views/katalyst/navigation/items/_button.html.erb +15 -0
- data/app/views/katalyst/navigation/items/_heading.html.erb +11 -0
- data/app/views/katalyst/navigation/items/_link.html.erb +13 -0
- data/app/views/katalyst/navigation/menus/edit.html.erb +12 -0
- data/app/views/katalyst/navigation/menus/new.html.erb +9 -0
- data/app/views/katalyst/navigation/menus/show.html.erb +18 -0
- data/app/views/layouts/koi/_environment.html.erb +4 -0
- data/app/views/layouts/koi/_flash.html.erb +8 -0
- data/app/views/layouts/koi/_header.html.erb +11 -0
- data/app/views/layouts/koi/_navigation.html.erb +13 -0
- data/app/views/layouts/koi/_navigation_collapse.html.erb +3 -0
- data/app/views/layouts/koi/_navigation_header.html.erb +6 -0
- data/app/views/layouts/koi/_navigation_item.html.erb +12 -0
- data/app/views/layouts/koi/application.html.erb +59 -0
- data/app/views/layouts/koi/login.html.erb +29 -0
- data/config/importmap.rb +9 -0
- data/config/initializers/flipper.rb +13 -0
- data/config/initializers/pagy.rb +1 -0
- data/config/initializers/time_formats.rb +5 -0
- data/config/locales/koi.en.yml +18 -0
- data/config/locales/pagy.en.yml +6 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20120220130849_devise_create_admins.rb +56 -0
- data/db/migrate/20130509235316_add_url_rewriter.rb +13 -0
- data/db/migrate/20230213053854_convert_devise_admins_to_rails.rb +7 -0
- data/db/migrate/20230412023411_create_admin_user_credentials.rb +20 -0
- data/db/migrate/20230531063707_update_admin_users.rb +37 -0
- data/db/migrate/20230602033610_add_archived_to_admin_users.rb +7 -0
- data/db/seeds.rb +9 -0
- data/lib/generators/koi/active_record/active_record_generator.rb +43 -0
- data/lib/generators/koi/admin/USAGE +8 -0
- data/lib/generators/koi/admin/admin_generator.rb +20 -0
- data/lib/generators/koi/admin_controller/USAGE +17 -0
- data/lib/generators/koi/admin_controller/admin_controller_generator.rb +51 -0
- data/lib/generators/koi/admin_controller/templates/controller.rb.tt +81 -0
- data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +135 -0
- data/lib/generators/koi/admin_route/admin_route_generator.rb +62 -0
- data/lib/generators/koi/admin_views/USAGE +12 -0
- data/lib/generators/koi/admin_views/admin_views_generator.rb +54 -0
- data/lib/generators/koi/admin_views/templates/_fields.html.erb.tt +3 -0
- data/lib/generators/koi/admin_views/templates/_record.html+row.erb.tt +10 -0
- data/lib/generators/koi/admin_views/templates/edit.html.erb.tt +12 -0
- data/lib/generators/koi/admin_views/templates/index.html.erb.tt +7 -0
- data/lib/generators/koi/admin_views/templates/new.html.erb.tt +11 -0
- data/lib/generators/koi/admin_views/templates/show.html.erb.tt +18 -0
- data/lib/govuk_design_system_formbuilder/concerns/file_element.rb +115 -0
- data/lib/govuk_design_system_formbuilder/elements/document.rb +59 -0
- data/lib/govuk_design_system_formbuilder/elements/image.rb +86 -0
- data/lib/katalyst/koi.rb +3 -0
- data/lib/koi/caching.rb +15 -0
- data/lib/koi/config.rb +11 -0
- data/lib/koi/engine.rb +40 -0
- data/lib/koi/form_builder.rb +76 -0
- data/lib/koi/menu/builder.rb +68 -0
- data/lib/koi/menu.rb +46 -0
- data/lib/koi/middleware/url_redirect.rb +44 -0
- data/lib/koi/release.rb +52 -0
- data/lib/koi/version.rb +5 -0
- data/lib/koi.rb +37 -0
- data/spec/factories/admins.rb +9 -0
- data/spec/factories/url_rewrites.rb +9 -0
- metadata +430 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Stimulus controllers. This should ultimately be moved to koi/admin.js
|
|
4
|
+
import "@hotwired/turbo-rails";
|
|
5
|
+
import "@rails/actiontext";
|
|
6
|
+
|
|
7
|
+
const application = Application.start();
|
|
8
|
+
|
|
9
|
+
window.Stimulus = application;
|
|
10
|
+
|
|
11
|
+
export { application };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import FileFieldController from "koi/controllers/file_field_controller";
|
|
2
|
+
|
|
3
|
+
export default class DocumentFieldController extends FileFieldController {
|
|
4
|
+
connect() {
|
|
5
|
+
this.initialPreviewContent = this.filenameTag.text;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
setPreviewContent(content) {
|
|
9
|
+
this.filenameTag.innerText = content;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
showPreview(file) {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
|
|
15
|
+
reader.onload = (e) => {
|
|
16
|
+
if (this.filenameTag) {
|
|
17
|
+
this.filenameTag.innerText = file.name;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
reader.readAsDataURL(file);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get filenameTag() {
|
|
24
|
+
return this.previewTarget.querySelector("p.preview-filename");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class FileFieldController extends Controller {
|
|
4
|
+
static targets = ["preview", "destroy"];
|
|
5
|
+
static values = {
|
|
6
|
+
mimeTypes: Array,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.counter = 0;
|
|
11
|
+
this.initialPreviewContent = null;
|
|
12
|
+
this.onUploadFlag = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
onUpload(event) {
|
|
16
|
+
this.onUploadFlag = true;
|
|
17
|
+
|
|
18
|
+
// Set the file to be destroyed only if it is already persisted
|
|
19
|
+
if (this.hasDestroyTarget) {
|
|
20
|
+
this.destroyTarget.value = false;
|
|
21
|
+
}
|
|
22
|
+
this.previewTarget.classList.remove("hidden");
|
|
23
|
+
|
|
24
|
+
// Show preview only if a file has been selected in the file picker popup. If cancelled, show previous file or do
|
|
25
|
+
// not show preview at all
|
|
26
|
+
if (this.hasPreviewTarget) {
|
|
27
|
+
if (event.currentTarget.files.length > 0) {
|
|
28
|
+
this.showPreview(event.currentTarget.files[0]);
|
|
29
|
+
} else {
|
|
30
|
+
this.setPreviewContent(this.initialPreviewContent);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setDestroy(event) {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
|
|
38
|
+
// If the data is already persisted and another image has been picked from the file picker popup, but the new image
|
|
39
|
+
// is removed, show the original image
|
|
40
|
+
if (this.initialPreviewContent && this.onUploadFlag) {
|
|
41
|
+
this.onUploadFlag = false;
|
|
42
|
+
this.setPreviewContent(this.initialPreviewContent);
|
|
43
|
+
} else {
|
|
44
|
+
// Set image to be destroyed, hide preview and remove image url
|
|
45
|
+
if (this.hasDestroyTarget) {
|
|
46
|
+
this.destroyTarget.value = true;
|
|
47
|
+
}
|
|
48
|
+
if (this.hasPreviewTarget) {
|
|
49
|
+
this.previewTarget.classList.add("hidden");
|
|
50
|
+
this.setPreviewContent("");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.fileInput.value = "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setPreviewContent(content) {
|
|
58
|
+
if (this.filenameTag) {
|
|
59
|
+
this.filenameTag.innerText = text;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
drop(event) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
|
|
66
|
+
const file = this.fileForEvent(event, this.mimeTypesValue);
|
|
67
|
+
if (file) {
|
|
68
|
+
const dT = new DataTransfer();
|
|
69
|
+
dT.items.add(file);
|
|
70
|
+
this.fileInput.files = dT.files;
|
|
71
|
+
this.fileInput.dispatchEvent(new Event("change"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.counter = 0;
|
|
75
|
+
this.element.classList.remove("droppable");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
dragover(event) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
dragenter(event) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
|
|
85
|
+
if (this.counter === 0) {
|
|
86
|
+
this.element.classList.add("droppable");
|
|
87
|
+
}
|
|
88
|
+
this.counter++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
dragleave(event) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
|
|
94
|
+
this.counter--;
|
|
95
|
+
if (this.counter === 0) {
|
|
96
|
+
this.element.classList.remove("droppable");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get fileInput() {
|
|
101
|
+
return this.element.querySelector("input[type='file']");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get filenameTag() {
|
|
105
|
+
if (!this.hasPreviewTarget) return null;
|
|
106
|
+
|
|
107
|
+
return this.previewTarget.querySelector("p.preview-filename");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
showPreview(file) {
|
|
111
|
+
const reader = new FileReader();
|
|
112
|
+
|
|
113
|
+
reader.onload = (e) => {
|
|
114
|
+
if (this.filenameTag) {
|
|
115
|
+
this.filenameTag.innerText = file.name;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
reader.readAsDataURL(file);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Given a drop event, find the first acceptable file.
|
|
123
|
+
* @param event {DropEvent}
|
|
124
|
+
* @param mimeTypes {String[]}
|
|
125
|
+
* @returns {File}
|
|
126
|
+
*/
|
|
127
|
+
fileForEvent(event, mimeTypes) {
|
|
128
|
+
const accept = (file) => mimeTypes.indexOf(file.type) > -1;
|
|
129
|
+
|
|
130
|
+
let file;
|
|
131
|
+
|
|
132
|
+
if (event.dataTransfer.items) {
|
|
133
|
+
const item = [...event.dataTransfer.items].find(accept);
|
|
134
|
+
if (item) {
|
|
135
|
+
file = item.getAsFile();
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
file = [...event.dataTransfer.files].find(accept);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class FlashController extends Controller {
|
|
4
|
+
close(e) {
|
|
5
|
+
e.target.closest("li").remove();
|
|
6
|
+
|
|
7
|
+
// remove the flash container if there are no more flashes
|
|
8
|
+
if (this.element.children.length === 0) {
|
|
9
|
+
this.element.remove();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
A stimulus controller to request form submissions.
|
|
5
|
+
This controller should be attached to a form element.
|
|
6
|
+
*/
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
requestSubmit() {
|
|
9
|
+
this.element.requestSubmit();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import FileFieldController from "koi/controllers/file_field_controller";
|
|
2
|
+
|
|
3
|
+
export default class ImageFieldController extends FileFieldController {
|
|
4
|
+
connect() {
|
|
5
|
+
this.initialPreviewContent = this.imageTag.getAttribute("src");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
setPreviewContent(content) {
|
|
9
|
+
this.imageTag.src = content;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
showPreview(file) {
|
|
13
|
+
const reader = new FileReader();
|
|
14
|
+
|
|
15
|
+
reader.onload = (e) => {
|
|
16
|
+
this.imageTag.src = e.target.result;
|
|
17
|
+
};
|
|
18
|
+
reader.readAsDataURL(file);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get imageTag() {
|
|
22
|
+
return this.previewTarget.querySelector("img");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { application } from "koi/controllers/application";
|
|
2
|
+
|
|
3
|
+
// Eager load all controllers defined in the import map under controllers/**/*_controller
|
|
4
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
|
|
5
|
+
eagerLoadControllersFrom("controllers", application);
|
|
6
|
+
eagerLoadControllersFrom("koi/controllers", application);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class IndexActionsController extends Controller {
|
|
4
|
+
static targets = ["create", "search", "sort"];
|
|
5
|
+
|
|
6
|
+
initialize() {
|
|
7
|
+
// debounce search
|
|
8
|
+
this.update = debounce(this, this.update);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
disconnect() {
|
|
12
|
+
clearTimeout(this.timer);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
create() {
|
|
16
|
+
this.createTarget.click();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
search() {
|
|
20
|
+
this.searchTarget.focus();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
clear() {
|
|
24
|
+
this.searchTarget.value = "";
|
|
25
|
+
this.searchTarget.closest("form").requestSubmit();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update() {
|
|
29
|
+
this.searchTarget.closest("form").requestSubmit();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
submit() {
|
|
33
|
+
if (this.searchTarget.value === "") {
|
|
34
|
+
this.searchTarget.disabled = true;
|
|
35
|
+
}
|
|
36
|
+
if (this.sortTarget.value === "") {
|
|
37
|
+
this.sortTarget.disabled = true;
|
|
38
|
+
}
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
this.searchTarget.disabled = false;
|
|
41
|
+
this.sortTarget.disabled = false;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
nextPage() {
|
|
46
|
+
this.element.parentElement.querySelector(".pagination .next a").click();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
prevPage() {
|
|
50
|
+
this.element.parentElement.querySelector(".pagination .prev a").click();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function debounce(self, f) {
|
|
55
|
+
return (...args) => {
|
|
56
|
+
clearTimeout(self.timer);
|
|
57
|
+
self.timer = setTimeout(() => {
|
|
58
|
+
f.apply(self, ...args);
|
|
59
|
+
}, 300);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const DEBUG = false;
|
|
4
|
+
|
|
5
|
+
export default class KeyboardController extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
mapping: String,
|
|
8
|
+
depth: { type: Number, default: 2 },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
event(cause) {
|
|
12
|
+
if (isFormField(cause.target) || this.#ignore(cause)) return;
|
|
13
|
+
|
|
14
|
+
const key = this.describeEvent(cause);
|
|
15
|
+
|
|
16
|
+
this.buffer = [...(this.buffer || []), key].slice(0 - this.depthValue);
|
|
17
|
+
|
|
18
|
+
if (DEBUG) console.debug("[keyboard] buffer:", ...this.buffer);
|
|
19
|
+
|
|
20
|
+
// test whether the tail of the buffer matches any of the configured chords
|
|
21
|
+
const action = this.buffer.reduceRight((mapping, key) => {
|
|
22
|
+
if (typeof mapping === "string" || typeof mapping === "undefined") {
|
|
23
|
+
return mapping;
|
|
24
|
+
} else {
|
|
25
|
+
return mapping[key];
|
|
26
|
+
}
|
|
27
|
+
}, this.mappings);
|
|
28
|
+
|
|
29
|
+
// if we don't have a string we may have a miss or an incomplete chord
|
|
30
|
+
if (typeof action !== "string") return;
|
|
31
|
+
|
|
32
|
+
// clear the buffer and prevent the key from being consumed elsewhere
|
|
33
|
+
this.buffer = [];
|
|
34
|
+
cause.preventDefault();
|
|
35
|
+
|
|
36
|
+
if (DEBUG) console.debug("[keyboard] event: %s", action);
|
|
37
|
+
|
|
38
|
+
// fire the configured event
|
|
39
|
+
const event = new CustomEvent(action, {
|
|
40
|
+
detail: { cause: cause },
|
|
41
|
+
bubbles: true,
|
|
42
|
+
});
|
|
43
|
+
cause.target.dispatchEvent(event);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param event KeyboardEvent input event to describe
|
|
48
|
+
* @return String description of keyboard event, e.g. 'C-KeyV' (CTRL+V)
|
|
49
|
+
*/
|
|
50
|
+
describeEvent(event) {
|
|
51
|
+
return [
|
|
52
|
+
event.ctrlKey && "C",
|
|
53
|
+
event.metaKey && "M",
|
|
54
|
+
event.altKey && "A",
|
|
55
|
+
event.shiftKey && "S",
|
|
56
|
+
event.code,
|
|
57
|
+
]
|
|
58
|
+
.filter((w) => w)
|
|
59
|
+
.join("-");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a tree for efficiently looking up key chords, where the last key in the sequence
|
|
64
|
+
* is the first key in tree.
|
|
65
|
+
*/
|
|
66
|
+
get mappings() {
|
|
67
|
+
const inputs = this.mappingValue
|
|
68
|
+
.replaceAll(/\s+/g, " ")
|
|
69
|
+
.split(" ")
|
|
70
|
+
.filter((f) => f.length > 0);
|
|
71
|
+
const mappings = {};
|
|
72
|
+
|
|
73
|
+
inputs.forEach((mapping) => this.#parse(mappings, mapping));
|
|
74
|
+
|
|
75
|
+
// memoize the result
|
|
76
|
+
Object.defineProperty(this, "mappings", {
|
|
77
|
+
value: mappings,
|
|
78
|
+
writable: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return mappings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a key chord pattern and an event and store it in the inverted tree lookup structure.
|
|
86
|
+
*
|
|
87
|
+
* @param mappings inverted tree lookup for key chords
|
|
88
|
+
* @param mapping input definition, e.g. "C-KeyC+C-KeyV->paste"
|
|
89
|
+
*/
|
|
90
|
+
#parse(mappings, mapping) {
|
|
91
|
+
const [pattern, event] = mapping.split("->");
|
|
92
|
+
const keys = pattern.split("+");
|
|
93
|
+
const first = keys.shift();
|
|
94
|
+
|
|
95
|
+
mappings = keys.reduceRight(
|
|
96
|
+
(mappings, key) => (mappings[key] ||= {}),
|
|
97
|
+
mappings,
|
|
98
|
+
);
|
|
99
|
+
mappings[first] = event;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Ignore modifier keys, as they will be captured in normal key presses.
|
|
104
|
+
*
|
|
105
|
+
* @param event KeyboardEvent
|
|
106
|
+
* @returns {boolean} true if key event should be ignored
|
|
107
|
+
*/
|
|
108
|
+
#ignore(event) {
|
|
109
|
+
switch (event.code) {
|
|
110
|
+
case "ControlLeft":
|
|
111
|
+
case "ControlRight":
|
|
112
|
+
case "MetaLeft":
|
|
113
|
+
case "MetaRight":
|
|
114
|
+
case "ShiftLeft":
|
|
115
|
+
case "ShiftRight":
|
|
116
|
+
case "AltLeft":
|
|
117
|
+
case "AltRight":
|
|
118
|
+
return true;
|
|
119
|
+
default:
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detect input nodes where we should not listen for events.
|
|
127
|
+
*
|
|
128
|
+
* Credit: github.com
|
|
129
|
+
*/
|
|
130
|
+
function isFormField(element) {
|
|
131
|
+
if (!(element instanceof HTMLElement)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const name = element.nodeName.toLowerCase();
|
|
136
|
+
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
137
|
+
return (
|
|
138
|
+
name === "select" ||
|
|
139
|
+
name === "textarea" ||
|
|
140
|
+
name === "trix-editor" ||
|
|
141
|
+
(name === "input" &&
|
|
142
|
+
type !== "submit" &&
|
|
143
|
+
type !== "reset" &&
|
|
144
|
+
type !== "checkbox" &&
|
|
145
|
+
type !== "radio" &&
|
|
146
|
+
type !== "file") ||
|
|
147
|
+
element.isContentEditable
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
export default class NavigationController extends Controller {
|
|
4
|
+
static targets = ["filter"];
|
|
5
|
+
|
|
6
|
+
focus() {
|
|
7
|
+
this.filterTarget.focus();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
filter() {
|
|
11
|
+
const filter = this.filterTarget.value;
|
|
12
|
+
this.clearFilter(filter);
|
|
13
|
+
|
|
14
|
+
if (filter.length > 0) {
|
|
15
|
+
this.applyFilter(filter);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
go() {
|
|
20
|
+
this.element.querySelector("li:not([hidden]) > a").click();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
applyFilter(filter) {
|
|
24
|
+
// hide items that don't match the search filter
|
|
25
|
+
this.links
|
|
26
|
+
.filter(
|
|
27
|
+
(li) =>
|
|
28
|
+
!this.prefixSearch(filter.toLowerCase(), li.innerText.toLowerCase()),
|
|
29
|
+
)
|
|
30
|
+
.forEach((li) => {
|
|
31
|
+
li.toggleAttribute("hidden", true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.menus
|
|
35
|
+
.filter((li) => !li.matches("li:has(li:not([hidden]) > a)"))
|
|
36
|
+
.forEach((li) => {
|
|
37
|
+
li.toggleAttribute("hidden", true);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clearFilter(filter) {
|
|
42
|
+
this.element.querySelectorAll("li").forEach((li) => {
|
|
43
|
+
li.toggleAttribute("hidden", false);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
prefixSearch(needle, haystack) {
|
|
48
|
+
const haystackLength = haystack.length;
|
|
49
|
+
const needleLength = needle.length;
|
|
50
|
+
if (needleLength > haystackLength) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (needleLength === haystackLength) {
|
|
54
|
+
return needle === haystack;
|
|
55
|
+
}
|
|
56
|
+
outer: for (let i = 0, j = 0; i < needleLength; i++) {
|
|
57
|
+
const needleChar = needle.charCodeAt(i);
|
|
58
|
+
if (needleChar === 32) {
|
|
59
|
+
// skip ahead to next space in the haystack
|
|
60
|
+
while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
while (j < haystackLength) {
|
|
64
|
+
if (haystack.charCodeAt(j++) === needleChar) continue outer;
|
|
65
|
+
// skip ahead to the next space in the haystack
|
|
66
|
+
while (j < haystackLength && haystack.charCodeAt(j++) !== 32) {}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
toggle() {
|
|
74
|
+
this.element.toggleAttribute("aria-expanded");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get links() {
|
|
78
|
+
return Array.from(this.element.querySelectorAll("li:has(> a)"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get menus() {
|
|
82
|
+
return Array.from(this.element.querySelectorAll("li:has(> ul)"));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import { Transition } from "koi/utils/transition";
|
|
3
|
+
|
|
4
|
+
export default class ShowHideController extends Controller {
|
|
5
|
+
static targets = ["content"];
|
|
6
|
+
|
|
7
|
+
toggle() {
|
|
8
|
+
const element = this.contentTarget;
|
|
9
|
+
const hide = element.toggleAttribute("data-collapsed");
|
|
10
|
+
|
|
11
|
+
// cancel previous animation, if any
|
|
12
|
+
if (this.transition) this.transition.cancel();
|
|
13
|
+
|
|
14
|
+
const transition = (this.transition = new Transition(element)
|
|
15
|
+
.addCallback("starting", function () {
|
|
16
|
+
element.setAttribute("data-collapsed-transitioning", "true");
|
|
17
|
+
})
|
|
18
|
+
.addCallback("complete", function () {
|
|
19
|
+
element.removeAttribute("data-collapsed-transitioning");
|
|
20
|
+
}));
|
|
21
|
+
hide ? transition.collapse() : transition.expand();
|
|
22
|
+
|
|
23
|
+
transition.start();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connect an input (e.g. title) to slug.
|
|
5
|
+
*/
|
|
6
|
+
export default class SluggableController extends Controller {
|
|
7
|
+
static targets = ["source", "slug"];
|
|
8
|
+
static values = {
|
|
9
|
+
slug: String,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
sourceChanged(e) {
|
|
13
|
+
if (this.slugValue === "") {
|
|
14
|
+
this.slugTarget.value = parameterize(this.sourceTarget.value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
slugChanged(e) {
|
|
19
|
+
this.slugValue = this.slugTarget.value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parameterize(input) {
|
|
24
|
+
return input
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/'/g, "-")
|
|
27
|
+
.replace(/[^-\w\s]/g, "")
|
|
28
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
29
|
+
.replace(/(^-|-$)/g, "");
|
|
30
|
+
}
|