/* Authors: * Pavel Zuna * Endi Sukma Dewata * Adam Young * Petr Vobornik * * Copyright (C) 2010-2011 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/declare', 'dojo/_base/lang', 'dojo/dom-construct', 'dojo/on', 'dojo/Stateful', 'dojo/Evented', './_base/Singleton_registry', './_base/construct', './builder', './ipa', './jquery', './navigation', './phases', './reg', './rpc', './spec_util', './text', './widgets/ActionDropdownWidget', './dialog', './field', './widget' ], function(declare, lang, construct, on, Stateful, Evented, Singleton_registry, construct_utils, builder, IPA, $, navigation, phases, reg, rpc, su, text, ActionDropdownWidget) { /** * Facet module * * @class facet * @singleton */ var exp = {}; exp.facet_spec = {}; /** * Facet represents the content of currently displayed page. * * ## Show, Clear, Refresh mechanism * * Use cases: * * - Display facet with defined arguments. * - Switch to facet * - Update facet state * * ## Display facet by route * * 1. somebody sets route * 2. Route is evaluated, arguments extracted. * 3. Facet state is updated `set_state(args, pkeys)`.(saves previous state) * 4. Facet show() is called * * ## Display facet with defined arguments * * 1. Somebody calls navigation.show(xxx); * 2. Facet state is updated `set_state(args, pkeys)`.(saves previous state) * 3. Route is updated, but the hash change is ignored * 4. Facet show() is called. * - First time show * a. creates DOM * b. display DOM * c. refresh(); * - Next time * a. display DOM * b. `needs_update()` (compares previous state with current) * - true: * 1. clear() - each facet can override to supress clear or * control the behaviour * 2. refresh() * * ## Swith to facet * * Same as display facet but only without arguments. Arguments are extracted at * step 2. * * ## Update facet state * * 1. set_state(args, pkeys?) * 2. needs_update()? * - true: * 1. clear() * 2. refresh() * 3. Update route, ignore hash change event * * ## Updating hash * Hash updates are responsibility of navigation component and application * controller. Application controller should listen to facet's `state_change` * event. And call something like navigation.update_hash(facet). * * navigation.update_hash should find all the necessary state properties (args, * pkeys). * * ## needs_update method * todo * * @class facet.facet * @alternateClassName IPA.facet */ exp.facet = IPA.facet = function(spec, no_init) { spec = spec || {}; var that = new Evented(); /** * Name of preferred facet container * * Leave unset to use default container. * @property {string} */ that.preferred_container = spec.preferred_container; /** * Entity this facet belongs to * @property {entity.entity} */ that.entity = IPA.get_entity(spec.entity); /** * Facet name * @property {string} */ that.name = spec.name; /** * Facet label * @property {string} */ that.label = text.get(spec.label); /** * Facet title * @property {string} */ that.title = text.get(spec.title || that.label); /** * Facet tab label * @property {string} */ that.tab_label = text.get(spec.tab_label || that.label); /** * Facet element's CSS class * @property {string} */ that.display_class = spec.display_class; /** * Flag. Marks the facet as read-only - doesn't support modify&update * operation. * @property {boolean} */ that.no_update = spec.no_update; /** * Breadcrumb navigation is not displayed when set. * @property {boolean} */ that.disable_breadcrumb = spec.disable_breadcrumb; /** * Facet tabs are not displayed when set. * @property {boolean} */ that.disable_facet_tabs = spec.disable_facet_tabs; /** * State object for actions * @property {facet.state} */ that.action_state = builder.build('', spec.state || {}, {}, { $factory: exp.state }); /** * Collection of facet actions * @property {facet.action_holder} */ that.actions = builder.build('', { actions: spec.actions }, {}, { $factory: exp.action_holder } ); /** * Array of actions which are displayed in facet header * @property {Array.} */ that.header_actions = spec.header_actions || []; /** * Facet header * @property {facet.facet_header} */ that.header = spec.header || IPA.facet_header({ facet: that }); /** * Hard override for `needs_update()` logic. When set, `needs_update` * should always return this value. * @property {boolean} */ that._needs_update = spec.needs_update; /** * Facet is shown * @property {Boolean} */ that.is_shown = false; /** * Marks facet as expired - needs update * * Difference between `_needs_update` is that `expired_flag` should be * cleared after update. * * @property {boolean} */ that.expired_flag = true; /** * Last time when facet was updated. * @property {Date} */ that.last_updated = null; /** * Timeout[s] from `last_modified` after which facet should be expired * @property {number} expire_timeout=600 */ that.expire_timeout = spec.expire_timeout || 600; //[seconds] /** * Raised when facet gets updated * @event */ that.on_update = IPA.observer(); /** * Raised after `load()` * @event */ that.post_load = IPA.observer(); /** * Dialogs * @property {ordered_map} */ that.dialogs = $.ordered_map(); /** * dom_node of container * Suppose to contain dom_node of this and other facets. * @property {jQuery} */ that.container_node = spec.container_node; /** * dom_node which contains all content of a facet. * Should contain error content and content. When error is moved to * standalone facet it will replace functionality of content. * @property {jQuery} */ that.dom_node = null; /** * Facet group name * @property {string} */ that.facet_group = spec.facet_group; /** * Redirection target information. * * Can be facet and/or entity name. * @property {Object} * @param {string} entity entity name * @param {string} facet facet name */ that.redirect_info = spec.redirect_info; /** * Facet requires authenticated user * @type {Boolean} */ that.requires_auth = spec.requires_auth !== undefined ? spec.requires_auth : true; /** * Public state * @property {facet.FacetState} */ that.state = new FacetState(); /** * Set and normalize pkeys. Merges with existing if present. If keys length * differs, the alignment is from the last one to the first one. */ that.set_pkeys = function(pkeys) { pkeys = that.get_pkeys(pkeys); that.state.set('pkeys', pkeys); }; /** * Return THE pkey of this facet. Basically the last one of pkeys list. * * @return {string} pkey */ that.get_pkey = function() { var pkeys = that.get_pkeys(); if (pkeys.length) { return pkeys[pkeys.length-1]; } return ''; }; /** * Gets copy of pkeys list. * It automatically adds empty pkeys ('') for each containing entity if not * specified. * * One can get merge current pkeys with supplied if `pkeys` param is * specified. * * @param {string[]} pkeys new pkeys to merge * @return {string[]} pkeys */ that.get_pkeys = function(pkeys) { var new_keys = []; var cur_keys = that.state.get('pkeys') || []; var current_entity = that.entity; pkeys = pkeys || []; var arg_l = pkeys.length; var cur_l = cur_keys.length; var tot_c = 0; while (current_entity) { if (current_entity.defines_key) tot_c++; current_entity = current_entity.get_containing_entity(); } if (tot_c < arg_l || tot_c < cur_l) throw { error: 'Invalid pkeys count. Supplied more than expected.' }; var arg_off = tot_c - arg_l; var cur_off = cur_l - tot_c; for (var i=0; i} */ that.get_pkey_prefix = function() { var pkeys = that.get_pkeys(); if (pkeys.length > 0) pkeys.pop(); return pkeys; }; /** * Checks if two objects has the same properties with equal values. * * @param {Object} a * @param {Object} b * @return {boolean} `a` and `b` are value-equal * @protected */ that.state_diff = function(a, b) { var diff = false; var checked = {}; var check_diff = function(a, b, skip) { var same = true; skip = skip || {}; for (var key in a) { if (a.hasOwnProperty(key) && !(key in skip)) { var va = a[key]; var vb = b[key]; if (lang.isArray(va)) { if (IPA.array_diff(va,vb)) { same = false; skip[a] = true; break; } } else { if (va != vb) { same = false; skip[a] = true; break; } } } } return !same; }; diff = check_diff(a,b, checked); diff = diff || check_diff(b,a, checked); return diff; }; /** * Reset facet state to supplied * * @param {Object} state state to set */ that.reset_state = function(state) { if (state.pkeys) { state.pkeys = that.get_pkeys(state.pkeys); } that.state.reset(state); }; /** * Get copy of current state * * @return {Object} state */ that.get_state = function() { return that.state.clone(); }; /** * Merges state into current and notifies it. * * @param {Object} state object to merge into current state */ that.set_state = function(state) { if (state.pkeys) { state.pkeys = that.get_pkeys(state.pkeys); } that.state.set(state); }; /** * Handle state set * @param {Object} old_state * @param {Object} state */ that.on_state_set = function(old_state, state) { that._on_state_change(state); }; /** * Handle state change * @protected */ that._on_state_change = function(state) { // basically a show method without displaying the facet // TODO: change to something fine grained that._notify_state_change(state); var needs_update = that.needs_update(state); that.old_state = state; // we don't have to reflect any changes if facet dom is not yet created if (!that.dom_node || !that.is_shown) { if (needs_update) that.set_expired_flag(); return; } if (needs_update) { that.clear(); } that.show_content(); that.header.select_tab(); if (needs_update) { that.refresh(); } }; /** * Fires `facet-state-change` event with given state as event parameter. * * @fires facet-state-change * @protected * @param {Object} state */ that._notify_state_change = function(state) { that.emit('facet-state-change', { facet: that, state: state }); }; /** * Get dialog with given name from facet dialog collection * * @param {string} name * @return {IPA.dialog} dialog */ that.get_dialog = function(name) { return that.dialogs.get(name); }; /** * Add dialog to facet dialog collection * * @param {IPA.dialog} dialog */ that.dialog = function(dialog) { that.dialogs.put(dialog.name, dialog); return that; }; /** * Create facet's HTML representation */ that.create = function() { var entity_name = !!that.entity ? that.entity.name : ''; if (that.dom_node) { that.dom_node.empty(); that.dom_node.detach(); } else { that.dom_node = $('
', { 'class': 'facet active-facet fluid-container', name: that.name, 'data-name': that.name, 'data-entity': entity_name }); } var dom_node = that.dom_node; that.container = dom_node; if (!that.container_node) throw { error: 'Can\'t create facet. No container node defined.' }; var node = dom_node[0]; construct.place(node,that.container_node); if (that.disable_facet_tabs) dom_node.addClass('no-facet-tabs'); dom_node.addClass(that.display_class); that.header_container = $('
', { 'class': 'facet-header col-sm-12' }).appendTo(dom_node); that.create_header(that.header_container); that.content = $('
', { 'class': 'facet-content col-sm-12' }).appendTo(dom_node); that.error_container = $('
', { 'class': 'facet-content facet-error col-sm-12' }).appendTo(dom_node); that.create_content(that.content); dom_node.removeClass('active-facet'); }; /** * Create facet header * * @param {jQuery} container * @protected */ that.create_header = function(container) { that.header.create(container); that.controls = $('
', { 'class': 'facet-controls clearfix' }).appendTo(container); that.controls_left = $('
', { 'class': 'facet-controls-left' }).appendTo(that.controls); that.controls_right = $('
', { 'class': 'facet-controls-right' }).appendTo(that.controls); }; /** * Create content * * @param {jQuery} container * @protected * @abstract */ that.create_content = function(container) { }; /** * Create control buttons * * @param {jQuery} container * @protected */ that.create_control_buttons = function(container) { if (that.control_buttons) { that.control_buttons.create(container); } }; that.create_action_dropdown = function(container) { if (that.action_dropdown && that.header_actions && that.header_actions.length > 0) { var dropdown = that.action_dropdown.render(); container.append(dropdown); } }; /** * Update h1 element in title container * * @deprecated Please update title in facet header or it's widget instead. */ that.set_title = function(container, title) { var element = $('h1', that.title_container); element.html(title); }; /** * Show facet * * - clear & refresh if needs update * - mark itself as active facet */ that.show = function() { if (that.is_shown) return; that.is_shown = true; that.entity.facet = that; // FIXME: remove if (!that.dom_node) { that.create(); } else if (!that.dom_node.parentElement) { construct.place(that.dom_node[0], that.container_node); } var state = that.state.clone(); var needs_update = that.needs_update(state); that.old_state = state; if (needs_update) { that.clear(); } that.dom_node.addClass('active-facet'); that.show_content(); that.header.select_tab(); if (needs_update) { that.refresh(); } }; /** * Show content container and hide error container. * * Opposite to `show_error`. * @protected */ that.show_content = function() { that.content.css('display', 'block'); that.error_container.css('display', 'none'); }; /** * Show error container and hide content container. * * Opposite to `show_content` * @protected */ that.show_error = function() { that.content.css('display', 'none'); that.error_container.css('display', 'block'); }; /** * Check if error is displayed (instead of content) * * @return {boolean} error visible */ that.error_displayed = function() { return that.error_container && that.error_container.css('display') === 'block'; }; /** * Un-mark itself as active facet */ that.hide = function() { that.is_shown = false; if (that.dom_node[0].parentElement) { that.container_node.removeChild(that.dom_node[0]); } that.dom_node.removeClass('active-facet'); }; /** * Update widget content with supplied data * @param {Object} data */ that.load = function(data) { that.data = data; that.header.load(data); }; /** * Start refresh * * - get up-to-date data * - load the data * @abstract */ that.refresh = function() { }; /** * Clear all widgets * @abstract */ that.clear = function() { }; /** * Check if facet needs update * * That means if: * * - new state (`state` or supplied state) is different that old_state * (`old_state`) * - facet is expired * - `expired_flag` is set or * - expire_timeout takes effect * - error is displayed * * * @param {Object} [new_state] supplied state * @return {boolean} needs update */ that.needs_update = function(new_state) { if (that._needs_update !== undefined) return that._needs_update; new_state = new_state || that.state.clone(); var needs_update = false; if (that.expire_timeout && that.expire_timeout > 0) { if (!that.last_updated) { needs_update = true; } else { var now = Date.now(); needs_update = (now - that.last_updated) > that.expire_timeout * 1000; } } needs_update = needs_update || that.expired_flag; needs_update = needs_update || that.error_displayed(); needs_update = needs_update || that.state_diff(that.old_state || {}, new_state); return needs_update; }; /** * Sets expire flag */ that.set_expired_flag = function() { that.expired_flag = true; }; /** * Clears `expired_flag` and resets `last_updated` */ that.clear_expired_flag = function() { that.expired_flag = false; that.last_updated = Date.now(); }; /** * Check whether the facet is dirty * * Dirty can mean that value of displayed object was modified but the change * was not reflected to data source * * @returns {boolean} */ that.is_dirty = function() { return false; }; /** * Whether we can switch to different facet. * @returns {boolean} */ that.can_leave = function() { return !that.is_dirty(); }; /** * Get dialog displaying a message explaining why we can't switch facet. * User can supply callback which is called when a leave is permitted. * * TODO: rename to get_leave_dialog * * @param {Function} permit_callback */ that.show_leave_dialog = function(permit_callback) { var dialog = IPA.dirty_dialog({ facet: that }); dialog.callback = permit_callback; return dialog; }; /** * Display error page instead of facet content * * Use this call when unrecoverable error occurs. * * @param {Object} error_thrown - error to be displayed * @param {string} error_thrown.name * @param {string} error_thrown.message */ that.report_error = function(error_thrown) { var add_option = function(ul, text, handler) { var li = $('
  • ').appendTo(ul); $('', { href: '#', text: text, click: function() { handler(); return false; } }).appendTo(li); }; var title = text.get('@i18n:error_report.title'); title = title.replace('${error}', error_thrown.name); that.error_container.empty(); that.error_container.append('

    '+title+'

    '); var details = $('
    ', { 'class': 'error-details' }).appendTo(that.error_container); details.append('

    '+error_thrown.message+'

    '); $('
    ', { text: text.get('@i18n:error_report.options') }).appendTo(that.error_container); var options_list = $('