plutonium 0.26.3 → 0.26.6
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 +4 -4
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +147 -43
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +18 -18
- data/app/assets/plutonium.min.js.map +3 -3
- data/config/initializers/action_policy.rb +9 -0
- data/lib/plutonium/action/base.rb +3 -1
- data/lib/plutonium/action_policy/sti_policy_lookup.rb +43 -0
- data/lib/plutonium/core/controller.rb +13 -4
- data/lib/plutonium/core/controllers/authorizable.rb +2 -2
- data/lib/plutonium/definition/actions.rb +1 -1
- data/lib/plutonium/railtie.rb +1 -1
- data/lib/plutonium/resource/controllers/authorizable.rb +4 -4
- data/lib/plutonium/resource/policy.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +2 -2
- data/lib/plutonium/ui/color_mode_selector.rb +12 -59
- data/lib/plutonium/ui/layout/header.rb +14 -2
- data/lib/plutonium/ui/layout/sidebar.rb +0 -8
- data/lib/plutonium/ui/table/theme.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/js/controllers/attachment_input_controller.js +41 -1
- data/src/js/controllers/color_mode_controller.js +33 -23
- data/src/js/controllers/easymde_controller.js +45 -6
- data/src/js/controllers/flatpickr_controller.js +24 -8
- data/src/js/controllers/intl_tel_input_controller.js +23 -5
- data/src/js/controllers/slim_select_controller.js +27 -12
- metadata +4 -2
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configure ActionPolicy for Plutonium
|
4
|
+
Rails.application.config.to_prepare do
|
5
|
+
# Install STI policy lookup support
|
6
|
+
# This allows STI models to use their base class's policy when a specific policy doesn't exist
|
7
|
+
require "plutonium/action_policy/sti_policy_lookup"
|
8
|
+
Plutonium::ActionPolicy::StiPolicyLookup.install!
|
9
|
+
end
|
@@ -16,7 +16,7 @@ module Plutonium
|
|
16
16
|
# @attr_reader [Symbol, nil] category The category of the action.
|
17
17
|
# @attr_reader [Integer] position The position of the action within its category.
|
18
18
|
class Base
|
19
|
-
attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :turbo_frame, :color, :category, :position
|
19
|
+
attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :turbo_frame, :color, :category, :position, :return_to
|
20
20
|
|
21
21
|
# Initialize a new action.
|
22
22
|
#
|
@@ -29,6 +29,7 @@ module Plutonium
|
|
29
29
|
# @option options [String] :confirmation The confirmation message to display before executing the action.
|
30
30
|
# @option options [RouteOptions, Hash] :route_options The routing options for the action.
|
31
31
|
# @option options [String] :turbo_frame The Turbo Frame ID for the action (used in Hotwire/Turbo Drive applications).
|
32
|
+
# @option options [String, Symbol] :return_to Override the return_to URL for this action. If not provided, defaults to current URL.
|
32
33
|
# @option options [Boolean] :bulk_action (false) If true, applies to a bulk selection of records (e.g., "Mark Selected as Read").
|
33
34
|
# @option options [Boolean] :collection_record_action (false) If true, applies to records in a collection (e.g., "Edit Record" button in a table).
|
34
35
|
# @option options [Boolean] :record_action (false) If true, applies to an individual record (e.g., "Delete" button on a Show page).
|
@@ -49,6 +50,7 @@ module Plutonium
|
|
49
50
|
@route_options = build_route_options(options[:route_options])
|
50
51
|
@turbo = options[:turbo]
|
51
52
|
@turbo_frame = options[:turbo_frame]
|
53
|
+
@return_to = options[:return_to]
|
52
54
|
@bulk_action = options[:bulk_action] || false
|
53
55
|
@collection_record_action = options[:collection_record_action] || false
|
54
56
|
@record_action = options[:record_action] || false
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Plutonium
|
4
|
+
module ActionPolicy
|
5
|
+
# Custom ActionPolicy lookup resolver for STI (Single Table Inheritance) models
|
6
|
+
# This resolver attempts to find a policy for the base class when a policy
|
7
|
+
# for the STI subclass is not found
|
8
|
+
module StiPolicyLookup
|
9
|
+
# STI base class policy resolver
|
10
|
+
# Checks if the record is an STI model and looks up the policy for its base class
|
11
|
+
STI_BASE_CLASS_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **) {
|
12
|
+
# Skip if record is a symbol or doesn't have a class
|
13
|
+
next unless record.respond_to?(:class)
|
14
|
+
|
15
|
+
record_class = record.is_a?(Module) ? record : record.class
|
16
|
+
|
17
|
+
# Check if this is an STI model (has base_class and is different from current class)
|
18
|
+
next unless record_class.respond_to?(:base_class)
|
19
|
+
next if record_class == record_class.base_class
|
20
|
+
|
21
|
+
# Try to find policy for the base class
|
22
|
+
policy_name = "#{record_class.base_class}Policy"
|
23
|
+
::ActionPolicy::LookupChain.send(:lookup_within_namespace, policy_name, namespace, strict: strict_namespace)
|
24
|
+
}
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def install!
|
28
|
+
# Insert STI resolver before the standard INFER_FROM_CLASS resolver
|
29
|
+
# This ensures we try the base class before giving up
|
30
|
+
infer_index = ::ActionPolicy::LookupChain.chain.index(::ActionPolicy::LookupChain::INFER_FROM_CLASS)
|
31
|
+
|
32
|
+
if infer_index
|
33
|
+
# Insert after INFER_FROM_CLASS so it runs as a fallback
|
34
|
+
::ActionPolicy::LookupChain.chain.insert(infer_index + 1, STI_BASE_CLASS_LOOKUP)
|
35
|
+
else
|
36
|
+
# If for some reason INFER_FROM_CLASS isn't found, append to end
|
37
|
+
::ActionPolicy::LookupChain.chain << STI_BASE_CLASS_LOOKUP
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -73,13 +73,22 @@ module Plutonium
|
|
73
73
|
if element.is_a?(Class)
|
74
74
|
controller_chain << element.to_s.pluralize
|
75
75
|
else
|
76
|
-
|
76
|
+
# For STI models, use the base class for routing if the specific class isn't registered
|
77
|
+
model_class = element.class
|
78
|
+
if model_class.respond_to?(:base_class) && model_class != model_class.base_class
|
79
|
+
# Check if the STI model is registered, if not use base class
|
80
|
+
route_configs = current_engine.routes.resource_route_config_for(model_class.to_s.pluralize.underscore)
|
81
|
+
model_class = model_class.base_class if route_configs.nil? || route_configs.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
controller_chain << model_class.to_s.pluralize
|
77
85
|
if index == args.length - 1
|
78
|
-
|
79
|
-
|
86
|
+
resource_route_configs = current_engine.routes.resource_route_config_for(model_class.to_s.pluralize.underscore)
|
87
|
+
resource_route_config = resource_route_configs&.first
|
88
|
+
url_args[:id] = element.to_param unless resource_route_config && resource_route_config[:route_type] == :resource
|
80
89
|
url_args[:action] ||= :show
|
81
90
|
else
|
82
|
-
url_args[
|
91
|
+
url_args[model_class.to_s.underscore.singularize.to_sym] = element.to_param
|
83
92
|
end
|
84
93
|
end
|
85
94
|
end
|
@@ -5,7 +5,7 @@ module Plutonium
|
|
5
5
|
module Controllers
|
6
6
|
module Authorizable
|
7
7
|
extend ActiveSupport::Concern
|
8
|
-
include ActionPolicy::Controller
|
8
|
+
include ::ActionPolicy::Controller
|
9
9
|
|
10
10
|
included do
|
11
11
|
authorize :user, through: :current_user
|
@@ -23,7 +23,7 @@ module Plutonium
|
|
23
23
|
raise ArgumentError("Expected resource to be a class inheriting ActiveRecord::Base")
|
24
24
|
end
|
25
25
|
|
26
|
-
options[:with] ||= ActionPolicy.lookup(resource, namespace: authorization_namespace)
|
26
|
+
options[:with] ||= ::ActionPolicy.lookup(resource, namespace: authorization_namespace)
|
27
27
|
relation ||= resource.all
|
28
28
|
|
29
29
|
authorized_scope(relation, **options)
|
@@ -47,7 +47,7 @@ module Plutonium
|
|
47
47
|
action(:destroy, route_options: {method: :delete},
|
48
48
|
record_action: true, collection_record_action: true, category: :danger,
|
49
49
|
icon: Phlex::TablerIcons::Trash, position: 100,
|
50
|
-
confirmation: "Are you sure?", turbo_frame: "_top")
|
50
|
+
confirmation: "Are you sure?", turbo_frame: "_top", return_to: "")
|
51
51
|
|
52
52
|
# Example of dynamic route options using custom url_resolver:
|
53
53
|
#
|
data/lib/plutonium/railtie.rb
CHANGED
@@ -54,7 +54,7 @@ module Plutonium
|
|
54
54
|
config.after_initialize do
|
55
55
|
Plutonium::Reloader.start! if Plutonium.configuration.enable_hotreload
|
56
56
|
Plutonium::Loader.eager_load if Rails.env.production?
|
57
|
-
ActionPolicy::PerThreadCache.enabled = !Rails.env.local?
|
57
|
+
::ActionPolicy::PerThreadCache.enabled = !Rails.env.local?
|
58
58
|
end
|
59
59
|
|
60
60
|
private
|
@@ -20,10 +20,10 @@ module Plutonium
|
|
20
20
|
extend ActiveSupport::Concern
|
21
21
|
|
22
22
|
# Custom exception for missing authorize_current call
|
23
|
-
class ActionMissingAuthorizeCurrent < ActionPolicy::UnauthorizedAction; end
|
23
|
+
class ActionMissingAuthorizeCurrent < ::ActionPolicy::UnauthorizedAction; end
|
24
24
|
|
25
25
|
# Custom exception for missing current_authorized_scope call
|
26
|
-
class ActionMissingCurrentAuthorizedScope < ActionPolicy::UnauthorizedAction; end
|
26
|
+
class ActionMissingCurrentAuthorizedScope < ::ActionPolicy::UnauthorizedAction; end
|
27
27
|
|
28
28
|
included do
|
29
29
|
after_action :verify_authorize_current
|
@@ -95,7 +95,7 @@ module Plutonium
|
|
95
95
|
|
96
96
|
# Returns the policy for the current resource
|
97
97
|
#
|
98
|
-
# @return [ActionPolicy::Base] the policy for the current resource
|
98
|
+
# @return [::ActionPolicy::Base] the policy for the current resource
|
99
99
|
def current_policy
|
100
100
|
@current_policy ||= policy_for(record: current_policy_subject, context: current_policy_context)
|
101
101
|
end
|
@@ -119,7 +119,7 @@ module Plutonium
|
|
119
119
|
#
|
120
120
|
# @param record [Object] the record to authorize
|
121
121
|
# @param options [Hash] additional options for authorization
|
122
|
-
# @raise [ActionPolicy::Unauthorized] if the action is not authorized
|
122
|
+
# @raise [::ActionPolicy::Unauthorized] if the action is not authorized
|
123
123
|
def authorize_current!(record, **options)
|
124
124
|
options[:context] = (options[:context] || {}).deep_merge(current_policy_context)
|
125
125
|
authorize!(record, **options)
|
@@ -5,7 +5,7 @@ module Plutonium
|
|
5
5
|
# Policy class to define permissions and attributes for a resource.
|
6
6
|
# This class provides methods to check permissions for various actions
|
7
7
|
# and to retrieve permitted attributes for these actions.
|
8
|
-
class Policy < ActionPolicy::Base
|
8
|
+
class Policy < ::ActionPolicy::Base
|
9
9
|
authorize :user, allow_nil: false
|
10
10
|
authorize :entity_scope, allow_nil: true
|
11
11
|
|
@@ -25,7 +25,7 @@ module Plutonium
|
|
25
25
|
def render_link
|
26
26
|
uri = URI.parse(@url)
|
27
27
|
params = Rack::Utils.parse_nested_query(uri.query)
|
28
|
-
params["return_to"] = request.original_url
|
28
|
+
params["return_to"] = @action.return_to.nil? ? request.original_url : @action.return_to
|
29
29
|
uri.query = params.to_query
|
30
30
|
uri.to_s
|
31
31
|
|
@@ -42,7 +42,7 @@ module Plutonium
|
|
42
42
|
button_to(
|
43
43
|
@url,
|
44
44
|
method: @action.route_options.method,
|
45
|
-
name: :return_to, value: request.original_url,
|
45
|
+
name: :return_to, value: (@action.return_to.nil? ? request.original_url : @action.return_to),
|
46
46
|
class: "inline-block",
|
47
47
|
form: {
|
48
48
|
data: {
|
@@ -8,77 +8,30 @@ module Plutonium
|
|
8
8
|
class ColorModeSelector < Plutonium::UI::Component::Base
|
9
9
|
# Common CSS classes used across the component
|
10
10
|
COMMON_CLASSES = {
|
11
|
-
button: "
|
12
|
-
icon: "w-
|
13
|
-
trigger: "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-200 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600",
|
14
|
-
dropdown: "hidden z-50 my-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700"
|
11
|
+
button: "inline-flex justify-center items-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-200 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors duration-200",
|
12
|
+
icon: "w-5 h-5"
|
15
13
|
}.freeze
|
16
14
|
|
17
15
|
# Available color modes with their associated icons and actions
|
18
16
|
COLOR_MODES = [
|
19
|
-
{
|
20
|
-
{
|
21
|
-
{label: "System", icon: Phlex::TablerIcons::DeviceDesktop, action: "setSystemColorMode"}
|
17
|
+
{mode: "light", icon: Phlex::TablerIcons::Sun, action: "setLightColorMode"},
|
18
|
+
{mode: "dark", icon: Phlex::TablerIcons::Moon, action: "setDarkColorMode"}
|
22
19
|
].freeze
|
23
20
|
|
24
21
|
# Renders the color mode selector
|
25
22
|
# @return [void]
|
26
23
|
def view_template
|
27
|
-
div(data_controller: "resource-drop-down") do
|
28
|
-
render_dropdown_trigger
|
29
|
-
render_dropdown_menu
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
# @private
|
36
|
-
def render_dropdown_trigger
|
37
24
|
button(
|
38
25
|
type: "button",
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
# @private
|
47
|
-
def render_dropdown_menu
|
48
|
-
div(
|
49
|
-
class: COMMON_CLASSES[:dropdown],
|
50
|
-
data_resource_drop_down_target: "menu"
|
26
|
+
class: COMMON_CLASSES[:button],
|
27
|
+
data_controller: "color-mode",
|
28
|
+
data_action: "click->color-mode#toggleMode",
|
29
|
+
data_color_mode_current_value: "light", # Default to light mode
|
30
|
+
title: "Toggle color mode"
|
51
31
|
) do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
# @private
|
57
|
-
def render_color_mode_options
|
58
|
-
ul(class: "py-1", role: "none") do
|
59
|
-
COLOR_MODES.each do |mode|
|
60
|
-
render_color_mode_button(**mode)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# @private
|
66
|
-
# @param label [String] The text label for the button
|
67
|
-
# @param icon [Class] The TablerIcon class to render
|
68
|
-
# @param action [String] The color-mode controller action to trigger
|
69
|
-
def render_color_mode_button(label:, icon:, action:)
|
70
|
-
li do
|
71
|
-
button(
|
72
|
-
type: "button",
|
73
|
-
class: COMMON_CLASSES[:button],
|
74
|
-
role: "menuitem",
|
75
|
-
data_action: "click->color-mode##{action}"
|
76
|
-
) do
|
77
|
-
div(class: "flex justify-start") do
|
78
|
-
render icon.new(class: COMMON_CLASSES[:icon])
|
79
|
-
plain " #{label}"
|
80
|
-
end
|
81
|
-
end
|
32
|
+
# Both icons rendered, only one visible at a time
|
33
|
+
render Phlex::TablerIcons::Sun.new(class: "#{COMMON_CLASSES[:icon]} color-mode-icon-light", data: {color_mode_icon: "light"})
|
34
|
+
render Phlex::TablerIcons::Moon.new(class: "#{COMMON_CLASSES[:icon]} color-mode-icon-dark", data: {color_mode_icon: "dark"})
|
82
35
|
end
|
83
36
|
end
|
84
37
|
end
|
@@ -56,6 +56,14 @@ module Plutonium
|
|
56
56
|
|
57
57
|
private
|
58
58
|
|
59
|
+
# Renders the color mode toggle controls
|
60
|
+
# @private
|
61
|
+
def render_color_mode_controls
|
62
|
+
div(class: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700") do
|
63
|
+
render ColorModeSelector.new
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
59
67
|
# Renders the left section of the header including sidebar toggle, brand elements,
|
60
68
|
# and any yielded content
|
61
69
|
# @private
|
@@ -111,8 +119,12 @@ module Plutonium
|
|
111
119
|
# Renders the action buttons section
|
112
120
|
# @private
|
113
121
|
def render_actions
|
114
|
-
div(class: "flex items-center
|
115
|
-
|
122
|
+
div(class: "flex items-center space-x-2") do
|
123
|
+
render_color_mode_controls
|
124
|
+
|
125
|
+
div(class: "flex items-center lg:order-2") do
|
126
|
+
action_slots.each { |action| render action }
|
127
|
+
end
|
116
128
|
end
|
117
129
|
end
|
118
130
|
end
|
@@ -15,7 +15,6 @@ module Plutonium
|
|
15
15
|
def view_template(&)
|
16
16
|
render_sidebar_container do
|
17
17
|
render_content(&) if block_given?
|
18
|
-
render_color_mode_controls
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
@@ -41,13 +40,6 @@ module Plutonium
|
|
41
40
|
&
|
42
41
|
)
|
43
42
|
end
|
44
|
-
|
45
|
-
# @private
|
46
|
-
def render_color_mode_controls
|
47
|
-
div(class: "absolute bottom-0 left-0 justify-center p-4 space-x-4 w-full flex bg-white dark:bg-gray-800 z-20 border-r border-gray-200 dark:border-gray-700") do
|
48
|
-
render ColorModeSelector.new
|
49
|
-
end
|
50
|
-
end
|
51
43
|
end
|
52
44
|
end
|
53
45
|
end
|
@@ -21,7 +21,7 @@ module Plutonium
|
|
21
21
|
header_cell_sort_wrapper: "flex items-center",
|
22
22
|
header_cell_sort_indicator: "ml-1.5",
|
23
23
|
body_row: "bg-white border-b last:border-none dark:bg-gray-800 dark:border-gray-700",
|
24
|
-
body_cell: "px-6 py-4 whitespace-pre max-w-[
|
24
|
+
body_cell: "px-6 py-4 whitespace-pre max-w-[450px] overflow-hidden text-ellipsis transition-all duration-300 ease-in-out",
|
25
25
|
sort_icon: "w-3 h-3",
|
26
26
|
sort_icon_active: "text-primary-600",
|
27
27
|
sort_icon_inactive: "text-gray-600 dark:text-gray-500",
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
@@ -26,6 +26,8 @@ export default class extends Controller {
|
|
26
26
|
//======= Lifecycle
|
27
27
|
|
28
28
|
connect() {
|
29
|
+
if (this.uppy) return;
|
30
|
+
|
29
31
|
// initialize
|
30
32
|
this.uploadedFiles = []
|
31
33
|
|
@@ -38,10 +40,48 @@ export default class extends Controller {
|
|
38
40
|
this.#buildTriggers()
|
39
41
|
// init state
|
40
42
|
this.#onAttachmentsChanged()
|
43
|
+
|
44
|
+
// Just recreate Uppy after morphing - preserve existing attachments
|
45
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
46
|
+
if (event.target === this.element && !this.morphing) {
|
47
|
+
this.morphing = true;
|
48
|
+
requestAnimationFrame(() => {
|
49
|
+
this.#handleMorph();
|
50
|
+
this.morphing = false;
|
51
|
+
});
|
52
|
+
}
|
53
|
+
});
|
41
54
|
}
|
42
55
|
|
43
56
|
disconnect() {
|
44
|
-
this
|
57
|
+
this.#cleanupUppy();
|
58
|
+
}
|
59
|
+
|
60
|
+
#handleMorph() {
|
61
|
+
if (!this.element.isConnected) return;
|
62
|
+
|
63
|
+
// Clean up the old instance
|
64
|
+
this.#cleanupUppy();
|
65
|
+
|
66
|
+
// Recreate everything - Uppy, triggers, etc.
|
67
|
+
this.uploadedFiles = []
|
68
|
+
this.element.style["display"] = "none"
|
69
|
+
this.configureUppy()
|
70
|
+
this.#buildTriggers()
|
71
|
+
this.#onAttachmentsChanged()
|
72
|
+
}
|
73
|
+
|
74
|
+
#cleanupUppy() {
|
75
|
+
if (this.uppy) {
|
76
|
+
this.uppy.destroy();
|
77
|
+
this.uppy = null;
|
78
|
+
}
|
79
|
+
|
80
|
+
// Clean up triggers
|
81
|
+
if (this.triggerContainer && this.triggerContainer.parentNode) {
|
82
|
+
this.triggerContainer.parentNode.removeChild(this.triggerContainer);
|
83
|
+
this.triggerContainer = null;
|
84
|
+
}
|
45
85
|
}
|
46
86
|
|
47
87
|
attachmentPreviewOutletConnected(outlet, element) {
|
@@ -1,40 +1,50 @@
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
2
|
-
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
3
2
|
|
4
3
|
// Connects to data-controller="color-mode"
|
5
4
|
export default class extends Controller {
|
6
|
-
|
5
|
+
static values = { current: String };
|
7
6
|
|
8
7
|
connect() {
|
9
|
-
|
8
|
+
// Set initial mode from localStorage or default
|
9
|
+
const mode = localStorage.theme || "light";
|
10
|
+
this.setMode(mode);
|
10
11
|
}
|
11
12
|
|
12
|
-
|
13
|
+
toggleMode() {
|
14
|
+
const current = this.currentValue || "light";
|
15
|
+
const next = current === "light" ? "dark" : "light";
|
16
|
+
this.setMode(next);
|
13
17
|
}
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
setMode(mode) {
|
20
|
+
// Update html class
|
21
|
+
if (mode === "dark") {
|
22
|
+
document.documentElement.classList.add("dark");
|
23
|
+
localStorage.theme = "dark";
|
18
24
|
} else {
|
19
|
-
document.documentElement.classList.remove(
|
25
|
+
document.documentElement.classList.remove("dark");
|
26
|
+
localStorage.theme = "light";
|
20
27
|
}
|
21
|
-
}
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
localStorage.theme = 'light'
|
26
|
-
this.updateColorMode()
|
27
|
-
}
|
29
|
+
// Update button state
|
30
|
+
this.currentValue = mode;
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
localStorage.theme = 'dark'
|
32
|
-
this.updateColorMode()
|
32
|
+
// Show/hide icons
|
33
|
+
this.toggleIcons(mode);
|
33
34
|
}
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
toggleIcons(mode) {
|
37
|
+
const sun = this.element.querySelector(".color-mode-icon-light");
|
38
|
+
const moon = this.element.querySelector(".color-mode-icon-dark");
|
39
|
+
|
40
|
+
if (sun && moon) {
|
41
|
+
if (mode === "light") {
|
42
|
+
sun.classList.remove("hidden");
|
43
|
+
moon.classList.add("hidden");
|
44
|
+
} else {
|
45
|
+
sun.classList.add("hidden");
|
46
|
+
moon.classList.remove("hidden");
|
47
|
+
}
|
48
|
+
}
|
39
49
|
}
|
40
50
|
}
|
@@ -4,21 +4,60 @@ import { marked } from 'marked';
|
|
4
4
|
|
5
5
|
// Connects to data-controller="easymde"
|
6
6
|
export default class extends Controller {
|
7
|
+
static targets = ["textarea"]
|
8
|
+
|
7
9
|
connect() {
|
10
|
+
if (this.easyMDE) return
|
11
|
+
|
12
|
+
this.originalValue = this.element.value
|
8
13
|
this.easyMDE = new EasyMDE(this.#buildOptions())
|
9
|
-
|
14
|
+
|
15
|
+
// Store the editor content before morphing
|
16
|
+
this.element.addEventListener("turbo:before-morph-element", (event) => {
|
17
|
+
if (event.target === this.element && this.easyMDE) {
|
18
|
+
this.storedValue = this.easyMDE.value()
|
19
|
+
}
|
20
|
+
})
|
21
|
+
|
22
|
+
// Restore after morphing
|
23
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
24
|
+
if (event.target === this.element) {
|
25
|
+
requestAnimationFrame(() => this.#handleMorph())
|
26
|
+
}
|
27
|
+
})
|
10
28
|
}
|
11
29
|
|
12
30
|
disconnect() {
|
13
31
|
if (this.easyMDE) {
|
14
|
-
|
32
|
+
try {
|
33
|
+
// Only call toTextArea if the element is still in the DOM
|
34
|
+
if (this.element.isConnected && this.element.parentNode) {
|
35
|
+
this.easyMDE.toTextArea()
|
36
|
+
}
|
37
|
+
} catch (error) {
|
38
|
+
console.warn('EasyMDE cleanup error:', error)
|
39
|
+
}
|
15
40
|
this.easyMDE = null
|
16
41
|
}
|
17
42
|
}
|
18
|
-
|
19
|
-
|
20
|
-
this.
|
21
|
-
|
43
|
+
|
44
|
+
#handleMorph() {
|
45
|
+
if (!this.element.isConnected) return
|
46
|
+
|
47
|
+
// Don't call toTextArea during morph - just clean up references
|
48
|
+
if (this.easyMDE) {
|
49
|
+
// Skip toTextArea cleanup - it causes DOM errors during morphing
|
50
|
+
this.easyMDE = null
|
51
|
+
}
|
52
|
+
|
53
|
+
// Recreate the editor
|
54
|
+
this.easyMDE = new EasyMDE(this.#buildOptions())
|
55
|
+
|
56
|
+
// Restore the stored value if we have it
|
57
|
+
if (this.storedValue !== undefined) {
|
58
|
+
this.easyMDE.value(this.storedValue)
|
59
|
+
this.storedValue = undefined
|
60
|
+
}
|
22
61
|
}
|
23
62
|
|
24
63
|
#buildOptions() {
|
@@ -3,14 +3,21 @@ import { Controller } from "@hotwired/stimulus";
|
|
3
3
|
// Connects to data-controller="flatpickr"
|
4
4
|
export default class extends Controller {
|
5
5
|
connect() {
|
6
|
-
this.
|
6
|
+
if (this.picker) return;
|
7
7
|
|
8
|
+
this.modal = document.querySelector("[data-controller=remote-modal]");
|
8
9
|
this.picker = new flatpickr(this.element, this.#buildOptions());
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
// Just recreate Flatpickr after morphing - the DOM will have correct value
|
12
|
+
this.element.addEventListener("turbo:morph-element", (event) => {
|
13
|
+
if (event.target === this.element && !this.morphing) {
|
14
|
+
this.morphing = true;
|
15
|
+
requestAnimationFrame(() => {
|
16
|
+
this.#handleMorph();
|
17
|
+
this.morphing = false;
|
18
|
+
});
|
19
|
+
}
|
20
|
+
});
|
14
21
|
}
|
15
22
|
|
16
23
|
disconnect() {
|
@@ -20,9 +27,18 @@ export default class extends Controller {
|
|
20
27
|
}
|
21
28
|
}
|
22
29
|
|
23
|
-
|
24
|
-
this.
|
25
|
-
|
30
|
+
#handleMorph() {
|
31
|
+
if (!this.element.isConnected) return;
|
32
|
+
|
33
|
+
// Clean up the old instance
|
34
|
+
if (this.picker) {
|
35
|
+
this.picker.destroy();
|
36
|
+
this.picker = null;
|
37
|
+
}
|
38
|
+
|
39
|
+
// Recreate the picker - it will pick up the current DOM value
|
40
|
+
this.modal = document.querySelector("[data-controller=remote-modal]");
|
41
|
+
this.picker = new flatpickr(this.element, this.#buildOptions());
|
26
42
|
}
|
27
43
|
|
28
44
|
#buildOptions() {
|