1 /**
  2  * @fileOverview event-hashchange
  3  * @author yiminghe@gmail.com , xiaomacji@gmail.com
  4  */
  5 KISSY.add('event/hashchange', function (S, Event, DOM, UA, special) {
  6 
  7     var win = S.Env.host,
  8         doc = win.document,
  9         docMode = doc['documentMode'],
 10         ie = docMode || UA['ie'],
 11         HASH_CHANGE = 'hashchange';
 12 
 13     // ie8 支持 hashchange
 14     // 但IE8以上切换浏览器模式到IE7(兼容模式),
 15     // 会导致 'onhashchange' in window === true,但是不触发事件
 16 
 17     // 1. 不支持 hashchange 事件,支持 hash 历史导航(opera??):定时器监控
 18     // 2. 不支持 hashchange 事件,不支持 hash 历史导航(ie67) : iframe + 定时器
 19     if ((!( 'on' + HASH_CHANGE in win)) || ie && ie < 8) {
 20 
 21         function getIframeDoc(iframe) {
 22             return iframe.contentWindow.document;
 23         }
 24 
 25         var POLL_INTERVAL = 50,
 26             IFRAME_TEMPLATE = "<html><head><title>" + (doc.title || "") +
 27                 " - {hash}</title>{head}</head><body>{hash}</body></html>",
 28 
 29             getHash = function () {
 30                 // 不能 location.hash
 31                 // http://xx.com/#yy?z=1
 32                 // ie6 => location.hash = #yy
 33                 // 其他浏览器 => location.hash = #yy?z=1
 34                 var url = location.href;
 35                 return '#' + url.replace(/^[^#]*#?(.*)$/, '$1');
 36             },
 37 
 38             timer,
 39 
 40         // 用于定时器检测,上次定时器记录的 hash 值
 41             lastHash,
 42 
 43             poll = function () {
 44                 var hash = getHash();
 45                 if (hash !== lastHash) {
 46                     // S.log("poll success :" + hash + " :" + lastHash);
 47                     // 通知完调用者 hashchange 事件前设置 lastHash
 48                     lastHash = hash;
 49                     // ie<8 同步 : hashChange -> onIframeLoad
 50                     hashChange(hash);
 51                 }
 52                 timer = setTimeout(poll, POLL_INTERVAL);
 53             },
 54 
 55             hashChange = ie && ie < 8 ? function (hash) {
 56                 // S.log("set iframe html :" + hash);
 57                 var html = S.substitute(IFRAME_TEMPLATE, {
 58                         // 防止 hash 里有代码造成 xss
 59                         // 后面通过 innerText,相当于 unEscapeHTML
 60                         hash:S.escapeHTML(hash),
 61                         // 一定要加哦
 62                         head:DOM.isCustomDomain() ? ("<script>document.domain = '" +
 63                             doc.domain
 64                             + "';</script>") : ""
 65                     }),
 66                     iframeDoc = getIframeDoc(iframe);
 67                 try {
 68                     // 写入历史 hash
 69                     iframeDoc.open();
 70                     // 取时要用 innerText !!
 71                     // 否则取 innerHtml 会因为 escapeHtml 导置 body.innerHTMl != hash
 72                     iframeDoc.write(html);
 73                     iframeDoc.close();
 74                     // 立刻同步调用 onIframeLoad !!!!
 75                 } catch (e) {
 76                     // S.log('doc write error : ', 'error');
 77                     // S.log(e, 'error');
 78                 }
 79             } : function () {
 80                 notifyHashChange();
 81             },
 82 
 83             notifyHashChange = function () {
 84                 // S.log("hash changed : " + getHash());
 85                 Event.fire(win, HASH_CHANGE);
 86             },
 87             setup = function () {
 88                 if (!timer) {
 89                     poll();
 90                 }
 91             },
 92             tearDown = function () {
 93                 timer && clearTimeout(timer);
 94                 timer = 0;
 95             },
 96             iframe;
 97 
 98         // ie6, 7, 覆盖一些function
 99         if (ie < 8) {
100 
101             /*
102              前进后退 : start -> notifyHashChange
103              直接输入 : poll -> hashChange -> start
104              iframe 内容和 url 同步
105              */
106             setup = function () {
107                 if (!iframe) {
108                     var iframeSrc = DOM.getEmptyIframeSrc();
109                     //http://www.paciellogroup.com/blog/?p=604
110                     iframe = DOM.create('<iframe ' +
111                         (iframeSrc ? 'src="' + iframeSrc + '"' : '') +
112                         ' style="display: none" ' +
113                         'height="0" ' +
114                         'width="0" ' +
115                         'tabindex="-1" ' +
116                         'title="empty"/>');
117                     // Append the iframe to the documentElement rather than the body.
118                     // Keeping it outside the body prevents scrolling on the initial
119                     // page load
120                     DOM.prepend(iframe, doc.documentElement);
121 
122                     // init,第一次触发,以后都是 onIframeLoad
123                     Event.add(iframe, "load", function () {
124                         Event.remove(iframe, "load");
125                         // Update the iframe with the initial location hash, if any. This
126                         // will create an initial history entry that the user can return to
127                         // after the state has changed.
128                         hashChange(getHash());
129                         Event.add(iframe, "load", onIframeLoad);
130                         poll();
131                     });
132 
133                     // Whenever `document.title` changes, update the Iframe's title to
134                     // prettify the back/next history menu entries. Since IE sometimes
135                     // errors with "Unspecified error" the very first time this is set
136                     // (yes, very useful) wrap this with a try/catch block.
137                     doc.onpropertychange = function () {
138                         try {
139                             if (event.propertyName === 'title') {
140                                 getIframeDoc(iframe).title =
141                                     doc.title + " - " + getHash();
142                             }
143                         } catch (e) {
144                         }
145                     };
146 
147                     /*
148                      前进后退 : onIframeLoad -> 触发
149                      直接输入 : timer -> hashChange -> onIframeLoad -> 触发
150                      触发统一在 start(load)
151                      iframe 内容和 url 同步
152                      */
153                     function onIframeLoad() {
154                         // S.log('iframe start load..');
155 
156                         // 2011.11.02 note: 不能用 innerHtml 会自动转义!!
157                         // #/x?z=1&y=2 => #/x?z=1&y=2
158                         var c = S.trim(getIframeDoc(iframe).body.innerText),
159                             ch = getHash();
160 
161                         // 后退时不等
162                         // 定时器调用 hashChange() 修改 iframe 同步调用过来的(手动改变 location)则相等
163                         if (c != ch) {
164                             S.log("set loc hash :" + c);
165                             location.hash = c;
166                             // 使lasthash为 iframe 历史, 不然重新写iframe,
167                             // 会导致最新状态(丢失前进状态)
168 
169                             // 后退则立即触发 hashchange,
170                             // 并更新定时器记录的上个 hash 值
171                             lastHash = c;
172                         }
173                         notifyHashChange();
174                     }
175                 }
176             };
177 
178             tearDown = function () {
179                 timer && clearTimeout(timer);
180                 timer = 0;
181                 Event.detach(iframe);
182                 DOM.remove(iframe);
183                 iframe = 0;
184             };
185         }
186 
187         special[HASH_CHANGE] = {
188             setup:function () {
189                 if (this !== win) {
190                     return;
191                 }
192                 // 第一次启动 hashchange 时取一下,不能类库载入后立即取
193                 // 防止类库嵌入后,手动修改过 hash,
194                 lastHash = getHash();
195                 // 不用注册 dom 事件
196                 setup();
197             },
198             tearDown:function () {
199                 if (this !== win) {
200                     return;
201                 }
202                 tearDown();
203             }
204         };
205     }
206 }, {
207     requires:["./base", "dom", "ua", "./special"]
208 });
209 
210 /**
211  * 已知 bug :
212  * - ie67 有时后退后取得的 location.hash 不和地址栏一致,导致必须后退两次才能触发 hashchange
213  *
214  * v1 : 2010-12-29
215  * v1.1: 支持非IE,但不支持onhashchange事件的浏览器(例如低版本的firefox、safari)
216  * refer : http://yiminghe.javaeye.com/blog/377867
217  *         https://github.com/cowboy/jquery-hashchange
218  */