outro_rails 0.1.4 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b57460d8171563b6c440d80d129e02e0426b1ac3fcd6886dc40994a525e39abb
4
- data.tar.gz: 1c749d767f9c6b0680509069176ec0d454ea4dd2a8bfb0476e15f704a647e10b
3
+ metadata.gz: 873a2a004ee523b79f100a4aa815e4986dbede341e9ca65740faef497821d8ad
4
+ data.tar.gz: a053185e62246437b3fd50381ffbacac6afc60ac3e498bcdb06a2a314683b9cd
5
5
  SHA512:
6
- metadata.gz: 178b499ca5c588f4ad584a540ac3f2bd43188b6bdbaba8886db6c34d213d0c1210bc7c137caf90d5327cdd56fb94e8dbae6ee99a4b925000c10a7297c4b1a7a4
7
- data.tar.gz: 315e08ed8694798be6d319b8f55fcc5254853e5d7f254db938945b1aaa938f75cfe5f8e3ab4354d01741c6ed2ba846d2d9e8f2fbce0500d62a49335447ae394b
6
+ metadata.gz: 0d52f9319f9948d5b0912341bf59bc73ca165ad7d618cd6c9eeb3cdb2068bf8e3a949512d4d66e336fea4ec0da2725a02439537e76fb974ab3fd3ee5ed3f4788
7
+ data.tar.gz: 60b284f1df23ebc1233f3c1a083e5b3ff263e3af45a4750dfea2d9b2ea112ae1ddb1cb9f402ba1e5a7b0be29c568db7624247a25c36b8c59bf20d71e8b4bf1ff
data/README.md CHANGED
@@ -200,9 +200,38 @@ This gives the model `chord_sheet`/`chord_sheet=`/`chord_sheet?`, backed by a
200
200
  (title), `tuning`, `capo`, `key`, and `body`). A model can attach more
201
201
  than one chart under different names.
202
202
 
203
+ #### Required or Optional
204
+
205
+ By default the chart is required: `song.chord_sheet` always returns a chart
206
+ (building an unsaved one on first access), and saving the record with the
207
+ artist or song blank fails validation with errors.
208
+
209
+ Pass `required: false` to let the chart be left out entirely:
210
+
211
+ ```ruby
212
+ class Song < ApplicationRecord
213
+ has_chord_chart :chord_sheet, required: false
214
+ end
215
+ ```
216
+
217
+ With an optional chart:
218
+
219
+ * `song.chord_sheet` returns `nil` until someone fills it in, and
220
+ `song.chord_sheet?` reports whether one exists.
221
+ * A form submitted with the artist, song, and body all blank saves the
222
+ record with no chart. Filling in *some* of those fields still
223
+ validates as usual, so the user sees what's missing.
224
+ * Clearing every field on a record that already had a chart removes that
225
+ chart when the record saves. If the record itself fails validation,
226
+ nothing is deleted.
227
+ * `song.chord_sheet = nil` removes a chart; `song.chord_sheet = { ... }`
228
+ builds one if needed and assigns the given attributes.
229
+
230
+ `song.chord_sheet_required?` reports which way the model was declared.
231
+
203
232
  ### Forms: chord_chart_area
204
233
 
205
- In a form, pair it with the `chord_chart_area` form builder method:
234
+ In a form, use the `chord_chart_area` form builder method:
206
235
 
207
236
  ```erb
208
237
  <%= form_with model: @song do |form| %>
@@ -211,10 +240,23 @@ In a form, pair it with the `chord_chart_area` form builder method:
211
240
  <% end %>
212
241
  ```
213
242
 
214
- `chord_chart_area` renders artist/song/tuning/capo/key/body fields. Tuning
215
- options are pulled from `Instruments::Guitar::Tunings`, capo options are capped
216
- at fret 7 (`Instruments::Guitar::Capo::FRETS`), and key options are set from
217
- from `Theory::Key`.
243
+ `chord_chart_area` renders a fieldset with a heading and
244
+ artist/song/tuning/capo/key/body fields. Tuning options are pulled from
245
+ `Instruments::Guitar::Tunings`, capo options are capped at fret 7
246
+ (`Instruments::Guitar::Capo::FRETS`). Key options are set from
247
+ `Theory::Key`.
248
+
249
+ The heading defaults to "Chord Chart". Set `legend:` to change it, or pass
250
+ `false` when the surrounding page already has a heading for this section:
251
+
252
+ ```erb
253
+ <%= form.chord_chart_area :chord_sheet, legend: "Lead sheet" %>
254
+ <%= form.chord_chart_area :chord_sheet, legend: false %>
255
+ ```
256
+
257
+ When the model declares the chart with `required: false`, the fieldset is
258
+ labeled "Optional" and tells the user to leave the fields blank if there's
259
+ no chord chart.
218
260
 
219
261
  ### Controller: Permitting the Parameters
220
262
 
@@ -233,15 +275,13 @@ class SongsController < ApplicationController
233
275
  def song_params
234
276
  params.require(:song).permit(
235
277
  :description,
236
- chord_chart_chord_sheet_attributes: %i[artist body capo key song tuning]
278
+ chord_chart_chord_sheet_attributes: %i[id artist body capo key song tuning]
237
279
  )
238
280
  end
239
281
  end
240
282
  ```
241
283
 
242
- The six permitted attributes are exactly the fields `chord_chart_area`
243
- renders. `record` and `name` are managed by the engine and should not be
244
- permitted. If a model attaches more than one chart, each one needs its own
284
+ If a model attaches more than one chart, each one needs its own
245
285
  key: `has_chord_chart :lead_sheet` is permitted as
246
286
  `chord_chart_lead_sheet_attributes`.
247
287
 
@@ -264,6 +304,11 @@ This renders the chart's metadata, an instrument toggle, a transpose-to-key
264
304
  picker, the chart body with recognized chords highlighted, and a
265
305
  diagram for every chord the body references.
266
306
 
307
+ The partial accepts `chord_chart: nil`, so it can be handed an optional
308
+ chart that hasn't been filled in yet; it renders a short "No chord chart."
309
+ placeholder in that case. Check `@song.chord_sheet?` instead if you'd
310
+ rather show nothing at all.
311
+
267
312
  ### Using Your Own Layout
268
313
 
269
314
  By default the engine renders its pages in its own layout,
@@ -1259,6 +1259,12 @@
1259
1259
  border: 0;
1260
1260
  }
1261
1261
 
1262
+ .outro-chord-chart-fields__hint {
1263
+ margin: 0;
1264
+ font-size: 0.9rem;
1265
+ color: var(--outro-muted);
1266
+ }
1267
+
1262
1268
  .outro-inline-form {
1263
1269
  display: inline;
1264
1270
  }
@@ -24,13 +24,19 @@ module OutroRails
24
24
  Instruments::ALL.map { |instrument| [ instrument.titleize, instrument ] }
25
25
  end
26
26
 
27
- def chord_chart_area(form, method, **options)
28
- chord_chart = form.object.public_send(method)
27
+ def chord_chart_area(form, method, legend: "Chord chart", **options)
28
+ record = form.object
29
+ required = record.public_send(:"#{method}_required?")
30
+
31
+ chord_chart = record.public_send(method) ||
32
+ OutroRails::ChordChart.new(name: method.to_s)
29
33
 
30
34
  form.fields_for(:"chord_chart_#{method}",
31
35
  chord_chart, **options) do |chord_chart_form|
32
36
  render partial: "outro_rails/chord_charts/fields",
33
- locals: { f: chord_chart_form }
37
+ locals: { f: chord_chart_form,
38
+ required: required,
39
+ legend: legend }
34
40
  end
35
41
  end
36
42
 
@@ -9,6 +9,8 @@ module OutroRails
9
9
  Theory::Key.canonical(:minor).map(&:to_s)
10
10
  ).freeze
11
11
 
12
+ CONTENT_ATTRIBUTES = %w[artist song body].freeze
13
+
12
14
  belongs_to :record, polymorphic: true, touch: true
13
15
 
14
16
  has_many :voicings,
@@ -22,7 +24,6 @@ module OutroRails
22
24
  validates :name, presence: true
23
25
  validates :artist, presence: true
24
26
  validates :song, presence: true
25
- validates :body, presence: true
26
27
  validates :tuning,
27
28
  inclusion: { in: Instruments::Guitar::Tunings.slugs },
28
29
  allow_blank: true
@@ -31,6 +32,15 @@ module OutroRails
31
32
  allow_nil: true
32
33
  validates :key, inclusion: { in: KEY_OPTIONS }, allow_blank: true
33
34
 
35
+ def self.blank_attributes?(attributes)
36
+ return true if attributes.nil?
37
+
38
+ attributes = attributes.to_h if attributes.respond_to?(:permitted?)
39
+ attributes = attributes.to_h.with_indifferent_access
40
+
41
+ CONTENT_ATTRIBUTES.all? { |field| attributes[field].blank? }
42
+ end
43
+
34
44
  def to_key
35
45
  Theory::Key.parse(key.presence || "C")
36
46
  end
@@ -8,7 +8,10 @@
8
8
  render "outro_rails/chord_charts/chart", chord_chart: @song.chord_sheet
9
9
 
10
10
  Locals:
11
- chord_chart - required, an OutroRails::ChordChart
11
+ chord_chart - required, an OutroRails::ChordChart. Pass nil (what an
12
+ optional `has_chord_chart ..., required: false` reader
13
+ returns before anyone fills it in) to render a short
14
+ "no chord chart" placeholder instead.
12
15
  instrument - "guitar" or "piano", default "guitar"
13
16
  to_key - preview the chart transposed into this key, default none
14
17
  editable - let the viewer change which voicing shows for each
@@ -19,268 +22,277 @@
19
22
 
20
23
  %>
21
24
 
22
- <%
23
- chord_chart = local_assigns.fetch(:chord_chart)
24
- valid_instruments = chord_chart_instrument_options.map(&:last)
25
- requested = local_assigns[:instrument]
26
- instrument = valid_instruments.include?(requested) ? requested : "guitar"
27
- to_key = local_assigns[:to_key].presence
28
- editable = local_assigns[:editable] || false
29
- transposed = to_key.present? && to_key != chord_chart.key
30
- chords = chord_chart.display_chords(to_key: to_key)
25
+ <% chord_chart = local_assigns.fetch(:chord_chart) %>
31
26
 
32
- voicings = chords.to_h do |symbol, chord|
33
- bass = OutroRails::Theory::Transposer.split_bass(symbol).last
34
- [ symbol,
35
- chord_chart.voicing_for(chord, instrument,
36
- transposed: transposed, bass: bass) ]
37
- end
27
+ <% if chord_chart.nil? %>
28
+ <div class="outro-chord-chart outro-chord-chart--empty">
29
+ <div class="outro-empty">
30
+ <p>No chord chart.</p>
31
+ </div>
32
+ </div>
33
+ <% else %>
38
34
 
39
- editing = editable && !transposed
40
- selections =
41
- if editing
42
- chord_chart.voicings.where(instrument: instrument)
43
- .index_by { |s| [ s.chord_id, s.bass ] }
44
- else
45
- {}
46
- end
47
- %>
35
+ <%
36
+ valid_instruments = chord_chart_instrument_options.map(&:last)
37
+ requested = local_assigns[:instrument]
38
+ instrument = valid_instruments.include?(requested) ? requested : "guitar"
39
+ to_key = local_assigns[:to_key].presence
40
+ editable = local_assigns[:editable] || false
41
+ transposed = to_key.present? && to_key != chord_chart.key
42
+ chords = chord_chart.display_chords(to_key: to_key)
48
43
 
49
- <%# The container is keyed by chart id so the voicing cycler's
50
- fetch-based refresh (see outro_rails.js) can swap in the updated
51
- chart from the redirected response without reloading the page. %>
52
- <div class="outro-chord-chart" data-outro-chord-chart="<%= chord_chart.id %>">
53
- <section class="outro-card">
54
- <h2 class="outro-section-heading">
55
- <%= chord_chart.song %>
56
- <span class="outro-symbol-list__detail"><%= chord_chart.artist %></span>
57
- </h2>
44
+ voicings = chords.to_h do |symbol, chord|
45
+ bass = OutroRails::Theory::Transposer.split_bass(symbol).last
46
+ [ symbol,
47
+ chord_chart.voicing_for(chord, instrument,
48
+ transposed: transposed, bass: bass) ]
49
+ end
58
50
 
59
- <dl class="outro-spec">
60
- <dt class="outro-spec__term">Tuning</dt>
61
- <dd class="outro-spec__detail"><%= chord_chart.to_tuning.label %></dd>
51
+ editing = editable && !transposed
52
+ selections =
53
+ if editing
54
+ chord_chart.voicings.where(instrument: instrument)
55
+ .index_by { |s| [ s.chord_id, s.bass ] }
56
+ else
57
+ {}
58
+ end
59
+ %>
62
60
 
63
- <dt class="outro-spec__term">Capo</dt>
64
- <dd class="outro-spec__detail">
65
- <% if chord_chart.capo.present? %>
66
- <%= capo_fret_label(chord_chart.capo) %>
67
- <% else %>
68
- None
69
- <% end %>
70
- </dd>
61
+ <div class="outro-chord-chart" data-outro-chord-chart="<%= chord_chart.id %>">
62
+ <section class="outro-card">
63
+ <h2 class="outro-section-heading">
64
+ <%= chord_chart.song %>
65
+ <span class="outro-symbol-list__detail"><%= chord_chart.artist %></span>
66
+ </h2>
71
67
 
72
- <dt class="outro-spec__term">Key</dt>
73
- <dd class="outro-spec__detail">
74
- <% if transposed %>
75
- <%= to_key %>
76
- <span class="outro-symbol-list__detail">
77
- originally <%= chord_chart.key %>
78
- </span>
79
- <% else %>
80
- <%= chord_chart.key %>
81
- <% end %>
82
- </dd>
83
- </dl>
68
+ <dl class="outro-spec">
69
+ <dt class="outro-spec__term">Tuning</dt>
70
+ <dd class="outro-spec__detail"><%= chord_chart.to_tuning.label %></dd>
84
71
 
85
- <div class="outro-picker outro-picker--flush">
86
- <%= form_with url: request.path, method: :get,
87
- class: "outro-picker__field" do %>
88
- <span class="outro-label">Instrument</span>
89
- <%= select_tag :instrument,
90
- options_for_select(chord_chart_instrument_options, instrument),
91
- class: "outro-select",
92
- onchange: "this.form.requestSubmit()" %>
93
- <%= hidden_field_tag :to_key, to_key if to_key %>
94
- <% end %>
72
+ <dt class="outro-spec__term">Capo</dt>
73
+ <dd class="outro-spec__detail">
74
+ <% if chord_chart.capo.present? %>
75
+ <%= capo_fret_label(chord_chart.capo) %>
76
+ <% else %>
77
+ None
78
+ <% end %>
79
+ </dd>
95
80
 
96
- <%= form_with url: request.path, method: :get,
97
- class: "outro-picker__field" do %>
98
- <span class="outro-label">Transpose to</span>
99
- <%= select_tag :to_key,
100
- grouped_options_for_select(chord_chart_key_options,
101
- to_key || chord_chart.key),
102
- class: "outro-select",
103
- onchange: "this.form.requestSubmit()" %>
104
- <%= hidden_field_tag :instrument, instrument %>
105
- <% end %>
81
+ <dt class="outro-spec__term">Key</dt>
82
+ <dd class="outro-spec__detail">
83
+ <% if transposed %>
84
+ <%= to_key %>
85
+ <span class="outro-symbol-list__detail">
86
+ originally <%= chord_chart.key %>
87
+ </span>
88
+ <% else %>
89
+ <%= chord_chart.key %>
90
+ <% end %>
91
+ </dd>
92
+ </dl>
106
93
 
107
- <% if transposed %>
108
- <%= link_to "Reset to #{chord_chart.key}",
109
- url_for(request.query_parameters.except("to_key")),
110
- class: "outro-chip outro-chip--link" %>
111
- <% end %>
112
- </div>
113
- </section>
94
+ <% if chord_chart&.body.present? %>
95
+ <div class="outro-picker outro-picker--flush">
96
+ <%= form_with url: request.path, method: :get,
97
+ class: "outro-picker__field" do %>
98
+ <span class="outro-label">Instrument</span>
99
+ <%= select_tag :instrument,
100
+ options_for_select(chord_chart_instrument_options, instrument),
101
+ class: "outro-select",
102
+ onchange: "this.form.requestSubmit()" %>
103
+ <%= hidden_field_tag :to_key, to_key if to_key %>
104
+ <% end %>
114
105
 
115
- <section class="outro-card">
116
- <h2 class="outro-section-heading">Chart</h2>
117
- <pre class="outro-chord-chart__body"><%=
118
- chord_chart_body(chord_chart, to_key: to_key)
119
- %></pre>
106
+ <%= form_with url: request.path, method: :get,
107
+ class: "outro-picker__field" do %>
108
+ <span class="outro-label">Transpose to</span>
109
+ <%= select_tag :to_key,
110
+ grouped_options_for_select(chord_chart_key_options,
111
+ to_key || chord_chart.key),
112
+ class: "outro-select",
113
+ onchange: "this.form.requestSubmit()" %>
114
+ <%= hidden_field_tag :instrument, instrument %>
115
+ <% end %>
120
116
 
121
- <%# Hover-popup figures, one per distinct chord in the body. <template>
122
- contents are inert; outro_rails.js clones the matching one into a
123
- floating popup when a chord in the chart text is hovered. %>
124
- <% voicings.each do |symbol, voicing| %>
125
- <% next unless voicing %>
126
- <template data-outro-chord-popup="<%= symbol %>">
127
- <p class="outro-chord-chart__popup-title"><%= voicing.symbol %></p>
128
- <%= render "outro_rails/chord_voicings/figure", voicing: voicing %>
129
- </template>
130
- <% end %>
131
- </section>
117
+ <% if transposed %>
118
+ <%= link_to "Reset to #{chord_chart.key}",
119
+ url_for(request.query_parameters.except("to_key")),
120
+ class: "outro-chip outro-chip--link" %>
121
+ <% end %>
122
+ </div>
123
+ <%end %>
124
+ </section>
132
125
 
133
- <section class="outro-card">
134
- <h2 class="outro-section-heading">
135
- Chords
136
- <% if editing %>
137
- <span class="outro-symbol-list__detail">
138
- Use the arrows to browse each chord's voicings and pick
139
- which one shows.
140
- </span>
141
- <% end %>
142
- </h2>
126
+ <% if chord_chart&.body.present? %>
127
+ <section class="outro-card">
128
+ <h2 class="outro-section-heading">Chart</h2>
129
+ <pre class="outro-chord-chart__body">
130
+ <%= chord_chart_body(chord_chart, to_key: to_key) %>
131
+ </pre>
143
132
 
144
- <% if chords.any? %>
145
- <ul class="outro-grid outro-grid--figures">
146
- <% chords.each do |symbol, chord| %>
147
- <% voicing = voicings[symbol] %>
148
- <li class="outro-grid__item">
149
- <div class="outro-card">
150
- <h3 class="outro-section-heading">
151
- <%= link_to symbol, outro_rails.chord_path(chord) %>
152
- <% if voicing && voicing.symbol != symbol %>
153
- <span class="outro-symbol-list__detail">
154
- <%= voicing.symbol %> voicing
155
- </span>
156
- <% end %>
157
- </h3>
133
+ <% voicings.each do |symbol, voicing| %>
134
+ <% next unless voicing %>
135
+ <template data-outro-chord-popup="<%= symbol %>">
136
+ <p class="outro-chord-chart__popup-title"><%= voicing.symbol %></p>
137
+ <%= render "outro_rails/chord_voicings/figure", voicing: voicing %>
138
+ </template>
139
+ <% end %>
140
+ </section>
158
141
 
159
- <% if !voicing %>
160
- <div class="outro-empty">
161
- <p>No <%= instrument %> voicing yet.</p>
162
- </div>
163
- <% elsif editing %>
164
- <%
165
- bass = OutroRails::Theory::Transposer
166
- .split_bass(symbol).last
167
- all_voicings = chord.voicings
168
- .where(instrument: instrument)
169
- .order(:position).to_a
170
- normalized = OutroRails::ChordChart::Voicing
171
- .normalize_bass(bass)
172
- selection = selections[[ chord.id, normalized ]]
173
- default_voicing =
174
- chord_chart.default_voicing_for(chord, instrument,
175
- bass: bass)
142
+ <section class="outro-card">
143
+ <h2 class="outro-section-heading">
144
+ Chords
145
+ <% if editing %>
146
+ <span class="outro-symbol-list__detail">
147
+ Use the arrows to browse each chord's voicings and pick
148
+ which one shows.
149
+ </span>
150
+ <% end %>
151
+ </h2>
176
152
 
177
- form_params = {
178
- chord_id: chord.id,
179
- instrument: instrument,
180
- bass: bass,
181
- return_to: request.fullpath
182
- }
183
- %>
153
+ <% if chords.any? %>
154
+ <ul class="outro-grid outro-grid--figures">
155
+ <% chords.each do |symbol, chord| %>
156
+ <% voicing = voicings[symbol] %>
157
+ <li class="outro-grid__item">
158
+ <div class="outro-card">
159
+ <h3 class="outro-section-heading">
160
+ <%= link_to symbol, outro_rails.chord_path(chord) %>
161
+ <% if voicing && voicing.symbol != symbol %>
162
+ <span class="outro-symbol-list__detail">
163
+ <%= voicing.symbol %> voicing
164
+ </span>
165
+ <% end %>
166
+ </h3>
184
167
 
185
- <div class="outro-voicing-cycler" data-outro-voicing-cycler>
186
- <% if all_voicings.many? %>
187
- <div class="outro-voicing-cycler__nav">
188
- <button type="button" data-voicing-prev
189
- class="outro-button
190
- outro-voicing-cycler__arrow"
191
- aria-label=
192
- "Previous voicing for <%= symbol %>">
193
- &#9664;
194
- </button>
195
- <span class="outro-voicing-cycler__counter"
196
- data-voicing-counter aria-live="polite"></span>
197
- <button type="button" data-voicing-next
198
- class="outro-button
199
- outro-voicing-cycler__arrow"
200
- aria-label=
201
- "Next voicing for <%= symbol %>">
202
- &#9654;
203
- </button>
168
+ <% if !voicing %>
169
+ <div class="outro-empty">
170
+ <p>No <%= instrument %> voicing yet.</p>
204
171
  </div>
205
- <% end %>
206
-
207
- <% all_voicings.each do |candidate| %>
172
+ <% elsif editing %>
208
173
  <%
209
- showing = candidate.id == voicing.id
210
- is_pick = selection&.chord_voicing_id == candidate.id
211
- is_default = default_voicing&.id == candidate.id
212
- caption = "#{candidate.symbol} - " \
213
- "position #{candidate.position}"
174
+ bass = OutroRails::Theory::Transposer
175
+ .split_bass(symbol).last
176
+ all_voicings = chord.voicings
177
+ .where(instrument: instrument)
178
+ .order(:position).to_a
179
+ normalized = OutroRails::ChordChart::Voicing
180
+ .normalize_bass(bass)
181
+ selection = selections[[ chord.id, normalized ]]
182
+ default_voicing =
183
+ chord_chart.default_voicing_for(chord, instrument,
184
+ bass: bass)
185
+
186
+ form_params = {
187
+ chord_id: chord.id,
188
+ instrument: instrument,
189
+ bass: bass,
190
+ return_to: request.fullpath
191
+ }
214
192
  %>
215
- <div class="outro-voicing-cycler__slide"
216
- data-voicing-slide
217
- <%= "hidden" unless showing %>>
218
- <p class="outro-spec__detail">
219
- <%= render "outro_rails/chord_voicings/figure",
220
- voicing: candidate,
221
- caption: caption %>
222
193
 
223
- </p>
194
+ <div class="outro-voicing-cycler" data-outro-voicing-cycler>
195
+ <% if all_voicings.many? %>
196
+ <div class="outro-voicing-cycler__nav">
197
+ <button type="button" data-voicing-prev
198
+ class="outro-button
199
+ outro-voicing-cycler__arrow"
200
+ aria-label=
201
+ "Previous voicing for <%= symbol %>">
202
+ &#9664;
203
+ </button>
204
+ <span class="outro-voicing-cycler__counter"
205
+ data-voicing-counter aria-live="polite"></span>
206
+ <button type="button" data-voicing-next
207
+ class="outro-button
208
+ outro-voicing-cycler__arrow"
209
+ aria-label=
210
+ "Next voicing for <%= symbol %>">
211
+ &#9654;
212
+ </button>
213
+ </div>
214
+ <% end %>
224
215
 
225
- <% if is_default && selection %>
226
- <%# Not currently on the default: navigating here
227
- offers the revert, same as the pick's slide. %>
228
- <%
229
- revert_path = outro_rails.chord_chart_voicing_path(
230
- chord_chart, selection, **form_params
231
- )
232
- %>
233
- <%= button_to revert_path,
234
- method: :delete, class: "outro-button",
235
- form_class: "outro-inline-form" do %>
236
- <%= image_tag "outro_rails/undo.svg",
237
- class: "outro-button__icon" %>
238
- Revert to default
239
- <% end %>
240
- <% elsif !showing %>
216
+ <% all_voicings.each do |candidate| %>
241
217
  <%
242
- pick_path = outro_rails.chord_chart_voicings_path(
243
- chord_chart, **form_params
244
- )
218
+ showing = candidate.id == voicing.id
219
+ is_pick = selection&.chord_voicing_id == candidate.id
220
+ is_default = default_voicing&.id == candidate.id
221
+ caption = "#{candidate.symbol} - " \
222
+ "position #{candidate.position}"
245
223
  %>
246
- <%= button_to pick_path,
247
- method: :post,
248
- params: { chord_voicing_id: candidate.id },
249
- class: "outro-button",
250
- form_class: "outro-inline-form" do %>
251
- <%= image_tag "outro_rails/check.svg",
252
- class: "outro-button__icon" %>
253
- Select Voicing
254
- <% end %>
255
- <% elsif is_pick %>
256
- <%
257
- revert_path = outro_rails.chord_chart_voicing_path(
258
- chord_chart, selection, **form_params
259
- )
260
- %>
261
- <%= button_to revert_path,
262
- method: :delete, class: "outro-button",
263
- form_class: "outro-inline-form" do %>
264
- <%= image_tag "outro_rails/undo.svg",
265
- class: "outro-button__icon" %>
266
- Revert to default
267
- <% end %>
224
+ <div class="outro-voicing-cycler__slide"
225
+ data-voicing-slide
226
+ <%= "hidden" unless showing %>>
227
+ <p class="outro-spec__detail">
228
+ <%= render "outro_rails/chord_voicings/figure",
229
+ voicing: candidate,
230
+ caption: caption %>
231
+
232
+ </p>
233
+
234
+ <% if is_default && selection %>
235
+ <%# Not currently on the default: navigating here
236
+ offers the revert, same as the pick's slide. %>
237
+ <%
238
+ revert_path = outro_rails.chord_chart_voicing_path(
239
+ chord_chart, selection, **form_params
240
+ )
241
+ %>
242
+ <%= button_to revert_path,
243
+ method: :delete, class: "outro-button",
244
+ form_class: "outro-inline-form" do %>
245
+ <%= image_tag "outro_rails/undo.svg",
246
+ class: "outro-button__icon" %>
247
+ Revert to default
248
+ <% end %>
249
+ <% elsif !showing %>
250
+ <%
251
+ pick_path = outro_rails.chord_chart_voicings_path(
252
+ chord_chart, **form_params
253
+ )
254
+ %>
255
+ <%= button_to pick_path,
256
+ method: :post,
257
+ params: { chord_voicing_id: candidate.id },
258
+ class: "outro-button",
259
+ form_class: "outro-inline-form" do %>
260
+ <%= image_tag "outro_rails/check.svg",
261
+ class: "outro-button__icon" %>
262
+ Select Voicing
263
+ <% end %>
264
+ <% elsif is_pick %>
265
+ <%
266
+ revert_path = outro_rails.chord_chart_voicing_path(
267
+ chord_chart, selection, **form_params
268
+ )
269
+ %>
270
+ <%= button_to revert_path,
271
+ method: :delete, class: "outro-button",
272
+ form_class: "outro-inline-form" do %>
273
+ <%= image_tag "outro_rails/undo.svg",
274
+ class: "outro-button__icon" %>
275
+ Revert to default
276
+ <% end %>
277
+ <% end %>
278
+ </div>
268
279
  <% end %>
269
280
  </div>
281
+ <% else %>
282
+ <%= render "outro_rails/chord_voicings/figure",
283
+ voicing: voicing %>
270
284
  <% end %>
271
285
  </div>
272
- <% else %>
273
- <%= render "outro_rails/chord_voicings/figure",
274
- voicing: voicing %>
275
- <% end %>
276
- </div>
277
- </li>
286
+ </li>
287
+ <% end %>
288
+ </ul>
289
+ <% else %>
290
+ <div class="outro-empty">
291
+ <p>No recognized chords.</p>
292
+ </div>
278
293
  <% end %>
279
- </ul>
280
- <% else %>
281
- <div class="outro-empty">
282
- <p>No recognized chords yet.</p>
283
- </div>
294
+ </section>
284
295
  <% end %>
285
- </section>
286
- </div>
296
+ </div>
297
+
298
+ <% end %>
@@ -5,10 +5,43 @@
5
5
 
6
6
  form_with model: @song do |form|
7
7
  form.chord_chart_area :chord_sheet
8
+ form.chord_chart_area :chord_sheet, legend: "Lead sheet"
9
+ form.chord_chart_area :chord_sheet, legend: false
8
10
  end
11
+
12
+ Locals:
13
+ f - the nested form builder for the chord chart
14
+ required - whether the owner declared the chart with `required: true`
15
+ (the default). When false, leaving every field blank
16
+ saves the record without a chart.
17
+ legend - heading shown above the fields, default "Chord chart".
18
+ Pass false to leave it out (when the host page already has
19
+ its own heading for this section).
20
+ %>
21
+
22
+ <%
23
+ required = local_assigns.fetch(:required, true)
24
+ legend = local_assigns.fetch(:legend, "Chord Chart")
9
25
  %>
10
26
 
11
27
  <fieldset class="outro-chord-chart-fields">
28
+ <% if legend %>
29
+ <legend class="outro-chord-chart-fields__legend">
30
+ <span class="outro-section-heading">
31
+ <%= legend %>
32
+ <% unless required %>
33
+ <span class="outro-symbol-list__detail">Optional</span>
34
+ <% end %>
35
+ </span>
36
+ </legend>
37
+ <% end %>
38
+
39
+ <% unless required %>
40
+ <p class="outro-chord-chart-fields__hint">
41
+ Leave artist, song, and chords blank if there is no chord chart.
42
+ </p>
43
+ <% end %>
44
+
12
45
  <div class="outro-picker outro-picker--grid outro-picker--cols-2">
13
46
  <label class="outro-picker__field">
14
47
  <span class="outro-label">Artist</span>
@@ -41,7 +74,7 @@
41
74
  </div>
42
75
 
43
76
  <label class="outro-transposer__field">
44
- <span class="outro-label">Chords &amp; lyrics</span>
77
+ <span class="outro-label">Chords &amp; lyrics (Optional)</span>
45
78
  <%= f.text_area :body,
46
79
  rows: 16,
47
80
  placeholder: "Paste or type a chord chart here.",
@@ -5,7 +5,7 @@ module OutroRails
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  class_methods do
8
- def has_chord_chart(name, dependent: :destroy)
8
+ def has_chord_chart(name, required: true, dependent: :destroy)
9
9
  association_name = :"chord_chart_#{name}"
10
10
 
11
11
  has_one association_name,
@@ -14,25 +14,77 @@ module OutroRails
14
14
  as: :record,
15
15
  inverse_of: :record,
16
16
  autosave: true,
17
+ validate: false,
17
18
  dependent: dependent
18
19
 
19
20
  accepts_nested_attributes_for association_name
20
21
 
22
+ validate do
23
+ chart = association(association_name).target
24
+ next if chart.nil? || chart.destroyed? || chart.marked_for_destruction?
25
+ next unless chart.changed_for_autosave?
26
+ next if chart.valid?
27
+
28
+ chart.errors.each do |error|
29
+ attribute = error.attribute == :base ? name : :"#{name}.#{error.attribute}"
30
+ errors.import(error, attribute: attribute)
31
+ end
32
+ end
33
+
34
+ fallback = if required
35
+ "build_#{association_name}(name: #{name.to_s.inspect})"
36
+ else
37
+ "nil"
38
+ end
39
+
21
40
  class_eval <<-CODE, __FILE__, __LINE__ + 1
22
41
  def #{name}
23
- #{association_name} ||
24
- build_#{association_name}(name: #{name.to_s.inspect})
42
+ chart = #{association_name}
43
+ return chart if chart && !chart.destroyed? &&
44
+ !chart.marked_for_destruction?
45
+
46
+ #{fallback}
25
47
  end
26
48
 
27
49
  def #{name}?
28
- #{association_name}.present?
50
+ chart = #{association_name}
51
+ !chart.nil? && !chart.destroyed? && !chart.marked_for_destruction?
29
52
  end
30
53
 
31
54
  def #{name}=(attributes)
32
- #{name}.assign_attributes(attributes)
55
+ if attributes.nil?
56
+ self.#{association_name} = nil
57
+ else
58
+ chart = #{association_name} ||
59
+ build_#{association_name}(name: #{name.to_s.inspect})
60
+ chart.assign_attributes(attributes)
61
+ end
62
+ end
63
+
64
+ def #{name}_required?
65
+ #{required}
33
66
  end
34
67
  CODE
35
68
 
69
+ unless required
70
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
71
+ def #{association_name}_attributes=(attributes)
72
+ unless OutroRails::ChordChart.blank_attributes?(attributes)
73
+ return super
74
+ end
75
+
76
+ chart = #{association_name}
77
+ return unless chart
78
+
79
+ if chart.persisted?
80
+ chart.mark_for_destruction
81
+ else
82
+ self.#{association_name} = nil
83
+ end
84
+ end
85
+ CODE
86
+ end
87
+
36
88
  scope :"with_#{name}", -> { includes(association_name) }
37
89
  end
38
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OutroRails
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: outro_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - endtoendpaper