1 package Koha
::FrameworkPlugin
;
3 # Copyright 2014 Rijksmuseum
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 Koha::FrameworkPlugin - Facilitate use of plugins in MARC/items editor
26 use Koha::FrameworkPlugin;
27 my $plugin = Koha::FrameworkPlugin({ name => 'EXAMPLE.pl' });
28 $plugin->build( { id => $id });
30 javascript => $plugin->javascript,
31 noclick => $plugin->noclick,
34 use Koha::FrameworkPlugin;
35 my $plugin = Koha::FrameworkPlugin({ name => 'EXAMPLE.pl' });
36 $plugin->launch( { cgi => $query });
40 A framework plugin provides additional functionality to a MARC or item
41 field. It can be attached to a field in the framework structure.
42 The functionality is twofold:
43 - Additional actions on the field via javascript in the editor itself
44 via events as onfocus, onblur, etc.
45 Focus may e.g. fill an empty field, Blur or Change may validate.
46 - Provide an additional form to edit the field value, possibly a
47 combination of various subvalues. Look at e.g. MARC leader.
48 The additional form is a popup on top of the MARC/items editor.
50 The plugin code is a perl script (with template for the popup),
51 essentially doing two things:
52 1) Build: The plugin returns javascript to the caller (addbiblio.pl a.o.)
53 2) Launch: The plugin launches the additional form (popup). Launching is
54 centralized via the plugin_launcher.pl script.
56 This object support two code styles:
57 - In the new style, the plugin returns a hashref with a builder and a
58 launcher key pointing to two anynomous subroutines.
59 - In the old style, the builder is subroutine plugin_javascript and the
60 launcher is subroutine plugin. For each plugin the routines are
63 In cataloguing/value_builder/EXAMPLE.pl, you can find a detailed example
64 of a new style plugin. As long as we support the old style plugins, the
65 unit test t/db_dependent/FrameworkPlugin.t still contains an example
72 Create object (via Class::Accessor).
76 Build uses the builder subroutine of the plugin to build javascript
81 Run the popup of the plugin, as defined by the launcher subroutine.
87 Filename of the plugin.
91 Optional pathname of the plugin.
92 By default plugins are found in cataloguing/value_builder.
97 If set, the plugin will no longer build or launch.
101 Generated javascript for the caller of the plugin (after building).
105 Tells you (after building) that this plugin has no action connected to
106 to clicking on the buttonDot anchor. (Note that some item plugins
107 redirect click to focus instead of launching a popup.)
109 =head1 ADDITIONAL COMMENTS
115 use base
qw(Class::Accessor);
119 __PACKAGE__
->mk_ro_accessors( qw
|
120 name path errstr javascript noclick
125 Returns new object based on Class::Accessor, loads additional params.
126 The params hash currently supports keys: name, path, item_style.
127 Name is mandatory. Path is used in unit testing.
128 Item_style is used to identify old-style item plugins that still use
129 an additional (irrelevant) first parameter in the javascript event
135 my ( $class, $params ) = @_;
136 my $self = $class->SUPER::new
();
137 if( ref($params) eq 'HASH' ) {
138 foreach( 'name', 'path', 'item_style' ) {
139 $self->{$_} = $params->{$_};
142 elsif( !ref($params) && $params ) { # use it as plugin name
143 $self->{name
} = $params;
144 if( $params =~ /^(.*)\/([^\
/]+)$/ ) {
149 $self->_error( 'Plugin needs a name' ) if !$self->{name
};
155 Generate html and javascript by calling the builder sub of the plugin.
157 Params is a hashref supporting keys: id (=html id for the input field),
158 record (MARC record or undef), dbh (database handle), tagslib, tabloop.
159 Note that some of these parameters are not used in most (if not all)
160 plugins and may be obsoleted in the future (kept for now to provide
161 backward compatibility).
162 The most important one is id; it is used to construct unique javascript
165 Returns success or failure.
170 my ( $self, $params ) = @_;
171 return if $self->{errstr
};
172 return 1 if exists $self->{html
}; # no rebuild
174 $self->_load if !$self->{_loaded
};
175 return if $self->{errstr
}; # load had error
176 return $self->_generate_js( $params );
181 Launches the popup for this plugin by calling its launcher sub
182 Old style plugins still expect to receive a CGI oject, new style
183 plugins expect a params hashref.
184 Returns undef on failure, otherwise launcher return value (if any).
189 my ( $self, $params ) = @_;
190 return if $self->{errstr
};
192 $self->_load if !$self->{_loaded
};
193 return if $self->{errstr
}; # load had error
194 return 1 if !exists $self->{launcher
}; #just ignore this request
195 if( defined( &{$self->{launcher
}} ) ) {
196 my $arg= $self->{oldschool
}?
$params->{cgi
}: $params;
197 return &{$self->{launcher
}}( $arg );
199 return $self->_error( 'No launcher sub defined' );
202 # ************** INTERNAL ROUTINES ********************************************
205 my ( $self, $info ) = @_;
206 $self->{errstr
} = 'ERROR: Plugin '. ( $self->{name
}//'' ). ': '. $info;
207 return; #always return false
214 return $self->_error( 'Plugin needs a name' ) if !$self->{name
}; #2chk
215 $self->{path
} //= _valuebuilderpath
();
216 $file= $self->{path
}. '/'. $self->{name
};
217 return $self->_error( 'File not found' ) if !-e
$file;
219 # undefine oldschool subroutines before defining them again
220 undef &plugin_parameters
;
221 undef &plugin_javascript
;
225 return $self->_error( $@
) if $@
;
227 my $type = ref( $rv );
228 if( $type eq 'HASH' ) { # new style
229 $self->{oldschool
} = 0;
230 if( exists $rv->{builder
} && ref($rv->{builder
}) eq 'CODE' ) {
231 $self->{builder
} = $rv->{builder
};
232 } elsif( exists $rv->{builder
} ) {
233 return $self->_error( 'Builder sub is no coderef' );
235 if( exists $rv->{launcher
} && ref($rv->{launcher
}) eq 'CODE' ) {
236 $self->{launcher
} = $rv->{launcher
};
237 } elsif( exists $rv->{launcher
} ) {
238 return $self->_error( 'Launcher sub is no coderef' );
240 } else { # old school
241 $self->{oldschool
} = 1;
242 if( defined(&plugin_javascript
) ) {
243 $self->{builder
} = \
&plugin_javascript
;
245 if( defined(&plugin
) ) {
246 $self->{launcher
} = \
&plugin
;
249 if( !$self->{builder
} && !$self->{launcher
} ) {
250 return $self->_error( 'Plugin does not contain builder nor launcher' );
252 $self->{_loaded
} = $self->{oldschool
}?
0: 1;
253 # old style needs reload due to possible sub redefinition
257 sub _valuebuilderpath
{
258 return C4
::Context
->config('intranetdir') . "/cataloguing/value_builder";
259 #Formerly, intranetdir/cgi-bin was tested first.
260 #But the intranetdir from koha-conf already includes cgi-bin for
261 #package installs, single and standard installs.
265 my ( $self, $params ) = @_;
267 my $sub = $self->{builder
};
269 #it is safe to assume here that we do have a launcher
270 #we assume that it is launched in an unorthodox fashion
271 #just useless to build, but no problem
273 if( !defined(&$sub) ) { # 2chk: if there is something, it should be code
274 return $self->_error( 'Builder sub not defined' );
277 my @params = $self->{oldschool
}//0 ?
278 ( $params->{dbh
}, $params->{record
}, $params->{tagslib
},
279 $params->{id
}, $params->{tabloop
} ):
281 my @rv = &$sub( @params );
282 return $self->_error( 'Builder sub failed: ' . $@
) if $@
;
284 my $arg= $self->{oldschool
}?
pop @rv: shift @rv;
285 #oldschool returns functionname and script; we only use the latter
286 if( $arg && $arg=~/^\s*\<script/ ) {
287 $self->_process_javascript( $params, $arg );
288 return 1; #so far, so good
290 return $self->_error( 'Builder sub returned bad value(s)' );
293 sub _process_javascript
{
294 my ( $self, $params, $script ) = @_;
296 #remove the script tags; we add them again later
297 $script =~ s/\<script[^>]*\>\s*(\/\/\
<!\
[CDATA\
[)?\s
*//s;
298 $script =~ s/(\/\/\
]\
]\
>\s
*)?\
<\
/script\>//s
;
300 my $id = $params->{id
}//'';
303 my @events = qw
|click focus blur change mouseover mouseout mousedown
304 mouseup mousemove keydown keypress keyup
|;
305 foreach my $ev ( @events ) {
306 my $scan = $ev eq 'click' && $self->{oldschool
}?
'clic': $ev;
307 if( $script =~ /function\s+($scan\w+)\s*\(([^\)]*)\)/is ) {
308 my ( $bl, $sl ) = $self->_add_binding( $1, $2, $ev, $id );
311 $clickfound = 1 if $ev eq 'click';
314 if( !$clickfound ) { # make buttonDot do nothing
315 my ( $bl ) = $self->_add_binding( 'noclick', '', 'click', $id );
318 $self->{noclick
} = !$clickfound;
319 $self->{javascript
}= _merge_script
( $id, $script, $bind );
323 # adds some jQuery code for event binding:
324 # $bind contains lines for the actual event binding: .click, .focus, etc.
325 # $script contains function definitions (if needed)
326 my ( $self, $fname, $pars, $ev, $id ) = @_;
327 my ( $bind, $script );
328 my $ctl= $ev eq 'click'?
'buttonDot_'.$id: $id;
329 #click event applies to buttonDot
331 if( $pars =~ /^(e|ev|event)$/i ) { # new style event handler assumed
332 $bind= qq| \
$("#$ctl").$ev(\
{id
: '$id'\
}, $fname);\n|;
334 } elsif( $fname eq 'noclick' ) { # no click: return false, no scroll
335 $bind= qq| \
$("#$ctl").$ev(function
() { return false
; });\n|;
337 } else { # add real event handler calling the function found
338 $bind=qq| \
$("#$ctl").$ev(\
{id
: '$id'\
}, ${fname
}_handler
);\n|;
339 $script = $self->_add_handler( $ev, $fname );
341 return ( $bind, $script );
345 # adds a handler with event parameter
346 # event.data.id is passed to the plugin function in parameters
347 # for the click event we always return false to prevent scrolling
348 my ( $self, $ev, $fname ) = @_;
349 my $first= $self->_first_item_par( $ev );
350 my $prefix= $ev eq 'click'?
'': 'return ';
351 my $suffix= $ev eq 'click'?
"\n return false;": '';
353 function ${fname}_handler(event) {
354 $prefix$fname(${first}event.data.id);$suffix
359 sub _first_item_par
{
360 my ( $self, $event ) = @_;
361 # needed for backward compatibility
362 # js event functions in old style item plugins have an extra parameter
363 # BUT.. not for all events (exceptions provide employment :)
364 if( $self->{item_style
} && $self->{oldschool
} &&
365 $event=~/focus|blur|change/ ) {
372 # Combine script and event bindings, enclosed in script tags.
373 # The BindEvents function is added to easily repeat event binding;
374 # this is used in additem.js for dynamically created item blocks.
375 my ( $id, $script, $bind ) = @_;
376 chomp ($script, $bind);
380 function BindEvents$id() {
383 \$(document).ready(function() {
392 Marcel de Rooy, Rijksmuseum Amsterdam, The Netherlands