Switching Themes in Drupal 7 module (mobile tools) - drupal-7

I've been trying to get mobile_tools for Drupal 7 to work but it appears it's not switching themes. (Or at least I can't get it to switch).
This is the code "in charge of changing to the mobile theme. If I print $custom_theme it does become the name of the mobile theme but the displayed theme is still the default. I have not been able to find any documentation on variable $custom_theme at least not for Drupal 7.
/**
* Being called in the hook_boot() implementation
* This function is in charge of changing to the mobile theme
*/
function mobile_tools_switch_theme($device) {
global $custom_theme, $conf;
// check if theme switching is forced
$current_url_type = mobile_tools_is_mobile_site();
if (($current_url_type == 'mobile' && variable_get('mobile-tools-theme-switch', '' ) == 'mobile-tools-mobile-url') ||
(variable_get('mobile-tools-theme-switch', '' ) == 'mobile-tools-mobile-device' && $device['type'] == 'mobile') ) {
$group = $device['group'];
$mobile_detection_module = variable_get('mobile-tools-device-detection', 'mobile_tools');
if (variable_get($mobile_detection_module . '_' . $group . '_enable', '') == 1) {
$custom_theme = variable_get($mobile_detection_module . '_' . $group . '_theme', $conf['theme_default']);
return TRUE;
}
else {
$custom_theme = variable_get('mobile_tools_theme_name', $conf['theme_default']);
return TRUE;
}
}
return FALSE;
}

$custom_theme is no longuer the way to switch themes in Drupal 7 as mentionned in the Upgrade modules from 6.x to 7.x page, you have to use hook_custom_theme or specify a theme callback for the path instead.
Example from the upgrade page:
<?php
/**
* Implements hook_menu_alter().
*/
function mymodule_menu_alter(&$items) {
// Set the theme callback function for all node pages. As per the
// standard behavior for hook_menu() properties, this will be
// inherited by all paths underneath node/%node as well, unless
// they define their own theme callback.
$items['node/%node']['theme callback'] = 'mymodule_default_node_theme';
// Set a different theme callback for node edit pages, and pass
// along the node object to this function so we can make decisions
// based on it.
$items['node/%node/edit']['theme callback'] = 'mymodule_edit_node_theme';
$items['node/%node/edit']['theme arguments'] = array(1);
}
/**
* Defaults to using the 'some_theme' theme for node pages.
*/
function mymodule_default_node_theme() {
return 'some_theme';
}
/**
* For editing page nodes, uses the 'some_other_theme' theme.
*/
function mymodule_edit_node_theme($node) {
return $node->type == 'page' ? 'some_other_theme' : mymodule_default_node_theme();
}
/**
* Implements hook_custom_theme().
*/
function mymodule_custom_theme() {
global $user;
// If the current user has a special role assigned to them, then display all
// pages of the site (including those listed above) using the 'special_theme'
// theme.
if (in_array(variable_get('mymodule_special_role', 0), array_keys($user->roles))) {
return 'special_theme';
}
}
?>

Related

How can we override Ext.Base?

I am using Ext JS v7.1 and I have overridden Ext.Base to set my naming scheme for the classes that inherits from Ext.Base: This eases my debugging.
Ext.define('App.class.Base', {
override: 'Ext.Base',
constructor: function() {
var me = this
/**
* App.base.store.Base => store-base-
* App.store.Menu => store-menu-
*/
if (me.isIdentifiable) {
if (!me.self.prototype.hasOwnProperty('identifiablePrefix')) {
const classNameParts = me.$className.match(/([^\.]+)/g)
if (classNameParts && classNameParts[0] === 'App') {
classNameParts.splice(0, classNameParts.length - 2)
me.self.prototype.identifiablePrefix = classNameParts.reduce((i, j) => i + '-' + j).toLocaleLowerCase() + '-'
}
}
}
return me.callParent()
}
})
This code was building before without an error but, after I upgraded Sencha Cmd to v7.3.0.19, I started the get the following error:
[ERR] C2016: Override target not found -- /...../packages/local/module-core/overrides/class/Base.js:2:64
[WRN] Override App.class.Base in file /..../packages/local/module-core/overrides/class/Base.js had no target detected
I don't know whether this is the right place/way to do this override, if not I can change my implementation. However, if there is no other way, how can get rid of the build error?
Thanks in advance,
Ipek
Because i am not using sencha build tools anymore, i can not help you directly but i would like to share another approach:
In case you have loaded the framework (ext-debug-all or ext-all, etc.) first and the class which should get overwritten is already defined you can do it like that:
Ext.Component.override({
initComponent: function () {
Ext.log('bootstraping ' + this.self.getName());
var me = this,
width = me.width,
height = me.height;
// If plugins have been added by a subclass's initComponent before calling up to here (or any components
// that don't have a table view), the processed flag will not have been set, and we must process them again.
// We could just call getPlugins here however most components don't have them so prevent the extra function call.
if (me.plugins && !me.plugins.processed) {
me.plugins = me.constructPlugins();
}
me.pluginsInitialized = true;
// this will properly (ignore or) constrain the configured width/height to their
// min/max values for consistency.
if (width != null || height != null) {
me.setSize(width, height);
}
if (me.listeners) {
me.on(me.listeners);
me.listeners = null; //change the value to remove any on prototype
}
if (me.focusable) {
me.initFocusable();
}
}
});
Depending on the further internal processing you can call callParent or callSuper.
More details here:
https://docs.sencha.com/extjs/6.5.3/classic/Ext.Class.html#cfg-override
You may be able to move this upper code inside a function and call it later, for example - when Ext.isReady. I guess this can solve or tackle some of the open tooling issues you are facing.
UPDATE:
Coming back to your question you can do the following and define it like that:
Ext.Base.override({
constructor: function() {
var me = this
/**
* App.base.store.Base => store-base-
* App.store.Menu => store-menu-
*/
if (me.isIdentifiable) {
if (!me.self.prototype.hasOwnProperty('identifiablePrefix')) {
const classNameParts = me.$className.match(/([^\.]+)/g)
if (classNameParts && classNameParts[0] === 'App') {
classNameParts.splice(0, classNameParts.length - 2)
me.self.prototype.identifiablePrefix = classNameParts.reduce((i, j) => i + '-' + j).toLocaleLowerCase() + '-'
}
}else{
console.log('isIdentifiable');
console.log(me.identifiablePrefix);
}
}
return me.callParent(arguments)
}
});
I have added an exampole fiddle here. It should log "helloWorld" in case identifiablePrefix is set.
https://fiddle.sencha.com/#view/editor&fiddle/3a8i

How to include GreenSock plugin for ScrollMagic in a ReactJS environment?

How do I include the Greensock plugin for ScrollMagic in my ReactJS project?
The authors of ScrollMagic made plugins to incorporate libraries like GreenSock and Velocity. These work great when you simply include them in your head of your html doc like so
<script type="text/javascript" src="js/lib/greensock/TweenMax.min.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/ScrollMagic.js"></script>
<script type="text/javascript" src="scrollmagic/uncompressed/plugins/animation.gsap.js"></script>
But when you're in ReactJS, you don't import javascript resources like this. You actually have to import them through processes like the npm command, then declare them in your react project like
import ScrollMagic from "scrollmagic"
Although I was able to import ScrollMagic into react files and start using scrollmagic, I haven't been able to import the greensock plugins. There's no documentation on how to do this. I tried to hack things apart by taking segments of code in animation.gsap.js and pasting it into the node_modules/scrollmagic/scrollmagic.js file (which isn't a good idea to be editing these files), but it either breaks the webpack compiler or it breaks my project code.
How do I use the greensock plugin for scrollmagic in a react environment?
I was able to build a wrapper. At a high level, what I did was study the code of plugins/animation.gsap.js, pulled out those Scene extended properties that I needed, changed the name space, and have it augment the behaviour of ScrollMagic in a separate react class prior to exporting it.
Specifically, what I did was create a new file called ./ScrollMagic.js and pasted the following contents:
import ScrollMagic from 'scrollmagic';
import {TweenLite as Tween,TimelineMax as Timeline} from 'gsap';
ScrollMagic.Scene.addOption("tweenChanges", // name
false, // default
function (val) { // validation callback
return !!val;
});
ScrollMagic.Scene.extend(function () {
var Scene = this,
_tween;
var log = function () {
if (Scene._log) { // not available, when main source minified
Array.prototype.splice.call(arguments, 1, 0, "(animation.gsap)", "->");
Scene._log.apply(this, arguments);
}
};
// set listeners
Scene.on("progress.plugin_gsap", function () {
updateTweenProgress();
});
Scene.on("destroy.plugin_gsap", function (e) {
Scene.removeTween(e.reset);
});
/**
* Update the tween progress to current position.
* #private
*/
var updateTweenProgress = function () {
if (_tween) {
var
progress = Scene.progress(),
state = Scene.state();
if (_tween.repeat && _tween.repeat() === -1) {
// infinite loop, so not in relation to progress
if (state === 'DURING' && _tween.paused()) {
_tween.play();
} else if (state !== 'DURING' && !_tween.paused()) {
_tween.pause();
}
} else if (progress != _tween.progress()) { // do we even need to update the progress?
// no infinite loop - so should we just play or go to a specific point in time?
if (Scene.duration() === 0) {
// play the animation
if (progress > 0) { // play from 0 to 1
_tween.play();
} else { // play from 1 to 0
_tween.reverse();
}
} else {
// go to a specific point in time
if (Scene.tweenChanges() && _tween.tweenTo) {
// go smooth
_tween.tweenTo(progress * _tween.duration());
} else {
// just hard set it
_tween.progress(progress).pause();
}
}
}
}
};
/**
* Add a tween to the scene.
* If you want to add multiple tweens, add them into a GSAP Timeline object and supply it instead (see example below).
*
* If the scene has a duration, the tween's duration will be projected to the scroll distance of the scene, meaning its progress will be synced to scrollbar movement.
* For a scene with a duration of `0`, the tween will be triggered when scrolling forward past the scene's trigger position and reversed, when scrolling back.
* To gain better understanding, check out the [Simple Tweening example](../examples/basic/simple_tweening.html).
*
* Instead of supplying a tween this method can also be used as a shorthand for `TweenMax.to()` (see example below).
* #memberof! animation.GSAP#
*
* #example
* // add a single tween directly
* scene.setTween(TweenMax.to("obj"), 1, {x: 100});
*
* // add a single tween via variable
* var tween = TweenMax.to("obj"), 1, {x: 100};
* scene.setTween(tween);
*
* // add multiple tweens, wrapped in a timeline.
* var timeline = new TimelineMax();
* var tween1 = TweenMax.from("obj1", 1, {x: 100});
* var tween2 = TweenMax.to("obj2", 1, {y: 100});
* timeline
* .add(tween1)
* .add(tween2);
* scene.addTween(timeline);
*
* // short hand to add a TweenMax.to() tween
* scene.setTween("obj3", 0.5, {y: 100});
*
* // short hand to add a TweenMax.to() tween for 1 second
* // this is useful, when the scene has a duration and the tween duration isn't important anyway
* scene.setTween("obj3", {y: 100});
*
* #param {(object|string)} TweenObject - A TweenMax, TweenLite, TimelineMax or TimelineLite object that should be animated in the scene. Can also be a Dom Element or Selector, when using direct tween definition (see examples).
* #param {(number|object)} duration - A duration for the tween, or tween parameters. If an object containing parameters are supplied, a default duration of 1 will be used.
* #param {object} params - The parameters for the tween
* #returns {Scene} Parent object for chaining.
*/
Scene.setTween = function (TweenObject, duration, params) {
var newTween;
if (arguments.length > 1) {
if (arguments.length < 3) {
params = duration;
duration = 1;
}
TweenObject = Tween.to(TweenObject, duration, params);
}
try {
// wrap Tween into a Timeline Object if available to include delay and repeats in the duration and standardize methods.
if (Timeline) {
newTween = new Timeline({
smoothChildTiming: true
}).add(TweenObject);
} else {
newTween = TweenObject;
}
newTween.pause();
} catch (e) {
log(1, "ERROR calling method 'setTween()': Supplied argument is not a valid TweenObject");
return Scene;
}
if (_tween) { // kill old tween?
Scene.removeTween();
}
_tween = newTween;
// some properties need to be transferred it to the wrapper, otherwise they would get lost.
if (TweenObject.repeat && TweenObject.repeat() === -1) { // TweenMax or TimelineMax Object?
_tween.repeat(-1);
_tween.yoyo(TweenObject.yoyo());
}
// Some tween validations and debugging helpers
if (Scene.tweenChanges() && !_tween.tweenTo) {
log(2, "WARNING: tweenChanges will only work if the TimelineMax object is available for ScrollMagic.");
}
// check if there are position tweens defined for the trigger and warn about it :)
if (_tween && Scene.controller() && Scene.triggerElement() && Scene.loglevel() >= 2) { // controller is needed to know scroll direction.
var
triggerTweens = Tween.getTweensOf(Scene.triggerElement()),
vertical = Scene.controller().info("vertical");
triggerTweens.forEach(function (value, index) {
var
tweenvars = value.vars.css || value.vars,
condition = vertical ? (tweenvars.top !== undefined || tweenvars.bottom !== undefined) : (tweenvars.left !== undefined || tweenvars.right !== undefined);
if (condition) {
log(2, "WARNING: Tweening the position of the trigger element affects the scene timing and should be avoided!");
return false;
}
});
}
// warn about tween overwrites, when an element is tweened multiple times
if (parseFloat(TweenLite.version) >= 1.14) { // onOverwrite only present since GSAP v1.14.0
var
list = _tween.getChildren ? _tween.getChildren(true, true, false) : [_tween],
// get all nested tween objects
newCallback = function () {
log(2, "WARNING: tween was overwritten by another. To learn how to avoid this issue see here: https://github.com/janpaepke/ScrollMagic/wiki/WARNING:-tween-was-overwritten-by-another");
};
for (var i = 0, thisTween, oldCallback; i < list.length; i++) { /*jshint loopfunc: true */
thisTween = list[i];
if (oldCallback !== newCallback) { // if tweens is added more than once
oldCallback = thisTween.vars.onOverwrite;
thisTween.vars.onOverwrite = function () {
if (oldCallback) {
oldCallback.apply(this, arguments);
}
newCallback.apply(this, arguments);
};
}
}
}
log(3, "added tween");
updateTweenProgress();
return Scene;
};
/**
* Remove the tween from the scene.
* This will terminate the control of the Scene over the tween.
*
* Using the reset option you can decide if the tween should remain in the current state or be rewound to set the target elements back to the state they were in before the tween was added to the scene.
* #memberof! animation.GSAP#
*
* #example
* // remove the tween from the scene without resetting it
* scene.removeTween();
*
* // remove the tween from the scene and reset it to initial position
* scene.removeTween(true);
*
* #param {boolean} [reset=false] - If `true` the tween will be reset to its initial values.
* #returns {Scene} Parent object for chaining.
*/
Scene.removeTween = function (reset) {
if (_tween) {
if (reset) {
_tween.progress(0).pause();
}
_tween.kill();
_tween = undefined;
log(3, "removed tween (reset: " + (reset ? "true" : "false") + ")");
}
return Scene;
};
});
export default ScrollMagic;
You will notice that this looks almost exactly like plugins/animation.gsap.js, with the exceptions being:
I added my own import lines at the top, so make sure you have npm installed scrollmagic and gsap ahead of time
I export the ScrollMagic object towards the end
plugins/animation.gsap.js had some other code that turn the plugin into a factory which I excluded because it doesn't apply here
Now I can use .setTween() in my react project. Example usage:
import React, {Component} from "react";
import ScrollMagic from "./ScrollMagic"; // my own wrapper for scrollmagic that includes greensock
export default class Home extends Component {
componentDidMount()
{
var controller = new ScrollMagic.Controller();
var item = "#whateverstuffselector";
var scene = new ScrollMagic.Scene({triggerElement:item})
.setTween(item, 0.5, {backgroundColor: "red", scale: 3})
.addTo(controller);
}
render()
{
return (<div id="whateverstuffselector">stuff</div>);
}
}
After escalating this issue for 1 month I guess I found great solution.
So this issue shows that in React environment we can not get animation.gsap file.
This fix does not require any webpack changes except animation.gsap file itself.
Find these files in "node_module" directory tree (may have different location on you PC) and import it in this way to your working JS file (App.js for example).
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/animation.gsap";
import "../../node_modules/scrollmagic/scrollmagic/uncompressed/plugins/debug.addIndicators";
Go to animation.gsap and add these two lines of code at the beginning of file.
import { TimelineMax, TweenMax, TweenLite} from "gsap/all";
import ScrollMagic from "scrollmagic";
Go to debug.addIndicators and add this line of code at the beginning of file (in case if you need indicator debugger, but I strongly suggest not to skip this step).
import ScrollMagic from "scrollmagic";
In animation.gsap find first function and delete all "root" variables and change them to variables that I provided below. ( you should find 8 of them).
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic), root.TweenMax || root.TweenLite, root.TimelineMax || root.TimelineLite);
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic', 'TweenMax', 'TimelineMax'], factory);
} else if (typeof exports === 'object') {
// CommonJS
// Loads whole gsap package onto global scope.
require('gsap');
factory(require('scrollmagic'), TweenMax, TimelineMax);
} else {
// Browser globals
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic), TweenMax || TweenLite, TimelineMax || TimelineLite);
}
}
In debug.addIndicators also delete all "root" variables ( you should find 4 of them ) and change them to variables that I provided below.
Before:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(root.ScrollMagic || (root.jQuery && root.jQuery.ScrollMagic));
}
}
After:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['ScrollMagic'], factory);
} else if (typeof exports === 'object') {
// CommonJS
factory(require('scrollmagic'));
} else {
// no browser global export needed, just execute
factory(ScrollMagic || (jQuery && jQuery.ScrollMagic));
}
}
I hope this solution will work for you.
In any case you can reach me for help.

Excel-like behaviour of Grids in Ext JS

I'm trying to figure out a way to have an Excel-like behavior with the Grids on Ext JS.
Here is the sample grid I am working with. So far we can already naviguate through the cells with the arrows but only in edit mode.
However what I am trying to reach is the naviguation with the arrows, TAB and Enter keys outside of the edit mode, just like excel.
I tried to integrate this piece of code which overrides the Editor class, hoping that it would change the behavior of the cells but it doesn't change a thing.
I believe this is the most important part that overrides the Editor class and tries to include the keys input :
Ext.override(Ext.Editor, {
startEdit: function (el, value) {
var me = this,
field = me.field;
me.completeEdit();
me.boundEl = Ext.get(el);
value = Ext.isDefined(value) ? value : me.boundEl.dom.innerHTML;
if (!me.rendered) {
me.render(me.parentEl || document.body);
}
if (me.fireEvent('beforestartedit', me, me.boundEl, value) !== false) {
me.startValue = value;
me.show();
field.reset();
if (deleteGridCellValue) {
field.setValue('');
me.editing = true;
me.completeEdit();
deleteGridCellValue = false; // reset global variable
}
else {
if (newGridCellValue == '') {
// default behaviour of Ext.Editor (see source if needed)
field.setValue(value);
}
else {
// custom behaviour to handle an alphanumeric key press from non-edit mode
field.setRawValue(newGridCellValue);
newGridCellValue = ''; // reset global variable
if (field instanceof Ext.form.field.ComboBox) {
// force the combo box's filtered dropdown list to be displayed (some browsers need this)
field.doQueryTask.delay(field.queryDelay);
}
}
me.realign(true);
field.focus(false, 10);
if (field.autoSize) {
field.autoSize();
}
me.editing = true;
}
}
}
});
This is the first time that I am working on a project that is outside of Comp-Sci classes so any help would be very much appreciated. Thanks !

What's a nice pattern for ExtJS View Component to have own private Controller?

I've some ExtJS 4 MVC apps that share certain custom view components. A shared component has its own namespace and is often sufficiently complex (e.g. with a large tree of descendant components that must be managed) that it warrants its own controller. I want this 'component specific controller' to be properly encapsulated (private) within the component (i.e. nothing outside the component should have to know or care that the component is using an embedded controller). Of course, it should be possible for an app to have multiple instances of the component (each encapsulating a separate instance of its component controller).
To this end, I've developed the following mixin (this version for ExtJS 4.1), but I'm keen to know if anyone has solved the same problem more elegantly:
/**
* A mixin that provides 'controller' style facilities to view components.
*
* This is for 'view' components that also serve as the 'controller' for the component.
* In such situations where an embedded controller is required we would have preferred to use a separate
* Ext.app.Controller derived controller but it looks like Ext.app.Controllers are global things,
* i.e. their refs can't be scoped to within the component (allowing multiple instances on the component to be used,
* each with their own controller each with its own set of refs).
*
* Usage:
* - Declare a 'refs' config just as in an Ext.app.Controller.
* - Call this.setupControllerRefs() from your initComponent() template function.
* - Call this.control(String/Object selectors, Object listeners) just as you would in a Ext.app.Controller.init()
* template function.
* - Any events fired from within a Window need special treatment, because Ext creates Windows as floated
* (top-level) components, so our usual selector scoping technique doesn't work. The trick is to give each Window
* an itemId prefixed with this component's itemId, e.g. itemId: me.itemId + '-lookup-window'
* Then, in the 'this.control({...})' block, define selectors as necessary that begin with "#{thisItemId}-", e.g.
* '#{thisItemId}-lookup-window aux-filter-criteria': ...
*
* It is also recommended to keep the 'view' aspect of the component minimal. If there is a significant proportion of
* view code, push it down into a new component class. Ideally, the component/controller should be just a Container.
*/
Ext.define('Acme.CmpController', {
setupControllerRefs: function() {
var me = this,
refs = me.refs;
// Copied from Ext.app.Controller.ref
refs = Ext.Array.from(refs);
Ext.Array.each(refs, function(info) {
var ref = info.ref,
fn = 'get' + Ext.String.capitalize(ref);
if (!me[fn]) {
me[fn] = Ext.Function.pass(me.getRef, [ref, info], me);
}
});
},
/** #private (copied from Ext.app.Controller.ref) */
getRef: function(ref, info, config) {
this.refCache = this.refCache || {};
info = info || {};
config = config || {};
Ext.apply(info, config);
if (info.forceCreate) {
return Ext.ComponentManager.create(info, 'component');
}
var me = this,
selector = info.selector,
cached = me.refCache[ref];
if (!cached) {
//me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
/**** ACME ****/ me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector, this)[0];
if (!cached && info.autoCreate) {
me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
}
if (cached) {
cached.on('beforedestroy', function() {
me.refCache[ref] = null;
});
}
}
return cached;
},
control: function(selectors, listeners) {
var me = this,
selectorPrefix,
thisIemIdPrefix = '#{thisItemId}',
newSelectors = {};
if (listeners)
throw "Support for the optional 'listeners' param (which we had thought was rarely used) has not yet been coded.";
// Since there could be multiple instances of the controller/component, each selector needs to be
// prefixed with something that scopes the query to within this component. Ensure each instance has
// an itemId, and use this as the basis for scoped selectors.
me.itemId = me.itemId || me.id;
if (!me.itemId)
throw "We assume the component will always have an 'id' by the time control() is called.";
selectorPrefix = '#' + me.itemId + ' ';
Ext.Object.each(selectors, function(selector, listeners) {
if (selector.indexOf(thisIemIdPrefix) === 0)
selector = '#' + me.itemId + selector.substring(thisIemIdPrefix.length);
else
selector = selectorPrefix + selector;
newSelectors[selector] = listeners;
});
selectors = newSelectors;
// Real Controllers use the EventBus, so let's do likewise.
// Note: this depends on a hacked EventBus ctor. See ext-fixes.js
Ext.app.EventBus.theInstance.control(selectors, listeners, me);
}
});
As referred to in that last comment, ExtJS must be patched as follows:
Ext.override(Ext.app.EventBus, {
/**
* Our CmpController mixin needs to get a handle on the EventBus, as created by the Ext.app.Application instance. Analysis
* of the ExtJS source code shows that only one instance of EventBus gets created (assuming there's never more than one
* Ext.app.Application per app). So we hack the ctor to store a reference to itself as a static 'theInstance' property.
*/
constructor: function() {
this.callOverridden();
/**** ACME ****/ this.self.theInstance = this;
},
/**
* Had to patch this routine on the line labelled **** ACME ****. Events intercepted by Pv were being received by the Pv
* instance first created by appPv. appPv created a new Pv instance every time a 'to.AcmeViewer.View' message is received.
* Even though the old Pv had isDestroyed:true, the routine below was dispatching the event to it.
*
* It's possible this surprising behaviour is not unconnected with our (mis?)use of EventBus in Acme.CmpController.
*
* This patched function is from ExtJS 4.1.1
*/
dispatch: function(ev, target, args) {
var bus = this.bus,
selectors = bus[ev],
selector, controllers, id, events, event, i, ln;
if (selectors) {
// Loop over all the selectors that are bound to this event
for (selector in selectors) {
// Check if the target matches the selector
if (selectors.hasOwnProperty(selector) && target.is(selector)) {
// Loop over all the controllers that are bound to this selector
controllers = selectors[selector];
for (id in controllers) {
if (controllers.hasOwnProperty(id)) {
// Loop over all the events that are bound to this selector on this controller
events = controllers[id];
for (i = 0, ln = events.length; i < ln; i++) {
event = events[i];
/**** ACME ****/ if (!event.observable.isDestroyed)
// Fire the event!
if (event.fire.apply(event, Array.prototype.slice.call(args, 1)) === false) {
return false;
}
}
}
}
}
}
}
return true;
}
});
Although I'm currently on ExtJS 4.1 I'm also interested to hear about solutions that depend on 4.2, as this may help motivate me to migrate.

Back button using Extjs 3.4 History with ie8 & ie9 standards document mode

I am having a problem with Ext.History utility (version 3.4.0) working properly in IE8+. It works in Quirks mode, but not with the document mode in IE8 Standards mode (IE8) or IE9 Standards mode (IE9). Quirks mode isn't working for us because it is not rendering our CSS properly.
I have stripped everything out of the app except the history utility and now have two files (besides the extjs files):
index.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
<head>
<script type="text/javascript" src="ext-base.js"></script>
<script type="text/javascript" src="ext-all.js"></script>
<script type="text/javascript" src="myapp.js"></script>
</head>
<body>
<div>
<div align="center">
<table width="97%" border="0" cellpadding="0" cellspacing="0" >
<tr>
<td>
Link1 |
Link2 |
Link3 |
Link4 |
link5
</td>
</tr>
</table>
</div>
</div>
<!-- Fields required for history management -->
<form id="history-form" class="x-hidden">
<input type="hidden" id="x-history-field"/>
<iframe id="x-history-frame"></iframe>
</form>
</body>
</html>
myapp.js:
Ext.ns('MyApp');
Ext.onReady(function()
{
Ext.History.init();
Ext.History.on('change', function(token){}, this);
});
When I load the app in a web server, go to index.html, and click link1, the address bar shows #link1. I then click link2 and the address bar shows #link2. I then click link3 and the address bar shows #link3.
Using the back button in IE with IE7 Emulation, in Chrome or in Firefox, the address bar will go from #link3 to #link2. When I hit the back button a second time, the address bar goes from #link2 to #link1. This is the behavior I expect.
However, using IE8 or IE 9 in the appropriate document standards mode, when I click the back button a second time, the address bar goes from #link2 back to #link3. Further clicks of the back button will just toggle the user between #link2 and #link3. This behavior is unexpected and is causing our application not to work properly.
Note that this is the way that the Sencha example works for 3.4.0:
Sencha 3.4 Sample
(the page renders in Quirks mode, but if you change it to IE8 Standards or IE9 Standards, it doesn't work).
It does work seem to work properly in 4.1:
(only let me posts 2 links, but you can probably find it...)
I don't have access to Ext 3.4.1, but this issue isn't listed as in the bug fixes. I have seen one thread (here) that suggests that changing the doctype would work, but that doesn't seem to be the case (I have tried all the doctypes...).
Note that many parts of our app are using the History utility for navigation, so removing it is not an acceptable solution.
Can anyone offer any suggestions on how I can get this to work?
This was actually pretty straightforward. I downloaded Ext 4.1 and looked at what they were doing with the Ext.util.History class. They define a variable for oldIEMode and use that for all the conditionals where in 3.4 they are using Ext.isIE.
So I edited the Ext.History class in ext-all-debug.js and defined the following variable at the top:
var oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
There were three conditionals in the class that were checking for Ext.isIE which I replaced with oldIEMode.
I rebuilt and deployed the app and the issue was resolved.
Editing ext-all.js is not best practice, but I should be able to overwrite this class instead.
This is how I ended up solving it:
I created a new patch javascript file and included it after the ext files
/*
#Author: RWR 20130224
This fixes the issue with backward traversal of history in IE8 & higher in standard document mode.
This class was challenging to override (http://www.sencha.com/forum/showthread.php?46306-Help-How-to-extend-Ext.History)
I ended up pasting all of the source original code here and making the necessary changes.
NOTE that this may be patched in version 3.4.1. It is definitely patched in 4.1. When upgrading, validate that this patch is still required.
*/
NewHistory = (function () {
var iframe, hiddenField;
var ready = false;
var currentToken;
var oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
function getHash() {
var href = location.href, i = href.indexOf("#"),
hash = i >= 0 ? href.substr(i + 1) : null;
if (Ext.isGecko) {
hash = decodeURIComponent(hash);
}
return hash;
}
function doSave() {
hiddenField.value = currentToken;
}
function handleStateChange(token) {
currentToken = token;
Ext.History.fireEvent('change', token);
}
function updateIFrame (token) {
var html = ['<html><body><div id="state">',Ext.util.Format.htmlEncode(token),'</div></body></html>'].join('');
try {
var doc = iframe.contentWindow.document;
doc.open();
doc.write(html);
doc.close();
return true;
} catch (e) {
return false;
}
}
function checkIFrame() {
if (!iframe.contentWindow || !iframe.contentWindow.document) {
setTimeout(checkIFrame, 10);
return;
}
var doc = iframe.contentWindow.document;
var elem = doc.getElementById("state");
var token = elem ? elem.innerText : null;
var hash = getHash();
setInterval(function () {
doc = iframe.contentWindow.document;
elem = doc.getElementById("state");
var newtoken = elem ? elem.innerText : null;
var newHash = getHash();
if (newtoken !== token) {
token = newtoken;
handleStateChange(token);
location.hash = token;
hash = token;
doSave();
} else if (newHash !== hash) {
hash = newHash;
updateIFrame(newHash);
}
}, 50);
ready = true;
Ext.History.fireEvent('ready', Ext.History);
}
function startUp() {
currentToken = hiddenField.value ? hiddenField.value : getHash();
if (oldIEMode) {
checkIFrame();
} else {
var hash = getHash();
setInterval(function () {
var newHash = getHash();
if (newHash !== hash) {
hash = newHash;
handleStateChange(hash);
doSave();
}
}, 50);
ready = true;
Ext.History.fireEvent('ready', Ext.History);
}
}
return {
/**
* The id of the hidden field required for storing the current history token.
* #type String
* #property s
*/
fieldId: 'x-history-field',
/**
* The id of the iframe required by IE to manage the history stack.
* #type String
* #property s
*/
iframeId: 'x-history-frame',
events:{},
/**
* Initialize the global History instance.
* #param {Boolean} onReady (optional) A callback function that will be called once the history
* component is fully initialized.
* #param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to the browser window.
*/
init: function (onReady, scope) {
if(ready) {
Ext.callback(onReady, scope, [this]);
return;
}
if(!Ext.isReady){
Ext.onReady(function(){
Ext.History.init(onReady, scope);
});
return;
}
hiddenField = Ext.getDom(Ext.History.fieldId);
if (oldIEMode) {
iframe = Ext.getDom(Ext.History.iframeId);
}
this.addEvents(
/**
* #event ready
* Fires when the Ext.History singleton has been initialized and is ready for use.
* #param {Ext.History} The Ext.History singleton.
*/
'ready',
/**
* #event change
* Fires when navigation back or forwards within the local page's history occurs.
* #param {String} token An identifier associated with the page state at that point in its history.
*/
'change'
);
if(onReady){
this.on('ready', onReady, scope, {single:true});
}
startUp();
},
/**
* Add a new token to the history stack. This can be any arbitrary value, although it would
* commonly be the concatenation of a component id and another id marking the specifc history
* state of that component. Example usage:
* <pre><code>
// Handle tab changes on a TabPanel
tabPanel.on('tabchange', function(tabPanel, tab){
Ext.History.add(tabPanel.id + ':' + tab.id);
});
</code></pre>
* #param {String} token The value that defines a particular application-specific history state
* #param {Boolean} preventDuplicates When true, if the passed token matches the current token
* it will not save a new history step. Set to false if the same state can be saved more than once
* at the same history stack location (defaults to true).
*/
add: function (token, preventDup) {
if(preventDup !== false){
if(this.getToken() == token){
return true;
}
}
if (oldIEMode) {
return updateIFrame(token);
} else {
location.hash = token;
return true;
}
},
/**
* Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
*/
back: function(){
history.go(-1);
},
/**
* Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
*/
forward: function(){
history.go(1);
},
/**
* Retrieves the currently-active history token.
* #return {String} The token
*/
getToken: function() {
return ready ? currentToken : getHash();
}
};
})();
Ext.apply(NewHistory, new Ext.util.Observable());
Ext.apply(Ext.History, NewHistory);

Resources