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 +4 -4
- data/README.md +54 -9
- data/app/assets/stylesheets/outro_rails/outro_rails.css +6 -0
- data/app/helpers/outro_rails/chord_charts_helper.rb +9 -3
- data/app/models/outro_rails/chord_chart.rb +11 -1
- data/app/views/outro_rails/chord_charts/_chart.html.erb +249 -237
- data/app/views/outro_rails/chord_charts/_fields.html.erb +34 -1
- data/lib/outro_rails/chord_chart_attribute.rb +57 -5
- data/lib/outro_rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 873a2a004ee523b79f100a4aa815e4986dbede341e9ca65740faef497821d8ad
|
|
4
|
+
data.tar.gz: a053185e62246437b3fd50381ffbacac6afc60ac3e498bcdb06a2a314683b9cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
|
215
|
-
options are pulled from
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
◀
|
|
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
|
-
▶
|
|
203
|
-
</button>
|
|
168
|
+
<% if !voicing %>
|
|
169
|
+
<div class="outro-empty">
|
|
170
|
+
<p>No <%= instrument %> voicing yet.</p>
|
|
204
171
|
</div>
|
|
205
|
-
<%
|
|
206
|
-
|
|
207
|
-
<% all_voicings.each do |candidate| %>
|
|
172
|
+
<% elsif editing %>
|
|
208
173
|
<%
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
+
◀
|
|
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
|
+
▶
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
224
215
|
|
|
225
|
-
<%
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
</
|
|
280
|
-
<% else %>
|
|
281
|
-
<div class="outro-empty">
|
|
282
|
-
<p>No recognized chords yet.</p>
|
|
283
|
-
</div>
|
|
294
|
+
</section>
|
|
284
295
|
<% end %>
|
|
285
|
-
</
|
|
286
|
-
|
|
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 & lyrics</span>
|
|
77
|
+
<span class="outro-label">Chords & 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
|
-
|
|
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}
|
|
50
|
+
chart = #{association_name}
|
|
51
|
+
!chart.nil? && !chart.destroyed? && !chart.marked_for_destruction?
|
|
29
52
|
end
|
|
30
53
|
|
|
31
54
|
def #{name}=(attributes)
|
|
32
|
-
|
|
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
|
data/lib/outro_rails/version.rb
CHANGED