hakumi_components 0.1.16.pre → 0.1.17.pre
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/README.md +169 -23
- data/app/assets/javascripts/hakumi_components.js +12 -12
- data/app/assets/stylesheets/hakumi_components.css +1 -1
- data/app/components/hakumi/alert/component.html.erb +12 -8
- data/app/components/hakumi/alert/component.rb +18 -62
- data/app/components/hakumi/base_component.rb +13 -0
- data/app/components/hakumi/card/component.html.erb +14 -22
- data/app/components/hakumi/card/component.rb +38 -31
- data/app/components/hakumi/checkbox/component.html.erb +39 -21
- data/app/components/hakumi/checkbox/component.rb +12 -2
- data/app/components/hakumi/collapse/component.html.erb +2 -2
- data/app/components/hakumi/collapse/component.rb +1 -1
- data/app/components/hakumi/collapse/panel/component.rb +9 -0
- data/app/components/hakumi/color_picker/component.rb +0 -4
- data/app/components/hakumi/drawer/component.html.erb +7 -7
- data/app/components/hakumi/drawer/component.rb +12 -19
- data/app/components/hakumi/input/component.rb +0 -2
- data/app/components/hakumi/input/text_area/component.rb +0 -2
- data/app/components/hakumi/input_number/component.rb +3 -4
- data/app/components/hakumi/mentions/component.rb +0 -1
- data/app/components/hakumi/modal/component.html.erb +40 -0
- data/app/components/hakumi/modal/component.rb +24 -102
- data/app/components/hakumi/modal/confirm/component.html.erb +23 -0
- data/app/components/hakumi/modal/confirm/component.rb +23 -41
- data/app/components/hakumi/modal/error/component.rb +12 -11
- data/app/components/hakumi/modal/info/component.rb +12 -11
- data/app/components/hakumi/modal/success/component.rb +12 -11
- data/app/components/hakumi/modal/warning/component.rb +15 -10
- data/app/components/hakumi/popconfirm/component.html.erb +25 -25
- data/app/components/hakumi/popconfirm/component.rb +11 -27
- data/app/components/hakumi/rate/component.rb +0 -1
- data/app/components/hakumi/segmented/component.rb +0 -4
- data/app/components/hakumi/slider/component.rb +2 -6
- data/app/components/hakumi/statistic/component.rb +0 -4
- data/app/components/hakumi/switch/component.html.erb +4 -0
- data/app/components/hakumi/switch/component.rb +1 -2
- data/app/components/hakumi/table/component.rb +3 -229
- data/app/components/hakumi/table/concerns/columns.rb +1 -1
- data/app/components/hakumi/table/concerns/editable.rb +121 -0
- data/app/components/hakumi/table/concerns/ellipsis.rb +63 -0
- data/app/components/hakumi/table/concerns/fixed_columns.rb +87 -0
- data/app/components/hakumi/transfer/component.rb +0 -4
- data/app/controllers/{hakumi_components → hakumi}/components_controller.rb +2 -2
- data/app/form_builders/hakumi/form_builder.rb +217 -175
- data/app/helpers/hakumi/form_helper.rb +39 -0
- data/app/javascript/hakumi_components/controllers/base/registry_controller.js +83 -3
- data/app/javascript/hakumi_components/controllers/hakumi/affix_controller.js +0 -23
- data/app/javascript/hakumi_components/controllers/hakumi/alert_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +0 -7
- data/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +1 -6
- data/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +28 -34
- data/app/javascript/hakumi_components/controllers/hakumi/drawer_controller.js +2 -1
- data/app/javascript/hakumi_components/controllers/hakumi/form_item_controller.js +9 -63
- data/app/javascript/hakumi_components/controllers/hakumi/mentions_controller.js +4 -11
- data/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +4 -20
- data/app/javascript/hakumi_components/controllers/hakumi/notification_controller.js +1 -1
- data/app/javascript/hakumi_components/controllers/hakumi/popconfirm_controller.js +33 -27
- data/app/javascript/hakumi_components/controllers/hakumi/popover_controller.js +2 -23
- data/app/javascript/hakumi_components/controllers/hakumi/qr_code_controller.js +0 -20
- data/app/javascript/hakumi_components/controllers/hakumi/segmented_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/spin_controller.js +1 -19
- data/app/javascript/hakumi_components/controllers/hakumi/statistic_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +48 -74
- data/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +15 -14
- data/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +14 -13
- data/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +24 -1
- data/app/javascript/hakumi_components/controllers/hakumi/time_picker_controller.js +3 -7
- data/app/javascript/hakumi_components/controllers/hakumi/timeline_controller.js +0 -16
- data/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +2 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +0 -2
- data/app/javascript/hakumi_components/controllers/hakumi/tree_select_controller.js +3 -3
- data/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +12 -26
- data/app/javascript/hakumi_components/core/persistence.js +3 -3
- data/app/javascript/hakumi_components/core/render_component.js +3 -1
- data/app/javascript/lib/validation_manager.js +101 -0
- data/app/javascript/stylesheets/_theme-tokens.scss +2 -1
- data/app/javascript/stylesheets/components/_modal.scss +13 -0
- data/app/services/{hakumi_components → hakumi}/component_handler.rb +1 -1
- data/app/services/hakumi/icon/loader.rb +2 -2
- data/app/services/hakumi/illustrations/loader.rb +3 -3
- data/app/views/hakumi/_drawer.html.erb +21 -0
- data/app/views/hakumi/_modal.html.erb +18 -0
- data/lib/hakumi_components/documentation.rb +127 -0
- data/lib/hakumi_components/engine.rb +13 -4
- data/lib/hakumi_components/rails/attribute_introspection.rb +1 -1
- data/lib/hakumi_components/rails/validation_introspection.rb +5 -5
- data/lib/hakumi_components/rails/validation_mapper.rb +484 -0
- data/lib/hakumi_components/rails.rb +2 -1
- data/lib/hakumi_components/version.rb +2 -2
- data/lib/hakumi_components.rb +3 -1
- data/lib/tasks/coverage.rake +37 -0
- data/sig/hakumi/base_component.rbs +5 -0
- data/sig/hakumi/checkbox/component.rbs +10 -0
- data/sig/hakumi/color_picker/component.rbs +0 -1
- data/sig/hakumi/form_builder.rbs +9 -1
- data/sig/{hakumi_components → hakumi}/rails/attribute_introspection.rbs +1 -1
- data/sig/{hakumi_components → hakumi}/rails/validation_introspection.rbs +1 -1
- data/sig/hakumi/rails/validation_mapper.rbs +53 -0
- data/sig/{hakumi_components → hakumi}/rails.rbs +1 -1
- data/sig/hakumi/segmented/component.rbs +0 -1
- data/sig/hakumi/slider/component.rbs +0 -1
- data/sig/hakumi/statistic/component.rbs +0 -2
- data/sig/hakumi/table/component.rbs +3 -4
- data/sig/hakumi/table/concerns/columns.rbs +2 -1
- data/sig/hakumi/table/concerns/editable.rbs +40 -0
- data/sig/hakumi/table/concerns/ellipsis.rbs +27 -0
- data/sig/hakumi/table/concerns/fixed_columns.rbs +33 -0
- data/sig/hakumi/transfer/component.rbs +0 -1
- data/sig/{hakumi_components.rbs → hakumi.rbs} +20 -3
- data/sig/rails/active_model/validations/comparison_validator.rbs +6 -0
- metadata +44 -29
- data/app/views/hakumi_components/_drawer.html.erb +0 -3
- data/app/views/hakumi_components/_modal.html.erb +0 -3
- /data/app/views/{hakumi_components → hakumi}/_admin_panel.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_affix.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_alert.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_confirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_message.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_notification.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popconfirm.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_popover.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_qr_code.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_result.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_segmented.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_skeleton.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_spin.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_statistic.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_table.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_tag.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_timeline.html.erb +0 -0
- /data/app/views/{hakumi_components → hakumi}/_tree.html.erb +0 -0
|
@@ -255,10 +255,6 @@ module Hakumi
|
|
|
255
255
|
.gsub("ss", seconds.to_s.rjust(2, "0"))
|
|
256
256
|
end
|
|
257
257
|
|
|
258
|
-
def cast_boolean(value)
|
|
259
|
-
ActiveModel::Type::Boolean.new.cast(value)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
258
|
def normalize_precision(value)
|
|
263
259
|
return nil if value.nil?
|
|
264
260
|
return value if value.is_a?(Integer)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
<% switch_control = capture do %>
|
|
2
|
+
<% # Rails convention: hidden field with value="0" ensures unchecked state sends params %>
|
|
3
|
+
<% if @name && !standalone? %>
|
|
4
|
+
<%= tag.input(type: "hidden", name: @name, value: "0", autocomplete: "off") %>
|
|
5
|
+
<% end %>
|
|
2
6
|
<%= content_tag(:label, **wrapper_attributes) do %>
|
|
3
7
|
<%= tag.input(**input_attributes) %>
|
|
4
8
|
<span class="hakumi-switch-handle">
|
|
@@ -23,7 +23,7 @@ module Hakumi
|
|
|
23
23
|
errors: [],
|
|
24
24
|
**html_options
|
|
25
25
|
)
|
|
26
|
-
@name = name || "
|
|
26
|
+
@name = name || generate_id("switch")
|
|
27
27
|
@label = label
|
|
28
28
|
@caption = caption
|
|
29
29
|
@checked = checked.nil? ? default_checked : checked
|
|
@@ -96,7 +96,6 @@ module Hakumi
|
|
|
96
96
|
class: [ "hakumi-switch-input", @html_options[:class] ].compact.join(" "),
|
|
97
97
|
checked: @checked ? true : nil,
|
|
98
98
|
disabled: (@disabled || @loading) ? true : nil,
|
|
99
|
-
required: @required ? true : nil,
|
|
100
99
|
"aria-invalid": has_error? ? "true" : nil,
|
|
101
100
|
"aria-describedby": describedby_ids,
|
|
102
101
|
tabindex: "-1",
|
|
@@ -5,6 +5,9 @@ module Hakumi
|
|
|
5
5
|
# A complex Table component supporting sorting, filtering, selection, expansion, and editing.
|
|
6
6
|
class Component < Hakumi::BaseComponent
|
|
7
7
|
include Concerns::Columns
|
|
8
|
+
include Concerns::Editable
|
|
9
|
+
include Concerns::FixedColumns
|
|
10
|
+
include Concerns::Ellipsis
|
|
8
11
|
|
|
9
12
|
# @group Configuration Constants
|
|
10
13
|
SIZES = %i[default middle small].freeze
|
|
@@ -12,9 +15,6 @@ module Hakumi
|
|
|
12
15
|
SORT_DIRECTIONS = %i[ascend descend].freeze
|
|
13
16
|
FIXED_SIDES = %i[left right].freeze
|
|
14
17
|
RESPONSIVE_BREAKPOINTS = %i[xs sm md lg xl xxl].freeze
|
|
15
|
-
EDITABLE_MODES = %i[cell row].freeze
|
|
16
|
-
EDITABLE_TRIGGERS = %i[click dblclick].freeze
|
|
17
|
-
EDITABLE_INPUT_TYPES = %i[text number textarea select].freeze
|
|
18
18
|
ROW_BUTTON_ACTIONS = %i[edit save cancel].freeze
|
|
19
19
|
|
|
20
20
|
# @group Slot Definitions
|
|
@@ -538,10 +538,6 @@ module Hakumi
|
|
|
538
538
|
}
|
|
539
539
|
end
|
|
540
540
|
|
|
541
|
-
def fixed_columns?
|
|
542
|
-
fixed_offsets.present?
|
|
543
|
-
end
|
|
544
|
-
|
|
545
541
|
private
|
|
546
542
|
|
|
547
543
|
def normalize_row_selection!
|
|
@@ -575,132 +571,6 @@ module Hakumi
|
|
|
575
571
|
}.compact.presence
|
|
576
572
|
end
|
|
577
573
|
|
|
578
|
-
def cast_boolean(value)
|
|
579
|
-
ActiveModel::Type::Boolean.new.cast(value)
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
def fixed_offsets
|
|
583
|
-
@fixed_offsets ||= {}
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
def apply_fixed_column_attributes!(attrs, column, header: false)
|
|
587
|
-
info = fixed_offsets[column[:key]]
|
|
588
|
-
return unless info
|
|
589
|
-
|
|
590
|
-
attrs[:class] = [ attrs[:class], "hakumi-table-cell-fixed", "hakumi-table-cell-fixed-#{info[:side]}" ].compact.join(" ")
|
|
591
|
-
|
|
592
|
-
style_parts = []
|
|
593
|
-
style_parts << attrs[:style] if attrs[:style].present?
|
|
594
|
-
style_parts << "position: sticky"
|
|
595
|
-
style_parts << "#{info[:side]}: #{info[:offset]}px"
|
|
596
|
-
style_parts << "z-index: #{header ? 3 : 2}"
|
|
597
|
-
attrs[:style] = style_parts.compact.join("; ")
|
|
598
|
-
|
|
599
|
-
attrs[:data] ||= {}
|
|
600
|
-
attrs[:data][:fixed] = info[:side]
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
def apply_editable_attributes!(attrs, column)
|
|
604
|
-
payload = cell_editable_payload(column)
|
|
605
|
-
return unless payload
|
|
606
|
-
|
|
607
|
-
attrs[:class] = [ attrs[:class], "hakumi-table-cell-editable" ].compact.join(" ")
|
|
608
|
-
attrs[:data] ||= {}
|
|
609
|
-
attrs[:data][:editable] = true
|
|
610
|
-
attrs[:data][:editableConfig] = payload.to_json
|
|
611
|
-
attrs[:data][:action] = "click->hakumi--table#editCell"
|
|
612
|
-
end
|
|
613
|
-
|
|
614
|
-
def compute_fixed_offsets(leaf_columns)
|
|
615
|
-
@fixed_offsets = {}
|
|
616
|
-
return if leaf_columns.blank?
|
|
617
|
-
|
|
618
|
-
left_offset = 0
|
|
619
|
-
leaf_columns.each do |column|
|
|
620
|
-
next unless column[:fixed] == :left
|
|
621
|
-
|
|
622
|
-
width = fixed_column_width!(column)
|
|
623
|
-
@fixed_offsets[column[:key]] = { side: :left, offset: left_offset }
|
|
624
|
-
left_offset += width
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
right_offset = 0
|
|
628
|
-
leaf_columns.reverse_each do |column|
|
|
629
|
-
next unless column[:fixed] == :right
|
|
630
|
-
|
|
631
|
-
width = fixed_column_width!(column)
|
|
632
|
-
@fixed_offsets[column[:key]] = { side: :right, offset: right_offset }
|
|
633
|
-
right_offset += width
|
|
634
|
-
end
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
def fixed_column_width!(column)
|
|
638
|
-
width = numeric_width(column[:width])
|
|
639
|
-
return width if width&.positive?
|
|
640
|
-
|
|
641
|
-
raise ArgumentError, "Column #{column[:key] || column[:data_index] || column[:title]} must define a pixel width when fixed"
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
def numeric_width(value)
|
|
645
|
-
return value.to_f if value.is_a?(Numeric)
|
|
646
|
-
return nil unless value.is_a?(String)
|
|
647
|
-
|
|
648
|
-
stripped = value.strip
|
|
649
|
-
return stripped.to_f if stripped.match?(/\A[\d.]+\z/)
|
|
650
|
-
|
|
651
|
-
match = stripped.match(/\A([\d.]+)px\z/i)
|
|
652
|
-
match ? match[1].to_f : nil
|
|
653
|
-
end
|
|
654
|
-
|
|
655
|
-
def ellipsis_config(column)
|
|
656
|
-
column[:ellipsis]
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
def normalize_ellipsis_option(option)
|
|
660
|
-
return nil if option.nil? || option == false
|
|
661
|
-
|
|
662
|
-
if option == true
|
|
663
|
-
{ show_title: true }
|
|
664
|
-
elsif option.is_a?(Hash)
|
|
665
|
-
normalized = option.deep_symbolize_keys
|
|
666
|
-
normalized[:tooltip] = normalize_ellipsis_tooltip_option(normalized[:tooltip])
|
|
667
|
-
normalized.compact.presence
|
|
668
|
-
else
|
|
669
|
-
{ tooltip: normalize_ellipsis_tooltip_option(option) }
|
|
670
|
-
end
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def normalize_ellipsis_tooltip_option(option)
|
|
674
|
-
return nil if option.nil? || option == false
|
|
675
|
-
return {} if option == true
|
|
676
|
-
return { title: option } if option.is_a?(String)
|
|
677
|
-
return option.deep_symbolize_keys if option.is_a?(Hash)
|
|
678
|
-
|
|
679
|
-
nil
|
|
680
|
-
end
|
|
681
|
-
|
|
682
|
-
def ellipsis_title(config, raw_value, content_fragment)
|
|
683
|
-
return nil unless config&.dig(:show_title)
|
|
684
|
-
|
|
685
|
-
value = raw_value.presence || content_fragment
|
|
686
|
-
value = value.to_s if value.respond_to?(:to_s)
|
|
687
|
-
value.present? ? value : nil
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
def ellipsis_tooltip_component(config, raw_value, row, row_index)
|
|
691
|
-
tooltip = config&.dig(:tooltip)
|
|
692
|
-
return nil unless tooltip
|
|
693
|
-
|
|
694
|
-
options = tooltip.deep_dup
|
|
695
|
-
title = options.delete(:title)
|
|
696
|
-
title ||= raw_value
|
|
697
|
-
title = title.call(row, row_index) if title.respond_to?(:call)
|
|
698
|
-
title = title.to_s if title.respond_to?(:to_s)
|
|
699
|
-
return nil if title.blank?
|
|
700
|
-
|
|
701
|
-
Hakumi::Tooltip::Component.new(**{ title: title }.merge(options))
|
|
702
|
-
end
|
|
703
|
-
|
|
704
574
|
def responsive_classes(column)
|
|
705
575
|
responsive = column[:responsive]
|
|
706
576
|
return [] unless responsive.present?
|
|
@@ -823,33 +693,6 @@ module Hakumi
|
|
|
823
693
|
}
|
|
824
694
|
end
|
|
825
695
|
|
|
826
|
-
# Normalizes the table-level editable configuration.
|
|
827
|
-
# Accepts boolean, symbol (:cell or :row), or hash with options.
|
|
828
|
-
# Returns nil if editing is disabled, or a hash with normalized options.
|
|
829
|
-
#
|
|
830
|
-
# @param value [Boolean, Symbol, Hash, nil] editable configuration
|
|
831
|
-
# @return [Hash, nil] normalized configuration
|
|
832
|
-
def normalize_editable(value)
|
|
833
|
-
return nil if value.nil? || value == false
|
|
834
|
-
|
|
835
|
-
case value
|
|
836
|
-
when true
|
|
837
|
-
{ mode: :cell, trigger: :click }
|
|
838
|
-
when Symbol
|
|
839
|
-
mode = EDITABLE_MODES.include?(value) ? value : :cell
|
|
840
|
-
{ mode: mode, trigger: :click }
|
|
841
|
-
when Hash
|
|
842
|
-
normalized = value.deep_symbolize_keys
|
|
843
|
-
mode = normalized[:mode]&.to_sym
|
|
844
|
-
normalized[:mode] = EDITABLE_MODES.include?(mode) ? mode : :cell
|
|
845
|
-
trigger = normalized[:trigger]&.to_sym
|
|
846
|
-
normalized[:trigger] = EDITABLE_TRIGGERS.include?(trigger) ? trigger : :click
|
|
847
|
-
normalized
|
|
848
|
-
else
|
|
849
|
-
nil
|
|
850
|
-
end
|
|
851
|
-
end
|
|
852
|
-
|
|
853
696
|
# Normalizes row drag configuration.
|
|
854
697
|
# Can be: true (drag whole row), :handle (drag via handle only), or Hash with options
|
|
855
698
|
#
|
|
@@ -874,44 +717,6 @@ module Hakumi
|
|
|
874
717
|
end
|
|
875
718
|
end
|
|
876
719
|
|
|
877
|
-
# Normalizes column-level editable configuration.
|
|
878
|
-
# Column editable can override table-level settings.
|
|
879
|
-
#
|
|
880
|
-
# @param value [Boolean, Hash, nil] column editable configuration
|
|
881
|
-
# @return [Hash, nil] normalized configuration
|
|
882
|
-
def normalize_column_editable(value)
|
|
883
|
-
return nil if value.nil? || value == false
|
|
884
|
-
return {} if value == true
|
|
885
|
-
|
|
886
|
-
return nil unless value.is_a?(Hash)
|
|
887
|
-
|
|
888
|
-
normalized = value.deep_symbolize_keys
|
|
889
|
-
input_type = normalized[:type]&.to_sym || normalized[:input_type]&.to_sym
|
|
890
|
-
normalized[:input_type] = EDITABLE_INPUT_TYPES.include?(input_type) ? input_type : :text
|
|
891
|
-
normalized.except(:type)
|
|
892
|
-
end
|
|
893
|
-
|
|
894
|
-
def normalize_should_cell_update(value)
|
|
895
|
-
case value
|
|
896
|
-
when Symbol
|
|
897
|
-
value.to_s
|
|
898
|
-
when String
|
|
899
|
-
value.strip.presence
|
|
900
|
-
else
|
|
901
|
-
nil
|
|
902
|
-
end
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
# Returns JSON configuration for the table-level editable settings
|
|
906
|
-
# to be passed to the Stimulus controller.
|
|
907
|
-
#
|
|
908
|
-
# @return [String, nil] JSON string or nil if not editable
|
|
909
|
-
def editable_config_json
|
|
910
|
-
return nil unless @editable
|
|
911
|
-
|
|
912
|
-
@editable.to_json
|
|
913
|
-
end
|
|
914
|
-
|
|
915
720
|
def columns_config_json
|
|
916
721
|
leaf_columns.map do |col|
|
|
917
722
|
{
|
|
@@ -921,37 +726,6 @@ module Hakumi
|
|
|
921
726
|
}.compact
|
|
922
727
|
end.to_json
|
|
923
728
|
end
|
|
924
|
-
|
|
925
|
-
# Returns the editable payload for a specific cell/column.
|
|
926
|
-
# Used to add data-editable-config attribute to editable cells.
|
|
927
|
-
#
|
|
928
|
-
# @param column [Hash] the column definition
|
|
929
|
-
# @return [Hash, nil] editable configuration for the cell
|
|
930
|
-
def cell_editable_payload(column)
|
|
931
|
-
return nil if column[:selection] || column[:expand]
|
|
932
|
-
return nil unless column[:data_index]
|
|
933
|
-
|
|
934
|
-
column_config = column[:editable]
|
|
935
|
-
|
|
936
|
-
# Column must explicitly opt-in to be editable
|
|
937
|
-
return nil if column_config.nil? || column_config == false
|
|
938
|
-
|
|
939
|
-
# Merge table-level and column-level config
|
|
940
|
-
base_config = @editable&.dup || {}
|
|
941
|
-
base_config.merge!(column_config) if column_config.is_a?(Hash)
|
|
942
|
-
|
|
943
|
-
base_config[:data_index] = column[:data_index]
|
|
944
|
-
base_config[:key] = column[:key]
|
|
945
|
-
base_config
|
|
946
|
-
end
|
|
947
|
-
|
|
948
|
-
def apply_should_cell_update_attributes!(attrs, column)
|
|
949
|
-
hook = column[:should_cell_update]
|
|
950
|
-
return unless hook.present?
|
|
951
|
-
|
|
952
|
-
attrs[:data] ||= {}
|
|
953
|
-
attrs[:data][:should_cell_update] = hook
|
|
954
|
-
end
|
|
955
729
|
end
|
|
956
730
|
end
|
|
957
731
|
end
|
|
@@ -66,7 +66,7 @@ module Hakumi
|
|
|
66
66
|
normalized = column.deep_symbolize_keys
|
|
67
67
|
return nil if normalized[:hidden]
|
|
68
68
|
|
|
69
|
-
normalized[:key] ||= normalized[:data_index] ||
|
|
69
|
+
normalized[:key] ||= normalized[:data_index] || generate_id("col")
|
|
70
70
|
normalized[:ellipsis] = normalize_ellipsis_option(normalized[:ellipsis])
|
|
71
71
|
normalized[:responsive] = normalize_responsive_breakpoints(normalized[:responsive])
|
|
72
72
|
normalized[:width] = dimension_to_css(normalized[:width])
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hakumi
|
|
4
|
+
module Table
|
|
5
|
+
module Concerns
|
|
6
|
+
# Editable cell and row functionality for Table component
|
|
7
|
+
module Editable
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
EDITABLE_MODES = %i[cell row].freeze
|
|
11
|
+
EDITABLE_TRIGGERS = %i[click dblclick].freeze
|
|
12
|
+
EDITABLE_INPUT_TYPES = %i[text number textarea select].freeze
|
|
13
|
+
|
|
14
|
+
# Returns JSON configuration for the table-level editable settings
|
|
15
|
+
# to be passed to the Stimulus controller.
|
|
16
|
+
#
|
|
17
|
+
# @return [String, nil] JSON string or nil if not editable
|
|
18
|
+
def editable_config_json
|
|
19
|
+
return nil unless @editable
|
|
20
|
+
|
|
21
|
+
@editable.to_json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Normalizes the table-level editable configuration.
|
|
27
|
+
# Accepts boolean, symbol (:cell or :row), or hash with options.
|
|
28
|
+
# Returns nil if editing is disabled, or a hash with normalized options.
|
|
29
|
+
#
|
|
30
|
+
# @param value [Boolean, Symbol, Hash, nil] editable configuration
|
|
31
|
+
# @return [Hash, nil] normalized configuration
|
|
32
|
+
def normalize_editable(value)
|
|
33
|
+
return nil if value.nil? || value == false
|
|
34
|
+
|
|
35
|
+
case value
|
|
36
|
+
when true
|
|
37
|
+
{ mode: :cell, trigger: :click }
|
|
38
|
+
when Symbol
|
|
39
|
+
mode = EDITABLE_MODES.include?(value) ? value : :cell
|
|
40
|
+
{ mode: mode, trigger: :click }
|
|
41
|
+
when Hash
|
|
42
|
+
normalized = value.deep_symbolize_keys
|
|
43
|
+
mode = normalized[:mode]&.to_sym
|
|
44
|
+
normalized[:mode] = EDITABLE_MODES.include?(mode) ? mode : :cell
|
|
45
|
+
trigger = normalized[:trigger]&.to_sym
|
|
46
|
+
normalized[:trigger] = EDITABLE_TRIGGERS.include?(trigger) ? trigger : :click
|
|
47
|
+
normalized
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Normalizes column-level editable configuration.
|
|
52
|
+
# Column editable can override table-level settings.
|
|
53
|
+
#
|
|
54
|
+
# @param value [Boolean, Hash, nil] column editable configuration
|
|
55
|
+
# @return [Hash, nil] normalized configuration
|
|
56
|
+
def normalize_column_editable(value)
|
|
57
|
+
return nil if value.nil? || value == false
|
|
58
|
+
return {} if value == true
|
|
59
|
+
return nil unless value.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
normalized = value.deep_symbolize_keys
|
|
62
|
+
input_type = normalized[:type]&.to_sym || normalized[:input_type]&.to_sym
|
|
63
|
+
normalized[:input_type] = EDITABLE_INPUT_TYPES.include?(input_type) ? input_type : :text
|
|
64
|
+
normalized.except(:type)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_should_cell_update(value)
|
|
68
|
+
case value
|
|
69
|
+
when Symbol
|
|
70
|
+
value.to_s
|
|
71
|
+
when String
|
|
72
|
+
value.strip.presence
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Applies editable attributes to cell
|
|
77
|
+
def apply_editable_attributes!(attrs, column)
|
|
78
|
+
payload = cell_editable_payload(column)
|
|
79
|
+
return unless payload
|
|
80
|
+
|
|
81
|
+
attrs[:class] = [ attrs[:class], "hakumi-table-cell-editable" ].compact.join(" ")
|
|
82
|
+
attrs[:data] ||= {}
|
|
83
|
+
attrs[:data][:editable] = true
|
|
84
|
+
attrs[:data][:editableConfig] = payload.to_json
|
|
85
|
+
attrs[:data][:action] = "click->hakumi--table#editCell"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_should_cell_update_attributes!(attrs, column)
|
|
89
|
+
hook = column[:should_cell_update]
|
|
90
|
+
return unless hook.present?
|
|
91
|
+
|
|
92
|
+
attrs[:data] ||= {}
|
|
93
|
+
attrs[:data][:should_cell_update] = hook
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the editable payload for a specific cell/column.
|
|
97
|
+
# Used to add data-editable-config attribute to editable cells.
|
|
98
|
+
#
|
|
99
|
+
# @param column [Hash] the column definition
|
|
100
|
+
# @return [Hash, nil] editable configuration for the cell
|
|
101
|
+
def cell_editable_payload(column)
|
|
102
|
+
return nil if column[:selection] || column[:expand]
|
|
103
|
+
return nil unless column[:data_index]
|
|
104
|
+
|
|
105
|
+
column_config = column[:editable]
|
|
106
|
+
|
|
107
|
+
# Column must explicitly opt-in to be editable
|
|
108
|
+
return nil if column_config.nil? || column_config == false
|
|
109
|
+
|
|
110
|
+
# Merge table-level and column-level config
|
|
111
|
+
base_config = @editable&.dup || {}
|
|
112
|
+
base_config.merge!(column_config) if column_config.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
base_config[:data_index] = column[:data_index]
|
|
115
|
+
base_config[:key] = column[:key]
|
|
116
|
+
base_config
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hakumi
|
|
4
|
+
module Table
|
|
5
|
+
module Concerns
|
|
6
|
+
# Text ellipsis and tooltip handling for Table cells
|
|
7
|
+
module Ellipsis
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def ellipsis_config(column)
|
|
13
|
+
column[:ellipsis]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def normalize_ellipsis_option(option)
|
|
17
|
+
return nil if option.nil? || option == false
|
|
18
|
+
|
|
19
|
+
if option == true
|
|
20
|
+
{ show_title: true }
|
|
21
|
+
elsif option.is_a?(Hash)
|
|
22
|
+
normalized = option.deep_symbolize_keys
|
|
23
|
+
normalized[:tooltip] = normalize_ellipsis_tooltip_option(normalized[:tooltip])
|
|
24
|
+
normalized.compact.presence
|
|
25
|
+
else
|
|
26
|
+
{ tooltip: normalize_ellipsis_tooltip_option(option) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def normalize_ellipsis_tooltip_option(option)
|
|
31
|
+
return nil if option.nil? || option == false
|
|
32
|
+
return {} if option == true
|
|
33
|
+
return { title: option } if option.is_a?(String)
|
|
34
|
+
return option.deep_symbolize_keys if option.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ellipsis_title(config, raw_value, content_fragment)
|
|
40
|
+
return nil unless config&.dig(:show_title)
|
|
41
|
+
|
|
42
|
+
value = raw_value.presence || content_fragment
|
|
43
|
+
value = value.to_s if value.respond_to?(:to_s)
|
|
44
|
+
value.present? ? value : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ellipsis_tooltip_component(config, raw_value, row, row_index)
|
|
48
|
+
tooltip = config&.dig(:tooltip)
|
|
49
|
+
return nil unless tooltip
|
|
50
|
+
|
|
51
|
+
options = tooltip.deep_dup
|
|
52
|
+
title = options.delete(:title)
|
|
53
|
+
title ||= raw_value
|
|
54
|
+
title = title.call(row, row_index) if title.respond_to?(:call)
|
|
55
|
+
title = title.to_s if title.respond_to?(:to_s)
|
|
56
|
+
return nil if title.blank?
|
|
57
|
+
|
|
58
|
+
Hakumi::Tooltip::Component.new(**{ title: title }.merge(options))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hakumi
|
|
4
|
+
module Table
|
|
5
|
+
module Concerns
|
|
6
|
+
# Fixed (sticky) column positioning for Table component
|
|
7
|
+
module FixedColumns
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# Check if table has any fixed columns
|
|
11
|
+
def fixed_columns?
|
|
12
|
+
fixed_offsets.present?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def fixed_offsets
|
|
18
|
+
@fixed_offsets ||= {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Applies fixed column positioning attributes to cell
|
|
22
|
+
def apply_fixed_column_attributes!(attrs, column, header: false)
|
|
23
|
+
info = fixed_offsets[column[:key]]
|
|
24
|
+
return unless info
|
|
25
|
+
|
|
26
|
+
attrs[:class] = [ attrs[:class], "hakumi-table-cell-fixed", "hakumi-table-cell-fixed-#{info[:side]}" ].compact.join(" ")
|
|
27
|
+
|
|
28
|
+
style_parts = Array.new
|
|
29
|
+
style_parts << attrs[:style] if attrs[:style].present?
|
|
30
|
+
style_parts << "position: sticky"
|
|
31
|
+
style_parts << "#{info[:side]}: #{info[:offset]}px"
|
|
32
|
+
style_parts << "z-index: #{header ? 3 : 2}"
|
|
33
|
+
attrs[:style] = style_parts.compact.join("; ")
|
|
34
|
+
|
|
35
|
+
attrs[:data] ||= {}
|
|
36
|
+
attrs[:data][:fixed] = info[:side]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Compute fixed column offsets from left and right
|
|
40
|
+
def compute_fixed_offsets(leaf_columns)
|
|
41
|
+
@fixed_offsets = {}
|
|
42
|
+
return if leaf_columns.blank?
|
|
43
|
+
|
|
44
|
+
# Calculate left fixed columns
|
|
45
|
+
left_offset = 0
|
|
46
|
+
leaf_columns.each do |column|
|
|
47
|
+
next unless column[:fixed] == :left
|
|
48
|
+
|
|
49
|
+
width = fixed_column_width!(column)
|
|
50
|
+
@fixed_offsets[column[:key]] = { side: :left, offset: left_offset }
|
|
51
|
+
left_offset += width
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Calculate right fixed columns (from right to left)
|
|
55
|
+
right_offset = 0
|
|
56
|
+
leaf_columns.reverse_each do |column|
|
|
57
|
+
next unless column[:fixed] == :right
|
|
58
|
+
|
|
59
|
+
width = fixed_column_width!(column)
|
|
60
|
+
@fixed_offsets[column[:key]] = { side: :right, offset: right_offset }
|
|
61
|
+
right_offset += width
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fixed_column_width!(column)
|
|
66
|
+
width = numeric_width(column[:width])
|
|
67
|
+
return width if width&.positive?
|
|
68
|
+
|
|
69
|
+
raise ArgumentError, "Column #{column[:key] || column[:data_index] || column[:title]} must define a pixel width when fixed"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def numeric_width(value)
|
|
73
|
+
case value
|
|
74
|
+
when Integer, Float
|
|
75
|
+
value.to_f
|
|
76
|
+
when String
|
|
77
|
+
stripped = value.strip
|
|
78
|
+
return stripped.to_f if stripped.match?(/\A[\d.]+\z/)
|
|
79
|
+
|
|
80
|
+
match = stripped.match(/\A([\d.]+)px\z/i)
|
|
81
|
+
match ? match[1].to_f : nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Hakumi
|
|
4
4
|
class ComponentsController < ApplicationController
|
|
5
5
|
COMPONENTS = %i[modal confirm alert drawer message admin_panel notification popconfirm popover result skeleton spin affix qr_code segmented statistic table tag timeline tree].freeze
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ module HakumiComponents
|
|
|
16
16
|
raise ActionController::RoutingError, "Not Found" unless COMPONENTS.include?(component)
|
|
17
17
|
|
|
18
18
|
handler = ComponentHandler.new(component, params)
|
|
19
|
-
html = render_to_string(partial: "
|
|
19
|
+
html = render_to_string(partial: "hakumi/#{component}", formats: [ :html ], locals: handler.locals)
|
|
20
20
|
|
|
21
21
|
respond_to do |format|
|
|
22
22
|
format.html { render html: html, layout: false }
|