UNPKG

22.6 kBJavaScriptView Raw
1/*!
2 * OpenUI5
3 * (c) Copyright 2009-2022 SAP SE or an SAP affiliate company.
4 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
5 */
6sap.ui.define([
7 "jquery.sap.global",
8 "sap/base/Log",
9 "sap/base/util/uid",
10 "sap/base/strings/escapeRegExp"
11], function(jQuery, Log, uid, escapeRegExp) {
12 "use strict";
13
14 (function(window){ // TODO remove inner scope function
15 //suffix of virtual hash
16 var skipSuffix = "_skip",
17
18 //the regular expression for matching the unique id in the hash
19 rIdRegex = /\|id-[0-9]+-[0-9]+/,
20
21 //the regular expression for matching the suffix in the hash
22 skipRegex = new RegExp(skipSuffix + "[0-9]*$"),
23
24 //array of routes
25 routes = [],
26
27 //array represents the current history stack
28 hashHistory = [],
29
30 //mark if the change of the hash is from the code or from pressing the back or forward button
31 mSkipHandler = {},
32
33 //index of the skip suffix
34 skipIndex = 0,
35
36 //the current hash of the history handling
37 currentHash,
38
39 //the hash format separator
40 sIdSeperator = "|",
41
42 //array that buffers the changed to the hash in order to make them handled one by one
43 aHashChangeBuffer = [],
44
45 //marker if the handling hash change is in processing
46 bInProcessing = false,
47
48 //default handler which will be called when url contains an empty hash
49 defaultHandler,
50
51 //avoid calling the history initialization twice
52 bInitialized = false;
53
54
55 /**
56 * jQuery.sap.history is deprecated. Please use {@link sap.ui.core.routing.Route} instead.
57 *
58 * Initialize the history handling and set the routes and default handler.
59 * This should be only called once with the mSettings set in the right format. If the mSettings is not an object,
60 * you have another chance to call this function again to initialize the history handling. But once the mSettings
61 * is set with an object, you can only call the addRoute and setDefaultHandler to set the data.
62 *
63 * @deprecated since 1.19.1. Please use {@link sap.ui.core.routing.Route} instead.
64 * @param {object} mSettings The map that contains data in format:
65 * <pre>
66 * {
67 * routes: [{
68 * path: string //identifier for one kind of hash
69 * handler: function //function what will be called when the changed hash is matched against the path.
70 * //first parameter: the json data passed in when calling the addHistory
71 * //second parameter: the type of the navigation {@link jQuery.sap.history.NavType}
72 * }],
73 * defaultHandler: function //this function will be called when empty hash is matched
74 * //first parameter: the type of the navigation {@link jQuery.sap.history.NavType}
75 * }
76 * </pre>
77 * @public
78 * @name jQuery.sap.history
79 * @class Enables the back and forward buttons in browser to navigate back or forth through the browser history stack.<br/><br/>
80 *
81 * It also supports adding virtual history which used only to mark some intermediate state in order to navigate back to the previous state.
82 * And this state will be skipped from the browser history stack immediately after a new history state is added to the history stack after this state <br/><br/>
83 *
84 * By providing the hash saved from the return value of calling jQuery.sap.history.addHistory, jQuery.sap.history.backToHash will navigate back directly to the
85 * history state with the same hash. <br/><br/>
86 *
87 * Please use {@link jQuery.sap.history#back}() to go one step back in the history stack instead of using window.history.back(), because it handles the empty history stack
88 * situation and will call the defaultHandler for this case. <br/><br/>
89 *
90 *
91 * Example for the usage of history handling:
92 * <pre>
93 * //Initialization
94 * jQuery.sap.history({
95 * routes: [], //please refer to the jQuery.sap.history function comment for the format.
96 * defaultHandler: function(){
97 * //code here
98 * }
99 * });
100 *
101 * //add history
102 * var hash = jQuery.sap.history.addHistory("IDENTIFIER", jsonData);
103 *
104 * //add virtual history
105 * jQuery.sap.history.addVirtualHistory();
106 *
107 * //back to hash
108 * jQuery.sap.history.backToHash(hash);
109 *
110 * //back one step along the history stack
111 * jQuery.sap.history.back();
112 * </pre>
113 *
114 */
115 jQuery.sap.history = function(mSettings){
116 //if mSetting is not an object map, return
117 if (!jQuery.isPlainObject(mSettings)) {
118 return;
119 }
120
121
122 if (!bInitialized) {
123 var jWindowDom = jQuery(window),
124 //using href instead of hash to avoid the escape problem in firefox
125 sHash = (window.location.href.split("#")[1] || "");
126
127 jWindowDom.on('hashchange', detectHashChange);
128
129 if (Array.isArray(mSettings.routes)) {
130 var i, route;
131 for (i = 0 ; i < mSettings.routes.length ; i++) {
132 route = mSettings.routes[i];
133 if (route.path && route.handler) {
134 jQuery.sap.history.addRoute(route.path, route.handler);
135 }
136 }
137 }
138
139 if (typeof mSettings.defaultHandler === "function") {
140 defaultHandler = mSettings.defaultHandler;
141 }
142
143 //push the current hash to the history stack
144 hashHistory.push(sHash);
145
146 //goes in from bookmark
147 if (sHash.length > 1) {
148 jWindowDom.trigger("hashchange", [true]);
149 } else {
150 currentHash = sHash;
151 }
152
153 bInitialized = true;
154 }
155 };
156
157 /**
158 * This function adds a history record. It will not trigger the related handler of the routes, the changes have to be done by the
159 * developer. Normally, a history record should be added when changes are done already.
160 *
161 * @param {string} sIdf The identifier defined in the routes which will be matched in order to call the corresponding handler
162 * @param {object} oStateData The object passed to the corresponding handler when the identifier is matched with the url hash
163 * @param {boolean} bBookmarkable Default value is set to true. If this is set to false, the default handler will be called when this identifier and data are matched
164 * @param {boolean} [bVirtual] This states if the history is a virtual history that should be skipped when going forward or backward in the history stack.
165 * @returns {string} sHash The complete hash string which contains the identifier, stringified data, optional uid, and bookmarkable digit. This hash can be passed into
166 * the backToHash function when navigating back to this state is intended.
167 *
168 * @function
169 * @public
170 * @name jQuery.sap.history#addHistory
171 */
172 jQuery.sap.history.addHistory = function(sIdf, oStateData, bBookmarkable, bVirtual){
173 var sUid, sHash;
174 if (bBookmarkable === undefined) {
175 bBookmarkable = true;
176 }
177
178 if (!bVirtual) {
179 sHash = preGenHash(sIdf, oStateData);
180 sUid = getAppendId(sHash);
181 if (sUid) {
182 sHash += (sIdSeperator + sUid);
183 }
184 sHash += (sIdSeperator + (bBookmarkable ? "1" : "0"));
185
186 } else {
187 sHash = getNextSuffix(currentHash);
188 }
189 aHashChangeBuffer.push(sHash);
190 mSkipHandler[sHash] = true;
191 window.location.hash = sHash;
192
193 return sHash;
194 };
195
196
197 /**
198 * This function adds a virtual history record based on the current hash. A virtual record is only for marking the current state of the application,
199 * and when the back button clicked it will return to the previous state. It is used when the marked state shouldn't be seen by the user when user click
200 * the back or forward button of the browser. For example, when showing a context menu a virtual history record should be added and this record will be skipped
201 * when user navigates back and it will return directly to the previous history record. If you avoid adding the virtual history record, it will return to one
202 * history record before the one your virtual record is based on. That's why virtual record is necessary.
203 *
204 * @function
205 * @public
206 * @name jQuery.sap.history#addVirtualHistory
207 */
208 jQuery.sap.history.addVirtualHistory = function(){
209 jQuery.sap.history.addHistory("", undefined, false, true);
210 };
211
212
213 /**
214 * Adds a route to the history handling.
215 *
216 * @param {string} sIdf The identifier that is matched with the hash in the url in order to call the corresponding handler.
217 * @param {function} fn The function that will be called when the identifier is matched with the hash.
218 * @param {object} [oThis] If oThis is provided, the fn function's this keyword will be bound to this object.
219 *
220 * @returns {object} It returns the this object to enable chaining.
221 *
222 * @function
223 * @public
224 * @name jQuery.sap.history#addRoute
225 */
226 jQuery.sap.history.addRoute = function(sIdf, fn, oThis){
227 if (oThis) {
228 fn = jQuery.proxy(fn, oThis);
229 }
230
231 var oRoute = {};
232 oRoute.sIdentifier = sIdf;
233 oRoute['action'] = fn;
234
235 routes.push(oRoute);
236 return this;
237 };
238
239 /**
240 * Set the default handler which will be called when there's an empty hash in the url.
241 *
242 * @param {function} fn The function that will be set as the default handler
243 * @public
244 *
245 * @function
246 * @name jQuery.sap.history#setDefaultHandler
247 */
248 jQuery.sap.history.setDefaultHandler = function(fn){
249 defaultHandler = fn;
250 };
251
252 jQuery.sap.history.getDefaultHandler = function(){
253 return defaultHandler;
254 };
255
256
257
258 /**
259 * This function calculate the number of back steps to the specific sHash passed as parameter,
260 * and then go back to the history state with this hash.
261 *
262 * @param {string} sHash The hash string needs to be navigated. This is normally returned when you call the addhistory method.
263 * @public
264 *
265 * @function
266 * @name jQuery.sap.history#backToHash
267 */
268 jQuery.sap.history.backToHash = function(sHash){
269 sHash = sHash || "";
270 var iSteps;
271
272 //back is called directly after restoring the bookmark. Since there's no history stored, call the default handler.
273 if (hashHistory.length === 1) {
274 if (typeof defaultHandler === "function") {
275 defaultHandler();
276 }
277 } else {
278 iSteps = calculateStepsToHash(currentHash, sHash);
279 if (iSteps < 0) {
280 window.history.go(iSteps);
281 } else {
282 Log.error("jQuery.sap.history.backToHash: " + sHash + "is not in the history stack or it's after the current hash");
283 }
284 }
285 };
286
287 /**
288 * This function will navigate back to the recent history state which has the sPath identifier. It is usually used to navigate back along one
289 * specific route and jump over the intermediate history state if there are any.
290 *
291 * @param {string} sPath The route identifier to which the history navigates back.
292 * @public
293 *
294 * @function
295 * @name jQuery.sap.history#backThroughPath
296 */
297 jQuery.sap.history.backThroughPath = function(sPath){
298 sPath = sPath || "";
299 sPath = window.encodeURIComponent(sPath);
300 var iSteps;
301
302 //back is called directly after restoring the bookmark. Since there's no history stored, call the default handler.
303 if (hashHistory.length === 1) {
304 if (typeof defaultHandler === "function") {
305 defaultHandler();
306 }
307 } else {
308 iSteps = calculateStepsToHash(currentHash, sPath, true);
309 if (iSteps < 0) {
310 window.history.go(iSteps);
311 } else {
312 Log.error("jQuery.sap.history.backThroughPath: there's no history state which has the " + sPath + " identifier in the history stack before the current hash");
313 }
314 }
315 };
316
317 /**
318 * This function navigates back through the history stack. The number of steps is set by the parameter iSteps. It also handles the situation when it's called while there's nothing in the history stack.
319 * Normally this happens when the application is restored from the bookmark. If there's nothing in the history stack, the default handler will be called with NavType jQuery.sap.history.NavType.Back.
320 *
321 * @param {int} [iSteps] how many steps you want to go back, by default the value is 1.
322 * @public
323 *
324 * @function
325 * @name jQuery.sap.history#back
326 */
327 jQuery.sap.history.back = function(iSteps){
328
329 //back is called directly after restoring the bookmark. Since there's no history stored, call the default handler.
330 if (hashHistory.length === 1) {
331 if (typeof defaultHandler === "function") {
332 defaultHandler(jQuery.sap.history.NavType.Back);
333 }
334 } else {
335 if (!iSteps) {
336 iSteps = 1;
337 }
338 window.history.go(-1 * iSteps);
339 }
340 };
341
342 /**
343 * @enum {string}
344 * @public
345 * @alias jQuery.sap.history.NavType
346 * @deprecated since 1.19.1. Please use {@link sap.ui.core.routing.HistoryDirection} instead.
347 */
348 jQuery.sap.history.NavType = {
349
350 /**
351 * This indicates that the new hash is achieved by pressing the back button.
352 * @public
353 * @constant
354 */
355 Back: "_back",
356
357 /**
358 * This indicates that the new hash is achieved by pressing the forward button.
359 * @public
360 * @constant
361 */
362 Forward: "_forward",
363
364 /**
365 * This indicates that the new hash is restored from the bookmark.
366 * @public
367 * @constant
368 */
369 Bookmark: "_bookmark",
370
371 /**
372 * This indicates that the new hash is achieved by some unknown direction.
373 * This happens when the user navigates out of the application and then click on the forward button
374 * in the browser to navigate back to the application.
375 * @public
376 * @constant
377 */
378 Unknown: "_unknown"
379
380 };
381
382 /**
383 * This function calculates the number of steps from the sCurrentHash to sToHash. If the sCurrentHash or the sToHash is not in the history stack, it returns 0.
384 *
385 * @private
386 */
387 function calculateStepsToHash(sCurrentHash, sToHash, bPrefix){
388 var iCurrentIndex = hashHistory.indexOf(sCurrentHash),
389 iToIndex,
390 i,
391 tempHash;
392 if (iCurrentIndex > 0) {
393 if (bPrefix) {
394 for (i = iCurrentIndex - 1; i >= 0 ; i--) {
395 tempHash = hashHistory[i];
396 if (tempHash.indexOf(sToHash) === 0 && !isVirtualHash(tempHash)) {
397 return i - iCurrentIndex;
398 }
399 }
400 } else {
401 iToIndex = hashHistory.indexOf(sToHash);
402
403 //When back to home is needed, and application is started with nonempty hash but it's nonbookmarkable
404 if ((iToIndex === -1) && sToHash.length === 0) {
405 return -1 * iCurrentIndex;
406 }
407
408 if ((iToIndex > -1) && (iToIndex < iCurrentIndex)) {
409 return iToIndex - iCurrentIndex;
410 }
411 }
412 }
413
414 return 0;
415 }
416
417
418
419 /**
420 * This function is bound to the window's hashchange event, and it detects the change of the hash.
421 * When history is added by calling the addHistory or addVirtualHistory function, it will not call the real onHashChange function
422 * because changes are already done. Only when a hash is navigated by clicking the back or forward buttons in the browser,
423 * the onHashChange will be called.
424 *
425 * @private
426 */
427 function detectHashChange(oEvent, bManual){
428 //Firefox will decode the hash when it's set to the window.location.hash,
429 //so we need to parse the href instead of reading the window.location.hash
430 var sHash = (window.location.href.split("#")[1] || "");
431 sHash = formatHash(sHash);
432
433 if (bManual || !mSkipHandler[sHash]) {
434 aHashChangeBuffer.push(sHash);
435 }
436
437 if (!bInProcessing) {
438 bInProcessing = true;
439 if (aHashChangeBuffer.length > 0) {
440 var newHash = aHashChangeBuffer.shift();
441
442 if (mSkipHandler[newHash]) {
443 reorganizeHistoryArray(newHash);
444 delete mSkipHandler[newHash];
445 } else {
446 onHashChange(newHash);
447 }
448 currentHash = newHash;
449 }
450 bInProcessing = false;
451 }
452 }
453
454 /**
455 * This function removes the leading # sign if there's any. If the bRemoveId is set to true, it will also remove the unique
456 * id inside the hash.
457 *
458 * @private
459 */
460 function formatHash(hash, bRemoveId){
461 var sRes = hash, iSharpIndex = hash ? hash.indexOf("#") : -1;
462
463 if (iSharpIndex === 0) {
464 sRes = sRes.slice(iSharpIndex + 1);
465 }
466
467 if (bRemoveId) {
468 sRes = sRes.replace(rIdRegex, "");
469 }
470
471 return sRes;
472 }
473
474 /**
475 * This function returns a hash with suffix added to the end based on the sHash parameter. It handles as well when the current
476 * hash is already with suffix. It returns a new suffix with a unique number in the end.
477 *
478 * @private
479 */
480 function getNextSuffix(sHash){
481 var sPath = sHash ? sHash : "";
482
483 if (isVirtualHash(sPath)) {
484 var iIndex = sPath.lastIndexOf(skipSuffix);
485 sPath = sPath.slice(0, iIndex);
486 }
487
488 return sPath + skipSuffix + skipIndex++;
489 }
490
491 /**
492 * This function encode the identifier and data into a string.
493 *
494 * @private
495 */
496 function preGenHash(sIdf, oStateData){
497 var sEncodedIdf = encodeURIComponent(sIdf);
498 var sEncodedData = encodeURIComponent(JSON.stringify(oStateData));
499 return sEncodedIdf + sIdSeperator + sEncodedData;
500 }
501
502 /**
503 * This function checks if the combination of the identifier and data is unique in the current history stack.
504 * If yes, it returns an empty string. Otherwise it returns a unique id.
505 *
506 * @private
507 */
508 function getAppendId(sHash){
509 var iIndex = hashHistory.indexOf(currentHash),
510 i, sHistory;
511 if (iIndex > -1) {
512 for (i = 0 ; i < iIndex + 1 ; i++) {
513 sHistory = hashHistory[i];
514 if (sHistory.slice(0, sHistory.length - 2) === sHash) {
515 return uid();
516 }
517 }
518 }
519
520 return "";
521 }
522
523 /**
524 * This function manages the internal array of history records.
525 *
526 * @private
527 */
528 function reorganizeHistoryArray(sHash){
529 var iIndex = hashHistory.indexOf(currentHash);
530
531 if ( !(iIndex === -1 || iIndex === hashHistory.length - 1) ) {
532 hashHistory.splice(iIndex + 1, hashHistory.length - 1 - iIndex);
533 }
534 hashHistory.push(sHash);
535 }
536
537 /**
538 * This method judges if a hash is a virtual hash that needs to be skipped.
539 *
540 * @private
541 */
542 function isVirtualHash(sHash){
543 return skipRegex.test(sHash);
544 }
545
546 /**
547 * This function calculates the steps forward or backward that need to skip the virtual history states.
548 *
549 * @private
550 */
551 function calcStepsToRealHistory(sCurrentHash, bForward){
552 var iIndex = hashHistory.indexOf(sCurrentHash),
553 i;
554
555 if (iIndex !== -1) {
556 if (bForward) {
557 for (i = iIndex ; i < hashHistory.length ; i++) {
558 if (!isVirtualHash(hashHistory[i])) {
559 return i - iIndex;
560 }
561 }
562 } else {
563 for (i = iIndex ; i >= 0 ; i--) {
564 if (!isVirtualHash(hashHistory[i])) {
565 return i - iIndex;
566 }
567 }
568 return -1 * (iIndex + 1);
569 }
570 }
571 }
572
573
574 /**
575 * This is the main function that handles the hash change event.
576 *
577 * @private
578 */
579 function onHashChange(sHash){
580 var oRoute, iStep, oParsedHash, iNewHashIndex, sNavType;
581
582 //handle the nonbookmarkable hash
583 if (currentHash === undefined) {
584 //url with hash opened from bookmark
585 oParsedHash = parseHashToObject(sHash);
586
587 if (!oParsedHash || !oParsedHash.bBookmarkable) {
588 if (typeof defaultHandler === "function") {
589 defaultHandler(jQuery.sap.history.NavType.Bookmark);
590 }
591 return;
592 }
593 }
594
595 if (sHash.length === 0) {
596 if (typeof defaultHandler === "function") {
597 defaultHandler(jQuery.sap.history.NavType.Back);
598 }
599 } else {
600 //application restored from bookmark with non-empty hash, and later navigates back to the first hash token
601 //the defaultHandler should be triggered
602 iNewHashIndex = hashHistory.indexOf(sHash);
603 if (iNewHashIndex === 0) {
604 oParsedHash = parseHashToObject(sHash);
605 if (!oParsedHash || !oParsedHash.bBookmarkable) {
606 if (typeof defaultHandler === "function") {
607 defaultHandler(jQuery.sap.history.NavType.Back);
608 }
609 return;
610 }
611 }
612
613 //need to handle when iNewHashIndex equals -1.
614 //This happens when user navigates out the current application, and later navigates back.
615 //In this case, the hashHistory is an empty array.
616
617
618 if (isVirtualHash(sHash)) {
619 //this is a virtual history, should do the skipping calculation
620 if (isVirtualHash(currentHash)) {
621 //go back to the first one that is not virtual
622 iStep = calcStepsToRealHistory(sHash, false);
623 window.history.go(iStep);
624 } else {
625 var sameFamilyRegex = new RegExp(escapeRegExp(currentHash + skipSuffix) + "[0-9]*$");
626 if (sameFamilyRegex.test(sHash)) {
627 //going forward
628 //search forward in history for the first non-virtual hash
629 //if there is, change to that one window.history.go
630 //if not, stay and return false
631 iStep = calcStepsToRealHistory(sHash, true);
632 if (iStep) {
633 window.history.go(iStep);
634 } else {
635 window.history.back();
636 }
637
638 } else {
639 //going backward
640 //search backward for the first non-virtual hash and there must be one
641 iStep = calcStepsToRealHistory(sHash, false);
642 window.history.go(iStep);
643 }
644 }
645 } else {
646 if (iNewHashIndex === -1) {
647 sNavType = jQuery.sap.history.NavType.Unknown;
648 hashHistory.push(sHash);
649 } else {
650 if (hashHistory.indexOf(currentHash, iNewHashIndex + 1) === -1) {
651 sNavType = jQuery.sap.history.NavType.Forward;
652 } else {
653 sNavType = jQuery.sap.history.NavType.Back;
654 }
655 }
656
657
658 oParsedHash = parseHashToObject(sHash);
659 if (oParsedHash) {
660 oRoute = findRouteByIdentifier(oParsedHash.sIdentifier);
661
662 if (oRoute) {
663 oRoute.action.apply(null, [oParsedHash.oStateData, sNavType]);
664 }
665 } else {
666 Log.error("hash format error! The current Hash: " + sHash);
667 }
668
669 }
670 }
671 }
672
673 /**
674 * This function returns the route object matched by the identifier passed as parameter.
675 * @private
676 */
677 function findRouteByIdentifier(sIdf){
678 var i;
679 for (i = 0 ; i < routes.length ; i++) {
680 if (routes[i].sIdentifier === sIdf) {
681 return routes[i];
682 }
683 }
684 }
685
686 /**
687 * This function parses the hash from the url to a concrete project in the format:
688 * {
689 * sIdentifier: string,
690 * oStateData: object,
691 * uid: string (optional),
692 * bBookmarkable: boolean
693 *
694 * }
695 * @private
696 */
697 function parseHashToObject(sHash){
698 if (isVirtualHash(sHash)) {
699 var i = sHash.lastIndexOf(skipSuffix);
700 sHash = sHash.slice(0, i);
701 }
702
703
704 var aParts = sHash.split(sIdSeperator), oReturn = {};
705 if (aParts.length === 4 || aParts.length === 3) {
706 oReturn.sIdentifier = window.decodeURIComponent(aParts[0]);
707 oReturn.oStateData = JSON.parse(window.decodeURIComponent(aParts[1]));
708 if (aParts.length === 4) {
709 oReturn.uid = aParts[2];
710 }
711
712 oReturn.bBookmarkable = aParts[aParts.length - 1] === "0" ? false : true;
713
714 return oReturn;
715 } else {
716 //here can be empty hash only with a skipable suffix
717 return null;
718 }
719 }
720
721 })(this);
722
723 return jQuery;
724
725});