+-> move/copy list available resolutions code to get_config
+-> bug: adding a remote device does not provide the available resolutions
+
+-> bug: if updating a remote camera config, local motion will get restarted
-> make camera frames positions configurable
-> hide horrible 404 image on cameras
-> prevent Request closed errors by stopping mjpg clients before stopping motion
data = _conf_to_dict(lines)
# determine the enabled status
- main_config = get_main()
- threads = main_config.get('thread', [])
- data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
- data['@id'] = camera_id
-
- _set_default_motion_camera(data)
+ if data['@proto'] == 'v4l2':
+ main_config = get_main()
+ threads = main_config.get('thread', [])
+ data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
+ data['@id'] = camera_id
+
+ _set_default_motion_camera(data)
return data
def set_camera(camera_id, data):
# TODO use a cache
- _set_default_motion_camera(data)
-
- # set the enabled status in main config
- main_config = get_main()
- threads = main_config.setdefault('thread', [])
- config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
- if data['@enabled'] and config_file_name not in threads:
- threads.append(config_file_name)
-
- elif not data['@enabled']:
- threads = [t for t in threads if t != config_file_name]
-
- main_config['thread'] = threads
-
- set_main(main_config)
+ if data.get('@proto') == 'v4l2':
+ _set_default_motion_camera(data)
+
+ # set the enabled status in main config
+ main_config = get_main()
+ threads = main_config.setdefault('thread', [])
+ config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
+ if data['@enabled'] and config_file_name not in threads:
+ threads.append(config_file_name)
+
+ elif not data['@enabled']:
+ threads = [t for t in threads if t != config_file_name]
+
+ main_config['thread'] = threads
+
+ set_main(main_config)
- del data['@enabled']
-
# read the actual configuration from file
config_file_path = _CAMERA_CONFIG_FILE_PATH % {'id': camera_id}
if os.path.isfile(config_file_path):
return data
-def add_camera(device):
+def add_camera(device_details):
# TODO use a cache
+ device = device_details.get('device')
+ proto = device_details.get('proto')
+
# determine the last camera id
camera_ids = get_camera_ids()
logging.info('adding new camera with id %(id)s...' % {'id': camera_id})
- # get device type
- proto = None
- if device.count('://'):
- proto, device = device.split('://', 1)
-
# add the default camera config
data = OrderedDict()
data['@name'] = 'Camera' + str(camera_id)
data['@proto'] = proto
data['@enabled'] = True
- data['videodevice'] = device
-
- # find a suitable resolution
- for (w, h) in v4l2ctl.list_resolutions(device):
- if w > 300:
- data['width'] = w
- data['height'] = h
- break
+ for k, v in device_details.items():
+ data['@' + k] = v
+
+ if proto == 'v4l2':
+ data['videodevice'] = device
+ # find a suitable resolution
+ for (w, h) in v4l2ctl.list_resolutions(device): # TODO move/copy this code to handler/get_config
+ if w > 300:
+ data['width'] = w
+ data['height'] = h
+ break
+
# write the configuration to file
set_camera(camera_id, data)
data.setdefault('quality', 75)
data.setdefault('@preserve_images', 0)
- data.setdefault('motion_movies', False)
data.setdefault('ffmpeg_variable_bitrate', 14)
data.setdefault('movie_filename', '%Y-%m-%d-%H-%M-%S')
data.setdefault('ffmpeg_cap_new', False)
data.setdefault('@motion_notifications_emails', '')
data.setdefault('@working_schedule', '')
-
import config
import mjpgclient
import motionctl
+import remote
import template
import v4l2ctl
raise HTTPError(404, 'no such camera')
camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] != 'v4l2':
+ try:
+ remote_data = remote.get_config(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'))
+
+ except Exception as e:
+ return self.finish_json({'error': unicode(e)})
+
+ remote_data = self._camera_ui_to_dict(remote_data)
+
+ camera_config.update(remote_data)
+
ui_config = self._camera_dict_to_ui(camera_config)
- resolutions = v4l2ctl.list_resolutions(camera_config['videodevice'])
- resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
- ui_config['available_resolutions'] = resolutions
+ if camera_config['@proto'] == 'v4l2':
+ resolutions = v4l2ctl.list_resolutions(camera_config['videodevice'])
+ resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
+ ui_config['available_resolutions'] = resolutions
+
self.finish_json(ui_config)
else:
raise
- restart = bool(data.get('restart'))
+ restart = bool(data.get('last'))
if restart and motionctl.running():
motionctl.stop()
camera_ids = config.get_camera_ids()
if camera_id not in camera_ids:
raise HTTPError(404, 'no such camera')
-
- data = self._camera_ui_to_dict(data)
- config.set_camera(camera_id, data)
+
+ camera_config = config.get_camera(camera_id)
+ if camera_config['@proto'] == 'v4l2':
+ data = self._camera_ui_to_dict(data)
+ config.set_camera(camera_id, data)
+
+ else:
+ remote.set_config(
+ camera_config.get('@host'),
+ camera_config.get('@port'),
+ camera_config.get('@username'),
+ camera_config.get('@password'),
+ camera_config.get('@remote_camera_id'),
+ data)
else:
logging.debug('setting main config')
def list_cameras(self):
logging.debug('listing cameras')
+
+ host = self.get_argument('host', None)
+ port = self.get_argument('port', None)
+ username = self.get_argument('username', None)
+ password = self.get_argument('password', None)
+
+ if host: # remote
+ try:
+ cameras = remote.list_cameras(host, port, username, password)
+
+ except Exception as e:
+ return self.finish_json({'error': unicode(e)})
- cameras = []
- for camera_id in config.get_camera_ids():
- data = config.get_camera(camera_id)
- data = self._camera_dict_to_ui(data)
- data['id'] = camera_id
- cameras.append(data)
+ else:
+ cameras = []
+ for camera_id in config.get_camera_ids():
+ data = config.get_camera(camera_id)
+ if data['@proto'] == 'v4l2':
+ data = self._camera_dict_to_ui(data)
+
+ else:
+ data = {
+ 'name': data['@name']
+ }
+
+ data['id'] = camera_id
+ cameras.append(data)
self.finish_json({'cameras': cameras})
def add_camera(self):
logging.debug('adding new camera')
- device = self.get_argument('device')
- camera_id, data = config.add_camera(device)
+ try:
+ device_details = json.loads(self.request.body)
+
+ except Exception as e:
+ logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+
+ raise
+
+ camera_id, data = config.add_camera(device_details)
data['@id'] = camera_id
- data['@enabled'] = True
- if motionctl.running():
+ if motionctl.running() and data['@proto'] == 'v4l2':
motionctl.stop()
- if config.has_enabled_cameras():
+ if config.has_enabled_cameras() and data['@proto'] == 'v4l2':
motionctl.start()
+
+ try:
+ remote_data = remote.get_config(
+ device_details.get('host'),
+ device_details.get('port'),
+ device_details.get('username'),
+ device_details.get('password'),
+ device_details.get('remote_camera_id'))
+
+ except Exception as e:
+ return self.finish_json({'error': unicode(e)})
+
+ remote_data = self._camera_ui_to_dict(remote_data)
+ remote_data.update(data)
- self.finish_json(self._camera_dict_to_ui(data))
+ self.finish_json(self._camera_dict_to_ui(remote_data))
def rem_camera(self, camera_id):
logging.debug('removing camera %(id)s' % {'id': camera_id})
+ local = config.get_camera(camera_id).get('@proto') == 'v4l2'
config.rem_camera(camera_id)
- if motionctl.running():
+ if motionctl.running() and local:
motionctl.stop()
- if config.has_enabled_cameras():
+ if config.has_enabled_cameras() and local:
motionctl.start()
def _main_ui_to_dict(self, ui):
}
def _camera_ui_to_dict(self, ui):
- video_device = ui.get('device', '')
- if video_device.count('://'):
- video_device = video_device.split('://')[-1]
-
if not ui.get('resolution'): # avoid errors for empty resolution setting
ui['resolution'] = '352x288'
# device
'@name': ui.get('name', ''),
'@enabled': ui.get('enabled', False),
- 'videodevice': video_device,
'lightswitch': int(ui.get('light_switch_detect', False) * 5),
'auto_brightness': ui.get('auto_brightness', False),
'brightness': max(1, int(round(int(ui.get('brightness', 0)) * 2.55))),
# device
'name': data['@name'],
'enabled': data['@enabled'],
- 'id': data['@id'],
- 'device': data['@proto'] + '://' + data['videodevice'],
- 'light_switch_detect': data['lightswitch'] > 0,
- 'auto_brightness': data['auto_brightness'],
- 'brightness': int(round(int(data['brightness']) / 2.55)),
- 'contrast': int(round(int(data['contrast']) / 2.55)),
- 'saturation': int(round(int(data['saturation']) / 2.55)),
- 'hue': int(round(int(data['hue']) / 2.55)),
- 'resolution': str(data['width']) + 'x' + str(data['height']),
- 'framerate': int(data['framerate']),
- 'rotation': int(data['rotate']),
+ 'id': data.get('@id'),
+ 'proto': data['@proto'],
+ 'light_switch_detect': data.get('lightswitch') > 0,
+ 'auto_brightness': data.get('auto_brightness'),
+ 'brightness': int(round(int(data.get('brightness')) / 2.55)),
+ 'contrast': int(round(int(data.get('contrast')) / 2.55)),
+ 'saturation': int(round(int(data.get('saturation')) / 2.55)),
+ 'hue': int(round(int(data.get('hue')) / 2.55)),
+ 'resolution': str(data.get('width')) + 'x' + str(data.get('height')),
+ 'framerate': int(data.get('framerate')),
+ 'rotation': int(data.get('rotate')),
# file storage
'storage_device': data['@storage_device'],
'network_share_name': data['@network_share_name'],
'network_username': data['@network_username'],
'network_password': data['@network_password'],
- 'root_directory': data['target_dir'],
+ 'root_directory': data.get('target_dir'),
# text overlay
'text_overlay': False,
'custom_right_text': '',
# streaming
- 'vudeo_streaming': not data['webcam_localhost'],
- 'streaming_port': int(data['webcam_port']),
- 'streaming_framerate': int(data['webcam_maxrate']),
- 'streaming_quality': int(data['webcam_quality']),
- 'streaming_motion': int(data['webcam_motion']),
+ 'vudeo_streaming': not data.get('webcam_localhost'),
+ 'streaming_port': int(data.get('webcam_port')),
+ 'streaming_framerate': int(data.get('webcam_maxrate')),
+ 'streaming_quality': int(data.get('webcam_quality')),
+ 'streaming_motion': int(data.get('webcam_motion')),
# still images
'still_images': False,
'preserve_images': data['@preserve_images'],
# motion movies
- 'motion_movies': data['motion_movies'],
- 'movie_quality': int((max(2, data['ffmpeg_variable_bitrate']) - 2) / 0.29),
- 'movie_file_name': data['movie_filename'],
+ 'motion_movies': data.get('ffmpeg_cap_new'),
+ 'movie_quality': int((max(2, data.get('ffmpeg_variable_bitrate')) - 2) / 0.29),
+ 'movie_file_name': data.get('movie_filename'),
'preserve_movies': data['@preserve_movies'],
# motion detection
'show_frame_changes': data.get('text_changes') or data.get('locate'),
- 'frame_change_threshold': data['threshold'],
- 'auto_noise_detect': data['noise_tune'],
- 'noise_level': int(int(data['noise_level']) / 2.55),
- 'gap': int(data['gap']),
- 'pre_capture': int(data['pre_capture']),
- 'post_capture': int(data['post_capture']),
+ 'frame_change_threshold': data.get('threshold'),
+ 'auto_noise_detect': data.get('noise_tune'),
+ 'noise_level': int(int(data.get('noise_level')) / 2.55),
+ 'gap': int(data.get('gap')),
+ 'pre_capture': int(data.get('pre_capture')),
+ 'post_capture': int(data.get('post_capture')),
# motion notifications
'motion_notifications': data['@motion_notifications'],
'sunday_from': '09:00', 'sunday_to': '17:00'
}
- text_left = data['text_left']
- text_right = data['text_right']
+ text_left = data.get('text_left')
+ text_right = data.get('text_right')
if text_left or text_right:
ui['text_overlay'] = True
--- /dev/null
+
+import json
+import logging
+import urllib2
+
+
+def _compose_url(host, port, username, password, uri, query=None):
+ url = '%(scheme)s://%(host)s:%(port)s%(uri)s' % {
+ 'scheme': 'http',
+ 'host': host,
+ 'port': port,
+ 'uri': uri}
+
+ if query:
+ url += '?' + '='.join(query.items())
+
+ return url
+
+
+def list_cameras(host, port, username, password):
+ logging.debug('listing remote cameras on %(host)s:%(port)s' % {
+ 'host': host,
+ 'port': port})
+
+ url = _compose_url(host, port, username, password, '/config/list/')
+ request = urllib2.Request(url)
+
+ try:
+ response = urllib2.urlopen(request)
+
+ except Exception as e:
+ logging.error('failed to list remote cameras on %(host)s:%(port)s: %(msg)s' % {
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(e)})
+
+ raise
+
+ try:
+ response = json.load(response)
+
+ except Exception as e:
+ logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % {
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(e)})
+
+ raise
+
+ return response['cameras']
+
+
+def get_config(host, port, username, password, camera_id):
+ logging.debug('getting config for remote camera %(id)s on %(host)s:%(port)s' % {
+ 'id': camera_id,
+ 'host': host,
+ 'port': port})
+
+ url = _compose_url(host, port, username, password, '/config/%(id)s/get/' % {'id': camera_id})
+ request = urllib2.Request(url)
+
+ try:
+ response = urllib2.urlopen(request)
+
+ except Exception as e:
+ logging.error('failed to get config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ 'id': camera_id,
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(e)})
+
+ raise
+
+ try:
+ response = json.load(response)
+
+ except Exception as e:
+ logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % {
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(e)})
+
+ raise
+
+ return response
+
+
+def set_config(host, port, username, password, camera_id, data):
+ logging.debug('setting config for remote camera %(id)s on %(host)s:%(port)s' % {
+ 'id': camera_id,
+ 'host': host,
+ 'port': port})
+
+ data = json.dumps(data)
+
+ url = _compose_url(host, port, username, password, '/config/%(id)s/set/' % {'id': camera_id})
+ request = urllib2.Request(url, data=data)
+
+ try:
+ urllib2.urlopen(request)
+
+ except Exception as e:
+ logging.error('failed to set config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+ 'id': camera_id,
+ 'host': host,
+ 'port': port,
+ 'msg': unicode(e)})
+
+ raise
+
}
};
- if (data && typeof data === 'object') {
+ if (data && method === 'POST' && typeof data === 'object') {
options['contentType'] = 'application/json';
options['data'] = JSON.stringify(options['data']);
}
runConfirmDialog('Remove device ' + deviceName + '?', function () {
showProgress();
- ajax('POST', '/config/' + cameraId + '/rem/', null, function () {
+ ajax('POST', '/config/' + cameraId + '/rem/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
hideApply();
fetchCurrentConfig();
});
}
});
- /* re-validate all the input validators */
- $('div.settings').find('input.validator').each(function () {
+ /* re-validate all the validators */
+ $('div.settings').find('.validator').each(function () {
this.validate();
});
}
function configUiValid() {
+ /* re-validate all the validators */
+ $('div.settings').find('.validator').each(function () {
+ this.validate();
+ });
+
var valid = true;
$('div.settings input, select').each(function () {
if (this.invalid) {
return; /* progress already visible */
}
- applyButton.html('<img class="apply-progress" src="' + staticUrl + 'img/progress.gif">');
+ applyButton.html('<img class="apply-progress" src="' + staticUrl + 'img/apply-progress.gif">');
applyButton.css('display', 'inline-block');
applyButton.animate({'opacity': '1'}, 100);
applyButton.addClass('progress');
for (var i = 0; i < configs.length; i++) {
var config = configs[i];
if (i === configs.length - 1) {
- config.config['restart'] = true;
+ config.config['last'] = true;
}
- ajax('POST', '/config/' + config.key + '/set/', config.config, function () {
+ ajax('POST', '/config/' + config.key + '/set/', config.config, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
finishedCount++;
testReady();
});
function fetchCurrentConfig() {
/* fetch the main configuration */
ajax('GET', '/config/main/get/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
dict2MainUi(data);
/* fetch the camera list */
ajax('GET', '/config/list/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
var i, cameras = data.cameras;
var videoDeviceSelect = $('#videoDeviceSelect');
videoDeviceSelect.html('');
var cameraId = $('#videoDeviceSelect').val();
if (cameraId != null) {
ajax('GET', '/config/' + cameraId + '/get/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
dict2CameraUi(data);
});
}
'hue': hue
};
- ajax('POST', '/config/' + cameraId + '/set_preview/', data);
+ ajax('POST', '/config/' + cameraId + '/set_preview/', data, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+ });
}
'<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
'<td><span class="help-mark" title="the remote administrator\'s password">?</span></td>' +
'</tr>' +
+ '<tr class="remote">' +
+ '<td class="dialog-item-label"><span class="dialog-item-label">Camera</span></td>' +
+ '<td class="dialog-item-value"><select class="styled" id="cameraSelect"></select></td>' +
+ '<td><span class="help-mark" title="the remote camera you wish to add to motionEye">?</span></td>' +
+ '</tr>' +
'</table>');
/* collect ui widgets */
var portEntry = content.find('#portEntry');
var usernameEntry = content.find('#usernameEntry');
var passwordEntry = content.find('#passwordEntry');
+ var cameraSelect = content.find('#cameraSelect');
/* make validators */
makeTextValidator(hostEntry, true);
makeNumberValidator(portEntry, 1, 65535, false, false, true);
makeTextValidator(usernameEntry, true);
+ makeTextValidator(deviceSelect, true);
+ makeComboValidator(cameraSelect, true);
/* ui interaction */
content.find('tr.remote').css('display', 'none');
- var updateUi = function () {
+
+ function updateUi() {
if (deviceSelect.val() === 'remote') {
content.find('tr.remote').css('display', 'table-row');
}
}
updateModalDialogPosition();
- };
+ cameraSelect.html('');
+
+ /* re-validate all the validators */
+ content.find('.validator').each(function () {
+ this.validate();
+ });
+
+ if (uiValid() && deviceSelect.val() == 'remote') {
+ fetchRemoteCameras();
+ }
+ }
- deviceSelect.change(updateUi).change();
+ function uiValid(includeCameraSelect) {
+ /* re-validate all the validators */
+ content.find('.validator').each(function () {
+ this.validate();
+ });
+
+ var valid = true;
+ var query = content.find('input, select');
+ if (!includeCameraSelect) {
+ query = query.not('#cameraSelect');
+ }
+ query.each(function () {
+ if (this.invalid) {
+ valid = false;
+ return false;
+ }
+ });
+
+ return valid;
+ }
+
+ function fetchRemoteCameras() {
+ var progress = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
+
+ cameraSelect.hide();
+ cameraSelect.before(progress);
+ cameraSelect.parent().find('div').remove(); /* remove any previous progress div */
+
+ var data = {
+ host: hostEntry.val(),
+ port: portEntry.val(),
+ username: usernameEntry.val(),
+ password: passwordEntry.val()
+ };
+
+ ajax('GET', '/config/list/', data, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
+ cameraSelect.html('');
+ progress.remove();
+
+ if (data.error || !data.cameras) {
+ return;
+ }
+
+ data.cameras.forEach(function (info) {
+ cameraSelect.append('<option value="' + info.id + '">' + info.name + '</option>');
+ });
+
+ cameraSelect.show();
+ });
+ }
+
+ deviceSelect.change(updateUi);
+ hostEntry.change(updateUi);
+ portEntry.change(updateUi);
+ usernameEntry.change(updateUi);
+ passwordEntry.change(updateUi);
+ updateUi();
showModalDialog('<div class="modal-progress"></div>');
/* fetch the available devices */
ajax('GET', '/config/list_devices/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
/* add available devices */
data.devices.forEach(function (device) {
if (!device.configured) {
deviceSelect.append('<option value="remote">Remote device...</option>');
+ updateUi();
+
runModalDialog({
title: 'Add Camera...',
closeButton: true,
buttons: 'okcancel',
content: content,
onOk: function () {
- var fullDevice;
+ if (!uiValid(true)) {
+ return false;
+ }
+
+ var data = {};
if (deviceSelect.val() == 'remote') {
- fullDevice = 'http://' + usernameEntry.val() + ':' + passwordEntry.val() + '@' + hostEntry + ':' + portEntry;
+ data.proto = 'http';
+ data.host = hostEntry.val();
+ data.port = portEntry.val();
+ data.username = usernameEntry.val();
+ data.password = passwordEntry.val();
+ data.remote_camera_id = cameraSelect.val();
}
else {
- fullDevice = 'v4l2://' + deviceSelect.val();
+ data.proto = 'v4l2';
+ data.device = deviceSelect.val();
}
-
+
showProgress();
-
- ajax('POST', '/config/add/?device=' + fullDevice, null, function (data) {
+
+ ajax('POST', '/config/add/', data, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
hideApply();
var addCameraOption = $('#videoDeviceSelect').find('option[value=add]');
addCameraOption.before('<option value="' + data.id + '">' + data.name + '</option>');
}
else {
ajax('GET', '/config/list/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
updateCameras(data.cameras);
});
}
remCameraFrameUi(cameraId);
showProgress();
ajax('GET', '/config/' + cameraId + '/get/', null, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
data['enabled'] = false;
- data['restart'] = true;
- ajax('POST', '/config/' + cameraId + '/set/', data, function () {
+ data['last'] = true;
+ ajax('POST', '/config/' + cameraId + '/set/', data, function (data) {
+ if (data == null || data.error) {
+ return; // TODO handle error
+ }
+
endProgress();
/* if the current camera in the settings panel is the closed camera,
$input[0].validate = validate;
}
+function makeComboValidator($select, required) {
+ if (required == null) {
+ required = true;
+ }
+
+ function isValid(strVal) {
+ if (!$select.parents('tr:eq(0)').is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ if (strVal.length === 0 && required) {
+ return false;
+ }
+
+ return true;
+ }
+
+ var msg = 'this field is required';
+
+ function validate() {
+ var strVal = $select.val() || '';
+ if (isValid(strVal)) {
+ $select.attr('title', '');
+ $select.removeClass('error');
+ $select[0].invalid = false;
+ }
+ else {
+ $select.attr('title', msg);
+ $select.addClass('error');
+ $select[0].invalid = true;
+ }
+ }
+
+ $select.keyup(validate);
+ $select.blur(validate);
+ $select.change(validate).change();
+
+ $select.addClass('validator');
+ $select.addClass('combo-validator');
+ $select[0].validate = validate;
+}
+
function makeNumberValidator($input, minVal, maxVal, floating, sign, required) {
if (minVal == null) {
minVal = -Infinity;
buttonsInfo.forEach(function (info) {
var buttonDiv = $('<div class="button dialog mouse-effect"></div>');
- buttonDiv.click(hideModalDialog); /* every button closes the dialog */
buttonDiv.attr('tabIndex', '0'); /* make button focusable */
buttonDiv.html(info.caption);
}
if (info.click) {
- buttonDiv.click(info.click);
+ var oldClick = info.click;
+ info.click = function () {
+ if (oldClick() == false) {
+ return;
+ }
+
+ hideModalDialog();
+ };
+ }
+ else {
+ info.click = hideModalDialog; /* every button closes the dialog */
}
+ buttonDiv.click(info.click);
+
var td = $('<td></td>');
td.append(buttonDiv);
tr.append(td);
switch (e.which) {
case 13:
if (defaultClick) {
- defaultClick();
+ if (defaultClick() == false) {
+ return;
+ };
}
/* intentionally no break */