StoreMenu.js (12168B)
1 /** 2 * Ext.ux.menu.StoreMenu Addon 3 * 4 * Inspired from the Ext.ux.menu.StoreMenu for ExtJs 3 by Marco Wienkoop 5 * 6 * This version is a complete rewrite and enhanced for ExtJs 4 with some of the old features removed. 7 * @author Joe Kuan 8 * @docauthor Joe Kuan 9 * Joe Kuan <kuan.joe@gmail.com> 10 * 11 * #Demo and Download 12 * Here are the links for the online [demo](http://joekuan.org/demos/StoreMenu_ExtJs_4/) and [github](http://github.com/JoeKuan/StoreMenu_ExtJs_4) 13 * download. For usage, see [license](http://github.com/JoeKuan/StoreMenu_ExtJs_4/blob/master/License). 14 * 15 * #Creating & Applying StoreMenu 16 * 17 * Suppose we define a JSON Store for a list of menus as follows: 18 * 19 * @example 20 * Ext.define('Menu', { 21 * extend: 'Ext.data.Model', 22 * fields: [ 'id', 'text', 'iconCls' ] 23 * }); 24 * 25 * var store = Ext.create('Ext.data.Store', { 26 * model: 'Menu', 27 * proxy: { 28 * type: 'ajax', 29 * url: 'demo/menu.php', 30 * reader: { 31 * type: 'json', 32 * root: 'root' 33 * } 34 * } 35 * }); 36 * 37 * The *id*, *text* and *iconCls* fields are mapped to menu item config. 38 * 39 * Assume the file menu.php returns the following in JSON: 40 * @example 41 * { "success": true, 42 * "root": [{ 43 * "id": 1, "text": "Menu 1" 44 * },{ 45 * "id": 2, "text": "Menu 2", "iconCls": "calendar" 46 * },{ 47 * "id": 3, "text": "Menu 3" 48 * } 49 * ] 50 * } 51 * 52 * To produce a simple Window with store menu inside a toolbar, here is the code 53 * @example 54 * Ext.create('Ext.window.Window', { 55 * height: 380, 56 * width: 450, 57 * title: 'Menu Store for ExtJs 4', 58 * tbar: [{ 59 * menu: Ext.create('Ext.ux.menu.StoreMenu', { 60 * store: store, 61 * itemsHandler: function(item, evt) { 62 * Ext.example.msg("Store Menu", 63 * "You click on item with id " + item.id); 64 * } 65 * }), 66 * // Need this for empty menu - no items option 67 * showEmptyMenu: true, 68 * text: 'Menu Demo 1' 69 * }] 70 * }).show(); 71 * 72 * Clicking any of the menus dynamically generated will call itemsHandler. To differentiate 73 * between the menus, each menu item is created with the id option which is taken from the 74 * id field of the store 75 * 76 * #StoreMenu without *items* option 77 * The menu rendering policy has been changed since ExtJs 4.2.1 so that menu is not rendered 78 * if the {@link Ext.menu.Menu#cfg-items} option is empty. Subsequently, this will stop the 79 * store from loading, hence no dynamic menus will be displayed. In order to force the 80 * StoreMenu to render, an option, {@link Ext.button.Button#cfg-showEmptyMenu}, 81 * is needed to pass to the owner button in this scenario. 82 * 83 * #Creating Specific Menus 84 * For more specific menus, StoreMenu supports object specifier through single field name, *config*. 85 * Store record with *config* field is expected to contain required options for creating menu objects, such as xtype. 86 * Menus like: menucheckitem, separator can be specified through this scheme. 87 * Moreover, this can be mixed with normal menu item creation which the data model definition 88 * includes both field name schemes. The following shows an example for creating specific menus along with default 89 * menu item through the store. 90 * @example 91 * Ext.define('Menu', { 92 * extend: 'Ext.data.Model', 93 * fields: [ 'id', 'text', 'iconCls', 'config' ] 94 * }); 95 * 96 * The server side is configured to return the following menus in JSON: 97 * @example 98 * { "root": [{ "config": { "id": "2A", "text": "Menu 2A", "xtype": "menucheckitem"} }, 99 * { "config": { "xtype": "menuseparator"} }, 100 * { "id": "2C", "text": "Menu 2C" } 101 * ] 102 * } 103 * 104 * #Creating Submenus 105 * The StoreMenu also supports submenu entries (single level only). The server will need to 106 * return additional fields for including submenu entries (see menuField) and id string for 107 * their handlers (see smHandlers). 108 * The following is what the server side should return for submenus 109 * @example 110 * { "root": [{ "id": "3A", "text": "Menu 3A", 111 * "menu": [{ "id": "3A-1", "text": "Submenu 3A-1", "smHandler": "submenu3A1" }, 112 * { "id": "3A-2", "text": "Submenu 3A-2", "smHandler": "submenu3A2" } 113 * ] 114 * } 115 * To bind with the submenu handlers, we can create the StoreMenu as follows: 116 * @example 117 * var menu3 = Ext.create('Ext.ux.menu.StoreMenu', { 118 * store: store, 119 * smHandlers: { 120 * submenu3A1: function(item) { 121 * Ext.example.msg("Third Menu Store", "This is submenu handler specific for menu 3A-1"); 122 * }, 123 * submenu3A2: function(item) { 124 * Ext.example.msg("Third Menu Store", "This is submenu handler specific for menu 3A-2"); 125 * } 126 * } 127 * }); 128 * 129 */ 130 Ext.define("Ext.ux.menu.StoreMenu", { 131 extend: 'Ext.menu.Menu', 132 alias: 'widget.storemenu', 133 134 config: { 135 /*** 136 * message shown next to the store menu when it is loading 137 */ 138 loadingText: 'Loading...', 139 /** 140 * offset is to use with static menus, i.e. the offset position for 141 * the dynamic menus to start. E.g. if two static menus are included inside the items 142 * option, by setting *offset* to 2 the dynamic menus start below the static 143 * menus. Also a separator is added between static and dynamic menus 144 */ 145 offset: 0, 146 /** 147 * reload the store everytime the top menu is expanded 148 */ 149 autoReload: true, 150 /** 151 * nameField is to map the field for the menu title returning from the 152 * store. 153 */ 154 nameField: 'text', 155 156 /** 157 * 158 * optional Field of the store 159 */ 160 url: 'url', 161 162 /** 163 * idField is to map the menu id entry from the store 164 */ 165 idField: 'id', 166 /** 167 * iconField is to map the menu icon (iconCls) 168 */ 169 iconField: 'iconCls', 170 /** 171 * itemsHandler is the general menu handler for the __first level__ menus. 172 * For submenu handler, see subMenuHandler 173 */ 174 itemsHandler: Ext.emptyFn, 175 /** 176 * configField is used for specific menu types other than 177 * menu item (default). The config field returned from the server 178 * side is expected to hold all the required options to create the 179 * specific menu 180 */ 181 configField: 'config', 182 /** 183 * the field name containing the list of submenu entries in the store 184 */ 185 menuField: 'menu' 186 }, 187 188 /*** 189 * @property {Object} store 190 * Data store for the menus & submenus entries 191 */ 192 store: null, 193 194 // Keep track of what menu items have been added 195 storeMenus: [], 196 197 loaded: false, 198 loadMask: null, 199 200 onMenuLoad: function () 201 { 202 if (!this.loaded || this.autoReload) 203 { 204 this.store.load(); 205 } 206 }, 207 208 updateMenuItems: function (loadedState, records) 209 { 210 211 for (var i = 0; i < this.storeMenus.length; i++) 212 { 213 this.remove(this.storeMenus[i]); 214 } 215 this.storeMenus = []; 216 217 if (loadedState) 218 { 219 220 // If offset is specified, it means we have to put the 221 // dynamic menus after the static menus. We put a separator 222 // to separate both 223 var count = 0; 224 if (this.offset) 225 { 226 this.storeMenus.push(this.insert(this.offset, {xtype: 'menuseparator'})); 227 count = 1; 228 } 229 230 Ext.each(records, function (record) 231 { 232 233 var menuSettings = {}; 234 var patterns = {}; 235 patterns.protocol = '^(http(s)?(:\/\/)){1}(www[.])?'; 236 patterns.domain = '([a-zA-Z0-9-_\.])+'; 237 patterns.params = '([.][a-zA-Z0-9(-|\/|=|?)?]+)'; 238 var regex = new RegExp(patterns.protocol + patterns.domain + patterns.params + "$"); 239 240 241 if (record.data[this.configField]) 242 { 243 Ext.apply(menuSettings, record.data[this.configField]); 244 } else 245 { 246 menuSettings = { 247 id: record.data[this.idField], 248 text: record.data[this.nameField], 249 iconCls: record.data[this.iconField], 250 handler: this.itemsHandler 251 }; 252 regex.test(record.data[this.url]) ? menuSettings.url = record.data[this.url] : ""; 253 } 254 255 if (record.data[this.menuField]) 256 { 257 menuSettings.menu = {items: []}; 258 Ext.each(record.data[this.menuField], function (menuitem) 259 { 260 // Make sure the handler name is defined 261 if (menuitem.smHandler && this.smHandlers[menuitem.smHandler]) 262 { 263 menuSettings.menu.items.push({ 264 id: menuitem[this.idField], 265 text: menuitem[this.nameField], 266 iconCls: menuitem[this.iconField], 267 url: menuItem[this.url], 268 handler: this.smHandlers[menuitem.smHandler] 269 }); 270 } 271 }, this); 272 } 273 274 this.storeMenus.push(this.insert(this.offset + count, menuSettings)); 275 count++; 276 }, this) 277 278 } else 279 { 280 this.storeMenus.push(this.insert(this.offset, 281 '<span class="loading-indicator">' + 282 this.loadingText + '</span>')); 283 } 284 285 this.loaded = loadedState; 286 }, 287 288 onBeforeLoad: function () 289 { 290 this.updateMenuItems(false); 291 }, 292 293 onLoad: function (store, records) 294 { 295 this.updateMenuItems(true, records); 296 }, 297 298 /*** 299 * Set a submenu handler. 300 * @param handlerType {String} the id value for the handler function 301 * @param handler {Function} the handler implementation - See menuitem handler for function parameters 302 */ 303 setSubMenuHandler: function (handlerType, handler) 304 { 305 this.smHandlers[handlerType] = handler; 306 }, 307 308 /** 309 * A utility method for changing parameters of the underlying 310 * JSON store. Values inside params object will overwrite the store's 311 * existing parameters with the same name 312 * @param {Object} params an object of parameters to be set in the store 313 */ 314 setParams: function (params) 315 { 316 Ext.apply(this.store.getProxy().extraParams, params); 317 }, 318 319 setStore: function (store) 320 { 321 this.store = store; 322 }, 323 324 /** 325 * @cfg smHandlers {Object} is an object holding all the submenu handler implementations. 326 * Inside the object, option name is the identifier for the handler function which is the option value. 327 * (See the [submenu section] (#submenu) for example) 328 */ 329 smHandlers: {}, 330 331 initComponent: function (config) 332 { 333 this.callParent(arguments); 334 335 if (!this.store) 336 { 337 //at least url/proxy or data need to be given in config when initiating this 338 // component 339 this.store = Ext.create('Ext.data.Store', { 340 model: this.model, 341 remoteFilter: true 342 }); 343 } 344 345 this.on("beforeshow", this.onMenuLoad, this); 346 this.store.on('beforeload', this.onBeforeLoad, this); 347 this.store.on('load', this.onLoad, this); 348 } 349 350 });