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

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);

Related

Anchor tags to jump halfway down a page in NetSuite (SuiteCommerce Advanced)

Does NetSuite SuiteCommerce not support anchor tags? I want to include some links at the top of my page that let the viewer jump down to that category (on the same page). I know I have my code set up properly, but all that happens is that it tries to open a new page with no content on it:
https://www.prospectfastener.com/product-interchange
Using Anchor Tags with NetSuite SuiteCommerce
Bundled together a few answers from other Backbone related questions. This can be used in custom extensions or it can be placed in to SMT CMS HTML Content as an inline script.
Tested with SC 2022.1
// This breaks the function out of the try/catch for use on page
var scrollToAnchorFn = null;
// SB env keeps doubling loading inline scripts for testing
// This tests/sets its own property on the window to test against for that
if (typeof window.anchorSet === 'undefined') {
try {
window.anchorSet = true;
// Call scrollToAnchor with anchor ID whenever you want to jump between points on a page
const scrollToAnchor = (anchorId) => {
try {
var aTag = $(anchorId);
$('html,body').animate({
scrollTop: aTag.offset().top
},
'slow',
);
} catch (err) {
console.error(err);
}
};
const initAnchorScroll = () => {
// Test if jQuery is loaded yet, if not set timeout and repeat fn
if (window.jQuery) {
// Check if document is already loaded and ready, run code now if dom is ready
if (document.readyState === 'complete')
return scrollToAnchor(PageHash);
// Before images or styling is finished loading, move to part of dom requested which is ready
return $(document).ready(() => {
scrollToAnchor(PageHash);
});
}
setTimeout(function() {
initAnchorScroll();
}, 50);
};
// Get our page hash
// const PageHash = window.location.hash;
// Snippet demo hash
const PageHash = '#testAnchor';
// Initialize on page load if hash detected in url
// Since hashes return the #,
// we want a length greater than 1 to ensure there is an id
if (PageHash.length > 1) initAnchorScroll();
scrollToAnchorFn = scrollToAnchor;
} catch (err) {
console.error(err);
}
}
.fillerDiv {
height: 100vh;
width: 1000vw;
background-color: #fedd00;
}
#testAnchor {
background-color: #4285f4;
color: #ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.2.3/jquery.min.js"></script>
<div class="container">
<div class="fillerDiv"></div>
<div id="testAnchor">this is test content down the page with anchor ID.
Go to Next Anchor</div>
<div class="fillerDiv "></div>
<div id="testAnchor2">Second anchor tag</div>
<div class="fillerDiv "></div>
</div>
Old Answer
I'm going to assume not, but I'd be happy to be wrong.
Note: Anchor Tags are not supported by Content Delivery.
NetSuite Applications Suite -- Understanding Content Delivery

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.

How can I get ng-click to function with ng-repeat and ng-bind-html?

I am trying to get a ng-click directive to function within an ng-repeat and ng-bind-html. The ng-click code is added to a string of html from data pulled from the server (hence the ng-bind-html). The setup has a controller, a base template that is put onto the page with Drupal, and a partial that is loaded via the template from Drupal.
The controller looks like this at the moment:
var guideListController = angular.module('app')
.controller('guideListController', [
'$scope',
'$sce',
'$compile',
'ViewService',
'ToolKit',
function($scope, $sce, $compile, ViewService, ToolKit) {
// Storage for which rows need more/less links
this.rowIndex = [];
this.showFull = false;
this.showFullClick = function() {
alert('Showfull');
};
this.trustIntro = function(code) {
return $sce.trustAsHtml(code);
};
// Fetch the guide list view from services
var data = ViewService.get({view_endpoint:'guide-service', view_path: 'guide-list'}, function(data) {
//console.log(data);
// Update/process results
for (var row in data.results) {
// Create short intro w/ truncate.js
data.results[row].Intro_short = $sce.trustAsHtml($scope.guideList.getShortIntro(data.results[row], row));
//data.results[row].Intro_short = $scope.guideList.getShortIntro(data.results[row], row);
// Update intro
data.results[row].Introduction = $sce.trustAsHtml($scope.guideList.updateIntro(data.results[row], row));
//data.results[row].Introduction = $scope.guideList.updateIntro(data.results[row], row);
}
$scope.guideList.guides = data.results;
});
// Add a read less anchor tag at the end of the main intro
this.updateIntro = function(row, row_index) {
var intro = row['Introduction'].trim();
if ($scope.guideList.rowIndex[row_index]) { // only apply Less link if needed
var index = intro.length - 1;
var tag = [];
if (intro.charAt(index) === '>') { // we have a tag at the end
index--;
do {
tag.push(intro.charAt(index));
index--;
} while (intro.charAt(index) != '/'); // the closing tag
index--; // we move the index one more for the "<"
tag.reverse(); // Reverse
tag = tag.join('');
}
var inserts = ['div', 'p']; // we insert the Less link here.
if (jQuery.inArray(tag, inserts) >= 0) { // insert into the tag
intro = intro.substr(0, index) + ' <a class="less" ng-click="$parent.guideList.showFull = false">Less</a>' + intro.substr(index);
}
else { // insert at the end of the html
intro = intro + '<a class="less" ng-click="this.showFull = false">Less</a>';
}
}
return intro;
};
// Truncate the long intro into a shorter length blurb
this.getShortIntro = function(row, row_index) {
// Truncate if necc.
var short_intro = jQuery.truncate(row['Introduction'], {
length: 250,
words: true,
ellipsis: '\u2026 <a class="more moreish" attr-ng-click="guideList.showFullClick()">Read on</a>'
});
var more = jQuery('.more', short_intro); // select more link
if (more.length) { // do we have a more link
$scope.guideList.rowIndex[row_index] = true;
}
else { // no more link
$scope.guideList.rowIndex[row_index] = false;
}
$compile(short_intro)($scope);
return short_intro;
};
}]);
As you can see in the ViewService.get() call, data is fetched and then processed. The processing simply involves putting a link at the end of the "Intro" field that is intended to be clickable.
For a while I was having a tough time to even get the ng-click directive to even show (it was being filtered out w/out $sce.trustAsHtml). Now it is there but clicking it has no effect.
The main template (from Drupal) currently looks like:
<div class="guide-listing" ng-controller="guideListController as guideList">
<a ng-click="guideList.showFullClick()">Click me</a>
<div class="guide-teaser"
ng-repeat="guide in guideList.guides"
ng-include src="'/sites/all/themes/ngTheme/ngApp/partials/guide_teaser.html'">
<!-- See guide_teaser.html partial for guide teasers -->
</div>
</div>
The ng-click as placed in the Drupal template above works as expected.
And for the partial that is used in the ng-repeat, it looks like so:
<div ng-controller="guideListController as guideList">
<h2 class="guide-teaser-title">{{guide.node_title}}</h2>
<div class="guide-teaser-intro" ng-bind-html="guide.Introduction" ng-show="guide.showFull">
{{guide.Introduction}}
</div>
<div class="guide-teaser-intro-short" ng-bind-html="guide.Intro_short" ng-show="!guide.showFull">
{{guide.Intro_short}}
</div>
</div>
So far I have only been working on getting the ng-click to work on the short_intro and have had no success so far. Any ideas as to what I am doing wrong would be greatly appreciated!
Ok, So I did get some traction! I used the ngHtmlCompile (http://ngmodules.org/modules/ng-html-compile) directive that was created by https://github.com/francisbouvier (thanks!)
The issue was that the new (dynamic) html wasn't being compiled.
At first it didn't work. I had two issues that prevented it from firing:
A: I stopped using $sce.trustAsHtml. Using this in conjunction with the directive caused the content to disappear!
B: The other issue was one of scope. After I changed the directive such that transclude was set to false it worked just fine!

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.

Resources