##// END OF EJS Templates
Responsive layout for mobile devices (#19097)....
Jean-Philippe Lang -
r14435:e680ae1aa31b
parent child
Show More
@@ -0,0 +1,83
1 // generic layout specific responsive stuff goes here
2
3 function openFlyout() {
4 $('html').addClass('flyout-is-active');
5 $('#wrapper2').on('click', function(e){
6 e.preventDefault();
7 e.stopPropagation();
8 closeFlyout();
9 });
10 }
11
12 function closeFlyout() {
13 $('html').removeClass('flyout-is-active');
14 $('#wrapper2').off('click');
15 }
16
17
18 function isMobile() {
19 return $('.js-flyout-menu-toggle-button').is(":visible");
20 }
21
22 function setupFlyout() {
23 var mobileInit = false,
24 desktopInit = false;
25
26 /* click handler for mobile menu toggle */
27 $('.js-flyout-menu-toggle-button').on('click', function(e) {
28 e.preventDefault();
29 e.stopPropagation();
30 if($('html').hasClass('flyout-is-active')) {
31 closeFlyout();
32 } else {
33 openFlyout();
34 }
35 });
36
37 /* bind resize handler */
38 $(window).resize(function() {
39 initMenu();
40 })
41
42 /* menu init function for dom detaching and appending on mobile / desktop view */
43 function initMenu() {
44
45 var _initMobileMenu = function() {
46 /* only init mobile menu, if it hasn't been done yet */
47 if(!mobileInit) {
48
49 $('#main-menu > ul').detach().appendTo('.js-project-menu');
50 $('#top-menu > ul').detach().appendTo('.js-general-menu');
51 $('#sidebar > *').detach().appendTo('.js-sidebar');
52 $('#account ul').detach().appendTo('.js-profile-menu');
53
54 mobileInit = true;
55 desktopInit = false;
56 }
57 }
58
59 var _initDesktopMenu = function() {
60 if(!desktopInit) {
61
62 $('.js-project-menu > ul').detach().appendTo('#main-menu');
63 $('.js-general-menu ul').detach().appendTo('#top-menu');
64 $('.js-sidebar > *').detach().appendTo('#sidebar');
65 $('.js-profile-menu ul').detach().appendTo('#account');
66
67 desktopInit = true;
68 mobileInit = false;
69 }
70 }
71
72 if(isMobile()) {
73 _initMobileMenu();
74 } else {
75 _initDesktopMenu();
76 }
77 }
78
79 // init menu on page load
80 initMenu();
81 }
82
83 $(document).ready(setupFlyout);
This diff has been collapsed as it changes many lines, (595 lines changed) Show them Hide them
@@ -0,0 +1,595
1 /*----------------------------------------*\
2 RESPONSIVE CSS
3 \*----------------------------------------*/
4
5
6 /*
7
8 CONTENTS
9
10 A) BASIC MOBILE RESETS
11 B) HEADER & TOP MENUS
12 C) MAIN CONTENT & SIDEBAR
13 D) TOGGLE BUTTON & FLYOUT MENU
14
15 */
16
17
18 /* Hide new elements (toggle button and flyout menu) above 900px */
19 .mobile-toggle-button,
20 .flyout-menu
21 {
22 display: none;
23 }
24
25 /*
26 redmine's body is set to min-width: 900px
27 add first breakpoint here and start adding responsiveness
28 */
29
30 @media all and (max-width: 899px)
31 {
32 /*----------------------------------------*\
33 A) BASIC MOBILE RESETS
34 \*----------------------------------------*/
35
36 /*
37 apply natural border box, see: http://www.paulirish.com/2012/box-sizing-border-box-ftw/
38 this helps us to better deal with percentages and padding / margin
39 */
40 *,
41 *:before,
42 *:after
43 {
44 -webkit-box-sizing: border-box;
45 -moz-box-sizing: border-box;
46 box-sizing: border-box;
47 }
48
49 body,
50 html
51 {
52 height: 100%;
53 margin: 0;
54 padding: 0;
55 }
56
57 html
58 {
59 overflow-y: auto; /* avoid 2nd scrollbar on desktop */
60 }
61
62 body
63 {
64 overflow-x: hidden; /* hide horizontal overflow */
65
66 min-width: 0; /* reset the min-width of 900px */
67 }
68
69
70 body,
71 input,
72 select,
73 textarea,
74 button
75 {
76 font-size: 14px; /* Set font-size for standard elements to 14px */
77 }
78
79
80 select
81 {
82 max-width: 100%; /* prevent long names within select menues from breaking content */
83 }
84
85
86 #wrapper
87 {
88 position: relative;
89
90 max-width: 100%;
91 }
92
93 #wrapper,
94 #wrapper2
95 {
96 margin: 0;
97 }
98
99 /*----------------------------------------*\
100 B) HEADER & TOP MENUS
101 \*----------------------------------------*/
102
103 #header
104 {
105 width: 100%;
106 height: 64px; /* the height of our header on mobile */
107 min-height: 0;
108 margin: 0;
109 padding: 0;
110
111 border: none;
112 background-color: #628db6;
113 }
114
115 /* Hide project name on mobile (project name is still visible in select menu) */
116 #header h1
117 {
118 display: none !important;
119 }
120
121 /* reset #header a color for mobile toggle button */
122 #header a.mobile-toggle-button
123 {
124 color: #f8f8f8;
125 }
126
127
128 /* Hide top-menu and main-menu on mobile, because it's placed in our flyout menu */
129 #top-menu,
130 #header #main-menu
131 {
132 display: none;
133 }
134
135 /* the quick search within header holding search form and #project_quick_jump_box box*/
136 #header #quick-search
137 {
138 float: none;
139 clear: none; /* there are themes which set clear property, this resets it */
140
141 max-width: 100%; /* reset max-width */
142 margin: 0;
143
144 background: inherit;
145 }
146
147 /* this represents the dropdown arrow to left of the mobile project menu */
148 #header .jump-box-arrow:before
149 {
150 /* set a font-size in order to achive same result in different themes */
151 font-family: Verdana, sans-serif;
152 font-size: 2em;
153 line-height: 64px;
154
155 position: absolute;
156 left: 0;
157
158 width: 2em;
159 padding: 0 .5em;
160 /* achieve dropdwon arrow by scaling a caret character */
161
162 content: '^';
163 -webkit-transform: scale(1,-.8);
164 -ms-transform: scale(1,-.8);
165 transform: scale(1,-.8);
166 text-align: right;
167 pointer-events: none;
168
169 opacity: .6;
170 }
171
172 /* styles for combobox within quick-search (#project_quick_jump_box) */
173 #header #quick-search select
174 {
175 font-size: 1.5em;
176 font-weight: bold;
177 line-height: 1.2;
178
179 position: absolute;
180 top: 15px;
181 left: 0;
182
183 float: left;
184
185 width: 100%;
186 max-width: 100%;
187 height: 2em;
188 height: 35px;
189 padding: 5px;
190 padding-right: 72px;
191 padding-left: 50px;
192
193 text-indent: .01px;
194
195 color: inherit;
196 border: 0;
197 -webkit-border-radius: 0;
198 border-radius: 0;
199 background: none;
200 -webkit-box-shadow: none;
201 box-shadow: none;
202 /* hide default browser arrow */
203
204 -webkit-appearance: none;
205 -moz-appearance: none;
206 }
207
208 #header #quick-search form
209 {
210 display: none;
211 }
212
213 /*----------------------------------------*\
214 C) MAIN CONTENT & SIDEBAR
215 \*----------------------------------------*/
216
217 #main
218 {
219 padding: 0;
220 }
221
222 #main.nosidebar #content,
223 div#content
224 {
225 width: 100%;
226 min-height: 0; /* reset min-height of #content */
227 margin: 0;
228 }
229
230
231 /* hide sidebar and sidebar switch panel, since it's placed in mobile flyout menu */
232 #sidebar,
233 #sidebar-switch-panel
234 {
235 display: none;
236 }
237
238 .splitcontentleft
239 {
240 width: 100%; /* use full width */
241 }
242
243 .splitcontentright
244 {
245 width: 100%; /* use full width */
246 }
247
248 /*----------------------------------------*\
249 D) TOGGLE BUTTON & FLYOUT MENU
250 \*----------------------------------------*/
251
252 /* Mobile toggle button */
253
254 .mobile-toggle-button
255 {
256 font-size: 42px;
257 line-height: 64px;
258
259 position: relative;
260 z-index: 10;
261
262 display: block; /* remove display: none; of non-mobile version */
263 float: right;
264
265 width: 60px;
266 height: 64px;
267 margin-top: 0;
268
269 text-align: center;
270
271 border-left: 1px solid #ddd;
272 }
273
274 .mobile-toggle-button:hover,
275 .mobile-toggle-button:active
276 {
277 text-decoration: none;
278 }
279
280 .mobile-toggle-button:after
281 {
282 font-family: Verdana, sans-serif;
283
284 display: block;
285
286 margin-top: -3px;
287
288 content: '\2261';
289 }
290
291 /* search magnifier icon */
292 .search-magnifier
293 {
294 font-family: Verdana;
295
296 cursor: pointer;
297 -webkit-transform: rotate(-45deg);
298 -moz-transform: rotate(45deg);
299 -o-transform: rotate(45deg);
300
301 color: #bbb;
302 }
303
304 .search-magnifier--flyout
305 {
306 font-size: 25px;
307 line-height: 54px;
308
309 position: absolute;
310 z-index: 1;
311 left: 12px;
312 }
313
314
315 /* Flyout Menu */
316
317 .flyout-menu
318 {
319 position: absolute;
320 right: -250px;
321
322 display: block; /* remove display: none; of non-mobile version */
323 overflow-x: hidden;
324
325 width: 250px;
326 height: 100%;
327 margin: 0; /* reset margin for themes that define it */
328 padding: 0; /* reset padding for themes that define it */
329
330 color: white;
331 background-color: #3e5b76;
332 }
333
334
335 /* avoid zoom on search input focus for ios devices */
336 .flyout-menu input[type='text']
337 {
338 font-size: 16px;
339 }
340
341 .flyout-menu h3
342 {
343 font-size: 11px;
344 line-height: 19px;
345
346 height: 20px;
347 margin: 0;
348 padding: 0;
349
350 letter-spacing: .1em;
351 text-transform: uppercase;
352
353 color: white;
354 border-top: 1px solid #506a83;
355 border-bottom: 1px solid #506a83;
356 background-color: #628db6;
357 }
358
359 .flyout-menu h4
360 {
361 color: white;
362 }
363
364 .flyout-menu h3,
365 .flyout-menu h4,
366 .flyout-menu > p,
367 .flyout-menu > a,
368 .flyout-menu ul li a,
369 .flyout-menu__search,
370 .flyout-menu__sidebar > div,
371 .flyout-menu__sidebar > p,
372 .flyout-menu__sidebar > a,
373 .flyout-menu__sidebar > form,
374 .flyout-menu > div,
375 .flyout-menu > form
376 {
377 padding-left: 8px;
378 }
379
380 .flyout-menu .flyout-menu__avatar
381 {
382 margin-top: -1px; /* move avatar up 1px */
383 padding-left: 0;
384 }
385
386 .flyout-menu__sidebar > form
387 {
388 display: block;
389 }
390
391 .flyout-menu__sidebar > form h3
392 {
393 margin-left: -8px;
394 }
395
396 .flyout-menu__sidebar > form label
397 {
398 display: inline-block;
399
400 margin: 8px 0;
401 }
402
403 .flyout-menu__sidebar > form br br
404 {
405 display: none;
406 }
407
408 .flyout-menu ul
409 {
410 margin: 0;
411 padding: 0;
412
413 list-style: none;
414 }
415
416 .flyout-menu ul li a
417 {
418 line-height: 40px;
419
420 display: block;
421 overflow: hidden;
422
423 height: 40px;
424
425 white-space: nowrap;
426 text-overflow: ellipsis;
427
428 border-top: 1px solid rgba(255,255,255,.1);
429 }
430
431 .flyout-menu ul li:first-child a
432 {
433 line-height: 39px;
434
435 height: 39px;
436
437 border-top: none;
438 }
439
440 .flyout-menu a
441 {
442 color: white;
443 }
444
445 .flyout-menu ul li a:hover
446 {
447 text-decoration: none;
448 }
449
450 .flyout-menu ul li a.new-object,
451 .new-object ~ .menu-children
452 {
453 display: none;
454 }
455
456 /* Left flyout search container */
457 .flyout-menu__search
458 {
459 line-height: 54px;
460
461 height: 64px;
462 padding-top: 3px;
463 padding-right: 8px;
464 }
465
466 .flyout-menu__search input[type='text']
467 {
468 line-height: 2;
469
470 width: 100%;
471 height: 38px;
472 padding-left: 27px;
473
474 vertical-align: middle;
475
476 border: none;
477 -webkit-border-radius: 3px;
478 border-radius: 3px;
479 background-color: #fff;
480 }
481
482 .flyout-menu__avatar
483 {
484 display: -webkit-box;
485 display: -webkit-flex;
486 display: -ms-flexbox;
487 display: flex;
488
489 width: 100%;
490
491 border-top: 1px solid rgba(255,255,255,.1);
492 }
493
494
495 .flyout-menu__avatar img.gravatar
496 {
497 width: 40px;
498 height: 40px;
499 padding: 0;
500
501 vertical-align: top;
502
503 border-width: 0;
504 }
505
506 .flyout-menu__avatar a
507 {
508 line-height: 40px;
509
510 height: auto;
511 height: 40px;
512
513 text-decoration: none;
514
515 color: white;
516 }
517
518 /* avatar */
519 .flyout-menu__avatar a:first-child
520 {
521 line-height: 0;
522
523 width: 40px;
524 padding: 0;
525 }
526
527 .flyout-menu__avatar .user
528 {
529 padding-left: 15px;
530 }
531
532 /* user link when no avatar is present */
533 .flyout-menu__avatar--no-avatar a.user
534 {
535 line-height: 40px;
536
537 padding-left: 8px;
538 }
539
540
541 .flyout-is-active body
542 {
543 overflow: hidden; /* for body not to have scrollbars when left flyout menu is active */
544 }
545
546 html.flyout-is-active
547 {
548 overflow: hidden;
549 }
550
551
552 .flyout-is-active #wrapper
553 {
554 right: 250px; /* when left flyout is active, move body to the right (same amount like flyout-menu's width) */
555
556 height: 100%;
557 }
558
559 .flyout-is-active .mobile-toggle-button:after
560 {
561 content: '\00D7'; /* close glyph */
562 }
563
564 .flyout-is-active #wrapper2
565 {
566
567 /*
568 * only relevant for devices with cursor when flyout it active, in order to show,
569 * that whole wrapper content is clickable and closes flyout menu
570 */
571 cursor: pointer;
572 }
573
574
575 #admin-menu
576 {
577 padding-left: 0;
578 }
579
580 #admin-menu li
581 {
582 padding-bottom: 0;
583 }
584
585 #admin-menu a,
586 #admin-menu a.selected
587 {
588 line-height: 40px;
589
590 padding: 0;
591 padding-left: 32px !important;
592
593 background-position: 8px 50%;
594 }
595 }
@@ -1,1337 +1,1338
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require 'forwardable'
21 21 require 'cgi'
22 22
23 23 module ApplicationHelper
24 24 include Redmine::WikiFormatting::Macros::Definitions
25 25 include Redmine::I18n
26 26 include GravatarHelper::PublicMethods
27 27 include Redmine::Pagination::Helper
28 28 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31
32 32 extend Forwardable
33 33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 #
42 42 # @param [String] name Anchor text (passed to link_to)
43 43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 44 # @param [optional, Hash] html_options Options passed to link_to
45 45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active? || (User.current.admin? && user.logged?)
55 55 link_to name, user_path(user), :class => user.css_classes
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 72 #
73 73 def link_to_issue(issue, options={})
74 74 title = nil
75 75 subject = nil
76 76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 77 if options[:subject] == false
78 78 title = issue.subject.truncate(60)
79 79 else
80 80 subject = issue.subject
81 81 if truncate_length = options[:truncate]
82 82 subject = subject.truncate(truncate_length)
83 83 end
84 84 end
85 85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 87 :class => issue.css_classes, :title => title)
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 100 html_options = options.slice!(:only_path)
101 101 options[:only_path] = true unless options.key?(:only_path)
102 102 url = send(route_method, attachment, attachment.filename, options)
103 103 link_to text, url, html_options
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision)),
119 119 :accesskey => options[:accesskey]
120 120 )
121 121 end
122 122
123 123 # Generates a link to a message
124 124 def link_to_message(message, options={}, html_options = nil)
125 125 link_to(
126 126 message.subject.truncate(60),
127 127 board_message_url(message.board_id, message.parent_id || message.id, {
128 128 :r => (message.parent_id && message.id),
129 129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 130 :only_path => true
131 131 }.merge(options)),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 142 #
143 143 def link_to_project(project, options={}, html_options = nil)
144 144 if project.archived?
145 145 h(project.name)
146 146 else
147 147 link_to project.name,
148 148 project_url(project, {:only_path => true}.merge(options)),
149 149 html_options
150 150 end
151 151 end
152 152
153 153 # Generates a link to a project settings if active
154 154 def link_to_project_settings(project, options={}, html_options=nil)
155 155 if project.active?
156 156 link_to project.name, settings_project_path(project, options), html_options
157 157 elsif project.archived?
158 158 h(project.name)
159 159 else
160 160 link_to project.name, project_path(project, options), html_options
161 161 end
162 162 end
163 163
164 164 # Generates a link to a version
165 165 def link_to_version(version, options = {})
166 166 return '' unless version && version.is_a?(Version)
167 167 options = {:title => format_date(version.effective_date)}.merge(options)
168 168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 169 end
170 170
171 171 # Helper that formats object for html or text rendering
172 172 def format_object(object, html=true, &block)
173 173 if block_given?
174 174 object = yield object
175 175 end
176 176 case object.class.name
177 177 when 'Array'
178 178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 179 when 'Time'
180 180 format_time(object)
181 181 when 'Date'
182 182 format_date(object)
183 183 when 'Fixnum'
184 184 object.to_s
185 185 when 'Float'
186 186 sprintf "%.2f", object
187 187 when 'User'
188 188 html ? link_to_user(object) : object.to_s
189 189 when 'Project'
190 190 html ? link_to_project(object) : object.to_s
191 191 when 'Version'
192 192 html ? link_to_version(object) : object.to_s
193 193 when 'TrueClass'
194 194 l(:general_text_Yes)
195 195 when 'FalseClass'
196 196 l(:general_text_No)
197 197 when 'Issue'
198 198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 199 when 'CustomValue', 'CustomFieldValue'
200 200 if object.custom_field
201 201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 202 if f.nil? || f.is_a?(String)
203 203 f
204 204 else
205 205 format_object(f, html, &block)
206 206 end
207 207 else
208 208 object.value.to_s
209 209 end
210 210 else
211 211 html ? h(object) : object.to_s
212 212 end
213 213 end
214 214
215 215 def wiki_page_path(page, options={})
216 216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 217 end
218 218
219 219 def thumbnail_tag(attachment)
220 220 link_to image_tag(thumbnail_path(attachment)),
221 221 named_attachment_path(attachment, attachment.filename),
222 222 :title => attachment.filename
223 223 end
224 224
225 225 def toggle_link(name, id, options={})
226 226 onclick = "$('##{id}').toggle(); "
227 227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 228 onclick << "return false;"
229 229 link_to(name, "#", :onclick => onclick)
230 230 end
231 231
232 232 def format_activity_title(text)
233 233 h(truncate_single_line_raw(text, 100))
234 234 end
235 235
236 236 def format_activity_day(date)
237 237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 238 end
239 239
240 240 def format_activity_description(text)
241 241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 243 end
244 244
245 245 def format_version_name(version)
246 246 if version.project == @project
247 247 h(version)
248 248 else
249 249 h("#{version.project} - #{version}")
250 250 end
251 251 end
252 252
253 253 def due_date_distance_in_words(date)
254 254 if date
255 255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 256 end
257 257 end
258 258
259 259 # Renders a tree of projects as a nested set of unordered lists
260 260 # The given collection may be a subset of the whole project tree
261 261 # (eg. some intermediate nodes are private and can not be seen)
262 262 def render_project_nested_lists(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 original_project = @project
267 267 projects.sort_by(&:lft).each do |project|
268 268 # set the project environment to please macros.
269 269 @project = project
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 classes = (ancestors.empty? ? 'root' : 'child')
281 281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 282 s << h(block_given? ? capture(project, &block) : project.name)
283 283 s << "</div>\n"
284 284 ancestors << project
285 285 end
286 286 s << ("</li></ul>\n" * ancestors.size)
287 287 @project = original_project
288 288 end
289 289 s.html_safe
290 290 end
291 291
292 292 def render_page_hierarchy(pages, node=nil, options={})
293 293 content = ''
294 294 if pages[node]
295 295 content << "<ul class=\"pages-hierarchy\">\n"
296 296 pages[node].each do |page|
297 297 content << "<li>"
298 298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 301 content << "</li>\n"
302 302 end
303 303 content << "</ul>\n"
304 304 end
305 305 content.html_safe
306 306 end
307 307
308 308 # Renders flash messages
309 309 def render_flash_messages
310 310 s = ''
311 311 flash.each do |k,v|
312 312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Renders tabs and their content
318 318 def render_tabs(tabs, selected=params[:tab])
319 319 if tabs.any?
320 320 unless tabs.detect {|tab| tab[:name] == selected}
321 321 selected = nil
322 322 end
323 323 selected ||= tabs.first[:name]
324 324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 325 else
326 326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 327 end
328 328 end
329 329
330 330 # Renders the project quick-jump box
331 331 def render_project_jump_box
332 332 return unless User.current.logged?
333 333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 334 if projects.any?
335 335 options =
336 336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 337 '<option value="" disabled="disabled">---</option>').html_safe
338 338
339 339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 341 end
342 342
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
343 344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 345 end
345 346 end
346 347
347 348 def project_tree_options_for_select(projects, options = {})
348 349 s = ''.html_safe
349 350 if blank_text = options[:include_blank]
350 351 if blank_text == true
351 352 blank_text = '&nbsp;'.html_safe
352 353 end
353 354 s << content_tag('option', blank_text, :value => '')
354 355 end
355 356 project_tree(projects) do |project, level|
356 357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 358 tag_options = {:value => project.id}
358 359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 360 tag_options[:selected] = 'selected'
360 361 else
361 362 tag_options[:selected] = nil
362 363 end
363 364 tag_options.merge!(yield(project)) if block_given?
364 365 s << content_tag('option', name_prefix + h(project), tag_options)
365 366 end
366 367 s.html_safe
367 368 end
368 369
369 370 # Yields the given block for each project with its level in the tree
370 371 #
371 372 # Wrapper for Project#project_tree
372 373 def project_tree(projects, &block)
373 374 Project.project_tree(projects, &block)
374 375 end
375 376
376 377 def principals_check_box_tags(name, principals)
377 378 s = ''
378 379 principals.each do |principal|
379 380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 381 end
381 382 s.html_safe
382 383 end
383 384
384 385 # Returns a string for users/groups option tags
385 386 def principals_options_for_select(collection, selected=nil)
386 387 s = ''
387 388 if collection.include?(User.current)
388 389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 390 end
390 391 groups = ''
391 392 collection.sort.each do |element|
392 393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 395 end
395 396 unless groups.empty?
396 397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 398 end
398 399 s.html_safe
399 400 end
400 401
401 402 def option_tag(name, text, value, selected=nil, options={})
402 403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 404 end
404 405
405 406 def truncate_single_line_raw(string, length)
406 407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 408 end
408 409
409 410 # Truncates at line break after 250 characters or options[:length]
410 411 def truncate_lines(string, options={})
411 412 length = options[:length] || 250
412 413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 414 "#{$1}..."
414 415 else
415 416 string
416 417 end
417 418 end
418 419
419 420 def anchor(text)
420 421 text.to_s.gsub(' ', '_')
421 422 end
422 423
423 424 def html_hours(text)
424 425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 426 end
426 427
427 428 def authoring(created, author, options={})
428 429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 430 end
430 431
431 432 def time_tag(time)
432 433 text = distance_of_time_in_words(Time.now, time)
433 434 if @project
434 435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 436 else
436 437 content_tag('abbr', text, :title => format_time(time))
437 438 end
438 439 end
439 440
440 441 def syntax_highlight_lines(name, content)
441 442 lines = []
442 443 syntax_highlight(name, content).each_line { |line| lines << line }
443 444 lines
444 445 end
445 446
446 447 def syntax_highlight(name, content)
447 448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 449 end
449 450
450 451 def to_path_param(path)
451 452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 453 str.blank? ? nil : str
453 454 end
454 455
455 456 def reorder_links(name, url, method = :post)
456 457 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
457 458 url.merge({"#{name}[move_to]" => 'highest'}),
458 459 :method => method, :title => l(:label_sort_highest)) +
459 460 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
460 461 url.merge({"#{name}[move_to]" => 'higher'}),
461 462 :method => method, :title => l(:label_sort_higher)) +
462 463 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
463 464 url.merge({"#{name}[move_to]" => 'lower'}),
464 465 :method => method, :title => l(:label_sort_lower)) +
465 466 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
466 467 url.merge({"#{name}[move_to]" => 'lowest'}),
467 468 :method => method, :title => l(:label_sort_lowest))
468 469 end
469 470
470 471 def breadcrumb(*args)
471 472 elements = args.flatten
472 473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
473 474 end
474 475
475 476 def other_formats_links(&block)
476 477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
477 478 yield Redmine::Views::OtherFormatsBuilder.new(self)
478 479 concat('</p>'.html_safe)
479 480 end
480 481
481 482 def page_header_title
482 483 if @project.nil? || @project.new_record?
483 484 h(Setting.app_title)
484 485 else
485 486 b = []
486 487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
487 488 if ancestors.any?
488 489 root = ancestors.shift
489 490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
490 491 if ancestors.size > 2
491 492 b << "\xe2\x80\xa6"
492 493 ancestors = ancestors[-2, 2]
493 494 end
494 495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
495 496 end
496 497 b << h(@project)
497 498 b.join(" \xc2\xbb ").html_safe
498 499 end
499 500 end
500 501
501 502 # Returns a h2 tag and sets the html title with the given arguments
502 503 def title(*args)
503 504 strings = args.map do |arg|
504 505 if arg.is_a?(Array) && arg.size >= 2
505 506 link_to(*arg)
506 507 else
507 508 h(arg.to_s)
508 509 end
509 510 end
510 511 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
511 512 content_tag('h2', strings.join(' &#187; ').html_safe)
512 513 end
513 514
514 515 # Sets the html title
515 516 # Returns the html title when called without arguments
516 517 # Current project name and app_title and automatically appended
517 518 # Exemples:
518 519 # html_title 'Foo', 'Bar'
519 520 # html_title # => 'Foo - Bar - My Project - Redmine'
520 521 def html_title(*args)
521 522 if args.empty?
522 523 title = @html_title || []
523 524 title << @project.name if @project
524 525 title << Setting.app_title unless Setting.app_title == title.last
525 526 title.reject(&:blank?).join(' - ')
526 527 else
527 528 @html_title ||= []
528 529 @html_title += args
529 530 end
530 531 end
531 532
532 533 # Returns the theme, controller name, and action as css classes for the
533 534 # HTML body.
534 535 def body_css_classes
535 536 css = []
536 537 if theme = Redmine::Themes.theme(Setting.ui_theme)
537 538 css << 'theme-' + theme.name
538 539 end
539 540
540 541 css << 'project-' + @project.identifier if @project && @project.identifier.present?
541 542 css << 'controller-' + controller_name
542 543 css << 'action-' + action_name
543 544 css.join(' ')
544 545 end
545 546
546 547 def accesskey(s)
547 548 @used_accesskeys ||= []
548 549 key = Redmine::AccessKeys.key_for(s)
549 550 return nil if @used_accesskeys.include?(key)
550 551 @used_accesskeys << key
551 552 key
552 553 end
553 554
554 555 # Formats text according to system settings.
555 556 # 2 ways to call this method:
556 557 # * with a String: textilizable(text, options)
557 558 # * with an object and one of its attribute: textilizable(issue, :description, options)
558 559 def textilizable(*args)
559 560 options = args.last.is_a?(Hash) ? args.pop : {}
560 561 case args.size
561 562 when 1
562 563 obj = options[:object]
563 564 text = args.shift
564 565 when 2
565 566 obj = args.shift
566 567 attr = args.shift
567 568 text = obj.send(attr).to_s
568 569 else
569 570 raise ArgumentError, 'invalid arguments to textilizable'
570 571 end
571 572 return '' if text.blank?
572 573 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
573 574 @only_path = only_path = options.delete(:only_path) == false ? false : true
574 575
575 576 text = text.dup
576 577 macros = catch_macros(text)
577 578 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
578 579
579 580 @parsed_headings = []
580 581 @heading_anchors = {}
581 582 @current_section = 0 if options[:edit_section_links]
582 583
583 584 parse_sections(text, project, obj, attr, only_path, options)
584 585 text = parse_non_pre_blocks(text, obj, macros) do |text|
585 586 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
586 587 send method_name, text, project, obj, attr, only_path, options
587 588 end
588 589 end
589 590 parse_headings(text, project, obj, attr, only_path, options)
590 591
591 592 if @parsed_headings.any?
592 593 replace_toc(text, @parsed_headings)
593 594 end
594 595
595 596 text.html_safe
596 597 end
597 598
598 599 def parse_non_pre_blocks(text, obj, macros)
599 600 s = StringScanner.new(text)
600 601 tags = []
601 602 parsed = ''
602 603 while !s.eos?
603 604 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
604 605 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
605 606 if tags.empty?
606 607 yield text
607 608 inject_macros(text, obj, macros) if macros.any?
608 609 else
609 610 inject_macros(text, obj, macros, false) if macros.any?
610 611 end
611 612 parsed << text
612 613 if tag
613 614 if closing
614 615 if tags.last && tags.last.casecmp(tag) == 0
615 616 tags.pop
616 617 end
617 618 else
618 619 tags << tag.downcase
619 620 end
620 621 parsed << full_tag
621 622 end
622 623 end
623 624 # Close any non closing tags
624 625 while tag = tags.pop
625 626 parsed << "</#{tag}>"
626 627 end
627 628 parsed
628 629 end
629 630
630 631 def parse_inline_attachments(text, project, obj, attr, only_path, options)
631 632 return if options[:inline_attachments] == false
632 633
633 634 # when using an image link, try to use an attachment, if possible
634 635 attachments = options[:attachments] || []
635 636 attachments += obj.attachments if obj.respond_to?(:attachments)
636 637 if attachments.present?
637 638 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638 639 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639 640 # search for the picture in attachments
640 641 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
641 642 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
642 643 desc = found.description.to_s.gsub('"', '')
643 644 if !desc.blank? && alttext.blank?
644 645 alt = " title=\"#{desc}\" alt=\"#{desc}\""
645 646 end
646 647 "src=\"#{image_url}\"#{alt}"
647 648 else
648 649 m
649 650 end
650 651 end
651 652 end
652 653 end
653 654
654 655 # Wiki links
655 656 #
656 657 # Examples:
657 658 # [[mypage]]
658 659 # [[mypage|mytext]]
659 660 # wiki links can refer other project wikis, using project name or identifier:
660 661 # [[project:]] -> wiki starting page
661 662 # [[project:|mytext]]
662 663 # [[project:mypage]]
663 664 # [[project:mypage|mytext]]
664 665 def parse_wiki_links(text, project, obj, attr, only_path, options)
665 666 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
666 667 link_project = project
667 668 esc, all, page, title = $1, $2, $3, $5
668 669 if esc.nil?
669 670 if page =~ /^([^\:]+)\:(.*)$/
670 671 identifier, page = $1, $2
671 672 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
672 673 title ||= identifier if page.blank?
673 674 end
674 675
675 676 if link_project && link_project.wiki
676 677 # extract anchor
677 678 anchor = nil
678 679 if page =~ /^(.+?)\#(.+)$/
679 680 page, anchor = $1, $2
680 681 end
681 682 anchor = sanitize_anchor_name(anchor) if anchor.present?
682 683 # check if page exists
683 684 wiki_page = link_project.wiki.find_page(page)
684 685 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
685 686 "##{anchor}"
686 687 else
687 688 case options[:wiki_links]
688 689 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689 690 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690 691 else
691 692 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692 693 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693 694 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
694 695 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695 696 end
696 697 end
697 698 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 699 else
699 700 # project or wiki doesn't exist
700 701 all
701 702 end
702 703 else
703 704 all
704 705 end
705 706 end
706 707 end
707 708
708 709 # Redmine links
709 710 #
710 711 # Examples:
711 712 # Issues:
712 713 # #52 -> Link to issue #52
713 714 # Changesets:
714 715 # r52 -> Link to revision 52
715 716 # commit:a85130f -> Link to scmid starting with a85130f
716 717 # Documents:
717 718 # document#17 -> Link to document with id 17
718 719 # document:Greetings -> Link to the document with title "Greetings"
719 720 # document:"Some document" -> Link to the document with title "Some document"
720 721 # Versions:
721 722 # version#3 -> Link to version with id 3
722 723 # version:1.0.0 -> Link to version named "1.0.0"
723 724 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
724 725 # Attachments:
725 726 # attachment:file.zip -> Link to the attachment of the current object named file.zip
726 727 # Source files:
727 728 # source:some/file -> Link to the file located at /some/file in the project's repository
728 729 # source:some/file@52 -> Link to the file's revision 52
729 730 # source:some/file#L120 -> Link to line 120 of the file
730 731 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
731 732 # export:some/file -> Force the download of the file
732 733 # Forum messages:
733 734 # message#1218 -> Link to message with id 1218
734 735 # Projects:
735 736 # project:someproject -> Link to project named "someproject"
736 737 # project#3 -> Link to project with id 3
737 738 #
738 739 # Links can refer other objects from other projects, using project identifier:
739 740 # identifier:r52
740 741 # identifier:document:"Some document"
741 742 # identifier:version:1.0.0
742 743 # identifier:source:some/file
743 744 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744 745 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
745 746 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
746 747 if tag_content
747 748 $&
748 749 else
749 750 link = nil
750 751 project = default_project
751 752 if project_identifier
752 753 project = Project.visible.find_by_identifier(project_identifier)
753 754 end
754 755 if esc.nil?
755 756 if prefix.nil? && sep == 'r'
756 757 if project
757 758 repository = nil
758 759 if repo_identifier
759 760 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
760 761 else
761 762 repository = project.repository
762 763 end
763 764 # project.changesets.visible raises an SQL error because of a double join on repositories
764 765 if repository &&
765 766 (changeset = Changeset.visible.
766 767 find_by_repository_id_and_revision(repository.id, identifier))
767 768 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
768 769 {:only_path => only_path, :controller => 'repositories',
769 770 :action => 'revision', :id => project,
770 771 :repository_id => repository.identifier_param,
771 772 :rev => changeset.revision},
772 773 :class => 'changeset',
773 774 :title => truncate_single_line_raw(changeset.comments, 100))
774 775 end
775 776 end
776 777 elsif sep == '#'
777 778 oid = identifier.to_i
778 779 case prefix
779 780 when nil
780 781 if oid.to_s == identifier &&
781 782 issue = Issue.visible.find_by_id(oid)
782 783 anchor = comment_id ? "note-#{comment_id}" : nil
783 784 link = link_to("##{oid}#{comment_suffix}",
784 785 issue_url(issue, :only_path => only_path, :anchor => anchor),
785 786 :class => issue.css_classes,
786 787 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
787 788 end
788 789 when 'document'
789 790 if document = Document.visible.find_by_id(oid)
790 791 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
791 792 end
792 793 when 'version'
793 794 if version = Version.visible.find_by_id(oid)
794 795 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
795 796 end
796 797 when 'message'
797 798 if message = Message.visible.find_by_id(oid)
798 799 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799 800 end
800 801 when 'forum'
801 802 if board = Board.visible.find_by_id(oid)
802 803 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
803 804 end
804 805 when 'news'
805 806 if news = News.visible.find_by_id(oid)
806 807 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
807 808 end
808 809 when 'project'
809 810 if p = Project.visible.find_by_id(oid)
810 811 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
811 812 end
812 813 end
813 814 elsif sep == ':'
814 815 # removes the double quotes if any
815 816 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
816 817 name = CGI.unescapeHTML(name)
817 818 case prefix
818 819 when 'document'
819 820 if project && document = project.documents.visible.find_by_title(name)
820 821 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
821 822 end
822 823 when 'version'
823 824 if project && version = project.versions.visible.find_by_name(name)
824 825 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
825 826 end
826 827 when 'forum'
827 828 if project && board = project.boards.visible.find_by_name(name)
828 829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 830 end
830 831 when 'news'
831 832 if project && news = project.news.visible.find_by_title(name)
832 833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 834 end
834 835 when 'commit', 'source', 'export'
835 836 if project
836 837 repository = nil
837 838 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
838 839 repo_prefix, repo_identifier, name = $1, $2, $3
839 840 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
840 841 else
841 842 repository = project.repository
842 843 end
843 844 if prefix == 'commit'
844 845 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
845 846 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
846 847 :class => 'changeset',
847 848 :title => truncate_single_line_raw(changeset.comments, 100)
848 849 end
849 850 else
850 851 if repository && User.current.allowed_to?(:browse_repository, project)
851 852 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
852 853 path, rev, anchor = $1, $3, $5
853 854 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
854 855 :path => to_path_param(path),
855 856 :rev => rev,
856 857 :anchor => anchor},
857 858 :class => (prefix == 'export' ? 'source download' : 'source')
858 859 end
859 860 end
860 861 repo_prefix = nil
861 862 end
862 863 when 'attachment'
863 864 attachments = options[:attachments] || []
864 865 attachments += obj.attachments if obj.respond_to?(:attachments)
865 866 if attachments && attachment = Attachment.latest_attach(attachments, name)
866 867 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
867 868 end
868 869 when 'project'
869 870 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
870 871 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
871 872 end
872 873 end
873 874 end
874 875 end
875 876 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
876 877 end
877 878 end
878 879 end
879 880
880 881 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
881 882
882 883 def parse_sections(text, project, obj, attr, only_path, options)
883 884 return unless options[:edit_section_links]
884 885 text.gsub!(HEADING_RE) do
885 886 heading = $1
886 887 @current_section += 1
887 888 if @current_section > 1
888 889 content_tag('div',
889 890 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
890 891 :class => 'contextual',
891 892 :title => l(:button_edit_section),
892 893 :id => "section-#{@current_section}") + heading.html_safe
893 894 else
894 895 heading
895 896 end
896 897 end
897 898 end
898 899
899 900 # Headings and TOC
900 901 # Adds ids and links to headings unless options[:headings] is set to false
901 902 def parse_headings(text, project, obj, attr, only_path, options)
902 903 return if options[:headings] == false
903 904
904 905 text.gsub!(HEADING_RE) do
905 906 level, attrs, content = $2.to_i, $3, $4
906 907 item = strip_tags(content).strip
907 908 anchor = sanitize_anchor_name(item)
908 909 # used for single-file wiki export
909 910 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
910 911 @heading_anchors[anchor] ||= 0
911 912 idx = (@heading_anchors[anchor] += 1)
912 913 if idx > 1
913 914 anchor = "#{anchor}-#{idx}"
914 915 end
915 916 @parsed_headings << [level, anchor, item]
916 917 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
917 918 end
918 919 end
919 920
920 921 MACROS_RE = /(
921 922 (!)? # escaping
922 923 (
923 924 \{\{ # opening tag
924 925 ([\w]+) # macro name
925 926 (\(([^\n\r]*?)\))? # optional arguments
926 927 ([\n\r].*?[\n\r])? # optional block of text
927 928 \}\} # closing tag
928 929 )
929 930 )/mx unless const_defined?(:MACROS_RE)
930 931
931 932 MACRO_SUB_RE = /(
932 933 \{\{
933 934 macro\((\d+)\)
934 935 \}\}
935 936 )/x unless const_defined?(:MACRO_SUB_RE)
936 937
937 938 # Extracts macros from text
938 939 def catch_macros(text)
939 940 macros = {}
940 941 text.gsub!(MACROS_RE) do
941 942 all, macro = $1, $4.downcase
942 943 if macro_exists?(macro) || all =~ MACRO_SUB_RE
943 944 index = macros.size
944 945 macros[index] = all
945 946 "{{macro(#{index})}}"
946 947 else
947 948 all
948 949 end
949 950 end
950 951 macros
951 952 end
952 953
953 954 # Executes and replaces macros in text
954 955 def inject_macros(text, obj, macros, execute=true)
955 956 text.gsub!(MACRO_SUB_RE) do
956 957 all, index = $1, $2.to_i
957 958 orig = macros.delete(index)
958 959 if execute && orig && orig =~ MACROS_RE
959 960 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
960 961 if esc.nil?
961 962 h(exec_macro(macro, obj, args, block) || all)
962 963 else
963 964 h(all)
964 965 end
965 966 elsif orig
966 967 h(orig)
967 968 else
968 969 h(all)
969 970 end
970 971 end
971 972 end
972 973
973 974 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
974 975
975 976 # Renders the TOC with given headings
976 977 def replace_toc(text, headings)
977 978 text.gsub!(TOC_RE) do
978 979 left_align, right_align = $2, $3
979 980 # Keep only the 4 first levels
980 981 headings = headings.select{|level, anchor, item| level <= 4}
981 982 if headings.empty?
982 983 ''
983 984 else
984 985 div_class = 'toc'
985 986 div_class << ' right' if right_align
986 987 div_class << ' left' if left_align
987 988 out = "<ul class=\"#{div_class}\"><li>"
988 989 root = headings.map(&:first).min
989 990 current = root
990 991 started = false
991 992 headings.each do |level, anchor, item|
992 993 if level > current
993 994 out << '<ul><li>' * (level - current)
994 995 elsif level < current
995 996 out << "</li></ul>\n" * (current - level) + "</li><li>"
996 997 elsif started
997 998 out << '</li><li>'
998 999 end
999 1000 out << "<a href=\"##{anchor}\">#{item}</a>"
1000 1001 current = level
1001 1002 started = true
1002 1003 end
1003 1004 out << '</li></ul>' * (current - root)
1004 1005 out << '</li></ul>'
1005 1006 end
1006 1007 end
1007 1008 end
1008 1009
1009 1010 # Same as Rails' simple_format helper without using paragraphs
1010 1011 def simple_format_without_paragraph(text)
1011 1012 text.to_s.
1012 1013 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1013 1014 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1014 1015 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1015 1016 html_safe
1016 1017 end
1017 1018
1018 1019 def lang_options_for_select(blank=true)
1019 1020 (blank ? [["(auto)", ""]] : []) + languages_options
1020 1021 end
1021 1022
1022 1023 def labelled_form_for(*args, &proc)
1023 1024 args << {} unless args.last.is_a?(Hash)
1024 1025 options = args.last
1025 1026 if args.first.is_a?(Symbol)
1026 1027 options.merge!(:as => args.shift)
1027 1028 end
1028 1029 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1029 1030 form_for(*args, &proc)
1030 1031 end
1031 1032
1032 1033 def labelled_fields_for(*args, &proc)
1033 1034 args << {} unless args.last.is_a?(Hash)
1034 1035 options = args.last
1035 1036 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1036 1037 fields_for(*args, &proc)
1037 1038 end
1038 1039
1039 1040 def error_messages_for(*objects)
1040 1041 html = ""
1041 1042 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1042 1043 errors = objects.map {|o| o.errors.full_messages}.flatten
1043 1044 if errors.any?
1044 1045 html << "<div id='errorExplanation'><ul>\n"
1045 1046 errors.each do |error|
1046 1047 html << "<li>#{h error}</li>\n"
1047 1048 end
1048 1049 html << "</ul></div>\n"
1049 1050 end
1050 1051 html.html_safe
1051 1052 end
1052 1053
1053 1054 def delete_link(url, options={})
1054 1055 options = {
1055 1056 :method => :delete,
1056 1057 :data => {:confirm => l(:text_are_you_sure)},
1057 1058 :class => 'icon icon-del'
1058 1059 }.merge(options)
1059 1060
1060 1061 link_to l(:button_delete), url, options
1061 1062 end
1062 1063
1063 1064 def preview_link(url, form, target='preview', options={})
1064 1065 content_tag 'a', l(:label_preview), {
1065 1066 :href => "#",
1066 1067 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1067 1068 :accesskey => accesskey(:preview)
1068 1069 }.merge(options)
1069 1070 end
1070 1071
1071 1072 def link_to_function(name, function, html_options={})
1072 1073 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1073 1074 end
1074 1075
1075 1076 # Helper to render JSON in views
1076 1077 def raw_json(arg)
1077 1078 arg.to_json.to_s.gsub('/', '\/').html_safe
1078 1079 end
1079 1080
1080 1081 def back_url
1081 1082 url = params[:back_url]
1082 1083 if url.nil? && referer = request.env['HTTP_REFERER']
1083 1084 url = CGI.unescape(referer.to_s)
1084 1085 end
1085 1086 url
1086 1087 end
1087 1088
1088 1089 def back_url_hidden_field_tag
1089 1090 url = back_url
1090 1091 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1091 1092 end
1092 1093
1093 1094 def check_all_links(form_name)
1094 1095 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1095 1096 " | ".html_safe +
1096 1097 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1097 1098 end
1098 1099
1099 1100 def toggle_checkboxes_link(selector)
1100 1101 link_to_function image_tag('toggle_check.png'),
1101 1102 "toggleCheckboxesBySelector('#{selector}')",
1102 1103 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1103 1104 end
1104 1105
1105 1106 def progress_bar(pcts, options={})
1106 1107 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1107 1108 pcts = pcts.collect(&:round)
1108 1109 pcts[1] = pcts[1] - pcts[0]
1109 1110 pcts << (100 - pcts[1] - pcts[0])
1110 1111 width = options[:width] || '100px;'
1111 1112 legend = options[:legend] || ''
1112 1113 content_tag('table',
1113 1114 content_tag('tr',
1114 1115 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1115 1116 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1116 1117 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1117 1118 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1118 1119 content_tag('p', legend, :class => 'percent').html_safe
1119 1120 end
1120 1121
1121 1122 def checked_image(checked=true)
1122 1123 if checked
1123 1124 @checked_image_tag ||= image_tag('toggle_check.png')
1124 1125 end
1125 1126 end
1126 1127
1127 1128 def context_menu(url)
1128 1129 unless @context_menu_included
1129 1130 content_for :header_tags do
1130 1131 javascript_include_tag('context_menu') +
1131 1132 stylesheet_link_tag('context_menu')
1132 1133 end
1133 1134 if l(:direction) == 'rtl'
1134 1135 content_for :header_tags do
1135 1136 stylesheet_link_tag('context_menu_rtl')
1136 1137 end
1137 1138 end
1138 1139 @context_menu_included = true
1139 1140 end
1140 1141 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1141 1142 end
1142 1143
1143 1144 def calendar_for(field_id)
1144 1145 include_calendar_headers_tags
1145 1146 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1146 1147 end
1147 1148
1148 1149 def include_calendar_headers_tags
1149 1150 unless @calendar_headers_tags_included
1150 1151 tags = ''.html_safe
1151 1152 @calendar_headers_tags_included = true
1152 1153 content_for :header_tags do
1153 1154 start_of_week = Setting.start_of_week
1154 1155 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1155 1156 # Redmine uses 1..7 (monday..sunday) in settings and locales
1156 1157 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1157 1158 start_of_week = start_of_week.to_i % 7
1158 1159 tags << javascript_tag(
1159 1160 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1160 1161 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1161 1162 path_to_image('/images/calendar.png') +
1162 1163 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1163 1164 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1164 1165 "beforeShow: beforeShowDatePicker};")
1165 1166 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1166 1167 unless jquery_locale == 'en'
1167 1168 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1168 1169 end
1169 1170 tags
1170 1171 end
1171 1172 end
1172 1173 end
1173 1174
1174 1175 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1175 1176 # Examples:
1176 1177 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1177 1178 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1178 1179 #
1179 1180 def stylesheet_link_tag(*sources)
1180 1181 options = sources.last.is_a?(Hash) ? sources.pop : {}
1181 1182 plugin = options.delete(:plugin)
1182 1183 sources = sources.map do |source|
1183 1184 if plugin
1184 1185 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1185 1186 elsif current_theme && current_theme.stylesheets.include?(source)
1186 1187 current_theme.stylesheet_path(source)
1187 1188 else
1188 1189 source
1189 1190 end
1190 1191 end
1191 1192 super *sources, options
1192 1193 end
1193 1194
1194 1195 # Overrides Rails' image_tag with themes and plugins support.
1195 1196 # Examples:
1196 1197 # image_tag('image.png') # => picks image.png from the current theme or defaults
1197 1198 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1198 1199 #
1199 1200 def image_tag(source, options={})
1200 1201 if plugin = options.delete(:plugin)
1201 1202 source = "/plugin_assets/#{plugin}/images/#{source}"
1202 1203 elsif current_theme && current_theme.images.include?(source)
1203 1204 source = current_theme.image_path(source)
1204 1205 end
1205 1206 super source, options
1206 1207 end
1207 1208
1208 1209 # Overrides Rails' javascript_include_tag with plugins support
1209 1210 # Examples:
1210 1211 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1211 1212 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1212 1213 #
1213 1214 def javascript_include_tag(*sources)
1214 1215 options = sources.last.is_a?(Hash) ? sources.pop : {}
1215 1216 if plugin = options.delete(:plugin)
1216 1217 sources = sources.map do |source|
1217 1218 if plugin
1218 1219 "/plugin_assets/#{plugin}/javascripts/#{source}"
1219 1220 else
1220 1221 source
1221 1222 end
1222 1223 end
1223 1224 end
1224 1225 super *sources, options
1225 1226 end
1226 1227
1227 1228 def sidebar_content?
1228 1229 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1229 1230 end
1230 1231
1231 1232 def view_layouts_base_sidebar_hook_response
1232 1233 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1233 1234 end
1234 1235
1235 1236 def email_delivery_enabled?
1236 1237 !!ActionMailer::Base.perform_deliveries
1237 1238 end
1238 1239
1239 1240 # Returns the avatar image tag for the given +user+ if avatars are enabled
1240 1241 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1241 1242 def avatar(user, options = { })
1242 1243 if Setting.gravatar_enabled?
1243 1244 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1244 1245 email = nil
1245 1246 if user.respond_to?(:mail)
1246 1247 email = user.mail
1247 1248 elsif user.to_s =~ %r{<(.+?)>}
1248 1249 email = $1
1249 1250 end
1250 1251 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1251 1252 else
1252 1253 ''
1253 1254 end
1254 1255 end
1255 1256
1256 1257 # Returns a link to edit user's avatar if avatars are enabled
1257 1258 def avatar_edit_link(user, options={})
1258 1259 if Setting.gravatar_enabled?
1259 1260 url = "https://gravatar.com"
1260 1261 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1261 1262 end
1262 1263 end
1263 1264
1264 1265 def sanitize_anchor_name(anchor)
1265 1266 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1266 1267 end
1267 1268
1268 1269 # Returns the javascript tags that are included in the html layout head
1269 1270 def javascript_heads
1270 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
1271 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1271 1272 unless User.current.pref.warn_on_leaving_unsaved == '0'
1272 1273 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1273 1274 end
1274 1275 tags
1275 1276 end
1276 1277
1277 1278 def favicon
1278 1279 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1279 1280 end
1280 1281
1281 1282 # Returns the path to the favicon
1282 1283 def favicon_path
1283 1284 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1284 1285 image_path(icon)
1285 1286 end
1286 1287
1287 1288 # Returns the full URL to the favicon
1288 1289 def favicon_url
1289 1290 # TODO: use #image_url introduced in Rails4
1290 1291 path = favicon_path
1291 1292 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1292 1293 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1293 1294 end
1294 1295
1295 1296 def robot_exclusion_tag
1296 1297 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1297 1298 end
1298 1299
1299 1300 # Returns true if arg is expected in the API response
1300 1301 def include_in_api_response?(arg)
1301 1302 unless @included_in_api_response
1302 1303 param = params[:include]
1303 1304 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1304 1305 @included_in_api_response.collect!(&:strip)
1305 1306 end
1306 1307 @included_in_api_response.include?(arg.to_s)
1307 1308 end
1308 1309
1309 1310 # Returns options or nil if nometa param or X-Redmine-Nometa header
1310 1311 # was set in the request
1311 1312 def api_meta(options)
1312 1313 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1313 1314 # compatibility mode for activeresource clients that raise
1314 1315 # an error when deserializing an array with attributes
1315 1316 nil
1316 1317 else
1317 1318 options
1318 1319 end
1319 1320 end
1320 1321
1321 1322 def generate_csv(&block)
1322 1323 decimal_separator = l(:general_csv_decimal_separator)
1323 1324 encoding = l(:general_csv_encoding)
1324 1325 end
1325 1326
1326 1327 private
1327 1328
1328 1329 def wiki_helper
1329 1330 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1330 1331 extend helper
1331 1332 return self
1332 1333 end
1333 1334
1334 1335 def link_to_content_update(text, url_params = {}, html_options = {})
1335 1336 link_to(text, url_params, html_options)
1336 1337 end
1337 1338 end
@@ -1,81 +1,123
1 1 <!DOCTYPE html>
2 2 <html lang="<%= current_language %>">
3 3 <head>
4 4 <meta charset="utf-8" />
5 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
6 6 <title><%= html_title %></title>
7 <meta name="viewport" content="width=device-width, initial-scale=1">
7 8 <meta name="description" content="<%= Redmine::Info.app_name %>" />
8 9 <meta name="keywords" content="issue,bug,tracker" />
9 10 <%= csrf_meta_tag %>
10 11 <%= favicon %>
11 <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', :media => 'all' %>
12 <%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', 'responsive', :media => 'all' %>
12 13 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
13 14 <%= javascript_heads %>
14 15 <%= heads_for_theme %>
15 16 <%= call_hook :view_layouts_base_html_head %>
16 17 <!-- page specific tags -->
17 18 <%= yield :header_tags -%>
18 19 </head>
19 20 <body class="<%= body_css_classes %>">
20 21 <div id="wrapper">
22
23 <div class="flyout-menu js-flyout-menu">
24
25
26 <% if User.current.logged? || !Setting.login_required? %>
27 <div class="flyout-menu__search">
28 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
29 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
30 <%= label_tag 'flyout-search', '&#9906;'.html_safe, :class => 'search-magnifier search-magnifier--flyout' %>
31 <%= text_field_tag 'q', @question, :id => 'flyout-search', :class => 'small js-search-input', :placeholder => l(:label_search) %>
32 <% end %>
33 </div>
34 <% end %>
35
36 <% if User.current.logged? %>
37 <div class="flyout-menu__avatar <% if !Setting.gravatar_enabled? %>flyout-menu__avatar--no-avatar<% end %>">
38 <% if Setting.gravatar_enabled? %>
39 <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %>
40 <% end %>
41 <%= link_to_user(User.current, :format => :username) %>
42 </div>
43 <% end %>
44
45 <% if display_main_menu?(@project) %>
46 <h3><%= l(:label_project) %></h3>
47 <span class="js-project-menu"></span>
48 <% end %>
49
50 <h3><%= l(:label_general) %></h3>
51 <span class="js-general-menu"></span>
52
53 <span class="js-sidebar flyout-menu__sidebar"></span>
54
55 <h3><%= l(:label_profile) %></h3>
56 <span class="js-profile-menu"></span>
57
58 </div>
59
21 60 <div id="wrapper2">
22 61 <div id="wrapper3">
23 62 <div id="top-menu">
24 63 <div id="account">
25 64 <%= render_menu :account_menu -%>
26 65 </div>
27 66 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
28 67 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
29 68 </div>
30 69
31 70 <div id="header">
71
72 <a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
73
32 74 <% if User.current.logged? || !Setting.login_required? %>
33 75 <div id="quick-search">
34 76 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
35 77 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
36 78 <label for='q'>
37 79 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
38 80 </label>
39 81 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
40 82 <% end %>
41 83 <%= render_project_jump_box %>
42 84 </div>
43 85 <% end %>
44 86
45 87 <h1><%= page_header_title %></h1>
46 88
47 89 <% if display_main_menu?(@project) %>
48 90 <div id="main-menu">
49 91 <%= render_main_menu(@project) %>
50 92 </div>
51 93 <% end %>
52 94 </div>
53 95
54 96 <div id="main" class="<%= sidebar_content? ? '' : 'nosidebar' %>">
55 97 <div id="sidebar">
56 98 <%= yield :sidebar %>
57 99 <%= view_layouts_base_sidebar_hook_response %>
58 100 </div>
59 101
60 102 <div id="content">
61 103 <%= render_flash_messages %>
62 104 <%= yield %>
63 105 <%= call_hook :view_layouts_base_content %>
64 106 <div style="clear:both;"></div>
65 107 </div>
66 108 </div>
67 109 </div>
68 110
69 111 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
70 112 <div id="ajax-modal" style="display:none;"></div>
71 113
72 114 <div id="footer">
73 115 <div class="bgl"><div class="bgr">
74 116 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2015 Jean-Philippe Lang
75 117 </div></div>
76 118 </div>
77 119 </div>
78 120 </div>
79 121 <%= call_hook :view_layouts_base_body_bottom %>
80 122 </body>
81 123 </html>
General Comments 0
You need to be logged in to leave comments. Login now