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