/*jsl:import ipa.js */ /* Authors: * Endi Sukma Dewata * Adam Young * Pavel Zuna * * Copyright (C) 2010 Red Hat * see file 'COPYING' for use and warranty information * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ define(['dojo/_base/array', 'dojo/_base/lang', 'dojo/dom-construct', 'dojo/Evented', 'dojo/has', 'dojo/keys', 'dojo/on', 'dojo/string', 'dojo/topic', './builder', './config', './datetime', './entity', './ipa', './jquery', './navigation', './phases', './reg', './rpc', './text', './util', 'exports' ], function(array, lang, construct, Evented, has, keys, on, string, topic, builder, config, datetime, entity_mod, IPA, $, navigation, phases, reg, rpc, text, util, exp) { /** * Widget module * ============= * * External usage: * * var widget = require('freeipa/widget') * @class widget * @singleton */ /** * Width of column which contains only checkbox * @member IPA * @property {number} */ IPA.checkbox_column_width = 13; /** * String to show next to required fields to indicate that the field is required. * @member IPA * @property {string} */ IPA.required_indicator = '*'; /** * Base widget * @class * @param {Object} spec * @abstract */ IPA.widget = function(spec) { spec = spec || {}; var that = new Evented(); /** * Normalize tooltip * @protected */ that._normalize_tooltip = function(tt_spec) { var tt = typeof tt_spec === 'string' ? { title: tt_spec } : tt_spec; if (tt) { tt.title = text.get(tt.title); } return tt; }; /** * Widget name. Should be container unique. */ that.name = spec.name; /** * Widget element ID. * @deprecated */ that.id = spec.id; /** * Label * @property {string} */ that.label = text.get(spec.label); /** * Title text * @property {string} */ that.title = text.get(spec.title); /** * Measurement unit * @property {string} */ that.measurement_unit = spec.measurement_unit; /** * Tooltip text * * ''' * var tooltip = { * title: 'Helper text', * placement: 'right' * // possible placements: left, top, bottom, right * }; * * // or just string, it will be normalized later: * tooltip = "Helper text"; * * ''' * * Check Bootstrap documentation for more tooltip options. * * @property {Object|string} */ that.tooltip = that._normalize_tooltip(spec.tooltip); /** * Parent entity * @deprecated * @property {IPA.entity} */ that.entity = IPA.get_entity(spec.entity); //some old widgets still need it /** * Parent facet * @property {IPA.facet} */ that.facet = spec.facet; /** * Widget is enabled - can be focus and edited (depends also on writable * and read_only) * @property {boolean} */ that.enabled = spec.enabled === undefined ? true : spec.enabled; /** * Enables showing of validation errors * @property {boolean} */ that.show_errors = spec.show_errors === undefined ? true : spec.show_errors; /** * Facet should be visible * @property {boolean} */ that.visible = spec.visible === undefined ? true : spec.visible; /** * If true, widget visible is set to false when value is empty * @property {boolean} */ that.hidden_if_empty = spec.hidden_if_empty === undefined ? config.hide_empty_widgets : spec.hidden_if_empty; /** * Disable `hidden_if_empty` * @property {boolean} */ that.ignore_empty_hiding = spec.ignore_empty_hiding === undefined ? config.hide_empty_sections : spec.ignore_empty_hiding; /** * Default main element's css classes * @property {string} */ that.base_css_class = spec.base_css_class || 'widget'; /** * Additional main element's css classes * * Intended to be overridden in spec objects * * @property {string} */ that.css_class = spec.css_class || ''; /** * Create HTML representation of a widget. * @method * @param {HTMLElement} container - Container node */ that.create = function(container) { container = $(container); container.addClass(that.base_css_class); container.addClass(that.css_class); that.container = container; }; /** * Reset widget content. All user-modifiable information have to be * changed back to widgets defaults. */ that.clear = function() { }; /** * Widget post constructor/factory initialization * * Called by builder by default. */ that.ctor_init = function() { }; /** * Set enabled state. * @param {boolean} value - True - enabled; False - disabled */ that.set_enabled = function(value) { var changed = that.enabled !== value; that.enabled = value; if (changed) { that.emit('enabled-change', { source: that, enabled: value }); } }; /** * Whether widget should be displayed. * @param {boolean} [value] - True - visible; False - hidden, * undefined - use previous (enforce state) */ that.set_visible = function(visible) { var old = that._effective_visible; visible = visible === undefined ? that.visible : visible; that.visible = visible; var current = that.get_visible(); that._effective_visible = current; if (current) { that.show(); } else { that.hide(); } if (old !== current) { that.emit('visible-change', { source: that, visible: current }); } }; that.get_visible = function() { return that.visible; }; that.hide = function() { that.container.hide(); }; that.show = function() { that.container.show(); }; /** * Utility method. Build widget based on spec with this widget's context. * @param {boolean} spec - Widget specification object * @param {Object} context - Context object. Gets mixed with this widget context. * @param {Object} overrides - Build overrides */ that.build_child = function(spec, context, overrides) { var def_c = { entity: that.entity, facet: that.facet }; context = lang.mixin(def_c, context); var child = builder.build('widget', spec, context, overrides); return child; }; that.add_class = function(cls) { if (that.container) { that.container.addClass(cls); } }; that.remove_class = function(cls) { if (that.container) { that.container.removeClass(cls); } }; that.toggle_class = function(cls, flag) { if (that.container) { that.container.toggleClass(cls, flag); } }; that.widget_create = that.create; that.widget_set_enabled = that.set_enabled; return that; }; /** * Base class for input gathering widgets. * @class * @extends IPA.widget * @abstract */ IPA.input_widget = function(spec) { spec = spec || {}; var that = IPA.widget(spec); /** * Placeholder * @property {string} */ that.placeholder = text.get(spec.placeholder); /** * Widget's width. * @deprecated * @property {number} */ that.width = spec.width; /** * Widget's height. * @deprecated * @property {number} */ that.height = spec.height; /** * Widget is required * @property {boolean} */ that.required = spec.required; /** * Enable undo button showing. Undo button is displayed when user * modifies data. * @property {boolean} undo=true */ that.undo = spec.undo === undefined ? true : spec.undo; /** * User has rights to modify widgets content. Ie. based on LDAP ACL. * @property {boolean} writable=true */ that.writable = spec.writable === undefined ? true : spec.writable; /** * This widget content is read-only. * @property {boolean} */ that.read_only = spec.read_only; //events //each widget can contain several events /** * Value changed event. * * Raised when user modifies data by hand. * @deprecated * * @event */ that.value_changed = IPA.observer(); /** * Undo clicked event. * @deprecated * * @event */ that.undo_clicked = IPA.observer(); /** * @inheritDoc */ that.ctor_init = function() { on(that, 'value-change', that.hide_if_empty); }; /** * Creates HTML representation of error link * @param {HTMLElement} container - node to place the error link */ that.create_error_link = function(container) { container.append(' '); $('', { name: 'error_link', 'class': 'help-block', style: 'display:none' }).appendTo(container); }; /** * Creates HTML representation of required indicator. * @param {HTMLElement} container - node to place the indicator */ that.create_required = function(container) { that.required_indicator = $('', { 'class': 'required-indicator', text: IPA.required_indicator, style: 'display: none;' }).appendTo(container); }; /** * Update displayed information by supplied values. * @param {Object|Array|null} values - values to be edited/displayed by * widget. */ that.update = function() { }; /** * Alias of update */ that.set_value = function(value) { that.update(value); }; /** * This function saves the values entered in the UI. * It returns the values in an array, or null if * the field should not be saved. * @returns {Array|null} entered values */ that.save = function() { return []; }; /** * Alias of save */ that.get_value = function() { return that.save(); }; /** * This function creates an undo link in the container. * On_undo is a link click callback. It can be specified to custom * callback. If a callback isn't set, default callback is used. If * spefified to value other than a function, no callback is registered. * @param {HTMLElement} container * @param {Function} link clicked callback */ that.create_undo = function(container, on_undo) { container.append(' '); that.undo_span = IPA.button({ name: 'undo', style: 'display: none;', 'class': 'undo', label: text.get('@i18n:widget.undo') }).appendTo(container); if(on_undo === undefined) { on_undo = function() { that.undo_clicked.notify([], that); that.emit('undo-click', { source: that }); }; } if(typeof on_undo === 'function') { that.undo_span.click(on_undo); } }; /** * Get reference to undo element * @return {jQuery} undo button jQuery reference */ that.get_undo = function() { return $(that.undo_span); }; /** * Display undo button */ that.show_undo = function() { that.get_undo().css('display', ''); }; /** * Hide undo button */ that.hide_undo = function() { $(that.undo_span).css('display', 'none'); }; /** * Get error link reference * @return {jQuery} error link jQuery reference */ that.get_error_link = function() { return $('span[name="error_link"]', that.container).eq(0); }; /** * Set's validity of widget's value. Usually checked by outside logic. * @param {Object} result Validation result as defined in IPA.validator */ that.set_valid = function(result) { var old = that.valid; that.valid = result.valid; that.toggle_class('valid', that.valid); if (!that.valid) { that.show_error(result.message); } else { that.hide_error(); } if (old !== that.valid) { that.emit("valid-change", { source: that, valid: that.valid, result: result }); } }; /** * Show error message * @protected * @fires error-show * @param {Object} error */ that.show_error = function(message) { if (that.show_errors) { var error_link = that.get_error_link(); error_link.html(message); error_link.css('display', ''); } that.emit('error-show', { source: that, error: message, displayed: that.show_errors }); }; /** * Hide error message * @protected * @fires error-hide */ that.hide_error = function() { var error_link = that.get_error_link(); error_link.html(''); error_link.css('display', 'none'); that.emit('error-hide', { source: that }); }; /** * Set required * @param {boolean} required */ that.set_required = function(required) { var changed = required !== that.required; that.required = required; if (that.required_indicator) { that.required_indicator.css('display', that.required ? '' : 'none'); } if (changed) { that.emit('require-change', { source: that, required: required }); } }; /** * Set enabled * @param {boolean} value - enabled */ that.set_enabled = function(value) { that.widget_set_enabled(value); if (that.input) { that.input.prop('disabled', !value); } }; /** * Raise value change event * @protected */ that.on_value_changed = function(value) { var old = that.value; if (value === undefined) value = that.save(); that.value = value; that.value_changed.notify([value], that); that.emit('value-change', { source: that, value: value, old: old }); }; /** * Hide widget if value is empty and widget is read_only. * @protected */ that.hide_if_empty = function(event) { var value = event.value !== undefined ? event.value : true; that.has_value = !util.is_empty(value); that.set_visible(); }; that.get_visible = function() { var visible = that.visible; if (that.has_value === false && !that.is_writable() && that.hidden_if_empty) { visible = false; } return visible; }; /** * Widget is writable * @return {boolean} */ that.is_writable = function() { return !that.read_only && !!that.writable; }; /** * Set writable * @fires writable-change * @param {boolean} writable */ that.set_writable = function(writable) { var changed = writable !== that.writable; that.writable = writable; that.update_read_only(); if (changed) { that.emit('writable-change', { source: that, writable: writable }); } }; /** * Set read only * @fires readonly-change * @param {boolean} writable */ that.set_read_only = function(read_only) { var changed = read_only !== that.read_only; that.read_only = read_only; that.update_read_only(); if (changed) { that.emit('readonly-change', { source: that, read_only: read_only }); } }; /** * Update widget's HTML based on `read_only` and `writable` properties * @protected */ that.update_read_only = function() { var input = that.get_input(); if (input) { var ro = that.is_writable(); input.prop('readOnly', !ro); } }; /** * Focus input element * @abstract */ that.focus_input = function() { var input = that.get_input(); if (!input) { return; } else if (input.jquery || input.length === undefined) { input.focus(); } else if (input.length) { input[0].focus(); } }; /** * Get input element or array of input elements in case of multivalued * widgets. * * - useful for label.for * * @return {null|HTMLElement[]} */ that.get_input = function() { if (that.input) return that.input; return null; }; /** * Mark element as deleted. * * Ie. textbox with strike-through * @abstract */ that.set_deleted = function() {}; // methods that should be invoked by subclasses that.widget_hide_error = that.hide_error; that.widget_show_error = that.show_error; that.widget_set_valid = that.set_valid; that.widget_hide_undo = that.hide_undo; that.widget_show_undo = that.show_undo; that.widget_set_writable = that.set_writable; that.widget_set_read_only = that.set_read_only; return that; }; /** * Select text in input. * Uses a browser specific technique to select a range. * @member IPA * @param {jQuery} input jQuery reference * @param {number} start * @param {number} end */ IPA.select_range = function(input,start, end) { input.focus(); if (input[0].setSelectionRange) { input[0].setSelectionRange(start, end); } else if (input[0].createTextRange) { var range = input[0].createTextRange(); range.collapse(true); range.moveEnd('character', end); range.moveStart('character', start); range.select(); } }; /** * A textbox widget. Displayed as label when not modifiable. * @class * @extends IPA.input_widget */ IPA.text_widget = function(spec) { spec = spec || {}; var that = IPA.input_widget(spec); /** * Size of the input. * @property {number} */ that.size = spec.size || 30; /** * Input type * @property {string} input_type='text' */ that.input_type = spec.input_type || 'text'; that.base_css_class = that.base_css_class + ' text-widget'; /** * Select range of text */ that.select_range = function(start, end){ IPA.select_range(that.input, start, end); }; /** * @inheritDoc */ that.create = function(container) { that.widget_create(container); that.display_control = $('

', { name: that.name, 'class': 'form-control-static', style: 'display: none;' }).appendTo(container); var id = IPA.html_util.get_next_id(that.name); that.input_group = $('

').appendTo(container); that.input = $('', { type: that.input_type, name: that.name, id: id, 'class': 'form-control', size: that.size, title: that.title, placeholder: that.placeholder, keyup: function() { that.on_value_changed(); } }).appendTo(that.input_group); that.input_group_btn = $('
', { 'class': 'input-group-btn' }).appendTo(that.input_group); that.input.bind('input', function() { that.on_value_changed(); }); if (that.undo) { that.create_undo(that.input_group_btn); } that.create_error_link(container); that.set_enabled(that.enabled); that.update_read_only(); that.update_input_group_state(); }; /** * @inheritDoc */ that.update = function(values) { var value = values && values.length ? values[0] : ''; that.display_control.text(value); that.input.val(value); that.on_value_changed(values); }; /** * @inheritDoc */ that.update_read_only = function() { if (!that.input) return; if (!that.is_writable()) { that.display_control.css('display', ''); that.input_group.css('display', 'none'); } else { that.display_control.css('display', 'none'); that.input_group.css('display', ''); } }; /** * @inheritDoc */ that.save = function() { var value = that.input.val(); return value === '' ? [] : [value]; }; /** * @inheritDoc */ that.clear = function() { that.input.val(''); that.display_control.text(''); that.on_value_changed([]); }; /** * @inheritDoc */ that.set_deleted = function(deleted) { if(deleted) { that.input.addClass('strikethrough'); } else { that.input.removeClass('strikethrough'); } }; /** * Display undo button */ that.show_undo = function() { that.widget_show_undo(); that.update_input_group_state(); }; /** * Hide undo button */ that.hide_undo = function() { that.widget_hide_undo(); that.update_input_group_state(); }; /** * Set 'input_group' class to input group if input_group_btn has any * visible content. */ that.update_input_group_state = function() { var children = that.input_group_btn.children(); var visible = $.grep(children, function(el, i) { return $(el).css('display') !== 'none'; }).length > 0; that.input_group.toggleClass('input-group', visible); }; // methods that should be invoked by subclasses that.text_load = that.load; return that; }; /** * @class * @extends IPA.text_widget * A textbox widget where input type is 'password'. */ IPA.password_widget = function(spec) { spec = spec || {}; spec.input_type = 'password'; var that = IPA.text_widget(spec); return that; }; /** * Widget which allows to edit multiple values. It display one * editor (text widget by default) for each value. * @class * @extends IPA.input_widget */ IPA.multivalued_widget = function(spec) { spec = spec || {}; var that = IPA.input_widget(spec); that.child_spec = spec.child_spec; that.size = spec.size || 30; that.undo_control; that.initialized = true; that.updating = false; that.rows = []; that.base_css_class = that.base_css_class + ' multivalued-widget'; that.on_child_value_changed = function(row) { if (that.test_dirty_row(row)) { that.toggle_remove_link(row, false); row.widget.show_undo(); } else { row.widget.hide_undo(); that.toggle_remove_link(row, true); } if (that.updating) return; that.on_value_changed(); that.emit('value-change', { source: that }); that.emit('child-value-change', { source: that, row: row }); }; that.on_child_undo_clicked = function(row) { if (row.is_new) { that.remove_row(row); } else { that.reset_row(row); } that.emit('child-undo-click', { source: that, row: row }); }; that.hide_undo = function() { $(that.undo_span).css('display', 'none'); for(var i=0; i 0) { var error_link = that.get_error_link(); error_link.css('display', 'none'); error_link.html(''); } } else if (!result.valid) { that.show_error(result.message); } else { that.hide_error(); } if (old !== that.valid) { that.emit("valid-change", { source: that, valid: that.valid, result: result }); } }; that.save = function() { var values = []; for (var i=0; i 0) { return value[0]; } return ''; } if (value) return value; return ''; }; that.focus_last = function() { if (!that.rows.length) return; var last_row = that.rows[that.rows.length-1]; last_row.widget.focus_input(); }; that.focus_input = function() { if (that.rows.length) { that.focus_last(); } else { that.add_link.focus(); } }; that.add_row = function(values) { var row = {}; that.rows.push(row); var row_index = that.rows.length - 1; row.is_new = that.initialized; row.container = $('
', { name: 'value'}); var spec = that.child_spec || {}; if (typeof spec !== 'function') { lang.mixin(spec, { name: that.name+'-'+row_index, undo: that.undo || row.is_new, read_only: that.read_only, writable: that.writable, enabled: that.enabled }); } row.widget = builder.build('widget', spec); row.widget.create(row.container); row.original_values = values; row.widget.update(values); on(row.widget, 'value-change', function() { that.on_child_value_changed(row); }); on(row.widget, 'undo-click', function() { that.on_child_undo_clicked(row); }); on(row.widget, 'error-show', function() { that.emit('error-show', { source: that }); }); var remove_link_visible = !(row.is_new || !that.is_writable()); row.remove_link = $('