1 | /** |
---|
2 | * t29v6 Menu |
---|
3 | * |
---|
4 | * Features: |
---|
5 | * - Collapsable (can be expanded by button) |
---|
6 | * - Scrollable (can be maked position:fixed by button) |
---|
7 | * - Guide beam (footer clone of navigation in a fancy beam like way) |
---|
8 | * |
---|
9 | * Language-specific strings are taken from t29.msg. |
---|
10 | * |
---|
11 | * (c) 2012 Sven Koeppel <sven@tech..29.de> |
---|
12 | * Licensed under GPL |
---|
13 | **/ |
---|
14 | |
---|
15 | if(!t29) window.t29 = {}; // the t29 namespace |
---|
16 | |
---|
17 | t29.menu = { collapsed:{}, scroll:{}, guide:{} }; // mit Unterklassen |
---|
18 | t29.menu.setup = function() { |
---|
19 | t29.menu.side = $("nav.side"); // Hauptseitennavigation |
---|
20 | t29.menu.beam = $("nav.guide"); // Strahlnavigation/Guide (Kopie von side) |
---|
21 | t29.menu.rel = $("nav.rel"); // relative navigation im footer (vor/zurück) |
---|
22 | |
---|
23 | if(t29.menu.side.hasClass("contains-menu")) { |
---|
24 | t29.menu.collapsed.setup(); |
---|
25 | t29.menu.scroll.setup(); |
---|
26 | } // else: hasClass("contains-custom") -> keine Navigation |
---|
27 | |
---|
28 | // t29v6 launch: Guide-Menü erst mal deaktiviert |
---|
29 | //t29.menu.guide.setup(); |
---|
30 | }; |
---|
31 | |
---|
32 | /*************************************************************************************** |
---|
33 | 1. Collapsable Menu system t29.menu.collapsed |
---|
34 | |
---|
35 | The collapsable menu system is capable of collapse parts of the menu by user |
---|
36 | interaction or program request. The current state is stored in t29.prefs. |
---|
37 | |
---|
38 | ***************************************************************************************/ |
---|
39 | |
---|
40 | /** |
---|
41 | * Construct a new collapsible. It needs three named arguments: |
---|
42 | * c = new t29.menu.Collapsible({ |
---|
43 | * id: 'foobar', // a senseful id for the t29.prefs cookie system |
---|
44 | * lists: $(".your ul"), // some element which should be collapsed |
---|
45 | * button: $("<button/>").appendTo("body"); // some button which should do the work |
---|
46 | * }); |
---|
47 | * |
---|
48 | **/ |
---|
49 | t29.menu.Collapsible = function(arglist) { |
---|
50 | for (var n in arguments[0]) { this[n] = arguments[0][n]; } |
---|
51 | this.store_key = 'menu-collapsible-'+this.id; // key for t29.prefs.get/set |
---|
52 | |
---|
53 | // default values |
---|
54 | if(!this.button) // button widget |
---|
55 | this.button_box = $('<li class="button-box"></li>'); // in nav.side |
---|
56 | this.button_box.appendTo("nav.side li.collapsible-button-after"); |
---|
57 | |
---|
58 | this.button = $('<span class="button collapse-menu"></span>') |
---|
59 | .addClass('for-'+this.id).//appendTo("nav.side"); |
---|
60 | appendTo(this.button_box); |
---|
61 | |
---|
62 | if(!this.label) { // button label |
---|
63 | this.label = {}; |
---|
64 | this.label[t29c.FOLD] = t29._("js-menu-collapse-out"); |
---|
65 | this.label[t29c.EXPAND] = t29._("js-menu-collapse-in"); |
---|
66 | } |
---|
67 | if(!this.initial) // initial state |
---|
68 | this.initial = t29.prefs.get(this.store_key, t29c.FOLD); |
---|
69 | |
---|
70 | // set initial state |
---|
71 | this.set(this.initial, t29c.QUICK); |
---|
72 | |
---|
73 | // set button callback |
---|
74 | this.button.click($.proxy(function(){ this.set(); }, this)); |
---|
75 | } |
---|
76 | |
---|
77 | // Constants: |
---|
78 | if(!window.t29c) window.t29c = {}; // namespace for t29 contstants |
---|
79 | t29c.FOLD = true; // state: folded menu (small) |
---|
80 | t29c.EXPAND = false; // state: expanded menu (big) |
---|
81 | t29c.QUICK = true; // action: quick crossover (no animation, instantanous) |
---|
82 | t29c.ANIMATE = false; // action: animated crossover (visible to user) |
---|
83 | |
---|
84 | /** |
---|
85 | * Menu ein- oder ausklappen. |
---|
86 | * |
---|
87 | * @param target_state true: Eingeklappt, false: ausgeklappt |
---|
88 | * @param quick true=keine Animation, false/undefined=Animation |
---|
89 | **/ |
---|
90 | t29.menu.Collapsible.prototype.set = function(collapse, quick) { |
---|
91 | if(collapse == undefined) |
---|
92 | collapse = ! this.is_collapsed(); |
---|
93 | log("Collapse: "+this.id+" FOLD " +(collapse==t29c.FOLD ? "<=" : "=>")+" EXPAND " + (quick==t29c.QUICK ? "[quick!]" : "")); |
---|
94 | if(this.set_pre) |
---|
95 | this.set_pre(collapse, quick); // execute some callback |
---|
96 | if(quick) this.lists[collapse ? 'hide' : 'show'](); |
---|
97 | else this.lists[collapse ? 'slideUp' : 'slideDown'](); |
---|
98 | this.button.text(this.label[collapse]); |
---|
99 | // body CSS class shall only be used for CSS interaction, not for JS readout. Use is_collapsed() instead. |
---|
100 | $("body")[collapse ? 'addClass' : 'removeClass']("collapsed-menu-"+this.id); |
---|
101 | t29.prefs.set(this.store_key, collapse); |
---|
102 | } |
---|
103 | |
---|
104 | // returns whether menu is collapsed (boolean). |
---|
105 | t29.menu.Collapsible.prototype.is_collapsed = function() { return t29.prefs.get(this.store_key) == t29c.FOLD; } |
---|
106 | |
---|
107 | t29.menu.collapsed.setup = function() { |
---|
108 | // set up some collapsible lists |
---|
109 | t29.menu.collapsed.u3 = new t29.menu.Collapsible({ |
---|
110 | id: 'u3', |
---|
111 | lists: $("nav.side .u3").not("nav.side li.active > .u3"), //, .geraete"), |
---|
112 | }); |
---|
113 | |
---|
114 | // check if we want mini menu for the beginning |
---|
115 | if( $("body").hasClass("in-geraete") ) { |
---|
116 | t29.menu.collapsed.u3.button.hide(); |
---|
117 | // mini doesn't care about cookie settings. |
---|
118 | t29.menu.collapsed.mini = new t29.menu.Collapsible({ |
---|
119 | id: 'mini', |
---|
120 | lists: $("nav.side li").not('.guide-only').not("li.active, li.active > ul.u3 > li, li.active > ul.u4 > li"), |
---|
121 | initial: t29c.FOLD, |
---|
122 | set_pre: function(collapse) { |
---|
123 | if(collapse == t29c.EXPAND) { |
---|
124 | // after first expanding, disable system and enable rest of systems |
---|
125 | this.button.hide(); |
---|
126 | t29.menu.collapsed.u3.button.show(); |
---|
127 | } |
---|
128 | } |
---|
129 | }); |
---|
130 | } |
---|
131 | |
---|
132 | /* |
---|
133 | t29.menu.collapsed.geraete = new t29.menu.Collapsible({ |
---|
134 | id: 'geraete', |
---|
135 | lists: $("nav.side ul.geraete"), |
---|
136 | label: (function(){ l = {}; l[t29c.FOLD] = '(+ extra)'; l[t29c.EXPAND] = '(- extra)'; return l; })(), |
---|
137 | }); |
---|
138 | */ |
---|
139 | |
---|
140 | // special situation on gerate pages (body.in-geraete): only active li's are shown there |
---|
141 | // by default. This is a third state next to FOLDed and EXPANDed menu: super-FOLDED. |
---|
142 | // Clicking the 'details' button yields ordinary FOLDed state. |
---|
143 | |
---|
144 | // hide geraete |
---|
145 | //t29.menu.collapsed.geraete.button.hide(); |
---|
146 | //$("ul.geraete").hide(); |
---|
147 | }; |
---|
148 | |
---|
149 | /*************************************************************************************** |
---|
150 | 2. Menu scroll system t29.menu.scroll |
---|
151 | |
---|
152 | The scrollable menu system can handle a position:fixed navigation area with dynamic |
---|
153 | switching to static or absolute positioning. It is narrowly toothed to the |
---|
154 | collapse system. Current state is stored in t29.prefs. |
---|
155 | |
---|
156 | ***************************************************************************************/ |
---|
157 | |
---|
158 | // enums, die CSS-Klassen im <html> entsprechen: |
---|
159 | t29.menu.scroll.States = Object.freeze({STATIC:"static-menu",FIX:"fixed-menu",STICK_TOP:"stick-top-menu",STICK_BOTTOM:"stick-bottom-menu"}); |
---|
160 | /** |
---|
161 | * Menuezustand beim Scrollen umschalten. |
---|
162 | * @param target_state Zustand aus scroll.States-Enum |
---|
163 | * @param |
---|
164 | * |
---|
165 | **/ |
---|
166 | t29.menu.scroll.set = function(target_state) { |
---|
167 | old_state = t29.menu.scroll.state; |
---|
168 | t29.menu.scroll.state = target_state; |
---|
169 | $("html").removeClass("static-menu fixed-menu stick-top-menu stick-bottom-menu").addClass(t29.menu.scroll.state); |
---|
170 | |
---|
171 | // Aufraeumen nach altem Status: |
---|
172 | switch(old_state) { |
---|
173 | case t29.menu.scroll.States.STICK_BOTTOM: |
---|
174 | t29.menu.side.attr("style",""); // reset css "top" value for positioning |
---|
175 | break; |
---|
176 | } |
---|
177 | |
---|
178 | // Einrichten des neuen Status: |
---|
179 | log("Gehe in Scroll-Zustand "+target_state); |
---|
180 | switch(target_state) { |
---|
181 | case t29.menu.scroll.States.STICK_TOP: |
---|
182 | // Menue schlaegt obene an. Prinzipiell Gleicher Zustand wie STATIC. Weiter. |
---|
183 | case t29.menu.scroll.States.STATIC: |
---|
184 | // die CSS-Klassen regeln eigentlich alles. |
---|
185 | //CSS// t29.menu.collapsed.u3.button.show(); |
---|
186 | t29.menu.scroll.but.text(t29._("js-menu-scroll-show")); |
---|
187 | t29.menu.side.show(); |
---|
188 | break; |
---|
189 | case t29.menu.scroll.States.FIX: |
---|
190 | // checken ob fixing ueberhaupt geht |
---|
191 | /* |
---|
192 | if( !t29.menu.collapsed.is() && t29.menu.side.height() > $(window).height()) { |
---|
193 | // Navi ist gerade ausgeklappt und zu groß fuer Bildschirm. Probiere Einklappen: |
---|
194 | t29.menu.collapsed.set(true, true); |
---|
195 | if(t29.menu.side.height() > $(window).height()) { |
---|
196 | // Navi ist auch eingeklappt noch zu groß! |
---|
197 | log("Navi ist auch eingeklappt zu groß zum fixen!"); |
---|
198 | // eingeklappt lassen. Weitermachen. |
---|
199 | // hier noch was ueberlegen: Bei zu kleinem Bildschirm |
---|
200 | // sollte der Button gar nicht erst angezeigt werden. |
---|
201 | // dazu braucht man einen resize-Listener, der aber im |
---|
202 | // ausgeklappten zustand jedesmal checken müsste, ob das |
---|
203 | // eingeklappte menue reinpasst. (werte muss man cachen) |
---|
204 | // Ziemlich doofe Aufgabe. |
---|
205 | } |
---|
206 | }*/ |
---|
207 | |
---|
208 | t29.menu.collapsed.u3.set(true, true); // Sicherstellen, dass Navi eingeklappt. |
---|
209 | //CSS// t29.menu.collapsed.u3.button.hide(); // Ausgeklappte Navi passt auf keinen Bildschirm. |
---|
210 | t29.menu.scroll.but.text(t29._("js-menu-scroll-hide")); |
---|
211 | break; |
---|
212 | case t29.menu.scroll.States.STICK_BOTTOM: |
---|
213 | // Position absolute; Top-Position manuell setzen. |
---|
214 | t29.menu.side.css({top: t29.menu.scroll.stick_bottom }); |
---|
215 | break; |
---|
216 | default: |
---|
217 | log("Schlechter Zustand: "+target_state); |
---|
218 | } |
---|
219 | } |
---|
220 | |
---|
221 | t29.menu.scroll.setup = function() { |
---|
222 | t29.menu.scroll.but = $('<span class="button get-menu"></span>').appendTo("section.sidebar"); |
---|
223 | |
---|
224 | /** |
---|
225 | * t29.prefs and t29.menu.scroll: Valid values are only FIX and STATIC. |
---|
226 | * Crossover states like STICK_BOTTOM, STICK_TOP shall not be stored. |
---|
227 | **/ |
---|
228 | t29.menu.scroll.store_key = 'menu-scroll'; // key for accessing t29.prefs value |
---|
229 | |
---|
230 | // set initial state: |
---|
231 | initial = t29.prefs.get(t29.menu.scroll.store_key, t29.menu.scroll.States.STATIC); |
---|
232 | switch(initial) { |
---|
233 | case t29.menu.scroll.States.STATIC: |
---|
234 | t29.menu.scroll.set(initial); |
---|
235 | break; |
---|
236 | case t29.menu.scroll.States.FIX: |
---|
237 | // davon ausgehend, dass wir uns ganz oben befinden beim Seiten-Laden. |
---|
238 | // TODO / PENDING: Wenn man mit Anker #foobar auf die Seite reinkommt, |
---|
239 | // ist das nicht der Fall! Das kann kombiniert werden |
---|
240 | // mit t29.smoothscroll! |
---|
241 | t29.menu.scroll.set(t29.menu.scroll.States.STICK_TOP); |
---|
242 | break; |
---|
243 | default: |
---|
244 | log("t29.menu.scroll.setup: Invalid value "+initial+" for initial prefs"); |
---|
245 | } |
---|
246 | |
---|
247 | t29.menu.scroll.but.click(function(){ |
---|
248 | switch(t29.menu.scroll.state) { |
---|
249 | case t29.menu.scroll.States.STATIC: |
---|
250 | // zu Fix uebergehen, mit Animation. |
---|
251 | t29.menu.side.hide(); |
---|
252 | t29.menu.scroll.set(t29.menu.scroll.States.FIX); |
---|
253 | t29.menu.side.fadeIn(); |
---|
254 | t29.prefs.set(t29.menu.scroll.store_key, t29.menu.scroll.States.FIX); |
---|
255 | break; |
---|
256 | case t29.menu.scroll.States.FIX: |
---|
257 | case t29.menu.scroll.States.STICK_BOTTOM: |
---|
258 | // zu Static uebergehen, mit Animation. |
---|
259 | t29.menu.side.fadeOut(function(){ |
---|
260 | t29.menu.scroll.set(t29.menu.scroll.States.STATIC); |
---|
261 | }); |
---|
262 | t29.prefs.set(t29.menu.scroll.store_key, t29.menu.scroll.States.STATIC); |
---|
263 | break; |
---|
264 | case t29.menu.scroll.States.STICK_TOP: |
---|
265 | default: |
---|
266 | // diese Faelle sollten nicht vorkommen. |
---|
267 | log("Get-Menu Scroll-Button gedrückt obwohl unmöglich; state="+t29.menu.scroll.state); |
---|
268 | } |
---|
269 | }); // end event t29.menu.scroll.but.click. |
---|
270 | |
---|
271 | // nun ein paar Y-Koordinaten. berechnet mit dem Ausgangs-menu.side (STATIC). |
---|
272 | t29.menu.scroll.origin_top = t29.menu.side.offset().top; |
---|
273 | t29.menu.scroll.max_bottom = $("footer").offset().top - t29.menu.side.height(); |
---|
274 | t29.menu.scroll.stick_bottom = $("footer").offset().top - t29.menu.side.height() - $("#background-color-container").offset().top; |
---|
275 | t29.menu.scroll.button_max_bottom = $("footer").offset().top; |
---|
276 | //t29.menu.scroll.max_bottom - $("#background-color-container").offset().top; |
---|
277 | |
---|
278 | $(window).scroll(function(){ |
---|
279 | var y = $(this).scrollTop(); |
---|
280 | |
---|
281 | switch(t29.menu.scroll.state) { |
---|
282 | case t29.menu.scroll.States.STATIC: break; // System inaktiv. |
---|
283 | case t29.menu.scroll.States.FIX: |
---|
284 | if(y < t29.menu.scroll.origin_top) |
---|
285 | t29.menu.scroll.set(t29.menu.scroll.States.STICK_TOP); |
---|
286 | else if(y > t29.menu.scroll.max_bottom) |
---|
287 | t29.menu.scroll.set(t29.menu.scroll.States.STICK_BOTTOM); |
---|
288 | break; |
---|
289 | case t29.menu.scroll.States.STICK_TOP: |
---|
290 | if(y > t29.menu.scroll.origin_top) { |
---|
291 | // wir sind wieder weiter runter gescrollt. |
---|
292 | if(t29.menu.collapsed.u3.is_collapsed()) |
---|
293 | // und der Benutzer hat zwischenzeitlich nicht das Menue ausgeklappt |
---|
294 | t29.menu.scroll.set(t29.menu.scroll.States.FIX); |
---|
295 | else |
---|
296 | // der Benutzer hat zwischenzeitlich ausgeklappt. Schalte fixing aus. |
---|
297 | t29.menu.scroll.set(t29.menu.scroll.States.STATIC); |
---|
298 | } |
---|
299 | break; |
---|
300 | case t29.menu.scroll.States.STICK_BOTTOM: |
---|
301 | if(y < t29.menu.scroll.max_bottom) { |
---|
302 | // wir sind wieder weiter hoch gescrollt. Entcollapsen bieten wir ganz |
---|
303 | // unten nicht an. Ergo: Fixing wieder einschalten. |
---|
304 | t29.menu.scroll.set(t29.menu.scroll.States.FIX); |
---|
305 | } |
---|
306 | break; |
---|
307 | } |
---|
308 | |
---|
309 | // Sichtbarkeit des Fixed-Buttons am unteren Seitenrand |
---|
310 | // festlegen: |
---|
311 | if(y + $(window).height() > t29.menu.scroll.button_max_bottom) { |
---|
312 | $("html").addClass('button-stick-bottom'); |
---|
313 | } else if($("html").hasClass('button-stick-bottom')) { |
---|
314 | $("html").removeClass('button-stick-bottom'); |
---|
315 | } |
---|
316 | }); // end event window.scroll. |
---|
317 | }; // end t29.menu.scroll.setup. |
---|
318 | |
---|
319 | |
---|
320 | /*************************************************************************************** |
---|
321 | 2. Footer Guided Tour System t29.menu.guide |
---|
322 | |
---|
323 | The "beam" is a fancy jquery application where the menu is cloned and displayed |
---|
324 | in the footer in a very other way. This is quite static compared to the |
---|
325 | applications above. |
---|
326 | |
---|
327 | ***************************************************************************************/ |
---|
328 | t29.menu.guide.setup = function() { |
---|
329 | // Beam nur anzeigen wenn auf Seite, die auch in der Beamnavigation drin ist, |
---|
330 | // sprich Seitenleiste. |
---|
331 | if(!t29.menu.side.hasClass( t29.conf['seite_in_nav'] )) |
---|
332 | return false; |
---|
333 | |
---|
334 | // Zentraler Hauptschritt: Das Menue ab oberster Ebene klonen und im Footer dranhaengen, |
---|
335 | // ausserdem ein paar Ummodellierungen. |
---|
336 | g = t29.menu.beam; |
---|
337 | t29.menu.side.find(".u1").clone().appendTo(g); |
---|
338 | g.find("ul").show(); // durch t29.menu.collapse.setup wurden die .u3er auf hide gesetzt. Revert! |
---|
339 | g.find(".geraete").remove(); // geraete-Links nicht anzeigen |
---|
340 | g.find("ul.u2 > li > a").remove(); // Ueberschriften entfernen |
---|
341 | |
---|
342 | |
---|
343 | // Texte ersetzen durch laengere verstaendlichere Beschreibungen im title |
---|
344 | g.find("a[title]").each(function(){ |
---|
345 | $(this).text( $(this).attr('title') ).attr('title',''); // title attribut entfernen |
---|
346 | }); |
---|
347 | |
---|
348 | // Abkuerzungen und Wrappings |
---|
349 | a = g.find("a"); li = g.find("li"); |
---|
350 | a.wrapInner("<span class='text'/>").append("<span class='bullet'/>"); |
---|
351 | |
---|
352 | // Punkte aequidistant verteilen |
---|
353 | count = a.length; |
---|
354 | bwidth = $(".bullet",g).outerWidth(); |
---|
355 | each_width = (g.width() + bwidth*2) / count; |
---|
356 | each_width = g.width() / count; |
---|
357 | |
---|
358 | |
---|
359 | // problem: each_width = 16.384023... -> Math.floor liefert zu schmale Werte, direktes |
---|
360 | // a.width(each_width) hingegen kann mit Fliesskomma nicht umgehen. Daher jetzt ein Ansatz, |
---|
361 | // CSS3-Subpixelwerte mit ueberschaubar vielen Dezimalstellen anzuwenden. |
---|
362 | roundNumber = function(num,dec) { return Math.round(num*Math.pow(10,dec))/Math.pow(10,dec); }; |
---|
363 | subpixel_width = roundNumber(each_width, 2); |
---|
364 | a.css("width", subpixel_width+"px"); |
---|
365 | //a.css("width", Math.floor(each_width)+"px"); |
---|
366 | // text-Label zentriert darstellen um den Punkt |
---|
367 | $(".text", a).css("left", function(){return(bwidth - $(this).width())/2; }); |
---|
368 | |
---|
369 | default_visibles = g.find(".start, .end, .current"); |
---|
370 | default_visibles.addClass("visible"); //.find("a").css("z-index",0); |
---|
371 | default_visibles = default_visibles.find("a:first-child"); // von li auf a |
---|
372 | |
---|
373 | // Overlappings finden |
---|
374 | // left,right: Funktionen geben links/rechts-Offset des Objekts wieder |
---|
375 | left = function($o) { return $o.offset().left; } |
---|
376 | right = function($o) { return $o.offset().left + $o.width(); } |
---|
377 | edges = default_visibles.map(function(){ |
---|
378 | t = $(".text", this); |
---|
379 | return {'left': left(t), 'right': right(t) }; |
---|
380 | }); |
---|
381 | min_left = Math.min.apply(null, edges.map(function(){ return this.left })); |
---|
382 | max_right = Math.max.apply(null, edges.map(function(){ return this.right; })); |
---|
383 | a.not(default_visibles).each(function(){ |
---|
384 | t = $(".text", this); this_a = $(this); |
---|
385 | l = left(t); r = right(t); |
---|
386 | edges.each(function(i){ |
---|
387 | if((l < this.right && l > this.left) || // rechte kante drin |
---|
388 | (r > this.left && r < this.right) || // linke kante drin |
---|
389 | (l < this.right && l < min_left) || |
---|
390 | (r > this.left && r > max_right)) { |
---|
391 | this_a.addClass("higher-text"); |
---|
392 | } |
---|
393 | }); |
---|
394 | }); |
---|
395 | |
---|
396 | // Split position for relative navigation |
---|
397 | // 20px von nav.side margin left; 40px = 4*10px padding left von nav.rel a |
---|
398 | ///// 22.04.2012: Deaktiviert, weil anderes Design vor Augen. |
---|
399 | /* |
---|
400 | split = $(".current a",g).offset().left - g.offset().left + bwidth/2; |
---|
401 | rest = $("#content").outerWidth() - split - 40; |
---|
402 | t29.menu.rel.find(".prev a").width(split); |
---|
403 | t29.menu.rel.find(".next a").width(rest); |
---|
404 | */ |
---|
405 | }; |
---|