1 | import {addEvt, removeEvt} from '../event';
|
2 | import {root} from '../root';
|
3 |
|
4 | const JSON = root.JSON;
|
5 | const location = root.location;
|
6 | const decodeURIComponent = root.decodeURIComponent;
|
7 | const encodeURIComponent = root.encodeURIComponent;
|
8 |
|
9 | /**
|
10 | * Checks if browser has onhashchange event
|
11 | */
|
12 | export const hasHashChange = () => {
|
13 | let docMode = root.documentMode;
|
14 | return ('onhashchange' in root) && (docMode === undefined || docMode > 7);
|
15 | };
|
16 |
|
17 | /**
|
18 | * Manages state via URL hash changes
|
19 | *
|
20 | * @export
|
21 | * @class Hash
|
22 | */
|
23 | export class Hash {
|
24 |
|
25 | /**
|
26 | * Creates an instance of Hash
|
27 | *
|
28 | * @param {State} state Instance of State
|
29 | */
|
30 | constructor(state) {
|
31 | /**
|
32 | * State object
|
33 | * @type {State}
|
34 | */
|
35 | this.state = state;
|
36 |
|
37 | /**
|
38 | * Cached URL hash
|
39 | * @type {String} Hash string
|
40 | * @private
|
41 | */
|
42 | this.lastHash = null;
|
43 |
|
44 | /**
|
45 | * Application event emitter instance
|
46 | * @type {Emitter}
|
47 | */
|
48 | this.emitter = state.emitter;
|
49 |
|
50 | /**
|
51 | * Bound sync wrapper for future use
|
52 | * @private
|
53 | */
|
54 | this.boundSync = null;
|
55 | }
|
56 |
|
57 | /**
|
58 | * Initializes the Hash object
|
59 | */
|
60 | init() {
|
61 | if (!hasHashChange()) {
|
62 | return;
|
63 | }
|
64 |
|
65 | this.lastHash = location.hash;
|
66 | //Store a bound sync wrapper
|
67 | this.boundSync = this.sync.bind(this);
|
68 | this.emitter.on(['state-changed'], (tf, state) => this.update(state));
|
69 | this.emitter.on(['initialized'], this.boundSync);
|
70 | addEvt(root, 'hashchange', this.boundSync);
|
71 | }
|
72 |
|
73 | /**
|
74 | * Updates the URL hash based on a state change
|
75 | *
|
76 | * @param {State} state Instance of State
|
77 | */
|
78 | update(state) {
|
79 | let hash = `#${encodeURIComponent(JSON.stringify(state))}`;
|
80 | if (this.lastHash === hash) {
|
81 | return;
|
82 | }
|
83 |
|
84 | location.hash = hash;
|
85 | this.lastHash = hash;
|
86 | }
|
87 |
|
88 | /**
|
89 | * Converts a URL hash into a JSON object
|
90 | *
|
91 | * @param {String} hash URL hash fragment
|
92 | * @returns {Object} JSON object
|
93 | */
|
94 | parse(hash) {
|
95 | if (hash.indexOf('#') === -1) {
|
96 | return null;
|
97 | }
|
98 | hash = hash.substr(1);
|
99 | return JSON.parse(decodeURIComponent(hash));
|
100 | }
|
101 |
|
102 | /**
|
103 | * Applies current hash state to features
|
104 | */
|
105 | sync() {
|
106 | let state = this.parse(location.hash);
|
107 | if (!state) {
|
108 | return;
|
109 | }
|
110 | // override current state with persisted one and sync features
|
111 | this.state.overrideAndSync(state);
|
112 | }
|
113 |
|
114 | /**
|
115 | * Release Hash event subscriptions and clear fields
|
116 | */
|
117 | destroy() {
|
118 | this.emitter.off(['state-changed'], (tf, state) => this.update(state));
|
119 | this.emitter.off(['initialized'], this.boundSync);
|
120 | removeEvt(root, 'hashchange', this.boundSync);
|
121 |
|
122 | this.state = null;
|
123 | this.lastHash = null;
|
124 | this.emitter = null;
|
125 | }
|
126 | }
|