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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/lib/proscenium/ui/badge/index.css +75 -0
- data/lib/proscenium/ui/badge.rb +32 -0
- data/lib/proscenium/ui/breadcrumbs/computed_element.rb +71 -0
- data/lib/proscenium/ui/breadcrumbs/control.rb +103 -0
- data/lib/proscenium/ui/breadcrumbs/element.rb +16 -0
- data/lib/proscenium/ui/breadcrumbs/index.css +84 -0
- data/lib/proscenium/ui/breadcrumbs.rb +136 -0
- data/lib/proscenium/ui/combobox/index.css +162 -0
- data/lib/proscenium/ui/combobox/index.js +420 -0
- data/lib/proscenium/ui/combobox.rb +186 -0
- data/lib/proscenium/ui/component.rb +22 -0
- data/lib/proscenium/ui/flash/index.css +1 -0
- data/lib/proscenium/ui/flash/index.js +77 -0
- data/lib/proscenium/ui/flash.rb +15 -0
- data/lib/proscenium/ui/form/field_methods.rb +95 -0
- data/lib/proscenium/ui/form/fields/base.rb +189 -0
- data/lib/proscenium/ui/form/fields/checkbox/index.jsx +48 -0
- data/lib/proscenium/ui/form/fields/checkbox/index.module.css +9 -0
- data/lib/proscenium/ui/form/fields/checkbox/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/checkbox.rb +32 -0
- data/lib/proscenium/ui/form/fields/combobox.rb +117 -0
- data/lib/proscenium/ui/form/fields/date.module.css +27 -0
- data/lib/proscenium/ui/form/fields/datetime.rb +15 -0
- data/lib/proscenium/ui/form/fields/hidden.rb +9 -0
- data/lib/proscenium/ui/form/fields/input/index.jsx +71 -0
- data/lib/proscenium/ui/form/fields/input/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/input/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/input.rb +14 -0
- data/lib/proscenium/ui/form/fields/radio_group.rb +175 -0
- data/lib/proscenium/ui/form/fields/radio_input/index.jsx +44 -0
- data/lib/proscenium/ui/form/fields/radio_input/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/radio_input/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/radio_input.rb +17 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.css +23 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.js +6 -0
- data/lib/proscenium/ui/form/fields/rich_textarea.rb +18 -0
- data/lib/proscenium/ui/form/fields/select.jsx +47 -0
- data/lib/proscenium/ui/form/fields/select.module.css +46 -0
- data/lib/proscenium/ui/form/fields/select.rb +302 -0
- data/lib/proscenium/ui/form/fields/tel.css +297 -0
- data/lib/proscenium/ui/form/fields/tel.js +83 -0
- data/lib/proscenium/ui/form/fields/tel.rb +54 -0
- data/lib/proscenium/ui/form/fields/textarea/index.jsx +50 -0
- data/lib/proscenium/ui/form/fields/textarea/index.module.css +13 -0
- data/lib/proscenium/ui/form/fields/textarea/previews/basic.jsx +8 -0
- data/lib/proscenium/ui/form/fields/textarea.rb +18 -0
- data/lib/proscenium/ui/form/index.css +52 -0
- data/lib/proscenium/ui/form/translation.rb +71 -0
- data/lib/proscenium/ui/form.rb +197 -0
- data/lib/proscenium/ui/railtie.rb +23 -0
- data/lib/proscenium/ui/ujs/class.js +15 -0
- data/lib/proscenium/ui/ujs/data_confirm.js +23 -0
- data/lib/proscenium/ui/ujs/data_disable_with.js +68 -0
- data/lib/proscenium/ui/ujs/index.js +10 -0
- data/lib/proscenium/ui/version.rb +7 -0
- data/lib/proscenium/ui.rb +36 -0
- 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,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,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
|