diff options
author | Petr Vobornik <pvoborni@redhat.com> | 2012-12-14 16:39:20 +0100 |
---|---|---|
committer | Petr Vobornik <pvoborni@redhat.com> | 2013-05-06 16:22:17 +0200 |
commit | 693dc560620d52dc24a0ab89e20147b10ed4f469 (patch) | |
tree | a748bc7a89f76d8ea4cdf1dc1a91895192a7ea56 | |
parent | a4d9e19c79b60b8f7269141374b2e3b6c0d66c45 (diff) | |
download | freeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.tar.gz freeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.tar.xz freeipa-693dc560620d52dc24a0ab89e20147b10ed4f469.zip |
Menu and application controller refactoring
https://fedorahosted.org/freeipa/ticket/3235
https://fedorahosted.org/freeipa/ticket/3236
-rw-r--r-- | install/ui/index.html | 41 | ||||
-rw-r--r-- | install/ui/ipa.css | 136 | ||||
-rw-r--r-- | install/ui/jsl.conf | 2 | ||||
-rw-r--r-- | install/ui/src/freeipa/Application_controller.js | 323 | ||||
-rw-r--r-- | install/ui/src/freeipa/app.js | 133 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation/Menu.js | 245 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation/Router.js | 337 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation/menu_spec.js | 107 | ||||
-rw-r--r-- | install/ui/src/freeipa/navigation2.js | 150 | ||||
-rw-r--r-- | install/ui/src/freeipa/widgets/App.js | 193 | ||||
-rw-r--r-- | install/ui/src/freeipa/widgets/Menu.js | 271 |
11 files changed, 1742 insertions, 196 deletions
diff --git a/install/ui/index.html b/install/ui/index.html index 5dc70db56..0523df4d1 100644 --- a/install/ui/index.html +++ b/install/ui/index.html @@ -28,43 +28,6 @@ </script> </head> -<body> - - <div id="container"> - - <div id="background"> - <div id="background-header"></div> - <div id="background-navigation"></div> - <div id="background-left"></div> - <div id="background-center"></div> - <div id="background-right"></div> - </div> - - <div id="header"> - <span class="header-logo"> - <a href="#"><img src="images/ipa-logo.png" /><img src="images/ipa-banner.png" /></a> - </span> - <span class="header-right"> - <span class="header-passwordexpires"></span> - <span id="loggedinas" class="header-loggedinas" style="visibility:hidden;"> - <a href="#"><span id="login_header">Logged in as</span>: <span class="login"></span></a> - </span> - <span class="header-loggedinas" style="visibility:hidden;"> - | <a href="#logout" id="logout">Logout</a> - </span> - <span id="header-network-activity-indicator" class="network-activity-indicator"> - <img src="images/spinner-header.gif" /> - </span> - </span> - </div> - - <div id="navigation"></div> - - <div id="content"></div> - - </div> - -</body> - -</html> +<body></body> +</html>
\ No newline at end of file diff --git a/install/ui/ipa.css b/install/ui/ipa.css index 3e443d54e..11b9aa7f3 100644 --- a/install/ui/ipa.css +++ b/install/ui/ipa.css @@ -280,7 +280,7 @@ body { } /* ---- Navigation ---- */ -#navigation { +.navigation { position: absolute; top: 34px; left: 6px; @@ -288,64 +288,61 @@ body { height: 102px; } -#navigation.tabs-3 { - height: 150px; -} - -div.tabs { - width: 100%; - min-height: 4em; - background: transparent; -} - -.tabs.ui-tabs, .tabs .ui-tabs { - padding: 0; +.navigation ul { + list-style-type: none; } -/* ---- Tabs level 1 ---- */ - -.tabs.ui-widget { - border: none; +.navigation .submenu li { + float: left; + position: relative; + list-style: none; + white-space:nowrap; } +/* +.navigation.tabs-3 { + height: 150px; +}*/ -.tabs1 > .ui-tabs-nav { - background: transparent; +.submenu { + width: 100%; +/* min-height: 4em; + background: transparent;*/ } -.tabs1 > .ui-tabs-nav > .ui-state-hover { - background: url(images/hover-tab.png); -} +/* ---- Navigation level 1 ---- */ -.tabs1 > .ui-tabs-nav { - padding: 33px 0 0; +.menu-level-1 > ul { + height: 38px; + padding: 34px 0 0; margin: 0; - border: none; +/* border: none;*/ } -.tabs1 > .ui-tabs-nav li { - -moz-border-radius: 0 !important; - -webkit-border-radius: 0 !important; - border-radius: 0 !important; +.menu-level-1 > ul > li { + height: 36px; + padding: 0 18px; border: 1px solid #A0A0A0; - background: none; + border-bottom:none; background-image: url(images/mainnav-tab-off.png); margin: 0 0.4em 0 0; text-align: center; vertical-align:baseline; } -.tabs1 > .ui-tabs-nav > li.ui-tabs-selected { - padding: 0; +.menu-level-1 > ul > li.ui-state-hover, +.menu-level-1 > ul > li:hover { + background: url(images/hover-tab.png); +} + +.menu-level-1 > ul > li.selected { + padding-bottom: 1px; background-image: url(images/mainnav-tab-on.png); - text-align: center; } -.tabs1 > .ui-tabs-nav > li > a { - -moz-border-radius: 0 !important; - -webkit-border-radius: 0 !important; +.menu-level-1 > ul > li > a { font-family: "Overpass Bold","Liberation Sans", Arial, sans-serif; min-width: 5em; - height: 20px; + line-height: 38px; color: #858585; margin: 0 auto; text-align:center; @@ -353,54 +350,41 @@ div.tabs { text-shadow: 1px 1px 0 #FFFFFF; } -.tabs1 > .ui-tabs-nav > li > a:link, -span.main-nav-off > a:visited{ - color: #858585; -} - -.tabs1 > .ui-tabs-nav > li.ui-tabs-selected > a { +.menu-level-1 > ul > li.selected > a { color: #1e5e05; } -.tabs1 .ui-tabs-panel { + +/* ---- Navigation level 2 ---- */ + +.menu-level-2 { display: block; border-width: 0; padding: 0 0 0 0; background-color: transparent; } -/* ---- Tabs level 2 ---- */ - -.tabs2 { -} - -.tabs2 > .ui-tabs-nav { +.menu-level-2 > ul { padding: 5px 24px 1px; margin: 0; height: 25px; - border: none; - -moz-border-radius: 0; - -webkit-border-radius: 0; - border-radius: 0; - background: transparent; } -.tabs2 > .ui-tabs-nav > li { +.menu-level-2 > ul > li { width: auto; margin: 0; - background: none repeat scroll 0 0 transparent !important; color: white; - border: none; + padding-top: 3px; } -.tabs2 > .ui-tabs-nav > li.ui-tabs-selected { +.menu-level-2 > ul > li.selected { background: url(images/nav-arrow.png) no-repeat scroll center 2.1em transparent !important; - height: 3.1em; + height: 31px; border: none; margin: 0; } -.tabs2 > .ui-tabs-nav > li > a { +.menu-level-2 > ul > li > a { width:auto; padding: 0.3em 0.8em ; -moz-border-radius: 2em !important; @@ -412,38 +396,28 @@ span.main-nav-off > a:visited{ margin: 0 0.3em; } -.tabs2 > .ui-tabs-nav li > a:link, -span.main-nav-off > a:visited { - color: #333333; -} - -.tabs2 > .ui-tabs-nav > li.ui-tabs-selected > a, -.tabs2 > .ui-tabs-nav > li > a:hover { +.menu-level-2 > ul > li.selected > a, +.menu-level-2 > ul > li > a:hover { background-color:#EEEEEE; color: #164304; text-shadow: 1px 1px 0 #FFFFFF; } -/* ---- Tabs level 3 ---- */ - -.tabs3 { +/* ---- Navigation level 3 ---- */ +.menu-level-3 { height: 28px; } -.tabs3 > .ui-tabs-nav { - padding: 1em 22px 0.1em; - border: none; - background: transparent; +.menu-level-3 > ul { + padding: 0 22px 0.1em; } -.tabs3 > .ui-tabs-nav > li { - background: transparent; - border: 0; +.menu-level-3 > ul > li { margin: 0 2.4em 1px 0; } -.tabs3 > .ui-tabs-nav > li > a { +.menu-level-3 > ul > li > a { width: auto; margin: 0; padding: 0.3em 0 0.3em 0; @@ -453,7 +427,7 @@ span.main-nav-off > a:visited { color: #858585; } -.tabs3 > .ui-tabs-nav > li.ui-tabs-selected > a { +.menu-level-3 > ul > li.selected > a { font-family: "Overpass Bold", "Liberation Sans", Arial, sans-serif; color: #1e5e05; } @@ -467,7 +441,7 @@ span.main-nav-off > a:visited { bottom: 10px; } -#content.tabs-3 { +#content.nav-space-3 { top: 175px; } diff --git a/install/ui/jsl.conf b/install/ui/jsl.conf index 4d9f9ef13..e9e3ecc5e 100644 --- a/install/ui/jsl.conf +++ b/install/ui/jsl.conf @@ -132,5 +132,7 @@ +process src/libs/jquery.ordered-map.js +process src/freeipa/*.js +process src/freeipa/_base/*.js ++process src/freeipa/navigation/*.js ++process src/freeipa/widgets/*.js +process src/*.js +process ./*.js
\ No newline at end of file diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js new file mode 100644 index 000000000..6406e8bc4 --- /dev/null +++ b/install/ui/src/freeipa/Application_controller.js @@ -0,0 +1,323 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + +/** + * Application controller + * + * Controls interaction between navigation, menu and facets. + */ + +define(['dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/_base/array', + 'dojo/on', + 'dojo/topic', + 'dojo/query', + 'dojo/dom-class', + './widgets/App', + './ipa', + './navigation/Menu', + './navigation/Router', + './navigation/menu_spec' + ], + function(declare, lang, array, on, topic, query, dom_class, + App_widget, IPA, Menu, Router, menu_spec) { + + /** + * Main application + * + * This class serves as top level widget. It creates basic UI: controls + * rendering of header, footer and placeholder for facets. + */ + var App = declare(null, { + + app_widget: null, + + router: null, + + menu: null, + + initialized: false, + + facet_changing: false, + + init: function() { + this.menu = new Menu(); + this.router = new Router(); + this.app_widget = new App_widget(); + this.app_widget.menu_widget.set_menu(this.menu); + this.app_widget.container_node = query('body')[0]; + + on(this.app_widget.menu_widget, 'item-select', lang.hitch(this, this.on_menu_click)); + on(this.app_widget, 'profile-click', lang.hitch(this, this.on_profile)); + on(this.app_widget, 'logout-click', lang.hitch(this, this.on_logout)); + on(this.menu, 'selected', lang.hitch(this, this.on_menu_select)); + + topic.subscribe('facet-show', lang.hitch(this, this.on_facet_show)); + topic.subscribe('facet-change', lang.hitch(this, this.on_facet_change)); + topic.subscribe('facet-change-canceled', lang.hitch(this, this.on_facet_canceled)); + topic.subscribe('phase-error', lang.hitch(this, this.on_phase_error)); + topic.subscribe('facet-state-change', lang.hitch(this, this.on_facet_state_changed)); + + this.app_widget.render(); + }, + + /** + * Gets: + * * metadata + * * server configuration + * * user information + */ + get_configuration: function(success_handler, error_handler) { + IPA.init({ on_success: success_handler, on_error: error_handler}); + }, + + /** + * Deduces current application profile - administraion or self-service. + * Initializes profiles's menu. + */ + choose_profile: function() { + + // TODO: change IPA.whoami.cn[0] to something readable + this.update_logged_in(true, IPA.whoami.cn[0]); + var selfservice = this.is_selfservice(); + + + this.app_widget.menu_widget.ignore_changes = true; + + if (selfservice) { + this.menu.name = menu_spec.self_service.name; + this.menu.add_items(menu_spec.self_service.items); + } else { + this.menu.name = menu_spec.admin.name; + this.menu.add_items(menu_spec.admin.items); + } + + this.app_widget.menu_widget.ignore_changes = false; + this.app_widget.menu_widget.render(); + this.app_widget.menu_widget.select(this.menu.selected); + + // now we are ready for displaying a facet + // cat match a facet if hash is set + this.router.startup(); + + // choose default facet if not defined by route + if (!this.current_facet) { + if (selfservice) { + this.on_profile(); + } else { + this.router.navigate_to_entity_facet('user', 'search'); + } + } + }, + + is_selfservice: function() { + var whoami = IPA.whoami; + var self_service = true; + + + if (whoami.hasOwnProperty('memberof_group') && + whoami.memberof_group.indexOf('admins') !== -1) { + self_service = false; + } else if (whoami.hasOwnProperty('memberofindirect_group')&& + whoami.memberofindirect_group.indexOf('admins') !== -1) { + self_service = false; + } else if (whoami.hasOwnProperty('memberof_role') && + whoami.memberof_role.length > 0) { + self_service = false; + } else if (whoami.hasOwnProperty('memberofindirect_role') && + whoami.memberofindirect_role.length > 0) { + self_service = false; + } + + IPA.is_selfservice = self_service; // quite ugly, needed for users + + return self_service; + }, + + update_logged_in: function(logged_in, fullname) { + this.app_widget.set('logged', logged_in); + this.app_widget.set('fullname', fullname); + }, + + on_profile: function() { + this.router.navigate_to_entity_facet('user', 'details', [IPA.whoami.uid[0]]); + }, + + on_logout: function(event) { + IPA.logout(); + }, + + on_phase_error: function(error) { + // FIXME: CHANGE!!! + window.alert('Initialization error, have a coffee and relax.'); +// var container = $('#content').empty(); +// container.append('<p>Error: '+error_thrown.name+'</p>'); +// container.append('<p>'+error_thrown.message+'</p>'); + }, + + on_facet_change: function(event) { + //this.facet_changing = true; + var new_facet = event.facet; + var current_facet = this.current_facet; + + if (current_facet && !current_facet.can_leave()) { + var permit_clb = function() { + // Some facet's might not call reset before this call but after + // so they are still dirty. Calling reset prevent's opening of + // dirty dialog again. + if (current_facet.is_dirty()) current_facet.reset(); //TODO change + this.router.navigate_to_hash(event.hash, event.facet); + }; + + var dialog = current_facet.show_leave_dialog(permit_clb); + this.router.canceled = true; + dialog.open(); + } + }, + + on_facet_canceled: function(event) { + }, + + on_facet_state_changed: function(event) { + if (event.facet === this.current_facet) { + var hash = this.router.create_hash(event.facet, event.state); + this.router.update_hash(hash, true); + } + }, + + on_facet_show: function(event) { + var facet = event.facet; + + // update menu + var menu_item = this._find_menu_item(facet); + if (menu_item) this.menu.select(menu_item); + + if (!facet.container) { + facet.container_node = this.app_widget.content_node; + } + if (this.current_facet) { + this.current_facet.hide(); + } + this.current_facet = facet; + facet.show(); + }, + + _find_menu_item: function(facet) { + + var items; + + // entity facets + if (facet.entity) { + items = this.menu.query({ entity: facet.entity.name, facet: facet.name }); + } + + // normal facets + if (!items.total) { + items = this.menu.query({ facet: facet.name }); + } + + // entity fallback + if (!items.total && facet.entity) { + items = this.menu.query({ entity: facet.entity.name }); + } + + // fallback: Top level item + if (!items.total) { + items = this.menu.query({ parent: null }); + } + + // select first + if (items.total) { + return items[0]; + } + }, + + /** + * Tries to find menu item with assigned facet and navigate to it. + */ + on_menu_click: function(menu_item) { + this._navigate_to_menu_item(menu_item); + }, + + _navigate_to_menu_item: function(menu_item) { + + if (menu_item.entity) { + // entity pages + this.router.navigate_to_entity_facet( + menu_item.entity, + menu_item.facet, + menu_item.pkeys, + menu_item.args); + } else if (menu_item.facet) { + // concrete facets + this.router.navigate_to_facet(menu_item.facet, menu_item.args); + } else { + // categories, select first posible child + var children = this.menu.query({parent: menu_item.name }); + if (children.total) { + var success = false; + for (var i=0; i<children.total;i++) { + success = this._navigate_to_menu_item(children[i]); + if (success) break; + } + } else { + return false; + } + } + + return true; + }, + + /** + * Watches menu changes and adjusts facet space when there is + * a need for larger menu space. + * + * Show extended menu space when: + * * there is 3+ levels of menu + * + * Don't show when: + * * all items of levels 3+ are hidden + */ + on_menu_select: function(select_state) { + + var has_visible = function(query_result) { + for (var i=0; i<query_result.total; i++) { + if (!query_result[i].hidden) return true; + } + return false; + }; + + var item = select_state.item; + var visible_simblings = has_visible(this.menu.query({parent: item.parent})); + var visible_children = has_visible(this.menu.query({parent: item.name})); + + var levels = select_state.new_selection.length; + + var three_levels = levels >= 3 && (visible_children > 0 || visible_simblings > 0); + + dom_class.toggle(this.app_widget.content_node, + 'nav-space-3', + three_levels); + } + }); + + return App; +});
\ No newline at end of file diff --git a/install/ui/src/freeipa/app.js b/install/ui/src/freeipa/app.js index 3dcb10f49..88c8c2bab 100644 --- a/install/ui/src/freeipa/app.js +++ b/install/ui/src/freeipa/app.js @@ -18,12 +18,16 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -// -// AMD Wrapper for json2 library -// - +/** + * Application wrapper + */ define([ //core + 'dojo/_base/lang', + 'dojo/Deferred', + './phases', + './Application_controller', + 'exports', // for circullar deps './ipa', './jquery', './navigation', @@ -50,78 +54,55 @@ define([ './trust', './user', 'dojo/domReady!' -],function(IPA, $) { - - /* main loop (hashchange event handler) */ - function window_hashchange(evt){ - IPA.nav.update(); - } - - function create_navigation() { - var whoami = IPA.whoami; - var factory; - - - if (whoami.hasOwnProperty('memberof_group') && - whoami.memberof_group.indexOf('admins') !== -1) { - factory = IPA.admin_navigation; - } else if (whoami.hasOwnProperty('memberofindirect_group')&& - whoami.memberofindirect_group.indexOf('admins') !== -1) { - factory = IPA.admin_navigation; - } else if (whoami.hasOwnProperty('memberof_role') && - whoami.memberof_role.length > 0) { - factory = IPA.admin_navigation; - } else if (whoami.hasOwnProperty('memberofindirect_role') && - whoami.memberofindirect_role.length > 0) { - factory = IPA.admin_navigation; - } else { - factory = IPA.self_serv_navigation; - } - - return factory({ - container: $('#navigation'), - content: $('#content') - }); - } - - - function init_on_success(data, text_status, xhr) { - $(window).bind('hashchange', window_hashchange); - - var whoami = IPA.whoami; - IPA.whoami_pkey = whoami.uid[0]; - $('#loggedinas .login').text(whoami.cn[0]); - $('#loggedinas a').fragment( - {'user-facet': 'details', 'user-pkey': IPA.whoami_pkey}, 2); - - $('#logout').click(function() { - IPA.logout(); - return false; - }).text(IPA.messages.login.logout); - - $('.header-loggedinas').css('visibility','visible'); - IPA.update_password_expiration(); - - IPA.nav = create_navigation(); - IPA.nav.create(); - IPA.nav.update(); - - $('#login_header').html(IPA.messages.login.header); - } - +],function(lang, Deferred, phases, Application_controller, exports) { + + var app = { + + /** + * Application instance + */ + app: null, + + /** + * Application class + */ + App_class: Application_controller, + + /** + * Phases registration + */ + register_phases: function() { + + phases.on('app-init', lang.hitch(this, function() { + var app = this.app = new this.App_class(); + app.init(); + return app; + })); + + phases.on('metadata', lang.hitch(this, function() { + var deferred = new Deferred(); + + this.app.get_configuration(function(success) { + deferred.resolve(success); + }, function(error) { + deferred.reject(error); + }); + + return deferred.promise; + })); + + phases.on('profile', lang.hitch(this, function() { + this.app.choose_profile(); + })); + }, + + run: function() { + this.register_phases(); + phases.controller.run(); + } + }; - function init_on_error(xhr, text_status, error_thrown) { - var container = $('#content').empty(); - container.append('<p>Error: '+error_thrown.name+'</p>'); - container.append('<p>'+error_thrown.message+'</p>'); - } + lang.mixin(exports, app); - return { - run: function() { - IPA.init({ - on_success: init_on_success, - on_error: init_on_error - }); - } - }; + return exports; });
\ No newline at end of file diff --git a/install/ui/src/freeipa/navigation/Menu.js b/install/ui/src/freeipa/navigation/Menu.js new file mode 100644 index 000000000..7b1a0ecce --- /dev/null +++ b/install/ui/src/freeipa/navigation/Menu.js @@ -0,0 +1,245 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + + +define(['dojo/_base/declare', + 'dojo/store/Memory', + 'dojo/_base/array', + 'dojo/_base/lang', + 'dojo/store/Observable', + 'dojo/Evented', + '../_base/i18n', + '../ipa' // TODO: remove dependance + ], function(declare, Memory_store, array, lang, Observable, Evented, i18n, IPA) { + +/** + * Menu store + * + * Maintains menu hierarchy and selection state. + */ +return declare([Evented], { + + /** + * Following properties can be specified in menu item spec: + * @property {String} name + * @property {String} label + * @property {String} title + * @property {Number} position: position among siblings + * @property {menu_item_spec_array} children + * @property {String} entity: entity name + * @property {String} facet: facet name + * @property {Boolean} hidden: menu item is no visible, but can serve for + * other evaluations (nested entities) + * + * Following properties are not in created menu item: + * - children + * + * + * Following properties can be stored in menu item at runtime: + * + * @property {Boolean} selected + * @property {String} parent: name of parent menu item + * @property {String} selected_child: last selected child. Can be set even + * if the child is not selected + * + */ + + /** + * Menu name + * @type String + */ + name: null, + + /** + * Dojo Store of menu items + * @type: Store + */ + items: null, + + /** + * Delimiter used in name creation + * To avoid having multiple menu items with the same name. + * @type String + */ + path_delimiter: '/', + + /** + * Selected menu items + * @type Array of menu items + */ + selected: [], + + /** + * Default search options: sort by position + * + * @type Object + */ + search_options: { sort: [{attribute:'position'}]}, + + /** + * Takes a spec of menu item. + * Normalizes item's name, parent, adds children if specified + * + * @param {menu_item} items + * @param {String|menu_item} parent + * @param {Object} options + */ + add_item: function(item, parent, options ) { + + item = lang.clone(item); //don't modify original spec + + // each item must have a name and a label + // FIXME: consider to move entity and facet stuff outside of this object + item.name = item.name || item.facet || item.entity; + if (!name) throw 'Missing menu item property: name'; + if (item.label) item.label = i18n.message(item.label); + if (item.title) item.title = i18n.message(item.title); + + if (item.entity) { + // FIXME: replace with 'entities' module in future + var entity = IPA.get_entity(item.entity); + if (!entity) return; //quit + //item.name = entity.name; + if (!item.label) item.label = entity.label; + if (!item.title) item.title = entity.title; + } //else if (item.facet) { + // TODO: uncomment when facet repository implemented +// var facet = facets.(item.facet); +// item.name = facet.name; +// if (!item.label) item.label = facet.label; +// if (!item.title) item.title = facet.title; +// } + + item.selected = false; + + // check parent + if (typeof parent === 'string') { + parent = this.items.get(parent); + if (!parent) throw 'Menu item\'s parent doesn\t exist'; + } else if (typeof parent === 'object') { + if (!this.items.getIdentity(parent)) { + throw 'Supplied parent isn\'t menu item'; + } + } + + var parent_name = parent ? parent.name : null; + var siblings = this.items.query({ parent: parent_name }); + if (!item.position) item.position = siblings.total; + // TODO: add reordering of siblings when position set + + item.parent = parent_name; + if (parent) { + // names have to be unique + item.name = parent.name + this.path_delimiter + item.name; + } + + // children will be added separatelly + var children = item.children; + delete item.children; + + // finally add the item + this.items.add(item); + + // add children + if (children) { + array.forEach(children, function(child) { + this.add_item(child, item); + }, this); + } + }, + + add_items: function(/* Array */ items) { + array.forEach(items, function(item) { + this.add_item(item); + }, this); + }, + + /** + * Query internal data store by using default search options. + * + * @param Object Query filter + * @return QueryResult + */ + query: function(query) { + return this.items.query(query, this.search_options); + }, + + /** + * Marks item and all its parents as selected. + */ + _select: function(item) { + + item.selected = true; + this.selected.push(item); + this.items.put(item); + + if (item.parent) { + var parent = this.items.get(item.parent); + this._select(parent); + } + }, + + /** + * Selects a menu item and all it's ancestors. + * @param {string|menu_item} Menu item to select + */ + select: function(item) { + + if (typeof item == 'string') { + item = this.items.get(item); + } + + // FIXME: consider to raise an exception + if (!item || !this.items.getIdentity(item)) return false; + + // deselect previous + var old_selection = lang.clone(this.selected); + array.forEach(this.selected, function(mi) { + mi.selected = false; + this.items.put(mi); + }, this); + this.selected = []; + + // select new + this._select(item); + + var select_state = { + item: item, + new_selection: this.selected, + old_selection: old_selection + }; + + this.emit('selected', select_state); + return select_state; + }, + + constructor: function(spec) { + spec = spec || {}; + this.items = new Observable( new Memory_store({ + idProperty: 'name' + })); + + spec = lang.clone(spec); + this.add_items(spec.items || []); + delete spec.items; + declare.safeMixin(this, spec); + } +}); //declare freeipa.menu +}); //define
\ No newline at end of file diff --git a/install/ui/src/freeipa/navigation/Router.js b/install/ui/src/freeipa/navigation/Router.js new file mode 100644 index 000000000..286ac4634 --- /dev/null +++ b/install/ui/src/freeipa/navigation/Router.js @@ -0,0 +1,337 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + +define(['dojo/_base/declare', + 'dojo/router', + 'dojo/_base/lang', + 'dojo/_base/array', + 'dojo/io-query', + 'dojo/topic', + '../entities', + '../facets', + '../ipa' //TODO: remove dependancy + ], + function(declare, router, lang, array, ioquery, topic, entities, facets, IPA) { + + /** + * Class navigation + * This component keeps menu and routes in sync. It signalizes + * other components to display facet by sending 'show-facet' event. + * Other components can use navigate_to_* methods to change currently + * displayed facet. This change can be canceled in 'facet-change' + * event handler. + */ + var navigation = declare(null, { + + /** + * Holds references to register route handlers. + * Can be used for unregistering routes. + * @type Array + */ + route_handlers: [], + + /** + * Prefix of all routes for this navigation. Useful for multiple + * navigation objects in one application. + * @type String + */ + route_prefix: '', + + /** + * Variations of entity routes + */ + entity_routes: [ + '/e/:entity/:facet/:pkeys/*args', + '/e/:entity/:facet//*args', + '/e/:entity/:facet/:pkeys', + '/e/:entity/:facet', + '/e/:entity' + ], + + /** + * Variations of simple page routes + */ + page_routes: [ + '/p/:page/*args', + '/p/:page' + ], + + /** + * Used during facet changing. Set it to true in 'facet-change' + * event handler to stop the change. + * @type Boolean + */ + canceled: false, + + /** + * Flag to indicate that next hash change should not invoke showing a + * facet. + * Main purpose: updating hash. + * @type Boolen + */ + ignore_next: false, + + + /** + * Register a route-handler pair to a dojo.router + * Handler will be run in context of this object + * + * @param {string|array} route or routes to register + * @param {function} handler to be associated with the route(s) + */ + register_route: function(route, handler) { + // TODO: add multiple routes for one handler + route = this.route_prefix + route; + this.route_handlers.push(router.register(route, lang.hitch(this, handler))); + }, + + /** + * Initializates router + * - registers handlers + */ + init_router: function() { + + // entity pages + array.forEach(this.entity_routes, function(route) { + this.register_route(route, this.entity_route_handler); + }, this); + + // special pages + this.register_route(this.page_routes, this.page_route_handler); + }, + + /** + * Handler for entity routes + * Shouldn't be invoked directly. + */ + entity_route_handler: function(event) { + + if (this.check_clear_ignore()) return; + + var entity_name = event.params.entity; + var facet_name = event.params.facet; + var pkeys = this._decode_pkeys(event.params.pkeys || ''); + var args = ioquery.queryToObject(event.params.args || ''); + args.pkeys = pkeys; + + // set new facet state + //var entity = entities.get(entity_name); + var entity = IPA.get_entity(entity_name); // TODO: replace with prev line + var facet = entity.get_facet(facet_name); + facet.set_state(args); + + this.show_facet(facet); + }, + + /** + * General facet route handler + * Shouldn't be invoked directly. + */ + page_route_handler: function(event) { + + if (this.check_clear_ignore()) return; + + var facet_name = event.params.page; + var args = ioquery.queryToObject(event.params.args || ''); + +// // Find menu item +// var items = this.menu.items.query({ page: facet_name }); +// +// // Select menu item +// if (items.total > 0) { +// this.menu.select(items[items.total-1]); +// } + + // set new facet state + var facet = facets.get(facet_name); + facet.set_state(args); + + this.show_facet(facet); + }, + + /** + * Used for switching to entitie's facets. Current target facet + * state is used as params (pkeys, args) when none of pkeys and args + * are used (useful for switching to previous page with keeping the context). + */ + navigate_to_entity_facet: function(entity_name, facet_name, pkeys, args) { + + //var entity = entities.get(entity_name); + var entity = IPA.get_entity(entity_name); // TODO: replace with prev line + var facet = entity.get_facet(facet_name); + + if (!facet) return false; // TODO: maybe replace with exception + + // Use current state if none supplied + if (!pkeys && !args) { + args = facet.get_state(); + } + args = args || {}; + + // Facets may be nested and require more pkeys than supplied. + args.pkeys = facet.get_pkeys(pkeys); + + var hash = this._create_entity_facet_hash(facet, args); + return this.navigate_to_hash(hash, facet); + }, + + /** + * Navigate to other facet. + */ + navigate_to_facet: function(facet_name, args) { + + // TODO: uncoment when `facets` are implemented +// var facet = facets.get(facet_name); +// if (!args) args = facet.get_args(); +// var hash = this._create_facet_hash(facet, { args: args }); +// return this.navigate_to_hash(hash, facet); + }, + + /** + * Low level function. + * + * Public usage should be limited reinitializing canceled navigations. + */ + navigate_to_hash: function(hash, facet) { + + this.canceled = false; + topic.publish('facet-change', { facet: facet, hash: hash }); + if (this.canceled) { + topic.publish('facet-change-canceled', { facet: facet, hash : hash }); + return false; + } + this.update_hash(hash, false); + return true; + }, + + /** + * Changes hash to supplied + * + * @param {String} Hash to set + * @param {Boolean} Whether to suppress following hash change handler + */ + update_hash: function(hash, ignore_change) { + this.ignore_next = !!ignore_change; + router.go(hash); + }, + + /** + * Returns and resets `ignore_next` property. + */ + check_clear_ignore: function() { + var ignore = this.ignore_next; + this.ignore_next = false; + return ignore; + }, + + /** + * Creates from facet state appropriate hash. + */ + _create_entity_facet_hash: function(facet, state) { + state = lang.clone(state); + var entity_name = facet.entity.name; + var pkeys = this._encode_pkeys(state.pkeys || []); + delete state.pkeys; + var args = ioquery.objectToQuery(state || {}); + + var hash = [this.route_prefix, 'e', entity_name, facet.name]; + if (!IPA.is_empty(args)) hash.push(pkeys, args); + else if (!IPA.is_empty(pkeys)) hash.push(pkeys); + + hash = hash.join('/'); + return hash; + }, + + /** + * Creates hash of general facet. + */ + _create_facet_hash: function(facet, state) { + var args = ioquery.objectToQuery(state.args || {}); + var hash = [this.route_prefix, 'p', facet.name]; + + if (!IPA.is_empty(args)) hash.push(args); + hash = hash.join('/'); + return hash; + }, + + /** + * Creates hash from supplied facet and state. + * + * @param {facet} facet + * @param {Object} state + */ + create_hash: function(facet, state) { + if (facet.entity) return this._create_entity_facet_hash(facet, state); + else return this._create_facet_hash(facet, state); + }, + + + /** + * Tells other component to show given facet. + */ + show_facet: function(facet) { + + topic.publish('facet-show', { + facet: facet + }); + }, + + /** + * URI Encodes array items and delimits them by '&' + * Example: ['foo ', 'bar'] => 'foo%20&bar' + */ + _encode_pkeys: function(pkeys) { + + var ret = []; + array.forEach(pkeys, function(pkey) { + ret.push(encodeURIComponent(pkey)); + }); + return ret.join('&'); + }, + + /** + * Splits strings by '&' and return an array of URI decoded parts. + * Example: 'foo%20&bar' => ['foo ', 'bar'] + */ + _decode_pkeys: function(str) { + + var keys = str.split('&'); + for (var i=0; i<keys.length; i++) { + keys[i] = decodeURIComponent(keys[i]); + } + return keys; + }, + + /** + * Starts routing + */ + startup: function() { + router.startup(); + }, + + constructor: function(spec) { + spec = spec || {}; + this.init_router(); + } + + }); + + return navigation; +}); diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js new file mode 100644 index 000000000..06e49597f --- /dev/null +++ b/install/ui/src/freeipa/navigation/menu_spec.js @@ -0,0 +1,107 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + +define([], function() { + +var nav = {}; + nav.admin = { + name: 'admin', + items: [ + { + name: 'identity', + label: '@i18n:tabs.identity', + children: [ + { entity: 'user' }, + { entity: 'group' }, + { entity: 'host' }, + { entity: 'hostgroup' }, + { entity: 'netgroup' }, + { entity: 'service' }, + { + name:'dns', + label: '@i18n:tabs.dns', + children: [ + { entity: 'dnszone' }, + { entity: 'dnsconfig' }, + { entity: 'dnsrecord', hidden:true } + ] + } + ] + }, + {name: 'policy', label: '@i18n:tabs.policy', children: [ + {name: 'hbac', label: '@i18n:tabs.hbac', children: [ + {entity: 'hbacrule'}, + {entity: 'hbacsvc'}, + {entity: 'hbacsvcgroup'}, + {entity: 'hbactest'} + ]}, + {name: 'sudo', label: '@i18n:tabs.sudo', children: [ + {entity: 'sudorule'}, + {entity: 'sudocmd'}, + {entity: 'sudocmdgroup'} + ]}, + { + name:'automount', + label: '@i18n:tabs.automount', + entity: 'automountlocation', + children:[ + {entity: 'automountlocation', hidden:true}, + {entity: 'automountmap', hidden: true}, + {entity: 'automountkey', hidden: true}] + }, + {entity: 'pwpolicy'}, + {entity: 'krbtpolicy'}, + {entity: 'selinuxusermap'}, + {name: 'automember', label: '@i18n:tabs.automember', + children: [ + { name: 'amgroup', entity: 'automember', + facet: 'searchgroup', label: '@i18n:objects.automember.usergrouprules'}, + { name: 'amhostgroup', entity: 'automember', + facet: 'searchhostgroup', label: '@i18n:objects.automember.hostgrouprules'} + ]} + ]}, + {name: 'ipaserver', label: '@i18n:tabs.ipaserver', children: [ + {name: 'rolebased', label: '@i18n:tabs.role', children: [ + {entity: 'role'}, + {entity: 'privilege'}, + {entity: 'permission'} + ]}, + {entity: 'selfservice'}, + {entity: 'delegation'}, + {entity: 'idrange'}, + {entity: 'trust'}, + {entity: 'config'} + ]} + ] +}; + +nav.self_service = { + name: 'self-service', + items: [ + { + name: 'identity', + label: '@i18n:tabs.identity', + children: [{entity: 'user'}] + } + ] +}; + +return nav; +});
\ No newline at end of file diff --git a/install/ui/src/freeipa/navigation2.js b/install/ui/src/freeipa/navigation2.js new file mode 100644 index 000000000..4278d0457 --- /dev/null +++ b/install/ui/src/freeipa/navigation2.js @@ -0,0 +1,150 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2013 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 <http://www.gnu.org/licenses/>. +*/ + +/** + * Navigation tells application to show certain facet. + * + * It's proxy for navigation/Router instace in current running + * application. + * + * Modules just use the interface, they don't have to care about the logic in + * the background. + */ +define([ + 'dojo/_base/lang', + './app', // creates circullar dependency + './ipa', + 'exports' // for handling circullar dependency + ], + function(lang, app, IPA, exports) { + + + var get_router = function() { + return app.app.router; + }, + + /** + * Sets property of params depending on arg type following way: + * for String sets params.facet + * for Facet sets params.facet (based on show function) + * for Object sets params.args + * for Array sets params.pkeys + * + * @param Object params + * @param {Object|Facet|String|Function} arg + */ + set_params = function(params, arg) { + if (lang.isArray(arg)) { + params.pkeys = arg; + } else if (typeof arg === 'object') { + + if (typeof arg.show === 'function') params.facet = arg; + else params.args = arg; + } else if (typeof arg === 'string') { + params.facet = arg; + } + }, + + /** + * Show facet. + * + * Takes 3 arguments: + * * facet(String or Facet) + * * pkeys (Array) + * * args (Object) + * + * Argument order is not defined. They are recognized based on their + * type. + * + * When facet is defined as a string it has to be registered in + * facet register. //FIXME: not yet implemented + * + * When it's an object (Facet) and has an entity set it will be + * dealt as entity facet. + * + */ + show = function(arg1, arg2, arg3) { + + var nav = get_router(); + var params = {}; + + set_params(params, arg1); + set_params(params, arg2); + set_params(params, arg3); + + var facet = params.facet; + + if (typeof facet === 'string') { + // FIXME: doesn't work at the moment + throw 'Not yet supported'; + //facet = IPA.get_facet(facet); + } + + if (!facet) throw 'Argument exception: missing facet'; + + if (facet && facet.entity) { + return nav.navigate_to_entity_facet( + facet.entity.name, + facet.name, + params.pkeys, + params.args); + } else { + return nav.navigate_to_facet(facet.name, params.args); + } + }, + + /** + * Show entity facet. + * + * @param String Enity name + * @param {Object|Facet|String|Function} arg1 + * @param {Object|Facet|String|Function} arg2 + * @param {Object|Facet|String|Function} arg3 + * + * arg1,arg2,arg3 are: + * facet name as String + * pkeys as Array + * args as Object + */ + show_entity = function(entity_name, arg1, arg2, arg3) { + var nav = get_router(); + var params = {}; + + set_params(params, arg1); + set_params(params, arg2); + set_params(params, arg3); + return nav.navigate_to_entity_facet(entity_name, params.facet, + params.pkeys, params.args); + }, + + show_default = function() { + // TODO: make configurable + return show_entity('user', 'search'); + }; + + // Module export + exports = { + show: show, + show_entity: show_entity, + show_default: show_default + }; + + return exports; +}); diff --git a/install/ui/src/freeipa/widgets/App.js b/install/ui/src/freeipa/widgets/App.js new file mode 100644 index 000000000..662d0ee0b --- /dev/null +++ b/install/ui/src/freeipa/widgets/App.js @@ -0,0 +1,193 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + +define(['dojo/_base/declare', + 'dojo/_base/lang', + 'dojo/_base/array', + 'dojo/dom', + 'dojo/dom-construct', + 'dojo/dom-prop', + 'dojo/dom-class', + 'dojo/dom-style', + 'dojo/query', + 'dojo/on', + 'dojo/Evented', + 'dojo/Stateful', + './Menu', + 'dojo/NodeList-dom' + ], + function(declare, lang, array, dom, construct, prop, dom_class, + dom_style, query, on, Stateful, Evented, Menu) { + + /** + * Main application widget + * + * This class serves as top level widget. It creates basic UI: controls + * rendering of header, footer and placeholder for facets. + * + * @name freeipa.widgets.app + * @class + */ + var app = declare([Stateful, Evented], { + + //widgets + menu_widget: null, + + //nodes: + + domNode: null, + + container_node: null, + + background_node: null, + + header_node: null, + + password_expires_node: null, + + logged_nodes: null, + + logged_user_node: null, + + logged_user_link_node: null, + + logout_link_node: null, + + menu_node: null, + + content_node: null, + + app_id: 'container', + + logged: false, + + _loggedSetter: function(value) { + this.logged = value; + if (this.logged_nodes) { + this.logged_nodes.style('visibility', value ? 'visible' : 'hidden'); + } + }, + + fullname: '', + + _fullnameSetter: function(value) { + this.fullname = value; + if (this.logged_user_node) { + prop.set(this.logged_user_node, 'textContent', value); + } + }, + + render: function() { + // TODO: this method may be split into several components + + + this.domNode = construct.create('div', { + id: this.app_id + }); + + if (this.container_node) { + construct.place(this.domNode, this.container_node); + } + + this._render_background(); + this._render_header(); + + this.menu_node = this.menu_widget.render(); + construct.place(this.menu_node, this.domNode); + + this.content_node = construct.create('div', { + id: 'content' + }, this.domNode); + }, + + _render_background: function() { + var inner_html = ''+ + '<div id="background-header"></div>'+ + '<div id="background-navigation"></div>'+ + '<div id="background-left"></div>'+ + '<div id="background-center"></div>'+ + '<div id="background-right"></div>'; + + this.background_node = construct.create('div', { + id: 'background', + innerHTML: inner_html + }, this.domNode); + }, + + _render_header: function() { + this.header_node = construct.create('div', { + id: 'header' + }, this.domNode); + + // logo + construct.place(''+ + '<span class="header-logo">'+ + '<a href="#"><img src="images/ipa-logo.png" />'+ + '<img src="images/ipa-banner.png" /></a>'+ + '</span>', this.header_node); + + // right part + construct.place(''+ + '<span class="header-right">'+ + '<span class="header-passwordexpires"></span>'+ + '<span id="loggedinas" class="header-loggedinas" style="visibility:hidden;">'+ + '<a href="#"><span id="login_header">Logged in as</span>: <span class="login"></span></a>'+ + '</span>'+ + '<span class="header-loggedinas" style="visibility:hidden;">'+ + ' | <a href="#logout" id="logout">Logout</a>'+ + '</span>'+ + '<span id="header-network-activity-indicator" class="network-activity-indicator">'+ + '<img src="images/spinner-header.gif" />'+ + '</span>'+ + '</span>', this.header_node); + + + this.password_expires_node = query('.header-passwordexpires', this.header_node)[0]; + this.logged_nodes = query('.header-loggedinas', this.header_node); + this.logged_header_node = dom.byId('login_header');// maybe ditch the id? + this.logged_user_node = query('#loggedinas .login', this.header_node)[0]; + this.logged_user_link_node = query('#loggedinas a', this.header_node)[0]; + this.logout_link_node = dom.byId('logout'); + + on(this.logout_link_node, 'click', lang.hitch(this,this.on_logout)); + on(this.logged_user_link_node, 'click', lang.hitch(this,this.on_profile)); + + construct.place(this.header_node, this.domNode); + }, + + on_profile: function(event) { + event.preventDefault(); + this.emit('profile-click'); + }, + + on_logout: function(event) { + event.preventDefault(); + this.emit('logout-click'); + }, + + constructor: function(spec) { + spec = spec || {}; + this.menu_widget = new Menu(); + } + + }); + + return app; +});
\ No newline at end of file diff --git a/install/ui/src/freeipa/widgets/Menu.js b/install/ui/src/freeipa/widgets/Menu.js new file mode 100644 index 000000000..0f69efa9d --- /dev/null +++ b/install/ui/src/freeipa/widgets/Menu.js @@ -0,0 +1,271 @@ +/* Authors: + * Petr Vobornik <pvoborni@redhat.com> + * + * Copyright (C) 2012 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 <http://www.gnu.org/licenses/>. +*/ + +define(['dojo/_base/declare', + 'dojo/_base/array', + 'dojo/_base/lang', + 'dojo/dom', + 'dojo/dom-construct', + 'dojo/dom-prop', + 'dojo/dom-class', + 'dojo/dom-style', + 'dojo/dom-attr', + 'dojo/query', + 'dojo/Evented', + 'dojo/on', + '../ipa'], function(declare, array, lang, dom, construct, prop, dom_class, + dom_style, attr, query, Evented, on, IPA) { + + return declare([Evented], { + /** + * @name freeipa.widget.menu + * @class + * + * Creates UI for freeipa.navigation.menu. Provides an event when + * a menu items is selected. + * + * event: item-select(menu_item) + */ + + + /** + * Object store of menu items + * @protected + * @type freeipa.navigation.menu + */ + menu: null, + + /** + * domNode of this widget. FIXME: move to superclass (none yet) + * @type Node + */ + domNode: null, + + /** + * Turns off update on data change + * @type Boolen + */ + ignore_changes: false, + + /** + * Css class for nodes containing a submenu of certain level_class + * @type String + */ + level_class: 'menu-level', + + /** + * Renders widget's elements + */ + render: function() { + if (this.domNode) { + construct.empty(this.domNode); + } else { + this.domNode = construct.create('div', { + 'class': 'navigation' + }); + } + if (this.menu) { + this._render_children(null, this.domNode, 1); + } + return this.domNode; + }, + + /** + * Render children of menu_item + * Top level items are rendered if menu_items is null + * + * @protected + * @param {menu_item|null} menu_item + * @param {Node} node + * @param {Number} level + */ + _render_children: function (menu_item, node, level) { + + var self = this; + var name = menu_item ? menu_item.name : null; + var children = this.menu.items.query({ parent: name }, + { sort: [{attribute:'position'}]}); + + var lvl_class = this._get_lvl_class(level); + + if (children.total > 0) { + var menu_node = construct.create('div', { + 'class': 'submenu ' + lvl_class + //style: { display: 'none' } + }); + + if (menu_item) { + attr.set(menu_node, 'data-item', menu_item.name); + } + + var ul_node = construct.create('ul', null, menu_node); + + array.forEach(children, function(menu_item) { + + var click_handler = function(event) { + self.item_clicked(menu_item, event); + event.preventDefault(); + }; + + var li_node = construct.create('li', { + 'data-name': menu_item.name, + click: click_handler + }, ul_node); + + var a_node = construct.create('a', { + click: click_handler + }, li_node); + + this._update_item(menu_item, li_node); + + // create submenu + this._render_children(menu_item, menu_node, level + 1); + }, this); + + construct.place(menu_node, node); + } + }, + + _get_lvl_class: function(level) { + return this.level_class + '-' + level; + }, + + /** + * Updates content of li_node associated with menu_item base on + * menu_item's state. + * + * @protected + * @param {menu_item|string} menu_item + * @param {Node} [li_node] + */ + _update_item: function(menu_item, li_node) { + + if (typeof menu_item === 'string') { + menu_item = this.menu.items.get(menu_item); + } + + if (!li_node) { + li_node = query('li[data-name=\''+menu_item.name+'\']')[0]; + + // Quit for non-existing nodes. + // FIXME: maybe change to exception + if (!li_node) return; + } + + dom_class.toggle(li_node, 'disabled', !menu_item.disabled); + dom_class.toggle(li_node, 'selected', menu_item.selected); + dom_style.set(li_node, { + display: menu_item.hidden ? 'none': 'default' + }); + + var a_node = query('a', li_node)[0]; + + prop.set(a_node, 'href', '#' + menu_item.name); + prop.set(a_node, 'textContent', menu_item.label); + prop.set(a_node, 'title', menu_item.title || menu_item.label); + }, + + /** + * Displays only supplied menu items. + * @param {menu_item[]} menu_items Items to show + */ + select: function(menu_items) { + + // hide all except top level + var exception = this._get_lvl_class(1); + query('div.submenu', this.domNode).forEach(function(submenu_node) { + + if (dom_class.contains(submenu_node, exception)) return; + + dom_style.set(submenu_node, { + display: 'none' + }); + }, this); + + // show and update selected + array.forEach(menu_items, function(item) { + this._update_item(item); + + // show submenu + var item_div = query('div[data-item=\''+item.name+'\']', this.domNode)[0]; + if (item_div) { + dom_style.set(item_div, { + display: 'block' + }); + } + }, this); + }, + + /** + * Handles changes in this.menu object. + * + * @protected + * @param {menu_item} object + * @param {Number} removedFrom + * @param {Number} insertedInto + */ + _items_changed: function(object, removedFrom, insertedInto) { + + if (this.ignore_changes) return; + + if (removedFrom === -1 && insertedInto === -1) { + this._update_item(object); + } else { + // on add or removal, replace whole menu + this.render(); + this.select(this.menu.selected); + } + }, + + /** + * Sets this.menu and starts to watch its changes + * @param {freeipa.navigation.menu} menu + */ + set_menu: function(menu) { + this.menu = menu; + //get all items + var q = menu.items.query(); + q.observe(lang.hitch(this, this._items_changed), true); + on(this.menu, 'selected', lang.hitch(this, function(event) { + this.select(event.new_selection); + })); + }, + + /** + * Internal handler for clicking on menu item. + * Raises item-select event. + */ + _item_clicked: function(menu_item) { + this.emit('item-select', menu_item); + }, + + /** + * Handles click on menu item. + * + * Intended for overriding. + * + * @param {menu_item} menu_item + * @param {Event} event + */ + item_clicked: function(menu_item/*, event*/) { + this._item_clicked(menu_item); + } + }); +});
\ No newline at end of file |