View | Details | Raw Unified | Return to bug 11559
Collapse All | Expand All

(-)a/Koha/MetaSearcher.pm (+351 lines)
Line 0 Link Here
1
package Koha::MetaSearcher;
2
3
# Copyright 2014 ByWater Solutions
4
#
5
# This file is part of Koha.
6
#
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.
11
#
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.
16
#
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>.
19
20
use Modern::Perl;
21
22
use base 'Class::Accessor';
23
24
use C4::Charset qw( MarcToUTF8Record );
25
use C4::Search qw(); # Purely for new_record_from_zebra
26
use DBIx::Class::ResultClass::HashRefInflator;
27
use IO::Select;
28
use Koha::Cache;
29
use Koha::Database;
30
use Koha::MetadataRecord;
31
use MARC::File::XML;
32
use Storable qw( store_fd fd_retrieve );
33
use Time::HiRes qw( clock_gettime CLOCK_MONOTONIC );
34
use UUID;
35
use ZOOM;
36
37
use sort 'stable';
38
39
__PACKAGE__->mk_accessors( qw( fetch offset on_error resultset ) );
40
41
sub new {
42
    my ( $class, $options ) = @_;
43
44
    my ( $uuid, $uuidstring );
45
    UUID::generate($uuid);
46
    UUID::unparse( $uuid, $uuidstring );
47
48
    return bless {
49
        offset => 0,
50
        fetch => 100,
51
        on_error => sub {},
52
        results => [],
53
        resultset => $uuidstring,
54
        %{ $options || {} }
55
    }, $class;
56
}
57
58
sub handle_hit {
59
    my ( $self, $index, $server, $marcrecord ) = @_;
60
61
    my $record = Koha::MetadataRecord->new( { schema => 'marc', record => $marcrecord } );
62
63
    my %fetch = (
64
        title => 'biblio.title',
65
        seriestitle => 'biblio.seriestitle',
66
        author => 'biblio.author',
67
        isbn =>'biblioitems.isbn',
68
        issn =>'biblioitems.issn',
69
        lccn =>'biblioitems.lccn', #LC control number (not call number)
70
        edition =>'biblioitems.editionstatement',
71
        date => 'biblio.copyrightdate', #MARC21
72
        date2 => 'biblioitems.publicationyear', #UNIMARC
73
    );
74
75
    my $metadata = {};
76
    while ( my ( $key, $kohafield ) = each %fetch ) {
77
        $metadata->{$key} = $record->getKohaField($kohafield);
78
    }
79
    $metadata->{date} //= $metadata->{date2};
80
81
    push @{ $self->{results} }, {
82
        server => $server,
83
        index => $index,
84
        record => $marcrecord,
85
        metadata => $metadata,
86
    };
87
}
88
89
sub search {
90
    my ( $self, $server_ids, $query ) = @_;
91
92
    my $resultset_expiry = 300;
93
94
    my $cache;
95
    eval { $cache = Koha::Cache->new(); };
96
    my $schema = Koha::Database->new->schema;
97
    my $stats = {
98
        num_fetched => {
99
            map { $_ => 0 } @$server_ids
100
        },
101
        num_hits => {
102
            map { $_ => 0 } @$server_ids
103
        },
104
        total_fetched => 0,
105
        total_hits => 0,
106
    };
107
    my $start = clock_gettime( CLOCK_MONOTONIC );
108
    my $select = IO::Select->new;
109
    my @worker_fhs;
110
111
    my @cached_sets;
112
    my @servers;
113
114
    foreach my $server_id ( @$server_ids ) {
115
        if ( $server_id =~ /^\d+$/ ) {
116
            # Z39.50 server
117
            my $server = $schema->resultset('Z3950server')->find(
118
                { id => $server_id },
119
                { result_class => 'DBIx::Class::ResultClass::HashRefInflator' },
120
            );
121
            $server->{type} = 'z3950';
122
123
            push @servers, $server;
124
        } elsif ( $server_id =~ /(\w+)(?::(\w+))?/ ) {
125
            # Special server
126
            push @servers, {
127
                type => $1,
128
                extra => $2,
129
                id => $server_id,
130
                host => $server_id,
131
                name => $server_id,
132
            };
133
        }
134
    }
135
136
    # HashRefInflator is used so that the information will survive into the fork
137
    foreach my $server ( @servers ) {
138
        if ( $cache ) {
139
            my $set = $cache->get_from_cache( 'z3950-resultset-' . $self->resultset . '-' . $server->{id} );
140
            if ( ref($set) eq 'HASH' ) {
141
                $set->{server} = $server;
142
                push @cached_sets, $set;
143
                next;
144
            }
145
        }
146
147
        $select->add( $self->_start_worker( $server, $query ) );
148
    }
149
150
    # Handle these while the servers are searching
151
    foreach my $set ( @cached_sets ) {
152
        $self->_handle_hits( $stats, $set );
153
    }
154
155
    while ( $select->count ) {
156
        foreach my $readfh ( $select->can_read() ) {
157
            my $result = fd_retrieve( $readfh );
158
159
            $select->remove( $readfh );
160
            close $readfh;
161
            wait;
162
163
            next if ( ref $result ne 'HASH' );
164
165
            if ( $result->{error} ) {
166
                $self->{on_error}->( $result->{server}, $result->{error} );
167
                next;
168
            }
169
170
            $self->_handle_hits( $stats, $result );
171
172
            if ( $cache ) {
173
                $cache->set_in_cache( 'z3950-resultset-' . $self->resultset . '-' . $result->{server}->{id}, {
174
                    hits => $result->{hits},
175
                    num_fetched => $result->{num_fetched},
176
                    num_hits => $result->{num_hits},
177
                }, $resultset_expiry );
178
            }
179
        }
180
    }
181
182
    $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start;
183
184
    return $stats;
185
}
186
187
sub _start_worker {
188
    my ( $self, $server, $query ) = @_;
189
    pipe my $readfh, my $writefh;
190
191
    # Accessing the cache or Koha database after the fork is risky, so get any resources we need
192
    # here.
193
    my $pid;
194
    my $marcflavour = C4::Context->preference('marcflavour');
195
196
    if ( ( $pid = fork ) ) {
197
        # Parent process
198
        close $writefh;
199
200
        return $readfh;
201
    } elsif ( !defined $pid ) {
202
        # Error
203
204
        $self->{on_error}->( $server, 'Failed to fork' );
205
        return;
206
    }
207
208
    close $readfh;
209
    my $connection;
210
    my ( $num_hits, $num_fetched, $hits, $results );
211
212
    eval {
213
        if ( $server->{type} eq 'z3950' ) {
214
            my $zoptions = ZOOM::Options->new();
215
            $zoptions->option( 'elementSetName', 'F' );
216
            $zoptions->option( 'databaseName',   $server->{db} );
217
            $zoptions->option( 'user', $server->{userid} ) if $server->{userid};
218
            $zoptions->option( 'password', $server->{password} ) if $server->{password};
219
            $zoptions->option( 'preferredRecordSyntax', $server->{syntax} );
220
            $zoptions->option( 'timeout', $server->{timeout} ) if $server->{timeout};
221
222
            $connection = ZOOM::Connection->create($zoptions);
223
224
            $connection->connect( $server->{host}, $server->{port} );
225
            $results = $connection->search_pqf( $query ); # Starts the search
226
        } elsif ( $server->{type} eq 'koha' ) {
227
            $connection = C4::Context->Zconn( $server->{extra} );
228
            $results = $connection->search_pqf( $query ); # Starts the search
229
        } elsif ( $server->{type} eq 'batch' )  {
230
            $server->{encoding} = 'utf-8';
231
        }
232
    };
233
    if ($@) {
234
        store_fd {
235
            error => $connection ? $connection->exception() : $@,
236
            server => $server,
237
        }, $writefh;
238
        exit;
239
    }
240
241
    if ( $server->{type} eq 'batch' ) {
242
        # TODO: actually handle PQF
243
        $query =~ s/@\w+ (?:\d+=\d+ )?//g;
244
        $query =~ s/"//g;
245
246
        my $schema = Koha::Database->new->schema;
247
        $schema->storage->debug(1);
248
        my $match_condition = [ map +{ -like => '%' . $_ . '%' }, split( /\s+/, $query ) ];
249
        $hits = [ $schema->resultset('ImportRecord')->search(
250
            {
251
                import_batch_id => $server->{extra},
252
                -or => [
253
                    { 'import_biblios.title' => $match_condition },
254
                    { 'import_biblios.author' => $match_condition },
255
                    { 'import_biblios.isbn' => $match_condition },
256
                    { 'import_biblios.issn' => $match_condition },
257
                ],
258
            },
259
            {
260
                join => [ qw( import_biblios ) ],
261
                rows => $self->{fetch},
262
            }
263
        )->get_column( 'marc' )->all ];
264
265
        $num_hits = $num_fetched = scalar @$hits;
266
    } else {
267
        $num_hits = $results->size;
268
        $num_fetched = ( $self->{offset} + $self->{fetch} ) < $num_hits ? $self->{fetch} : $num_hits;
269
270
        $hits = [ map { $_->raw() } @{ $results->records( $self->{offset}, $num_fetched, 1 ) } ];
271
    }
272
273
    if ( !@$hits && $connection && $connection->exception() ) {
274
        store_fd {
275
            error => $connection->exception(),
276
            server => $server,
277
        }, $writefh;
278
        exit;
279
    }
280
281
    if ( $server->{type} eq 'koha' ) {
282
        $hits = [ map { C4::Search::new_record_from_zebra( $server->{extra}, $_ ) } @$hits ];
283
    } else {
284
        $hits = [ map { $self->_import_record( $_, $marcflavour, $server->{encoding} ? $server->{encoding} : "iso-5426" ) } @$hits ];
285
    }
286
287
    store_fd {
288
        hits => $hits,
289
        num_fetched => $num_fetched,
290
        num_hits => $num_hits,
291
        server => $server,
292
    }, $writefh;
293
294
    exit;
295
}
296
297
sub _import_record {
298
    my ( $self, $raw, $marcflavour, $encoding ) = @_;
299
300
    my ( $marcrecord ) = MarcToUTF8Record( $raw, $marcflavour, $encoding ); #ignores charset return values
301
302
    return $marcrecord;
303
}
304
305
sub _handle_hits {
306
    my ( $self, $stats, $set ) = @_;
307
308
    my $server = $set->{server};
309
310
    my $num_hits = $stats->{num_hits}->{ $server->{id} } = $set->{num_hits};
311
    my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = $set->{num_fetched};
312
313
    $stats->{total_hits} += $num_hits;
314
    $stats->{total_fetched} += $num_fetched;
315
316
    foreach my $j ( 0..$#{ $set->{hits} } ) {
317
        $self->handle_hit( $self->{offset} + $j, $server, $set->{hits}->[$j] );
318
    }
319
}
320
321
sub sort {
322
    my ( $self, $key, $direction ) = @_;
323
324
    my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys.
325
326
    foreach my $hit ( @{ $self->{results} } ) {
327
        ( $hit->{sort_key} = $hit->{metadata}->{$key} || '' ) =~ s/\W//g;
328
    }
329
330
    $self->{results} = [ sort {
331
        # Sort empty records at the end
332
        return -$empty_flip unless $a->{sort_key};
333
        return $empty_flip unless $b->{sort_key};
334
335
        $direction * ( $a->{sort_key} cmp $b->{sort_key} );
336
    } @{ $self->{results} } ];
337
}
338
339
sub results {
340
    my ( $self, $offset, $length ) = @_;
341
342
    my @subset;
343
344
    foreach my $i ( $offset..( $offset + $length - 1 ) ) {
345
        push @subset, $self->{results}->[$i] if $self->{results}->[$i];
346
    }
347
348
    return @subset;
349
}
350
351
1;
(-)a/Koha/MetadataRecord.pm (+25 lines)
Lines 56-59 sub createMergeHash { Link Here
56
    }
56
    }
57
}
57
}
58
58
59
sub getKohaField {
60
    my ($self, $kohafield) = @_;
61
62
    if ($self->schema =~ m/marc/) {
63
        my $relations = C4::Context->marcfromkohafield->{''};
64
        my $tagfield = $relations->{$kohafield};
65
66
        return '' if ref($tagfield) ne 'ARRAY';
67
68
        my ($tag, $subfield) = @$tagfield;
69
        my @kohafield;
70
        foreach my $field ( $self->record->field($tag) ) {
71
            if ( $field->tag() < 10 ) {
72
                push @kohafield, $field->data();
73
            } else {
74
                foreach my $contents ( $field->subfield($subfield) ) {
75
                    push @kohafield, $contents;
76
                }
77
            }
78
        }
79
80
        return join ' | ', @kohafield;
81
    }
82
}
83
59
1;
84
1;
(-)a/cataloguing/addbiblio.pl (+6 lines)
Lines 746-753 if ($frameworkcode eq 'FA'){ Link Here
746
        'stickyduedate'      => $fa_stickyduedate,
746
        'stickyduedate'      => $fa_stickyduedate,
747
        'duedatespec'        => $fa_duedatespec,
747
        'duedatespec'        => $fa_duedatespec,
748
    );
748
    );
749
} elsif ( C4::Context->preference('EnableAdvancedCatalogingEditor') && $input->cookie( 'catalogue_editor_' . $loggedinuser ) eq 'advanced' && !$breedingid ) {
750
    # Only use the advanced editor for non-fast-cataloging.
751
    # breedingid is not handled because those would only come off a Z39.50
752
    # search initiated by the basic editor.
753
    print $input->redirect( '/cgi-bin/koha/cataloguing/editor.pl' . ( $biblionumber ? ( '#catalog/' . $biblionumber ) : '' ) );
749
}
754
}
750
755
756
751
# Getting the list of all frameworks
757
# Getting the list of all frameworks
752
# get framework list
758
# get framework list
753
my $frameworks = getframeworks;
759
my $frameworks = getframeworks;
(-)a/cataloguing/editor.pl (+67 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
#
3
# Copyright 2013 ByWater
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 2 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
#
20
21
use Modern::Perl '2009';
22
23
use CGI;
24
use MARC::Record;
25
26
use C4::Auth;
27
use C4::Biblio;
28
use C4::Context;
29
use C4::Output;
30
use DBIx::Class::ResultClass::HashRefInflator;
31
use Koha::Database;
32
33
my $input = CGI->new;
34
35
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
36
    {
37
        template_name   => 'cataloguing/editor.tt',
38
        query           => $input,
39
        type            => 'intranet',
40
        authnotrequired => 0,
41
        flagsrequired   => { editcatalogue => 'edit_catalogue' },
42
    }
43
);
44
45
my $schema = Koha::Database->new->schema;
46
47
# Available import batches
48
$template->{VARS}->{editable_batches} = [ $schema->resultset('ImportBatch')->search(
49
    {
50
        batch_type => [ 'batch', 'webservice' ],
51
        import_status => 'staged',
52
    },
53
    { result_class => 'DBIx::Class::ResultClass::HashRefInflator' },
54
) ];
55
56
# Needed information for cataloging plugins
57
$template->{VARS}->{DefaultLanguageField008} = pack( 'A3', C4::Context->preference('DefaultLanguageField008') || 'eng' );
58
59
# Z39.50 servers
60
my $dbh = C4::Context->dbh;
61
$template->{VARS}->{z3950_servers} = $dbh->selectall_arrayref( q{
62
    SELECT * FROM z3950servers
63
    WHERE recordtype != 'authority' AND servertype = 'zed'
64
    ORDER BY servername
65
}, { Slice => {} } );
66
67
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/installer/data/mysql/atomicupdate/bug_11559-add_EnableAdvancedCatalogingEditor_syspref.sql (+1 lines)
Line 0 Link Here
1
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo');
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 117-122 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
117
('dontmerge','1',NULL,'If ON, modifying an authority record will not update all associated bibliographic records immediately, ask your system administrator to enable the merge_authorities.pl cron job','YesNo'),
117
('dontmerge','1',NULL,'If ON, modifying an authority record will not update all associated bibliographic records immediately, ask your system administrator to enable the merge_authorities.pl cron job','YesNo'),
118
('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'),
118
('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'),
119
('emailLibrarianWhenHoldIsPlaced','0',NULL,'If ON, emails the librarian whenever a hold is placed','YesNo'),
119
('emailLibrarianWhenHoldIsPlaced','0',NULL,'If ON, emails the librarian whenever a hold is placed','YesNo'),
120
('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo'),
120
('EnableBorrowerFiles','0',NULL,'If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'),
121
('EnableBorrowerFiles','0',NULL,'If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'),
121
('EnableOpacSearchHistory','1','YesNo','Enable or disable opac search history',''),
122
('EnableOpacSearchHistory','1','YesNo','Enable or disable opac search history',''),
122
('EnableSearchHistory','0','','Enable or disable search history','YesNo'),
123
('EnableSearchHistory','0','','Enable or disable search history','YesNo'),
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js (+220 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=define', 'marc-record' ], function( defaultFramework, MARC ) {
21
    var _authorised_values = defaultFramework.authorised_values;
22
    var _frameworks = {};
23
    var _framework_mappings = {};
24
25
    function _fromXMLStruct( data ) {
26
        result = {};
27
28
        $(data).children().eq(0).children().each( function() {
29
            var $contents = $(this).contents();
30
            if ( $contents.length == 1 && $contents[0].nodeType == Node.TEXT_NODE ) {
31
                result[ this.localName ] = $contents[0].data;
32
            } else {
33
                result[ this.localName ] = $contents.filter( function() { return this.nodeType != Node.TEXT_NODE || !this.data.match( /^\s+$/ ) } ).toArray();
34
            }
35
        } );
36
37
        return result;
38
    }
39
40
    function _importFramework( frameworkcode, frameworkinfo ) {
41
        _frameworks[frameworkcode] = frameworkinfo;
42
        _framework_mappings[frameworkcode] = {};
43
44
        $.each( frameworkinfo, function( i, tag ) {
45
            var tagnum = tag[0], taginfo = tag[1];
46
47
            var subfields = {};
48
49
            $.each( taginfo.subfields, function( i, subfield ) {
50
                subfields[ subfield[0] ] = subfield[1];
51
            } );
52
53
            _framework_mappings[frameworkcode][tagnum] = $.extend( {}, taginfo, { subfields: subfields } );
54
        } );
55
    }
56
57
    _importFramework( '', defaultFramework.framework );
58
59
    var KohaBackend = {
60
        NOT_EMPTY: {}, // Sentinel value
61
62
        GetAllTagsInfo: function( frameworkcode, tagnumber ) {
63
            return _framework_mappings[frameworkcode];
64
        },
65
66
        GetAuthorisedValues: function( category ) {
67
            return _authorised_values[category];
68
        },
69
70
        GetTagInfo: function( frameworkcode, tagnumber ) {
71
            if ( !_framework_mappings[frameworkcode] ) return undefined;
72
            return _framework_mappings[frameworkcode][tagnumber];
73
        },
74
75
        GetRecord: function( id, callback ) {
76
            $.get(
77
                '/cgi-bin/koha/svc/bib/' + id
78
            ).done( function( data ) {
79
                var record = new MARC.Record();
80
                record.loadMARCXML(data);
81
                callback(record);
82
            } ).fail( function( data ) {
83
                callback( { error: data } );
84
            } );
85
        },
86
87
        CreateRecord: function( record, callback ) {
88
            $.ajax( {
89
                type: 'POST',
90
                url: '/cgi-bin/koha/svc/new_bib',
91
                data: record.toXML(),
92
                contentType: 'text/xml'
93
            } ).done( function( data ) {
94
                callback( _fromXMLStruct( data ) );
95
            } ).fail( function( data ) {
96
                callback( { error: data } );
97
            } );
98
        },
99
100
        SaveRecord: function( id, record, callback ) {
101
            $.ajax( {
102
                type: 'POST',
103
                url: '/cgi-bin/koha/svc/bib/' + id,
104
                data: record.toXML(),
105
                contentType: 'text/xml'
106
            } ).done( function( data ) {
107
                callback( _fromXMLStruct( data ) );
108
            } ).fail( function( data ) {
109
                callback( { data: { error: data } } );
110
            } );
111
        },
112
113
        GetTagsBy: function( frameworkcode, field, value ) {
114
            var result = {};
115
116
            $.each( _frameworks[frameworkcode], function( undef, tag ) {
117
                var tagnum = tag[0], taginfo = tag[1];
118
119
                if ( taginfo[field] == value ) result[tagnum] = true;
120
            } );
121
122
            return result;
123
        },
124
125
        GetSubfieldsBy: function( frameworkcode, field, value ) {
126
            var result = {};
127
128
            $.each( _frameworks[frameworkcode], function( undef, tag ) {
129
                var tagnum = tag[0], taginfo = tag[1];
130
131
                $.each( taginfo.subfields, function( undef, subfield ) {
132
                    var subfieldcode = subfield[0], subfieldinfo = subfield[1];
133
134
                    if ( subfieldinfo[field] == value ) {
135
                        if ( !result[tagnum] ) result[tagnum] = {};
136
137
                        result[tagnum][subfieldcode] = true;
138
                    }
139
                } );
140
            } );
141
142
            return result;
143
        },
144
145
        FillRecord: function( frameworkcode, record, allTags ) {
146
            $.each( _frameworks[frameworkcode], function( undef, tag ) {
147
                var tagnum = tag[0], taginfo = tag[1];
148
149
                if ( taginfo.mandatory != "1" && !allTags ) return;
150
151
                var fields = record.fields(tagnum);
152
153
                if ( fields.length == 0 ) {
154
                    var newField = new MARC.Field( tagnum, ' ', ' ', [] );
155
                    fields.push( newField );
156
                    record.addFieldGrouped( newField );
157
158
                    if ( tagnum < '010' ) {
159
                        newField.addSubfield( [ '@', '' ] );
160
                        return;
161
                    }
162
                }
163
164
                $.each( taginfo.subfields, function( undef, subfield ) {
165
                    var subfieldcode = subfield[0], subfieldinfo = subfield[1];
166
167
                    if ( subfieldinfo.mandatory != "1" && !allTags ) return;
168
169
                    $.each( fields, function( undef, field ) {
170
                        if ( !field.hasSubfield(subfieldcode) ) field.addSubfieldGrouped( [ subfieldcode, '' ] );
171
                    } );
172
                } );
173
            } );
174
        },
175
176
        ValidateRecord: function( frameworkcode, record ) {
177
            var errors = [];
178
179
            var mandatoryTags = KohaBackend.GetTagsBy( '', 'mandatory', '1' );
180
            var mandatorySubfields = KohaBackend.GetSubfieldsBy( '', 'mandatory', '1' );
181
            var nonRepeatableTags = KohaBackend.GetTagsBy( '', 'repeatable', '0' );
182
            var nonRepeatableSubfields = KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' );
183
184
            $.each( mandatoryTags, function( tag ) {
185
                if ( !record.hasField( tag ) ) errors.push( { type: 'missingTag', tag: tag } );
186
            } );
187
188
            var seenTags = {};
189
190
            $.each( record.fields(), function( undef, field ) {
191
                if ( seenTags[ field.tagnumber() ] && nonRepeatableTags[ field.tagnumber() ] ) {
192
                    errors.push( { type: 'unrepeatableTag', line: field.sourceLine, tag: field.tagnumber() } );
193
                    return;
194
                }
195
196
                seenTags[ field.tagnumber() ] = true;
197
198
                var seenSubfields = {};
199
200
                $.each( field.subfields(), function( undef, subfield ) {
201
                    if ( seenSubfields[ subfield[0] ] != null && nonRepeatableSubfields[ field.tagnumber() ][ subfield[0] ] ) {
202
                        errors.push( { type: 'unrepeatableSubfield', subfield: subfield[0], line: field.sourceLine } );
203
                    } else {
204
                        seenSubfields[ subfield[0] ] = subfield[1];
205
                    }
206
                } );
207
208
                $.each( mandatorySubfields[ field.tagnumber() ] || {}, function( subfield ) {
209
                    if ( !seenSubfields[ subfield ] ) {
210
                        errors.push( { type: 'missingSubfield', subfield: subfield[0], line: field.sourceLine } );
211
                    }
212
                } );
213
            } );
214
215
            return errors;
216
        },
217
    };
218
219
    return KohaBackend;
220
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js (+38 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'macros/its', 'macros/rancor' ], function( ITSMacro, RancorMacro ) {
21
    var Macros = {
22
        formats: {
23
            its: {
24
                description: 'TLC® ITS',
25
                Run: ITSMacro.Run,
26
            },
27
            rancor: {
28
                description: 'Rancor',
29
                Run: RancorMacro.Run,
30
            },
31
        },
32
        Run: function( editor, format, macro ) {
33
            return Macros.formats[format].Run( editor, macro );
34
        },
35
    };
36
37
    return Macros;
38
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js (+208 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( function() {
21
    var NAV_FAILED = new Object();
22
    var NAV_SUCCEEDED = new Object();
23
24
    var _commandGenerators = [
25
        [ /^copy field data$/i, function() {
26
            return function( editor, state ) {
27
                if ( state.field == null ) return;
28
29
                return state.field.getText();
30
            };
31
        } ],
32
        [ /^copy subfield data$/i, function() {
33
            return function( editor, state ) {
34
                if ( state.field == null ) return;
35
36
                var cur = editor.getCursor();
37
                var subfields = state.field.getSubfields();
38
39
                for (var i = 0; i < subfields.length; i++) {
40
                    if ( cur.ch > subfields[i].end ) continue;
41
42
                    state.clipboard = subfields[i].text;
43
                    return;
44
                }
45
46
                return false;
47
            }
48
        } ],
49
        [ /^del(ete)? field$/i, function() {
50
            return function( editor, state ) {
51
                if ( state.field == null ) return;
52
53
                state.field.delete();
54
                return NAV_FAILED;
55
            }
56
        } ],
57
        [ /^goto field end$/i, function() {
58
            return function( editor, state ) {
59
                if ( state.field == null ) return NAV_FAILED;
60
                var cur = editor.cm.getCursor();
61
62
                editor.cm.setCursor( { line: cur.line } );
63
                return NAV_SUCCEEDED;
64
            }
65
        } ],
66
        [ /^goto field (\w{3})$/i, function(tag) {
67
            return function( editor, state ) {
68
                var field = editor.getFirstField(tag);
69
                if ( field == null ) return NAV_FAILED;
70
71
                field.focus();
72
                return NAV_SUCCEEDED;
73
            }
74
        } ],
75
        [ /^goto subfield end$/i, function() {
76
            return function( editor, state ) {
77
                if ( state.field == null ) return NAV_FAILED;
78
79
                var cur = editor.getCursor();
80
                var subfields = state.field.getSubfields();
81
82
                for (var i = 0; i < subfields.length; i++) {
83
                    if ( cur.ch > subfields[i].end ) continue;
84
85
                    subfield.focusEnd();
86
                    return NAV_SUCCEEDED;
87
                }
88
89
                return NAV_FAILED;
90
            }
91
        } ],
92
        [ /^goto subfield (\w)$/i, function( code ) {
93
            return function( editor, state ) {
94
                if ( state.field == null ) return NAV_FAILED;
95
96
                var subfield = state.field.getFirstSubfield( code );
97
                if ( subfield == null ) return NAV_FAILED;
98
99
                subfield.focus();
100
                return NAV_SUCCEEDED;
101
            }
102
        } ],
103
        [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, tag, text) {
104
            text = text.replace(/\\([0-9a-z])/g, '$$$1 ');
105
            return function( editor, state ) {
106
                editor.createFieldGrouped(tag).setText(text).focus();
107
                return NAV_SUCCEEDED;
108
            }
109
        } ],
110
        [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, code, text) {
111
            return function( editor, state ) {
112
                if ( state.field == null ) return;
113
114
                state.field.appendSubfield(code).setText(text);
115
            }
116
        } ],
117
        [ /^paste$/i, function() {
118
            return function( editor, state ) {
119
                var cur = editor.cm.getCursor();
120
121
                editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' );
122
            }
123
        } ],
124
        [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) {
125
            return function( editor, state ) {
126
                if ( state.field == null ) return;
127
                if ( state.field.isControlField ) return false;
128
129
                if ( ind == '1' ) {
130
                    state.field.setIndicator1(value);
131
                    return true;
132
                } else if ( ind == '2' ) {
133
                    state.field.setIndicator2(value);
134
                    return true;
135
                } else {
136
                    return false;
137
                }
138
            }
139
        } ],
140
        [ /^set indicators=([ _0-9])([ _0-9])?$/i, function( ind1, ind2 ) {
141
            return function( editor, state ) {
142
                if ( state.field == null ) return;
143
                if ( state.field.isControlField ) return false;
144
145
                state.field.setIndicator1(ind1);
146
                state.field.setIndicator2(ind2 || '_');
147
            }
148
        } ],
149
    ];
150
151
    var ITSMacro = {
152
        Compile: function( macro ) {
153
            var result = { commands: [], errors: [] };
154
155
            $.each( macro.split(/\r\n|\n/), function( line, contents ) {
156
                var command;
157
158
                if ( contents.match(/^\s*$/) ) return;
159
160
                $.each( _commandGenerators, function( undef, gen ) {
161
                    var match;
162
163
                    if ( !( match = gen[0].exec( contents ) ) ) return;
164
165
                    command = gen[1].apply(null, match.slice(1));
166
                    return false;
167
                } );
168
169
                if ( !command ) {
170
                    result.errors.push( { line: line, error: 'unrecognized' } );
171
                }
172
173
                result.commands.push( { func: command, orig: contents, line: line } );
174
            } );
175
176
            return result;
177
        },
178
        Run: function( editor, macro ) {
179
            var compiled = ITSMacro.Compile(macro);
180
            if ( compiled.errors.length ) return { errors: compiled.errors };
181
            var state = {
182
                clipboard: '',
183
                field: null,
184
            };
185
186
            var run_result = { errors: [] };
187
188
            editor.cm.operation( function() {
189
                $.each( compiled.commands, function( undef, command ) {
190
                    var result = command.func( editor, state );
191
192
                    if ( result == NAV_FAILED ) {
193
                        state.field = null;
194
                    } else if ( result == NAV_SUCCEEDED ) {
195
                        state.field = editor.getCurrentField();
196
                    } else if ( result === false ) {
197
                        run_result.errors.push( { line: command.line, error: 'failed' } );
198
                        return false;
199
                    }
200
                } );
201
            } );
202
203
            return run_result;
204
        },
205
    };
206
207
    return ITSMacro;
208
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js (+277 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'marc-editor' ], function( MARCEditor ) {
21
    // These are the generators for targets that appear on the left-hand side of an assignment.
22
    var _lhsGenerators = [
23
        // Field; will replace the entire contents of the tag except for indicators.
24
        // Examples:
25
        //   * 245 - will return the first 245 tag it finds, or create a new one
26
        //   * new 245 - will always create a new 245
27
        //   * new 245 grouped - will always create a new 245, and insert it at the end of the 2xx
28
        //     block
29
        [ /^(new )?(\w{3})( (grouped))?$/, function( forceCreate, tag, position, positionGrouped ) {
30
            if ( !forceCreate && positionGrouped ) return null;
31
32
            // The extra argument allows the delete command to prevent this from needlessly creating
33
            // a tag that it is about to delete.
34
            return function( editor, state, extra ) {
35
                extra = extra || {};
36
37
                if ( !forceCreate ) {
38
                    var result = editor.getFirstField(tag);
39
40
                    if ( result != null || extra.dontCreate ) return result;
41
                }
42
43
                if ( positionGrouped ) {
44
                    return editor.createFieldGrouped(tag);
45
                } else {
46
                    return editor.createFieldOrdered(tag);
47
                }
48
            }
49
        } ],
50
51
        // This regex is a little complicated, but allows for the following possibilities:
52
        //   * 245a - Finds the first 245 field, then tries to find an a subfield within it. If none
53
        //            exists, it is created. Will still fail if there is no 245 field.
54
        //   * new 245a - always creates a new a subfield.
55
        //   * new 245a at end - does the same as the above.
56
        //   * $a or new $a - does the same as the above, but for the last-used tag.
57
        //   * new 245a after b - creates a new subfield, placing it after the first subfield $b.
58
        [ /^(new )?(\w{3}|\$)(\w)( (at end)| after (\w))?$/, function( forceCreate, tag, code, position, positionAtEnd, positionAfterSubfield ) {
59
            if ( tag != '$' && tag < '010' ) return null;
60
            if ( !forceCreate && position ) return null;
61
62
            return function( editor, state, extra ) {
63
                extra = extra || {};
64
65
                var field;
66
67
                if ( tag == '$' ) {
68
                    field = state.field;
69
                } else {
70
                    field = editor.getFirstField(tag);
71
                }
72
                if ( field == null || field.isControlField ) return null;
73
74
                if ( !forceCreate ) {
75
                    var subfield = field.getFirstSubfield(code)
76
77
                    if ( subfield != null || extra.dontCreate ) return subfield;
78
                }
79
80
                if ( !position || position == ' at end' ) {
81
                    return field.appendSubfield(code);
82
                } else if ( positionAfterSubfield ) {
83
                    var afterSubfield = field.getFirstSubfield(positionAfterSubfield);
84
85
                    if ( afterSubfield == null ) return null;
86
87
                    return field.insertSubfield( code, afterSubfield.index + 1 );
88
                }
89
            }
90
        } ],
91
92
        // Can set indicatators either for a particular field or the last-used tag.
93
        [ /^((\w{3}) )?indicators$/, function( undef, tag ) {
94
            if ( tag && tag < '010' ) return null;
95
96
            return function( editor, state ) {
97
                var field;
98
99
                if ( tag == null ) {
100
                    field = state.field;
101
                } else {
102
                    field = editor.getFirstField(tag);
103
                }
104
                if ( field == null || field.isControlField ) return null;
105
106
                return {
107
                    field: field,
108
                    setText: function( text ) {
109
                        field.setIndicator1( text.substr( 0, 1 ) );
110
                        field.setIndicator2( text.substr( 1, 1 ) );
111
                    }
112
                };
113
            }
114
        } ],
115
    ];
116
117
    // These patterns, on the other hand, appear inside interpolations on the right hand side.
118
    var _rhsGenerators = [
119
        [ /^(\w{3})$/, function( tag ) {
120
            return function( editor, state, extra ) {
121
                return editor.getFirstField(tag);
122
            }
123
        } ],
124
        [ /^(\w{3})(\w)$/, function( tag, code ) {
125
            if ( tag < '010' ) return null;
126
127
            return function( editor, state, extra ) {
128
                extra = extra || {};
129
130
                var field = editor.getFirstField(tag);
131
                if ( field == null ) return null;
132
133
                return field.getFirstSubfield(code);
134
            }
135
        } ],
136
    ];
137
138
    var _commandGenerators = [
139
        [ /^delete (.+)$/, function( target ) {
140
            var target_closure = _generate( _lhsGenerators, target );
141
            if ( !target_closure ) return null;
142
143
            return function( editor, state ) {
144
                var target = target_closure( editor, state, { dontCreate: true } );
145
                if ( target == null ) return;
146
                if ( !target.delete ) return false;
147
148
                state.field = null; // As other fields may have been invalidated
149
                target.delete();
150
            }
151
        } ],
152
        [ /^([^=]+)=([^=]*)$/, function( lhs_desc, rhs_desc ) {
153
            var lhs_closure = _generate( _lhsGenerators, lhs_desc );
154
            if ( !lhs_closure ) return null;
155
156
            var rhs_closure = _generateInterpolation( _rhsGenerators, rhs_desc );
157
            if ( !rhs_closure ) return null;
158
159
            return function( editor, state ) {
160
                var lhs = lhs_closure( editor, state );
161
                if ( lhs == null ) return;
162
163
                state.field = lhs.field || lhs;
164
165
                try {
166
                    return lhs.setText( rhs_closure( editor, state ) );
167
                } catch (e) {
168
                    if ( e instanceof MARCEditor.FieldError ) {
169
                        return false;
170
                    } else {
171
                        throw e;
172
                    }
173
                }
174
            };
175
        } ],
176
    ];
177
178
    function _generate( set, contents ) {
179
        var closure;
180
181
        if ( contents.match(/^\s*$/) ) return;
182
183
        $.each( set, function( undef, gen ) {
184
            var match;
185
186
            if ( !( match = gen[0].exec( contents ) ) ) return;
187
188
            closure = gen[1].apply(null, match.slice(1));
189
            return false;
190
        } );
191
192
        return closure;
193
    }
194
195
    function _generateInterpolation( set, contents ) {
196
        // While this regex will not match at all for an empty string, that just leaves an empty
197
        // parts array which yields an empty string (which is what we want.)
198
        var matcher = /\{([^}]+)\}|([^{]+)/g;
199
        var match;
200
201
        var parts = [];
202
203
        while ( ( match = matcher.exec(contents) ) ) {
204
            var closure;
205
            if ( match[1] ) {
206
                // Found an interpolation
207
                var rhs_closure = _generate( set, match[1] );
208
                if ( rhs_closure == null ) return null;
209
210
                closure = ( function(rhs_closure) { return function( editor, state ) {
211
                    var rhs = rhs_closure( editor, state );
212
213
                    return rhs ? rhs.getText() : '';
214
                } } )( rhs_closure );
215
            } else {
216
                // Plain text (needs artificial closure to keep match)
217
                closure = ( function(text) { return function() { return text }; } )( match[2] );
218
            }
219
220
            parts.push( closure );
221
        }
222
223
        return function( editor, state ) {
224
            var result = '';
225
            $.each( parts, function( i, part ) {
226
                result += part( editor, state );
227
            } );
228
229
            return result;
230
        };
231
    }
232
233
    var RancorMacro = {
234
        Compile: function( macro ) {
235
            var result = { commands: [], errors: [] };
236
237
            $.each( macro.split(/\r\n|\n/), function( line, contents ) {
238
                contents = contents.replace( /#.*$/, '' );
239
                if ( contents.match(/^\s*$/) ) return;
240
241
                var command = _generate( _commandGenerators, contents );
242
243
                if ( !command ) {
244
                    result.errors.push( { line: line, error: 'unrecognized' } );
245
                }
246
247
                result.commands.push( { func: command, orig: contents, line: line } );
248
            } );
249
250
            return result;
251
        },
252
        Run: function( editor, macro ) {
253
            var compiled = RancorMacro.Compile(macro);
254
            if ( compiled.errors.length ) return { errors: compiled.errors };
255
            var state = {
256
                field: null,
257
            };
258
259
            var run_result = { errors: [] };
260
261
            editor.cm.operation( function() {
262
                $.each( compiled.commands, function( undef, command ) {
263
                    var result = command.func( editor, state );
264
265
                    if ( result === false ) {
266
                        run_result.errors.push( { line: command.line, error: 'failed' } );
267
                        return false;
268
                    }
269
                } );
270
            } );
271
272
            return run_result;
273
        },
274
    };
275
276
    return RancorMacro;
277
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js (+671 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) {
21
    var NOTIFY_TIMEOUT = 250;
22
23
    function editorCursorActivity( cm ) {
24
        var editor = cm.marceditor;
25
        var field = editor.getCurrentField();
26
        if ( !field ) return;
27
28
        // Set overwrite mode for tag numbers/indicators and contents of fixed fields
29
        if ( field.isControlField || cm.getCursor().ch < 8 ) {
30
            cm.toggleOverwrite(true);
31
        } else {
32
            cm.toggleOverwrite(false);
33
        }
34
35
        editor.onCursorActivity();
36
    }
37
38
    // This function exists to prevent inserting or partially deleting text that belongs to a
39
    // widget. The 'marcAware' change source exists for other parts of the editor code to bypass
40
    // this check.
41
    function editorBeforeChange( cm, change ) {
42
        var editor = cm.marceditor;
43
        if ( editor.textMode || change.origin == 'marcAware' || change.origin == 'widget.clearToText' ) return;
44
45
        // FIXME: Should only cancel changes if this is a control field/subfield widget
46
        if ( change.from.line !== change.to.line || Math.abs( change.from.ch - change.to.ch ) > 1 || change.text.length != 1 || change.text[0].length != 0 ) return; // Not single-char change
47
48
        if ( change.from.ch == change.to.ch - 1 && cm.findMarksAt( { line: change.from.line, ch: change.from.ch + 1 } ).length ) {
49
            change.cancel();
50
        } else if ( change.from.ch == change.to.ch && cm.findMarksAt(change.from).length && !change.text[0].match(/^[$|ǂ‡]$/) ) {
51
            change.cancel();
52
        }
53
    }
54
55
    function editorChanges( cm, changes ) {
56
        var editor = cm.marceditor;
57
        if ( editor.textMode ) return;
58
59
        for (var i = 0; i < changes.length; i++) {
60
            var change = changes[i];
61
62
            var origin = change.from.line;
63
            var newTo = CodeMirror.changeEnd(change);
64
65
            for (var delLine = origin; delLine <= change.to.line; delLine++) {
66
                // Line deleted; currently nothing to do
67
            }
68
69
            for (var line = origin; line <= newTo.line; line++) {
70
                if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( cm.marceditor, line );
71
                if ( change.origin != 'setValue' && change.origin != 'marcWidgetPrefill' && change.origin != 'widget.clearToText' ) cm.addLineClass( line, 'wrapper', 'modified-line' );
72
            }
73
        }
74
75
        Widget.ActivateAt( cm, cm.getCursor() );
76
        cm.marceditor.startNotify();
77
    }
78
79
    // Editor helper functions
80
    function activateTabPosition( cm, pos, idx ) {
81
        // Allow tabbing to as-yet-nonexistent positions
82
        var lenDiff = pos.ch - cm.getLine( pos.line ).length;
83
        if ( lenDiff > 0 ) {
84
            var extra = '';
85
            while ( lenDiff-- > 0 ) extra += ' ';
86
            if ( pos.prefill ) extra += pos.prefill;
87
            cm.replaceRange( extra, { line: pos.line } );
88
        }
89
90
        cm.setCursor( pos );
91
        Widget.ActivateAt( cm, pos, idx );
92
    }
93
94
    function getTabPositions( editor, cur ) {
95
        cur = cur || editor.cm.getCursor();
96
        var field = editor.getFieldAt( cur.line );
97
98
        if ( field ) {
99
            if ( field.isControlField ) {
100
                var positions = [ { ch: 0 }, { ch: 4 } ];
101
102
                $.each( positions, function( undef, pos ) {
103
                    pos.line = cur.line;
104
                } );
105
106
                return positions;
107
            } else {
108
                var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ];
109
110
                $.each( positions, function( undef, pos ) {
111
                    pos.line = cur.line;
112
                } );
113
                $.each( field.getSubfields(), function( undef, subfield ) {
114
                    positions.push( { line: cur.line, ch: subfield.contentsStart } );
115
                } );
116
117
                // Allow to tab to start of empty field
118
                if ( field.getSubfields().length == 0 ) {
119
                    positions.push( { line: cur.line, ch: 8 } );
120
                }
121
122
                return positions;
123
            }
124
        } else {
125
            return [];
126
        }
127
    }
128
129
    var _editorKeys = {
130
        Enter: function( cm ) {
131
            var cursor = cm.getCursor();
132
            cm.replaceRange( '\n', { line: cursor.line }, null, 'marcAware' );
133
            cm.setCursor( { line: cursor.line + 1, ch: 0 } );
134
        },
135
136
        'Ctrl-X': function( cm ) {
137
            // Delete line (or cut)
138
            if ( cm.somethingSelected() ) return true;
139
140
            var field = cm.marceditor.getCurrentField();
141
            if ( field ) field.delete();
142
        },
143
144
        'Shift-Ctrl-X': function( cm ) {
145
            // Delete subfield
146
            var field = cm.marceditor.getCurrentField();
147
            if ( !field ) return;
148
149
            var subfield = field.getSubfieldAt( cm.getCursor().ch );
150
            if ( subfield ) subfield.delete();
151
        },
152
153
        Tab: function( cm ) {
154
            // Move through parts of tag/fixed fields
155
            var positions = getTabPositions( cm.marceditor );
156
            var cur = cm.getCursor();
157
158
            for ( var i = 0; i < positions.length; i++ ) {
159
                if ( positions[i].ch > cur.ch ) {
160
                    activateTabPosition( cm, positions[i] );
161
                    return false;
162
                }
163
            }
164
165
            cm.setCursor( { line: cur.line + 1, ch: 0 } );
166
        },
167
168
        'Shift-Tab': function( cm ) {
169
            // Move backwards through parts of tag/fixed fields
170
            var positions = getTabPositions( cm.marceditor );
171
            var cur = cm.getCursor();
172
173
            for ( var i = positions.length - 1; i >= 0; i-- ) {
174
                if ( positions[i].ch < cur.ch ) {
175
                    activateTabPosition( cm, positions[i] );
176
                    return false;
177
                }
178
            }
179
180
            if ( cur.line == 0 ) return;
181
182
            var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
183
184
            if ( prevPositions.length ) {
185
                activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 );
186
            } else {
187
                cm.setCursor( { line: cur.line - 1, ch: 0 } );
188
            }
189
        },
190
191
        'Ctrl-D': function( cm ) {
192
            // Insert subfield delimiter
193
            // This will be extended later to allow either a configurable subfield delimiter or just
194
            // make it be the double cross.
195
            var cur = cm.getCursor();
196
197
            cm.replaceRange( "$", cur, null );
198
        },
199
    };
200
201
    // The objects below are part of a field/subfield manipulation API, accessed through the base
202
    // editor object.
203
    //
204
    // Each one is tied to a particular line; this means that using a field or subfield object after
205
    // any other changes to the record will cause entertaining explosions. The objects are meant to
206
    // be temporary, and should only be reused with great care. The macro code does this only
207
    // because it is careful to dispose of the object after any other updates.
208
    //
209
    // Note, however, tha you can continue to use a field object after changing subfields. It's just
210
    // the subfield objects that become invalid.
211
212
    // This is an exception raised by the EditorSubfield and EditorField when an invalid change is
213
    // attempted.
214
    function FieldError(line, message) {
215
        this.line = line;
216
        this.message = message;
217
    };
218
219
    FieldError.prototype.toString = function() {
220
        return 'FieldError(' + this.line + ', "' + this.message + '")';
221
    };
222
223
    // This is the temporary object for a particular subfield in a field. Any change to any other
224
    // subfields will invalidate this subfield object.
225
    function EditorSubfield( field, index, start, end ) {
226
        this.field = field;
227
        this.index = index;
228
        this.start = start;
229
        this.end = end;
230
231
        if ( this.field.isControlField ) {
232
            this.contentsStart = start;
233
            this.code = '@';
234
        } else {
235
            this.contentsStart = start + 3;
236
            this.code =  this.field.contents.substr( this.start + 1, 1 );
237
        }
238
239
        this.cm = field.cm;
240
241
        var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } );
242
        if ( marks[0] && marks[0].widget ) {
243
            this.widget = marks[0].widget;
244
245
            this.text = this.widget.text;
246
            this.setText = this.widget.setText;
247
            this.getFixed = this.widget.getFixed;
248
            this.setFixed = this.widget.setFixed;
249
        } else {
250
            this.widget = null;
251
            this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart );
252
        }
253
    };
254
255
    $.extend( EditorSubfield.prototype, {
256
        _invalid: function() {
257
            return this.field._subfieldsInvalid();
258
        },
259
260
        focus: function() {
261
            this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } );
262
        },
263
        focusEnd: function() {
264
            this.cm.setCursor( { line: this.field.line, ch: this.end } );
265
        },
266
        getText: function() {
267
            return this.text;
268
        },
269
        setText: function( text ) {
270
            if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' );
271
            this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' );
272
            this.field._invalidateSubfields();
273
        },
274
    } );
275
276
    function EditorField( editor, line ) {
277
        this.editor = editor;
278
        this.line = line;
279
280
        this.cm = editor.cm;
281
282
        this._updateInfo();
283
        this.tag = this.contents.substr( 0, 3 );
284
        this.isControlField = ( this.tag < '010' );
285
286
        if ( this.isControlField ) {
287
            this._ind1 = this.contents.substr( 4, 1 );
288
            this._ind2 = this.contents.substr( 6, 1 );
289
        } else {
290
            this._ind1 = null;
291
            this._ind2 = null;
292
        }
293
294
        this.subfields = null;
295
    }
296
297
    $.extend( EditorField.prototype, {
298
        _subfieldsInvalid: function() {
299
            return !this.subfields;
300
        },
301
        _invalidateSubfields: function() {
302
            this._subfields = null;
303
        },
304
305
        _updateInfo: function() {
306
            this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } );
307
            if ( this.info == null ) throw new FieldError( 'Invalid field' );
308
            this.contents = this.info.contents;
309
        },
310
        _scanSubfields: function() {
311
            this._updateInfo();
312
313
            if ( this.isControlField ) {
314
                this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ];
315
            } else {
316
                var field = this;
317
                var subfields = this.info.subfields;
318
                this._subfields = [];
319
320
                for (var i = 0; i < this.info.subfields.length; i++) {
321
                    var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch;
322
323
                    this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) );
324
                }
325
            }
326
        },
327
328
        delete: function() {
329
            this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' );
330
        },
331
        focus: function() {
332
            this.cm.setCursor( { line: this.line, ch: 0 } );
333
334
            return this;
335
        },
336
337
        getText: function() {
338
            var result = '';
339
340
            $.each( this.getSubfields(), function() {
341
                if ( this.code != '@' ) result += '$' + this.code + ' ';
342
343
                result += this.getText();
344
            } );
345
346
            return result;
347
        },
348
        setText: function( text ) {
349
            var indicator_match = /^([_ 0-9])([_ 0-9])\$/.exec( text );
350
            if ( indicator_match ) {
351
                text = text.substr(2);
352
                this.setIndicator1( indicator_match[1] );
353
                this.setIndicator2( indicator_match[2] );
354
            }
355
356
            this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' );
357
            this._invalidateSubfields();
358
359
            return this;
360
        },
361
362
        getIndicator1: function() {
363
            return this._ind1;
364
        },
365
        getIndicator2: function() {
366
            return this._ind2;
367
        },
368
        setIndicator1: function(val) {
369
            if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
370
371
            this._ind1 = ( !val || val == ' ' ) ? '_' : val;
372
            this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' );
373
374
            return this;
375
        },
376
        setIndicator2: function(val) {
377
            if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
378
379
            this._ind2 = ( !val || val == ' ' ) ? '_' : val;
380
            this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' );
381
382
            return this;
383
        },
384
385
        appendSubfield: function( code ) {
386
            if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
387
388
            this._invalidateSubfields();
389
            this.cm.replaceRange( '$' + code + ' ', { line: this.line }, null, 'marcAware' );
390
            var subfields = this.getSubfields();
391
392
            return subfields[ subfields.length - 1 ];
393
        },
394
        insertSubfield: function( code, position ) {
395
            if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
396
397
            position = position || 0;
398
399
            var subfields = this.getSubfields();
400
            this._invalidateSubfields();
401
            this.cm.replaceRange( '$' + code + ' ', { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' );
402
            subfields = this.getSubfields();
403
404
            return subfields[ position ];
405
        },
406
        getSubfields: function( code ) {
407
            if ( !this._subfields ) this._scanSubfields();
408
            if ( code == null ) return this._subfields;
409
410
            var result = [];
411
412
            $.each( this._subfields, function() {
413
                if ( code == null || this.code == code ) result.push(this);
414
            } );
415
416
            return result;
417
        },
418
        getFirstSubfield: function( code ) {
419
            var result = this.getSubfields( code );
420
421
            return ( result && result.length ) ? result[0] : null;
422
        },
423
        getSubfieldAt: function( ch ) {
424
            var subfields = this.getSubfields();
425
426
            for (var i = 0; i < subfields.length; i++) {
427
                if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i];
428
            }
429
        },
430
    } );
431
432
    function MARCEditor( options ) {
433
        this.cm = CodeMirror(
434
            options.position,
435
            {
436
                extraKeys: _editorKeys,
437
                gutters: [
438
                    'modified-line-gutter',
439
                ],
440
                lineWrapping: true,
441
                mode: {
442
                    name: 'marc',
443
                    nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ),
444
                    nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' )
445
                }
446
            }
447
        );
448
        this.cm.marceditor = this;
449
450
        this.cm.on( 'beforeChange', editorBeforeChange );
451
        this.cm.on( 'changes', editorChanges );
452
        this.cm.on( 'cursorActivity', editorCursorActivity );
453
454
        this.onCursorActivity = options.onCursorActivity;
455
456
        this.subscribers = [];
457
        this.subscribe( function( marceditor ) {
458
            Widget.Notify( marceditor );
459
        } );
460
    }
461
462
    MARCEditor.FieldError = FieldError;
463
464
    $.extend( MARCEditor.prototype, {
465
        setUseWidgets: function( val ) {
466
            if ( val ) {
467
                for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
468
                    Widget.UpdateLine( this, line );
469
                }
470
            } else {
471
                $.each( this.cm.getAllMarks(), function( undef, mark ) {
472
                    if ( mark.widget ) mark.widget.clearToText();
473
                } );
474
            }
475
        },
476
477
        focus: function() {
478
            this.cm.focus();
479
        },
480
481
        getCursor: function() {
482
            return this.cm.getCursor();
483
        },
484
485
        refresh: function() {
486
            this.cm.refresh();
487
        },
488
489
        displayRecord: function( record ) {
490
            this.cm.setValue( TextMARC.RecordToText(record) );
491
        },
492
493
        getRecord: function() {
494
            this.textMode = true;
495
496
            $.each( this.cm.getAllMarks(), function( undef, mark ) {
497
                if ( mark.widget ) mark.widget.clearToText();
498
            } );
499
            var record = TextMARC.TextToRecord( this.cm.getValue() );
500
            for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
501
                if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line );
502
            }
503
504
            this.textMode = false;
505
506
            return record;
507
        },
508
509
        getLineInfo: function( pos ) {
510
            var contents = this.cm.getLine( pos.line );
511
            if ( contents == null ) return {};
512
513
            var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
514
515
            if ( !tagNumber ) return null; // No tag at all on this line
516
            tagNumber = tagNumber[1];
517
518
            if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
519
520
            var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
521
            var match;
522
523
            var subfields = [];
524
            var currentSubfield;
525
526
            while ( ( match = matcher.exec(contents) ) ) {
527
                subfields.push( { code: match[1], ch: match.index } );
528
                if ( match.index < pos.ch ) currentSubfield = match[1];
529
            }
530
531
            return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
532
        },
533
534
        addError: function( line, error ) {
535
            var found = false;
536
            var options = {};
537
538
            if ( line == null ) {
539
                line = 0;
540
                options.above = true;
541
            }
542
543
            $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
544
                if ( !widget.isErrorMarker ) return;
545
546
                found = true;
547
548
                $( widget.node ).append( '; ' + error );
549
                widget.changed();
550
551
                return false;
552
            } );
553
554
            if ( found ) return;
555
556
            var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0];
557
            var widget = this.cm.addLineWidget( line, node, options );
558
559
            widget.node = node;
560
            widget.isErrorMarker = true;
561
        },
562
563
        removeErrors: function() {
564
            for ( var line = 0; line < this.cm.lineCount(); line++ ) {
565
                $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) {
566
                    if ( lineWidget.isErrorMarker ) lineWidget.clear();
567
                } );
568
            }
569
        },
570
571
        startNotify: function() {
572
            if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
573
            this.notifyTimeout = setTimeout( $.proxy( function() {
574
                this.notifyAll();
575
576
                this.notifyTimeout = null;
577
            }, this ), NOTIFY_TIMEOUT );
578
        },
579
580
        notifyAll: function() {
581
            $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
582
                subscriber(this);
583
            }, this ) );
584
        },
585
586
        subscribe: function( subscriber ) {
587
            this.subscribers.push( subscriber );
588
        },
589
590
        createField: function( tag, line ) {
591
            var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
592
593
            if ( line > this.cm.lastLine() ) {
594
                contents = '\n' + contents;
595
            } else {
596
                contents = contents + '\n';
597
            }
598
599
            this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
600
601
            return new EditorField( this, line );
602
        },
603
604
        createFieldOrdered: function( tag ) {
605
            var line, contents;
606
607
            for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
608
                if ( contents && contents.substr(0, 3) > tag ) break;
609
            }
610
611
            return this.createField( tag, line );
612
        },
613
614
        createFieldGrouped: function( tag ) {
615
            // Control fields should be inserted in actual order, whereas other fields should be
616
            // inserted grouped
617
            if ( tag < '010' ) return this.createFieldOrdered( tag );
618
619
            var line, contents;
620
621
            for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
622
                if ( contents && contents[0] > tag[0] ) break;
623
            }
624
625
            return this.createField( tag, line );
626
        },
627
628
        getFieldAt: function( line ) {
629
            try {
630
                return new EditorField( this, line );
631
            } catch (e) {
632
                return null;
633
            }
634
        },
635
636
        getCurrentField: function() {
637
            return this.getFieldAt( this.cm.getCursor().line );
638
        },
639
640
        getFields: function( tag ) {
641
            var result = [];
642
643
            if ( tag != null ) tag += ' ';
644
645
            for ( var line = 0; line < this.cm.lineCount(); line++ ) {
646
                if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue;
647
648
                // If this throws a FieldError, pretend it doesn't exist
649
                try {
650
                    result.push( new EditorField( this, line ) );
651
                } catch (e) {
652
                    if ( !( e instanceof FieldError ) ) throw e;
653
                }
654
            }
655
656
            return result;
657
        },
658
659
        getFirstField: function( tag ) {
660
            var result = this.getFields( tag );
661
662
            return ( result && result.length ) ? result[0] : null;
663
        },
664
665
        getAllFields: function( tag ) {
666
            return this.getFields( null );
667
        },
668
    } );
669
670
    return MARCEditor;
671
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js (+168 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
// Expected format: 245 _ 1 $a Pizza |c 34ars
21
22
CodeMirror.defineMode( 'marc', function( config, modeConfig ) {
23
    modeConfig.nonRepeatableTags = modeConfig.nonRepeatableTags || {};
24
    modeConfig.nonRepeatableSubfields = modeConfig.nonRepeatableSubfields || {};
25
26
    return {
27
        startState: function( prevState ) {
28
            var state = prevState || {};
29
30
            if ( !prevState ) {
31
                state.seenTags = {};
32
            }
33
34
            state.indicatorNeeded = false;
35
            state.subAllowed = true;
36
            state.subfieldCode = undefined;
37
            state.tagNumber = undefined;
38
            state.seenSubfields = {};
39
40
            return state;
41
        },
42
        copyState: function( prevState ) {
43
            var result = $.extend( {}, prevState );
44
            result.seenTags = $.extend( {}, prevState.seenTags );
45
            result.seenSubfields = $.extend( {}, prevState.seenSubfields );
46
47
            return result;
48
        },
49
        token: function( stream, state ) {
50
            var match;
51
            // First, try to match some kind of valid tag
52
            if ( stream.sol() ) {
53
                this.startState( state );
54
                if ( match = stream.match( /[0-9A-Za-z]+/ ) ) {
55
                    match = match[0];
56
                    if ( match.length != 3 ) {
57
                        if ( stream.eol() && match.length < 3 ) {
58
                            // Don't show error for incomplete number
59
                            return 'tagnumber';
60
                        } else {
61
                            stream.skipToEnd();
62
                            return 'error';
63
                        }
64
                    }
65
66
                    state.tagNumber = match;
67
                    if ( state.tagNumber < '010' ) {
68
                        // Control field, no subfields or indicators
69
                        state.subAllowed = false;
70
                    }
71
72
                    if ( state.seenTags[state.tagNumber] && modeConfig.nonRepeatableTags[state.tagNumber] ) {
73
                        return 'bad-tagnumber';
74
                    } else {
75
                        state.seenTags[state.tagNumber] = true;
76
                        return 'tagnumber';
77
                    }
78
                } else {
79
                    stream.skipToEnd();
80
                    return 'error';
81
                }
82
            }
83
84
            // Don't need to do anything
85
            if ( stream.eol() ) {
86
                return;
87
            }
88
89
            // Check for the correct space after the tag number for a control field
90
            if ( !state.subAllowed && stream.pos == 3 ) {
91
                if ( stream.next() == ' ' ) {
92
                    return 'required-space';
93
                } else {
94
                    stream.skipToEnd();
95
                    return 'error';
96
                }
97
            }
98
99
            // For a normal field, check for correct indicators and spacing
100
            if ( stream.pos < 8 && state.subAllowed ) {
101
                switch ( stream.pos ) {
102
                    case 3:
103
                    case 5:
104
                    case 7:
105
                        if ( stream.next() == ' ' ) {
106
                            return 'required-space';
107
                        } else {
108
                            stream.skipToEnd();
109
                            return 'error';
110
                        }
111
                    case 4:
112
                    case 6:
113
                        if ( /[0-9A-Za-z_]/.test( stream.next() ) ) {
114
                            return 'indicator';
115
                        } else {
116
                            stream.skipToEnd();
117
                            return 'error';
118
                        }
119
                }
120
            }
121
122
            // Otherwise, we're after the start of the line.
123
            if ( state.subAllowed ) {
124
                // If we don't have to match a subfield, try to consume text.
125
                if ( stream.pos != 8 ) {
126
                    // Try to match space at the end of the line, then everything but spaces, and as
127
                    // a final fallback, only spaces.
128
                    //
129
                    // This is required to keep the contents matching from stepping on the end-space
130
                    // matching.
131
                    if ( stream.match( /[ \t]+$/ ) ) {
132
                        return 'end-space';
133
                    } else if ( stream.match( /[^ \t$|ǂ‡]+/ ) || stream.match( /[ \t]+/ ) ) {
134
                        return;
135
                    }
136
                }
137
138
                if ( stream.eat( /[$|ǂ‡]/ ) ) {
139
                    var subfieldCode;
140
                    if ( ( subfieldCode = stream.eat( /[a-z0-9%]/ ) ) && stream.eat( ' ' ) ) {
141
                        state.subfieldCode = subfieldCode;
142
                        if ( state.seenSubfields[state.subfieldCode] && ( modeConfig.nonRepeatableSubfields[state.tagNumber] || {} )[state.subfieldCode] ) {
143
                            return 'bad-subfieldcode';
144
                        } else {
145
                            state.seenSubfields[state.subfieldCode] = true;
146
                            return 'subfieldcode';
147
                        }
148
                    }
149
                }
150
151
                if ( stream.pos < 11 && ( !stream.eol() || stream.pos == 8 ) ) {
152
                    stream.skipToEnd();
153
                    return 'error';
154
                }
155
            } else {
156
                // Match space at end of line
157
                if ( stream.match( /[ \t]+$/ ) ) {
158
                    return 'end-space';
159
                } else {
160
                    stream.match( /[ \t]+/ );
161
                }
162
163
                stream.match( /[^ \t]+/ );
164
                return;
165
            }
166
        }
167
    };
168
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js (+370 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
/**
21
 * Adapted and cleaned up from biblios.net, which is purportedly under the GPL.
22
 * Source: http://git.librarypolice.com/?p=biblios.git;a=blob_plain;f=plugins/marc21editor/marcrecord.js;hb=master
23
 *
24
 * ISO2709 import/export is cribbed from marcjs, which is under the MIT license.
25
 * Source: https://github.com/fredericd/marcjs/blob/master/lib/marcjs.js
26
 */
27
28
define( function() {
29
    var MARC = {};
30
31
    var _escape_map = {
32
        "<": "&lt;",
33
        "&": "&amp;",
34
        "\"": "&quot;"
35
    };
36
37
    function _escape(str) {
38
        return str.replace( /[<&"]/, function (c) { return _escape_map[c] } );
39
    }
40
41
    function _intpadded(i, digits) {
42
        i = i + '';
43
        while (i.length < digits) {
44
            i = '0' + i;
45
        }
46
        return i;
47
    }
48
49
    MARC.Record = function (fieldlist) {
50
        this._fieldlist = fieldlist || [];
51
    }
52
53
    $.extend( MARC.Record.prototype, {
54
        leader: function(val) {
55
            var field = this.field('000');
56
57
            if (val) {
58
                if (field) {
59
                    field.subfield( '@', val );
60
                } else {
61
                    field = new MARC.Field( '000', '', '', [ [ '@', val ] ] );
62
                    this.addFieldGrouped(field);
63
                }
64
            } else {
65
                return ( field && field.subfield('@') ) || '     nam a22     7a 4500';
66
            }
67
        },
68
69
        /**
70
         * If a tagnumber is given, returns all fields with that tagnumber.
71
         * Otherwise, returns all fields.
72
         */
73
        fields: function(fieldno) {
74
            if (!fieldno) return this._fieldlist;
75
76
            var results = [];
77
            for(var i=0; i<this._fieldlist.length; i++){
78
                if( this._fieldlist[i].tagnumber() == fieldno ) {
79
                    results.push(this._fieldlist[i]);
80
                }
81
            }
82
83
            return results;
84
        },
85
86
        /**
87
         * Returns the first field with the given tagnumber, or false.
88
         */
89
        field: function(fieldno) {
90
            for(var i=0; i<this._fieldlist.length; i++){
91
                if( this._fieldlist[i].tagnumber() == fieldno ) {
92
                    return this._fieldlist[i];
93
                }
94
            }
95
            return false;
96
        },
97
98
        /**
99
         * Adds the given MARC.Field to the record, at the end.
100
         */
101
        addField: function(field) {
102
            this._fieldlist.push(field);
103
            return true;
104
        },
105
106
        /**
107
         * Adds the given MARC.Field to the record, at the end of the matching
108
         * x00 group. If a record has a 100, 245 and 300 field, for instance, a
109
         * 260 field would be added after the 245 field.
110
         */
111
        addFieldGrouped: function(field) {
112
            for ( var i = this._fieldlist.length - 1; i >= 0; i-- ) {
113
                if ( this._fieldlist[i].tagnumber()[0] <= field.tagnumber()[0] ) {
114
                    this._fieldlist.splice(i+1, 0, field);
115
                    return true;
116
                }
117
            }
118
            this._fieldlist.push(field);
119
            return true;
120
        },
121
122
        /**
123
         * Removes the first field with the given tagnumber. Returns false if no
124
         * such field was found.
125
         */
126
        removeField: function(fieldno) {
127
            for(var i=0; i<this._fieldlist.length; i++){
128
                if( this._fieldlist[i].tagnumber() == fieldno ) {
129
                    this._fieldlist.splice(i, 1);
130
                    return true;
131
                }
132
            }
133
            return false;
134
        },
135
136
        /**
137
         * Check to see if this record contains a field with the given
138
         * tagnumber.
139
         */
140
        hasField: function(fieldno) {
141
            for(var i=0; i<this._fieldlist.length; i++){
142
                if( this._fieldlist[i].tagnumber() == fieldno ) {
143
                    return true;
144
                }
145
            }
146
            return false;
147
        },
148
149
        toXML: function() {
150
            var xml = '<record xmlns="http://www.loc.gov/MARC21/slim">';
151
            for(var i=0; i<this._fieldlist.length; i++){
152
                xml += this._fieldlist[i].toXML();
153
            }
154
            xml += '</record>';
155
            return xml;
156
        },
157
158
        /**
159
         * Truncates this record, and loads in the data from the given MARCXML
160
         * document.
161
         */
162
        loadMARCXML: function(xmldoc) {
163
            var record = this;
164
            record.xmlSource = xmldoc;
165
            this._fieldlist.length = 0;
166
            this.leader( $('leader', xmldoc).text() );
167
            $('controlfield', xmldoc).each( function(i) {
168
                val = $(this).text();
169
                tagnum = $(this).attr('tag');
170
                record._fieldlist.push( new MARC.Field(tagnum, '', '', [ [ '@', val ] ]) );
171
            });
172
            $('datafield', xmldoc).each(function(i) {
173
                var value = $(this).text();
174
                var tagnum = $(this).attr('tag');
175
                var ind1 = $(this).attr('ind1') || ' ';
176
                var ind2 = $(this).attr('ind2') || ' ';
177
                var subfields = new Array();
178
                $('subfield', this).each(function(j) {
179
                    var sfval = $(this).text();
180
                    var sfcode = $(this).attr('code');
181
                    subfields.push( [ sfcode, sfval ] );
182
                });
183
                record._fieldlist.push( new MARC.Field(tagnum, ind1, ind2, subfields) );
184
            });
185
        },
186
187
        toISO2709: function() {
188
            var FT = '\x1e', RT = '\x1d', DE = '\x1f';
189
            var directory = '',
190
                from = 0,
191
                chunks = ['', ''];
192
193
            $.each( this._fieldlist, function( undef, element ) {
194
                var chunk = '';
195
                var tag = element.tagnumber();
196
                if (tag == '000') {
197
                    return;
198
                } else if (tag < '010') {
199
                    chunk = element.subfields()[0][1];
200
                } else {
201
                    chunk = element.indicators().join('');
202
                    $.each( element.subfields(), function( undef, subfield ) {
203
                        chunk += DE + subfield[0] + subfield[1];
204
                    } );
205
                }
206
                chunk += FT;
207
                chunks.push(chunk);
208
                directory += _intpadded(tag,3) + _intpadded(chunk.length,4) + _intpadded(from,5);
209
                from += chunk.length;
210
            });
211
212
            chunks.push(RT);
213
            directory += FT;
214
            var offset = 24 + 12 * (this._fieldlist.length - 1) + 1;
215
            var length = offset + from + 1;
216
            var leader = this.leader();
217
            leader = _intpadded(length,5) + leader.substr(5,7) + _intpadded(offset,5) +
218
                leader.substr(17);
219
            chunks[0] = leader;
220
            chunks[1] = directory;
221
            return chunks.join('');
222
        },
223
224
        loadISO2709: function(data) {
225
            this._fieldlist.length = 0;
226
            this.leader(data.substr(0, 24));
227
            var directory_len = parseInt(data.substring(12, 17), 0) - 25,
228
                number_of_tag = directory_len / 12;
229
            for (var i = 0; i < number_of_tag; i++) {
230
                var off = 24 + i * 12,
231
                    tag = data.substring(off, off+3),
232
                    len = parseInt(data.substring(off+3, off+7), 0) - 1,
233
                    pos = parseInt(data.substring(off+7, off+12), 0) + 25 + directory_len,
234
                    value = data.substring(pos, pos+len);
235
                if ( parseInt(tag) < 10 ) {
236
                    this.addField( new MARC.Field( tag, '', '', [ [ '@', value ] ] ) );
237
                } else {
238
                    if ( value.indexOf('\x1F') ) { // There are some letters
239
                        var ind1 = value.substr(0, 1), ind2 = value.substr(1, 1);
240
                        var subfields = [];
241
242
                        $.each( value.substr(3).split('\x1f'), function( undef, v ) {
243
                            if (v.length < 2) return;
244
                            subfields.push([v.substr(0, 1), v.substr(1)]);
245
                        } );
246
247
                        this.addField( new MARC.Field( tag, ind1, ind2, subfields ) );
248
                    }
249
                }
250
            }
251
        }
252
    } );
253
254
    MARC.Field = function(tagnumber, indicator1, indicator2, subfields) {
255
        this._tagnumber = tagnumber;
256
        this._indicators = [ indicator1, indicator2 ];
257
        this._subfields = subfields;
258
    };
259
260
    $.extend( MARC.Field.prototype, {
261
        tagnumber: function() {
262
            return this._tagnumber;
263
        },
264
265
        isControlField: function() {
266
            return this._tagnumber < '010';
267
        },
268
269
        indicator: function(num, val) {
270
            if( val != null ) {
271
                this._indicators[num] = val;
272
            }
273
            return this._indicators[num];
274
        },
275
276
        indicators: function() {
277
            return this._indicators;
278
        },
279
280
        hasSubfield: function(code) {
281
            for(var i = 0; i<this._subfields.length; i++) {
282
                if( this._subfields[i][0] == code ) {
283
                    return true;
284
                }
285
            }
286
            return false;
287
        },
288
289
        removeSubfield: function(code) {
290
            for(var i = 0; i<this._subfields.length; i++) {
291
                if( this._subfields[i][0] == code ) {
292
                    this._subfields.splice(i,1);
293
                    return true;
294
                }
295
            }
296
            return false;
297
        },
298
299
        subfields: function() {
300
            return this._subfields;
301
        },
302
303
        addSubfield: function(sf) {
304
            this._subfields.push(sf);
305
            return true;
306
        },
307
308
        addSubfieldGrouped: function(sf) {
309
            function _kind( sc ) {
310
                if ( /[a-z]/.test( sc ) ) {
311
                    return 0;
312
                } else if ( /[0-9]/.test( sc ) ) {
313
                    return 1;
314
                } else {
315
                    return 2;
316
                }
317
            }
318
319
            for ( var i = this._subfields.length - 1; i >= 0; i-- ) {
320
                if ( i == 0 && _kind( sf[0] ) < _kind( this._subfields[i][0] ) ) {
321
                    this._subfields.splice( 0, 0, sf );
322
                    return true;
323
                } else if ( _kind( this._subfields[i][0] ) <= _kind( sf[0] )  ) {
324
                    this._subfields.splice( i + 1, 0, sf );
325
                    return true;
326
                }
327
            }
328
329
            this._subfields.push(sf);
330
            return true;
331
        },
332
333
        subfield: function(code, val) {
334
            var sf = '';
335
            for(var i = 0; i<this._subfields.length; i++) {
336
                if( this._subfields[i][0] == code ) {
337
                    sf = this._subfields[i];
338
                    if( val != null ) {
339
                        sf[1] = val;
340
                    }
341
                    return sf[1];
342
                }
343
            }
344
            return false;
345
        },
346
347
        toXML: function() {
348
            // decide if it's controlfield of datafield
349
            if( this._tagnumber == '000') {
350
                return '<leader>' + _escape( this._subfields[0][1] ) + '</leader>';
351
            } else if ( this._tagnumber < '010' ) {
352
                return '<controlfield tag="' + this._tagnumber + '">' + _escape( this._subfields[0][1] ) + '</controlfield>';
353
            } else {
354
                var result = '<datafield tag="' + this._tagnumber + '"';
355
                result += ' ind1="' + this._indicators[0] + '"';
356
                result += ' ind2="' + this._indicators[1] + '">';
357
                for( var i = 0; i< this._subfields.length; i++) {
358
                    result += '<subfield code="' + this._subfields[i][0] + '">';
359
                    result += _escape( this._subfields[i][1] );
360
                    result += '</subfield>';
361
                }
362
                result += '</datafield>';
363
364
                return result;
365
            }
366
        }
367
    } );
368
369
    return MARC;
370
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js (+49 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( function() {
21
    var Preferences = {
22
        Load: function( borrowernumber ) {
23
            if ( borrowernumber == null ) return;
24
25
            var saved_prefs;
26
            try {
27
                saved_prefs = JSON.parse( localStorage[ 'cateditor_preferences_' + borrowernumber ] );
28
            } catch (e) {}
29
30
            Preferences.user = $.extend( {
31
                // Preference defaults
32
                fieldWidgets: true,
33
                font: 'monospace',
34
                fontSize: '1em',
35
                macros: {},
36
                selected_search_targets: {},
37
            }, saved_prefs );
38
        },
39
40
        Save: function( borrowernumber ) {
41
            if ( !borrowernumber ) return;
42
            if ( !Preferences.user ) Preferences.Load(borrowernumber);
43
44
            localStorage[ 'cateditor_preferences_' + borrowernumber ] = JSON.stringify(Preferences.user);
45
        },
46
    };
47
48
    return Preferences;
49
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js (+38 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'module' ], function( module ) {
21
    var _allResources = [];
22
23
    var Resources = {
24
        GetAll: function() {
25
            return $.when.call( null, _allResources );
26
        }
27
    };
28
29
    function _res( name, deferred ) {
30
        Resources[name] = deferred;
31
        _allResources.push(deferred);
32
    }
33
34
    _res( 'marc21/xml/006', $.get( module.config().themelang + '/data/marc21_field_006.xml' ) );
35
    _res( 'marc21/xml/008', $.get( module.config().themelang + '/data/marc21_field_008.xml' ) );
36
37
    return Resources;
38
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js (+114 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'marc-record' ], function( MARC ) {
21
    var _options;
22
    var _records = {};
23
    var _last;
24
25
    var _pqfMapping = {
26
        author: '1=1004', // s=al',
27
        cn_dewey: '1=13',
28
        cn_lc: '1=16',
29
        date: '1=30', // r=r',
30
        isbn: '1=7',
31
        issn: '1=8',
32
        lccn: '1=9',
33
        local_number: '1=12',
34
        music_identifier: '1=51',
35
        standard_identifier: '1=1007',
36
        subject: '1=21', // s=al',
37
        term: '1=1016', // t=l,r s=al',
38
        title: '1=4', // s=al',
39
    }
40
41
    var Search = {
42
        Init: function( options ) {
43
            _options = options;
44
        },
45
        JoinTerms: function( terms ) {
46
            var q = '';
47
48
            $.each( terms, function( i, term ) {
49
                var term = '@attr ' + _pqfMapping[ term[0] ] + ' "' + term[1].replace( '"', '\\"' ) + '"'
50
51
                if ( q ) {
52
                    q = '@and ' + q + ' ' + term;
53
                } else {
54
                    q = term;
55
                }
56
            } );
57
58
            return q;
59
        },
60
        Run: function( servers, q, options ) {
61
            options = $.extend( {
62
                offset: 0,
63
                page_size: 20,
64
            }, _options, options );
65
66
            Search.includedServers = [];
67
            _records = {};
68
            _last = {
69
                servers: servers,
70
                q: q,
71
                options: options,
72
            };
73
74
            $.each( servers, function ( id, info ) {
75
                if ( info.checked ) Search.includedServers.push( id );
76
            } );
77
78
            $.get(
79
                '/cgi-bin/koha/svc/cataloguing/metasearch',
80
                {
81
                    q: q,
82
                    servers: Search.includedServers.join( ',' ),
83
                    offset: options.offset,
84
                    page_size: options.page_size,
85
                    sort_direction: options.sort_direction,
86
                    sort_key: options.sort_key,
87
                    resultset: options.resultset,
88
                }
89
            )
90
                .done( function( data ) {
91
                    _last.options.resultset = data.resultset;
92
                    $.each( data.hits, function( undef, hit ) {
93
                        var record = new MARC.Record();
94
                        record.loadMARCXML( hit.record );
95
                        hit.record = record;
96
                    } );
97
98
                    _options.onresults( data );
99
                } )
100
                .fail( function( error ) {
101
                    _options.onerror( error );
102
                } );
103
104
            return true;
105
        },
106
        Fetch: function( options ) {
107
            if ( !_last ) return;
108
            $.extend( _last.options, options );
109
            Search.Run( _last.servers, _last.q, _last.options );
110
        }
111
    };
112
113
    return Search;
114
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js (+106 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'marc-record' ], function( MARC ) {
21
    // Convert any characters for display
22
    function _sanitize( text ) {
23
        return text.replace( '$', '{dollar}' );
24
    }
25
26
    // Undo conversion
27
    function _desanitize( text ) {
28
        return text.replace( '{dollar}', '$' );
29
    }
30
    return {
31
        RecordToText: function( record ) {
32
            var lines = [];
33
            var fields = record.fields();
34
35
            for ( var i = 0; i < fields.length; i++ ) {
36
                var field = fields[i];
37
38
                if ( field.isControlField() ) {
39
                    lines.push( field.tagnumber() + ' ' + _sanitize( field.subfield( '@' ) ) );
40
                } else {
41
                    var result = [ field.tagnumber() + ' ' ];
42
43
                    result.push( field.indicator(0) == ' ' ? '_' : field.indicator(0), ' ' );
44
                    result.push( field.indicator(1) == ' ' ? '_' : field.indicator(1), ' ' );
45
46
                    $.each( field.subfields(), function( i, subfield ) {
47
                        result.push( '$' + subfield[0] + ' ' + _sanitize( subfield[1] ) );
48
                    } );
49
50
                    lines.push( result.join('') );
51
                }
52
            }
53
54
            return lines.join('\n');
55
        },
56
57
        TextToRecord: function( text ) {
58
            var record = new MARC.Record();
59
            var errors = [];
60
61
            $.each( text.split('\n'), function( i, line ) {
62
                var tagNumber = line.match( /^([A-Za-z0-9]{3}) / );
63
64
                if ( !tagNumber ) {
65
                    errors.push( { type: 'noTag', line: i } );
66
                    return;
67
                }
68
                tagNumber = tagNumber[1];
69
70
                if ( tagNumber < '010' ) {
71
                    var field = new MARC.Field( tagNumber, ' ', ' ', [ [ '@', _desanitize( line.substring( 4 ) ) ] ] );
72
                    field.sourceLine = i;
73
                    record.addField( field );
74
                } else {
75
                    var indicators = line.match( /^... ([0-9A-Za-z_]) ([0-9A-Za-z_])/ );
76
                    if ( !indicators ) {
77
                        errors.push( { type: 'noIndicators', line: i } );
78
                        return;
79
                    }
80
81
                    var field = new MARC.Field( tagNumber, ( indicators[1] == '_' ? ' ' : indicators[1] ), ( indicators[2] == '_' ? ' ' : indicators[2] ), [] );
82
83
                    var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
84
                    var match;
85
86
                    var subfields = [];
87
88
                    while ( ( match = matcher.exec(line) ) ) {
89
                        subfields.push( { code: match[1], ch: match.index } );
90
                    }
91
92
                    $.each( subfields, function( i, subfield ) {
93
                        var next = subfields[ i + 1 ];
94
95
                        field.addSubfield( [ subfield.code, _desanitize( line.substring( subfield.ch + 3, next ? next.ch : line.length ) ) ] );
96
                    } );
97
98
                    field.sourceLine = i;
99
                    record.addField( field );
100
                }
101
            } );
102
103
            return errors.length ? { errors: errors } : record;
104
        }
105
    };
106
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js (+310 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'resources' ], function( Resources ) {
21
    var _widgets = {};
22
23
    var Widget = {
24
        Register: function( tagfield, widget ) {
25
            _widgets[tagfield] = widget;
26
        },
27
28
        PadNum: function( number, length ) {
29
            var result = number.toString();
30
31
            while ( result.length < length ) result = '0' + result;
32
33
            return result;
34
        },
35
36
        PadString: function( result, length ) {
37
            while ( result.length < length ) result = ' ' + result;
38
39
            return result;
40
        },
41
42
        PadStringRight: function( result, length ) {
43
            result = '' + result;
44
            while ( result.length < length ) result += ' ';
45
46
            return result;
47
        },
48
49
        Base: {
50
            // Marker utils
51
            clearToText: function() {
52
                var range = this.mark.find();
53
                if ( this.text == null ) throw new Error('Tried to clear widget with no text');
54
                this.mark.doc.replaceRange( this.text, range.from, range.to, 'widget.clearToText' );
55
            },
56
57
            reCreate: function() {
58
                this.postCreate( this.node, this.mark );
59
            },
60
61
            // Fixed field utils
62
            bindFixed: function( sel, start, end ) {
63
                var $node = $( this.node ).find( sel );
64
                $node.val( this.getFixed( start, end ) );
65
66
                var widget = this;
67
                var $collapsed = $( '<span class="fixed-collapsed" title="' + $node.attr('title') + '">' + $node.val() + '</span>' ).insertAfter( $node );
68
69
                function show() {
70
                    $collapsed.hide();
71
                    $node.val( widget.getFixed( start, end ).replace(/\s+$/, '') );
72
                    $node.show();
73
                    $node[0].focus();
74
                }
75
76
                function hide() {
77
                    $node.hide();
78
                    $collapsed.text( Widget.PadStringRight( $node.val(), end - start ) ).show();
79
                }
80
81
                $node.on( 'change keyup', function() {
82
                    widget.setFixed( start, end, $node.val(), '+input' );
83
                } ).focus( show ).blur( hide );
84
85
                hide();
86
87
                $collapsed.click( show );
88
            },
89
90
            getFixed: function( start, end ) {
91
                return this.text.substring( start, end );
92
            },
93
94
            setFixed: function( start, end, value, source ) {
95
                this.setText( this.text.substring( 0, start ) + Widget.PadStringRight( value.toString().substr( 0, end - start ), end - start ) + this.text.substring( end ), source );
96
            },
97
98
            setText: function( text, source ) {
99
                if ( source == '+input' ) this.mark.doc.cm.addLineClass( this.mark.find().from.line, 'wrapper', 'modified-line' );
100
                this.text = text;
101
                this.editor.startNotify();
102
            },
103
104
            createFromXML: function( resourceId ) {
105
                var widget = this;
106
107
                Resources[resourceId].done( function( xml ) {
108
                    $(widget.node).find('.widget-loading').remove();
109
                    var $matSelect = $('<select class="material-select"></select>').appendTo(widget.node);
110
                    var $contents = $('<span class="material-contents"/>').appendTo(widget.node);
111
                    var materialInfo = {};
112
113
                    $('Tagfield', xml).children('Material').each( function() {
114
                        $matSelect.append( '<option value="' + $(this).attr('id') + '">' + $(this).attr('id') + ' - ' + $(this).children('name').text() + '</option>' );
115
116
                        materialInfo[ $(this).attr('id') ] = this;
117
                    } );
118
119
                    $matSelect.change( function() {
120
                        widget.loadXMLMaterial( materialInfo[ $matSelect.val() ] );
121
                    } ).change();
122
                } );
123
            },
124
125
            loadXMLMaterial: function( materialInfo ) {
126
                var $contents = $(this.node).children('.material-contents');
127
                $contents.empty();
128
129
                var widget = this;
130
131
                $(materialInfo).children('Position').each( function() {
132
                    var match = $(this).attr('pos').match(/(\d+)(?:-(\d+))?/);
133
                    if (!match) return;
134
135
                    var start = parseInt(match[1]);
136
                    var end = ( match[2] ? parseInt(match[2]) : start ) + 1;
137
                    var $input;
138
                    var $values = $(this).children('Value');
139
140
                    if ($values.length == 0) {
141
                        $contents.append( '<span title="' + $(this).children('name').text() + '">' + widget.getFixed(start, end) + '</span>' );
142
                        return;
143
                    }
144
145
                    if ( match[2] ) {
146
                        $input = $( '<input name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '" maxlength="' + (end - start) + '" />' );
147
                    } else {
148
                        $input = $( '<select name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '"></select>' );
149
150
                        $values.each( function() {
151
                            $input.append( '<option value="' + $(this).attr('code') + '">' + $(this).attr('code') + ' - ' + $(this).children('description').text() + '</option>' );
152
                        } );
153
                    }
154
155
                    $contents.append( $input );
156
                    widget.bindFixed( $input, start, end );
157
                } );
158
            },
159
160
            nodeChanged: function() {
161
                this.mark.changed();
162
                var widget = this;
163
164
                var $inputs = $(this.node).find('input, select');
165
                if ( !$inputs.length ) return;
166
167
                $inputs.off('keydown.marc-tab');
168
                var editor = widget.editor;
169
170
                $inputs.each( function( i ) {
171
                    $(this).on( 'keydown.marc-tab', function( e ) {
172
                        if ( e.which != 9 ) return; // Tab
173
174
                        var span = widget.mark.find();
175
                        var cur = editor.cm.getCursor();
176
177
                        if ( e.shiftKey ) {
178
                            if ( i > 0 ) {
179
                                $inputs.eq(i - 1).trigger( 'focus' );
180
                            } else {
181
                                editor.cm.setCursor( span.from );
182
                                // FIXME: ugly hack
183
                                editor.cm.options.extraKeys['Shift-Tab']( editor.cm );
184
                                editor.focus();
185
                            }
186
                        } else {
187
                            if ( i < $inputs.length - 1 ) {
188
                                $inputs.eq(i + 1).trigger( 'focus' );
189
                            } else {
190
                                editor.cm.setCursor( span.to );
191
                                editor.focus();
192
                            }
193
                        }
194
195
                        return false;
196
                    } );
197
                } );
198
            },
199
200
            // Template utils
201
            insertTemplate: function( sel ) {
202
                var wsOnly = /^\s*$/;
203
                $( sel ).contents().clone().each( function() {
204
                    if ( this.nodeType == Node.TEXT_NODE ) {
205
                        this.data = this.data.replace( /^\s+|\s+$/g, '' );
206
                    }
207
                } ).appendTo( this.node );
208
            },
209
        },
210
211
        ActivateAt: function( editor, cur, idx ) {
212
            var marks = editor.findMarksAt( cur );
213
            if ( !marks.length ) return false;
214
215
            var $input = $(marks[0].widget.node).find('input, select').eq(idx || 0);
216
            if ( !$input.length ) return false;
217
218
            $input.focus();
219
            return true;
220
        },
221
222
        Notify: function( editor ) {
223
            $.each( editor.cm.getAllMarks(), function( undef, mark ) {
224
                if ( mark.widget && mark.widget.notify ) mark.widget.notify();
225
            } );
226
        },
227
228
        UpdateLine: function( editor, line ) {
229
            var info = editor.getLineInfo( { line: line, ch: 0 } );
230
            var lineh = editor.cm.getLineHandle( line );
231
            if ( !lineh ) return;
232
233
            if ( !info ) {
234
                if ( lineh.markedSpans ) {
235
                    $.each( lineh.markedSpans, function ( undef, span ) {
236
                        var mark = span.marker;
237
                        if ( !mark.widget ) return;
238
239
                        mark.widget.clearToText();
240
                    } );
241
                }
242
                return;
243
            }
244
245
            var subfields = [];
246
247
            var end = editor.cm.getLine( line ).length;
248
            if ( info.tagNumber < '010' ) {
249
                if ( end >= 4 ) subfields.push( { code: '@', from: 4, to: end } );
250
            } else {
251
                for ( var i = 0; i < info.subfields.length; i++ ) {
252
                    var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end;
253
                    subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 3, to: next } );
254
                }
255
                // If not a fixed field, and we didn't find any subfields, we need to throw in the
256
                // '@' subfield so we can properly remove it
257
                if ( subfields.length == 0 ) subfields.push( { code: '@', from: 4, to: end } );
258
            }
259
260
            $.each( subfields, function ( undef, subfield ) {
261
                var id = info.tagNumber + subfield.code;
262
                var marks = editor.cm.findMarksAt( { line: line, ch: subfield.from } );
263
264
                if ( marks.length ) {
265
                    if ( marks[0].id == id ) {
266
                        return;
267
                    } else {
268
                        marks[0].widget.clearToText();
269
                    }
270
                }
271
272
                if ( !_widgets[id] ) return;
273
                var fullBase = $.extend( Object.create( Widget.Base ), _widgets[id] );
274
                var widget = Object.create( fullBase );
275
276
                if ( subfield.from == subfield.to ) {
277
                    editor.cm.replaceRange( widget.makeTemplate ? widget.makeTemplate() : '<empty>', { line: line, ch: subfield.from }, null, 'marcWidgetPrefill' );
278
                    return; // We'll do the actual work when the change event is triggered again
279
                }
280
281
                var text = editor.cm.getRange( { line: line, ch: subfield.from }, { line: line, ch: subfield.to } );
282
283
                widget.text = text;
284
                var node = widget.init();
285
286
                var mark = editor.cm.markText( { line: line, ch: subfield.from }, { line: line, ch: subfield.to }, {
287
                    atomic: true,
288
                    inclusiveLeft: false,
289
                    inclusiveRight: false,
290
                    replacedWith: node,
291
                } );
292
293
                mark.id = id;
294
                mark.widget = widget;
295
296
                widget.node = node;
297
                widget.mark = mark;
298
                widget.editor = editor;
299
300
                if ( widget.postCreate ) {
301
                    widget.postCreate();
302
                }
303
304
                widget.nodeChanged();
305
            } );
306
        },
307
    };
308
309
    return Widget;
310
} );
(-)a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css (+434 lines)
Line 0 Link Here
1
/*> Infrastructure */
2
body {
3
    padding: 0;
4
}
5
6
#loading {
7
    background-color: #FFF;
8
    cursor: wait;
9
    height: 100%;
10
    left: 0;
11
    opacity: .7;
12
    position: fixed;
13
    top: 0;
14
    width: 100%;
15
    z-index: 1000;
16
}
17
18
#loading div {
19
    background : transparent url(../../img/loading.gif) top left no-repeat;
20
    font-size : 175%;
21
    font-weight: bold;
22
    height: 2em;
23
    left: 50%;
24
    margin: -1em 0 0 -2.5em;
25
    padding-left : 50px;
26
    position: absolute;
27
    top: 50%;
28
    width: 15em;
29
}
30
31
#alerts-container {
32
    font-size: 12px;
33
}
34
35
#alerts-container h3 {
36
    font-size: inherit;
37
}
38
39
#alerts-container > ul {
40
    padding: 0;
41
}
42
43
#alerts-container > ul > li {
44
    border-bottom: 1px solid #DDD;
45
    display: block;
46
    padding: 4px 0;
47
}
48
49
#alerts-container > ul > li:first-child {
50
    padding-top: 0;
51
}
52
53
#alerts-container > ul > li:last-child {
54
    border-bottom: none;
55
    padding-bottom: 0;
56
}
57
58
/*> MARC editor */
59
#editor .CodeMirror {
60
    line-height: 1.2;
61
}
62
63
.cm-tagnumber {
64
    color: #080;
65
    font-weight: bold;
66
}
67
68
.cm-bad-tagnumber {
69
    color: #A20;
70
    font-weight: bold;
71
}
72
73
.cm-indicator {
74
    color: #884;
75
}
76
77
.cm-subfieldcode {
78
    background-color: #F4F4F4;
79
    color: #187848;
80
    border-radius: 3px 8px 8px 3px;
81
    border-right: 2px solid white;
82
    font-weight: bold;
83
    margin-right: -2px;
84
}
85
86
.cm-bad-subfieldcode {
87
    background-color: #FFD9D9;
88
    color: #482828;
89
    border-radius: 3px 8px 8px 3px;
90
    font-weight: bold;
91
}
92
93
.cm-end-space {
94
    background-color: #DDDDBB;
95
}
96
97
#editor .modified-line-gutter {
98
    width: 10px;
99
}
100
101
#editor .modified-line {
102
    background: #F8F8F8;
103
    border-left: 5px solid black;
104
    margin-left: -10px;
105
    padding-left: 5px;
106
}
107
108
#editor .CodeMirror-gutters {
109
    background: transparent;
110
    border-right: none;
111
}
112
113
/*> MARC editor widgets */
114
115
#editor .subfield-widget {
116
    color: #538200;
117
    border: solid 2px #538200;
118
    border-radius: 6px;
119
    font-family: inherit;
120
    line-height: 2.75;
121
    margin: 3px 0;
122
    padding: 4px;
123
}
124
125
#editor .subfield-widget select, #editor .subfield-widget input {
126
    height: 1.5em;
127
    vertical-align: middle;
128
}
129
130
#editor .subfield-widget select:focus {
131
    outline: 2px #83A230 solid;
132
}
133
134
#editor .fixed-widget input {
135
    width: 4em;
136
}
137
138
#editor .fixed-widget select {
139
    width: 3em;
140
}
141
142
#editor .fixed-widget .material-select {
143
    width: 4.5em;
144
    margin-right: .5em;
145
}
146
147
#editor .fixed-collapsed {
148
    display: inline-block;
149
    margin: 0 .25em;
150
    text-align: center;
151
    text-decoration: underline;
152
}
153
154
#editor .hidden-widget {
155
    color: #999999;
156
    border: solid 2px #AAAAAA;
157
    line-height: 2;
158
    padding: 2px;
159
}
160
161
.structure-error {
162
    background: #FFEEEE;
163
    font-size: 0.9em;
164
    line-height: 1.5;
165
    margin: .5em;
166
    padding: 0 .5em;
167
}
168
169
.structure-error i {
170
    vertical-align: text-bottom;
171
}
172
173
#statusbar {
174
    background-color: #F4F8F9;
175
    border: solid 2px #b9d8d9;
176
    border-bottom-style: none;
177
    border-radius: 6px 6px 0 0;
178
    height: 18px;
179
    margin-bottom: -32px;
180
    overflow: auto;
181
    padding: 4px;
182
    padding-bottom: 0;
183
}
184
185
#statusbar #status-tag-info, #statusbar #status-subfield-info {
186
    float: left;
187
    overflow: hidden;
188
    padding-right: 2%;
189
    width: 48%;
190
}
191
192
#record-info .label {
193
    float: none;
194
}
195
196
#record-info .label + span {
197
    display: block;
198
    padding-left: 1em;
199
}
200
201
/*> Search */
202
203
#advanced-search-ui, #search-results-ui, #macro-ui {
204
    padding: 5px;
205
    width: 90%;
206
}
207
208
.modal-body {
209
    max-height: none;
210
    padding: 0;
211
}
212
213
#quicksearch-overlay {
214
    background: rgba(255, 255, 255, .9);
215
    border: 2px solid #CC8877;
216
    border-radius: 5px;
217
    -moz-box-sizing: border-box;
218
    -webkit-box-sizing: border-box;
219
    box-sizing: border-box;
220
    color: #664444;
221
    position: relative;
222
    vertical-align: middle;
223
}
224
225
#quicksearch-overlay h3 {
226
    font-size: 1.5em%;
227
    margin: 0;
228
    text-align: center;
229
    padding: 50px 5px;
230
}
231
232
#quicksearch-overlay p {
233
    bottom: 0;
234
    font-size: .8em;
235
    overflow: hidden;
236
    padding: 8px 15px;
237
    position: absolute;
238
    text-align: center;
239
}
240
241
#quicksearch input, #quicksearch a {
242
    font-size: 1.2em;
243
    padding: 3px 0;
244
    width: 96%; /* I have no idea why this is necessary */
245
}
246
247
#show-advanced-search {
248
    display: block;
249
    margin-top: .3em;
250
}
251
252
#advanced-search-fields {
253
    -moz-column-width: 26em;
254
    -webkit-column-width: 26em;
255
    column-width: 26em;
256
    margin: 0;
257
    padding: 0;
258
}
259
260
#advanced-search-fields li {
261
    display: block;
262
    list-style-type: none;
263
}
264
265
#advanced-search-fields label {
266
    display: inline-block;
267
    font-weight: bold;
268
    padding: 1em 1em 1em 0;
269
    width: 10em;
270
    text-align: right;
271
}
272
273
#advanced-search-fields input {
274
    display: inline-block;
275
    margin: 0px auto;
276
    width: 14em;
277
}
278
279
.icon-loading {
280
    display: inline-block;
281
    height: 16px;
282
    width: 16px;
283
    background: transparent url("../../img/spinner-small.gif") top left no-repeat;
284
    padding: -1px;
285
    vertical-align: text-top;
286
}
287
288
/*> Search results */
289
290
#search-serversinfo li {
291
    list-style-type: none;
292
}
293
294
#search-serversinfo .search-toggle-server {
295
    margin-right: 5px;
296
}
297
298
#searchresults table {
299
    width: 100%;
300
}
301
302
.sourcecol {
303
    width: 50px;
304
}
305
306
.results-info {
307
    height: 100px;
308
    overflow: auto;
309
}
310
311
.toolscol {
312
    padding: 0;
313
    width: 100px;
314
}
315
316
.toolscol ul {
317
    margin: 0;
318
    padding: 0;
319
}
320
321
#searchresults .toolscol li {
322
    list-style-type: none;
323
    list-style-image: none;
324
}
325
326
.toolscol a {
327
    border-bottom: 1px solid #BCBCBC;
328
    display: block;
329
    padding: 0 1em;
330
    line-height: 24px;
331
}
332
333
.marccol {
334
    font-family: monospace;
335
    height: auto;
336
    white-space: pre-wrap;
337
}
338
339
#searchresults {
340
    position: relative;
341
}
342
343
#search-overlay {
344
    background: white;
345
    bottom: 0;
346
    font-size: 2em;
347
    left: 0;
348
    opacity: .7;
349
    padding: 2em;
350
    position: absolute;
351
    right: 0;
352
    text-align: center;
353
    top: 0;
354
    z-index: 9001;
355
}
356
357
/*> Macros */
358
359
#macro-ui .CodeMirror {
360
    width: 100%;
361
}
362
363
#macro-save-message {
364
    color: #666;
365
    font-size: 13px;
366
    float: right;
367
    line-height: 26px;
368
}
369
370
#macro-list > li {
371
    border: 2px solid #F0F0F0;
372
    border-radius: 6px;
373
    display: block;
374
    font-size: 115%;
375
}
376
377
#macro-list > li + li {
378
    margin-top: -2px;
379
}
380
381
#macro-list .active {
382
    background: #EDF4F6;
383
    border-color: none;
384
}
385
386
#macro-list a {
387
    display: block;
388
    padding: 6px;
389
}
390
391
#macro-list a:focus {
392
    outline: none;
393
}
394
395
.macro-info {
396
    background-color: #F4F4F4;
397
    display: none;
398
    margin: 0;
399
    padding: 10px;
400
    text-align: right;
401
}
402
403
.macro-info li {
404
    color: #666;
405
    font-size: 75%;
406
    list-style-type: none;
407
}
408
409
.macro-info .label {
410
    clear: left;
411
    font-weight: bold;
412
    float: left;
413
}
414
415
#macro-list .active .macro-info {
416
    display: block;
417
}
418
419
.btn-toolbar label, .btn-toolbar select {
420
    font-size: 13px;
421
    vertical-align: middle;
422
}
423
424
.btn-toolbar label {
425
    margin-left: 1em;
426
}
427
428
.btn-toolbar select {
429
    padding: 2px;
430
}
431
432
#macro-editor .CodeMirror {
433
    height: 100%;
434
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css (-3 / +8 lines)
Lines 418-430 dd { Link Here
418
	font-weight : normal;
418
	font-weight : normal;
419
}
419
}
420
420
421
div#toolbar {
421
.btn-toolbar {
422
       background-color : #EDF4F6;
422
       background-color : #EDF4F6;
423
     padding: 5px 5px 5px 5px;
423
     padding: 5px 5px 5px 5px;
424
      border-radius: 5px 5px 0 0;
424
      border-radius: 5px 5px 0 0;
425
    border: 1px solid #E6F0F2;
425
    border: 1px solid #E6F0F2;
426
}
426
}
427
427
428
.btn-toolbar .yui-menu-button button,
429
.btn-toolbar .yui-button-button button {
430
	line-height : 1.7em;
431
}
432
428
ul.toolbar {
433
ul.toolbar {
429
	padding-left : 0;
434
	padding-left : 0;
430
}
435
}
Lines 2412-2419 video { Link Here
2412
    background-position:-48px -166px;
2417
    background-position:-48px -166px;
2413
}
2418
}
2414
2419
2415
#toolbar .btn,
2420
.btn-toolbar .btn,
2416
#toolbar .dropdown-menu {
2421
.btn-toolbar .dropdown-menu {
2417
    font-size: 13px;
2422
    font-size: 13px;
2418
}
2423
}
2419
a.btn:link,
2424
a.btn:link,
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc (+1051 lines)
Line 0 Link Here
1
<script src="/intranet-tmpl/lib/codemirror/codemirror-compressed.js"></script>
2
<script src="/intranet-tmpl/lib/filesaver.js"></script>
3
<script src="/intranet-tmpl/lib/koha/cateditor/marc-mode.js"></script>
4
<script src="/intranet-tmpl/lib/require.js"></script>
5
<script>
6
require.config( {
7
    baseUrl: '/intranet-tmpl/lib/koha/cateditor/',
8
    config: {
9
        resources: {
10
            themelang: '[% themelang %]',
11
        },
12
    },
13
    waitSeconds: 30,
14
} );
15
</script>
16
17
[% IF marcflavour == 'MARC21' %]
18
[% PROCESS 'cateditor-widgets-marc21.inc' %]
19
[% ELSE %]
20
<script>var editorWidgets = {};</script>
21
[% END %]
22
23
<script>
24
require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) {
25
    var z3950Servers = {
26
        'koha:biblioserver': {
27
            name: _("Local catalog"),
28
            recordtype: 'biblio',
29
            checked: false,
30
        },
31
        [%- FOREACH server = z3950_servers -%]
32
            [% server.id %]: {
33
                name: '[% server.servername %]',
34
                recordtype: '[% server.recordtype %]',
35
                checked: [% server.checked ? 'true' : 'false' %],
36
            },
37
        [%- END -%]
38
    };
39
40
    // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config
41
    var z3950Labels = [
42
		[ "local_number", _("Local number") ],
43
		[ "title", _("Title") ],
44
		[ "series", _("Series title") ],
45
		[ "author", _("Author") ],
46
		[ "lccn", _("LCCN") ],
47
		[ "isbn", _("ISBN") ],
48
		[ "issn", _("ISSN") ],
49
		[ "medium", _("Medium") ],
50
		[ "edition", _("Edition") ],
51
		[ "notes", _("Notes") ],
52
    ];
53
54
    var state = {
55
        backend: '',
56
        saveBackend: 'catalog',
57
        recordID: undefined
58
    };
59
60
    var editor;
61
    var macroEditor;
62
63
    function makeAuthorisedValueWidgets( frameworkCode ) {
64
        $.each( KohaBackend.GetAllTagsInfo( frameworkCode ), function( tag, tagInfo ) {
65
            $.each( tagInfo.subfields, function( subfield, subfieldInfo ) {
66
                if ( !subfieldInfo.authorised_value ) return;
67
                var authvals = KohaBackend.GetAuthorisedValues( subfieldInfo.authorised_value );
68
                if ( !authvals ) return;
69
70
                var defaultvalue = subfield.defaultvalue || authvals[0].value;
71
72
                Widget.Register( tag + subfield, {
73
                    init: function() {
74
                        var $result = $( '<span class="subfield-widget"></span>' );
75
76
                        return $result[0];
77
                    },
78
                    postCreate: function() {
79
                        this.setText( defaultvalue );
80
81
                        $( '<select></select>' ).appendTo( this.node );
82
                        var $node = $( this.node ).find( 'select' );
83
                        $.each( authvals, function( undef, authval ) {
84
                            $node.append( '<option value="' + authval.value + '"' + (authval.value == defaultvalue ? ' selected="selected"' : '') + '>' + authval.lib + '</option>' );
85
                        } );
86
                        $node.val( this.text );
87
88
                        $node.change( $.proxy( function() {
89
                            this.setText( $node.val() );
90
                        }, this ) );
91
                    },
92
                    makeTemplate: function() {
93
                        return defaultvalue;
94
                    },
95
                } );
96
            } );
97
        } );
98
    }
99
100
    function bindGlobalKeys() {
101
        shortcut.add( 'ctrl+s', function(event) {
102
            $( '#save-record' ).click();
103
104
            event.preventDefault();
105
        } );
106
107
        shortcut.add( 'alt+ctrl+k', function(event) {
108
            if ( Search.IsAvailable() ) $( '#search-by-keywords' ).focus();
109
110
            return false;
111
        } );
112
113
        shortcut.add( 'alt+ctrl+a', function(event) {
114
            if ( Search.IsAvailable() ) $( '#search-by-author' ).focus();
115
116
            return false;
117
        } );
118
119
        shortcut.add( 'alt+ctrl+i', function(event) {
120
            if ( Search.IsAvailable() ) $( '#search-by-isbn' ).focus();
121
122
            return false;
123
        } );
124
125
        shortcut.add( 'alt+ctrl+t', function(event) {
126
            if ( Search.IsAvailable() ) $( '#search-by-title' ).focus();
127
128
            return false;
129
        } );
130
131
        shortcut.add( 'ctrl+h', function() {
132
            var field = editor.getCurrentField();
133
134
            if ( !field ) return;
135
136
            window.open( getFieldHelpURL( field.tag ) );
137
        } );
138
139
        $('#quicksearch .search-box').each( function() {
140
            shortcut.add( 'enter', $.proxy( function() {
141
                var terms = [];
142
143
                $('#quicksearch .search-box').each( function() {
144
                    if ( !this.value ) return;
145
146
                    terms.push( [ $(this).data('qualifier'), this.value ] );
147
                } );
148
149
                if ( !terms.length ) return;
150
151
                if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
152
                    $("#search-overlay").show();
153
                    showResultsBox();
154
                }
155
156
                return false;
157
            }, this), { target: this, type: 'keypress' } );
158
        } );
159
    }
160
161
    function getFieldHelpURL( tag ) {
162
        [% IF ( marcflavour == 'MARC21' ) %]
163
            if ( tag == '000' ) {
164
                return "http://www.loc.gov/marc/bibliographic/bdleader.html";
165
            } else if ( tag < '900' ) {
166
                return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html";
167
            } else {
168
                return "http://www.loc.gov/marc/bibliographic/bd9xx.html";
169
            }
170
        [% ELSIF ( marcflavour == 'UNIMARC' ) %]
171
            /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but
172
               seems to be the only version available that can be linked to per tag.  More recent
173
               versions of the UNIMARC standard are available on the IFLA website only as
174
               PDFs!
175
            */
176
            if ( tag == '000' ) {
177
               return  "http://archive.ifla.org/VI/3/p1996-1/uni.htm";
178
            } else {
179
                var first = tag[0];
180
                var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#";
181
                if ( first == '0' ) url += "b";
182
                if ( first != '9' ) url += field;
183
184
                return url;
185
            }
186
        [% END %]
187
    }
188
189
    // Record loading
190
    var backends = {
191
       'new': {
192
            titleForRecord: _("Editing new record"),
193
            get: function( id, callback ) {
194
                record = new MARC.Record();
195
                KohaBackend.FillRecord( '', record );
196
197
                callback( record );
198
            },
199
        },
200
        'new-full': {
201
            titleForRecord: _("Editing new full record"),
202
            get: function( id, callback ) {
203
                record = new MARC.Record();
204
                KohaBackend.FillRecord( '', record, true );
205
206
                callback( record );
207
            },
208
        },
209
        'catalog': {
210
            titleForRecord: _("Editing catalog record #{ID}"),
211
            links: [
212
                { title: _("view"), href: "/cgi-bin/koha/catalogue/detail.pl?biblionumber={ID}" },
213
                { title: _("edit items"), href: "/cgi-bin/koha/cataloguing/additem.pl?biblionumber={ID}" },
214
            ],
215
            saveLabel: _("Save to catalog"),
216
            get: function( id, callback ) {
217
                if ( !id ) return false;
218
219
                KohaBackend.GetRecord( id, callback );
220
            },
221
            save: function( id, record, done ) {
222
                function finishCb( data ) {
223
                    done( { error: data.error, newRecord: data.marcxml && data.marcxml[0], newId: data.biblionumber && [ 'catalog', data.biblionumber ] } );
224
                }
225
226
                if ( id ) {
227
                    KohaBackend.SaveRecord( id, record, finishCb );
228
                } else {
229
                    KohaBackend.CreateRecord( record, finishCb );
230
                }
231
            }
232
        },
233
        'iso2709': {
234
            saveLabel: _("Save as ISO2709 (.mrc) file"),
235
            save: function( id, record, done ) {
236
                saveAs( new Blob( [record.toISO2709()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.mrc' );
237
238
                done( {} );
239
            }
240
        },
241
        'marcxml': {
242
            saveLabel: _("Save as MARCXML (.xml) file"),
243
            save: function( id, record, done ) {
244
                saveAs( new Blob( [record.toXML()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.xml' );
245
246
                done( {} );
247
            }
248
        },
249
        'search': {
250
            titleForRecord: _("Editing search result"),
251
            get: function( id, callback ) {
252
                if ( !id ) return false;
253
                if ( !backends.search.records[ id ] ) {
254
                    callback( { error: _( "Invalid record" ) } );
255
                    return false;
256
                }
257
258
                callback( backends.search.records[ id ] );
259
            },
260
            records: {},
261
        },
262
    };
263
264
    function setSource(parts) {
265
        state.backend = parts[0];
266
        state.recordID = parts[1];
267
        state.canSave = backends[ state.backend ].save != null;
268
        state.saveBackend = state.canSave ? state.backend : 'catalog';
269
270
        var backend = backends[state.backend];
271
272
        document.location.hash = '#' + parts[0] + '/' + parts[1];
273
274
        $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
275
276
        $.each( backend.links || [], function( i, link ) {
277
            $('#title').append(' <a target="_blank" href="' + link.href.replace( '{ID}', parts[1] ) + '">(' + link.title + ')</a>' );
278
        } );
279
        $( 'title', document.head ).html( _("Koha &rsaquo; Cataloging &rsaquo; ") + backend.titleForRecord.replace( '{ID}', parts[1] ) );
280
        $('#save-record span').text( backends[ state.saveBackend ].saveLabel );
281
    }
282
283
    function saveRecord( recid, editor, callback ) {
284
        var parts = recid.split('/');
285
        if ( parts.length != 2 ) return false;
286
287
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
288
289
        editor.removeErrors();
290
        var record = editor.getRecord();
291
292
        if ( record.errors ) {
293
            state.saving = false;
294
            callback( { error: 'syntax', errors: record.errors } );
295
            return;
296
        }
297
298
        var errors = KohaBackend.ValidateRecord( '', record );
299
        if ( errors.length ) {
300
            state.saving = false;
301
            callback( { error: 'invalid', errors: errors } );
302
            return;
303
        }
304
305
        backends[ parts[0] ].save( parts[1], record, function(data) {
306
            state.saving = false;
307
308
            if (data.newRecord) {
309
                var record = new MARC.Record();
310
                record.loadMARCXML(data.newRecord);
311
                editor.displayRecord( record );
312
            }
313
314
            if (data.newId) {
315
                setSource(data.newId);
316
            } else {
317
                setSource( [ state.backend, state.recordID ] );
318
            }
319
320
            if (callback) callback( data );
321
        } );
322
    }
323
324
    function loadRecord( recid, editor, callback ) {
325
        var parts = recid.split('/');
326
        if ( parts.length != 2 ) return false;
327
328
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
329
330
        backends[ parts[0] ].get( parts[1], function( record ) {
331
            if ( !record.error ) {
332
                editor.displayRecord( record );
333
                editor.focus();
334
            }
335
336
            if (callback) callback(record);
337
        } );
338
339
        return true;
340
    }
341
342
    function openRecord( recid, editor, callback ) {
343
        return loadRecord( recid, editor, function ( record ) {
344
            setSource( recid.split('/') );
345
346
            if (callback) callback( record );
347
        } );
348
    }
349
350
    // Search functions
351
    function showAdvancedSearch() {
352
        $('#advanced-search-servers').empty();
353
        $.each( z3950Servers, function( server_id, server ) {
354
            $('#advanced-search-servers').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</li>' );
355
        } );
356
        $('#advanced-search-ui').modal('show');
357
    }
358
359
    function startAdvancedSearch() {
360
        var terms = [];
361
362
        $('#advanced-search-ui .search-box').each( function() {
363
            if ( !this.value ) return;
364
365
            terms.push( [ $(this).data('qualifier'), this.value ] );
366
        } );
367
368
        if ( !terms.length ) return;
369
370
        if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
371
            $('#advanced-search-ui').modal('hide');
372
            $("#search-overlay").show();
373
            showResultsBox();
374
        }
375
    }
376
377
    function showResultsBox(data) {
378
        $('#search-top-pages, #search-bottom-pages').find('.pagination').empty();
379
        $('#searchresults thead tr').empty();
380
        $('#searchresults tbody').empty();
381
        $('#search-serversinfo').empty().append('<li>' + _("Loading...") + '</li>');
382
        $('#search-results-ui').modal('show');
383
    }
384
385
    function showSearchSorting( sort_key, sort_direction ) {
386
        var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]');
387
        $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' );
388
389
        if ( sort_direction == 'asc' ) {
390
            direction = 'asc';
391
            $th.attr( 'class', 'sorting_asc' );
392
        } else {
393
            direction = 'desc';
394
            $th.attr( 'class', 'sorting_desc' );
395
        }
396
    }
397
398
    function showSearchResults( editor, data ) {
399
        backends.search.records = {};
400
401
        $('#searchresults thead tr').empty();
402
        $('#searchresults tbody').empty();
403
        $('#search-serversinfo').empty();
404
405
        $.each( z3950Servers, function( server_id, server ) {
406
            var num_fetched = data.num_fetched[server_id];
407
408
            if ( data.errors[server_id] ) {
409
                num_fetched = data.errors[server_id];
410
            } else if ( num_fetched == null ) {
411
                num_fetched = '-';
412
            } else if ( num_fetched < data.num_hits[server_id] ) {
413
                num_fetched += '+';
414
            }
415
416
            $('#search-serversinfo').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + ' (' + num_fetched + ')' + '</li>' );
417
        } );
418
419
        var seenColumns = {};
420
421
        $.each( data.hits, function( undef, hit ) {
422
            $.each( hit.metadata, function(key) {
423
                seenColumns[key] = true;
424
            } );
425
        } );
426
427
        $('#searchresults thead tr').append('<th>' + _("Source") + '</th>');
428
429
        $.each( z3950Labels, function( undef, label ) {
430
            if ( seenColumns[ label[0] ] ) {
431
                $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' );
432
            }
433
        } );
434
435
        showSearchSorting( data.sort_key, data.sort_direction );
436
437
        $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
438
439
        $.each( data.hits, function( undef, hit ) {
440
            backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
441
            hit.id = 'search/' + hit.server + ':' + hit.index;
442
443
            var result = '<tr>';
444
            result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
445
446
            $.each( z3950Labels, function( undef, label ) {
447
                if ( !seenColumns[ label[0] ] ) return;
448
449
                if ( hit.metadata[ label[0] ] ) {
450
                    result += '<td class="infocol">' + hit.metadata[ label[0] ] + '</td>';
451
                } else {
452
                    result += '<td class="infocol">&nbsp;</td>';
453
                }
454
            } );
455
456
            result += '<td class="toolscol"><ul><li><a href="#" class="marc-link">' + _("View MARC") + '</a></li>';
457
            result += '<li><a href="#" class="open-link">' + _("Import") + '</a></li>';
458
            if ( state.canSave ) result += '<li><a href="#" class="substitute-link" title="' + _("Replace the current record's contents") + '">' + _("Substitute") + '</a></li>';
459
            result += '</ul></td></tr>';
460
461
            var $tr = $( result );
462
            $tr.find( '.marc-link' ).click( function() {
463
                var $info_columns = $tr.find( '.infocol' );
464
                var $marc_column = $tr.find( '.marccol' );
465
466
                if ( !$marc_column.length ) {
467
                    $marc_column = $( '<td class="marccol" colspan="' + $info_columns.length + '"></td>' ).insertAfter( $info_columns.eq(-1) ).hide();
468
                    CodeMirror.runMode( TextMARC.RecordToText( hit.record ), 'marc', $marc_column[0] );
469
                }
470
471
                if ( $marc_column.is(':visible') ) {
472
                    $tr.find('.marc-link').text( _("View MARC") );
473
                    $info_columns.show();
474
                    $marc_column.hide();
475
                } else {
476
                    $tr.find('.marc-link').text( _("Hide MARC") );
477
                    $marc_column.show();
478
                    $info_columns.hide();
479
                }
480
481
                return false;
482
            } );
483
            $tr.find( '.open-link' ).click( function() {
484
                $( '#search-results-ui' ).modal('hide');
485
                openRecord( hit.id, editor );
486
487
                return false;
488
            } );
489
            $tr.find( '.substitute-link' ).click( function() {
490
                $( '#search-results-ui' ).modal('hide');
491
                loadRecord( hit.id, editor );
492
493
                return false;
494
            } );
495
            $('#searchresults tbody').append( $tr );
496
        } );
497
498
        var pages = [];
499
        var cur_page = data.offset / data.page_size;
500
        var max_page = Math.ceil( data.total_fetched / data.page_size ) - 1;
501
502
        if ( cur_page != 0 ) {
503
            pages.push( '<li><a class="search-nav" href="#" data-offset="' + (data.offset - data.page_size) + '">&laquo; ' + _("Previous") + '</a></li>' );
504
        }
505
506
        for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) {
507
            if ( page == cur_page ) {
508
                pages.push( ' <li class="active"><a href="#">' + ( page + 1 ) + '</a></li>' );
509
            } else {
510
                pages.push( ' <li><a class="search-nav" href="#" data-offset="' + ( page * data.page_size ) + '">' + ( page + 1 ) + '</a></li>' );
511
            }
512
        }
513
514
        if ( cur_page < max_page ) {
515
            pages.push( ' <li><a class="search-nav" href="#" data-offset="' + (data.offset + data.page_size) + '">' + _("Next") + ' &raquo;</a></li>' );
516
        }
517
518
        if ( pages.length > 1 ) $( '#search-top-pages, #search-bottom-pages' ).find( '.pagination' ).html( '<ul>' + pages.join( '' ) + '</ul>');
519
520
        var $overlay = $('#search-overlay');
521
        $overlay.find('span').text(_("Loading"));
522
        $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
523
524
        if ( data.activeclients ) {
525
            $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } );
526
            $overlay.show();
527
        } else {
528
            $overlay.find('.bar').css( { display: 'block', width: '100%' } );
529
            $overlay.fadeOut();
530
        }
531
    }
532
533
    function invalidateSearchResults() {
534
        var $overlay = $('#search-overlay');
535
        $overlay.find('span').text(_("Search expired, please try again"));
536
        $overlay.find('.bar').css( { display: 'none' } );
537
        $overlay.show();
538
    }
539
540
    function handleSearchError(error) {
541
        if (error.code == 1) {
542
            invalidateSearchResults();
543
            Search.Reconnect();
544
        } else {
545
            humanMsg.displayMsg( _("<h3>Internal search error</h3>") + '<p>' + error + '</p>' + _("<p>Please <b>refresh</b> the page and try again."), { className: 'humanError' } );
546
        }
547
    }
548
549
    function handleSearchInitError(error) {
550
        $('#quicksearch-overlay').fadeIn().find('p').text(error);
551
    }
552
553
    // Preference functions
554
    function showPreference( pref ) {
555
        var value = Preferences.user[pref];
556
557
        switch (pref) {
558
            case 'fieldWidgets':
559
                $( '#set-field-widgets' ).text( value ? _("Show fields verbatim") : _("Show helpers for fixed and coded fields") );
560
                break;
561
            case 'font':
562
                $( '#editor .CodeMirror' ).css( { fontFamily: value } );
563
                editor.refresh();
564
                break;
565
            case 'fontSize':
566
                $( '#editor .CodeMirror' ).css( { fontSize: value } );
567
                editor.refresh();
568
                break;
569
            case 'macros':
570
                showSavedMacros();
571
                break;
572
            case 'selected_search_targets':
573
                $.each( z3950Servers, function( server_id, server ) {
574
                    var saved_val = Preferences.user.selected_search_targets[server_id];
575
576
                    if ( saved_val != null ) server.checked = saved_val;
577
                } );
578
                break;
579
        }
580
    }
581
582
    function bindPreference( editor, pref ) {
583
        function _addHandler( sel, event, handler ) {
584
            $( sel ).on( event, function (e) {
585
                e.preventDefault();
586
                handler( e, Preferences.user[pref] );
587
                Preferences.Save( [% USER_INFO.0.borrowernumber %] );
588
                showPreference(pref);
589
            } );
590
        }
591
592
        switch (pref) {
593
            case 'fieldWidgets':
594
                _addHandler( '#set-field-widgets', 'click', function( e, oldValue ) {
595
                    editor.setUseWidgets( Preferences.user.fieldWidgets = !Preferences.user.fieldWidgets );
596
                } );
597
                break;
598
            case 'font':
599
                _addHandler( '#prefs-menu .set-font', 'click', function( e, oldValue ) {
600
                    Preferences.user.font = $( e.target ).css( 'font-family' );
601
                } );
602
                break;
603
            case 'fontSize':
604
                _addHandler( '#prefs-menu .set-fontSize', 'click', function( e, oldValue ) {
605
                    Preferences.user.fontSize = $( e.target ).css( 'font-size' );
606
                } );
607
                break;
608
            case 'selected_search_targets':
609
                $( document ).on( 'change', 'input.search-toggle-server', function() {
610
                    var server_id = $( this ).parent().data('server-id');
611
                    Preferences.user.selected_search_targets[server_id] = this.checked;
612
                    Preferences.Save( [% USER_INFO.0.borrowernumber %] );
613
                } );
614
                break;
615
        }
616
    }
617
618
    function displayPreferences( editor ) {
619
        $.each( Preferences.user, function( pref, value ) {
620
            showPreference( pref );
621
            bindPreference( editor, pref );
622
        } );
623
    }
624
625
    //> Macro functions
626
    function loadMacro( name ) {
627
        $( '#macro-list li' ).removeClass( 'active' );
628
        macroEditor.activeMacro = name;
629
630
        if ( !name ) {
631
            macroEditor.setValue( '' );
632
            return;
633
        }
634
635
        $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
636
        var macro = Preferences.user.macros[name];
637
        macroEditor.setValue( macro.contents );
638
        $( '#macro-format' ).val( macro.format || 'its' );
639
        if ( macro.history ) macroEditor.setHistory( macro.history );
640
    }
641
642
    function storeMacro( name, macro ) {
643
        if ( macro ) {
644
            Preferences.user.macros[name] = macro;
645
        } else {
646
            delete Preferences.user.macros[name];
647
        }
648
649
        Preferences.Save( [% USER_INFO.0.borrowernumber %] );
650
    }
651
652
    function showSavedMacros( macros ) {
653
        var scrollTop = $('#macro-list').scrollTop();
654
        $( '#macro-list' ).empty();
655
        var macro_list = $.map( Preferences.user.macros, function( macro, name ) {
656
            return $.extend( { name: name }, macro );
657
        } );
658
        macro_list.sort( function( a, b ) {
659
            return a.name.localeCompare(b.name);
660
        } );
661
        $.each( macro_list, function( undef, macro ) {
662
            var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' );
663
            $li.click( function() {
664
                loadMacro(macro.name);
665
                return false;
666
            } );
667
            if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' );
668
            var modified = macro.modified && new Date(macro.modified);
669
            $li.find( '.macro-info' ).append(
670
                '<li><span class="label">' + _("Last changed:") + '</span>' +
671
                ( modified ? modified.toLocaleFormat() : _("never") ) + '</li>'
672
            );
673
            $('#macro-list').append($li);
674
        } );
675
        var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' );
676
        $new_li.click( function() {
677
            // TODO: make this a bit less retro
678
            var name = prompt(_("Please enter the name for the new macro:"));
679
            if (!name) return;
680
681
            if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } );
682
            showSavedMacros();
683
            loadMacro( name );
684
        } );
685
        $('#macro-list').append($new_li);
686
        $('#macro-list').scrollTop(scrollTop);
687
    }
688
689
    function saveMacro() {
690
        var name = macroEditor.activeMacro;
691
692
        if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
693
694
        macroEditor.savedGeneration = macroEditor.changeGeneration();
695
        storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } );
696
        $('#macro-save-message').text(_("Saved"));
697
        showSavedMacros();
698
    }
699
700
    $(document).ready( function() {
701
        // Editor setup
702
        editor = new MARCEditor( {
703
            onCursorActivity: function() {
704
                $('#status-tag-info').empty();
705
                $('#status-subfield-info').empty();
706
707
                var field = editor.getCurrentField();
708
                var cur = editor.getCursor();
709
710
                if ( !field ) return;
711
712
                var taginfo = KohaBackend.GetTagInfo( '', field.tag );
713
                $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
714
715
                if ( taginfo ) {
716
                    $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
717
718
                    var subfield = field.getSubfieldAt( cur.ch );
719
                    if ( !subfield ) return;
720
721
                    var subfieldinfo = taginfo.subfields[ subfield.code ];
722
                    $('#status-subfield-info').html( '<strong>$' + subfield.code + ':</strong> ' );
723
724
                    if ( subfieldinfo ) {
725
                        $('#status-subfield-info').append( subfieldinfo.lib );
726
                    } else {
727
                        $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
728
                    }
729
                } else {
730
                    $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
731
                }
732
            },
733
            position: function (elt) { $(elt).insertAfter('#toolbar') },
734
        } );
735
736
        macroEditor = CodeMirror(
737
            $('#macro-editor')[0],
738
            {
739
                mode: 'null',
740
                lineNumbers: true,
741
            }
742
        );
743
744
        // Automatically detect resizes and change the height of the editor and position of modals.
745
        var resizeTimer = null;
746
        $( window ).resize( function() {
747
            if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
748
                resizeTimer = null;
749
750
                var pos = $('#editor .CodeMirror').position();
751
                $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
752
753
                $('.modal-body').each( function() {
754
                    $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
755
                } );
756
            }, 100);
757
758
            $("#advanced-search-ui, #search-results-ui, #macro-ui").css( {
759
                marginLeft: function() {
760
                    return -($(this).width() / 2);
761
                }
762
            } );
763
764
        } ).resize();
765
766
        var saveableBackends = [];
767
        $.each( backends, function( id, backend ) {
768
            if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] );
769
        } );
770
        saveableBackends.sort();
771
        $.each( saveableBackends, function( undef, backend ) {
772
            $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
773
        } );
774
775
        var macro_format_list = $.map( Macros.formats, function( format, name ) {
776
            return $.extend( { name: name }, format );
777
        } );
778
        macro_format_list.sort( function( a, b ) {
779
            return a.description.localeCompare(b.description);
780
        } );
781
        $.each( macro_format_list, function() {
782
            $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
783
        } );
784
785
        // Click bindings
786
        $( '#save-record, #save-dropdown a' ).click( function() {
787
            $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
788
789
            function finishCb(result) {
790
                if ( result.error == 'syntax' ) {
791
                    humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } );
792
                } else if ( result.error == 'invalid' ) {
793
                    humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } );
794
                } else if ( !result.error ) {
795
                    humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } );
796
                }
797
798
                $.each( result.errors || [], function( undef, error ) {
799
                    switch ( error.type ) {
800
                        case 'noTag':
801
                            editor.addError( error.line, _("Invalid tag number") );
802
                            break;
803
                        case 'noIndicators':
804
                            editor.addError( error.line, _("Invalid indicators") );
805
                            break;
806
                        case 'missingTag':
807
                            editor.addError( null, _("Missing mandatory tag: ") + error.tag );
808
                            break;
809
                        case 'missingSubfield':
810
                            if ( error.subfield == '@' ) {
811
                                editor.addError( error.line, _("Missing control field contents") );
812
                            } else {
813
                                editor.addError( error.line, _("Missing mandatory subfield: $") + error.subfield );
814
                            }
815
                            break;
816
                        case 'unrepeatableTag':
817
                            editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") );
818
                            break;
819
                        case 'unrepeatableSubfield':
820
                            editor.addError( error.line, _("Subfield $") + error.subfield + _(" cannot be repeated") );
821
                            break;
822
                    }
823
                } );
824
825
                $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' );
826
827
                if ( result.error ) {
828
                    // Reset backend info
829
                    setSource( [ state.backend, state.recordID ] );
830
                }
831
            }
832
833
            var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
834
            if ( state.backend == backend ) {
835
                saveRecord( backend + '/' + state.recordID, editor, finishCb );
836
            } else {
837
                saveRecord( backend + '/', editor, finishCb );
838
            }
839
840
            return false;
841
        } );
842
843
        $('#import-records').click( function() {
844
            $('#import-records-input')
845
                .off('change')
846
                .change( function() {
847
                    if ( !this.files || !this.files.length ) return;
848
849
                    var file = this.files[0];
850
                    var reader = new FileReader();
851
852
                    reader.onload = function() {
853
                        var record = new MARC.Record();
854
855
                        if ( /\.mrc$/.test( file.name ) ) {
856
                            record.loadISO2709( reader.result );
857
                        } else if ( /\.xml$/.test( file.name ) ) {
858
                            record.loadMARCXML( reader.result );
859
                        } else {
860
                            humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
861
                            return;
862
                        }
863
864
                        editor.displayRecord( record );
865
                    };
866
867
                    reader.readAsText( file );
868
                } )
869
                .click();
870
871
            return false;
872
        } );
873
874
        $('#open-macros').click( function() {
875
            $('#macro-ui').modal('show');
876
877
            return false;
878
        } );
879
880
        $('#run-macro').click( function() {
881
            var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
882
883
            if ( !result.errors.length ) {
884
                $('#macro-ui').modal('hide');
885
                return false;
886
            }
887
888
            var errors = [];
889
            $.each( result.errors, function() {
890
                var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> ';
891
892
                switch ( this.error ) {
893
                    case 'failed': error += _("failed to run"); break;
894
                    case 'unrecognized': error += _("unrecognized command"); break;
895
                }
896
897
                errors.push(error);
898
            } );
899
900
            humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } );
901
902
            return false;
903
        } );
904
905
        $('#delete-macro').click( function() {
906
            if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return;
907
908
            storeMacro( macroEditor.activeMacro, undefined );
909
            showSavedMacros();
910
            loadMacro( undefined );
911
912
            return false;
913
        } );
914
915
        var saveTimeout;
916
        macroEditor.on( 'change', function( cm, change ) {
917
            $('#macro-save-message').empty();
918
            if ( change.origin == 'setValue' ) return;
919
920
            if ( saveTimeout ) clearTimeout( saveTimeout );
921
            saveTimeout = setTimeout( function() {
922
                saveMacro();
923
924
                saveTimeout = null;
925
            }, 500 );
926
        } );
927
928
        $( '#switch-editor' ).click( function() {
929
            if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return;
930
931
            $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } );
932
933
            if ( state.backend == 'catalog' ) {
934
                window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID;
935
            } else if ( state.backend == 'new' ) {
936
                window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl';
937
            } else {
938
                humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } );
939
            }
940
        } );
941
942
        $( '#show-advanced-search' ).click( function() {
943
            showAdvancedSearch();
944
945
            return false;
946
        } );
947
948
        $('#advanced-search').submit( function() {
949
            startAdvancedSearch();
950
951
            return false;
952
        } );
953
954
        $( document ).on( 'click', 'a.search-nav', function() {
955
            $("#search-overlay").show();
956
            Search.Fetch( { offset: $( this ).data( 'offset' ) } );
957
            return false;
958
        });
959
960
        $( document ).on( 'click', 'th[data-sort-label]', function() {
961
            $("#search-overlay").show();
962
            var direction;
963
964
            if ( $( this ).hasClass( 'sorting_asc' ) ) {
965
                direction = 'desc';
966
            } else {
967
                direction = 'asc';
968
            }
969
970
            showSearchSorting( $( this ).data( 'sort-label' ), direction );
971
972
            Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } );
973
            return false;
974
        });
975
976
        $( document ).on( 'change', 'input.search-toggle-server', function() {
977
            var server = z3950Servers[ $( this ).parent().data('server-id') ];
978
            server.checked = this.checked;
979
980
            if ( $('#search-results-ui').is( ':visible' ) ) {
981
                $("#search-overlay").show();
982
                Search.Fetch();
983
            }
984
        } );
985
986
        // Key bindings
987
        bindGlobalKeys();
988
989
        // Setup UI
990
        $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
991
            $(this).modal({ show: false });
992
        } );
993
994
        var $quicksearch = $('#quicksearch fieldset');
995
        $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({
996
            position: 'absolute',
997
            top: $quicksearch.offset().top,
998
            left: $quicksearch.offset().left,
999
            height: $quicksearch.outerHeight(),
1000
            width: $quicksearch.outerWidth(),
1001
        }).appendTo(document.body).hide();
1002
1003
        var prevAlerts = [];
1004
        humanMsg.logMsg = function(msg, options) {
1005
            $('#show-alerts').popover('hide');
1006
            prevAlerts.unshift('<li>' + msg + '</li>');
1007
            prevAlerts.splice(5, 999); // Truncate old messages
1008
        };
1009
1010
        $('#show-alerts').popover({
1011
            html: true,
1012
            placement: 'bottom',
1013
            content: function() {
1014
                return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1015
            },
1016
        });
1017
        $('#new-record' ).click( function() {
1018
            openRecord( 'new/', editor );
1019
            return false;
1020
        } );
1021
1022
        // Start editor
1023
        Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1024
        displayPreferences(editor);
1025
        makeAuthorisedValueWidgets( '' );
1026
        Search.Init( {
1027
            page_size: 20,
1028
            onresults: function(data) { showSearchResults( editor, data ) },
1029
            onerror: handleSearchError,
1030
        } );
1031
1032
        function finishCb( data ) {
1033
            if ( data.error ) openRecord( 'new/', editor, finishCb );
1034
1035
            Resources.GetAll().done( function() {
1036
                $("#loading").hide();
1037
                editor.focus();
1038
            } );
1039
        }
1040
1041
        if ( "[% auth_forwarded_hash %]" ) {
1042
            document.location.hash = "[% auth_forwarded_hash %]";
1043
        }
1044
1045
        if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1046
            openRecord( 'new/', editor, finishCb );
1047
        }
1048
    } );
1049
} )();
1050
1051
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc (+158 lines)
Line 0 Link Here
1
<div id="editor-widget-templates" style="display:none">
2
    <div id="widget-leader">
3
        Leader:&nbsp;<span title="Record length (autogenerated)">#####</span>
4
        <select name="f5" title="Record status">
5
            <option value="a">a - Increase in encoding level</option>
6
            <option value="c">c - Corrected or revised</option>
7
            <option value="d">d - Deleted</option>
8
            <option value="n">n - New</option>
9
            <option value="p">p - Increase in encoding level from prepublication</option>
10
        </select>
11
        <select name="f6" title="Type of record">
12
            <option value="a">a - Language material</option>
13
            <option value="c">c - Notated music</option>
14
            <option value="d">d - Manuscript notated music</option>
15
            <option value="e">e - Cartographic material</option>
16
            <option value="f">f - Manuscript cartographic material</option>
17
            <option value="g">g - Projected medium</option>
18
            <option value="i">i - Nonmusical sound recording</option>
19
            <option value="j">j - Musical sound recording</option>
20
            <option value="k">k - Two-dimensional nonprojectable graphic</option>
21
            <option value="m">m - Computer file</option>
22
            <option value="o">o - Kit</option>
23
            <option value="p">p - Mixed materials</option>
24
            <option value="r">r - Three-dimensional artifact or naturally occurring object</option>
25
            <option value="t">t - Manuscript language material</option>
26
        </select>
27
        <select name="f7" title="Bibliographic level">
28
            <option value="a">a - Monographic component part</option>
29
            <option value="b">b - Serial component part</option>
30
            <option value="c">c - Collection</option>
31
            <option value="d">d - Subunit</option>
32
            <option value="i">i - Integrating resource</option>
33
            <option value="m">m - Monograph/item</option>
34
            <option value="s">s - Serial</option>
35
        </select>
36
        <select name="f8" title="Type of control">
37
                <option value=" ">_ - No specific type</option>
38
                <option value="a">a - Archival</option>
39
        </select>
40
        <span title="Encoding (forced Unicode)">a</span>
41
        <span title="Indicator/subfield lengths">22</span>
42
        <span title="Data base address (autogenerated)">#####</span>
43
        <select name="f17" title="Encoding level">
44
            <option value=" ">_ - Full level</option>
45
            <option value="1">1 - Full level, material not examined</option>
46
            <option value="2">2 - Less-than-full level, material not examined</option>
47
            <option value="3">3 - Abbreviated level</option>
48
            <option value="4">4 - Core level</option>
49
            <option value="5">5 - Partial (preliminary) level</option>
50
            <option value="7">7 - Minimal level</option>
51
            <option value="8">8 - Prepublication level</option>
52
            <option value="u">u - Unknown</option>
53
            <option value="z">z - Not applicable</option>
54
        </select>
55
        <select name="f18" title="Descriptive cataloging form">
56
            <option value=" ">_ - Non-ISBD</option>
57
            <option value="a">a - AACR 2</option>
58
            <option value="c">c - ISBD punctuation omitted</option>
59
            <option value="i">i - ISBD punctuation included</option>
60
            <option value="u">u - Unknown</option>
61
        </select>
62
        <select name="f19" title="Multipart record resource level">
63
            <option value=" ">_ - Not specified or not applicable</option>
64
            <option value="a">a - Set</option>
65
            <option value="b">b - Part with independent title</option>
66
            <option value="c">c - Part with dependent title</option>
67
        </select>
68
        <span title="Length of directory elements">4500</span>
69
    </div>
70
</div>
71
72
<script>
73
74
/**
75
 * Each widget should provide one to three methods:
76
 *   init( text ): Returns the DOM node for this widget.
77
 *   postCreate( node, mark ): Optional, called once the mark has been created
78
 *                             and the node shown. Bind event handlers here.
79
 *   makeTemplate(): Optional, should return some sane default contents for a
80
 *                   newly created field/subfield. '<empty>' will be used if this
81
 *                   method is unset.
82
 *
83
 * Following the Koha convention, control fields are defined as tags with a
84
 * single subfield, '@'.
85
 */
86
87
require( [ 'widget' ], function( Widget ) {
88
    Widget.Register( '000@', {
89
        makeTemplate: function() {
90
            return '     nam a22     7a 4500';
91
        },
92
        init: function() {
93
            var $result = $( '<span class="subfield-widget fixed-widget"></span>' );
94
95
            return $result[0];
96
        },
97
        postCreate: function() {
98
            // Clear the length and directory start fields; these are unnecessary for MARCXML and will be filled in upon USMARC export
99
            this.setFixed( 0, 5, '     ' );
100
            this.setFixed( 9, 17, 'a22     ' );
101
            this.setFixed( 20, 24, '4500' );
102
103
            this.insertTemplate( '#widget-leader' );
104
105
            this.bindFixed( '[name=f5]', 5, 6 );
106
            this.bindFixed( '[name=f6]', 6, 7 );
107
            this.bindFixed( '[name=f7]', 7, 8 );
108
            this.bindFixed( '[name=f8]', 8, 9 );
109
            this.bindFixed( '[name=f17]', 17, 18 );
110
            this.bindFixed( '[name=f18]', 18, 19 );
111
            this.bindFixed( '[name=f19]', 19, 20 );
112
        },
113
    } );
114
115
    Widget.Register( '005@', {
116
        init: function() {
117
            var $result = $( '<span class="subfield-widget fixed-widget">Updated: </span>' );
118
119
            return $result[0];
120
        },
121
        postCreate: function( node, mark ) {
122
            var parts = this.text.match( /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d)/ );
123
124
            if ( parts ) {
125
                var dateVal = new Date(
126
                    parseInt( parts[1] ), // Year
127
                    parseInt( parts[2] ) - 1, // Month (0-11)
128
                    parseInt( parts[3] ), // Day
129
                    parseInt( parts[4] ), // Hour
130
                    parseInt( parts[5] ), // Minute
131
                    parseInt( parts[6] ), // Second
132
                    parseInt( parts[7] ) * 100 // Millisecond
133
                );
134
135
                $( this.node ).append( dateVal.toLocaleString() );
136
            } else {
137
                $( this.node ).append( '<span class="hint">unset</span>' );
138
            }
139
        }
140
    } );
141
142
    Widget.Register( '008@', {
143
        makeTemplate: function() {
144
            var now = new Date();
145
            return Widget.PadNum( now.getYear() % 100, 2 ) + Widget.PadNum( now.getMonth() + 1, 2 ) + Widget.PadNum( now.getDate(), 2 ) + "b        xxu||||| |||| 00| 0 [% DefaultLanguageField008 %] d";
146
        },
147
        init: function() {
148
            var $result = $( '<span class="subfield-widget fixed-widget">Fixed data:<span class="hint widget-loading">Loading...</span></span>' );
149
150
            return $result[0];
151
        },
152
        postCreate: function( node, mark ) {
153
            this.createFromXML( 'marc21/xml/008' );
154
        }
155
    } );
156
} );
157
158
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc (+1 lines)
Lines 7-12 Link Here
7
[% IF ( circulation ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Circulation" href="/cgi-bin/koha/admin/preferences.pl?tab=circulation">Circulation</a></li>
7
[% IF ( circulation ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Circulation" href="/cgi-bin/koha/admin/preferences.pl?tab=circulation">Circulation</a></li>
8
[% IF ( enhanced_content ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Enhanced content settings" href="/cgi-bin/koha/admin/preferences.pl?tab=enhanced_content">Enhanced content</a></li>
8
[% IF ( enhanced_content ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Enhanced content settings" href="/cgi-bin/koha/admin/preferences.pl?tab=enhanced_content">Enhanced content</a></li>
9
[% IF ( i18n_l10n ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Internationalization and localization" href="/cgi-bin/koha/admin/preferences.pl?tab=i18n_l10n">I18N/L10N</a></li>
9
[% IF ( i18n_l10n ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Internationalization and localization" href="/cgi-bin/koha/admin/preferences.pl?tab=i18n_l10n">I18N/L10N</a></li>
10
[% IF ( labs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Experimental features" href="/cgi-bin/koha/admin/preferences.pl?tab=labs">Labs</a></li>
10
[% IF ( local_use ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/admin/systempreferences.pl">Local use</a></li>
11
[% IF ( local_use ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/admin/systempreferences.pl">Local use</a></li>
11
[% IF ( logs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Transaction logs" href="/cgi-bin/koha/admin/preferences.pl?tab=logs">Logs</a></li>
12
[% IF ( logs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Transaction logs" href="/cgi-bin/koha/admin/preferences.pl?tab=logs">Logs</a></li>
12
[% IF ( opac ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Online Public Access Catalog" href="/cgi-bin/koha/admin/preferences.pl?tab=opac">OPAC</a></li>
13
[% IF ( opac ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Online Public Access Catalog" href="/cgi-bin/koha/admin/preferences.pl?tab=opac">OPAC</a></li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/labs.pref (+11 lines)
Line 0 Link Here
1
Labs:
2
    All:
3
        -
4
            - pref: EnableAdvancedCatalogingEditor
5
              default: 0
6
              choices:
7
                  yes: Enable
8
                  no: "Don't enable"
9
            - the advanced cataloging editor.
10
            - "<br/> NOTE:"
11
            - This feature is currently experimental, and may have bugs that cause corruption of records. Please help us test it and report any bugs, but do so at your own risk.
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt (-1 / +23 lines)
Lines 105-111 Link Here
105
            redirect("just_save", tab);
105
            redirect("just_save", tab);
106
            return false;
106
            return false;
107
        });
107
        });
108
    });
108
109
        $( '#switcheditor' ).click( function() {
110
            var breedingid = [% breedingid || "null" %];
111
112
            if ( !confirm( breedingid ? _("This record cannot be transferred to the advanced editor. Continue?") : _("Any changes will not be saved. Continue?") ) ) return false;
113
114
            $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } );
115
116
            var biblionumber = [% biblionumber || "null" %];
117
118
            if ( biblionumber ) {
119
                window.location = '/cgi-bin/koha/cataloguing/editor.pl#catalog:' + biblionumber;
120
            } else {
121
                window.location = '/cgi-bin/koha/cataloguing/editor.pl';
122
            }
123
124
            return false;
125
        } );
126
127
	});
109
128
110
function redirect(dest){
129
function redirect(dest){
111
    $("#redirect").attr("value",dest);
130
    $("#redirect").attr("value",dest);
Lines 462-467 function Changefwk(FwkList) { Link Here
462
481
463
    [% UNLESS (circborrowernumber) %][%# Hide in fast cataloging %]
482
    [% UNLESS (circborrowernumber) %][%# Hide in fast cataloging %]
464
        <div class="btn-group"><a class="btn btn-small" href="#" id="z3950search"><i class="icon-search"></i> Z39.50/SRU search</a></div>
483
        <div class="btn-group"><a class="btn btn-small" href="#" id="z3950search"><i class="icon-search"></i> Z39.50/SRU search</a></div>
484
        [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %]
485
            <div class="btn-group"><a href="#" id="switcheditor" class="btn btn-small">Switch to advanced editor</a></div>
486
        [% END %]
465
        [% IF (biblionumber) %]
487
        [% IF (biblionumber) %]
466
            [% IF ( BiblioDefaultViewmarc ) %]
488
            [% IF ( BiblioDefaultViewmarc ) %]
467
                <div class="btn-group">
489
                <div class="btn-group">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt (+9 lines)
Lines 1-3 Link Here
1
[% USE Koha %]
1
[% INCLUDE 'doc-head-open.inc' %]
2
[% INCLUDE 'doc-head-open.inc' %]
2
<title>Koha &rsaquo; Cataloging</title>
3
<title>Koha &rsaquo; Cataloging</title>
3
[% INCLUDE 'greybox.inc' %]
4
[% INCLUDE 'greybox.inc' %]
Lines 22-27 Link Here
22
            e.preventDefault();
23
            e.preventDefault();
23
            MergeItems();
24
            MergeItems();
24
        });
25
        });
26
27
        $("#useadvanced").click(function(){
28
            $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } );
29
            return true;
30
        });
25
     });
31
     });
26
32
27
    /* this function open a popup to search on z3950 server.  */
33
    /* this function open a popup to search on z3950 server.  */
Lines 70-75 Link Here
70
76
71
[% IF ( CAN_user_editcatalogue_edit_catalogue ) %]
77
[% IF ( CAN_user_editcatalogue_edit_catalogue ) %]
72
  <div id="toolbar" class="btn-toolbar">
78
  <div id="toolbar" class="btn-toolbar">
79
        [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %]
80
            <a id="useadvanced" href="/cgi-bin/koha/cataloguing/editor.pl" class="btn btn-small"><i class="icon-edit"></i> Advanced editor</a>
81
        [% END %]
73
        <div class="btn-group">
82
        <div class="btn-group">
74
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-plus"></i> New record <span class="caret"></span></button>
83
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-plus"></i> New record <span class="caret"></span></button>
75
            <ul class="dropdown-menu">
84
            <ul class="dropdown-menu">
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt (+234 lines)
Line 0 Link Here
1
[% INCLUDE 'doc-head-open.inc' %]
2
<title>Koha &rsaquo; Cataloging &rsaquo; Editor</title>
3
[% INCLUDE 'doc-head-close.inc' %]
4
<link rel="stylesheet" href="[% themelang %]/css/cateditor.css" />
5
<link rel="stylesheet" href="[% themelang %]/css/datatables.css" />
6
<link rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" />
7
<link rel="stylesheet" href="[% themelang %]/css/humanmsg.css" />
8
<script src="[% interface %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script>
9
[% IF ( bidi ) %]
10
   <link rel="stylesheet" type="text/css" href="[% themelang %]/css/right-to-left.css" />
11
[% END %]
12
</head>
13
<body id="cat_addbiblio" class="cat">
14
15
   <div id="loading">
16
       <div>Loading, please wait...</div>
17
   </div>
18
19
[% INCLUDE 'header.inc' %]
20
21
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a> &rsaquo; Editor</div>
22
23
<div id="doc3" class="yui-t2">
24
<div id="bd">
25
26
<h1 id="title">Cataloging editor</h1>
27
28
<div id="yui-main"><div class="yui-b">
29
30
<div id="editor">
31
    <input id="import-records-input" type="file" style="display: none">
32
    <div id="toolbar" class="btn-toolbar">
33
        <button class="btn btn-small" id="new-record" title="Open fresh record"><i class="icon-plus"></i> <span>New record</span></button>
34
        <div class="btn-group">
35
            <button class="btn btn-small" id="save-record" title="Save current record (Ctrl-S)"><i class="icon-hdd"></i> <span>Save</span></button>
36
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
37
            <span class="caret"></span>
38
            </button>
39
            <ul class="dropdown-menu" id="save-dropdown">
40
            </ul>
41
        </div>
42
        <button class="btn btn-small" id="import-records" title="Import an ISO2709 or MARCXML record"><i class="icon-upload"></i> <span>Import record...</span></button>
43
        <button class="btn btn-small" id="open-macros" title="Run and edit macros"><i class="icon-play"></i> <span>Macros...</span></button>
44
        <div class="btn-group">
45
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i> Settings <span class="caret"></span></button>
46
            <ul id="prefs-menu" class="dropdown-menu">
47
                <li><a id="switch-editor" href="#">Switch to basic editor</a></li>
48
                <li><a id="set-field-widgets" href="#"></a></li>
49
                <li class="divider"></li>
50
                <li><a class="set-fontSize" style="font-size: .92em" href="#">Small text</a></li>
51
                <li><a class="set-fontSize" style="font-size: 1em" href="#">Normal text</a></li>
52
                <li><a class="set-fontSize" style="font-size: 1.08em" href="#">Large text</a></li>
53
                <li><a class="set-fontSize" style="font-size: 1.18em" href="#">Huge text</a></li>
54
                <li class="divider"></li>
55
                <li><a class="set-font" style="font-family: monospace" href="#">Default font</a></li>
56
                <li><a class="set-font" style="font-family: 'Courier New'" href="#">Courier New</a></li>
57
                <li><a class="set-font" style="font-family: peep" href="#">peep</a></li>
58
            </ul>
59
        </div>
60
        <button class="btn btn-small" id="show-alerts" title="Previous alerts"><i class="icon-info-sign"></i> Alerts <span class="caret"></span></button>
61
    </div>
62
    [%# CodeMirror instance will be inserted here %]
63
    <div id="statusbar">
64
        <div id="status-tag-info">
65
        </div>
66
        <div id="status-subfield-info">
67
        </div>
68
    </div>
69
</div>
70
71
</div></div>
72
73
<div class="yui-b" id="sidebar">
74
75
<h3>Search</h3>
76
<form id="quicksearch">
77
    <fieldset class="brief">
78
    <ol>
79
        <li><label for="search-by-keywords">Keywords:</label></li>
80
        <li><input class="search-box" data-qualifier="term" id="search-by-keywords" placeholder="(Ctrl-Alt-K)" /></li>
81
        <li><label for="search-by-author">Author:</label></li>
82
        <li><input class="search-box" data-qualifier="author" id="search-by-author" placeholder="(Ctrl-Alt-A)" /></li>
83
        <li><label for="search-by-isbn">ISBN:</label></li>
84
        <li><input class="search-box" data-qualifier="isbn" id="search-by-isbn" placeholder="(Ctrl-Alt-I)" /></li>
85
        <li><label for="search-by-title">Title:</label></li>
86
        <li><input class="search-box" data-qualifier="title" id="search-by-title" placeholder="(Ctrl-Alt-T)" /></li>
87
        <li><a href="#" id="show-advanced-search" title="Show advanced search (Ctrl-Alt-S)">Advanced &raquo;</a></li>
88
    </fieldset>
89
</form>
90
91
</div>
92
93
</div>
94
</div>
95
96
<div id="advanced-search-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="advanced-search-title" aria-hidden="true">
97
98
<div class="modal-header">
99
    <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
100
    <h3 id="advanced-search-title">Advanced search</h3>
101
</div>
102
103
<form id="advanced-search" class="modal-body">
104
    <div class="span3">
105
        <div id="search-facets">
106
            <ul>
107
                <li>Servers:<ul id="advanced-search-servers"></ul></li>
108
            </ul>
109
        </div>
110
    </div>
111
    <div class="span9">
112
        <div id="toolbar" class="btn-toolbar">
113
            <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button>
114
            <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button>
115
        </div>
116
        <ul id="advanced-search-fields">
117
            <li>
118
                <label for="advanced-search-by-author">Author:</label>
119
                <input class="search-box" data-qualifier="author" id="advanced-search-by-author" />
120
            </li>
121
            <li>
122
                <label for="advanced-search-by-control-number">Control number:</label>
123
                <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" />
124
            </li>
125
            <li>
126
                <label for="advanced-search-by-dewey">Dewey number:</label>
127
                <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" />
128
            </li>
129
            <li>
130
                <label for="advanced-search-by-isbn">ISBN:</label>
131
                <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" />
132
            </li>
133
            <li>
134
                <label for="advanced-search-by-issn">ISSN:</label>
135
                <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" />
136
            </li>
137
            <li>
138
                <label for="advanced-search-by-lccn">LCCN:</label>
139
                <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" />
140
            </li>
141
            <li>
142
                <label for="advanced-search-by-lc-number">LC call number:</label>
143
                <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" />
144
            </li>
145
            <li>
146
                <label for="advanced-search-by-publisher-number">Publisher number:</label>
147
                <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" />
148
            </li>
149
            <li>
150
                <label for="advanced-search-by-standard-number">Standard number:</label>
151
                <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" />
152
            </li>
153
            <li>
154
                <label for="advanced-search-by-subject">Subject:</label>
155
                <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" />
156
            </li>
157
            <li>
158
                <label for="advanced-search-by-publication-date">Publication date:</label>
159
                <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" />
160
            </li>
161
            <li>
162
                <label for="advanced-search-by-title">Title:</label>
163
                <input class="search-box" data-qualifier="title" id="advanced-search-by-title" />
164
            </li>
165
        </ul>
166
    </div>
167
</form>
168
169
</div>
170
171
<div id="search-results-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="search-results-title" aria-hidden="true">
172
173
<div class="modal-header">
174
    <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
175
    <h3 id="search-results-title">Results</h3>
176
</div>
177
178
<div class="modal-body row-fluid">
179
    <div class="span3">
180
        <div id="search-facets">
181
            <ul>
182
                <li>Servers:<ul id="search-serversinfo"></ul></li>
183
            </ul>
184
        </div>
185
    </div>
186
    <div class="span9">
187
        <div id="searchresults">
188
            <div id="search-top-pages">
189
                <div class="pagination pagination-small">
190
                </div>
191
            </div>
192
193
            <table>
194
                <thead>
195
                    <tr></tr>
196
                </thead>
197
                <tbody></tbody>
198
            </table>
199
200
            <div id="search-bottom-pages">
201
                <div class="pagination pagination-small">
202
                </div>
203
            </div>
204
        </div>
205
    </div>
206
    <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div>
207
</div>
208
209
</div>
210
211
<div id="macro-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="macro-title" aria-hidden="true">
212
213
<div class="modal-header">
214
    <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
215
    <h3 id="macro-title">Macros</h3>
216
</div>
217
218
<div class="modal-body row-fluid">
219
    <div class="span3"><ul id="macro-list"></ul></div>
220
    <div class="span9" id="macro-editor">
221
        <div id="macro-toolbar" class="btn-toolbar">
222
            <button class="btn btn-small" id="run-macro" title="Run and edit macros"><i class="icon-play"></i> Run macro</button>
223
            <button class="btn btn-small" id="delete-macro" title="Delete macro"><i class="icon-remove"></i> Delete macro</button>
224
            <label for="macro-format">Format: </label> <select id="macro-format"></select>
225
            <div id="macro-save-message"></div>
226
        </div>
227
    </div>
228
</div>
229
230
</div>
231
232
[% PROCESS 'cateditor-ui.inc' %]
233
234
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/svc/cataloguing/framework (+77 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl '2009';
4
5
use CGI;
6
use C4::Branch;
7
use C4::ClassSource;
8
use C4::Context;
9
use C4::Biblio;
10
use C4::Service;
11
use Koha::Database;
12
13
my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' );
14
15
my $frameworkcode = $query->param( 'frameworkcode' ) // '';
16
17
my $tagslib = GetMarcStructure( 1, $frameworkcode );
18
19
my @tags;
20
21
foreach my $tag ( sort keys %$tagslib ) {
22
    my $taglib = $tagslib->{$tag};
23
    my $taginfo = { map { $_, $taglib->{$_} } grep { length $_ > 1 } keys %$taglib };
24
    $taginfo->{subfields} = [ map { [ $_, $taglib->{$_} ] } grep { length $_ == 1 } sort keys %$taglib ];
25
26
    push @tags, [ $tag, $taginfo ];
27
}
28
29
my $schema = Koha::Database->new->schema;
30
my $authorised_values = {};
31
32
$authorised_values->{branches} = [];
33
my $onlymine=C4::Context->preference('IndependentBranches') &&
34
        C4::Context->userenv &&
35
        C4::Context->userenv->{flags} % 2 == 0 &&
36
        C4::Context->userenv->{branch};
37
my $branches = GetBranches($onlymine);
38
foreach my $thisbranch ( sort keys %$branches ) {
39
    push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} };
40
}
41
42
$authorised_values->{itemtypes} = [ $schema->resultset( "Itemtype" )->search( undef, {
43
    columns => [ { value => 'itemtype' }, { lib => "description" } ],
44
    order_by => "description",
45
    result_class => 'DBIx::Class::ResultClass::HashRefInflator'
46
} ) ];
47
48
my $class_sources = GetClassSources();
49
50
my $default_source = C4::Context->preference("DefaultClassificationSource");
51
52
foreach my $class_source (sort keys %$class_sources) {
53
    next unless $class_sources->{$class_source}->{'used'} or
54
                ($class_source eq $default_source);
55
    push @{ $authorised_values->{cn_source} }, { value => $class_source, lib => $class_sources->{$class_source}->{'description'} };
56
}
57
58
my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
59
my $results;
60
if( $branch_limit ) {
61
    $results = $schema->resultset( "AuthorisedValue" )->search(
62
    { "authorised_values_branches.branchcode" => { "=", [ $branch_limit, undef ] } },
63
    { join => "authorised_values_branches", order_by => "lib" } );
64
} else {
65
    $results = $schema->resultset( "AuthorisedValue" )->search(
66
    undef,
67
    { order_by => "lib" } );
68
}
69
70
foreach my $result ( $results->all ) {
71
    $authorised_values->{$result->category} ||= [];
72
    push @{ $authorised_values->{$result->category} }, { value => $result->authorised_value, lib => $result->lib };
73
}
74
75
$response->param( framework => \@tags, authorised_values => $authorised_values );
76
77
C4::Service->return_success( $response );
(-)a/svc/cataloguing/metasearch (-1 / +76 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
#
3
# Copyright 2014 ByWater Solutions
4
#
5
# This file is part of Koha.
6
#
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.
11
#
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.
16
#
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>.
19
20
use Modern::Perl;
21
22
use C4::Service;
23
use Encode qw( encode_utf8 );
24
use Koha::MetaSearcher;
25
26
my ( $query, $response ) = C4::Service->init( catalogue => 1 );
27
28
my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' );
29
30
my $server_errors = {};
31
32
my $sort_key = $query->param( 'sort_key' ) || 'title';
33
my $sort_direction = $query->param( 'sort_direction' ) || 'asc';
34
my $offset = $query->param( 'offset' ) || 0;
35
my $page_size = $query->param( 'page_size' ) || 20;
36
my $fetched = $query->param( 'fetched' ) || 100;
37
38
my $searcher = Koha::MetaSearcher->new( {
39
    fetched => $fetched,
40
    on_error => sub {
41
        my ( $server, $exception ) = @_;
42
43
        $server_errors->{ $server->{id} } = $exception->message;
44
    },
45
} );
46
47
$searcher->resultset( $query->param('resultset') ) if ( $query->param('resultset') );
48
49
my @server_ids = split( /,/, $servers );
50
my $stats = $searcher->search( \@server_ids, $query_string );
51
52
$searcher->sort( $sort_key, $sort_direction eq 'desc' ? -1 : 1 );
53
54
my @hits;
55
56
foreach my $hit ( $searcher->results( $offset, $page_size ) ) {
57
    push @hits, {
58
        server => $hit->{server}->{id},
59
        index => $hit->{index},
60
        record => $hit->{record}->as_xml_record(),
61
        metadata => $hit->{metadata}
62
    };
63
}
64
65
$response->param(
66
    resultset => $searcher->resultset,
67
    sort_key => $sort_key,
68
    sort_direction => $sort_direction,
69
    offset => $offset,
70
    page_size => $page_size,
71
    errors => $server_errors,
72
    hits => \@hits,
73
    %$stats
74
);
75
76
C4::Service->return_success( $response );

Return to bug 11559