cck_forms 3.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.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/README.md +1 -0
- data/Rakefile +40 -0
- data/lib/cck_forms/date_time.rb +58 -0
- data/lib/cck_forms/engine.rb +23 -0
- data/lib/cck_forms/form_builder_extensions.rb +18 -0
- data/lib/cck_forms/parameter_type_class/album.rb +100 -0
- data/lib/cck_forms/parameter_type_class/base.rb +275 -0
- data/lib/cck_forms/parameter_type_class/boolean.rb +24 -0
- data/lib/cck_forms/parameter_type_class/checkboxes.rb +202 -0
- data/lib/cck_forms/parameter_type_class/date.rb +35 -0
- data/lib/cck_forms/parameter_type_class/date_range.rb +53 -0
- data/lib/cck_forms/parameter_type_class/date_time.rb +76 -0
- data/lib/cck_forms/parameter_type_class/enum.rb +53 -0
- data/lib/cck_forms/parameter_type_class/file.rb +80 -0
- data/lib/cck_forms/parameter_type_class/float.rb +20 -0
- data/lib/cck_forms/parameter_type_class/image.rb +20 -0
- data/lib/cck_forms/parameter_type_class/integer.rb +77 -0
- data/lib/cck_forms/parameter_type_class/integer_range.rb +150 -0
- data/lib/cck_forms/parameter_type_class/map.rb +259 -0
- data/lib/cck_forms/parameter_type_class/phones.rb +204 -0
- data/lib/cck_forms/parameter_type_class/string.rb +13 -0
- data/lib/cck_forms/parameter_type_class/string_collection.rb +23 -0
- data/lib/cck_forms/parameter_type_class/text.rb +18 -0
- data/lib/cck_forms/parameter_type_class/time.rb +30 -0
- data/lib/cck_forms/parameter_type_class/work_hours.rb +369 -0
- data/lib/cck_forms/version.rb +3 -0
- data/lib/cck_forms.rb +4 -0
- data/vendor/assets/javascripts/cck_forms/jquery.workhours.js +399 -0
- data/vendor/assets/javascripts/cck_forms/map.js.coffee +322 -0
- metadata +116 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::IntegerRange # Rover :)
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
def self.name
|
5
|
+
'Диапазон между двумя целыми числами'
|
6
|
+
end
|
7
|
+
|
8
|
+
# {from: 500, to: 1000, ranges: {"300-600" => true, "601-900" => true, "901-1500" => false}}
|
9
|
+
def mongoize
|
10
|
+
value_from_form = value
|
11
|
+
return nil if value_from_form.blank?
|
12
|
+
|
13
|
+
from = value_from_form.try(:[], 'from').to_i
|
14
|
+
till = value_from_form.try(:[], 'till').to_i
|
15
|
+
|
16
|
+
db_representation = {
|
17
|
+
from: from,
|
18
|
+
till: till,
|
19
|
+
ranges: {}
|
20
|
+
}
|
21
|
+
|
22
|
+
if @extra_options[:ranges].respond_to? :each
|
23
|
+
@extra_options[:ranges].each do |range_string|
|
24
|
+
low, high = range_string.split(range_string_delimiter)
|
25
|
+
if high.to_i.to_s != high.to_s
|
26
|
+
high = Integer::MAX_32BIT
|
27
|
+
end
|
28
|
+
low, high = low.to_i, high.to_i
|
29
|
+
|
30
|
+
# -----
|
31
|
+
# [ RANGE ]
|
32
|
+
completely_in_range = (from >= low && till <= high)
|
33
|
+
|
34
|
+
# -------
|
35
|
+
# [ RANGE ]
|
36
|
+
#
|
37
|
+
# ------
|
38
|
+
# [ RANGE ]
|
39
|
+
intersects_range_partially = (from <= low && till >= low) || (from <= high && till >= high)
|
40
|
+
|
41
|
+
# -----------
|
42
|
+
# [ RANGE ]
|
43
|
+
contains_range = from < low && till > high
|
44
|
+
|
45
|
+
db_representation[:ranges][range_string] = completely_in_range || intersects_range_partially || contains_range
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
db_representation
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s(options = {})
|
53
|
+
options ||= {}
|
54
|
+
return '' if value.blank?
|
55
|
+
|
56
|
+
delimiter = options[:delimeter].presence || default_integer_range_delimiter
|
57
|
+
|
58
|
+
from = value.try(:[], 'from').to_i
|
59
|
+
till = value.try(:[], 'till').to_i
|
60
|
+
|
61
|
+
return '' if from.zero? && till.zero?
|
62
|
+
|
63
|
+
if from.zero?
|
64
|
+
"до #{till}"
|
65
|
+
elsif till.zero?
|
66
|
+
"от #{from}"
|
67
|
+
elsif from == till
|
68
|
+
from.to_s
|
69
|
+
else
|
70
|
+
[from, till].join(delimiter)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_form(form_builder, options)
|
75
|
+
set_value_in_hash options
|
76
|
+
if options.delete(:for) == :search
|
77
|
+
build_search_form(form_builder, options)
|
78
|
+
else
|
79
|
+
build_for_admin_interface_form(form_builder, options)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def search(criteria, field, query)
|
84
|
+
criteria.where("#{field}.ranges.#{query}" => true)
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def build_for_admin_interface_form(form_builder, options)
|
92
|
+
delimiter = options[:delimeter].presence || ' — '
|
93
|
+
|
94
|
+
default_style = {class: 'form-control input-small'}
|
95
|
+
result = ['<div class="form-inline">']
|
96
|
+
form_builder.fields_for :value do |ff|
|
97
|
+
from_field = ff.number_field :from, options.merge(value: value.try(:[], 'from')).reverse_merge(default_style)
|
98
|
+
till_field = ff.number_field :till, options.merge(value: value.try(:[], 'till')).reverse_merge(default_style)
|
99
|
+
result << [from_field, till_field].join(delimiter).html_safe
|
100
|
+
end
|
101
|
+
result << '</div>'
|
102
|
+
result.join.html_safe
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_search_form(form_builder, options)
|
106
|
+
delimiter = options[:delimeter].presence || default_integer_range_delimiter
|
107
|
+
form_fields = []
|
108
|
+
visual_representation = options.delete(:as)
|
109
|
+
show_only = options.delete(:only)
|
110
|
+
|
111
|
+
if visual_representation == :select
|
112
|
+
klazz = options.delete :class
|
113
|
+
form_fields << form_builder.select(:value, [['', '']] + humanized_integer_ranges_for_select, options.merge(selected: options[:value]), {class: klazz} )
|
114
|
+
else
|
115
|
+
show_all_fields = !show_only
|
116
|
+
|
117
|
+
if show_all_fields or show_only == :low
|
118
|
+
form_fields << form_builder.text_field(:from, options.merge(index: 'value', value: value.try(:[], 'from')))
|
119
|
+
end
|
120
|
+
|
121
|
+
if show_all_fields or show_only == :high
|
122
|
+
form_fields << form_builder.text_field(:till, options.merge(index: 'value', value: value.try(:[], 'till')))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
form_fields.join(delimiter).html_safe
|
127
|
+
end
|
128
|
+
|
129
|
+
def default_integer_range_delimiter
|
130
|
+
"–"
|
131
|
+
end
|
132
|
+
|
133
|
+
def humanized_integer_ranges_for_select
|
134
|
+
@extra_options[:ranges].map do |range_string|
|
135
|
+
low, high = range_string.split(range_string_delimiter)
|
136
|
+
if low.to_i.to_s != low.to_s
|
137
|
+
option_text = "до #{high}"
|
138
|
+
elsif high.to_i.to_s != high.to_s
|
139
|
+
option_text = "свыше #{low}"
|
140
|
+
else
|
141
|
+
option_text = [low, high].join(default_integer_range_delimiter)
|
142
|
+
end
|
143
|
+
[option_text, range_string]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def range_string_delimiter
|
148
|
+
/[-:\\]/
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::Map
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
MAP_TYPE_GOOGLE = 'google'.freeze
|
5
|
+
MAP_TYPE_YANDEX = 'yandex'.freeze
|
6
|
+
DEFAULT_MAP_TYPE = MAP_TYPE_GOOGLE
|
7
|
+
|
8
|
+
mattr_accessor :map_providers
|
9
|
+
@@map_providers = [MAP_TYPE_YANDEX, MAP_TYPE_GOOGLE]
|
10
|
+
|
11
|
+
mattr_accessor :google_maps_api_key
|
12
|
+
|
13
|
+
def self.name
|
14
|
+
'Точка на карте'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Было базе: {latlon: [x, y], zoom: z}
|
18
|
+
#
|
19
|
+
# Стало в модели: {
|
20
|
+
# latitude: x,
|
21
|
+
# longitude: y,
|
22
|
+
# zoom: z
|
23
|
+
# }
|
24
|
+
def self.demongoize_value(value, parameter_type_class=nil)
|
25
|
+
value = value.to_h
|
26
|
+
value.stringify_keys!
|
27
|
+
latlon = value['latlon'] || []
|
28
|
+
|
29
|
+
latitude = value['latitude'] || latlon[0]
|
30
|
+
longitude = value['longitude'] || latlon[1]
|
31
|
+
type_of_map = value['type'] || DEFAULT_MAP_TYPE
|
32
|
+
|
33
|
+
{
|
34
|
+
'latitude' => latitude,
|
35
|
+
'longitude' => longitude,
|
36
|
+
'zoom' => value['zoom'].presence,
|
37
|
+
'type' => type_of_map
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Было в модели: {
|
43
|
+
# latitude: x,
|
44
|
+
# longitude: y,
|
45
|
+
# zoom: z
|
46
|
+
# }
|
47
|
+
#
|
48
|
+
# Стало в базе: {latlon: [x, y], zoom: z}
|
49
|
+
def mongoize
|
50
|
+
value = self.value.is_a?(Hash) ? self.value : {}
|
51
|
+
return {
|
52
|
+
'latlon' => [value['latitude'].presence, value['longitude'].presence],
|
53
|
+
'zoom' => value['zoom'].presence,
|
54
|
+
'type' => value['type'].presence || DEFAULT_MAP_TYPE
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
# Если переданы :width и :height, вызовет img_tag, иначе вернет пустую строку.
|
59
|
+
def to_s(options = {})
|
60
|
+
options ||= {}
|
61
|
+
if options[:width].to_i > 0 and options[:height].to_i > 0
|
62
|
+
return a_tag(to_s(options.except :link), options[:link]) if options[:link]
|
63
|
+
return img_tag options[:width], options[:height]
|
64
|
+
end
|
65
|
+
|
66
|
+
''
|
67
|
+
end
|
68
|
+
|
69
|
+
# Возвращает тэг IMG с картинкой карты и точкой на ней, если value не пустое (содержит координаты точки).
|
70
|
+
# См. Google/Yandex Maps Static API.
|
71
|
+
def img_tag(width, height, options = {})
|
72
|
+
map_type = value['type']
|
73
|
+
|
74
|
+
if value['latitude'].present? and value['longitude'].present?
|
75
|
+
if map_type == MAP_TYPE_GOOGLE
|
76
|
+
zoom_if_any = value['zoom'].present? ? "&zoom=#{value['zoom']}" : nil
|
77
|
+
marker_size_if_any = options[:marker_size] ? "|size:#{options[:marker_size]}" : nil
|
78
|
+
|
79
|
+
url = %Q(
|
80
|
+
http://maps.googleapis.com/maps/api/staticmap?
|
81
|
+
language=ru&
|
82
|
+
size=#{width}x#{height}&
|
83
|
+
maptype=roadmap&
|
84
|
+
markers=color:red#{marker_size_if_any}|
|
85
|
+
#{value['latitude']},#{value['longitude']}&
|
86
|
+
sensor=false
|
87
|
+
#{zoom_if_any}
|
88
|
+
).gsub(/\s+/, '')
|
89
|
+
|
90
|
+
else # yandex
|
91
|
+
zoom_if_any = value['zoom'].present? ? "&z=#{value['zoom']}" : nil
|
92
|
+
marker_size = options[:marker_size] == :large ? 'l' : 'm'
|
93
|
+
|
94
|
+
url = %Q(
|
95
|
+
http://static-maps.yandex.ru/1.x/?
|
96
|
+
l=map&
|
97
|
+
size=#{width},#{height}&
|
98
|
+
pt=#{value['longitude']},#{value['latitude']},pm2bl#{marker_size}&
|
99
|
+
#{zoom_if_any}
|
100
|
+
).gsub(/\s+/, '')
|
101
|
+
end
|
102
|
+
%Q(<img src="#{url}" width="#{width}" height="#{height}" />).html_safe
|
103
|
+
else
|
104
|
+
''
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Возвращает тэг A со ссылкой на карту с маркером объекта.
|
109
|
+
def a_tag(content, attrs)
|
110
|
+
if attrs[:href] = url
|
111
|
+
attrs_strings = []
|
112
|
+
attrs.each_pair { |name, value| attrs_strings << sprintf('%s="%s"', name, value) }
|
113
|
+
sprintf '<a %s>%s</a>', attrs_strings.join, content
|
114
|
+
else
|
115
|
+
''
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Возвращает урл на карту.
|
120
|
+
def url
|
121
|
+
if value['latitude'].present? and value['longitude'].present?
|
122
|
+
if value['type'] == MAP_TYPE_GOOGLE
|
123
|
+
sprintf(
|
124
|
+
'http://maps.google.com/maps?%s&t=m&q=%s+%s',
|
125
|
+
value['zoom'].present? ? 'z=' + value['zoom'] : '',
|
126
|
+
value['latitude'],
|
127
|
+
value['longitude']
|
128
|
+
)
|
129
|
+
else # yandex
|
130
|
+
sprintf(
|
131
|
+
'http://maps.yandex.ru/?l=map&text=%s&ll=%s%s',
|
132
|
+
[value['latitude'], value['longitude']].join(','),
|
133
|
+
[value['longitude'], value['latitude']].join(','),
|
134
|
+
value['zoom'].present? ? '&z=' + value['zoom'] : nil
|
135
|
+
)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Построим форму для карты. Представляет собой 3 спрятанных поля latitude, longitude, zoom. Затем рядом ставится ДИВ,
|
141
|
+
# на который вешается карта Гугла, и с помощью скриптов изменение полей привязывается к кликам на карте и изменению
|
142
|
+
# масштаба.
|
143
|
+
#
|
144
|
+
# 1 клик на пустом месте ставит точку (пишем в поля), старая точка при этом удаляется. Клик на точке удаляет ее
|
145
|
+
# (очищаем поля).
|
146
|
+
#
|
147
|
+
# Также, слушает событие change поля "город" (facility_page_cck_params_city_value), чтобы по известным названиям
|
148
|
+
# городов отцентровать карту на выбранном городе.
|
149
|
+
#
|
150
|
+
# options:
|
151
|
+
#
|
152
|
+
# value - тек. значение, см. self.demongoize_value
|
153
|
+
# width - ширина карты
|
154
|
+
# height - высота карты
|
155
|
+
# latitude - широта центра карты, если ничего не выбрано
|
156
|
+
# longitude - долгота центра карты, если ничего не выбрано
|
157
|
+
# zoom - масштаб карты, если ничего не выбрано
|
158
|
+
|
159
|
+
def build_form(form_builder, options)
|
160
|
+
set_value_in_hash options
|
161
|
+
|
162
|
+
options = {
|
163
|
+
width: 550,
|
164
|
+
height: 400,
|
165
|
+
latitude: 47.757581,
|
166
|
+
longitude: 67.298256,
|
167
|
+
zoom: 5,
|
168
|
+
value: {},
|
169
|
+
}.merge options
|
170
|
+
|
171
|
+
value = (options[:value].is_a? Hash) ? options[:value].stringify_keys : {}
|
172
|
+
|
173
|
+
inputs = []
|
174
|
+
id = ''
|
175
|
+
|
176
|
+
form_builder.tap do |value_builder|
|
177
|
+
id = form_builder_name_to_id value_builder
|
178
|
+
inputs << value_builder.hidden_field(:latitude, value: value['latitude'])
|
179
|
+
inputs << value_builder.hidden_field(:longitude, value: value['longitude'])
|
180
|
+
inputs << value_builder.hidden_field(:zoom, value: value['zoom'])
|
181
|
+
inputs << value_builder.hidden_field(:type, value: value['type'])
|
182
|
+
end
|
183
|
+
|
184
|
+
city_id = id.clone
|
185
|
+
city_id['map'] = 'city'
|
186
|
+
city_id += '_value'
|
187
|
+
|
188
|
+
cities_js = []
|
189
|
+
city_class = if defined?(KazakhstanCities::City) then KazakhstanCities::City elsif defined?(City) then City end
|
190
|
+
if city_class
|
191
|
+
city_class.all.each do |city|
|
192
|
+
cities_js.push [city.id, {lat: city.lat.to_f, lon: city.lon.to_f, zoom: city.zoom.to_i}]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
cities_js = Hash[cities_js].to_json
|
196
|
+
|
197
|
+
allowed_maps = @@map_providers
|
198
|
+
map_names = {'google' => 'Google', 'yandex' => 'Яндекс'}
|
199
|
+
selected_map_type = value['type'].in?(allowed_maps) ? value['type'] : allowed_maps.first
|
200
|
+
|
201
|
+
switchers = []
|
202
|
+
switchers << %Q|<div class="btn-group cck-map-switchers #{'hide' if allowed_maps.count < 2}" style="margin-top: 5px;">|
|
203
|
+
allowed_maps.map do |map|
|
204
|
+
switchers << %Q|<a class="btn btn-default #{selected_map_type == map ? 'active' : nil}" href="#" data-map-type="#{map}">#{map_names[map]}</a>|
|
205
|
+
end
|
206
|
+
switchers << '</div>'
|
207
|
+
|
208
|
+
map_html_containers = []
|
209
|
+
allowed_maps.each do |map|
|
210
|
+
map_html_containers.push %Q|<div id="#{id}_#{map}" data-id=#{id} class="map_widget" style="display: none; width: #{options[:width]}px; height: #{options[:height]}px"></div>|
|
211
|
+
end
|
212
|
+
|
213
|
+
api_key = @@google_maps_api_key.present? ? "&key=#{@@google_maps_api_key}" : nil
|
214
|
+
|
215
|
+
%Q|
|
216
|
+
<div class="map-canvas">
|
217
|
+
#{inputs.join}
|
218
|
+
|
219
|
+
<script>
|
220
|
+
var mapsReady = {google: false, yandex: false, callback: null, on: function(callback) {
|
221
|
+
this.callback = callback;
|
222
|
+
this.fireIfReady();
|
223
|
+
}, fireIfReady: function() {
|
224
|
+
if(this.google && this.yandex && this.callback) { this.callback() }
|
225
|
+
}}
|
226
|
+
|
227
|
+
function googleMapReady() { mapsReady.google = true; mapsReady.fireIfReady() }
|
228
|
+
function yandexMapReady() { mapsReady.yandex = true; mapsReady.fireIfReady() }
|
229
|
+
|
230
|
+
function loadMapScripts() {
|
231
|
+
var script;
|
232
|
+
script = document.createElement('script');
|
233
|
+
script.type = 'text/javascript';
|
234
|
+
script.src = 'https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=googleMapReady#{api_key}';
|
235
|
+
document.body.appendChild(script);
|
236
|
+
|
237
|
+
script = document.createElement('script');
|
238
|
+
script.type = 'text/javascript';
|
239
|
+
script.src = 'https://api-maps.yandex.ru/2.0/?coordorder=longlat&load=package.full&wizard=constructor&lang=ru-RU&onload=yandexMapReady';
|
240
|
+
document.body.appendChild(script);
|
241
|
+
}
|
242
|
+
|
243
|
+
window.onload = loadMapScripts;
|
244
|
+
</script>
|
245
|
+
|
246
|
+
<div data-map-data-source data-options='#{options.to_json}' data-id="#{id}" data-cities='#{cities_js}' data-cityid="#{city_id}" data-allowed-maps='#{allowed_maps.to_json}' style="width: #{options[:width]}px; height: #{options[:height]}px">
|
247
|
+
#{map_html_containers.join}
|
248
|
+
</div>
|
249
|
+
|
250
|
+
#{switchers.join}
|
251
|
+
</div>
|
252
|
+
|
|
253
|
+
end
|
254
|
+
|
255
|
+
def to_diff_value(options = {})
|
256
|
+
demongoize_value!
|
257
|
+
img_tag(64, 64, marker_size: :small)
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::Phones
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
MIN_PHONES_IN_FORM = Rails.application.config.cck_forms.phones.min_phones_in_form
|
5
|
+
MOBILE_CODES = Rails.application.config.cck_forms.phones.mobile_codes
|
6
|
+
PREFIX = Rails.application.config.cck_forms.phones.prefix
|
7
|
+
NUMBER_PARTS_GLUE = Rails.application.config.cck_forms.phones.number_parts_glue
|
8
|
+
|
9
|
+
def self.name
|
10
|
+
'Телефоны'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Проверяет входной массив на наличие телефонов (хэша с ключами prefix, code и number).
|
14
|
+
# На выходе выдает очищенный массив хэшей с такими ключами, пропуская пустые телефоны.
|
15
|
+
#
|
16
|
+
# Было в модели: [{prefix: '+7'}, {code: ' 123 ', number: '1234567', zzz: ''}]
|
17
|
+
#
|
18
|
+
# Стало в базе: [{prefix: '', code: '123', number: '1234567'}]
|
19
|
+
def mongoize
|
20
|
+
value = self.value
|
21
|
+
return [] unless value.respond_to? :each
|
22
|
+
|
23
|
+
value = value.values if value.is_a? Hash
|
24
|
+
|
25
|
+
result = []
|
26
|
+
value.each do |phone|
|
27
|
+
phone = {} if phone.blank? or !(phone.is_a? Hash)
|
28
|
+
phone = blank_phone.merge phone.stringify_keys
|
29
|
+
|
30
|
+
phone['prefix'] = phone['prefix'].strip
|
31
|
+
phone['code'] = clean_numbers(phone['code'].to_s)
|
32
|
+
phone['number'] = clean_numbers(phone['number'].to_s)
|
33
|
+
|
34
|
+
if phone['code'].present? or phone['number'].present?
|
35
|
+
result << {
|
36
|
+
'prefix' => phone['prefix'],
|
37
|
+
'code' => phone['code'],
|
38
|
+
'number' => phone['number'],
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
# Просто приводит телефоны в базе в соответствие формату.
|
47
|
+
def self.demongoize_value(value, parameter_type_class=nil)
|
48
|
+
if value
|
49
|
+
value.map do |phone|
|
50
|
+
phone = phone.stringify_keys!
|
51
|
+
{
|
52
|
+
'prefix' => phone['prefix'],
|
53
|
+
'code' => phone['code'],
|
54
|
+
'number' => phone['number'],
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Строит форму для заполнения телефонов. В ней будет минимум MIN_PHONES_IN_FORM телефонов, если их меньше, остальные
|
61
|
+
# будут добавлены пустые.
|
62
|
+
#
|
63
|
+
# Также, если заполнены все MIN_PHONES_IN_FORM, добавит 1 пустое поле, чтобы можно было добавить новый телефон.
|
64
|
+
#
|
65
|
+
# Возвращает ХТМЛ.
|
66
|
+
def build_form(form_builder, options)
|
67
|
+
set_value_in_hash options
|
68
|
+
value = options[:value].presence
|
69
|
+
value = [] unless !value.blank? and value.is_a? Array
|
70
|
+
|
71
|
+
result = value.map { |phone| build_single_form(form_builder, phone) }
|
72
|
+
|
73
|
+
[1, CckForms::ParameterTypeClass::Phones::MIN_PHONES_IN_FORM - result.length].max.times { result << build_single_form(form_builder, {}) }
|
74
|
+
|
75
|
+
id = form_builder_name_to_id form_builder
|
76
|
+
sprintf '<div id="%s">%s</div>%s', id, result.join, script(id)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Строит форму для 1 телефона из значения вида {prefix: '', code: '', number: ''}
|
80
|
+
def build_single_form(form_builder, phone)
|
81
|
+
phone = {} unless phone.is_a? Hash
|
82
|
+
phone = blank_phone.merge phone
|
83
|
+
|
84
|
+
phone_form = []
|
85
|
+
|
86
|
+
form_builder.fields_for(:value, index: '') do |phone_builder|
|
87
|
+
phone_form << phone_builder.text_field(:prefix, class: 'input-tiny form-control', value: phone['prefix'])
|
88
|
+
phone_form << phone_builder.text_field(:code, class: 'input-mini form-control', value: phone['code'])
|
89
|
+
phone_form << phone_builder.text_field(:number, class: 'input-small form-control', value: phone['number'])
|
90
|
+
end
|
91
|
+
|
92
|
+
sprintf '<p class="form-inline">%s — %s — %s</p>', phone_form[0], phone_form[1], phone_form[2]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Возвращает 1 пустой телефон (хэш вида {prefix: '+7', code: '', number: ''}). Так сказать, эталон.
|
96
|
+
def blank_phone
|
97
|
+
{
|
98
|
+
'prefix' => PREFIX,
|
99
|
+
'code' => '',
|
100
|
+
'number' => '',
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def script(id)
|
105
|
+
<<HTML
|
106
|
+
<script type="text/javascript">
|
107
|
+
$(function() {
|
108
|
+
var $phones = $("##{id}");
|
109
|
+
var doTimes = #{CckForms::ParameterTypeClass::Phones::MIN_PHONES_IN_FORM};
|
110
|
+
|
111
|
+
var createPhone = function() {
|
112
|
+
var $newPhone = $phones.children("p:last").clone();
|
113
|
+
$newPhone.children("input").each(function() {
|
114
|
+
var $this = $(this);
|
115
|
+
var isPrefix = $this.prop('name').match(/\\[prefix\\]$/);
|
116
|
+
$this.val(isPrefix ? "#{blank_phone['prefix']}" : '');
|
117
|
+
var index = $this.prop("id").match(/value_([0-9]+)_/);
|
118
|
+
if(!index) {
|
119
|
+
return;
|
120
|
+
}
|
121
|
+
index = index[1] * 1;
|
122
|
+
$this.prop("id", $this.prop("id").replace(index, index + 1));
|
123
|
+
$this.prop("name", $this.prop("name").replace(index, index + 1));
|
124
|
+
})
|
125
|
+
$phones.children("p:last").after($newPhone);
|
126
|
+
}
|
127
|
+
|
128
|
+
$phones.append('<a href="#" class="add_more">Добавить еще телефонов</a>');
|
129
|
+
$phones.children(".add_more").click(function() {
|
130
|
+
for(var i = 0; i < doTimes; ++ i) {
|
131
|
+
createPhone();
|
132
|
+
}
|
133
|
+
return false;
|
134
|
+
})
|
135
|
+
});
|
136
|
+
</script>
|
137
|
+
HTML
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_html(options = {})
|
141
|
+
phones_list = []
|
142
|
+
(value || []).each do |phone|
|
143
|
+
if phone['number'] && clean_numbers(phone['number'].to_s).present?
|
144
|
+
if phone['prefix'].present? || phone['code'].present?
|
145
|
+
prefix = phone['prefix'].present? ? "<span class=\"phone-prefix\">#{phone['prefix']}</span>" : ''
|
146
|
+
code = phone['code'].present? ? "<span class=\"phone-code\">#{phone['code']}</span>" : ''
|
147
|
+
start = sprintf(phone['code'].in?(MOBILE_CODES) ? '<span class="phone-mobile-prefix">%s(%s)</span>' : '<span class="phone-city-prefix">%s(%s)</span>', prefix, code)
|
148
|
+
else
|
149
|
+
start = ''
|
150
|
+
end
|
151
|
+
|
152
|
+
number = split_number(clean_numbers(phone['number'])).join(NUMBER_PARTS_GLUE)
|
153
|
+
phones_list << sprintf('<span class="phone">%s<span class="phone-number">%s</span></span>', start, number)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
phones_list = phones_list.take(options[:limit]) if options[:limit]
|
158
|
+
|
159
|
+
if options[:as_list]
|
160
|
+
phones_list
|
161
|
+
else
|
162
|
+
phones_list.join(', ').html_safe
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def to_s(options = {})
|
167
|
+
HTML::FullSanitizer.new.sanitize to_html(options)
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# 123-45-67 asdasd -> 1234567
|
175
|
+
def clean_numbers(number)
|
176
|
+
number.gsub /\D/, ''
|
177
|
+
end
|
178
|
+
|
179
|
+
# 1234567 -> 123 45 67 с тэгами
|
180
|
+
def split_number(number)
|
181
|
+
if number.length > 4
|
182
|
+
tokens = []
|
183
|
+
|
184
|
+
# перевернем строку и разобъем на пары
|
185
|
+
number.reverse.scan(/.(?:.|$)/) do |token|
|
186
|
+
token.reverse!
|
187
|
+
if token.length == 1
|
188
|
+
tokens.last.prepend token
|
189
|
+
else
|
190
|
+
tokens << token
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# сольем все обратно
|
195
|
+
tokens.reverse!
|
196
|
+
tokens.tap do |tokens|
|
197
|
+
tokens.map! { |token| yield token } if block_given?
|
198
|
+
end
|
199
|
+
else
|
200
|
+
yield number if block_given?
|
201
|
+
[number]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::String
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
def self.name
|
5
|
+
'Строка'
|
6
|
+
end
|
7
|
+
|
8
|
+
def build_form(form_builder, options)
|
9
|
+
set_value_in_hash options
|
10
|
+
attrs = @extra_options.slice(:maxlength, :pattern)
|
11
|
+
form_builder.text_field :value, {class: 'form-control'}.merge(attrs).merge(options)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::StringCollection
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
def self.name
|
5
|
+
'Массив строк'
|
6
|
+
end
|
7
|
+
|
8
|
+
def mongoize
|
9
|
+
value.split "\r\n" if value.is_a? String
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.demongoize_value(value, parameter_type_class=nil)
|
13
|
+
value = [value] if value.is_a? String
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_form(form_builder, options)
|
18
|
+
set_value_in_hash options
|
19
|
+
options[:value] = value.join("\r\n") if value
|
20
|
+
|
21
|
+
form_builder.text_area :value, {cols: 50, rows: 5, class: 'form-control'}.merge(options)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CckForms::ParameterTypeClass::Text
|
2
|
+
include CckForms::ParameterTypeClass::Base
|
3
|
+
|
4
|
+
def self.name
|
5
|
+
'Текст'
|
6
|
+
end
|
7
|
+
|
8
|
+
def build_form(form_builder, options)
|
9
|
+
set_value_in_hash options
|
10
|
+
form_builder.text_area :value, {cols: 50, rows: 5, class: 'form-control'}.merge(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_diff_value(options = {})
|
14
|
+
to_html.presence.try do |html|
|
15
|
+
"<div class='well well-small'>#{html}</div>".html_safe
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|