phlex-forms 0.3.1 → 0.4.1
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/CHANGELOG.md +54 -0
- data/README.md +16 -6
- data/app/javascript/phlex_forms/controllers/validations/acceptance_controller.js +2 -3
- data/app/javascript/phlex_forms/controllers/validations/base_controller.js +12 -1
- data/app/javascript/phlex_forms/controllers/validations/confirmation_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/exclusion_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/format_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/inclusion_controller.js +1 -2
- data/app/javascript/phlex_forms/controllers/validations/length_controller.js +15 -4
- data/app/javascript/phlex_forms/controllers/validations/numericality_controller.js +10 -11
- data/app/javascript/phlex_forms/controllers/validations/presence_controller.js +1 -2
- data/app/javascript/phlex_forms/i18n.js +1 -1
- data/lib/forms/choices_select.rb +1 -1
- data/lib/forms/collection_check_box.rb +1 -1
- data/lib/forms/field_error.rb +1 -1
- data/lib/forms/field_hint.rb +1 -1
- data/lib/forms/form.rb +3 -4
- data/lib/forms/form_control.rb +2 -3
- data/lib/forms/rich_textarea.rb +1 -1
- data/lib/forms/row.rb +1 -2
- data/lib/forms/tag_field.rb +1 -1
- data/lib/forms/validations/introspector.rb +24 -1
- data/lib/forms/validations/manual_rules.rb +3 -3
- data/lib/forms/validations/message.rb +77 -0
- data/lib/phlex_forms/configuration.rb +2 -2
- data/lib/phlex_forms/delegated_field.rb +3 -4
- data/lib/phlex_forms/theme.rb +1 -8
- data/lib/phlex_forms/version.rb +1 -1
- data/lib/phlex_forms.rb +2 -9
- metadata +16 -2
- data/lib/phlex_forms/class_merge.rb +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9b781532660ae609037596969274d95acbb9eac449c60475c51a91329a9fcb5
|
|
4
|
+
data.tar.gz: e242d6ecb20a6eb2238b5a2dfb1bb86bd68ad5f2b7a783bdb9b23e2d6cc1c42c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9bbc43663fb51f9e8f414702a4aaf7349870ca23f854ce3b9b38220dc32abe42f77820ab840a78708c473d5e30bbb9f918f6ae39025d94b5e94103e9b2ae3de
|
|
7
|
+
data.tar.gz: 9435b6699cab6da09d974077c8b2ff4f6f83d4f28b0cc44d2fd56419fea1b5bca2cb7347ef86b16a6ede417fddda62ce2d4c03c1e02d9e2ae58b315272701969
|
data/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Upgrading to 0.4
|
|
11
|
+
|
|
12
|
+
phlex-forms now depends on **daisyui `~> 2.0`** and merges every default +
|
|
13
|
+
caller `class:` through `DaisyUI::ClassMerge` (a port of tailwind_merge 1.5.6
|
|
14
|
+
plus daisyUI's modifier groups). `PhlexForms::ClassMerge` is gone.
|
|
15
|
+
|
|
16
|
+
- **Bundle daisyui 2.** It's a hard dependency now, so a `gem "daisyui", "~> 1.x"`
|
|
17
|
+
pin in your Gemfile must move to 2.x (or be dropped).
|
|
18
|
+
- **Register your custom Tailwind utilities.** The old merger only knew daisy
|
|
19
|
+
sizes/colors and `w-*`, and passed everything else through. The new one
|
|
20
|
+
classifies every utility, so an unregistered custom one can be dropped as a
|
|
21
|
+
conflict. For example, a `text-h1` you pass next to `FieldError`'s
|
|
22
|
+
`text-error` reads as a colour:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# config/initializers/daisy_ui.rb
|
|
26
|
+
DaisyUI.configure do |config|
|
|
27
|
+
config.class_merge.utility("text-h1", like: "text-lg")
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **`CollectionCheckBox` merges a caller `class:` instead of replacing its
|
|
32
|
+
base class.** `class: "checkbox-primary"` now renders `checkbox
|
|
33
|
+
checkbox-primary` (it used to drop `checkbox`). A caller who used `class:` to
|
|
34
|
+
swap the base class entirely (`class: "toggle"`) now gets both classes.
|
|
35
|
+
- **More conflicts resolve now.** A caller's `mt-4` / `text-xs` replaces a
|
|
36
|
+
component's default `mt-1` / `text-sm` instead of stacking beside it.
|
|
37
|
+
- **Daisy leaves lean on daisyui's own merge.** Input, Select, Textarea and the
|
|
38
|
+
other delegated leaves hand `class:` to the daisyui component, so
|
|
39
|
+
`DaisyUI.configure { |c| c.class_merge.enabled = false }` applies to them too.
|
|
40
|
+
The components that render plain HTML (FieldHint, FieldError, Row, Form,
|
|
41
|
+
FormControl, TagField, ChoicesSelect) merge through `DaisyUI::ClassMerge.merge`
|
|
42
|
+
directly, which that switch doesn't affect.
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- **`PhlexForms::ClassMerge`**: use `DaisyUI::ClassMerge.merge`.
|
|
47
|
+
- **The daisyui soft-dependency path.** `require "phlex_forms"` loads daisyui
|
|
48
|
+
unconditionally, the daisy theme is always the default, and `Theme.daisy` no
|
|
49
|
+
longer raises `FeatureUnavailable`. The Plain theme is unchanged and still
|
|
50
|
+
emits zero daisy classes.
|
|
51
|
+
|
|
10
52
|
### Fixed
|
|
11
53
|
|
|
54
|
+
- **Client-side validators honour the validator's `message:`** (issue #41).
|
|
55
|
+
`validates :code, format: { with: /\A[a-z]*\z/, message: "lowercase only" }`
|
|
56
|
+
used to show the generic "is invalid" in the browser and the specific text
|
|
57
|
+
only after a round trip. The introspector now emits
|
|
58
|
+
`data-validations--<validator>-message-value`: a String as is (`%{count}`
|
|
59
|
+
stays for the browser to fill), a Symbol resolved on the server through
|
|
60
|
+
ActiveModel's lookup chain in the current locale, a Proc skipped (it needs a
|
|
61
|
+
record). Length's `too_long:` / `too_short:` / `wrong_length:` win over
|
|
62
|
+
`message:`, as on the server. Every shipped controller prefers the value and
|
|
63
|
+
falls back to its bundled message; the submit coordinator collects it too. A
|
|
64
|
+
`validate: { presence: { message: "…" } }` / `confirmation: { message: "…" }`
|
|
65
|
+
rules hash now keeps its options (it used to collapse them to `true`).
|
|
12
66
|
- **`rich_textarea` forwards its block into `<lexxy-editor>`**: Lexxy configures
|
|
13
67
|
an editor through CHILD elements — `<lexxy-prompt>` for @mentions,
|
|
14
68
|
`<lexxy-code-language-picker>` for code blocks — but the component rendered
|
data/README.md
CHANGED
|
@@ -42,7 +42,6 @@ end
|
|
|
42
42
|
```ruby
|
|
43
43
|
# Gemfile
|
|
44
44
|
gem "phlex-forms"
|
|
45
|
-
gem "daisyui" # optional — the daisy theme; omit for plain semantic HTML
|
|
46
45
|
gem "phlex-reactive" # optional — the `live` server-truth validation
|
|
47
46
|
```
|
|
48
47
|
|
|
@@ -254,11 +253,10 @@ f.field(:search).wrapped_input(:primary) do
|
|
|
254
253
|
end
|
|
255
254
|
```
|
|
256
255
|
|
|
257
|
-
## Themes — using phlex-forms without
|
|
256
|
+
## Themes — using phlex-forms without daisyUI styling
|
|
258
257
|
|
|
259
|
-
Every component resolves through a theme (a role → component-class map).
|
|
260
|
-
the
|
|
261
|
-
demand — the **Plain theme** renders bare semantic HTML: the same binding
|
|
258
|
+
Every component resolves through a theme (a role → component-class map). The
|
|
259
|
+
daisy theme is the default. On demand, the **Plain theme** renders bare semantic HTML: the same binding
|
|
262
260
|
(names, ids, values, required, errors), variants accepted and ignored, no
|
|
263
261
|
styling classes, and stable hooks (`aria-invalid`, `role="alert"`,
|
|
264
262
|
`data-field-error`, `data-field-hint`, `data-form-row`) for your own CSS.
|
|
@@ -347,7 +345,19 @@ the form-level `validations--form` coordinator), which `lazyLoadControllersFrom`
|
|
|
347
345
|
resolves to `phlex_forms/controllers/validations/*_controller` — the path the gem
|
|
348
346
|
ships them at.
|
|
349
347
|
|
|
350
|
-
|
|
348
|
+
A validator's own `message:` reaches the browser too, so the client shows what
|
|
349
|
+
the server would: a String as is (`%{value}` is filled in, and `%{count}` for
|
|
350
|
+
rules that have one: length and the numericality bounds), a Symbol resolved on
|
|
351
|
+
the server through your app's i18n, a Proc skipped (it needs a record).
|
|
352
|
+
Length's `too_long:` / `too_short:` / `wrong_length:` win over `message:`.
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
validates :code, format: { with: /\A[a-z]*\z/, message: "may only contain lowercase letters" }
|
|
356
|
+
f.field :note, validate: { length: { maximum: 30, too_long: "keep it under %{count}" } }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Without a `message:`, the bundled messages apply: they ship for `en` / `fr` /
|
|
360
|
+
`af`; override them page-wide via `window.PhlexForms.messages`.
|
|
351
361
|
|
|
352
362
|
## Nested attributes, collections & escape valves
|
|
353
363
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::AcceptanceValidator. For
|
|
5
4
|
// checkboxes, asserts `.checked`. For other inputs, asserts the
|
|
@@ -11,11 +10,11 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
|
|
12
11
|
check(value) {
|
|
13
12
|
if (this.element.type === "checkbox") {
|
|
14
|
-
return this.element.checked ? null :
|
|
13
|
+
return this.element.checked ? null : this.messageOr("js.forms.validations.acceptance.accepted")
|
|
15
14
|
}
|
|
16
15
|
try {
|
|
17
16
|
const list = JSON.parse(this.acceptValue)
|
|
18
|
-
return list.map(String).includes(String(value ?? "")) ? null :
|
|
17
|
+
return list.map(String).includes(String(value ?? "")) ? null : this.messageOr("js.forms.validations.acceptance.accepted")
|
|
19
18
|
} catch (_e) {
|
|
20
19
|
return null
|
|
21
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { interpolate, t } from "phlex_forms/i18n"
|
|
2
3
|
|
|
3
4
|
// Shared base for every field-level validator. Subclasses implement
|
|
4
5
|
// `check(value)` and return either `null` (valid) or a translation
|
|
@@ -8,7 +9,8 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
8
9
|
//
|
|
9
10
|
// Each subclass declares its own `static values` for the rules it
|
|
10
11
|
// reads from data attributes; the base class only knows about the
|
|
11
|
-
// `allowBlank` / `allowNil` short-circuits
|
|
12
|
+
// `allowBlank` / `allowNil` short-circuits and the `message` value,
|
|
13
|
+
// which carries the validator's own `message:` option (issue #41).
|
|
12
14
|
export class FieldValidatorController extends Controller {
|
|
13
15
|
// `error` is opt-in: callers that pre-render a `<p data-validations--<validator>-target="error">`
|
|
14
16
|
// get a stable slot the controller toggles. Inputs without an
|
|
@@ -18,6 +20,7 @@ export class FieldValidatorController extends Controller {
|
|
|
18
20
|
static values = {
|
|
19
21
|
allowBlank: { type: Boolean, default: false },
|
|
20
22
|
allowNil: { type: Boolean, default: false },
|
|
23
|
+
message: String,
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
// Element must be the input itself — these controllers are attached
|
|
@@ -64,6 +67,14 @@ export class FieldValidatorController extends Controller {
|
|
|
64
67
|
return null
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// The validator's own `message:` when the server rendered one, else the
|
|
71
|
+
// bundled translation for `key`. Either way `%{count}` & co. are filled in;
|
|
72
|
+
// a custom message may also carry `%{value}`, as on the server.
|
|
73
|
+
messageOr(key, vars = {}) {
|
|
74
|
+
if (!(this.hasMessageValue && this.messageValue)) return t(key, vars)
|
|
75
|
+
return interpolate(this.messageValue, { value: this.fieldValue(), ...vars })
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
shouldSkip(value) {
|
|
68
79
|
if (this.allowNilValue && (value === null || value === undefined)) return true
|
|
69
80
|
if (this.allowBlankValue && this.isBlank(value)) return true
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::ConfirmationValidator. Looks
|
|
5
4
|
// for the `<name>_confirmation` field by name within the same form
|
|
@@ -15,7 +14,7 @@ export default class extends FieldValidatorController {
|
|
|
15
14
|
if (!form) return null
|
|
16
15
|
const partner = form.elements[this.matchValue] || this.partnerByAttribute(form)
|
|
17
16
|
if (!partner) return null
|
|
18
|
-
return String(partner.value) === String(value ?? "") ? null :
|
|
17
|
+
return String(partner.value) === String(value ?? "") ? null : this.messageOr("js.forms.validations.confirmation.confirmation")
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
partnerByAttribute(form) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::ExclusionValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -11,7 +10,7 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
if (!this.hasInValue) return null
|
|
12
11
|
try {
|
|
13
12
|
const list = JSON.parse(this.inValue)
|
|
14
|
-
return list.map(String).includes(String(value ?? "")) ?
|
|
13
|
+
return list.map(String).includes(String(value ?? "")) ? this.messageOr("js.forms.validations.exclusion.exclusion") : null
|
|
15
14
|
} catch (_e) {
|
|
16
15
|
return null
|
|
17
16
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::FormatValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -12,7 +11,7 @@ export default class extends FieldValidatorController {
|
|
|
12
11
|
if (!this.hasPatternValue) return null
|
|
13
12
|
try {
|
|
14
13
|
const regex = new RegExp(this.patternValue, this.flagsValue)
|
|
15
|
-
return regex.test(String(value ?? "")) ? null :
|
|
14
|
+
return regex.test(String(value ?? "")) ? null : this.messageOr("js.forms.validations.format.invalid")
|
|
16
15
|
} catch (_e) {
|
|
17
16
|
// If Rails' regex doesn't translate cleanly to JS, fall back to
|
|
18
17
|
// letting the server be the authority — don't block submit on a
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::InclusionValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -11,7 +10,7 @@ export default class extends FieldValidatorController {
|
|
|
11
10
|
if (!this.hasInValue) return null
|
|
12
11
|
try {
|
|
13
12
|
const list = JSON.parse(this.inValue)
|
|
14
|
-
return list.map(String).includes(String(value ?? "")) ? null :
|
|
13
|
+
return list.map(String).includes(String(value ?? "")) ? null : this.messageOr("js.forms.validations.inclusion.inclusion")
|
|
15
14
|
} catch (_e) {
|
|
16
15
|
return null
|
|
17
16
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import {
|
|
2
|
+
import { interpolate } from "phlex_forms/i18n"
|
|
3
3
|
|
|
4
4
|
// Mirrors ActiveModel::Validations::LengthValidator. Reads:
|
|
5
5
|
// - maximum / minimum / is
|
|
6
|
+
// - too-long / too-short / wrong-length messages (win over `message`)
|
|
6
7
|
// - allow-blank / allow-nil
|
|
7
8
|
//
|
|
8
9
|
// Counter is rendered next to the input when `maximum` is set, so
|
|
@@ -20,6 +21,9 @@ export default class extends FieldValidatorController {
|
|
|
20
21
|
maximum: Number,
|
|
21
22
|
minimum: Number,
|
|
22
23
|
is: Number,
|
|
24
|
+
tooLongMessage: String,
|
|
25
|
+
tooShortMessage: String,
|
|
26
|
+
wrongLengthMessage: String,
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
connect() {
|
|
@@ -41,17 +45,24 @@ export default class extends FieldValidatorController {
|
|
|
41
45
|
const length = this.codepointLength(value)
|
|
42
46
|
|
|
43
47
|
if (this.hasIsValue && length !== this.isValue) {
|
|
44
|
-
return
|
|
48
|
+
return this.lengthMessage(this.wrongLengthMessageValue, "wrong_length", this.isValue)
|
|
45
49
|
}
|
|
46
50
|
if (this.hasMaximumValue && length > this.maximumValue) {
|
|
47
|
-
return
|
|
51
|
+
return this.lengthMessage(this.tooLongMessageValue, "too_long", this.maximumValue)
|
|
48
52
|
}
|
|
49
53
|
if (this.hasMinimumValue && length < this.minimumValue) {
|
|
50
|
-
return
|
|
54
|
+
return this.lengthMessage(this.tooShortMessageValue, "too_short", this.minimumValue)
|
|
51
55
|
}
|
|
52
56
|
return null
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// A per-case message (`too_long:` & co.) wins over `message:`, as on the
|
|
60
|
+
// server; the bundled translation is the final fallback.
|
|
61
|
+
lengthMessage(caseMessage, key, count) {
|
|
62
|
+
if (caseMessage) return interpolate(caseMessage, { value: this.fieldValue(), count })
|
|
63
|
+
return this.messageOr(`js.forms.validations.length.${key}`, { count })
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
updateCounter = () => {
|
|
56
67
|
const counter = this.counterElement()
|
|
57
68
|
if (!counter) return
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::NumericalityValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -20,40 +19,40 @@ export default class extends FieldValidatorController {
|
|
|
20
19
|
if (raw === "") return null
|
|
21
20
|
|
|
22
21
|
const number = Number(raw)
|
|
23
|
-
if (Number.isNaN(number)) return
|
|
22
|
+
if (Number.isNaN(number)) return this.messageOr("js.forms.validations.numericality.not_a_number")
|
|
24
23
|
|
|
25
24
|
if (this.onlyIntegerValue && !Number.isInteger(number)) {
|
|
26
|
-
return
|
|
25
|
+
return this.messageOr("js.forms.validations.numericality.not_an_integer")
|
|
27
26
|
}
|
|
28
27
|
if (this.hasGreaterThanValue && !(number > this.greaterThanValue)) {
|
|
29
|
-
return
|
|
28
|
+
return this.messageOr("js.forms.validations.numericality.greater_than", { count: this.greaterThanValue })
|
|
30
29
|
}
|
|
31
30
|
if (this.hasGreaterThanOrEqualToValue && !(number >= this.greaterThanOrEqualToValue)) {
|
|
32
|
-
return
|
|
31
|
+
return this.messageOr("js.forms.validations.numericality.greater_than_or_equal_to", {
|
|
33
32
|
count: this.greaterThanOrEqualToValue,
|
|
34
33
|
})
|
|
35
34
|
}
|
|
36
35
|
if (this.hasLessThanValue && !(number < this.lessThanValue)) {
|
|
37
|
-
return
|
|
36
|
+
return this.messageOr("js.forms.validations.numericality.less_than", { count: this.lessThanValue })
|
|
38
37
|
}
|
|
39
38
|
if (this.hasLessThanOrEqualToValue && !(number <= this.lessThanOrEqualToValue)) {
|
|
40
|
-
return
|
|
39
|
+
return this.messageOr("js.forms.validations.numericality.less_than_or_equal_to", {
|
|
41
40
|
count: this.lessThanOrEqualToValue,
|
|
42
41
|
})
|
|
43
42
|
}
|
|
44
43
|
if (this.hasEqualToValue && number !== this.equalToValue) {
|
|
45
|
-
return
|
|
44
|
+
return this.messageOr("js.forms.validations.numericality.equal_to", { count: this.equalToValue })
|
|
46
45
|
}
|
|
47
46
|
if (this.hasOtherThanValue && number === this.otherThanValue) {
|
|
48
|
-
return
|
|
47
|
+
return this.messageOr("js.forms.validations.numericality.other_than", { count: this.otherThanValue })
|
|
49
48
|
}
|
|
50
49
|
// Rails' NumericalityValidator coerces with `.to_i` before
|
|
51
50
|
// checking parity, so `2.5` is treated as `2` (even). JS `%` on
|
|
52
51
|
// a float diverges from that — truncate first to keep client and
|
|
53
52
|
// server in agreement.
|
|
54
53
|
const truncated = Math.trunc(number)
|
|
55
|
-
if (this.oddValue && truncated % 2 === 0) return
|
|
56
|
-
if (this.evenValue && truncated % 2 !== 0) return
|
|
54
|
+
if (this.oddValue && truncated % 2 === 0) return this.messageOr("js.forms.validations.numericality.odd")
|
|
55
|
+
if (this.evenValue && truncated % 2 !== 0) return this.messageOr("js.forms.validations.numericality.even")
|
|
57
56
|
return null
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldValidatorController } from "phlex_forms/controllers/validations/base_controller"
|
|
2
|
-
import { t } from "phlex_forms/i18n"
|
|
3
2
|
|
|
4
3
|
// Mirrors ActiveModel::Validations::PresenceValidator.
|
|
5
4
|
export default class extends FieldValidatorController {
|
|
@@ -9,6 +8,6 @@ export default class extends FieldValidatorController {
|
|
|
9
8
|
|
|
10
9
|
check(value) {
|
|
11
10
|
if (!this.requiredValue) return null
|
|
12
|
-
return this.isBlank(value) ?
|
|
11
|
+
return this.isBlank(value) ? this.messageOr("js.forms.validations.presence.blank") : null
|
|
13
12
|
}
|
|
14
13
|
}
|
|
@@ -26,7 +26,7 @@ function lookup(scope, locale) {
|
|
|
26
26
|
return scope.split(".").reduce((node, key) => (node == null ? undefined : node[key]), table)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function interpolate(string, vars) {
|
|
29
|
+
export function interpolate(string, vars = {}) {
|
|
30
30
|
return string.replace(/%\{(\w+)\}/g, (_, key) => (key in vars ? String(vars[key]) : `%{${key}}`))
|
|
31
31
|
}
|
|
32
32
|
|
data/lib/forms/choices_select.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Forms
|
|
|
44
44
|
a = {
|
|
45
45
|
name: select_name,
|
|
46
46
|
id: @id,
|
|
47
|
-
class:
|
|
47
|
+
class: DaisyUI::ClassMerge.merge("choices-select w-full", @options[:class]),
|
|
48
48
|
data: stimulus_data,
|
|
49
49
|
**@options.except(:class, :value, :data)
|
|
50
50
|
}
|
data/lib/forms/field_error.rb
CHANGED
data/lib/forms/field_hint.rb
CHANGED
data/lib/forms/form.rb
CHANGED
|
@@ -224,10 +224,9 @@ module Forms
|
|
|
224
224
|
end
|
|
225
225
|
|
|
226
226
|
def form_classes
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
PhlexForms::ClassMerge.merge(classes.join(" "), @options[:class]).presence
|
|
227
|
+
spaced = "space-y-4" if @base_modifiers.include?(:spaced)
|
|
228
|
+
spacious = "space-y-6" if @base_modifiers.include?(:spacious)
|
|
229
|
+
DaisyUI::ClassMerge.merge(spaced, spacious, @options[:class]).presence
|
|
231
230
|
end
|
|
232
231
|
|
|
233
232
|
def authenticity_token_field
|
data/lib/forms/form_control.rb
CHANGED
|
@@ -40,9 +40,8 @@ module Forms
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
42
|
def control_classes
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
PhlexForms::ClassMerge.merge(base.join(" "), @options[:class])
|
|
43
|
+
horizontal = "flex flex-row items-center gap-4" if @modifiers.include?(:horizontal)
|
|
44
|
+
DaisyUI::ClassMerge.merge("form-control w-full", horizontal, @options[:class])
|
|
46
45
|
end
|
|
47
46
|
end
|
|
48
47
|
end
|
data/lib/forms/rich_textarea.rb
CHANGED
data/lib/forms/row.rb
CHANGED
|
@@ -27,8 +27,7 @@ module Forms
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def row_classes
|
|
30
|
-
|
|
31
|
-
PhlexForms::ClassMerge.merge(base, @options[:class])
|
|
30
|
+
DaisyUI::ClassMerge.merge("grid grid-cols-1 gap-4", COLUMN_CLASSES[@columns], @options[:class])
|
|
32
31
|
end
|
|
33
32
|
end
|
|
34
33
|
end
|
data/lib/forms/tag_field.rb
CHANGED
|
@@ -135,6 +135,6 @@ module Forms
|
|
|
135
135
|
def option_classes = nil
|
|
136
136
|
def chip_classes = "badge badge-primary gap-1"
|
|
137
137
|
def remove_classes = "cursor-pointer"
|
|
138
|
-
def input_classes =
|
|
138
|
+
def input_classes = DaisyUI::ClassMerge.merge("input w-full", @attributes[:class])
|
|
139
139
|
end
|
|
140
140
|
end
|
|
@@ -46,6 +46,11 @@ module Forms
|
|
|
46
46
|
only_integer odd even
|
|
47
47
|
].freeze
|
|
48
48
|
|
|
49
|
+
# LengthValidator's per-case messages; each wins over `message:` on the
|
|
50
|
+
# server, so the length controller checks them first. Each maps to the
|
|
51
|
+
# bound it reports, so a Symbol resolves with that count.
|
|
52
|
+
LENGTH_MESSAGE_KEYS = { too_long: :maximum, too_short: :minimum, wrong_length: :is }.freeze
|
|
53
|
+
|
|
49
54
|
# Convenience: returns a null introspector for nil models so
|
|
50
55
|
# callers don't need to guard.
|
|
51
56
|
def self.for(model_or_class)
|
|
@@ -83,7 +88,8 @@ module Forms
|
|
|
83
88
|
next unless suffix
|
|
84
89
|
|
|
85
90
|
controllers << "#{CONTROLLER_PREFIX}--#{suffix}"
|
|
86
|
-
value_attributes(suffix, validator, attribute).
|
|
91
|
+
value_attributes(suffix, validator, attribute).merge(message_attrs(suffix, validator, attribute))
|
|
92
|
+
.each do |key, value|
|
|
87
93
|
attrs[data_key(suffix, key)] = value
|
|
88
94
|
end
|
|
89
95
|
end
|
|
@@ -131,6 +137,23 @@ module Forms
|
|
|
131
137
|
end
|
|
132
138
|
end
|
|
133
139
|
|
|
140
|
+
# `message` (and length's `too_long_message` & co.) values, for the
|
|
141
|
+
# validator's own `message:` options. Omitted when there is none, or when
|
|
142
|
+
# it can't be resolved without a record, so the generic message stays.
|
|
143
|
+
def message_attrs(suffix, validator, attribute)
|
|
144
|
+
counts = suffix == "length" ? length_message_counts(validator) : {}
|
|
145
|
+
options = counts.keys + [:message]
|
|
146
|
+
options.each_with_object({}) do |option, attrs|
|
|
147
|
+
text = Message.resolve(validator.options[option], model_class:, attribute:, count: counts[option])
|
|
148
|
+
attrs[option == :message ? :message : :"#{option}_message"] = text if text
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def length_message_counts(validator)
|
|
153
|
+
bounds = length_attrs(validator)
|
|
154
|
+
LENGTH_MESSAGE_KEYS.to_h { |option, bound| [option, bounds[bound]&.to_i] }
|
|
155
|
+
end
|
|
156
|
+
|
|
134
157
|
def presence_attrs
|
|
135
158
|
{ required: "true" }
|
|
136
159
|
end
|
|
@@ -13,7 +13,7 @@ module Forms
|
|
|
13
13
|
# ManualRules.new(
|
|
14
14
|
# length: { maximum: 60, minimum: 3 },
|
|
15
15
|
# presence: true,
|
|
16
|
-
# format: { with: /\A[a-z]+\z/i },
|
|
16
|
+
# format: { with: /\A[a-z]+\z/i, message: "letters only" },
|
|
17
17
|
# numericality: { greater_than_or_equal_to: 0 },
|
|
18
18
|
# inclusion: { in: %w[a b c] },
|
|
19
19
|
# exclusion: { in: %w[admin] },
|
|
@@ -49,7 +49,7 @@ module Forms
|
|
|
49
49
|
captured_rules.each do |key, opts|
|
|
50
50
|
case key
|
|
51
51
|
when :presence
|
|
52
|
-
validates :value, presence: true if opts
|
|
52
|
+
validates :value, presence: opts.is_a?(Hash) ? opts : true if opts
|
|
53
53
|
when :length
|
|
54
54
|
validates :value, length: opts
|
|
55
55
|
when :format
|
|
@@ -61,7 +61,7 @@ module Forms
|
|
|
61
61
|
when :exclusion
|
|
62
62
|
validates :value, exclusion: opts
|
|
63
63
|
when :confirmation
|
|
64
|
-
validates :value, confirmation: true if opts
|
|
64
|
+
validates :value, confirmation: opts.is_a?(Hash) ? opts : true if opts
|
|
65
65
|
when :acceptance
|
|
66
66
|
validates :value, acceptance: opts.is_a?(Hash) ? opts : { accept: %w[1 true] }
|
|
67
67
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Forms
|
|
4
|
+
module Validations
|
|
5
|
+
# Turns a validator's `message:` option into the text the browser should
|
|
6
|
+
# show, so the client mirror says what the server would (issue #41).
|
|
7
|
+
#
|
|
8
|
+
# - a String is emitted as is; `%{count}`-style placeholders stay in place
|
|
9
|
+
# for the Stimulus controller to fill, as it does for its own messages.
|
|
10
|
+
# A blank String resolves to nil: the browser reads "" as valid.
|
|
11
|
+
# - a Symbol is resolved on the server through ActiveModel's lookup chain
|
|
12
|
+
# (`<scope>.errors.models.<model>.attributes.<attr>.<key>` ... then
|
|
13
|
+
# `errors.messages.<key>`), in the current locale. With a known `count:`
|
|
14
|
+
# (length's per-case messages) it is passed to I18n, so a pluralised
|
|
15
|
+
# translation picks the right form; otherwise `%{count}` stays a
|
|
16
|
+
# placeholder and the plural `:other` form is used.
|
|
17
|
+
# - anything else (a Proc needs a record) resolves to nil, so the
|
|
18
|
+
# controller keeps its generic message.
|
|
19
|
+
#
|
|
20
|
+
# Mirrors `ActiveModel::Error.generate_message` against the class alone: the
|
|
21
|
+
# introspector never instantiates the model.
|
|
22
|
+
class Message
|
|
23
|
+
# Interpolations the browser fills in, kept as literal placeholders.
|
|
24
|
+
PLACEHOLDERS = { count: "%{count}", value: "%{value}" }.freeze
|
|
25
|
+
|
|
26
|
+
def self.resolve(message, model_class:, attribute:, count: nil)
|
|
27
|
+
case message
|
|
28
|
+
when String then message unless message.empty?
|
|
29
|
+
when Symbol then new(model_class, attribute).translate(message, count:)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(model_class, attribute)
|
|
34
|
+
@model_class = model_class
|
|
35
|
+
@attribute = attribute.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def translate(key, count: nil)
|
|
39
|
+
return unless defined?(I18n)
|
|
40
|
+
|
|
41
|
+
first, *rest = lookup_keys(key)
|
|
42
|
+
vars = interpolations
|
|
43
|
+
vars[:count] = count unless count.nil?
|
|
44
|
+
I18n.translate(first, **vars, default: rest, raise: true)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :model_class, :attribute
|
|
52
|
+
|
|
53
|
+
def lookup_keys(key)
|
|
54
|
+
model_keys(key) + [:"errors.attributes.#{attribute}.#{key}", :"errors.messages.#{key}"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def model_keys(key)
|
|
58
|
+
return [] unless model_class.respond_to?(:i18n_scope) && model_class.respond_to?(:lookup_ancestors)
|
|
59
|
+
|
|
60
|
+
scope = model_class.i18n_scope
|
|
61
|
+
keys = model_class.lookup_ancestors.flat_map do |klass|
|
|
62
|
+
i18n_key = klass.model_name.i18n_key
|
|
63
|
+
[:"#{scope}.errors.models.#{i18n_key}.attributes.#{attribute}.#{key}",
|
|
64
|
+
:"#{scope}.errors.models.#{i18n_key}.#{key}"]
|
|
65
|
+
end
|
|
66
|
+
keys << :"#{scope}.errors.messages.#{key}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def interpolations
|
|
70
|
+
names = {}
|
|
71
|
+
names[:model] = model_class.model_name.human if model_class.respond_to?(:model_name)
|
|
72
|
+
names[:attribute] = model_class.human_attribute_name(attribute) if model_class.respond_to?(:human_attribute_name)
|
|
73
|
+
PLACEHOLDERS.merge(names)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -51,11 +51,11 @@ module PhlexForms
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# The default theme: :daisy, :plain, or a PhlexForms::Theme instance.
|
|
54
|
-
# Defaults to daisy
|
|
54
|
+
# Defaults to daisy.
|
|
55
55
|
attr_writer :theme
|
|
56
56
|
|
|
57
57
|
def theme
|
|
58
|
-
@theme ||=
|
|
58
|
+
@theme ||= Theme.daisy
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def infer_from_model
|
|
@@ -42,13 +42,12 @@ module PhlexForms
|
|
|
42
42
|
attrs.compact
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# source order — that's how admin filter selects went full-width, zazu#2934).
|
|
45
|
+
# Handed to the daisyui component as parts: its class merge lets a caller
|
|
46
|
+
# width (`w-36`) REPLACE the default instead of stacking with it (zazu#2934).
|
|
48
47
|
def width_class
|
|
49
48
|
return @attributes[:class] unless @full_width
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
["w-full", *@attributes[:class]]
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
# Unstyled variant of binding_attributes for the Plain theme: caller classes
|
data/lib/phlex_forms/theme.rb
CHANGED
|
@@ -9,8 +9,7 @@ module PhlexForms
|
|
|
9
9
|
# visual variant nor an error state.
|
|
10
10
|
#
|
|
11
11
|
# Built-ins:
|
|
12
|
-
# Theme.daisy — the DaisyUI-delegating Forms::* leaves (default
|
|
13
|
-
# daisyui gem is loaded)
|
|
12
|
+
# Theme.daisy — the DaisyUI-delegating Forms::* leaves (the default)
|
|
14
13
|
# Theme.plain — bare semantic HTML (Forms::Plain::*): variants accepted and
|
|
15
14
|
# ignored, zero styling classes, aria/data hooks only
|
|
16
15
|
#
|
|
@@ -47,12 +46,6 @@ module PhlexForms
|
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def daisy
|
|
50
|
-
unless defined?(DaisyUI)
|
|
51
|
-
raise PhlexForms::FeatureUnavailable,
|
|
52
|
-
"the daisy theme requires the daisyui gem. Add `gem \"daisyui\"` to your " \
|
|
53
|
-
"Gemfile, or use the plain theme."
|
|
54
|
-
end
|
|
55
|
-
|
|
56
49
|
@daisy ||= new({
|
|
57
50
|
input: Forms::Input, select: Forms::Select, choices_select: Forms::ChoicesSelect,
|
|
58
51
|
textarea: Forms::Textarea, rich_textarea: Forms::RichTextarea,
|
data/lib/phlex_forms/version.rb
CHANGED
data/lib/phlex_forms.rb
CHANGED
|
@@ -7,14 +7,7 @@ require "active_support/core_ext/string" # underscore/pluralize/humanize (scope/
|
|
|
7
7
|
require "phlex"
|
|
8
8
|
require "glyphs"
|
|
9
9
|
require "zeitwerk"
|
|
10
|
-
|
|
11
|
-
begin
|
|
12
|
-
require "daisy_ui"
|
|
13
|
-
rescue LoadError
|
|
14
|
-
# daisyui is a soft dependency: without it the Plain theme is the default.
|
|
15
|
-
# The daisy leaf components only reference DaisyUI inside view_template, so
|
|
16
|
-
# they load fine — they just must not be rendered (Theme.daisy raises).
|
|
17
|
-
end
|
|
10
|
+
require "daisy_ui"
|
|
18
11
|
|
|
19
12
|
begin
|
|
20
13
|
require "phlex/reactive"
|
|
@@ -75,7 +68,7 @@ loader.inflector.inflect(
|
|
|
75
68
|
)
|
|
76
69
|
|
|
77
70
|
# Two sibling (non-nested) autoload roots:
|
|
78
|
-
# lib/phlex_forms -> PhlexForms:: (gem internals: config,
|
|
71
|
+
# lib/phlex_forms -> PhlexForms:: (gem internals: config, inference, theme, ...)
|
|
79
72
|
# lib/forms -> Forms:: (the component kit)
|
|
80
73
|
# Keeping them as separate top-level dirs avoids the ambiguity of nesting one
|
|
81
74
|
# namespace root inside another.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phlex-forms
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mikael Henriksson
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: daisyui
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: glyphs
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -164,11 +178,11 @@ files:
|
|
|
164
178
|
- lib/forms/toggle.rb
|
|
165
179
|
- lib/forms/validations/introspector.rb
|
|
166
180
|
- lib/forms/validations/manual_rules.rb
|
|
181
|
+
- lib/forms/validations/message.rb
|
|
167
182
|
- lib/forms/wrapped_input.rb
|
|
168
183
|
- lib/phlex-forms.rb
|
|
169
184
|
- lib/phlex_forms.rb
|
|
170
185
|
- lib/phlex_forms/builder.rb
|
|
171
|
-
- lib/phlex_forms/class_merge.rb
|
|
172
186
|
- lib/phlex_forms/configuration.rb
|
|
173
187
|
- lib/phlex_forms/delegated_field.rb
|
|
174
188
|
- lib/phlex_forms/engine.rb
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module PhlexForms
|
|
4
|
-
# Class-string merging for phlex-forms components.
|
|
5
|
-
#
|
|
6
|
-
# DaisyUI component-class families are mutually exclusive: an element can't be
|
|
7
|
-
# both `input-sm` and `input-lg`, or both `input-primary` and `input-error`.
|
|
8
|
-
# When a component's default classes and a caller's `class:` override both name
|
|
9
|
-
# the same family, the LAST occurrence wins — matching Tailwind/daisyui
|
|
10
|
-
# intuition (the later class overrides the earlier).
|
|
11
|
-
#
|
|
12
|
-
# Width utilities (`w-*`) are the one core-Tailwind family this gem injects a
|
|
13
|
-
# default for (DelegatedField's `w-full`), so they conflict the same way: a
|
|
14
|
-
# caller's `w-36` must REPLACE the default, or both land on the element and
|
|
15
|
-
# stylesheet source order — not author intent — decides which applies.
|
|
16
|
-
#
|
|
17
|
-
# We deliberately do NOT depend on tailwind_merge: daisyui modifier families
|
|
18
|
-
# and the width family are the only conflicts that occur on form fields, so
|
|
19
|
-
# tailwind_merge would add a runtime dependency for conflicts this handles.
|
|
20
|
-
# Other non-conflicting utilities (`py-0`, `min-w-32`, ...) pass through in
|
|
21
|
-
# order.
|
|
22
|
-
module ClassMerge
|
|
23
|
-
SIZE = /-(xs|sm|md|lg|xl)\z/
|
|
24
|
-
COLOR = /-(primary|secondary|accent|neutral|info|success|warning|error)\z/
|
|
25
|
-
# Anchored so min-w-*/max-w-* (different CSS properties) stay out of the family.
|
|
26
|
-
WIDTH = /\Aw-/
|
|
27
|
-
|
|
28
|
-
module_function
|
|
29
|
-
|
|
30
|
-
# Merge any number of class strings/nil into one deduped string. Preserves
|
|
31
|
-
# order; within a daisyui size/color family only the last token survives.
|
|
32
|
-
def merge(*parts)
|
|
33
|
-
tokens = parts.flat_map { |p| p.to_s.split }.reject(&:empty?)
|
|
34
|
-
dedupe_daisyui_families(tokens).join(" ")
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# For each (component-prefix, family) pair, keep only the last token.
|
|
38
|
-
def dedupe_daisyui_families(tokens)
|
|
39
|
-
last_index = {}
|
|
40
|
-
tokens.each_with_index do |tok, idx|
|
|
41
|
-
key = family_key(tok)
|
|
42
|
-
last_index[key] = idx if key
|
|
43
|
-
end
|
|
44
|
-
tokens.each_with_index.filter_map do |tok, idx|
|
|
45
|
-
key = family_key(tok)
|
|
46
|
-
next tok if key.nil? # not a daisyui family token — keep
|
|
47
|
-
next tok if last_index[key] == idx # last in its family — keep
|
|
48
|
-
|
|
49
|
-
nil # earlier duplicate in the family — drop
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# A stable bucket key like "width" / "input:size" / "select:color", or nil if
|
|
54
|
-
# the token is not a recognized conflicting family. WIDTH is checked first so
|
|
55
|
-
# a `w-*` token can never be mis-bucketed as a daisyui modifier.
|
|
56
|
-
def family_key(token)
|
|
57
|
-
return "width" if WIDTH.match?(token)
|
|
58
|
-
|
|
59
|
-
if (m = token.match(SIZE))
|
|
60
|
-
"#{token[0...m.begin(0)]}:size"
|
|
61
|
-
elsif (m = token.match(COLOR))
|
|
62
|
-
"#{token[0...m.begin(0)]}:color"
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|