proscenium-ui 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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/lib/proscenium/ui/badge/index.css +75 -0
  5. data/lib/proscenium/ui/badge.rb +32 -0
  6. data/lib/proscenium/ui/breadcrumbs/computed_element.rb +71 -0
  7. data/lib/proscenium/ui/breadcrumbs/control.rb +103 -0
  8. data/lib/proscenium/ui/breadcrumbs/element.rb +16 -0
  9. data/lib/proscenium/ui/breadcrumbs/index.css +84 -0
  10. data/lib/proscenium/ui/breadcrumbs.rb +136 -0
  11. data/lib/proscenium/ui/combobox/index.css +162 -0
  12. data/lib/proscenium/ui/combobox/index.js +420 -0
  13. data/lib/proscenium/ui/combobox.rb +186 -0
  14. data/lib/proscenium/ui/component.rb +22 -0
  15. data/lib/proscenium/ui/flash/index.css +1 -0
  16. data/lib/proscenium/ui/flash/index.js +77 -0
  17. data/lib/proscenium/ui/flash.rb +15 -0
  18. data/lib/proscenium/ui/form/field_methods.rb +95 -0
  19. data/lib/proscenium/ui/form/fields/base.rb +189 -0
  20. data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
  21. data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
  22. data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
  23. data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
  24. data/lib/proscenium/ui/form/fields/combobox.rb +117 -0
  25. data/lib/proscenium/ui/form/fields/date.module.css +27 -0
  26. data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
  27. data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
  28. data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
  29. data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
  30. data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
  31. data/lib/proscenium/ui/form/fields/input.rb +14 -0
  32. data/lib/proscenium/ui/form/fields/radio_group.rb +175 -0
  33. data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
  34. data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
  35. data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
  36. data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
  37. data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
  38. data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
  39. data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
  40. data/lib/proscenium/ui/form/fields/select.jsx +47 -0
  41. data/lib/proscenium/ui/form/fields/select.module.css +46 -0
  42. data/lib/proscenium/ui/form/fields/select.rb +302 -0
  43. data/lib/proscenium/ui/form/fields/tel.css +297 -0
  44. data/lib/proscenium/ui/form/fields/tel.js +83 -0
  45. data/lib/proscenium/ui/form/fields/tel.rb +54 -0
  46. data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
  47. data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
  48. data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
  49. data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
  50. data/lib/proscenium/ui/form/index.css +52 -0
  51. data/lib/proscenium/ui/form/translation.rb +71 -0
  52. data/lib/proscenium/ui/form.rb +197 -0
  53. data/lib/proscenium/ui/railtie.rb +23 -0
  54. data/lib/proscenium/ui/ujs/class.js +15 -0
  55. data/lib/proscenium/ui/ujs/data_confirm.js +23 -0
  56. data/lib/proscenium/ui/ujs/data_disable_with.js +68 -0
  57. data/lib/proscenium/ui/ujs/index.js +10 -0
  58. data/lib/proscenium/ui/version.rb +7 -0
  59. data/lib/proscenium/ui.rb +36 -0
  60. metadata +177 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'phonelib'
4
+ require 'countries/iso3166'
5
+
6
+ module Proscenium::UI::Form::Fields
7
+ class Tel < Base
8
+ DEFAULT_COUNTRY = 'US'
9
+
10
+ sideload_assets js: { type: 'module' }
11
+
12
+ register_element :pui_tel_field
13
+
14
+ def initialize(attribute, model, form, type: nil, error: nil, **attributes)
15
+ super
16
+
17
+ @default_country = @attributes.delete(:default_country)&.to_s&.upcase || DEFAULT_COUNTRY
18
+ end
19
+
20
+ def view_template
21
+ field :pui_tel_field do
22
+ label for: field_id
23
+
24
+ div part: :inputs do
25
+ div part: :country do
26
+ select do
27
+ countries.each do |name, code|
28
+ option(value: code, selected: code == country) { name }
29
+ end
30
+ end
31
+ end
32
+
33
+ input(name: field_name, type: 'text', part: :number, id: field_id, **build_attributes)
34
+ end
35
+
36
+ hint
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def country
43
+ @country ||= if value.blank?
44
+ @default_country
45
+ else
46
+ Phonelib.parse(value, @default_country).country || @default_country
47
+ end
48
+ end
49
+
50
+ def countries
51
+ ISO3166::Country.all_names_with_codes.to_h
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,50 @@
1
+ import clsx from 'clsx'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import dsx from '/hue/lib/hue/utils/dsx'
5
+ import { useFormError } from '../../hooks'
6
+
7
+ import styles from './index.module.css'
8
+
9
+ const Textarea = ({ label, hint, className, inputClassName, errorAttrName, ...props }) => {
10
+ const [error, hasError] = useFormError(errorAttrName || props.name)
11
+
12
+ return (
13
+ <div className={clsx(styles.fieldWrapper, className)} {...dsx({ fieldError: hasError })}>
14
+ <label>
15
+ <span>
16
+ {label ? <span>{label}</span> : null}
17
+ {hasError ? <span>{error}</span> : null}
18
+ </span>
19
+
20
+ <textarea className={inputClassName || styles.input} {...props} />
21
+ </label>
22
+
23
+ {hint ? <div className={styles.hint}>{hint}</div> : null}
24
+ </div>
25
+ )
26
+ }
27
+
28
+ Textarea.displayName = 'Hue.Form.Fields.Textarea'
29
+ Textarea.propTypes = {
30
+ name: PropTypes.string.isRequired,
31
+
32
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]),
33
+
34
+ // Custom class name. This will be appended to the default class.
35
+ className: PropTypes.string,
36
+
37
+ // Custom class name for the actual textarea element. This will replace the default class.
38
+ inputClassName: PropTypes.string,
39
+
40
+ // The name of the attribute to use for the error message. Default: 'props.name'.
41
+ errorAttrName: PropTypes.string,
42
+
43
+ id: PropTypes.string,
44
+ hint: PropTypes.string,
45
+ disabled: PropTypes.bool
46
+
47
+ // All remaining non-descript props will be forwarded to the <input> element.
48
+ }
49
+
50
+ export default Textarea
@@ -0,0 +1,13 @@
1
+ @layer pui {
2
+ .fieldWrapper {
3
+ @mixin fieldWrapper from url('/hue/lib/hue/mixins/form.mixin.css');
4
+ }
5
+
6
+ .input {
7
+ @mixin textarea from url('/hue/lib/hue/mixins/textarea.mixin.css');
8
+ }
9
+
10
+ .hint {
11
+ @mixin fieldHint from url('/hue/lib/hue/mixins/field.mixin.css');
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ import Textarea from '../'
2
+
3
+ const Component = () => {
4
+ return <Textarea name="name" label="Name" hint="Some hint about the input" />
5
+ }
6
+ Component.displayName = 'Hue.Form.Fields.Textarea.Previews.Basic'
7
+
8
+ export default Component
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Fields
4
+ class Textarea < Base
5
+ register_element :pui_textarea
6
+
7
+ def view_template
8
+ field :pui_textarea do
9
+ label do
10
+ attrs = build_attributes
11
+ value = attrs.delete(:value)
12
+ textarea(name: field_name, **attrs) { value }
13
+ end
14
+ hint
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ @import "@rubygems/proscenium-ui/config/props.css";
2
+
3
+ pui-field {
4
+ display: block;
5
+
6
+ [part="error"] {
7
+ &::before {
8
+ content: " ";
9
+ }
10
+
11
+ color: red;
12
+ }
13
+
14
+ [part="hint"] {
15
+ font-size: 0.9em;
16
+ margin-top: 0.2em;
17
+ }
18
+
19
+ input {
20
+ border: var(--pui-input-border);
21
+ border-radius: var(--pui-input-border-radius);
22
+ background-color: var(--pui-input-background-color);
23
+ }
24
+ }
25
+
26
+ pui-checkbox {
27
+ display: block;
28
+
29
+ label {
30
+ display: flex;
31
+ gap: 0.3rem;
32
+ }
33
+ }
34
+
35
+ pui-radio-group {
36
+ [part="radio_group_inputs"] {
37
+ display: flex;
38
+ gap: 1em;
39
+
40
+ > label > input {
41
+ margin-right: 0.3em;
42
+ }
43
+ }
44
+ }
45
+
46
+ pui-textarea {
47
+ display: block;
48
+
49
+ textarea {
50
+ display: block;
51
+ }
52
+ }
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI::Form::Translation
4
+ # Lookup translations for the given namespace using I18n, based on model name, and attribute name.
5
+ #
6
+ # Lookup priority with nested attributes:
7
+ #
8
+ # form.{namespace}.{model}/{attribute.first}.{attribute.last}
9
+ # form.{namespace}.{attribute.first}.{attribute.last}
10
+ # form.{namespace}.{model}.{attribute.last}
11
+ # form.{namespace}.defaults.{attribute.last}
12
+ # {default}
13
+ #
14
+ # Lookup priority without nested attributes:
15
+ #
16
+ # form.{namespace}.{model}.{attribute}
17
+ # form.{namespace}.defaults.{attribute}
18
+ # {default}
19
+ #
20
+ # Namespace is used for :labels and :hints.
21
+ #
22
+ # Model is the actual object name, for a @user object you'll have :user.
23
+ # Attribute is the attribute itself, :name for example, or [:user, :name] if nested.
24
+ #
25
+ # If :postfix is given, it will be appended to the end of each lookup entry. So with a :postfix of
26
+ # 'stuff', '_stuff' will be appended:
27
+ #
28
+ # form.{namespace}.{model}.{attribute}_{postfix}
29
+ #
30
+ def translate(namespace, attribute, postfix: nil, default: '')
31
+ lookups = []
32
+ postfix = "_#{postfix}" if postfix
33
+ model_key = model.model_name.i18n_key
34
+
35
+ if attribute.is_a?(Array) && attribute.length > 1
36
+ joined_attrs = attribute.join('.')
37
+ lookups << :"#{model_key}/#{joined_attrs}#{postfix}"
38
+ lookups << :"#{joined_attrs}#{postfix}"
39
+ lookups << :"defaults.#{attribute.last}#{postfix}"
40
+ else
41
+ attribute = attribute.first if attribute.is_a?(Array)
42
+ lookups << :"#{model_key}.#{attribute}#{postfix}"
43
+ lookups << :"defaults.#{attribute}#{postfix}"
44
+ end
45
+
46
+ lookups << default
47
+
48
+ I18n.t(lookups.shift, scope: :"#{i18n_scope}.#{namespace}",
49
+ default: lookups).presence
50
+ end
51
+
52
+ def translate_label(attribute, default: nil, postfix: nil)
53
+ if !default
54
+ model = @model.class
55
+
56
+ if @model.class.respond_to?(:reflect_on_association) && attribute.many?
57
+ model = @model.class.reflect_on_association(attribute.first).klass
58
+ end
59
+
60
+ default = model.human_attribute_name(attribute.last)
61
+ end
62
+
63
+ translate :labels, attribute, default:, postfix:
64
+ end
65
+
66
+ private
67
+
68
+ def i18n_scope
69
+ 'form'
70
+ end
71
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::UI
4
+ # Helpers to aid in building forms and associated inputs with built-in styling, and inspired by
5
+ # Rails form helpers and SimpleForm.
6
+ #
7
+ # Start by creating the form with `Proscenium::UI::Form`, which expects a model
8
+ # instance, and a block in which you define one or more fields. It automatically includes a hidden
9
+ # authenticity token field for you.
10
+ #
11
+ # Example:
12
+ #
13
+ # render Proscenium::UI::Form.new(User.new) do |f|
14
+ # f.text_field :name
15
+ # f.radio_group :role, %i[admin manager]
16
+ # f.submit 'Save'
17
+ # end
18
+ #
19
+ # The following fields (inputs) are available:
20
+ #
21
+ # - `url_field` - <input> with 'url' type.
22
+ # - `text_field` - <input> with 'text' type.
23
+ # - `textarea_field` - <textarea>.
24
+ # - `rich_textarea_field` - A rich <textarea> using ActionText and Trix.
25
+ # - `email_field` - <input> with 'email' type.
26
+ # - `number_field` - <input> with 'number' type.
27
+ # - `color_field` - <input> with 'color' type.
28
+ # - `hidden_field` - <input> with 'hidden' type.
29
+ # - `search_field` - <input> with 'search' type.
30
+ # - `password_field` - <input> with 'password' type.
31
+ # - `tel_field` - <input> with 'tel' type.
32
+ # - `range_field` - <input> with 'range' type.
33
+ # - `time_field` - <input> with 'time' type.
34
+ # - `date_field` - <input> with 'date' type.
35
+ # - `week_field` - <input> with 'week' type.
36
+ # - `month_field` - <input> with 'month' type.
37
+ # - `datetime_local_field` - <input> with 'datetime-local' type.
38
+ # - `checkbox_field` - <input> with 'checkbox' type.
39
+ # - `radio_field` - <input> with 'radio' type.
40
+ # - `radio_group` - group of <input>'s with 'radio' type.
41
+ # - `select_field` - <select> input.
42
+ #
43
+ class Form < Component
44
+ include FieldMethods
45
+ include Translation
46
+
47
+ STANDARD_METHOD_VERBS = %w[get post].freeze
48
+
49
+ def self.input_field(method_name, type:)
50
+ define_method method_name do |*args, **attributes|
51
+ merge_bang_attributes! args, attributes
52
+ render Fields::Input.new(args, @model, self, type:, **attributes)
53
+ end
54
+ end
55
+
56
+ # @param model [#to_model] The model instance for the form.
57
+ prop :model, _Interface(:to_model), :positional, reader: :public
58
+
59
+ # @param method [Symbol, String, nil] The HTTP method for the form. Defaults to 'patch' for
60
+ # persisted models, 'post' otherwise. (:get, :post, :put, :patch, :delete)
61
+ prop :method, _Union?(:get, :post, :put, :patch, :delete,
62
+ 'get', 'post', 'put', 'patch', 'delete') do |value|
63
+ value ||= 'patch' if @model.respond_to?(:persisted?) && @model.persisted?
64
+ value&.to_s&.downcase || 'post'
65
+ end
66
+
67
+ # @param action [String, Symbol, Array, Hash, nil] The form action URL. Accepts any value
68
+ # that can be passed to Rails `url_for` helper.
69
+ prop :action, _Union?(String, Symbol, Array, Hash)
70
+
71
+ # @param attributes [Hash] Additional HTML attributes passed to the <form> element.
72
+ prop :attributes, Hash, :**
73
+
74
+ def self.source_path = super / '../form/index.rb'
75
+
76
+ # Use the given `field_class` to render a custom field. This allows you to create a custom
77
+ # form field on an as-needed basis. The `field_class` must be a subclass of
78
+ # `Proscenium::UI::Form::Fields::Base`.
79
+ #
80
+ # Example:
81
+ #
82
+ # render Proscenium::UI::Form.new @resource do |f|
83
+ # f.use_field Administrator::EmailField, :email, :required!
84
+ # end
85
+ #
86
+ # @param field_class [Class<Proscenium::UI::Form::Fields::Base>]
87
+ # @param args [Array<Symbol>] name or nested names of model attribute
88
+ # @param attributes [Hash] passed through to each input
89
+ def use_field(field_class, *args, **attributes)
90
+ merge_bang_attributes! args, attributes
91
+ render field_class.new(args, model, self, **attributes)
92
+ end
93
+
94
+ # Returns a button with type of 'submit', using the `value` given.
95
+ #
96
+ # @param value [String] Value of the `value` attribute.
97
+ def submit(value = 'Save', **)
98
+ input(name: 'commit', type: :submit, value:, **)
99
+ end
100
+
101
+ # Returns a <div> with the given `message` as its content. If `message` is not given, and
102
+ # `attribute` is, then first error message for the given model `attribute`.
103
+ #
104
+ # @param message [String] error message to display.
105
+ # @param attribute [Symbol] name of the model attribute.
106
+ def error(message: nil, attribute: nil, &content)
107
+ if message.nil? && attribute.nil? && !content
108
+ raise ArgumentError, 'One of `message:`, `attribute:` or a block is required'
109
+ end
110
+
111
+ if content
112
+ div class: :@error, &content
113
+ else
114
+ div class: :@error do
115
+ message || @model.errors[attribute]&.first
116
+ end
117
+ end
118
+ end
119
+
120
+ def view_template(&)
121
+ form action:, method:, **@attributes do
122
+ method_field
123
+ authenticity_token_field
124
+ error_for_base
125
+ yield if block_given?
126
+ end
127
+ end
128
+
129
+ def error_for_base
130
+ return if !@model.errors.key?(:base)
131
+
132
+ callout :danger do |x|
133
+ x.title { 'Unable to save...' }
134
+ div { @model.errors.full_messages_for(:base).first }
135
+ end
136
+ end
137
+
138
+ def field_name(*names, multiple: false)
139
+ # Delete the `?` suffix if present.
140
+ lname = names.pop.to_s
141
+ names.append lname.delete_suffix('?').to_sym
142
+
143
+ view_context.field_name(ActiveModel::Naming.param_key(@model.class), *names,
144
+ multiple:)
145
+ end
146
+
147
+ def field_id(*)
148
+ view_context.field_id(ActiveModel::Naming.param_key(@model.class), *)
149
+ end
150
+
151
+ def authenticity_token_field
152
+ return if method == 'get'
153
+
154
+ input(
155
+ name: 'authenticity_token',
156
+ type: 'hidden',
157
+ value: view_context.form_authenticity_token(form_options: { action:,
158
+ method: @method })
159
+ )
160
+ end
161
+
162
+ def action
163
+ view_context.url_for(@action || @model)
164
+ end
165
+
166
+ def method_field
167
+ return if STANDARD_METHOD_VERBS.include?(@method)
168
+
169
+ input type: 'hidden', name: '_method', value: @method, autocomplete: 'off'
170
+ end
171
+
172
+ def method
173
+ STANDARD_METHOD_VERBS.include?(@method) ? @method : 'post'
174
+ end
175
+
176
+ input_field :file_field, type: 'file'
177
+ input_field :url_field, type: 'url'
178
+ input_field :text_field, type: 'text'
179
+ input_field :time_field, type: 'time'
180
+ input_field :date_field, type: 'date'
181
+ input_field :number_field, type: 'number'
182
+ input_field :week_field, type: 'week'
183
+ input_field :month_field, type: 'month'
184
+ input_field :email_field, type: 'email'
185
+ input_field :color_field, type: 'color'
186
+ input_field :search_field, type: 'search'
187
+ input_field :password_field, type: 'password'
188
+ input_field :range_field, type: 'range'
189
+
190
+ private
191
+
192
+ def merge_bang_attributes!(attrs, kw_attributes, additional_bang_attrs: [])
193
+ Proscenium::Utils.merge_bang_attributes! attrs, kw_attributes,
194
+ %i[required disabled].concat(additional_bang_attrs)
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ module UI
5
+ class Railtie < ::Rails::Railtie
6
+ initializer 'proscenium-ui.reloader' do |app|
7
+ next if !Proscenium::UI::LOADER.reloading_enabled?
8
+
9
+ lib_path = File.expand_path('../..', __dir__)
10
+
11
+ checker = app.config.file_watcher.new([], { lib_path => [:rb] }) do
12
+ Proscenium::UI::LOADER.reload
13
+ end
14
+
15
+ app.reloaders << checker
16
+
17
+ app.reloader.to_run do
18
+ checker.execute_if_updated
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ import DataConfirm from "./data_confirm";
2
+ import DataDisableWith from "./data_disable_with";
3
+
4
+ export default class UJS {
5
+ constructor() {
6
+ this.dc = new DataConfirm();
7
+ this.ddw = new DataDisableWith();
8
+
9
+ document.addEventListener("submit", this, { capture: true });
10
+ }
11
+
12
+ handleEvent(event) {
13
+ this.dc.onSubmit(event) && this.ddw.onSubmit(event);
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ export default class DataConfirm {
2
+ onSubmit = (event) => {
3
+ if (
4
+ !event.target.matches("[data-turbo=true]") &&
5
+ event.submitter &&
6
+ "confirm" in event.submitter.dataset
7
+ ) {
8
+ const v = event.submitter.dataset.confirm;
9
+
10
+ if (
11
+ v !== "false" &&
12
+ !confirm(v === "true" || v === "" ? "Are you sure?" : v)
13
+ ) {
14
+ event.preventDefault();
15
+ event.stopPropagation();
16
+ event.stopImmediatePropagation();
17
+ return false;
18
+ }
19
+ }
20
+
21
+ return true;
22
+ };
23
+ }
@@ -0,0 +1,68 @@
1
+ export default class DataDisableWith {
2
+ onSubmit = event => {
3
+ const target = event.target
4
+ const formId = target.id
5
+
6
+ if (target.matches('[data-turbo=true]')) return
7
+
8
+ const submitElements = Array.from(
9
+ target.querySelectorAll(
10
+ ['input[type=submit][data-disable-with]', 'button[type=submit][data-disable-with]'].join(
11
+ ', '
12
+ )
13
+ )
14
+ )
15
+
16
+ submitElements.push(
17
+ ...Array.from(
18
+ document.querySelectorAll(
19
+ [
20
+ `input[type=submit][data-disable-with][form='${formId}']`,
21
+ `button[type=submit][data-disable-with][form='${formId}']`
22
+ ].join(', ')
23
+ )
24
+ )
25
+ )
26
+
27
+ for (const ele of submitElements) {
28
+ if (ele.hasAttribute('form') && ele.getAttribute('form') !== target.id) continue
29
+
30
+ this.#disableButton(ele)
31
+ }
32
+ }
33
+
34
+ #disableButton(ele) {
35
+ const defaultTextValue = 'Please wait...'
36
+ let textValue = ele.dataset.disableWith || defaultTextValue
37
+ if (textValue === 'false') return
38
+ if (textValue === 'true') {
39
+ textValue = defaultTextValue
40
+ }
41
+
42
+ ele.disabled = true
43
+
44
+ if (ele.matches('button')) {
45
+ ele.dataset.valueBeforeDisabled = ele.innerHTML
46
+ ele.innerHTML = textValue
47
+ } else {
48
+ ele.dataset.valueBeforeDisabled = ele.value
49
+ ele.value = textValue
50
+ }
51
+
52
+ if (ele.resetDisableWith === undefined) {
53
+ // This function can be called on the element to reset the disabled state. Useful for when
54
+ // form submission fails, and the button should be re-enabled.
55
+ ele.resetDisableWith = function () {
56
+ this.disabled = false
57
+
58
+ if (this.matches('button')) {
59
+ this.innerHTML = this.dataset.valueBeforeDisabled
60
+ } else {
61
+ this.value = this.dataset.valueBeforeDisabled
62
+ }
63
+
64
+ delete this.dataset.valueBeforeDisabled
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,10 @@
1
+ export default async () => {
2
+ window.Proscenium = window.Proscenium || {};
3
+
4
+ if (!window.Proscenium.UJS) {
5
+ const classPath =
6
+ "/node_modules/@rubygems/proscenium-ui/lib/proscenium/ui/ujs/class.js";
7
+ const module = await import(classPath);
8
+ window.Proscenium.UJS = new module.default();
9
+ }
10
+ };
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ module UI
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'proscenium/ui/railtie'
4
+ require 'proscenium/phlex'
5
+ require 'literal'
6
+
7
+ require 'zeitwerk'
8
+
9
+ module Proscenium
10
+ module UI
11
+ LOADER = Zeitwerk::Loader.for_gem_extension(Proscenium)
12
+ LOADER.inflector.inflect('ui' => 'UI', 'ujs' => 'UJS')
13
+ LOADER.enable_reloading if defined?(Rails.root) && Rails.env.development? &&
14
+ __dir__.start_with?(Rails.root.to_s)
15
+ LOADER.setup
16
+
17
+ class << self
18
+ def method_missing(name, ...)
19
+ if methods.exclude?(name) && name[0] == name[0].upcase && const_defined?(name) &&
20
+ const_get(name) < Component
21
+ define_singleton_method(name) do |*args, **kwargs, &block|
22
+ const_get(name).new(*args, **kwargs, &block)
23
+ end
24
+ public_send(name, ...)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def respond_to_missing?(name, include_private = false)
31
+ (methods.exclude?(name) && name[0] == name[0].upcase &&
32
+ const_defined?(name) && const_get(name) < Component) || super
33
+ end
34
+ end
35
+ end
36
+ end