/**
* This plugin can be applied to any `Component` (although almost always to a `Container`)
* to make it fill the browser viewport. This plugin is used internally by the more familiar
* `Ext.container.Viewport` class.
*
* The `Viewport` container is commonly used but it can be an issue if you need to fill the
* viewport with a container that derives from another class (e.g., `Ext.tab.Panel`). Prior
* to this plugin, you would have to do this:
*
* Ext.create('Ext.container.Viewport', {
* layout: 'fit', // full the viewport with the tab panel
*
* items: [{
* xtype: 'tabpanel',
* items: [{
* ...
* }]
* }]
* });
*
* With this plugin you can create the `tabpanel` as the viewport:
*
* Ext.create('Ext.tab.Panel', {
* plugins: {
* viewport: true
* },
*
* items: [{
* ...
* }]
* });
*
* More importantly perhaps is that as a plugin, the view class can be reused in other
* contexts such as the content of a `{@link Ext.window.Window window}`.
*
* The Viewport renders itself to the document body, and automatically sizes itself to the size of
* the browser viewport and manages window resizing. There may only be one Viewport created
* in a page.
*
* ## Responsive Design
*
* This plugin enables {@link Ext.mixin.Responsive#responsiveConfig} for the components
* by requiring `Ext.Responsive`.
*
* @since 5.0.0
*/
Ext.define('Ext.plugin.Viewport', {
extend: 'Ext.plugin.Abstract',
requires: [
'Ext.Responsive'
],
alias: 'plugin.viewport',
setCmp: function(cmp) {
this.cmp = cmp;
if (cmp && !cmp.isViewport) {
this.decorate(cmp);
if (cmp.renderConfigs) {
cmp.flushRenderConfigs();
}
cmp.setupViewport();
}
},
destroy: function() {
var el = this.cmp.el;
this.callParent();
// Remove the injected overrides so that the bodyEl singleton
// can be reused by subsequent code (eg, unit tests)
if (el) {
delete el.setHeight;
delete el.setWidth;
}
},
statics: {
decorate: function(target) {
Ext.applyIf(target.prototype || target, {
ariaRole: 'application',
viewportCls: Ext.baseCSSPrefix + 'viewport'
});
Ext.override(target, {
isViewport: true,
preserveElOnDestroy: true,
initComponent: function() {
this.callParent();
this.setupViewport();
},
// Because we don't stamp the size until onRender, our size model
// won't return correctly. As we're always going to be configured,
// just return the value here
getSizeModel: function() {
var configured = Ext.layout.SizeModel.configured;
return configured.pairsByHeightOrdinal[configured.ordinal];
},
handleViewportResize: function() {
var me = this,
Element = Ext.dom.Element,
width = Element.getViewportWidth(),
height = Element.getViewportHeight();
if (width !== me.width || height !== me.height) {
me.setSize(width, height);
}
},
setupViewport: function() {
var me = this,
el = document.body;
if (!me.$responsiveId) {
me.setResponsiveConfig(true);
Ext.mixin.Responsive.register(me);
me.setupResponsiveContext();
}
// Here in the (perhaps unlikely) case that the body dom el doesn't yet have
// an id, we want to give it the same id as the viewport component so getCmp
// lookups will be able to find the owner component.
//
// Note that nothing says that components that use configured elements
// have to have matching ids (they probably won't), but this is at least making
// the attempt so that getCmp *may* be able to find the component.
// However, in these cases, it's really better to use Component#from
// to find the owner component.
if (!el.id) {
el.id = me.id;
}
// In addition, stamp on the data-componentid so lookups using Component's
// from will work.
el.setAttribute('data-componentid', me.id);
if (!me.ariaStaticRoles[me.ariaRole]) {
el.setAttribute('role', me.ariaRole);
}
el = me.el = Ext.getBody();
Ext.fly(document.documentElement).addCls(me.viewportCls);
el.setHeight = el.setWidth = Ext.emptyFn;
el.dom.scroll = 'no';
me.allowDomMove = false;
me.renderTo = el;
if (Ext.supports.Touch) {
me.addMeta('apple-mobile-web-app-capable', 'yes');
}
// Get the DOM disruption over with before the Viewport renders
// and begins a layout
Ext.scrollbar.size();
// Clear any dimensions, we will size later on in onRender
me.width = me.height = undefined;
// ... but take the measurements now because doing that in onRender
// will cause a costly reflow which we just forced with getScrollbarSize()
me.initialViewportHeight = Ext.Element.getViewportHeight();
me.initialViewportWidth = Ext.Element.getViewportWidth();
},
afterLayout: function(layout) {
if (Ext.supports.Touch) {
document.body.scrollTop = 0;
}
this.callParent([layout]);
},
onRender: function() {
var me = this,
overflowEl = me.getOverflowEl(), // eslint-disable-line no-unused-vars
body = Ext.getBody();
me.callParent(arguments);
// The global scroller is our scroller.
// We must provide a non-scrolling one if we are not configured to scroll,
// otherwise the deferred ready listener in Scroller will create
// one with scroll: true
Ext.setViewportScroller(me.getScrollable() || {
x: false,
y: false,
element: body
});
// If we are not scrolling the body, the body has to be overflow:hidden
if (me.getOverflowEl() !== body) {
body.setStyle('overflow', 'hidden');
}
// Important to start life as the proper size (to avoid extra layouts)
// But after render so that the size is not stamped into the body,
// although measurement has to take place before render to avoid
// causing a reflow.
me.width = me.initialViewportWidth;
me.height = me.initialViewportHeight;
me.initialViewportWidth = me.initialViewportHeight = null;
},
initInheritedState: function(inheritedState, inheritedStateInner) {
var me = this,
root = Ext.rootInheritedState;
if (inheritedState !== root) {
// We need to go at this again but with the rootInheritedState object. Let
// any derived class poke on the proper object!
me.initInheritedState(
me.inheritedState = root,
me.inheritedStateInner = Ext.Object.chain(root)
);
}
else {
me.callParent([inheritedState, inheritedStateInner]);
}
},
doDestroy: function() {
var me = this,
root = Ext.rootInheritedState,
scroller = me.scrollable,
key;
// We set the global body scroller aboce in onRender.
// Just relinquish it here and allow it to live on.
if (scroller) {
// Return the body scroller to default; X and Y scrolling
scroller.setConfig({
x: true,
y: true
});
me.scrollable = null;
}
// Clear any properties from the inheritedState so we don't pollute the
// global namespace. If we have a rtl flag set, leave it alone because it's
// likely we didn't write it
for (key in root) {
if (key !== 'rtl') {
delete root[key];
}
}
delete me.el.setHeight;
delete me.el.setWidth;
me.removeUIFromElement();
me.el.removeCls(me.baseCls);
Ext.fly(document.body.parentNode).removeCls(me.viewportCls);
me.callParent();
},
addMeta: function(name, content) {
var meta = document.createElement('meta');
meta.setAttribute('name', name);
meta.setAttribute('content', content);
Ext.getHead().appendChild(meta, true);
},
privates: {
// override here to prevent an extraneous warning
applyTargetCls: function(targetCls) {
var el = this.el;
if (el === this.getTargetEl()) {
this.el.addCls(targetCls);
}
else {
this.callParent([targetCls]);
}
},
// Override here to prevent tabIndex set/reset on the body
disableTabbing: function() {
var el = this.el;
if (el) {
el.saveTabbableState({
skipSelf: true
});
}
},
enableTabbing: function() {
var el = this.el;
if (el) {
el.restoreTabbableState({ skipSelf: true });
}
},
updateResponsiveState: function() {
// By providing this method we are in sync with the layout suspend/resume as
// well as other changes to configs that need to happen during this pulse of
// size change.
// Since we are not using the Viewport plugin beyond applying its methods on
// to our prototype, we need to be Responsive ourselves and call this here:
this.handleViewportResize();
this.callParent();
}
}
});
}
}
}, function(Viewport) {
Viewport.prototype.decorate = Viewport.decorate;
});