dynaspan 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ // Dynaspan: click-to-edit, in-place AJAX text editing for Rails.
2
+ // https://github.com/danielpclark/dynaspan
3
+ // MIT License. Copyright (c) 2014-2026 Daniel P. Clark
4
+ //
5
+ // Works with Sprockets (`//= require dynaspan/dynaspan`), Propshaft
6
+ // (`javascript_include_tag "dynaspan/dynaspan"`) and importmap-rails
7
+ // (`import "dynaspan"`). No jQuery, rails-ujs or Turbo required.
8
+ (function (window, document) {
9
+ 'use strict';
10
+
11
+ if (!window || !document || (window.Dynaspan && window.Dynaspan.version)) return;
12
+
13
+ var BLOCK_SELECTOR = '[data-dynaspan]';
14
+
15
+ function parts(uid) {
16
+ return {
17
+ block: document.getElementById('dyna_span_block' + uid),
18
+ container: document.getElementById('dyna_span_div' + uid),
19
+ field: document.getElementById('dyna_span_field_val_' + uid),
20
+ span: document.getElementById('dyna_span_span' + uid),
21
+ last: document.getElementById('last_dyna_span_val_' + uid)
22
+ };
23
+ }
24
+
25
+ function uidFor(element) {
26
+ var block = element && element.closest && element.closest(BLOCK_SELECTOR);
27
+ return block ? block.getAttribute('data-dynaspan') : null;
28
+ }
29
+
30
+ function dispatch(target, name, detail, cancelable) {
31
+ var event = new CustomEvent('dynaspan:' + name, { bubbles: true, cancelable: !!cancelable, detail: detail });
32
+ return target.dispatchEvent(event);
33
+ }
34
+
35
+ function fieldValue(field) {
36
+ if (field.multiple) {
37
+ return Array.prototype.map.call(field.selectedOptions, function (option) { return option.value; }).join(',');
38
+ }
39
+ return field.value;
40
+ }
41
+
42
+ function fieldText(field) {
43
+ if (field.tagName === 'SELECT') {
44
+ return Array.prototype.map.call(field.selectedOptions, function (option) { return option.text; }).join(', ');
45
+ }
46
+ return field.value;
47
+ }
48
+
49
+ function isTextInput(field) {
50
+ return field.tagName === 'TEXTAREA' || (field.tagName === 'INPUT' && /^(text|search|url|tel|email|password)?$/i.test(field.type));
51
+ }
52
+
53
+ // Resets the field to the last value that was saved.
54
+ function restore(uid) {
55
+ var p = parts(uid);
56
+ if (p.field && p.last && !p.field.multiple) p.field.value = p.last.value;
57
+ }
58
+
59
+ function show(uid) {
60
+ var p = parts(uid);
61
+ if (!p.block || !p.container || !p.field || !p.span) return;
62
+ if (!p.block.classList.contains('ds-dialog-open')) {
63
+ restore(uid);
64
+ p.span.setAttribute('data-ds-display', p.span.style.display);
65
+ p.span.style.display = 'none';
66
+ p.container.style.display = '';
67
+ p.block.classList.add('ds-dialog-open');
68
+ dispatch(p.block, 'open', { selector: '#' + p.block.id, value: fieldValue(p.field) });
69
+ }
70
+ p.field.focus();
71
+ if (isTextInput(p.field) && p.field.setSelectionRange) {
72
+ var end = p.field.value.length;
73
+ try { p.field.setSelectionRange(end, end); } catch (e) { /* not supported for this input type */ }
74
+ }
75
+ }
76
+
77
+ function hide(uid, options) {
78
+ options = options || {};
79
+ var p = parts(uid);
80
+ if (!p.block || !p.container || !p.field || !p.span) return;
81
+ if (!p.block.classList.contains('ds-dialog-open')) return;
82
+
83
+ if (options.cancel) restore(uid);
84
+
85
+ var value = fieldValue(p.field);
86
+ var text = fieldText(p.field);
87
+ var changed = !options.cancel && (!p.last || p.last.value !== value);
88
+
89
+ p.block.classList.remove('ds-dialog-open');
90
+ p.container.style.display = 'none';
91
+ p.span.style.display = p.span.getAttribute('data-ds-display') || '';
92
+
93
+ if (changed) {
94
+ p.span.textContent = text;
95
+ if (p.last) p.last.value = value;
96
+ p.block.classList.toggle('ds-content-present', text.length > 0);
97
+ save(p, text, value);
98
+ }
99
+
100
+ dispatch(p.block, 'close', { selector: '#' + p.block.id, value: value, changed: changed });
101
+ }
102
+
103
+ // Appends a parameter to a JavaScript call written as a string:
104
+ // "foo('a');" => "foo('a',param);"
105
+ function appendParameter(command, param) {
106
+ var withArgs = /\(([^)]+)\);?$/.exec(command);
107
+ var withoutArgs = /\(\);?$/.exec(command);
108
+ if (withArgs) return command.replace(withArgs[0], '(' + withArgs[1] + ',' + param + ');');
109
+ if (withoutArgs) return command.replace(withoutArgs[0], '(' + param + ');');
110
+ return command.replace(/;$/, '') + '(' + param + ');';
111
+ }
112
+
113
+ function runCallback(block, code, values) {
114
+ try {
115
+ if (values) {
116
+ new Function('__dynaspan_values__', appendParameter(code, '__dynaspan_values__')).call(block, values);
117
+ } else {
118
+ new Function(code).call(block);
119
+ }
120
+ } catch (error) {
121
+ if (window.console) window.console.error('Dynaspan callback failed:', error);
122
+ }
123
+ }
124
+
125
+ function csrfToken(form) {
126
+ var field = form.querySelector('input[name="authenticity_token"]');
127
+ if (field && field.value) return field.value;
128
+ var meta = document.querySelector('meta[name="csrf-token"]');
129
+ return meta ? meta.getAttribute('content') : null;
130
+ }
131
+
132
+ function cspNonce() {
133
+ var meta = document.querySelector('meta[name="csp-nonce"]');
134
+ return meta ? meta.getAttribute('content') : null;
135
+ }
136
+
137
+ // Handles `update.turbo_stream.erb` and `update.js.erb` style responses.
138
+ function handleResponse(response, body) {
139
+ var type = response.headers.get('Content-Type') || '';
140
+ if (/turbo-stream/.test(type) && window.Turbo && window.Turbo.renderStreamMessage) {
141
+ window.Turbo.renderStreamMessage(body);
142
+ } else if (/(java|ecma)script/.test(type) && body) {
143
+ var script = document.createElement('script');
144
+ var nonce = cspNonce();
145
+ if (nonce) script.setAttribute('nonce', nonce);
146
+ script.text = body;
147
+ document.head.appendChild(script).parentNode.removeChild(script);
148
+ }
149
+ }
150
+
151
+ function save(p, text, value) {
152
+ var block = p.block;
153
+ var form = p.container.querySelector('form');
154
+ var detail = { selector: '#' + block.id, input: text, value: value, form: form };
155
+
156
+ if (!form || !dispatch(block, 'update', detail, true)) return;
157
+
158
+ var headers = {
159
+ Accept: 'text/vnd.turbo-stream.html, text/javascript, application/javascript, application/json, text/html;q=0.9, */*;q=0.1',
160
+ 'X-Requested-With': 'XMLHttpRequest'
161
+ };
162
+ var token = csrfToken(form);
163
+ if (token) headers['X-CSRF-Token'] = token;
164
+
165
+ block.classList.add('ds-saving');
166
+ block.classList.remove('ds-error');
167
+
168
+ window.fetch(form.action, { method: 'POST', body: new FormData(form), headers: headers, credentials: 'same-origin' })
169
+ .then(function (response) {
170
+ return response.text().then(function (body) {
171
+ if (!response.ok) {
172
+ var error = new Error('Dynaspan update failed with HTTP status ' + response.status);
173
+ error.response = response;
174
+ throw error;
175
+ }
176
+ handleResponse(response, body);
177
+ detail.response = response;
178
+ dispatch(block, 'success', detail);
179
+ });
180
+ })
181
+ .catch(function (error) {
182
+ block.classList.add('ds-error');
183
+ detail.error = error;
184
+ detail.response = error.response;
185
+ dispatch(block, 'error', detail);
186
+ })
187
+ .then(function () {
188
+ block.classList.remove('ds-saving');
189
+ });
190
+
191
+ var withValues = block.getAttribute('data-ds-callback-with-values');
192
+ if (withValues) runCallback(block, withValues, { ds_selector: '#' + block.id, ds_input: text });
193
+ var onUpdate = block.getAttribute('data-ds-callback-on-update');
194
+ if (onUpdate) runCallback(block, onUpdate);
195
+ }
196
+
197
+ function isTrigger(element) {
198
+ return element.closest('.dyna-span-text, .dyna-span-edit-text');
199
+ }
200
+
201
+ function isInput(element) {
202
+ return element.classList && element.classList.contains('dyna-span-input');
203
+ }
204
+
205
+ document.addEventListener('click', function (event) {
206
+ var uid = uidFor(event.target);
207
+ if (uid && isTrigger(event.target)) show(uid);
208
+ });
209
+
210
+ document.addEventListener('focusout', function (event) {
211
+ var uid = uidFor(event.target);
212
+ if (uid && isInput(event.target)) hide(uid);
213
+ });
214
+
215
+ document.addEventListener('keydown', function (event) {
216
+ var target = event.target;
217
+ var uid = uidFor(target);
218
+ if (!uid) return;
219
+
220
+ if (target.classList.contains('dyna-span-text')) {
221
+ if (event.key === 'Enter' || event.key === ' ') {
222
+ event.preventDefault();
223
+ show(uid);
224
+ }
225
+ return;
226
+ }
227
+
228
+ if (!isInput(target)) return;
229
+
230
+ var textarea = target.tagName === 'TEXTAREA';
231
+ var submitKey = event.key === 'Enter' && (!textarea || event.ctrlKey || event.metaKey);
232
+ if (event.key === 'Escape' || (submitKey && target.tagName !== 'SELECT')) {
233
+ event.preventDefault();
234
+ hide(uid, { cancel: event.key === 'Escape' });
235
+ var span = parts(uid).span;
236
+ if (span) span.focus();
237
+ }
238
+ });
239
+
240
+ // Never let the browser, rails-ujs or Turbo submit a Dynaspan form.
241
+ document.addEventListener('submit', function (event) {
242
+ var uid = uidFor(event.target);
243
+ if (!uid) return;
244
+ event.preventDefault();
245
+ event.stopPropagation();
246
+ hide(uid);
247
+ }, true);
248
+
249
+ var Dynaspan = {
250
+ version: '1.0.0',
251
+ show: show,
252
+ hide: function (uid) { hide(uid); },
253
+ cancel: function (uid) { hide(uid, { cancel: true }); },
254
+ appendParameter: appendParameter
255
+ };
256
+ window.Dynaspan = Dynaspan;
257
+
258
+ // Backwards compatibility with the jQuery API of Dynaspan 0.x:
259
+ // $().dynaspan.upShow(id), $().dynaspan.upHide(id), $().dynaspan.upLast(id)
260
+ function installJQueryApi() {
261
+ var $ = window.jQuery;
262
+ if (!$ || !$.fn || $.fn.dynaspan) return;
263
+ $.fn.dynaspan = function () { return this; };
264
+ $.fn.dynaspan.upShow = show;
265
+ $.fn.dynaspan.upHide = Dynaspan.hide;
266
+ $.fn.dynaspan.upLast = restore;
267
+ $.fn.dynaspan.appendParameter = appendParameter;
268
+ }
269
+ installJQueryApi();
270
+ document.addEventListener('DOMContentLoaded', installJQueryApi);
271
+ })(typeof window !== 'undefined' ? window : undefined, typeof document !== 'undefined' ? document : undefined);
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Dynaspan default styles (optional).
3
+ * Load with stylesheet_link_tag "dynaspan/dynaspan", require it from your
4
+ * Sprockets manifest, or write your own.
5
+ */
6
+ .dyna-span .dyna-span-text {
7
+ cursor: text;
8
+ min-height: 1.5em;
9
+ border-radius: 4px;
10
+ outline-offset: 2px;
11
+ transition: background-color 0.15s ease-in-out;
12
+ }
13
+
14
+ .dyna-span .dyna-span-text:hover,
15
+ .dyna-span .dyna-span-text:focus-visible {
16
+ background-color: rgba(59, 130, 246, 0.08);
17
+ box-shadow: inset 0 -1px 0 rgba(59, 130, 246, 0.6);
18
+ }
19
+
20
+ .dyna-span[data-dynaspan-kind="text_area"] .dyna-span-text {
21
+ white-space: pre-wrap;
22
+ }
23
+
24
+ .dyna-span .dyna-span-input {
25
+ box-sizing: border-box;
26
+ width: 100%;
27
+ font: inherit;
28
+ }
29
+
30
+ .dyna-span .dyna-span-edit-text {
31
+ cursor: pointer;
32
+ }
33
+
34
+ .dyna-span.ds-dialog-open > .dyna-span-edit-text {
35
+ display: none;
36
+ }
37
+
38
+ .dyna-span.ds-saving .dyna-span-text {
39
+ opacity: 0.6;
40
+ }
41
+
42
+ .dyna-span.ds-error .dyna-span-text {
43
+ box-shadow: inset 0 -2px 0 #dc2626;
44
+ }
@@ -1,67 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
1
5
  module Dynaspan
6
+ # View helpers that render a piece of text which turns into a form field
7
+ # when clicked and saves itself over AJAX when the field loses focus.
8
+ #
9
+ # Every helper takes the same arguments:
10
+ #
11
+ # dynaspan_text_field(record, :attribute)
12
+ # dynaspan_text_field(record, :attribute, '[edit]', options)
13
+ # dynaspan_text_field(record, nested_record, :attribute, '[edit]', options)
14
+ #
15
+ # See the README for the full list of options.
2
16
  module ApplicationHelper
17
+ RESERVED_HTML_OPTIONS = %w[id onblur onfocus].freeze
18
+ INPUT_CLASSES = 'dyna-span form-control dyna-span-input'
3
19
 
4
- def dynaspan_text_field(master_ds_object,*parameters)
5
- dynaspan_text('field', master_ds_object, parameters)
20
+ def dynaspan_text_field(master_ds_object, *parameters)
21
+ dynaspan_render(:text_field, master_ds_object, parameters)
6
22
  end
7
23
 
8
- def dynaspan_text_area(master_ds_object,*parameters)
9
- dynaspan_text('area', master_ds_object, parameters)
24
+ def dynaspan_text_area(master_ds_object, *parameters)
25
+ dynaspan_render(:text_area, master_ds_object, parameters)
10
26
  end
11
27
 
12
- def dynaspan_select(master_ds_object,*parameters, &block)
13
- dynaspan_text('select', master_ds_object, parameters, &block)
28
+ def dynaspan_select(master_ds_object, *parameters, &block)
29
+ dynaspan_render(:select, master_ds_object, parameters, &block)
14
30
  end
15
31
 
16
32
  private
17
- module_function
18
33
 
19
- def to_id(obj)
20
- !!obj ? "#{obj.class.name}#{obj.try(:id)}" : ""
34
+ def dynaspan_render(kind, master_ds_object, parameters, &block)
35
+ raise ArgumentError, 'Dynaspan needs a record to update as its first argument.' if master_ds_object.nil?
36
+
37
+ parameters = parameters.dup
38
+ options = parameters.last.is_a?(Hash) ? parameters.pop.with_indifferent_access : ActiveSupport::HashWithIndifferentAccess.new
39
+ attr_object, attrib, edit_text = dynaspan_parse_arguments(parameters)
40
+
41
+ association = dynaspan_nested_association(master_ds_object, attr_object)
42
+ target = attr_object || master_ds_object
43
+ value = target.public_send(attrib)
44
+ unique_ref_id = options.fetch(:unique_id) { dynaspan_unique_id(master_ds_object, attr_object, attrib) }.to_s
45
+ choices = options[:choices]
46
+
47
+ render(
48
+ partial: 'dynaspan/dynaspan',
49
+ locals: {
50
+ kind: kind,
51
+ master_ds_object: master_ds_object,
52
+ attr_object: attr_object,
53
+ association: association,
54
+ attrib: attrib,
55
+ value: value,
56
+ display_text: kind == :select ? dynaspan_choice_text(choices, value) || value : value,
57
+ unique_ref_id: unique_ref_id,
58
+ dyna_span_edit_text: edit_text,
59
+ hidden_fields: options[:hidden_fields] || {},
60
+ ds_callback_on_update: options[:callback_on_update],
61
+ ds_callback_with_values: options[:callback_with_values],
62
+ form_for_options: dynaspan_form_options(options[:form_for]),
63
+ choices: choices,
64
+ select_options: (options[:options] || {}).to_h.symbolize_keys,
65
+ html_options: dynaspan_html_options(options[:html_options], kind, unique_ref_id),
66
+ block: block
67
+ }
68
+ )
21
69
  end
22
70
 
23
- def dynaspan_text(kind, master_ds_object,*parameters, &block)
24
- parameters.flatten!
25
- if parameters.first.is_a? Symbol
71
+ # Accepts (attribute, edit_text?) or (nested_record, attribute, edit_text?).
72
+ def dynaspan_parse_arguments(parameters)
73
+ if parameters.first.is_a?(Symbol)
26
74
  attr_object = nil
27
- attrib = parameters.first
28
- edit_text = parameters.try(:[],1)
29
- elsif parameters[1].is_a? Symbol
30
- attr_object, attrib = parameters
31
- edit_text = parameters.try(:[],2)
75
+ attrib, edit_text = parameters
76
+ elsif parameters[1].is_a?(Symbol)
77
+ attr_object, attrib, edit_text = parameters
32
78
  else
33
- raise 'You did not provide a symbol for the form field.'
79
+ raise ArgumentError, 'Dynaspan needs a Symbol naming the attribute to edit, e.g. dynaspan_text_field(user, :name).'
34
80
  end
35
- edit_text = nil unless edit_text.is_a? String
36
- options = ActiveSupport::HashWithIndifferentAccess.new(parameters[-1].is_a?(Hash) ? parameters[-1] : {})
37
- options.default = nil
38
- options[:form_for] = ActiveSupport::HashWithIndifferentAccess.new(options[:form_for])
39
- options[:form_for].reverse_merge!(
40
- method: :patch,
41
- remote: true,
42
- authenticity_token: true
43
- )
44
- options[:html_options] = ActiveSupport::HashWithIndifferentAccess.new(options[:html_options])
45
- options[:html_options][:class] = "dyna-span form-control dyna-span-input #{options[:html_options][:class]}"
46
- options[:html_options].delete_if {|k,v| [:id, :onblur, :onfocus].include? k}
47
- render(
48
- partial: "dynaspan/dynaspan_text_#{kind}",
49
- locals: {
50
- master_ds_object: master_ds_object,
51
- attr_object: attr_object,
52
- attrib: attrib,
53
- unique_ref_id: options.fetch(:unique_id) { [to_id(master_ds_object), to_id(attr_object), attrib, SecureRandom.hex(3)].join },
54
- dyna_span_edit_text: edit_text,
55
- hidden_fields: options[:hidden_fields],
56
- ds_callback_on_update: options[:callback_on_update],
57
- ds_callback_with_values: options[:callback_with_values],
58
- form_for_options: options[:form_for],
59
- choices: options[:choices], # For form 'select' field
60
- options: options.fetch(:options) { Hash.new }, # For form 'select' field
61
- html_options: options[:html_options],
62
- block: block
63
- }
64
- )
81
+
82
+ [attr_object, attrib, edit_text.is_a?(String) ? edit_text : nil]
83
+ end
84
+
85
+ # Finds the `accepts_nested_attributes_for` association on the master
86
+ # record that the nested record belongs to.
87
+ def dynaspan_nested_association(master_ds_object, attr_object)
88
+ return if attr_object.nil?
89
+
90
+ klass = master_ds_object.class
91
+ names = klass.respond_to?(:nested_attributes_options) ? klass.nested_attributes_options.keys.map(&:to_s) : []
92
+
93
+ name = names.find do |association_name|
94
+ reflection = klass.try(:reflect_on_association, association_name)
95
+ reflection && !reflection.polymorphic? && attr_object.is_a?(reflection.klass)
96
+ end
97
+
98
+ name ||= begin
99
+ model_name = attr_object.class.model_name
100
+ candidates = [model_name.singular, model_name.plural, model_name.element, attr_object.class.try(:table_name)].compact
101
+ names.find { |association_name| candidates.any? { |candidate| association_name.include?(candidate) } }
102
+ end
103
+
104
+ unless name
105
+ raise ArgumentError,
106
+ "Dynaspan could not find a nested association for #{attr_object.class.name} on #{klass.name}. " \
107
+ "Add `accepts_nested_attributes_for` for it to #{klass.name}."
108
+ end
109
+
110
+ reflection = klass.try(:reflect_on_association, name)
111
+ { name: name.to_sym, collection: reflection.respond_to?(:collection?) && reflection.collection? }
112
+ end
113
+
114
+ def dynaspan_unique_id(master_ds_object, attr_object, attrib)
115
+ [dynaspan_record_id(master_ds_object), dynaspan_record_id(attr_object), attrib, SecureRandom.hex(3)]
116
+ .join.gsub(/[^A-Za-z0-9_-]/, '_')
117
+ end
118
+
119
+ def dynaspan_record_id(record)
120
+ record ? "#{record.class.name}#{record.try(:id)}" : ''
121
+ end
122
+
123
+ def dynaspan_form_options(custom)
124
+ { method: :patch, authenticity_token: true }
125
+ .merge((custom || {}).to_h.symbolize_keys)
126
+ .tap do |form_options|
127
+ form_options[:html] = (form_options[:html] || {}).to_h.symbolize_keys
128
+ form_options[:html][:data] = { turbo: false }.merge((form_options[:html][:data] || {}).to_h.symbolize_keys)
129
+ end
130
+ end
131
+
132
+ def dynaspan_html_options(custom, kind, unique_ref_id)
133
+ html_options = (custom || {}).to_h.stringify_keys.except(*RESERVED_HTML_OPTIONS)
134
+ html_options['class'] = [INPUT_CLASSES, html_options['class']].compact.join(' ').strip
135
+ html_options['id'] = "dyna_span_field_val_#{unique_ref_id}"
136
+ html_options['data'] = (html_options['data'] || {}).to_h.merge('dynaspan-input' => kind)
137
+ html_options.symbolize_keys
138
+ end
139
+
140
+ def dynaspan_input(builder, kind, attrib, locals)
141
+ case kind
142
+ when :text_field then builder.text_field(attrib, locals[:html_options])
143
+ when :text_area then builder.text_area(attrib, locals[:html_options])
144
+ when :select
145
+ builder.select(attrib, locals[:choices], locals[:select_options], locals[:html_options], &locals[:block])
146
+ end
147
+ end
148
+
149
+ # Looks up the label shown for `value` among select choices. Supports the
150
+ # formats Rails' `select` accepts: arrays, hashes, grouped choices and
151
+ # pre-rendered option tags (e.g. from `options_for_select`).
152
+ def dynaspan_choice_text(choices, value)
153
+ return if choices.blank?
154
+
155
+ target = value.to_s
156
+ if choices.is_a?(String)
157
+ require 'nokogiri'
158
+ option = Nokogiri::HTML::DocumentFragment.parse(choices).css('option').find do |node|
159
+ (node['value'] || node.text) == target
160
+ end
161
+ return option&.text
162
+ end
163
+
164
+ choices = choices.to_a if choices.is_a?(Hash)
165
+ choices.each do |element|
166
+ parts = element.is_a?(Array) ? element.reject { |part| part.is_a?(Hash) } : [element, element]
167
+ label = parts.first
168
+ option_value = parts.last
169
+
170
+ if option_value.is_a?(Array) || option_value.is_a?(Hash)
171
+ found = dynaspan_choice_text(option_value, value)
172
+ return found if found
173
+ elsif option_value.to_s == target
174
+ return label.to_s
175
+ end
176
+ end
177
+ nil
65
178
  end
66
179
  end
67
180
  end
@@ -0,0 +1,32 @@
1
+ <%# Rendered by dynaspan_text_field, dynaspan_text_area and dynaspan_select. %>
2
+ <%= tag.div id: "dyna_span_block#{unique_ref_id}",
3
+ class: ['dyna-span', ('ds-content-present' if value.present?)],
4
+ data: {
5
+ dynaspan: unique_ref_id,
6
+ dynaspan_kind: kind,
7
+ ds_callback_on_update: ds_callback_on_update.presence,
8
+ ds_callback_with_values: ds_callback_with_values.presence
9
+ } do %>
10
+ <%= tag.input type: 'hidden', id: "last_dyna_span_val_#{unique_ref_id}", value: value, autocomplete: 'off' %>
11
+ <%= tag.div id: "dyna_span_div#{unique_ref_id}", class: 'dyna-span-form', style: 'display:none;' do %>
12
+ <%= form_for(master_ds_object, **form_for_options) do |f| %>
13
+ <% if association %>
14
+ <%= f.fields_for association[:name], (association[:collection] ? [attr_object] : attr_object) do |nested| %>
15
+ <%= dynaspan_input(nested, kind, attrib, local_assigns) %>
16
+ <% hidden_fields.each do |name, hidden_value| %>
17
+ <%= nested.hidden_field name, value: hidden_value %>
18
+ <% end %>
19
+ <% end %>
20
+ <% else %>
21
+ <%= dynaspan_input(f, kind, attrib, local_assigns) %>
22
+ <% hidden_fields.each do |name, hidden_value| %>
23
+ <%= f.hidden_field name, value: hidden_value %>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+ <% end %>
28
+ <%= tag.span display_text, id: "dyna_span_span#{unique_ref_id}", class: 'dyna-span dyna-span-text', style: 'display:block;', tabindex: 0 %>
29
+ <% if dyna_span_edit_text %>
30
+ <%= tag.div dyna_span_edit_text, class: 'dyna-span-edit-text pull-right', style: 'cursor:pointer;' %>
31
+ <% end %>
32
+ <% end %>
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin 'dynaspan', to: 'dynaspan/dynaspan.js'
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+
5
+ module Dynaspan
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Dynaspan
8
+
9
+ ASSETS = %w[dynaspan/dynaspan.js dynaspan/dynaspan.css].freeze
10
+
11
+ # Make the view helpers available in every view of the host application.
12
+ initializer 'dynaspan.helpers' do
13
+ ActiveSupport.on_load(:action_view) do
14
+ include Dynaspan::ApplicationHelper
15
+ end
16
+ end
17
+
18
+ # Sprockets needs to be told about stand-alone assets. Propshaft serves
19
+ # everything in the engine's app/assets directories automatically.
20
+ initializer 'dynaspan.assets' do |app|
21
+ if app.config.respond_to?(:assets)
22
+ precompile = app.config.assets.precompile
23
+ precompile.concat(ASSETS) if precompile.respond_to?(:concat)
24
+ end
25
+ end
26
+
27
+ # With importmap-rails, `import "dynaspan"` works without any extra pins.
28
+ initializer 'dynaspan.importmap', before: 'importmap' do |app|
29
+ app.config.importmap.paths << root.join('config/importmap.rb') if app.config.respond_to?(:importmap)
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynaspan
2
- VERSION = '0.1.4'
4
+ VERSION = '1.0.0'
3
5
  end
data/lib/dynaspan.rb CHANGED
@@ -1,32 +1,8 @@
1
- require 'dynaspan/version'
1
+ # frozen_string_literal: true
2
2
 
3
+ require 'dynaspan/version'
4
+ require 'dynaspan/engine'
3
5
 
6
+ # Click-to-edit, in-place AJAX text editing for Rails.
4
7
  module Dynaspan
5
- ASSETS = %W(dynaspan/*.js dynaspan/*.css dynaspan/*.png dynaspan/*.gif dynaspan/*.html dynaspan/*.md)
6
- INITIALIZER = "initializer 'dynaspan.assets.precompile', group: :all do |app| app.config.assets.precompile += Dynaspan::ASSETS; end"
7
-
8
- class Engine < ::Rails::Engine
9
- isolate_namespace Dynaspan
10
-
11
- eval Dynaspan::INITIALIZER
12
-
13
- initializer 'dynaspan.action_controller' do |app|
14
- ActiveSupport.on_load :action_controller do
15
- helper Dynaspan::ApplicationHelper
16
- end
17
- end
18
-
19
- if ::Rails.version.to_s =~ /^4/
20
- rake_tasks do
21
- load "dynaspan/tasks.rake"
22
- end
23
- end
24
- end
25
-
26
- if ::Rails.version.to_s =~ /^3\.[0]/
27
- class Railtie < ::Rails::Railtie
28
- eval Dynaspan::INITIALIZER
29
- end
30
- end
31
8
  end
32
-