/* 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', './builder', './ipa', './jquery', './navigation', './phases', './reg', './spec_util', './text', './dialog', './field', './widget' ], function(declare, lang, construct, on, Stateful, Evented, Singleton_registry, builder, IPA, $, navigation, phases, reg, su, text) { /** * Facet represents the content of currently displayed page. * * = Show, Clear, Refresh mechanism = * * Use cases: * a) Display facet with defined arguments. * b) Switch to facet * c) 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. * 5.1) First time show * a) creates DOM * b) display DOM * c) refresh(); * 5.2) 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: * a) clear() * b) refresh() * 2) 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 == * * */ var exp = {}; exp.facet_spec = {}; exp.facet = IPA.facet = function(spec, no_init) { spec = spec || {}; var that = new Evented(); that.entity = IPA.get_entity(spec.entity); that.name = spec.name; that.label = text.get(spec.label); that.title = text.get(spec.title || that.label); that.tab_label = text.get(spec.tab_label || that.label); that.display_class = spec.display_class; that.no_update = spec.no_update; that.disable_breadcrumb = spec.disable_breadcrumb; that.disable_facet_tabs = spec.disable_facet_tabs; that.action_state = builder.build('', spec.state || {}, {}, { $factory: exp.state }); that.actions = builder.build('', { actions: spec.actions }, {}, { $factory: exp.action_holder } ); that.header_actions = spec.header_actions; that.header = spec.header || IPA.facet_header({ facet: that }); that._needs_update = spec.needs_update; that.expired_flag = true; that.last_updated = null; that.expire_timeout = spec.expire_timeout || 600; //[seconds] that.on_update = IPA.observer(); that.post_load = IPA.observer(); that.dialogs = $.ordered_map(); /** * domNode of container * Suppose to contain domNode of this and other facets. */ that.container_node = spec.container_node; /** * FIXME: that.container should be eliminated * now it's the same as domNode */ //that.container /** * domNode 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. */ that.domNode = null; // facet group name that.facet_group = spec.facet_group; that.redirect_info = spec.redirect_info; /** * Public state * */ that.state = new FacetState(); 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. * * @type String */ 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[] new pkeys to merge */ 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 0) pkeys.pop(); return pkeys; }; 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; }; that.reset_state = function(state) { if (state.pkeys) { state.pkeys = that.get_pkeys(state.pkeys); } that.state.reset(state); }; that.get_state = function() { return that.state.clone(); }; /** * Merges state into current and notifies it. */ that.set_state = function(state) { if (state.pkeys) { state.pkeys = that.get_pkeys(state.pkeys); } that.state.set(state); }; that.on_state_set = function(old_state, state) { that._on_state_change(state); }; 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.domNode) { 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(); } }; that._notify_state_change = function(state) { that.emit('facet-state-change', { facet: that, state: state }); }; that.get_dialog = function(name) { return that.dialogs.get(name); }; that.dialog = function(dialog) { that.dialogs.put(dialog.name, dialog); return that; }; that.create = function() { var entity_name = !!that.entity ? that.entity.name : ''; if (that.domNode) { that.domNode.empty(); that.domNode.detach(); } else { that.domNode = $('
', { 'class': 'facet', name: that.name, 'data-name': that.name, 'data-entity': entity_name }); } var domNode = that.domNode; that.container = domNode; if (!that.container_node) throw { error: 'Can\'t create facet. No container node defined.' }; var node = domNode[0]; construct.place(node,that.container_node); if (that.disable_facet_tabs) domNode.addClass('no-facet-tabs'); domNode.addClass(that.display_class); that.header_container = $('
', { 'class': 'facet-header' }).appendTo(domNode); that.create_header(that.header_container); that.content = $('
', { 'class': 'facet-content' }).appendTo(domNode); that.error_container = $('
', { 'class': 'facet-content facet-error' }).appendTo(domNode); that.create_content(that.content); }; that.create_header = function(container) { that.header.create(container); that.controls = $('
', { 'class': 'facet-controls' }).appendTo(container); }; that.create_content = function(container) { }; that.create_control_buttons = function(container) { if (that.control_buttons) { that.control_buttons.create(container); } }; that.set_title = function(container, title) { var element = $('h1', that.title_container); element.html(title); }; that.show = function() { that.entity.facet = that; // FIXME: remove if (!that.domNode) { that.create(); var state = that.state.clone(); var needs_update = that.needs_update(state); that.old_state = state; if (needs_update) { that.clear(); } that.domNode.css('display', 'block'); that.show_content(); that.header.select_tab(); if (needs_update) { that.refresh(); } } else { that.domNode.css('display', 'block'); that.show_content(); that.header.select_tab(); } }; that.show_content = function() { that.content.css('display', 'block'); that.error_container.css('display', 'none'); }; that.show_error = function() { that.content.css('display', 'none'); that.error_container.css('display', 'block'); }; that.error_displayed = function() { return that.error_container && that.error_container.css('display') === 'block'; }; that.hide = function() { that.domNode.css('display', 'none'); }; that.load = function(data) { that.data = data; that.header.load(data); }; that.refresh = function() { }; that.clear = function() { }; 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; }; that.set_expired_flag = function() { that.expired_flag = true; }; that.clear_expired_flag = function() { that.expired_flag = false; that.last_updated = Date.now(); }; that.is_dirty = function() { return false; }; /** * Wheater we can switch to different facet. * @returns Boolean */ that.can_leave = function() { return !that.is_dirty(); }; /** * Show dialog displaying a message explaining why we can't switch facet. * User can supply callback which is called when a leave is permitted. * * Listeneres should set 'callback' property to listen state evaluation. */ that.show_leave_dialog = function(permit_callback) { var dialog = IPA.dirty_dialog({ facet: that }); dialog.callback = permit_callback; return dialog; }; 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 = $('