/** * This class manages a drag-drop Dashboard similar to the legacy Ext JS Portal example. * The user-directed layout of the Dashboard is preserved the Ext JS `stateful` mechanism * to preserve potentially dynamic user sizing and collapsed states as well as order of * items in their columns. */Ext.define('Ext.dashboard.Dashboard', { extend: 'Ext.panel.Panel', xtype: 'dashboard', requires: [ 'Ext.layout.container.Dashboard', 'Ext.dashboard.DropZone', 'Ext.dashboard.Column', 'Ext.dashboard.Part' ], isDashboard: true, cls: Ext.baseCSSPrefix + 'dashboard', bodyCls: Ext.baseCSSPrefix + 'dashboard-body', defaultType: 'dashboard-column', scrollable: true, layout: { type: 'dashboard' }, stateful: false, idSeed: 1, config: { /** * @cfg {Object} parts * An object keyed by `id` for the parts that can be created for this `Dashboard`. */ parts: null }, renderConfig: { /** * @cfg {Number} maxColumns * The maximum number of visible columns. * @accessor */ maxColumns: 4 }, /** * @cfg {Number[]} columnWidths * An array designating the width of columns in your dashboard's default state as described * by the {@link #cfg-defaultContent} property. For example: * * columnWidths: [ * 0.35, * 0.40, * 0.25 * ] * * As you can see, this array contains the default widths for the 3 columns in the dashboard's * initial view. The column widths should total to an integer value, typically 1 as shown * above. When column widths exceed 1, they will be wrapped effectively creating "rows". This * means that if your column widths add up to more than 1, you would still want the first few * to total 1 to ensure that the first row fills the dashboard space. This applies whenever * the column widths extend past an integer value. * * **Note:** columnWidths will not be utilized if there is stateful information that * dictates different user saved column widths. */ /** * @cfg {Object[]} defaultContent * An array of {@link Ext.dashboard.Part part} configuration objects that define your * dashboard's default state. These should not be confused with component configurations. * * Each config object should also include: * * + `type` - The type of {@link Ext.dashboard.Part part} that you want to be generated. * + `columnIndex` - The column position in which the {@link Ext.dashboard.Part part} should * reside. * + `height` - The desired height of the {@link Ext.dashboard.Part part} to be generated. * * The remaining properties are specific to your part's config object. For example: * * defaultContent: [{ * type: 'rss', * columnIndex: 0, * height: 500, * feedUrl: 'http://feeds.feedburner.com/extblog' * }, { * type: 'stockTicker', * columnIndex: 1, * height: 300 * }, { * type: 'stocks', * columnIndex: 1, * height: 300 * }, { * type: 'rss', * columnIndex: 2, * height: 350, * feedUrl: 'http://rss.cnn.com/rss/edition.rss' * }] * * Default column widths are defined by {@link #cfg-columnWidths} and not in these * part config objects. * * **Note:** defaultContent will not be utilized if there is stateful information that * dictates different user saved positioning and componentry. */ /** * @event validatedrop */ /** * @event beforedragover */ /** * @event dragover */ /** * @event beforedrop */ /** * @event drop */ initComponent: function() { var me = this; me.callParent(); me.addStateEvents('remove'); }, applyParts: function(parts, collection) { var id, part; if (!collection) { collection = new Ext.util.Collection({ decoder: Ext.Factory.part }); } for (id in parts) { part = parts[id]; if (Ext.isString(part)) { part = { type: part }; } part.id = id; part.dashboard = this; collection.add(part); } return collection; }, /** * @private */ getPart: function(type) { var parts = this.getParts(); return parts.getByKey(type); }, addNew: function(type, columnIndex, beforeAfter) { var me = this, part = me.getPart(type); part.displayForm(null, null, function(config) { config.type = type; me.addView(config, columnIndex, beforeAfter); }); }, addView: function(instance, columnIndex, beforeAfter) { var me = this, // We are only concerned with columns (ignore splitters). items = me.query('dashboard-column'), count = items.length, index = columnIndex || 0, view = instance.id ? instance : me.createView(instance), columnWidths = me.columnWidths, column; if (!count) { // if the layout is empty, assert a full initially column = me.add(0, me.createColumn({ columnWidth: (Ext.isArray(columnWidths) ? columnWidths[0] : 1) })); items = [column]; count = 1; } if (index >= count) { index = count - 1; beforeAfter = 1; // after } if (!beforeAfter) { column = items[index]; if (column) { return column.add(view); } } if (beforeAfter > 0) { // after... ++index; } column = me.createColumn(); if (columnWidths) { column.columnWidth = columnWidths[index] || (columnWidths[index] = 1); } if (!column.items) { column.items = []; } column.items.push(view); column = me.add(column); return column.items.first(); }, createColumn: function(config) { var cycle = this.cycleLayout; return Ext.apply({ items: [], bubbleEvents: ['add', 'childmove', 'resize'], listeners: { expand: cycle, collapse: cycle, scope: this } }, config); }, createView: function(config) { var me = this, type = config.type, part = me.getPart(type), view = part.createView(config); if (!view.id) { view.id = me.id + '_' + type + (me.idSeed++); } view.bubbleEvents = Ext.Array.from(view.bubbleEvents).concat(['expand', 'collapse']); view.stateful = me.stateful; view.listeners = { removed: this.onItemRemoved, scope: this }; return view; }, initEvents: function() { this.callParent(); this.dd = new Ext.dashboard.DropZone(this, this.dropConfig); }, /** * Readjust column/splitter heights for collapsing child panels * @private */ cycleLayout: function() { this.updateLayout(); }, doDestroy: function() { if (this.dd) { Ext.destroy(this.dd); } this.callParent(); }, //------------------------------------------------- // State and Item Persistence applyState: function(state) { delete state.items; this.callParent([state]); // eslint-disable-next-line vars-on-top var me = this, columnWidths = state.columnWidths, items = me.items.items, length = items.length, columnLength = columnWidths ? columnWidths.length : 0, i; // Splitters have not been inserted so the length is sans-splitter if (columnLength) { me.columnWidths = []; for (i = 0; i < length; ++i) { me.columnWidths.push( items[i].columnWidth = (i < columnLength) ? columnWidths[i] : (1 / length) ); } } }, getState: function() { var me = this, columnWidths = [], items = me.items.items, state = me.callParent() || {}, length = items.length, i, item; for (i = 0; i < length; ++i) { if (!(item = items[i]).isSplitter) { columnWidths.push(item.columnWidth); } } state.idSeed = me.idSeed; state.items = me.serializeItems(); // only overwrite column widths if they are defined in the state if (columnWidths.length) { state.columnWidths = me.columnWidths = columnWidths; } // no column widths are defined from the state, let's remove it so any defaults // can be used instead else { delete state.columnWidths; } return state; }, initItems: function() { var me = this, defaultContent = me.defaultContent, state; if (me.stateful) { state = Ext.state.Manager.get(me.getStateId()); defaultContent = (state && state.items) || defaultContent; } if (!me.items && defaultContent) { me.items = me.deserializeItems(defaultContent); } me.callParent(); }, deserializeItems: function(serialized) { var me = this, length = serialized.length, columns = [], columnWidths = me.columnWidths, maxColumns = me.getMaxColumns(), column, columnIndex, columnWidth, i, item, partConfig; for (i = 0; i < length; ++i) { partConfig = serialized[i]; columnIndex = Math.min(partConfig.columnIndex || 0, maxColumns - 1); delete partConfig.columnIndex; if (!(column = columns[columnIndex])) { columns[columnIndex] = column = me.createColumn(); columnWidth = columnWidths && columnWidths[columnIndex]; if (columnWidth) { column.columnWidth = columnWidth; } } item = me.createView(partConfig); column.items.push(item); } for (i = 0, length = columns.length; i < length; ++i) { column = columns[i]; if (!column.columnWidth) { column.columnWidth = 1 / length; } } return columns; }, serializeItem: function(item) { return Ext.apply({ type: item.part.id, id: item.id, columnIndex: item.columnIndex }, item._partConfig); }, serializeItems: function() { var me = this, items = me.items.items, length = items.length, ret = [], columnIndex = 0, child, childItems, i, item, j, k; for (i = 0; i < length; ++i) { item = items[i]; if (!item.isSplitter) { childItems = item.items.items; for (j = 0, k = childItems.length; j < k; ++j) { child = childItems[j]; child.columnIndex = columnIndex; ret.push(me.serializeItem(child)); } ++columnIndex; } } return ret; }, onItemRemoved: function(item, column) { // Removing items from a Dashboard is a persistent action, so we must remove the // state data for it or leak it. if (item.stateful && !item.isMoving) { Ext.state.Manager.clear(item.getStateId()); } }});