advanced_select 0.1.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.
@@ -0,0 +1,74 @@
1
+ <div class="<%= advanced_select_class(class_map, :root) %>"
2
+ data-controller="advanced-select"
3
+ data-advanced-select-url-value="<%= options_url %>"
4
+ data-advanced-select-target-id-value="<%= target_id %>"
5
+ data-advanced-select-name-value="<%= name %>"
6
+ data-advanced-select-input-id-value="<%= id %>"
7
+ data-advanced-select-placeholder-value="<%= placeholder %>"
8
+ data-advanced-select-loading-text-value="<%= t("shared.advanced_select.loading") %>"
9
+ data-advanced-select-error-text-value="<%= t("shared.advanced_select.error") %>"
10
+ data-advanced-select-dependent-fields-value="<%= dependent_fields.to_json %>"
11
+ data-advanced-select-multiple-value="<%= multiple %>"
12
+ data-advanced-select-searchable-value="<%= searchable %>"
13
+ data-advanced-select-add-mode-value="<%= add_mode %>"
14
+ data-advanced-select-placeholder-class="<%= advanced_select_class(class_map, :placeholder) %>"
15
+ data-advanced-select-value-class="<%= advanced_select_class(class_map, :value) %>"
16
+ data-advanced-select-token-class="<%= advanced_select_class(class_map, :token) %>"
17
+ data-advanced-select-loading-class="<%= advanced_select_class(class_map, :loading) %>"
18
+ data-advanced-select-error-class="<%= advanced_select_class(class_map, :error) %>"
19
+ data-advanced-select-option-active-class="<%= advanced_select_state_class(class_map, :option_active) %>"
20
+ data-advanced-select-add-option-active-class="<%= advanced_select_state_class(class_map, :add_option_active) %>"
21
+ data-advanced-select-option-selected-class="<%= advanced_select_state_class(class_map, :option_selected) %>"
22
+ data-advanced-select-selected-value="<%= advanced_select_selected_value(selected_options) %>">
23
+ <div data-advanced-select-target="hiddenFields">
24
+ <% if multiple %>
25
+ <% selected_options.each do |option| %>
26
+ <%= hidden_field_tag name, option.fetch(:value, option.fetch(:id)) %>
27
+ <% end %>
28
+ <% else %>
29
+ <%= hidden_field_tag name, selected_options.first&.fetch(:value, selected_options.first&.fetch(:id)), id: id %>
30
+ <% end %>
31
+ </div>
32
+
33
+ <button type="button"
34
+ id="<%= "#{id}_trigger" %>"
35
+ class="<%= advanced_select_class(class_map, :trigger) %>"
36
+ aria-haspopup="listbox"
37
+ aria-expanded="false"
38
+ aria-controls="<%= "#{id}_dropdown" %>"
39
+ data-advanced-select-target="trigger"
40
+ data-action="advanced-select#toggle keydown->advanced-select#keydown">
41
+ <span id="<%= "#{id}_summary" %>" class="<%= advanced_select_class(class_map, :summary) %>" data-advanced-select-target="summary">
42
+ <%= render partial: "advanced_select/summary", locals: { selected_options: selected_options, multiple: multiple, placeholder: placeholder, class_map: class_map } %>
43
+ </span>
44
+ <span id="<%= "#{id}_caret" %>" class="<%= [advanced_select_class(class_map, :caret), ("hidden" if selected_options.any?)].compact.join(" ") %>" data-advanced-select-target="caret">&#8964;</span>
45
+ <span id="<%= "#{id}_clear" %>" class="<%= [advanced_select_class(class_map, :clear), ("hidden" if selected_options.empty?)].compact.join(" ") %>" data-advanced-select-target="clear" data-action="click->advanced-select#clear">&times;</span>
46
+ </button>
47
+
48
+ <div id="<%= "#{id}_dropdown" %>"
49
+ class="<%= [advanced_select_class(class_map, :dropdown), "hidden"].join(" ") %>"
50
+ data-advanced-select-target="dropdown">
51
+ <% if searchable %>
52
+ <%= tag.input type: "search",
53
+ id: "#{id}_search",
54
+ autocomplete: "off",
55
+ placeholder: placeholder,
56
+ class: advanced_select_class(class_map, :search),
57
+ data: {
58
+ advanced_select_target: "search",
59
+ action: "input->advanced-select#search keydown->advanced-select#keydown"
60
+ } %>
61
+ <% end %>
62
+
63
+ <%= render partial: "advanced_select/options", locals: {
64
+ target_id: target_id,
65
+ selected_options: selected_options,
66
+ options: advanced_select_options_for_render(options, selected_options, searchable),
67
+ multiple: multiple,
68
+ add_mode: add_mode,
69
+ query: nil,
70
+ option_content_partial: option_content_partial,
71
+ class_map: class_map
72
+ } %>
73
+ </div>
74
+ </div>
@@ -0,0 +1,14 @@
1
+ <% if selected_options.any? %>
2
+ <% if multiple %>
3
+ <% selected_options.first(2).each do |option| %>
4
+ <span class="<%= advanced_select_class(class_map, :token) %>"><%= option.fetch(:display_label) %></span>
5
+ <% end %>
6
+ <% if selected_options.size > 2 %>
7
+ <span class="<%= advanced_select_class(class_map, :token) %>"><%= "& +#{selected_options.size - 2}" %></span>
8
+ <% end %>
9
+ <% else %>
10
+ <span class="<%= advanced_select_class(class_map, :value) %>"><%= selected_options.first.fetch(:display_label) %></span>
11
+ <% end %>
12
+ <% else %>
13
+ <span class="<%= advanced_select_class(class_map, :placeholder) %>"><%= placeholder %></span>
14
+ <% end %>
@@ -0,0 +1,7 @@
1
+ en:
2
+ shared:
3
+ advanced_select:
4
+ add_option: "Add %{query}"
5
+ empty: "No options found"
6
+ error: "Options could not be loaded"
7
+ loading: "Loading..."
@@ -0,0 +1,7 @@
1
+ tr:
2
+ shared:
3
+ advanced_select:
4
+ add_option: "%{query} ekle"
5
+ empty: "Seçenek bulunamadı"
6
+ error: "Seçenekler yüklenemedi"
7
+ loading: "Yükleniyor..."
@@ -0,0 +1,55 @@
1
+ module AdvancedSelect
2
+ class ClassMap
3
+ DEFAULTS = {
4
+ root: "ui-advanced-select",
5
+ trigger: "ui-advanced-select-trigger",
6
+ summary: "ui-advanced-select-summary",
7
+ placeholder: "ui-advanced-select-placeholder",
8
+ value: "ui-advanced-select-value",
9
+ token: "ui-advanced-select-token",
10
+ caret: "ui-advanced-select-caret",
11
+ clear: "ui-advanced-select-clear",
12
+ dropdown: "ui-advanced-select-dropdown",
13
+ search: "ui-advanced-select-search",
14
+ options: "ui-advanced-select-options",
15
+ option: "ui-advanced-select-option",
16
+ option_active: "ui-advanced-select-option-active",
17
+ option_selected: "",
18
+ option_check: "ui-advanced-select-option-check",
19
+ option_content: "ui-advanced-select-option-content",
20
+ option_description: "ui-advanced-select-option-description",
21
+ group_label: "ui-advanced-select-group-label",
22
+ add_option: "ui-advanced-select-add-option",
23
+ add_option_active: "",
24
+ empty: "ui-advanced-select-empty",
25
+ loading: "ui-advanced-select-loading",
26
+ error: "ui-advanced-select-error"
27
+ }.freeze
28
+
29
+ def initialize(classes = {}, append_classes = {})
30
+ @classes = normalize(classes)
31
+ @append_classes = normalize(append_classes)
32
+ end
33
+
34
+ def class_name(*keys)
35
+ keys.compact.map { |key| class_for(key.to_sym) }.compact.reject(&:empty?).join(" ")
36
+ end
37
+
38
+ def state_class(key)
39
+ class_name(key)
40
+ end
41
+
42
+ private
43
+
44
+ def normalize(classes)
45
+ classes.to_h.each_with_object({}) do |(key, value), normalized|
46
+ class_name = value.to_s.squish
47
+ normalized[key.to_sym] = class_name if class_name.present?
48
+ end
49
+ end
50
+
51
+ def class_for(key)
52
+ [@classes.fetch(key) { DEFAULTS[key] }, @append_classes[key]].compact.reject(&:empty?).join(" ")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module AdvancedSelect
2
+ class Engine < ::Rails::Engine
3
+ initializer "advanced_select.helper" do
4
+ ActiveSupport.on_load(:action_view) do
5
+ include AdvancedSelect::Helper
6
+ end
7
+ end
8
+
9
+ initializer "advanced_select.assets" do |app|
10
+ if app.config.respond_to?(:assets)
11
+ javascript_path = root.join("app/javascript")
12
+ controller_asset = "advanced_select/advanced_select_controller.js"
13
+
14
+ unless app.config.assets.paths.map(&:to_s).include?(javascript_path.to_s)
15
+ app.config.assets.paths << javascript_path
16
+ end
17
+
18
+ unless app.config.assets.precompile.include?(controller_asset)
19
+ app.config.assets.precompile << controller_asset
20
+ end
21
+ end
22
+ end
23
+
24
+ rake_tasks do
25
+ load root.join("lib/tasks/advanced_select/tasks.rake")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,153 @@
1
+ module AdvancedSelect
2
+ module Helper
3
+ def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_url: nil, multiple: false, searchable: true, add_mode: false, dependent_fields: {}, option_content_partial: nil, classes: {}, append_classes: {})
4
+ selected_options = advanced_select_selected_options(selected)
5
+ class_map = advanced_select_class_map(classes, append_classes)
6
+
7
+ render partial: "advanced_select/select", locals: {
8
+ name: name,
9
+ id: id,
10
+ options_url: options_url,
11
+ selected_options: selected_options,
12
+ options: options,
13
+ placeholder: placeholder,
14
+ multiple: multiple,
15
+ searchable: searchable && options_url.present?,
16
+ add_mode: add_mode,
17
+ dependent_fields: dependent_fields,
18
+ target_id: "#{id}_options",
19
+ option_content_partial: option_content_partial,
20
+ class_map: class_map
21
+ }
22
+ end
23
+
24
+ def advanced_select_options_tag(target_id:, selected:, options:, multiple: false, add_mode: false, query: nil, option_content_partial: nil, classes: {}, append_classes: {})
25
+ render partial: "advanced_select/options", locals: {
26
+ target_id: target_id,
27
+ selected_options: advanced_select_selected_options(selected),
28
+ options: options,
29
+ multiple: multiple,
30
+ add_mode: add_mode,
31
+ query: query,
32
+ option_content_partial: option_content_partial,
33
+ class_map: advanced_select_class_map(classes, append_classes)
34
+ }
35
+ end
36
+
37
+ def advanced_select_class(class_map, *keys)
38
+ class_map.class_name(*keys)
39
+ end
40
+
41
+ def advanced_select_state_class(class_map, key)
42
+ class_map.state_class(key)
43
+ end
44
+
45
+ def advanced_select_selected_options(selected)
46
+ advanced_select_array(selected).map do |option|
47
+ {
48
+ id: option.fetch(:id).to_s,
49
+ value: advanced_select_option_value(option),
50
+ label: advanced_select_option_label(option),
51
+ display_label: advanced_select_option_display_label(option)
52
+ }
53
+ end
54
+ end
55
+
56
+ def advanced_select_selected_value(selected_options)
57
+ selected_options.map do |option|
58
+ option.merge(displayLabel: option.fetch(:display_label))
59
+ end.to_json
60
+ end
61
+
62
+ def advanced_select_options_for_render(options, selected_options, searchable)
63
+ searchable ? selected_options.presence || options : options
64
+ end
65
+
66
+ def advanced_select_add_option?(options, selected_options, add_mode, query)
67
+ return false unless add_mode && query.present?
68
+
69
+ query_label = advanced_select_normalized_label(query)
70
+ advanced_select_matched_labels(options, selected_options).none? do |label|
71
+ advanced_select_normalized_label(label) == query_label
72
+ end
73
+ end
74
+
75
+ def advanced_select_option_groups(options)
76
+ options.map do |option|
77
+ if option.key?(:options)
78
+ { label: option.fetch(:label), options: option.fetch(:options) }
79
+ else
80
+ { label: nil, options: [option] }
81
+ end
82
+ end
83
+ end
84
+
85
+ def advanced_select_option_selected?(option, selected_options)
86
+ selected_options.any? { |selected_option| selected_option.fetch(:id).to_s == option.fetch(:id).to_s }
87
+ end
88
+
89
+ def advanced_select_options_empty?(options)
90
+ advanced_select_flat_options(options).empty?
91
+ end
92
+
93
+ def advanced_select_option_label(option)
94
+ option.fetch(:label, option.fetch(:display_label, option.fetch(:value, option.fetch(:id)))).to_s
95
+ end
96
+
97
+ def advanced_select_option_value(option)
98
+ option.fetch(:value, option.fetch(:id)).to_s
99
+ end
100
+
101
+ def advanced_select_option_display_label(option)
102
+ option.fetch(:display_label, advanced_select_display_label(advanced_select_option_label(option))).to_s
103
+ end
104
+
105
+ def advanced_select_option_description(option)
106
+ option[:description].to_s
107
+ end
108
+
109
+ def advanced_select_display_label(label)
110
+ label.to_s.split(" > ").last
111
+ end
112
+
113
+ def advanced_select_array(value)
114
+ case value
115
+ when nil
116
+ []
117
+ when Array
118
+ value.compact
119
+ else
120
+ [value]
121
+ end
122
+ end
123
+
124
+ def advanced_select_matched_labels(options, selected_options)
125
+ option_labels = advanced_select_flat_options(options).flat_map do |option|
126
+ label = advanced_select_option_label(option)
127
+ [label, advanced_select_option_display_label(option)]
128
+ end
129
+
130
+ selected_labels = selected_options.flat_map do |option|
131
+ [advanced_select_option_label(option), advanced_select_option_display_label(option)]
132
+ end
133
+
134
+ option_labels + selected_labels
135
+ end
136
+
137
+ def advanced_select_normalized_label(label)
138
+ I18n.transliterate(label.to_s.squish).downcase
139
+ end
140
+
141
+ def advanced_select_flat_options(options)
142
+ options.flat_map { |option| option.key?(:options) ? option.fetch(:options) : option }
143
+ end
144
+
145
+ def advanced_select_class_map(classes, append_classes = {})
146
+ if classes.is_a?(AdvancedSelect::ClassMap)
147
+ classes
148
+ else
149
+ AdvancedSelect::ClassMap.new(classes, append_classes)
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,3 @@
1
+ module AdvancedSelect
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,8 @@
1
+ require "advanced_select/version"
2
+ require "advanced_select/class_map"
3
+ require "advanced_select/engine"
4
+ require "advanced_select/helper"
5
+
6
+ module AdvancedSelect
7
+ # Your code goes here...
8
+ end
@@ -0,0 +1,161 @@
1
+ module AdvancedSelect
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ VALID_SETUPS = %w[importmap jsbundling].freeze
5
+
6
+ source_root File.expand_path("templates", __dir__)
7
+ class_option :setup,
8
+ type: :string,
9
+ default: "importmap",
10
+ desc: "Host app setup: importmap or jsbundling"
11
+
12
+ def validate_options
13
+ return if VALID_SETUPS.include?(setup)
14
+
15
+ raise Thor::Error, "Invalid --setup=#{setup.inspect}. Expected one of: #{VALID_SETUPS.join(', ')}."
16
+ end
17
+
18
+ def install_stimulus_controller
19
+ case setup
20
+ when "importmap"
21
+ pin_importmap_controller
22
+ when "jsbundling"
23
+ copy_file "advanced_select_controller.js", "app/javascript/controllers/advanced_select_controller.js"
24
+ end
25
+ end
26
+
27
+ def install_stylesheet
28
+ if setup == "jsbundling"
29
+ copy_file "advanced_select.css", "app/assets/stylesheets/advanced_select.css"
30
+ end
31
+ end
32
+
33
+ def register_stimulus_controller
34
+ case setup
35
+ when "importmap"
36
+ register_importmap_controller
37
+ when "jsbundling"
38
+ register_manifest_controller
39
+ end
40
+ end
41
+
42
+ def import_stylesheet
43
+ case setup
44
+ when "importmap"
45
+ import_application_stylesheet
46
+ when "jsbundling"
47
+ append_postcss_import
48
+ end
49
+ end
50
+
51
+ def show_next_steps
52
+ say "AdvancedSelect installed.", :green
53
+ say "Setup: #{setup}"
54
+ say "Host apps still own routes, controllers, queries, and Turbo Stream option endpoints."
55
+ end
56
+
57
+ private
58
+
59
+ def pin_importmap_controller
60
+ path = "config/importmap.rb"
61
+ unless File.exist?(target_path(path))
62
+ say "Could not find #{path}; pin advanced_select/advanced_select_controller manually."
63
+ return
64
+ end
65
+ return if file_contains?(path, 'pin "advanced_select/advanced_select_controller"')
66
+
67
+ append_to_file path, <<~RUBY
68
+ pin "advanced_select/advanced_select_controller", to: "advanced_select/advanced_select_controller.js"
69
+ RUBY
70
+ end
71
+
72
+ def register_importmap_controller
73
+ path = "app/javascript/controllers/index.js"
74
+ unless File.exist?(target_path(path))
75
+ raise Thor::Error, "Could not find #{path} for --setup=importmap."
76
+ end
77
+ return if file_contains?(path, "advanced-select")
78
+
79
+ append_to_file path, <<~JS
80
+
81
+ import AdvancedSelectController from "advanced_select/advanced_select_controller"
82
+ application.register("advanced-select", AdvancedSelectController)
83
+ JS
84
+ end
85
+
86
+ def register_manifest_controller
87
+ unless File.exist?(target_path("app/javascript/controllers/index.js"))
88
+ raise Thor::Error, "Could not find app/javascript/controllers/index.js for --setup=jsbundling."
89
+ end
90
+ return if file_contains?("app/javascript/controllers/index.js", "advanced-select")
91
+
92
+ append_to_file "app/javascript/controllers/index.js", <<~JS
93
+
94
+ import AdvancedSelectController from "./advanced_select_controller"
95
+ application.register("advanced-select", AdvancedSelectController)
96
+ JS
97
+ end
98
+
99
+ def append_postcss_import
100
+ path = "app/assets/stylesheets/application.postcss.css"
101
+ unless File.exist?(target_path(path))
102
+ raise Thor::Error, "Could not find #{path} for --setup=jsbundling."
103
+ end
104
+ return if file_contains?(path, "advanced_select.css")
105
+
106
+ insert_css_import(path)
107
+ end
108
+
109
+ def import_application_stylesheet
110
+ path = "app/assets/stylesheets/application.css"
111
+ unless File.exist?(target_path(path))
112
+ say "Could not find #{path}; require advanced_select/advanced_select through your host app stylesheet entrypoint."
113
+ return
114
+ end
115
+
116
+ unless sprockets_manifest?(path) && normalize_sprockets_requires(path)
117
+ say "Could not safely patch #{path}; require advanced_select/advanced_select through your host app stylesheet entrypoint."
118
+ end
119
+ end
120
+
121
+ def file_contains?(path, content)
122
+ File.exist?(target_path(path)) && File.read(target_path(path)).include?(content)
123
+ end
124
+
125
+ def sprockets_manifest?(path)
126
+ content = File.read(target_path(path))
127
+ content.include?("/*") && content.include?("*/") && content.include?("*=")
128
+ end
129
+
130
+ def normalize_sprockets_requires(path)
131
+ lines = File.readlines(target_path(path))
132
+ lines.reject! { |line| line.match?(%r{^\s*\*=\s*require\s+advanced_select/advanced_select\s*$}) }
133
+
134
+ insert_at = lines.index { |line| line.match?(%r{^\s*\*=\s*require_tree\s+\.\s*$}) }
135
+ insert_at ||= lines.index { |line| line.match?(%r{^\s*\*=\s*require_self\s*$}) }
136
+ insert_at ||= lines.index { |line| line.strip == "*/" }
137
+ return false unless insert_at
138
+
139
+ lines.insert(insert_at, " *= require advanced_select/advanced_select\n")
140
+ File.write(target_path(path), lines.join)
141
+ true
142
+ end
143
+
144
+ def insert_css_import(path)
145
+ lines = File.readlines(target_path(path))
146
+ import_index = lines.rindex { |line| line.strip.start_with?("@import") }
147
+ lines.insert((import_index || -1) + 1, "@import \"advanced_select.css\";\n")
148
+
149
+ File.write(target_path(path), lines.join)
150
+ end
151
+
152
+ def setup
153
+ options[:setup].to_s
154
+ end
155
+
156
+ def target_path(path)
157
+ File.join(destination_root, path)
158
+ end
159
+ end
160
+ end
161
+ end