rails_admin_next 1.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/Gemfile +46 -0
- data/LICENSE.md +21 -0
- data/README.md +317 -0
- data/Rakefile +25 -0
- data/app/assets/javascripts/rails_admin/custom/ui.js +1 -0
- data/app/assets/stylesheets/rails_admin/framework.css +2375 -0
- data/app/assets/stylesheets/rails_admin/skin.css +472 -0
- data/app/assets/stylesheets/rails_admin/tokens.css +281 -0
- data/app/assets/stylesheets/rails_admin.css +47 -0
- data/app/controllers/rails_admin_next/application_controller.rb +98 -0
- data/app/controllers/rails_admin_next/main_controller.rb +185 -0
- data/app/helpers/rails_admin_next/application_helper.rb +254 -0
- data/app/helpers/rails_admin_next/form_builder.rb +173 -0
- data/app/helpers/rails_admin_next/main_helper.rb +63 -0
- data/app/views/layouts/rails_admin_next/_head.html.erb +29 -0
- data/app/views/layouts/rails_admin_next/_navigation.html.erb +18 -0
- data/app/views/layouts/rails_admin_next/_secondary_navigation.html.erb +27 -0
- data/app/views/layouts/rails_admin_next/_sidebar_navigation.html.erb +6 -0
- data/app/views/layouts/rails_admin_next/application.html.erb +42 -0
- data/app/views/layouts/rails_admin_next/content.html.erb +22 -0
- data/app/views/layouts/rails_admin_next/modal.js.erb +7 -0
- data/app/views/rails_admin_next/main/_dashboard_history.html.erb +46 -0
- data/app/views/rails_admin_next/main/_delete_notice.html.erb +36 -0
- data/app/views/rails_admin_next/main/_form_action_text.html.erb +2 -0
- data/app/views/rails_admin_next/main/_form_boolean.html.erb +15 -0
- data/app/views/rails_admin_next/main/_form_colorpicker.html.erb +6 -0
- data/app/views/rails_admin_next/main/_form_datetime.html.erb +14 -0
- data/app/views/rails_admin_next/main/_form_enumeration.html.erb +26 -0
- data/app/views/rails_admin_next/main/_form_field.html.erb +2 -0
- data/app/views/rails_admin_next/main/_form_file_upload.html.erb +18 -0
- data/app/views/rails_admin_next/main/_form_filtering_multiselect.html.erb +20 -0
- data/app/views/rails_admin_next/main/_form_filtering_select.html.erb +22 -0
- data/app/views/rails_admin_next/main/_form_multiple_file_upload.html.erb +21 -0
- data/app/views/rails_admin_next/main/_form_nested_many.html.erb +20 -0
- data/app/views/rails_admin_next/main/_form_nested_one.html.erb +19 -0
- data/app/views/rails_admin_next/main/_form_polymorphic_association.html.erb +24 -0
- data/app/views/rails_admin_next/main/_form_text.html.erb +2 -0
- data/app/views/rails_admin_next/main/_pagination.html.erb +54 -0
- data/app/views/rails_admin_next/main/_submit_buttons.html.erb +26 -0
- data/app/views/rails_admin_next/main/bulk_delete.html.erb +19 -0
- data/app/views/rails_admin_next/main/dashboard.html.erb +64 -0
- data/app/views/rails_admin_next/main/delete.html.erb +21 -0
- data/app/views/rails_admin_next/main/edit.html.erb +3 -0
- data/app/views/rails_admin_next/main/export.html.erb +149 -0
- data/app/views/rails_admin_next/main/history.html.erb +80 -0
- data/app/views/rails_admin_next/main/index.html.erb +186 -0
- data/app/views/rails_admin_next/main/new.html.erb +3 -0
- data/app/views/rails_admin_next/main/show.html.erb +30 -0
- data/config/brakeman.ignore +145 -0
- data/config/importmap.rails_admin.rb +29 -0
- data/config/initializers/active_record_extensions.rb +21 -0
- data/config/locales/rails_admin_next.en.yml +158 -0
- data/config/routes.rb +14 -0
- data/lib/generators/rails_admin_next/install_generator.rb +39 -0
- data/lib/generators/rails_admin_next/templates/initializer.erb +40 -0
- data/lib/generators/rails_admin_next/utils.rb +21 -0
- data/lib/rails_admin_next/abstract_model.rb +266 -0
- data/lib/rails_admin_next/adapters/active_record/association.rb +130 -0
- data/lib/rails_admin_next/adapters/active_record/object_extension.rb +13 -0
- data/lib/rails_admin_next/adapters/active_record/property.rb +58 -0
- data/lib/rails_admin_next/adapters/active_record.rb +371 -0
- data/lib/rails_admin_next/config/actions/base.rb +160 -0
- data/lib/rails_admin_next/config/actions/bulk_delete.rb +67 -0
- data/lib/rails_admin_next/config/actions/dashboard.rb +62 -0
- data/lib/rails_admin_next/config/actions/delete.rb +58 -0
- data/lib/rails_admin_next/config/actions/edit.rb +56 -0
- data/lib/rails_admin_next/config/actions/export.rb +47 -0
- data/lib/rails_admin_next/config/actions/history_index.rb +40 -0
- data/lib/rails_admin_next/config/actions/history_show.rb +40 -0
- data/lib/rails_admin_next/config/actions/index.rb +108 -0
- data/lib/rails_admin_next/config/actions/new.rb +69 -0
- data/lib/rails_admin_next/config/actions/show.rb +36 -0
- data/lib/rails_admin_next/config/actions/show_in_app.rb +37 -0
- data/lib/rails_admin_next/config/actions.rb +122 -0
- data/lib/rails_admin_next/config/configurable.rb +107 -0
- data/lib/rails_admin_next/config/const_load_suppressor.rb +78 -0
- data/lib/rails_admin_next/config/fields/association.rb +169 -0
- data/lib/rails_admin_next/config/fields/base.rb +391 -0
- data/lib/rails_admin_next/config/fields/collection_association.rb +90 -0
- data/lib/rails_admin_next/config/fields/factories/action_text.rb +14 -0
- data/lib/rails_admin_next/config/fields/factories/active_storage.rb +34 -0
- data/lib/rails_admin_next/config/fields/factories/association.rb +32 -0
- data/lib/rails_admin_next/config/fields/factories/encrypted_password.rb +28 -0
- data/lib/rails_admin_next/config/fields/factories/enum.rb +23 -0
- data/lib/rails_admin_next/config/fields/factories/password.rb +19 -0
- data/lib/rails_admin_next/config/fields/group.rb +77 -0
- data/lib/rails_admin_next/config/fields/singular_association.rb +62 -0
- data/lib/rails_admin_next/config/fields/types/action_text.rb +20 -0
- data/lib/rails_admin_next/config/fields/types/active_record_enum.rb +62 -0
- data/lib/rails_admin_next/config/fields/types/active_storage.rb +70 -0
- data/lib/rails_admin_next/config/fields/types/all.rb +32 -0
- data/lib/rails_admin_next/config/fields/types/belongs_to_association.rb +48 -0
- data/lib/rails_admin_next/config/fields/types/boolean.rb +70 -0
- data/lib/rails_admin_next/config/fields/types/citext.rb +15 -0
- data/lib/rails_admin_next/config/fields/types/color.rb +43 -0
- data/lib/rails_admin_next/config/fields/types/date.rb +38 -0
- data/lib/rails_admin_next/config/fields/types/datetime.rb +79 -0
- data/lib/rails_admin_next/config/fields/types/decimal.rb +23 -0
- data/lib/rails_admin_next/config/fields/types/enum.rb +52 -0
- data/lib/rails_admin_next/config/fields/types/file_upload.rb +87 -0
- data/lib/rails_admin_next/config/fields/types/float.rb +23 -0
- data/lib/rails_admin_next/config/fields/types/has_and_belongs_to_many_association.rb +16 -0
- data/lib/rails_admin_next/config/fields/types/has_many_association.rb +16 -0
- data/lib/rails_admin_next/config/fields/types/has_one_association.rb +35 -0
- data/lib/rails_admin_next/config/fields/types/hidden.rb +31 -0
- data/lib/rails_admin_next/config/fields/types/inet.rb +15 -0
- data/lib/rails_admin_next/config/fields/types/integer.rb +20 -0
- data/lib/rails_admin_next/config/fields/types/json.rb +37 -0
- data/lib/rails_admin_next/config/fields/types/multiple_active_storage.rb +82 -0
- data/lib/rails_admin_next/config/fields/types/multiple_file_upload.rb +134 -0
- data/lib/rails_admin_next/config/fields/types/numeric.rb +24 -0
- data/lib/rails_admin_next/config/fields/types/password.rb +45 -0
- data/lib/rails_admin_next/config/fields/types/polymorphic_association.rb +118 -0
- data/lib/rails_admin_next/config/fields/types/serialized.rb +28 -0
- data/lib/rails_admin_next/config/fields/types/string.rb +50 -0
- data/lib/rails_admin_next/config/fields/types/string_like.rb +25 -0
- data/lib/rails_admin_next/config/fields/types/text.rb +28 -0
- data/lib/rails_admin_next/config/fields/types/time.rb +39 -0
- data/lib/rails_admin_next/config/fields/types/timestamp.rb +16 -0
- data/lib/rails_admin_next/config/fields/types/uuid.rb +15 -0
- data/lib/rails_admin_next/config/fields/types.rb +29 -0
- data/lib/rails_admin_next/config/fields.rb +88 -0
- data/lib/rails_admin_next/config/groupable.rb +20 -0
- data/lib/rails_admin_next/config/has_description.rb +14 -0
- data/lib/rails_admin_next/config/has_fields.rb +144 -0
- data/lib/rails_admin_next/config/has_groups.rb +27 -0
- data/lib/rails_admin_next/config/hideable.rb +30 -0
- data/lib/rails_admin_next/config/inspectable.rb +39 -0
- data/lib/rails_admin_next/config/lazy_model.rb +74 -0
- data/lib/rails_admin_next/config/model.rb +126 -0
- data/lib/rails_admin_next/config/proxyable/proxy.rb +42 -0
- data/lib/rails_admin_next/config/proxyable.rb +26 -0
- data/lib/rails_admin_next/config/sections/base.rb +36 -0
- data/lib/rails_admin_next/config/sections/create.rb +13 -0
- data/lib/rails_admin_next/config/sections/edit.rb +13 -0
- data/lib/rails_admin_next/config/sections/export.rb +13 -0
- data/lib/rails_admin_next/config/sections/list.rb +63 -0
- data/lib/rails_admin_next/config/sections/modal.rb +12 -0
- data/lib/rails_admin_next/config/sections/nested.rb +12 -0
- data/lib/rails_admin_next/config/sections/show.rb +12 -0
- data/lib/rails_admin_next/config/sections/update.rb +12 -0
- data/lib/rails_admin_next/config/sections.rb +39 -0
- data/lib/rails_admin_next/config.rb +436 -0
- data/lib/rails_admin_next/engine.rb +91 -0
- data/lib/rails_admin_next/extension.rb +35 -0
- data/lib/rails_admin_next/extensions/cancancan/authorization_adapter.rb +90 -0
- data/lib/rails_admin_next/extensions/cancancan.rb +5 -0
- data/lib/rails_admin_next/extensions/controller_extension.rb +8 -0
- data/lib/rails_admin_next/extensions/paper_trail/auditing_adapter.rb +192 -0
- data/lib/rails_admin_next/extensions/paper_trail.rb +5 -0
- data/lib/rails_admin_next/extensions/pundit/authorization_adapter.rb +72 -0
- data/lib/rails_admin_next/extensions/pundit.rb +5 -0
- data/lib/rails_admin_next/extensions/url_for_extension.rb +15 -0
- data/lib/rails_admin_next/icons.rb +143 -0
- data/lib/rails_admin_next/support/composite_keys_serializer.rb +15 -0
- data/lib/rails_admin_next/support/csv_converter.rb +100 -0
- data/lib/rails_admin_next/support/hash_helper.rb +31 -0
- data/lib/rails_admin_next/support/paginated_collection.rb +79 -0
- data/lib/rails_admin_next/version.rb +17 -0
- data/lib/rails_admin_next.rb +72 -0
- data/lib/tasks/rails_admin.rake +40 -0
- data/src/rails_admin/application.js +15 -0
- data/src/rails_admin/base.js +12 -0
- data/src/rails_admin/controllers/alert_controller.js +28 -0
- data/src/rails_admin/controllers/collapse_controller.js +16 -0
- data/src/rails_admin/controllers/dropdown_controller.js +110 -0
- data/src/rails_admin/controllers/feedback_controller.js +73 -0
- data/src/rails_admin/controllers/filter_box_controller.js +346 -0
- data/src/rails_admin/controllers/filtering_multiselect_controller.js +386 -0
- data/src/rails_admin/controllers/filtering_select_controller.js +509 -0
- data/src/rails_admin/controllers/index.js +31 -0
- data/src/rails_admin/controllers/modal_controller.js +142 -0
- data/src/rails_admin/controllers/nested_form_controller.js +117 -0
- data/src/rails_admin/controllers/polymorphic_controller.js +26 -0
- data/src/rails_admin/controllers/remote_form_controller.js +173 -0
- data/src/rails_admin/controllers/sidescroll_controller.js +23 -0
- data/src/rails_admin/dom-ready.js +20 -0
- data/src/rails_admin/dom.js +19 -0
- data/src/rails_admin/i18n.js +20 -0
- data/src/rails_admin/icons.js +64 -0
- data/src/rails_admin/interactions.js +221 -0
- data/src/rails_admin/select_query.js +65 -0
- metadata +346 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAdminNext
|
|
4
|
+
class Version
|
|
5
|
+
MAJOR = 1
|
|
6
|
+
MINOR = 0
|
|
7
|
+
PATCH = 0
|
|
8
|
+
PRE = nil
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# @return [String]
|
|
12
|
+
def to_s
|
|
13
|
+
[MAJOR, MINOR, PATCH, PRE].compact.join(".")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_admin_next/engine"
|
|
4
|
+
require "rails_admin_next/abstract_model"
|
|
5
|
+
require "rails_admin_next/config"
|
|
6
|
+
require "rails_admin_next/config/const_load_suppressor"
|
|
7
|
+
require "rails_admin_next/icons"
|
|
8
|
+
require "rails_admin_next/extension"
|
|
9
|
+
require "rails_admin_next/extensions/cancancan"
|
|
10
|
+
require "rails_admin_next/extensions/pundit"
|
|
11
|
+
require "rails_admin_next/extensions/paper_trail"
|
|
12
|
+
require "rails_admin_next/support/csv_converter"
|
|
13
|
+
require "rails_admin_next/support/hash_helper"
|
|
14
|
+
require "rails_admin_next/support/paginated_collection"
|
|
15
|
+
require "yaml"
|
|
16
|
+
|
|
17
|
+
module RailsAdminNext
|
|
18
|
+
extend RailsAdminNext::Config::ConstLoadSuppressor
|
|
19
|
+
|
|
20
|
+
# Setup RailsAdminNext
|
|
21
|
+
#
|
|
22
|
+
# Given the first argument is a model class, a model class name
|
|
23
|
+
# or an abstract model object proxies to model configuration method.
|
|
24
|
+
#
|
|
25
|
+
# If only a block is passed it is stored to initializer stack to be evaluated
|
|
26
|
+
# on first request in production mode and on each request in development. If
|
|
27
|
+
# initialization has already occurred (in other words RailsAdminNext.setup has
|
|
28
|
+
# been called) the block will be added to stack and evaluated at once.
|
|
29
|
+
#
|
|
30
|
+
# Otherwise returns RailsAdminNext::Config class.
|
|
31
|
+
#
|
|
32
|
+
# @see RailsAdminNext::Config
|
|
33
|
+
def self.config(entity = nil, &)
|
|
34
|
+
if entity
|
|
35
|
+
RailsAdminNext::Config.model(entity, &)
|
|
36
|
+
elsif block_given?
|
|
37
|
+
RailsAdminNext::Config::ConstLoadSuppressor.suppressing { yield(RailsAdminNext::Config) }
|
|
38
|
+
else
|
|
39
|
+
RailsAdminNext::Config
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Deprecator for RailsAdminNext's own configuration options. Rails 8.1 made the class-level
|
|
44
|
+
# ActiveSupport::Deprecation.warn private, so deprecations go through an instance deprecator.
|
|
45
|
+
# Intentionally left unregistered, so a deprecated-but-removed option warns rather than raising
|
|
46
|
+
# even in a host whose deprecation behavior is :raise — upgrading must never blow up at boot.
|
|
47
|
+
def self.deprecator
|
|
48
|
+
@deprecator ||= ActiveSupport::Deprecation.new(RailsAdminNext::Version.to_s, "RailsAdminNext")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Backwards-compatible with safe_yaml/load when SafeYAML isn't available.
|
|
52
|
+
# Evaluates available YAML loaders at boot and creates appropriate method,
|
|
53
|
+
# so no conditionals are required at runtime.
|
|
54
|
+
begin
|
|
55
|
+
require "safe_yaml/load"
|
|
56
|
+
def self.yaml_load(yaml)
|
|
57
|
+
SafeYAML.load(yaml)
|
|
58
|
+
end
|
|
59
|
+
rescue LoadError
|
|
60
|
+
if YAML.respond_to?(:safe_load)
|
|
61
|
+
def self.yaml_load(yaml)
|
|
62
|
+
YAML.safe_load(yaml)
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
raise LoadError.new "Safe-loading of YAML is not available. Please install 'safe_yaml' or install Psych 2.0+"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.yaml_dump(object)
|
|
70
|
+
YAML.dump(object)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :rails_admin do
|
|
4
|
+
desc "Install rails_admin_next"
|
|
5
|
+
task :install do
|
|
6
|
+
system "rails g rails_admin_next:install"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc "CI env for GitHub Actions"
|
|
10
|
+
task :prepare_ci_env do
|
|
11
|
+
adapter = ENV["CI_DB_ADAPTER"] || "sqlite3"
|
|
12
|
+
database = ((adapter == "sqlite3") ? "db/development.sqlite3" : "ci_rails_admin_next")
|
|
13
|
+
username =
|
|
14
|
+
case adapter
|
|
15
|
+
when "postgresql"
|
|
16
|
+
"postgres"
|
|
17
|
+
when "mysql2"
|
|
18
|
+
"root"
|
|
19
|
+
else
|
|
20
|
+
""
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
configuration = {
|
|
24
|
+
"test" => {
|
|
25
|
+
"adapter" => adapter,
|
|
26
|
+
"database" => database,
|
|
27
|
+
"username" => username,
|
|
28
|
+
"password" => ((adapter == "postgresql") ? "postgres" : ""),
|
|
29
|
+
"host" => "127.0.0.1",
|
|
30
|
+
"encoding" => "utf8",
|
|
31
|
+
"pool" => 5,
|
|
32
|
+
"timeout" => 5000
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
filename = Rails.root.join("config/database.yml")
|
|
37
|
+
|
|
38
|
+
File.write(filename, configuration.to_yaml)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Application } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// RailsAdminNext runs its OWN Stimulus application, separate from any Stimulus app the
|
|
4
|
+
// host may run for its own UI. Controllers are registered against this instance
|
|
5
|
+
// (see ./controllers/index.js) so the engine never reaches into — or depends on —
|
|
6
|
+
// the host's controllers/ directory.
|
|
7
|
+
const application = Application.start();
|
|
8
|
+
application.warnings = false;
|
|
9
|
+
application.debug = false;
|
|
10
|
+
|
|
11
|
+
// Expose for debugging and host extension under the engine namespace.
|
|
12
|
+
window.RailsAdminNext = window.RailsAdminNext || {};
|
|
13
|
+
window.RailsAdminNext.application = application;
|
|
14
|
+
|
|
15
|
+
export { application };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "@hotwired/turbo-rails";
|
|
2
|
+
|
|
3
|
+
// RailsAdminNext's own Stimulus application + controllers.
|
|
4
|
+
import "rails_admin/application";
|
|
5
|
+
import "rails_admin/controllers";
|
|
6
|
+
|
|
7
|
+
// Vanilla, jQuery-free document-level page interactions.
|
|
8
|
+
import "rails_admin/interactions";
|
|
9
|
+
|
|
10
|
+
// Owns rails_admin.dom_ready. Imported last so every listener registered above
|
|
11
|
+
// is in place before the first dispatch fires.
|
|
12
|
+
import "rails_admin/dom-ready";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const FOCUSABLE =
|
|
4
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
5
|
+
|
|
6
|
+
// Dismissible flash alert. Replaces Bootstrap's data-bs-dismiss="alert".
|
|
7
|
+
// Removing an alert with the keyboard would otherwise drop focus to <body>, so
|
|
8
|
+
// on dismiss focus returns to the nearest focusable element preceding the alert
|
|
9
|
+
// (falling back to the first focusable element on the page).
|
|
10
|
+
export default class extends Controller {
|
|
11
|
+
dismiss() {
|
|
12
|
+
const target = this.#focusReturnTarget();
|
|
13
|
+
this.element.remove();
|
|
14
|
+
target?.focus();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#focusReturnTarget() {
|
|
18
|
+
const focusable = Array.from(document.querySelectorAll(FOCUSABLE)).filter(
|
|
19
|
+
(el) => !this.element.contains(el) && el.offsetParent !== null
|
|
20
|
+
);
|
|
21
|
+
const preceding = focusable.filter(
|
|
22
|
+
(el) =>
|
|
23
|
+
this.element.compareDocumentPosition(el) &
|
|
24
|
+
Node.DOCUMENT_POSITION_PRECEDING
|
|
25
|
+
);
|
|
26
|
+
return preceding.at(-1) || focusable[0] || null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Responsive navbar toggler (WAI-ARIA disclosure): the hamburger toggles
|
|
4
|
+
// `aria-expanded` and the region's Bootstrap `.collapse`/`.show` classes
|
|
5
|
+
// (`.collapse:not(.show)` is hidden; `.navbar-expand-md` forces the region
|
|
6
|
+
// visible above the breakpoint). The sidebar groups use native <details>, but a
|
|
7
|
+
// <details> navbar can't be force-expanded at the md breakpoint, so the navbar
|
|
8
|
+
// stays class-driven.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["trigger", "region"];
|
|
11
|
+
|
|
12
|
+
toggle() {
|
|
13
|
+
const expanded = this.regionTarget.classList.toggle("show");
|
|
14
|
+
this.triggerTarget.setAttribute("aria-expanded", String(expanded));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const ITEM = ".dropdown-item";
|
|
4
|
+
|
|
5
|
+
// Menu button (WAI-ARIA APG) on a native popover. The toggle is a
|
|
6
|
+
// <button popovertarget> and the menu carries popover="auto", so the platform
|
|
7
|
+
// owns the top layer, click-to-toggle, and outside-click/Escape light
|
|
8
|
+
// dismissal (do NOT reimplement those). The controller lives on the toggle;
|
|
9
|
+
// its menu is the next sibling (`.dropdown-menu`), so the menu items stay out
|
|
10
|
+
// of Stimulus's action-parsing scope — important because the bulk-action
|
|
11
|
+
// items carry their own `data-action` attribute read by interactions.js.
|
|
12
|
+
//
|
|
13
|
+
// What is left for JS:
|
|
14
|
+
// - ArrowDown/ArrowUp on the closed toggle open the menu onto its
|
|
15
|
+
// first/last item; opening any other way focuses the first item.
|
|
16
|
+
// - ArrowUp/Down/Home/End rove focus between items; Tab closes.
|
|
17
|
+
// - an item click closes the menu (clicks inside a popover don't dismiss).
|
|
18
|
+
// - aria-expanded on the toggle syncs from the menu's popover toggle event,
|
|
19
|
+
// and focus returns to the toggle when a light dismissal dropped it on
|
|
20
|
+
// <body> (Escape restores it natively; an outside click on a
|
|
21
|
+
// non-focusable target does not).
|
|
22
|
+
export default class extends Controller {
|
|
23
|
+
#focusOnOpen = "first";
|
|
24
|
+
#onToggle;
|
|
25
|
+
#onMenuKeydown;
|
|
26
|
+
#onMenuClick;
|
|
27
|
+
|
|
28
|
+
connect() {
|
|
29
|
+
this.element.setAttribute("aria-haspopup", "true");
|
|
30
|
+
// Also resets a stale value restored from the Turbo page cache (popovers
|
|
31
|
+
// are snapshotted closed, but the attribute travels as-is).
|
|
32
|
+
this.element.setAttribute("aria-expanded", "false");
|
|
33
|
+
this.#onToggle = this.#toggled.bind(this);
|
|
34
|
+
this.#onMenuKeydown = this.#menuKeydown.bind(this);
|
|
35
|
+
this.#onMenuClick = this.#menuClick.bind(this);
|
|
36
|
+
this.#menu.addEventListener("toggle", this.#onToggle);
|
|
37
|
+
this.#menu.addEventListener("keydown", this.#onMenuKeydown);
|
|
38
|
+
this.#menu.addEventListener("click", this.#onMenuClick);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
disconnect() {
|
|
42
|
+
this.#menu?.removeEventListener("toggle", this.#onToggle);
|
|
43
|
+
this.#menu?.removeEventListener("keydown", this.#onMenuKeydown);
|
|
44
|
+
this.#menu?.removeEventListener("click", this.#onMenuClick);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Enter, Space and click toggle natively via [popovertarget]; the arrows
|
|
48
|
+
// open with an explicit landing item.
|
|
49
|
+
onKeydown(event) {
|
|
50
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
if (this.#menu.matches(":popover-open")) return;
|
|
53
|
+
this.#focusOnOpen = event.key === "ArrowUp" ? "last" : "first";
|
|
54
|
+
this.#menu.showPopover();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get #menu() {
|
|
58
|
+
return this.element.nextElementSibling;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get #items() {
|
|
62
|
+
return Array.from(this.#menu.querySelectorAll(ITEM));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Popover toggle events fire for every show/hide path — the toggle button,
|
|
66
|
+
// light dismissal, hidePopover(), even a <dialog> claiming the top layer.
|
|
67
|
+
#toggled(event) {
|
|
68
|
+
const open = event.newState === "open";
|
|
69
|
+
this.element.setAttribute("aria-expanded", String(open));
|
|
70
|
+
if (open) {
|
|
71
|
+
const items = this.#items;
|
|
72
|
+
(this.#focusOnOpen === "last" ? items.at(-1) : items[0])?.focus();
|
|
73
|
+
this.#focusOnOpen = "first";
|
|
74
|
+
} else if (document.activeElement === document.body) {
|
|
75
|
+
this.element.focus();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#menuKeydown(event) {
|
|
80
|
+
const items = this.#items;
|
|
81
|
+
const current = items.indexOf(document.activeElement);
|
|
82
|
+
switch (event.key) {
|
|
83
|
+
case "ArrowDown":
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
items[(current + 1) % items.length]?.focus();
|
|
86
|
+
break;
|
|
87
|
+
case "ArrowUp":
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
items[(current - 1 + items.length) % items.length]?.focus();
|
|
90
|
+
break;
|
|
91
|
+
case "Home":
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
items[0]?.focus();
|
|
94
|
+
break;
|
|
95
|
+
case "End":
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
items.at(-1)?.focus();
|
|
98
|
+
break;
|
|
99
|
+
case "Tab":
|
|
100
|
+
// hidePopover() restores focus to the toggle first, so the default
|
|
101
|
+
// Tab then moves on from there — the APG menu-button behavior.
|
|
102
|
+
this.#menu.hidePopover();
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#menuClick(event) {
|
|
108
|
+
if (event.target.closest(ITEM)) this.#menu.hidePopover();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const BUSY_CLASS = "rails_admin-submitting";
|
|
4
|
+
|
|
5
|
+
// Shared UX-feedback for Turbo-driven forms. Wired onto every admin form by
|
|
6
|
+
// RailsAdminNext::MainHelper#rails_admin_form_for as:
|
|
7
|
+
//
|
|
8
|
+
// data-controller="feedback"
|
|
9
|
+
// data-action="turbo:submit-start->feedback#start turbo:submit-end->feedback#end"
|
|
10
|
+
//
|
|
11
|
+
// `start` disables the form's submit controls and marks the form busy; `end`
|
|
12
|
+
// reverses both symmetrically. Turbo fires `turbo:submit-end` on success AND on a
|
|
13
|
+
// 422 validation failure, so the form never gets stuck disabled. State is driven
|
|
14
|
+
// entirely by the Turbo submit lifecycle — there are no fixed timeouts.
|
|
15
|
+
//
|
|
16
|
+
// On connect it also flags every errored field `aria-invalid="true"` and moves
|
|
17
|
+
// focus to the first one, so a 422 re-render (which Turbo renders in place,
|
|
18
|
+
// re-running connect) lands the cursor on the field that needs fixing —
|
|
19
|
+
// including fields inside nested subforms.
|
|
20
|
+
export default class extends Controller {
|
|
21
|
+
connect() {
|
|
22
|
+
// A back/forward-cache restore can replay a snapshot captured mid-submit;
|
|
23
|
+
// start every connection from a clean, non-busy state.
|
|
24
|
+
this.#reset();
|
|
25
|
+
this.#flagInvalidFields();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start() {
|
|
29
|
+
this.element.setAttribute("aria-busy", "true");
|
|
30
|
+
this.element.classList.add(BUSY_CLASS);
|
|
31
|
+
this.#submitControls.forEach((control) => {
|
|
32
|
+
control.disabled = true;
|
|
33
|
+
this.#disabledControls.add(control);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
end() {
|
|
38
|
+
this.#reset();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// RailsAdminNext marks an errored field's wrapper `.control-group.error`; expose
|
|
42
|
+
// that on the inputs themselves as `aria-invalid` and focus the first. Focus
|
|
43
|
+
// is only taken from a freshly rendered page (activeElement is the body), so
|
|
44
|
+
// a user already typing somewhere is never interrupted.
|
|
45
|
+
#flagInvalidFields() {
|
|
46
|
+
const invalid = this.element.querySelectorAll(
|
|
47
|
+
".control-group.error input:not([type='hidden']), .control-group.error select, .control-group.error textarea"
|
|
48
|
+
);
|
|
49
|
+
invalid.forEach((field) => field.setAttribute("aria-invalid", "true"));
|
|
50
|
+
|
|
51
|
+
const focusable = document.activeElement;
|
|
52
|
+
if (invalid[0] && (!focusable || focusable === document.body)) {
|
|
53
|
+
invalid[0].focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#reset() {
|
|
58
|
+
this.element.removeAttribute("aria-busy");
|
|
59
|
+
this.element.classList.remove(BUSY_CLASS);
|
|
60
|
+
this.#disabledControls.forEach((control) => {
|
|
61
|
+
control.disabled = false;
|
|
62
|
+
});
|
|
63
|
+
this.#disabledControls.clear();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get #submitControls() {
|
|
67
|
+
return this.element.querySelectorAll(
|
|
68
|
+
'input[type="submit"], button[type="submit"], button:not([type])'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#disabledControls = new Set();
|
|
73
|
+
}
|