a1df9ebefc0b65f59f1374d2bec5592fa92858e1
[kivitendo-erp.git] / SL / Layout / Base.pm
1 package SL::Layout::Base;
2
3 use strict;
4 use parent qw(Rose::Object);
5
6 use File::Slurp qw(read_file);
7 use List::MoreUtils qw(uniq);
8 use Time::HiRes qw();
9
10 use Rose::Object::MakeMethods::Generic (
11   'scalar --get_set_init' => [ qw(menu auto_reload_resources_param sub_layouts_by_name) ],
12   'scalar'                => qw(focus),
13   'array'                 => [
14     'add_stylesheets_inline' => { interface => 'add', hash_key => 'stylesheets_inline' },
15     'add_javascripts_inline' => { interface => 'add', hash_key => 'javascripts_inline' },
16     'sub_layouts',           => { interface => 'get_set_init' },
17     'add_sub_layouts'        => { interface => 'add', hash_key => 'sub_layouts' },
18   ],
19 );
20
21 use SL::Menu;
22 use SL::Presenter;
23 use SL::System::Process;
24
25 my %menu_cache;
26
27 sub new {
28   my ($class, @slurp) = @_;
29
30   my $self = $class->SUPER::new(@slurp);
31 }
32
33 sub init_menu {
34   SL::Menu->new('user');
35 }
36
37 sub init_sublayouts_by_name {
38   {}
39 }
40
41 sub get {
42   $_[0]->sub_layouts;
43   return grep { $_ } ($_[0]->sub_layouts_by_name->{$_[1]});
44 }
45
46 sub init_auto_reload_resources_param {
47   if ($::lx_office_conf{debug}->{auto_reload_resources}) {
48     return sprintf('?rand=%d-%d-%d', Time::HiRes::gettimeofday(), int(rand 1000000000000));
49   }
50
51   if ($::lx_office_conf{debug}{git_commit_reload_resources}) {
52     my $git_dir = SL::System::Process::exe_dir() . '/.git';
53
54     return '' unless -d $git_dir;
55
56     my $content = eval { scalar(read_file($git_dir . '/HEAD')) };
57
58     return '' unless ($content // '') =~ m{\Aref: ([^\r\n]+)};
59
60     $content = eval { scalar(read_file($git_dir . '/' . $1)) };
61
62     return '' unless ($content // '') =~ m{\A([0-9a-fA-F]+)};
63
64     return '?rand=' . $1;
65   }
66
67   return '';
68 }
69
70 ##########################################
71 #  inheritable/overridable
72 ##########################################
73
74 sub pre_content {
75   join '', map { $_->pre_content } $_[0]->sub_layouts;
76 }
77
78 sub start_content {
79   join '', map { $_->start_content } $_[0]->sub_layouts;
80 }
81
82 sub end_content {
83   join '', map { $_->end_content } $_[0]->sub_layouts;
84 }
85
86 sub post_content {
87   join '', map { $_->post_content } $_[0]->sub_layouts;
88 }
89
90 sub stylesheets_inline {
91   uniq ( map { $_->stylesheets_inline } $_[0]->sub_layouts ),
92   @{ $_[0]->{stylesheets_inline} || [] };
93 }
94
95 sub javascripts_inline {
96   uniq ( map { $_->javascripts_inline } $_[0]->sub_layouts ),
97   @{ $_[0]->{javascripts_inline} || [] };
98 }
99
100 sub init_sub_layouts { [] }
101
102 sub init_sub_layouts_by_name { +{} }
103
104
105 #########################################
106 # Stylesheets
107 ########################################
108
109 # override in sub layouts
110 sub static_stylesheets {}
111
112 sub add_stylesheets {
113   &use_stylesheet;
114 }
115
116 sub use_stylesheet {
117   my $self = shift;
118   push @{ $self->{stylesheets} ||= [] }, @_ if @_;
119     (map { $_->use_stylesheet } $self->sub_layouts), $self->static_stylesheets, @{ $self->{stylesheets} ||= [] };
120 }
121
122 sub stylesheets {
123   my ($self) = @_;
124   my $css_path = $self->get_stylesheet_for_user;
125
126   return uniq grep { $_ } map { $self->_find_stylesheet($_, $css_path)  }
127     $self->use_stylesheet;
128 }
129
130 sub _find_stylesheet {
131   my ($self, $stylesheet, $css_path) = @_;
132
133   return "$css_path/$stylesheet" if -f "$css_path/$stylesheet";
134   return "css/$stylesheet"       if -f "css/$stylesheet";
135   return $stylesheet             if -f $stylesheet;
136   return $stylesheet             if $stylesheet =~ /^http/; # external
137 }
138
139 sub get_stylesheet_for_user {
140   my $css_path = 'css';
141   if (my $user_style = $::myconfig{stylesheet}) {
142     $user_style =~ s/\.css$//; # nuke trailing .css, this is a remnant of pre 2.7.0 stylesheet handling
143     if (-d "$css_path/$user_style" &&
144         -f "$css_path/$user_style/main.css") {
145       $css_path = "$css_path/$user_style";
146     } else {
147       $css_path = "$css_path/kivitendo";
148     }
149   } else {
150     $css_path = "$css_path/kivitendo";
151   }
152
153   return $css_path;
154 }
155
156 #########################################
157 # Javascripts
158 ########################################
159
160 # override in sub layouts
161 sub static_javascripts {}
162
163 sub add_javascripts {
164   &use_javascript
165 }
166
167 sub use_javascript {
168   my $self = shift;
169   push @{ $self->{javascripts} ||= [] }, @_ if @_;
170   map({ $_->use_javascript } $self->sub_layouts), $self->static_javascripts, @{ $self->{javascripts} ||= [] };
171 }
172
173 sub javascripts {
174   my ($self) = @_;
175
176   return uniq grep { $_ } map { $self->_find_javascript($_)  }
177      $self->use_javascript;
178 }
179
180 sub _find_javascript {
181   my ($self, $javascript) = @_;
182
183   return "js/$javascript"        if -f "js/$javascript";
184   return $javascript             if -f $javascript;
185   return $javascript             if $javascript =~ /^http/;
186 }
187
188
189 ############################################
190 # track state of form header
191 ############################################
192
193 sub header_done {
194   $_[0]{_header_done} = 1;
195 }
196
197 sub need_footer {
198   $_[0]{_header_done};
199 }
200
201 sub presenter {
202   SL::Presenter->get;
203 }
204
205 1;
206
207 __END__
208
209 =encoding utf-8
210
211 =head1 NAME
212
213 SL::Layout::Base - Base class for layouts
214
215 =head1 SYNOPSIS
216
217   package SL::Layout::MyLayout;
218
219   use parent qw(SL::Layout::Base);
220
221 =head1 DESCRIPTION
222
223 For a description of the external interface of layouts in general see
224 L<SL::Layout::Dispatcher>.
225
226 This is a base class for layouts in general. It provides the basic interface
227 and some capabilities to extend and cascade layouts.
228
229
230 =head1 IMPLEMENTING LAYOUT CALLBACKS
231
232 There are eight callbacks (C<pre_content>, C<post_content>, C<start_content>,
233 C<end_content>, C<stylesheets>, C<stylesheets_inline>, C<javascripts>,
234 C<javascripts_inline>) which are documented in L<SL::Layout::Dispatcher>. If
235 you are writing a new simple layout, you can just override some of them like
236 this:
237
238   package SL::Layout::MyEvilLayout;
239
240   sub pre_content {
241     '<h1>This is MY page now</h1>'
242   }
243
244   sub post_content {
245     '<p align="right"><small><em>Brought to you by my own layout class</em></small></p>'
246   }
247
248
249 To preserve the sanitizing effects of C<stylesheets> and C<javascripts> you should instead do the following:
250
251   sub stylesheets {
252     $_[0]->add_stylesheets(qw(mystyle1.css mystyle2.css);
253     $_[0]->SUPER::stylesheets;
254   }
255
256 If you want to add something to a different layout, you should write a sub
257 layout and add it to the other layouts.
258
259
260 =head1 SUB LAYOUTS
261
262 Layouts can be aggregated, so that common elements can be used in different
263 layouts. Currently this is used for the L<None|SL::Layout::None> sub layout,
264 which contains a lot of the stylesheets and javascripts necessary. Another
265 example is the L<Top|SL::Layout::Top> layout, which is used to generate a
266 common top bar for all menu types.
267
268 To add a sub layout to your layout just overwrite the sub_layout method:
269
270   package SL::Layout::MyFinalLayout;
271
272   sub init_sub_layout {
273     [
274       SL::Layout::None->new,
275       SL::Layout::MyEvilLayout->new,
276     ]
277   }
278
279 You can also add a sublayout at runtime:
280
281   $layout->add_sub_layout(SL::Layout::SideBar->new);
282
283 The standard implementation for the callbacks will see to it that the contents
284 of all sub layouts will get rendered.
285
286
287 =head1 COMBINING SUB LAYOUTS AND OWN BEHAVIOUR
288
289 This is still somewhat rough, and improvements are welcome.
290
291 For the C<*_content> callbacks this works if you just remember to dispatch to the base method:
292
293   sub post_content {
294     return $_[0]->render_status_bar .
295     $_[0]->SUPER::post_content
296   }
297
298
299 Stylesheets and Javascripts can be added to every layout and sub-layout at
300 runtime with L<SL::Layout::Dispatcher/add_stylesheets> and
301 L<SL::Layout::Dispatcher/add_javascripts> (C<use_stylesheets> and
302 C<use_javascripts> are aliases for backwards compatibility):
303
304   $layout->add_stylesheets("custom.css");
305   $layout->add_javascripts("app.js", "widget.js");
306
307 Or they can be overwritten in sub layouts with the calls
308 L<SL::Layout::Displatcher/static_stylesheets> and
309 L<SL::Layout::Dispatcher/static_javascripts>:
310
311   sub static_stylesheets {
312     "custom.css"
313   }
314
315   sub static_javascripts {
316     qw(app.css widget.js)
317   }
318
319 Note how these are relative to the base dirs of the currently selected
320 stylesheets. Javascripts are resolved relative to the C<js/> basedir.
321
322 Setting directly with C<stylesheets> and C<javascripts> is eprecated.
323
324
325 =head1 GORY DETAILS ABOUT JAVASCRIPT AND STYLESHEET OVERLOADING
326
327 The original code used to store one stylesheet in C<< $form->{stylesheet} >> and
328 allowed/expected authors of potential C<bin/mozilla/> controllers to change
329 that into their own modified stylesheet.
330
331 This was at some point cleaned up into a method C<use stylesheet> which took a
332 string of space separated stylesheets and processed them into the response.
333
334 A lot of controllers are still using this method so the layout interface
335 supports it to change as little controller code as possible, while providing the
336 more intuitive C<add_stylesheets> method.
337
338 At the same time the following things need to be possible:
339
340 =over 4
341
342 =item 1.
343
344 Runtime additions.
345
346   $layout->add_stylesheets(...)
347
348 Since add_stylesheets adds to C<< $self->{stylesheets} >> there must be a way to read
349 from it. Currently this is the deprecated C<use_stylesheet>.
350
351 =item 2.
352
353 Overriding Callbacks
354
355 A leaf layout should be able to override a callback to return a list.
356
357 =item 3.
358
359 Sanitizing
360
361 C<stylesheets> needs to retain its sanitizing behaviour.
362
363 =item 4.
364
365 Aggregation
366
367 The standard implementation should be able to collect from sub layouts.
368
369 =item 5.
370
371 Preserving Inclusion Order
372
373 Since there is currently no standard way of mixing own content and including
374 sub layouts, this has to be done manually. Certain things like jquery get added
375 in L<SL::Layout::None> so that they get rendered first.
376
377 =back
378
379 The current implementation provides no good candidate for overriding in sub
380 classes, which should be changed. The other points work pretty well.
381
382 =head1 BUGS
383
384 * stylesheet/javascript interface is a horrible mess.
385
386 * It's currently not possible to do compositor layouts without assupmtions
387 about the position of the content. That's because the content will return
388 control to the actual controller, so the layouts need to know where to split
389 pre- and post-content.
390
391 =head1 AUTHOR
392
393 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
394
395 =cut