Bugzilla – Attachment 43616 Details for
Bug 11559
Professional cataloger's interface
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11559: Rancor: advanced cataloging interface
Bug-11559-Rancor-advanced-cataloging-interface.patch (text/plain), 192.58 KB, created by
Jesse Weaver
on 2015-10-19 23:49:19 UTC
(
hide
)
Description:
Bug 11559: Rancor: advanced cataloging interface
Filename:
MIME Type:
Creator:
Jesse Weaver
Created:
2015-10-19 23:49:19 UTC
Size:
192.58 KB
patch
obsolete
>From 18ed35cd38c3aeabc86ab7481d75191f2f06e2e7 Mon Sep 17 00:00:00 2001 >From: Jesse Weaver <pianohacker@gmail.com> >Date: Mon, 15 Jun 2015 17:09:27 -0400 >Subject: [PATCH] Bug 11559: Rancor: advanced cataloging interface > >Full test plan is posted on bug. Test plan for system preference: > > 1. Apply patch, clear cookies. > 2. Go to "Cataloging." > 3. Add new record, verify that basic editor is used. > 4. Navigate to existing record, click on "Edit record", verify that > basic editor is used. > 5. Inside basic editor, verify that no button appears to switch to the > advanced editor. > 6. Enable the "EnableAdvancedCatalogingEditor" syspref. > 7. Repeat above steps, should still go to basic editor, but button > should appear to switch to the advanced editor; click it. > 8. Now, adding new records and editing existing records should go to > the advanced editor. >--- > Koha/MetaSearcher.pm | 351 +++++++ > Koha/MetadataRecord.pm | 25 + > cataloguing/addbiblio.pl | 6 + > cataloguing/editor.pl | 67 ++ > ...-add_EnableAdvancedCatalogingEditor_syspref.sql | 1 + > installer/data/mysql/sysprefs.sql | 1 + > .../lib/koha/cateditor/koha-backend.js | 220 ++++ > .../intranet-tmpl/lib/koha/cateditor/macros.js | 38 + > .../intranet-tmpl/lib/koha/cateditor/macros/its.js | 208 ++++ > .../lib/koha/cateditor/macros/rancor.js | 277 ++++++ > .../lib/koha/cateditor/marc-editor.js | 671 +++++++++++++ > .../intranet-tmpl/lib/koha/cateditor/marc-mode.js | 168 ++++ > .../lib/koha/cateditor/marc-record.js | 370 +++++++ > .../lib/koha/cateditor/preferences.js | 49 + > .../intranet-tmpl/lib/koha/cateditor/resources.js | 38 + > .../intranet-tmpl/lib/koha/cateditor/search.js | 114 +++ > .../intranet-tmpl/lib/koha/cateditor/text-marc.js | 106 ++ > .../intranet-tmpl/lib/koha/cateditor/widget.js | 310 ++++++ > koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css | 434 ++++++++ > .../intranet-tmpl/prog/en/css/staff-global.css | 11 +- > .../prog/en/includes/cateditor-ui.inc | 1051 ++++++++++++++++++++ > .../prog/en/includes/cateditor-widgets-marc21.inc | 158 +++ > .../intranet-tmpl/prog/en/includes/prefs-menu.inc | 1 + > .../prog/en/modules/admin/preferences/labs.pref | 11 + > .../prog/en/modules/cataloguing/addbiblio.tt | 24 +- > .../prog/en/modules/cataloguing/addbooks.tt | 9 + > .../prog/en/modules/cataloguing/editor.tt | 234 +++++ > svc/cataloguing/framework | 77 ++ > svc/cataloguing/metasearch | 76 ++ > 29 files changed, 5102 insertions(+), 4 deletions(-) > create mode 100644 Koha/MetaSearcher.pm > create mode 100755 cataloguing/editor.pl > create mode 100644 installer/data/mysql/atomicupdate/bug_11559-add_EnableAdvancedCatalogingEditor_syspref.sql > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/labs.pref > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt > create mode 100755 svc/cataloguing/framework > create mode 100755 svc/cataloguing/metasearch > >diff --git a/Koha/MetaSearcher.pm b/Koha/MetaSearcher.pm >new file mode 100644 >index 0000000..7c4c87a >--- /dev/null >+++ b/Koha/MetaSearcher.pm >@@ -0,0 +1,351 @@ >+package Koha::MetaSearcher; >+ >+# Copyright 2014 ByWater Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use base 'Class::Accessor'; >+ >+use C4::Charset qw( MarcToUTF8Record ); >+use C4::Search qw(); # Purely for new_record_from_zebra >+use DBIx::Class::ResultClass::HashRefInflator; >+use IO::Select; >+use Koha::Cache; >+use Koha::Database; >+use Koha::MetadataRecord; >+use MARC::File::XML; >+use Storable qw( store_fd fd_retrieve ); >+use Time::HiRes qw( clock_gettime CLOCK_MONOTONIC ); >+use UUID; >+use ZOOM; >+ >+use sort 'stable'; >+ >+__PACKAGE__->mk_accessors( qw( fetch offset on_error resultset ) ); >+ >+sub new { >+ my ( $class, $options ) = @_; >+ >+ my ( $uuid, $uuidstring ); >+ UUID::generate($uuid); >+ UUID::unparse( $uuid, $uuidstring ); >+ >+ return bless { >+ offset => 0, >+ fetch => 100, >+ on_error => sub {}, >+ results => [], >+ resultset => $uuidstring, >+ %{ $options || {} } >+ }, $class; >+} >+ >+sub handle_hit { >+ my ( $self, $index, $server, $marcrecord ) = @_; >+ >+ my $record = Koha::MetadataRecord->new( { schema => 'marc', record => $marcrecord } ); >+ >+ my %fetch = ( >+ title => 'biblio.title', >+ seriestitle => 'biblio.seriestitle', >+ author => 'biblio.author', >+ isbn =>'biblioitems.isbn', >+ issn =>'biblioitems.issn', >+ lccn =>'biblioitems.lccn', #LC control number (not call number) >+ edition =>'biblioitems.editionstatement', >+ date => 'biblio.copyrightdate', #MARC21 >+ date2 => 'biblioitems.publicationyear', #UNIMARC >+ ); >+ >+ my $metadata = {}; >+ while ( my ( $key, $kohafield ) = each %fetch ) { >+ $metadata->{$key} = $record->getKohaField($kohafield); >+ } >+ $metadata->{date} //= $metadata->{date2}; >+ >+ push @{ $self->{results} }, { >+ server => $server, >+ index => $index, >+ record => $marcrecord, >+ metadata => $metadata, >+ }; >+} >+ >+sub search { >+ my ( $self, $server_ids, $query ) = @_; >+ >+ my $resultset_expiry = 300; >+ >+ my $cache; >+ eval { $cache = Koha::Cache->new(); }; >+ my $schema = Koha::Database->new->schema; >+ my $stats = { >+ num_fetched => { >+ map { $_ => 0 } @$server_ids >+ }, >+ num_hits => { >+ map { $_ => 0 } @$server_ids >+ }, >+ total_fetched => 0, >+ total_hits => 0, >+ }; >+ my $start = clock_gettime( CLOCK_MONOTONIC ); >+ my $select = IO::Select->new; >+ my @worker_fhs; >+ >+ my @cached_sets; >+ my @servers; >+ >+ foreach my $server_id ( @$server_ids ) { >+ if ( $server_id =~ /^\d+$/ ) { >+ # Z39.50 server >+ my $server = $schema->resultset('Z3950server')->find( >+ { id => $server_id }, >+ { result_class => 'DBIx::Class::ResultClass::HashRefInflator' }, >+ ); >+ $server->{type} = 'z3950'; >+ >+ push @servers, $server; >+ } elsif ( $server_id =~ /(\w+)(?::(\w+))?/ ) { >+ # Special server >+ push @servers, { >+ type => $1, >+ extra => $2, >+ id => $server_id, >+ host => $server_id, >+ name => $server_id, >+ }; >+ } >+ } >+ >+ # HashRefInflator is used so that the information will survive into the fork >+ foreach my $server ( @servers ) { >+ if ( $cache ) { >+ my $set = $cache->get_from_cache( 'z3950-resultset-' . $self->resultset . '-' . $server->{id} ); >+ if ( ref($set) eq 'HASH' ) { >+ $set->{server} = $server; >+ push @cached_sets, $set; >+ next; >+ } >+ } >+ >+ $select->add( $self->_start_worker( $server, $query ) ); >+ } >+ >+ # Handle these while the servers are searching >+ foreach my $set ( @cached_sets ) { >+ $self->_handle_hits( $stats, $set ); >+ } >+ >+ while ( $select->count ) { >+ foreach my $readfh ( $select->can_read() ) { >+ my $result = fd_retrieve( $readfh ); >+ >+ $select->remove( $readfh ); >+ close $readfh; >+ wait; >+ >+ next if ( ref $result ne 'HASH' ); >+ >+ if ( $result->{error} ) { >+ $self->{on_error}->( $result->{server}, $result->{error} ); >+ next; >+ } >+ >+ $self->_handle_hits( $stats, $result ); >+ >+ if ( $cache ) { >+ $cache->set_in_cache( 'z3950-resultset-' . $self->resultset . '-' . $result->{server}->{id}, { >+ hits => $result->{hits}, >+ num_fetched => $result->{num_fetched}, >+ num_hits => $result->{num_hits}, >+ }, $resultset_expiry ); >+ } >+ } >+ } >+ >+ $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start; >+ >+ return $stats; >+} >+ >+sub _start_worker { >+ my ( $self, $server, $query ) = @_; >+ pipe my $readfh, my $writefh; >+ >+ # Accessing the cache or Koha database after the fork is risky, so get any resources we need >+ # here. >+ my $pid; >+ my $marcflavour = C4::Context->preference('marcflavour'); >+ >+ if ( ( $pid = fork ) ) { >+ # Parent process >+ close $writefh; >+ >+ return $readfh; >+ } elsif ( !defined $pid ) { >+ # Error >+ >+ $self->{on_error}->( $server, 'Failed to fork' ); >+ return; >+ } >+ >+ close $readfh; >+ my $connection; >+ my ( $num_hits, $num_fetched, $hits, $results ); >+ >+ eval { >+ if ( $server->{type} eq 'z3950' ) { >+ my $zoptions = ZOOM::Options->new(); >+ $zoptions->option( 'elementSetName', 'F' ); >+ $zoptions->option( 'databaseName', $server->{db} ); >+ $zoptions->option( 'user', $server->{userid} ) if $server->{userid}; >+ $zoptions->option( 'password', $server->{password} ) if $server->{password}; >+ $zoptions->option( 'preferredRecordSyntax', $server->{syntax} ); >+ $zoptions->option( 'timeout', $server->{timeout} ) if $server->{timeout}; >+ >+ $connection = ZOOM::Connection->create($zoptions); >+ >+ $connection->connect( $server->{host}, $server->{port} ); >+ $results = $connection->search_pqf( $query ); # Starts the search >+ } elsif ( $server->{type} eq 'koha' ) { >+ $connection = C4::Context->Zconn( $server->{extra} ); >+ $results = $connection->search_pqf( $query ); # Starts the search >+ } elsif ( $server->{type} eq 'batch' ) { >+ $server->{encoding} = 'utf-8'; >+ } >+ }; >+ if ($@) { >+ store_fd { >+ error => $connection ? $connection->exception() : $@, >+ server => $server, >+ }, $writefh; >+ exit; >+ } >+ >+ if ( $server->{type} eq 'batch' ) { >+ # TODO: actually handle PQF >+ $query =~ s/@\w+ (?:\d+=\d+ )?//g; >+ $query =~ s/"//g; >+ >+ my $schema = Koha::Database->new->schema; >+ $schema->storage->debug(1); >+ my $match_condition = [ map +{ -like => '%' . $_ . '%' }, split( /\s+/, $query ) ]; >+ $hits = [ $schema->resultset('ImportRecord')->search( >+ { >+ import_batch_id => $server->{extra}, >+ -or => [ >+ { 'import_biblios.title' => $match_condition }, >+ { 'import_biblios.author' => $match_condition }, >+ { 'import_biblios.isbn' => $match_condition }, >+ { 'import_biblios.issn' => $match_condition }, >+ ], >+ }, >+ { >+ join => [ qw( import_biblios ) ], >+ rows => $self->{fetch}, >+ } >+ )->get_column( 'marc' )->all ]; >+ >+ $num_hits = $num_fetched = scalar @$hits; >+ } else { >+ $num_hits = $results->size; >+ $num_fetched = ( $self->{offset} + $self->{fetch} ) < $num_hits ? $self->{fetch} : $num_hits; >+ >+ $hits = [ map { $_->raw() } @{ $results->records( $self->{offset}, $num_fetched, 1 ) } ]; >+ } >+ >+ if ( !@$hits && $connection && $connection->exception() ) { >+ store_fd { >+ error => $connection->exception(), >+ server => $server, >+ }, $writefh; >+ exit; >+ } >+ >+ if ( $server->{type} eq 'koha' ) { >+ $hits = [ map { C4::Search::new_record_from_zebra( $server->{extra}, $_ ) } @$hits ]; >+ } else { >+ $hits = [ map { $self->_import_record( $_, $marcflavour, $server->{encoding} ? $server->{encoding} : "iso-5426" ) } @$hits ]; >+ } >+ >+ store_fd { >+ hits => $hits, >+ num_fetched => $num_fetched, >+ num_hits => $num_hits, >+ server => $server, >+ }, $writefh; >+ >+ exit; >+} >+ >+sub _import_record { >+ my ( $self, $raw, $marcflavour, $encoding ) = @_; >+ >+ my ( $marcrecord ) = MarcToUTF8Record( $raw, $marcflavour, $encoding ); #ignores charset return values >+ >+ return $marcrecord; >+} >+ >+sub _handle_hits { >+ my ( $self, $stats, $set ) = @_; >+ >+ my $server = $set->{server}; >+ >+ my $num_hits = $stats->{num_hits}->{ $server->{id} } = $set->{num_hits}; >+ my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = $set->{num_fetched}; >+ >+ $stats->{total_hits} += $num_hits; >+ $stats->{total_fetched} += $num_fetched; >+ >+ foreach my $j ( 0..$#{ $set->{hits} } ) { >+ $self->handle_hit( $self->{offset} + $j, $server, $set->{hits}->[$j] ); >+ } >+} >+ >+sub sort { >+ my ( $self, $key, $direction ) = @_; >+ >+ my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys. >+ >+ foreach my $hit ( @{ $self->{results} } ) { >+ ( $hit->{sort_key} = $hit->{metadata}->{$key} || '' ) =~ s/\W//g; >+ } >+ >+ $self->{results} = [ sort { >+ # Sort empty records at the end >+ return -$empty_flip unless $a->{sort_key}; >+ return $empty_flip unless $b->{sort_key}; >+ >+ $direction * ( $a->{sort_key} cmp $b->{sort_key} ); >+ } @{ $self->{results} } ]; >+} >+ >+sub results { >+ my ( $self, $offset, $length ) = @_; >+ >+ my @subset; >+ >+ foreach my $i ( $offset..( $offset + $length - 1 ) ) { >+ push @subset, $self->{results}->[$i] if $self->{results}->[$i]; >+ } >+ >+ return @subset; >+} >+ >+1; >diff --git a/Koha/MetadataRecord.pm b/Koha/MetadataRecord.pm >index 8b0d8a4..1577cd0 100644 >--- a/Koha/MetadataRecord.pm >+++ b/Koha/MetadataRecord.pm >@@ -117,4 +117,29 @@ sub createMergeHash { > } > } > >+sub getKohaField { >+ my ($self, $kohafield) = @_; >+ >+ if ($self->schema =~ m/marc/) { >+ my $relations = C4::Context->marcfromkohafield->{''}; >+ my $tagfield = $relations->{$kohafield}; >+ >+ return '' if ref($tagfield) ne 'ARRAY'; >+ >+ my ($tag, $subfield) = @$tagfield; >+ my @kohafield; >+ foreach my $field ( $self->record->field($tag) ) { >+ if ( $field->tag() < 10 ) { >+ push @kohafield, $field->data(); >+ } else { >+ foreach my $contents ( $field->subfield($subfield) ) { >+ push @kohafield, $contents; >+ } >+ } >+ } >+ >+ return join ' | ', @kohafield; >+ } >+} >+ > 1; >diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl >index 890db0f..8cf3b6a 100755 >--- a/cataloguing/addbiblio.pl >+++ b/cataloguing/addbiblio.pl >@@ -746,8 +746,14 @@ if ($frameworkcode eq 'FA'){ > 'stickyduedate' => $fa_stickyduedate, > 'duedatespec' => $fa_duedatespec, > ); >+} elsif ( C4::Context->preference('EnableAdvancedCatalogingEditor') && $input->cookie( 'catalogue_editor_' . $loggedinuser ) eq 'advanced' && !$breedingid ) { >+ # Only use the advanced editor for non-fast-cataloging. >+ # breedingid is not handled because those would only come off a Z39.50 >+ # search initiated by the basic editor. >+ print $input->redirect( '/cgi-bin/koha/cataloguing/editor.pl' . ( $biblionumber ? ( '#catalog/' . $biblionumber ) : '' ) ); > } > >+ > # Getting the list of all frameworks > # get framework list > my $frameworks = getframeworks; >diff --git a/cataloguing/editor.pl b/cataloguing/editor.pl >new file mode 100755 >index 0000000..6d0fd0d >--- /dev/null >+++ b/cataloguing/editor.pl >@@ -0,0 +1,67 @@ >+#!/usr/bin/perl >+# >+# Copyright 2013 ByWater >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it under the >+# terms of the GNU General Public License as published by the Free Software >+# Foundation; either version 2 of the License, or (at your option) any later >+# version. >+# >+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >+# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License along >+# with Koha; if not, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# >+ >+use Modern::Perl '2009'; >+ >+use CGI; >+use MARC::Record; >+ >+use C4::Auth; >+use C4::Biblio; >+use C4::Context; >+use C4::Output; >+use DBIx::Class::ResultClass::HashRefInflator; >+use Koha::Database; >+ >+my $input = CGI->new; >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'cataloguing/editor.tt', >+ query => $input, >+ type => 'intranet', >+ authnotrequired => 0, >+ flagsrequired => { editcatalogue => 'edit_catalogue' }, >+ } >+); >+ >+my $schema = Koha::Database->new->schema; >+ >+# Available import batches >+$template->{VARS}->{editable_batches} = [ $schema->resultset('ImportBatch')->search( >+ { >+ batch_type => [ 'batch', 'webservice' ], >+ import_status => 'staged', >+ }, >+ { result_class => 'DBIx::Class::ResultClass::HashRefInflator' }, >+) ]; >+ >+# Needed information for cataloging plugins >+$template->{VARS}->{DefaultLanguageField008} = pack( 'A3', C4::Context->preference('DefaultLanguageField008') || 'eng' ); >+ >+# Z39.50 servers >+my $dbh = C4::Context->dbh; >+$template->{VARS}->{z3950_servers} = $dbh->selectall_arrayref( q{ >+ SELECT * FROM z3950servers >+ WHERE recordtype != 'authority' AND servertype = 'zed' >+ ORDER BY servername >+}, { Slice => {} } ); >+ >+output_html_with_http_headers $input, $cookie, $template->output; >diff --git a/installer/data/mysql/atomicupdate/bug_11559-add_EnableAdvancedCatalogingEditor_syspref.sql b/installer/data/mysql/atomicupdate/bug_11559-add_EnableAdvancedCatalogingEditor_syspref.sql >new file mode 100644 >index 0000000..c7f0fac >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_11559-add_EnableAdvancedCatalogingEditor_syspref.sql >@@ -0,0 +1 @@ >+INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo'); >diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql >index 5a9dd2a..ecaccec 100644 >--- a/installer/data/mysql/sysprefs.sql >+++ b/installer/data/mysql/sysprefs.sql >@@ -121,6 +121,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('DumpTemplateVarsOpac', '0', NULL , 'If enabled, dump all Template Toolkit variable to a comment in the html source for the opac.', 'YesNo'), > ('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'), > ('emailLibrarianWhenHoldIsPlaced','0',NULL,'If ON, emails the librarian whenever a hold is placed','YesNo'), >+('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo'), > ('EnableBorrowerFiles','0',NULL,'If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'), > ('EnableOpacSearchHistory','1','YesNo','Enable or disable opac search history',''), > ('EnableSearchHistory','0','','Enable or disable search history','YesNo'), >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js >new file mode 100644 >index 0000000..7827f60 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js >@@ -0,0 +1,220 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=define', 'marc-record' ], function( defaultFramework, MARC ) { >+ var _authorised_values = defaultFramework.authorised_values; >+ var _frameworks = {}; >+ var _framework_mappings = {}; >+ >+ function _fromXMLStruct( data ) { >+ result = {}; >+ >+ $(data).children().eq(0).children().each( function() { >+ var $contents = $(this).contents(); >+ if ( $contents.length == 1 && $contents[0].nodeType == Node.TEXT_NODE ) { >+ result[ this.localName ] = $contents[0].data; >+ } else { >+ result[ this.localName ] = $contents.filter( function() { return this.nodeType != Node.TEXT_NODE || !this.data.match( /^\s+$/ ) } ).toArray(); >+ } >+ } ); >+ >+ return result; >+ } >+ >+ function _importFramework( frameworkcode, frameworkinfo ) { >+ _frameworks[frameworkcode] = frameworkinfo; >+ _framework_mappings[frameworkcode] = {}; >+ >+ $.each( frameworkinfo, function( i, tag ) { >+ var tagnum = tag[0], taginfo = tag[1]; >+ >+ var subfields = {}; >+ >+ $.each( taginfo.subfields, function( i, subfield ) { >+ subfields[ subfield[0] ] = subfield[1]; >+ } ); >+ >+ _framework_mappings[frameworkcode][tagnum] = $.extend( {}, taginfo, { subfields: subfields } ); >+ } ); >+ } >+ >+ _importFramework( '', defaultFramework.framework ); >+ >+ var KohaBackend = { >+ NOT_EMPTY: {}, // Sentinel value >+ >+ GetAllTagsInfo: function( frameworkcode, tagnumber ) { >+ return _framework_mappings[frameworkcode]; >+ }, >+ >+ GetAuthorisedValues: function( category ) { >+ return _authorised_values[category]; >+ }, >+ >+ GetTagInfo: function( frameworkcode, tagnumber ) { >+ if ( !_framework_mappings[frameworkcode] ) return undefined; >+ return _framework_mappings[frameworkcode][tagnumber]; >+ }, >+ >+ GetRecord: function( id, callback ) { >+ $.get( >+ '/cgi-bin/koha/svc/bib/' + id >+ ).done( function( data ) { >+ var record = new MARC.Record(); >+ record.loadMARCXML(data); >+ callback(record); >+ } ).fail( function( data ) { >+ callback( { error: data } ); >+ } ); >+ }, >+ >+ CreateRecord: function( record, callback ) { >+ $.ajax( { >+ type: 'POST', >+ url: '/cgi-bin/koha/svc/new_bib', >+ data: record.toXML(), >+ contentType: 'text/xml' >+ } ).done( function( data ) { >+ callback( _fromXMLStruct( data ) ); >+ } ).fail( function( data ) { >+ callback( { error: data } ); >+ } ); >+ }, >+ >+ SaveRecord: function( id, record, callback ) { >+ $.ajax( { >+ type: 'POST', >+ url: '/cgi-bin/koha/svc/bib/' + id, >+ data: record.toXML(), >+ contentType: 'text/xml' >+ } ).done( function( data ) { >+ callback( _fromXMLStruct( data ) ); >+ } ).fail( function( data ) { >+ callback( { data: { error: data } } ); >+ } ); >+ }, >+ >+ GetTagsBy: function( frameworkcode, field, value ) { >+ var result = {}; >+ >+ $.each( _frameworks[frameworkcode], function( undef, tag ) { >+ var tagnum = tag[0], taginfo = tag[1]; >+ >+ if ( taginfo[field] == value ) result[tagnum] = true; >+ } ); >+ >+ return result; >+ }, >+ >+ GetSubfieldsBy: function( frameworkcode, field, value ) { >+ var result = {}; >+ >+ $.each( _frameworks[frameworkcode], function( undef, tag ) { >+ var tagnum = tag[0], taginfo = tag[1]; >+ >+ $.each( taginfo.subfields, function( undef, subfield ) { >+ var subfieldcode = subfield[0], subfieldinfo = subfield[1]; >+ >+ if ( subfieldinfo[field] == value ) { >+ if ( !result[tagnum] ) result[tagnum] = {}; >+ >+ result[tagnum][subfieldcode] = true; >+ } >+ } ); >+ } ); >+ >+ return result; >+ }, >+ >+ FillRecord: function( frameworkcode, record, allTags ) { >+ $.each( _frameworks[frameworkcode], function( undef, tag ) { >+ var tagnum = tag[0], taginfo = tag[1]; >+ >+ if ( taginfo.mandatory != "1" && !allTags ) return; >+ >+ var fields = record.fields(tagnum); >+ >+ if ( fields.length == 0 ) { >+ var newField = new MARC.Field( tagnum, ' ', ' ', [] ); >+ fields.push( newField ); >+ record.addFieldGrouped( newField ); >+ >+ if ( tagnum < '010' ) { >+ newField.addSubfield( [ '@', '' ] ); >+ return; >+ } >+ } >+ >+ $.each( taginfo.subfields, function( undef, subfield ) { >+ var subfieldcode = subfield[0], subfieldinfo = subfield[1]; >+ >+ if ( subfieldinfo.mandatory != "1" && !allTags ) return; >+ >+ $.each( fields, function( undef, field ) { >+ if ( !field.hasSubfield(subfieldcode) ) field.addSubfieldGrouped( [ subfieldcode, '' ] ); >+ } ); >+ } ); >+ } ); >+ }, >+ >+ ValidateRecord: function( frameworkcode, record ) { >+ var errors = []; >+ >+ var mandatoryTags = KohaBackend.GetTagsBy( '', 'mandatory', '1' ); >+ var mandatorySubfields = KohaBackend.GetSubfieldsBy( '', 'mandatory', '1' ); >+ var nonRepeatableTags = KohaBackend.GetTagsBy( '', 'repeatable', '0' ); >+ var nonRepeatableSubfields = KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' ); >+ >+ $.each( mandatoryTags, function( tag ) { >+ if ( !record.hasField( tag ) ) errors.push( { type: 'missingTag', tag: tag } ); >+ } ); >+ >+ var seenTags = {}; >+ >+ $.each( record.fields(), function( undef, field ) { >+ if ( seenTags[ field.tagnumber() ] && nonRepeatableTags[ field.tagnumber() ] ) { >+ errors.push( { type: 'unrepeatableTag', line: field.sourceLine, tag: field.tagnumber() } ); >+ return; >+ } >+ >+ seenTags[ field.tagnumber() ] = true; >+ >+ var seenSubfields = {}; >+ >+ $.each( field.subfields(), function( undef, subfield ) { >+ if ( seenSubfields[ subfield[0] ] != null && nonRepeatableSubfields[ field.tagnumber() ][ subfield[0] ] ) { >+ errors.push( { type: 'unrepeatableSubfield', subfield: subfield[0], line: field.sourceLine } ); >+ } else { >+ seenSubfields[ subfield[0] ] = subfield[1]; >+ } >+ } ); >+ >+ $.each( mandatorySubfields[ field.tagnumber() ] || {}, function( subfield ) { >+ if ( !seenSubfields[ subfield ] ) { >+ errors.push( { type: 'missingSubfield', subfield: subfield[0], line: field.sourceLine } ); >+ } >+ } ); >+ } ); >+ >+ return errors; >+ }, >+ }; >+ >+ return KohaBackend; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js >new file mode 100644 >index 0000000..4d747f9 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js >@@ -0,0 +1,38 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'macros/its', 'macros/rancor' ], function( ITSMacro, RancorMacro ) { >+ var Macros = { >+ formats: { >+ its: { >+ description: 'TLC® ITS', >+ Run: ITSMacro.Run, >+ }, >+ rancor: { >+ description: 'Rancor', >+ Run: RancorMacro.Run, >+ }, >+ }, >+ Run: function( editor, format, macro ) { >+ return Macros.formats[format].Run( editor, macro ); >+ }, >+ }; >+ >+ return Macros; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js >new file mode 100644 >index 0000000..07bfbeb >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js >@@ -0,0 +1,208 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( function() { >+ var NAV_FAILED = new Object(); >+ var NAV_SUCCEEDED = new Object(); >+ >+ var _commandGenerators = [ >+ [ /^copy field data$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ return state.field.getText(); >+ }; >+ } ], >+ [ /^copy subfield data$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ var cur = editor.getCursor(); >+ var subfields = state.field.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( cur.ch > subfields[i].end ) continue; >+ >+ state.clipboard = subfields[i].text; >+ return; >+ } >+ >+ return false; >+ } >+ } ], >+ [ /^del(ete)? field$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ state.field.delete(); >+ return NAV_FAILED; >+ } >+ } ], >+ [ /^goto field end$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ var cur = editor.cm.getCursor(); >+ >+ editor.cm.setCursor( { line: cur.line } ); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^goto field (\w{3})$/i, function(tag) { >+ return function( editor, state ) { >+ var field = editor.getFirstField(tag); >+ if ( field == null ) return NAV_FAILED; >+ >+ field.focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^goto subfield end$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ >+ var cur = editor.getCursor(); >+ var subfields = state.field.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( cur.ch > subfields[i].end ) continue; >+ >+ subfield.focusEnd(); >+ return NAV_SUCCEEDED; >+ } >+ >+ return NAV_FAILED; >+ } >+ } ], >+ [ /^goto subfield (\w)$/i, function( code ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ >+ var subfield = state.field.getFirstSubfield( code ); >+ if ( subfield == null ) return NAV_FAILED; >+ >+ subfield.focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, tag, text) { >+ text = text.replace(/\\([0-9a-z])/g, '$$$1 '); >+ return function( editor, state ) { >+ editor.createFieldGrouped(tag).setText(text).focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, code, text) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ state.field.appendSubfield(code).setText(text); >+ } >+ } ], >+ [ /^paste$/i, function() { >+ return function( editor, state ) { >+ var cur = editor.cm.getCursor(); >+ >+ editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' ); >+ } >+ } ], >+ [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ if ( state.field.isControlField ) return false; >+ >+ if ( ind == '1' ) { >+ state.field.setIndicator1(value); >+ return true; >+ } else if ( ind == '2' ) { >+ state.field.setIndicator2(value); >+ return true; >+ } else { >+ return false; >+ } >+ } >+ } ], >+ [ /^set indicators=([ _0-9])([ _0-9])?$/i, function( ind1, ind2 ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ if ( state.field.isControlField ) return false; >+ >+ state.field.setIndicator1(ind1); >+ state.field.setIndicator2(ind2 || '_'); >+ } >+ } ], >+ ]; >+ >+ var ITSMacro = { >+ Compile: function( macro ) { >+ var result = { commands: [], errors: [] }; >+ >+ $.each( macro.split(/\r\n|\n/), function( line, contents ) { >+ var command; >+ >+ if ( contents.match(/^\s*$/) ) return; >+ >+ $.each( _commandGenerators, function( undef, gen ) { >+ var match; >+ >+ if ( !( match = gen[0].exec( contents ) ) ) return; >+ >+ command = gen[1].apply(null, match.slice(1)); >+ return false; >+ } ); >+ >+ if ( !command ) { >+ result.errors.push( { line: line, error: 'unrecognized' } ); >+ } >+ >+ result.commands.push( { func: command, orig: contents, line: line } ); >+ } ); >+ >+ return result; >+ }, >+ Run: function( editor, macro ) { >+ var compiled = ITSMacro.Compile(macro); >+ if ( compiled.errors.length ) return { errors: compiled.errors }; >+ var state = { >+ clipboard: '', >+ field: null, >+ }; >+ >+ var run_result = { errors: [] }; >+ >+ editor.cm.operation( function() { >+ $.each( compiled.commands, function( undef, command ) { >+ var result = command.func( editor, state ); >+ >+ if ( result == NAV_FAILED ) { >+ state.field = null; >+ } else if ( result == NAV_SUCCEEDED ) { >+ state.field = editor.getCurrentField(); >+ } else if ( result === false ) { >+ run_result.errors.push( { line: command.line, error: 'failed' } ); >+ return false; >+ } >+ } ); >+ } ); >+ >+ return run_result; >+ }, >+ }; >+ >+ return ITSMacro; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js >new file mode 100644 >index 0000000..e9b484e >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js >@@ -0,0 +1,277 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'marc-editor' ], function( MARCEditor ) { >+ // These are the generators for targets that appear on the left-hand side of an assignment. >+ var _lhsGenerators = [ >+ // Field; will replace the entire contents of the tag except for indicators. >+ // Examples: >+ // * 245 - will return the first 245 tag it finds, or create a new one >+ // * new 245 - will always create a new 245 >+ // * new 245 grouped - will always create a new 245, and insert it at the end of the 2xx >+ // block >+ [ /^(new )?(\w{3})( (grouped))?$/, function( forceCreate, tag, position, positionGrouped ) { >+ if ( !forceCreate && positionGrouped ) return null; >+ >+ // The extra argument allows the delete command to prevent this from needlessly creating >+ // a tag that it is about to delete. >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ if ( !forceCreate ) { >+ var result = editor.getFirstField(tag); >+ >+ if ( result != null || extra.dontCreate ) return result; >+ } >+ >+ if ( positionGrouped ) { >+ return editor.createFieldGrouped(tag); >+ } else { >+ return editor.createFieldOrdered(tag); >+ } >+ } >+ } ], >+ >+ // This regex is a little complicated, but allows for the following possibilities: >+ // * 245a - Finds the first 245 field, then tries to find an a subfield within it. If none >+ // exists, it is created. Will still fail if there is no 245 field. >+ // * new 245a - always creates a new a subfield. >+ // * new 245a at end - does the same as the above. >+ // * $a or new $a - does the same as the above, but for the last-used tag. >+ // * new 245a after b - creates a new subfield, placing it after the first subfield $b. >+ [ /^(new )?(\w{3}|\$)(\w)( (at end)| after (\w))?$/, function( forceCreate, tag, code, position, positionAtEnd, positionAfterSubfield ) { >+ if ( tag != '$' && tag < '010' ) return null; >+ if ( !forceCreate && position ) return null; >+ >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ var field; >+ >+ if ( tag == '$' ) { >+ field = state.field; >+ } else { >+ field = editor.getFirstField(tag); >+ } >+ if ( field == null || field.isControlField ) return null; >+ >+ if ( !forceCreate ) { >+ var subfield = field.getFirstSubfield(code) >+ >+ if ( subfield != null || extra.dontCreate ) return subfield; >+ } >+ >+ if ( !position || position == ' at end' ) { >+ return field.appendSubfield(code); >+ } else if ( positionAfterSubfield ) { >+ var afterSubfield = field.getFirstSubfield(positionAfterSubfield); >+ >+ if ( afterSubfield == null ) return null; >+ >+ return field.insertSubfield( code, afterSubfield.index + 1 ); >+ } >+ } >+ } ], >+ >+ // Can set indicatators either for a particular field or the last-used tag. >+ [ /^((\w{3}) )?indicators$/, function( undef, tag ) { >+ if ( tag && tag < '010' ) return null; >+ >+ return function( editor, state ) { >+ var field; >+ >+ if ( tag == null ) { >+ field = state.field; >+ } else { >+ field = editor.getFirstField(tag); >+ } >+ if ( field == null || field.isControlField ) return null; >+ >+ return { >+ field: field, >+ setText: function( text ) { >+ field.setIndicator1( text.substr( 0, 1 ) ); >+ field.setIndicator2( text.substr( 1, 1 ) ); >+ } >+ }; >+ } >+ } ], >+ ]; >+ >+ // These patterns, on the other hand, appear inside interpolations on the right hand side. >+ var _rhsGenerators = [ >+ [ /^(\w{3})$/, function( tag ) { >+ return function( editor, state, extra ) { >+ return editor.getFirstField(tag); >+ } >+ } ], >+ [ /^(\w{3})(\w)$/, function( tag, code ) { >+ if ( tag < '010' ) return null; >+ >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ var field = editor.getFirstField(tag); >+ if ( field == null ) return null; >+ >+ return field.getFirstSubfield(code); >+ } >+ } ], >+ ]; >+ >+ var _commandGenerators = [ >+ [ /^delete (.+)$/, function( target ) { >+ var target_closure = _generate( _lhsGenerators, target ); >+ if ( !target_closure ) return null; >+ >+ return function( editor, state ) { >+ var target = target_closure( editor, state, { dontCreate: true } ); >+ if ( target == null ) return; >+ if ( !target.delete ) return false; >+ >+ state.field = null; // As other fields may have been invalidated >+ target.delete(); >+ } >+ } ], >+ [ /^([^=]+)=([^=]*)$/, function( lhs_desc, rhs_desc ) { >+ var lhs_closure = _generate( _lhsGenerators, lhs_desc ); >+ if ( !lhs_closure ) return null; >+ >+ var rhs_closure = _generateInterpolation( _rhsGenerators, rhs_desc ); >+ if ( !rhs_closure ) return null; >+ >+ return function( editor, state ) { >+ var lhs = lhs_closure( editor, state ); >+ if ( lhs == null ) return; >+ >+ state.field = lhs.field || lhs; >+ >+ try { >+ return lhs.setText( rhs_closure( editor, state ) ); >+ } catch (e) { >+ if ( e instanceof MARCEditor.FieldError ) { >+ return false; >+ } else { >+ throw e; >+ } >+ } >+ }; >+ } ], >+ ]; >+ >+ function _generate( set, contents ) { >+ var closure; >+ >+ if ( contents.match(/^\s*$/) ) return; >+ >+ $.each( set, function( undef, gen ) { >+ var match; >+ >+ if ( !( match = gen[0].exec( contents ) ) ) return; >+ >+ closure = gen[1].apply(null, match.slice(1)); >+ return false; >+ } ); >+ >+ return closure; >+ } >+ >+ function _generateInterpolation( set, contents ) { >+ // While this regex will not match at all for an empty string, that just leaves an empty >+ // parts array which yields an empty string (which is what we want.) >+ var matcher = /\{([^}]+)\}|([^{]+)/g; >+ var match; >+ >+ var parts = []; >+ >+ while ( ( match = matcher.exec(contents) ) ) { >+ var closure; >+ if ( match[1] ) { >+ // Found an interpolation >+ var rhs_closure = _generate( set, match[1] ); >+ if ( rhs_closure == null ) return null; >+ >+ closure = ( function(rhs_closure) { return function( editor, state ) { >+ var rhs = rhs_closure( editor, state ); >+ >+ return rhs ? rhs.getText() : ''; >+ } } )( rhs_closure ); >+ } else { >+ // Plain text (needs artificial closure to keep match) >+ closure = ( function(text) { return function() { return text }; } )( match[2] ); >+ } >+ >+ parts.push( closure ); >+ } >+ >+ return function( editor, state ) { >+ var result = ''; >+ $.each( parts, function( i, part ) { >+ result += part( editor, state ); >+ } ); >+ >+ return result; >+ }; >+ } >+ >+ var RancorMacro = { >+ Compile: function( macro ) { >+ var result = { commands: [], errors: [] }; >+ >+ $.each( macro.split(/\r\n|\n/), function( line, contents ) { >+ contents = contents.replace( /#.*$/, '' ); >+ if ( contents.match(/^\s*$/) ) return; >+ >+ var command = _generate( _commandGenerators, contents ); >+ >+ if ( !command ) { >+ result.errors.push( { line: line, error: 'unrecognized' } ); >+ } >+ >+ result.commands.push( { func: command, orig: contents, line: line } ); >+ } ); >+ >+ return result; >+ }, >+ Run: function( editor, macro ) { >+ var compiled = RancorMacro.Compile(macro); >+ if ( compiled.errors.length ) return { errors: compiled.errors }; >+ var state = { >+ field: null, >+ }; >+ >+ var run_result = { errors: [] }; >+ >+ editor.cm.operation( function() { >+ $.each( compiled.commands, function( undef, command ) { >+ var result = command.func( editor, state ); >+ >+ if ( result === false ) { >+ run_result.errors.push( { line: command.line, error: 'failed' } ); >+ return false; >+ } >+ } ); >+ } ); >+ >+ return run_result; >+ }, >+ }; >+ >+ return RancorMacro; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js >new file mode 100644 >index 0000000..bc79eb6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js >@@ -0,0 +1,671 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) { >+ var NOTIFY_TIMEOUT = 250; >+ >+ function editorCursorActivity( cm ) { >+ var editor = cm.marceditor; >+ var field = editor.getCurrentField(); >+ if ( !field ) return; >+ >+ // Set overwrite mode for tag numbers/indicators and contents of fixed fields >+ if ( field.isControlField || cm.getCursor().ch < 8 ) { >+ cm.toggleOverwrite(true); >+ } else { >+ cm.toggleOverwrite(false); >+ } >+ >+ editor.onCursorActivity(); >+ } >+ >+ // This function exists to prevent inserting or partially deleting text that belongs to a >+ // widget. The 'marcAware' change source exists for other parts of the editor code to bypass >+ // this check. >+ function editorBeforeChange( cm, change ) { >+ var editor = cm.marceditor; >+ if ( editor.textMode || change.origin == 'marcAware' || change.origin == 'widget.clearToText' ) return; >+ >+ // FIXME: Should only cancel changes if this is a control field/subfield widget >+ 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 >+ >+ if ( change.from.ch == change.to.ch - 1 && cm.findMarksAt( { line: change.from.line, ch: change.from.ch + 1 } ).length ) { >+ change.cancel(); >+ } else if ( change.from.ch == change.to.ch && cm.findMarksAt(change.from).length && !change.text[0].match(/^[$|Çâ¡]$/) ) { >+ change.cancel(); >+ } >+ } >+ >+ function editorChanges( cm, changes ) { >+ var editor = cm.marceditor; >+ if ( editor.textMode ) return; >+ >+ for (var i = 0; i < changes.length; i++) { >+ var change = changes[i]; >+ >+ var origin = change.from.line; >+ var newTo = CodeMirror.changeEnd(change); >+ >+ for (var delLine = origin; delLine <= change.to.line; delLine++) { >+ // Line deleted; currently nothing to do >+ } >+ >+ for (var line = origin; line <= newTo.line; line++) { >+ if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( cm.marceditor, line ); >+ if ( change.origin != 'setValue' && change.origin != 'marcWidgetPrefill' && change.origin != 'widget.clearToText' ) cm.addLineClass( line, 'wrapper', 'modified-line' ); >+ } >+ } >+ >+ Widget.ActivateAt( cm, cm.getCursor() ); >+ cm.marceditor.startNotify(); >+ } >+ >+ // Editor helper functions >+ function activateTabPosition( cm, pos, idx ) { >+ // Allow tabbing to as-yet-nonexistent positions >+ var lenDiff = pos.ch - cm.getLine( pos.line ).length; >+ if ( lenDiff > 0 ) { >+ var extra = ''; >+ while ( lenDiff-- > 0 ) extra += ' '; >+ if ( pos.prefill ) extra += pos.prefill; >+ cm.replaceRange( extra, { line: pos.line } ); >+ } >+ >+ cm.setCursor( pos ); >+ Widget.ActivateAt( cm, pos, idx ); >+ } >+ >+ function getTabPositions( editor, cur ) { >+ cur = cur || editor.cm.getCursor(); >+ var field = editor.getFieldAt( cur.line ); >+ >+ if ( field ) { >+ if ( field.isControlField ) { >+ var positions = [ { ch: 0 }, { ch: 4 } ]; >+ >+ $.each( positions, function( undef, pos ) { >+ pos.line = cur.line; >+ } ); >+ >+ return positions; >+ } else { >+ var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ]; >+ >+ $.each( positions, function( undef, pos ) { >+ pos.line = cur.line; >+ } ); >+ $.each( field.getSubfields(), function( undef, subfield ) { >+ positions.push( { line: cur.line, ch: subfield.contentsStart } ); >+ } ); >+ >+ // Allow to tab to start of empty field >+ if ( field.getSubfields().length == 0 ) { >+ positions.push( { line: cur.line, ch: 8 } ); >+ } >+ >+ return positions; >+ } >+ } else { >+ return []; >+ } >+ } >+ >+ var _editorKeys = { >+ Enter: function( cm ) { >+ var cursor = cm.getCursor(); >+ cm.replaceRange( '\n', { line: cursor.line }, null, 'marcAware' ); >+ cm.setCursor( { line: cursor.line + 1, ch: 0 } ); >+ }, >+ >+ 'Ctrl-X': function( cm ) { >+ // Delete line (or cut) >+ if ( cm.somethingSelected() ) return true; >+ >+ var field = cm.marceditor.getCurrentField(); >+ if ( field ) field.delete(); >+ }, >+ >+ 'Shift-Ctrl-X': function( cm ) { >+ // Delete subfield >+ var field = cm.marceditor.getCurrentField(); >+ if ( !field ) return; >+ >+ var subfield = field.getSubfieldAt( cm.getCursor().ch ); >+ if ( subfield ) subfield.delete(); >+ }, >+ >+ Tab: function( cm ) { >+ // Move through parts of tag/fixed fields >+ var positions = getTabPositions( cm.marceditor ); >+ var cur = cm.getCursor(); >+ >+ for ( var i = 0; i < positions.length; i++ ) { >+ if ( positions[i].ch > cur.ch ) { >+ activateTabPosition( cm, positions[i] ); >+ return false; >+ } >+ } >+ >+ cm.setCursor( { line: cur.line + 1, ch: 0 } ); >+ }, >+ >+ 'Shift-Tab': function( cm ) { >+ // Move backwards through parts of tag/fixed fields >+ var positions = getTabPositions( cm.marceditor ); >+ var cur = cm.getCursor(); >+ >+ for ( var i = positions.length - 1; i >= 0; i-- ) { >+ if ( positions[i].ch < cur.ch ) { >+ activateTabPosition( cm, positions[i] ); >+ return false; >+ } >+ } >+ >+ if ( cur.line == 0 ) return; >+ >+ var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } ); >+ >+ if ( prevPositions.length ) { >+ activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 ); >+ } else { >+ cm.setCursor( { line: cur.line - 1, ch: 0 } ); >+ } >+ }, >+ >+ 'Ctrl-D': function( cm ) { >+ // Insert subfield delimiter >+ // This will be extended later to allow either a configurable subfield delimiter or just >+ // make it be the double cross. >+ var cur = cm.getCursor(); >+ >+ cm.replaceRange( "$", cur, null ); >+ }, >+ }; >+ >+ // The objects below are part of a field/subfield manipulation API, accessed through the base >+ // editor object. >+ // >+ // Each one is tied to a particular line; this means that using a field or subfield object after >+ // any other changes to the record will cause entertaining explosions. The objects are meant to >+ // be temporary, and should only be reused with great care. The macro code does this only >+ // because it is careful to dispose of the object after any other updates. >+ // >+ // Note, however, tha you can continue to use a field object after changing subfields. It's just >+ // the subfield objects that become invalid. >+ >+ // This is an exception raised by the EditorSubfield and EditorField when an invalid change is >+ // attempted. >+ function FieldError(line, message) { >+ this.line = line; >+ this.message = message; >+ }; >+ >+ FieldError.prototype.toString = function() { >+ return 'FieldError(' + this.line + ', "' + this.message + '")'; >+ }; >+ >+ // This is the temporary object for a particular subfield in a field. Any change to any other >+ // subfields will invalidate this subfield object. >+ function EditorSubfield( field, index, start, end ) { >+ this.field = field; >+ this.index = index; >+ this.start = start; >+ this.end = end; >+ >+ if ( this.field.isControlField ) { >+ this.contentsStart = start; >+ this.code = '@'; >+ } else { >+ this.contentsStart = start + 3; >+ this.code = this.field.contents.substr( this.start + 1, 1 ); >+ } >+ >+ this.cm = field.cm; >+ >+ var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } ); >+ if ( marks[0] && marks[0].widget ) { >+ this.widget = marks[0].widget; >+ >+ this.text = this.widget.text; >+ this.setText = this.widget.setText; >+ this.getFixed = this.widget.getFixed; >+ this.setFixed = this.widget.setFixed; >+ } else { >+ this.widget = null; >+ this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart ); >+ } >+ }; >+ >+ $.extend( EditorSubfield.prototype, { >+ _invalid: function() { >+ return this.field._subfieldsInvalid(); >+ }, >+ >+ focus: function() { >+ this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } ); >+ }, >+ focusEnd: function() { >+ this.cm.setCursor( { line: this.field.line, ch: this.end } ); >+ }, >+ getText: function() { >+ return this.text; >+ }, >+ setText: function( text ) { >+ if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' ); >+ this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' ); >+ this.field._invalidateSubfields(); >+ }, >+ } ); >+ >+ function EditorField( editor, line ) { >+ this.editor = editor; >+ this.line = line; >+ >+ this.cm = editor.cm; >+ >+ this._updateInfo(); >+ this.tag = this.contents.substr( 0, 3 ); >+ this.isControlField = ( this.tag < '010' ); >+ >+ if ( this.isControlField ) { >+ this._ind1 = this.contents.substr( 4, 1 ); >+ this._ind2 = this.contents.substr( 6, 1 ); >+ } else { >+ this._ind1 = null; >+ this._ind2 = null; >+ } >+ >+ this.subfields = null; >+ } >+ >+ $.extend( EditorField.prototype, { >+ _subfieldsInvalid: function() { >+ return !this.subfields; >+ }, >+ _invalidateSubfields: function() { >+ this._subfields = null; >+ }, >+ >+ _updateInfo: function() { >+ this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } ); >+ if ( this.info == null ) throw new FieldError( 'Invalid field' ); >+ this.contents = this.info.contents; >+ }, >+ _scanSubfields: function() { >+ this._updateInfo(); >+ >+ if ( this.isControlField ) { >+ this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ]; >+ } else { >+ var field = this; >+ var subfields = this.info.subfields; >+ this._subfields = []; >+ >+ for (var i = 0; i < this.info.subfields.length; i++) { >+ var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch; >+ >+ this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) ); >+ } >+ } >+ }, >+ >+ delete: function() { >+ this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' ); >+ }, >+ focus: function() { >+ this.cm.setCursor( { line: this.line, ch: 0 } ); >+ >+ return this; >+ }, >+ >+ getText: function() { >+ var result = ''; >+ >+ $.each( this.getSubfields(), function() { >+ if ( this.code != '@' ) result += '$' + this.code + ' '; >+ >+ result += this.getText(); >+ } ); >+ >+ return result; >+ }, >+ setText: function( text ) { >+ var indicator_match = /^([_ 0-9])([_ 0-9])\$/.exec( text ); >+ if ( indicator_match ) { >+ text = text.substr(2); >+ this.setIndicator1( indicator_match[1] ); >+ this.setIndicator2( indicator_match[2] ); >+ } >+ >+ this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' ); >+ this._invalidateSubfields(); >+ >+ return this; >+ }, >+ >+ getIndicator1: function() { >+ return this._ind1; >+ }, >+ getIndicator2: function() { >+ return this._ind2; >+ }, >+ setIndicator1: function(val) { >+ if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field'); >+ >+ this._ind1 = ( !val || val == ' ' ) ? '_' : val; >+ this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' ); >+ >+ return this; >+ }, >+ setIndicator2: function(val) { >+ if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field'); >+ >+ this._ind2 = ( !val || val == ' ' ) ? '_' : val; >+ this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' ); >+ >+ return this; >+ }, >+ >+ appendSubfield: function( code ) { >+ if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field'); >+ >+ this._invalidateSubfields(); >+ this.cm.replaceRange( '$' + code + ' ', { line: this.line }, null, 'marcAware' ); >+ var subfields = this.getSubfields(); >+ >+ return subfields[ subfields.length - 1 ]; >+ }, >+ insertSubfield: function( code, position ) { >+ if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field'); >+ >+ position = position || 0; >+ >+ var subfields = this.getSubfields(); >+ this._invalidateSubfields(); >+ this.cm.replaceRange( '$' + code + ' ', { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' ); >+ subfields = this.getSubfields(); >+ >+ return subfields[ position ]; >+ }, >+ getSubfields: function( code ) { >+ if ( !this._subfields ) this._scanSubfields(); >+ if ( code == null ) return this._subfields; >+ >+ var result = []; >+ >+ $.each( this._subfields, function() { >+ if ( code == null || this.code == code ) result.push(this); >+ } ); >+ >+ return result; >+ }, >+ getFirstSubfield: function( code ) { >+ var result = this.getSubfields( code ); >+ >+ return ( result && result.length ) ? result[0] : null; >+ }, >+ getSubfieldAt: function( ch ) { >+ var subfields = this.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i]; >+ } >+ }, >+ } ); >+ >+ function MARCEditor( options ) { >+ this.cm = CodeMirror( >+ options.position, >+ { >+ extraKeys: _editorKeys, >+ gutters: [ >+ 'modified-line-gutter', >+ ], >+ lineWrapping: true, >+ mode: { >+ name: 'marc', >+ nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ), >+ nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' ) >+ } >+ } >+ ); >+ this.cm.marceditor = this; >+ >+ this.cm.on( 'beforeChange', editorBeforeChange ); >+ this.cm.on( 'changes', editorChanges ); >+ this.cm.on( 'cursorActivity', editorCursorActivity ); >+ >+ this.onCursorActivity = options.onCursorActivity; >+ >+ this.subscribers = []; >+ this.subscribe( function( marceditor ) { >+ Widget.Notify( marceditor ); >+ } ); >+ } >+ >+ MARCEditor.FieldError = FieldError; >+ >+ $.extend( MARCEditor.prototype, { >+ setUseWidgets: function( val ) { >+ if ( val ) { >+ for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >+ Widget.UpdateLine( this, line ); >+ } >+ } else { >+ $.each( this.cm.getAllMarks(), function( undef, mark ) { >+ if ( mark.widget ) mark.widget.clearToText(); >+ } ); >+ } >+ }, >+ >+ focus: function() { >+ this.cm.focus(); >+ }, >+ >+ getCursor: function() { >+ return this.cm.getCursor(); >+ }, >+ >+ refresh: function() { >+ this.cm.refresh(); >+ }, >+ >+ displayRecord: function( record ) { >+ this.cm.setValue( TextMARC.RecordToText(record) ); >+ }, >+ >+ getRecord: function() { >+ this.textMode = true; >+ >+ $.each( this.cm.getAllMarks(), function( undef, mark ) { >+ if ( mark.widget ) mark.widget.clearToText(); >+ } ); >+ var record = TextMARC.TextToRecord( this.cm.getValue() ); >+ for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >+ if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line ); >+ } >+ >+ this.textMode = false; >+ >+ return record; >+ }, >+ >+ getLineInfo: function( pos ) { >+ var contents = this.cm.getLine( pos.line ); >+ if ( contents == null ) return {}; >+ >+ var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ ); >+ >+ if ( !tagNumber ) return null; // No tag at all on this line >+ tagNumber = tagNumber[1]; >+ >+ if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield >+ >+ var matcher = /[$|Çâ¡]([a-z0-9%]) /g; >+ var match; >+ >+ var subfields = []; >+ var currentSubfield; >+ >+ while ( ( match = matcher.exec(contents) ) ) { >+ subfields.push( { code: match[1], ch: match.index } ); >+ if ( match.index < pos.ch ) currentSubfield = match[1]; >+ } >+ >+ return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents }; >+ }, >+ >+ addError: function( line, error ) { >+ var found = false; >+ var options = {}; >+ >+ if ( line == null ) { >+ line = 0; >+ options.above = true; >+ } >+ >+ $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) { >+ if ( !widget.isErrorMarker ) return; >+ >+ found = true; >+ >+ $( widget.node ).append( '; ' + error ); >+ widget.changed(); >+ >+ return false; >+ } ); >+ >+ if ( found ) return; >+ >+ var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0]; >+ var widget = this.cm.addLineWidget( line, node, options ); >+ >+ widget.node = node; >+ widget.isErrorMarker = true; >+ }, >+ >+ removeErrors: function() { >+ for ( var line = 0; line < this.cm.lineCount(); line++ ) { >+ $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) { >+ if ( lineWidget.isErrorMarker ) lineWidget.clear(); >+ } ); >+ } >+ }, >+ >+ startNotify: function() { >+ if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout ); >+ this.notifyTimeout = setTimeout( $.proxy( function() { >+ this.notifyAll(); >+ >+ this.notifyTimeout = null; >+ }, this ), NOTIFY_TIMEOUT ); >+ }, >+ >+ notifyAll: function() { >+ $.each( this.subscribers, $.proxy( function( undef, subscriber ) { >+ subscriber(this); >+ }, this ) ); >+ }, >+ >+ subscribe: function( subscriber ) { >+ this.subscribers.push( subscriber ); >+ }, >+ >+ createField: function( tag, line ) { >+ var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' ); >+ >+ if ( line > this.cm.lastLine() ) { >+ contents = '\n' + contents; >+ } else { >+ contents = contents + '\n'; >+ } >+ >+ this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' ); >+ >+ return new EditorField( this, line ); >+ }, >+ >+ createFieldOrdered: function( tag ) { >+ var line, contents; >+ >+ for ( line = 0; (contents = this.cm.getLine(line)); line++ ) { >+ if ( contents && contents.substr(0, 3) > tag ) break; >+ } >+ >+ return this.createField( tag, line ); >+ }, >+ >+ createFieldGrouped: function( tag ) { >+ // Control fields should be inserted in actual order, whereas other fields should be >+ // inserted grouped >+ if ( tag < '010' ) return this.createFieldOrdered( tag ); >+ >+ var line, contents; >+ >+ for ( line = 0; (contents = this.cm.getLine(line)); line++ ) { >+ if ( contents && contents[0] > tag[0] ) break; >+ } >+ >+ return this.createField( tag, line ); >+ }, >+ >+ getFieldAt: function( line ) { >+ try { >+ return new EditorField( this, line ); >+ } catch (e) { >+ return null; >+ } >+ }, >+ >+ getCurrentField: function() { >+ return this.getFieldAt( this.cm.getCursor().line ); >+ }, >+ >+ getFields: function( tag ) { >+ var result = []; >+ >+ if ( tag != null ) tag += ' '; >+ >+ for ( var line = 0; line < this.cm.lineCount(); line++ ) { >+ if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue; >+ >+ // If this throws a FieldError, pretend it doesn't exist >+ try { >+ result.push( new EditorField( this, line ) ); >+ } catch (e) { >+ if ( !( e instanceof FieldError ) ) throw e; >+ } >+ } >+ >+ return result; >+ }, >+ >+ getFirstField: function( tag ) { >+ var result = this.getFields( tag ); >+ >+ return ( result && result.length ) ? result[0] : null; >+ }, >+ >+ getAllFields: function( tag ) { >+ return this.getFields( null ); >+ }, >+ } ); >+ >+ return MARCEditor; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js >new file mode 100644 >index 0000000..f9bf862 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js >@@ -0,0 +1,168 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+// Expected format: 245 _ 1 $a Pizza |c 34ars >+ >+CodeMirror.defineMode( 'marc', function( config, modeConfig ) { >+ modeConfig.nonRepeatableTags = modeConfig.nonRepeatableTags || {}; >+ modeConfig.nonRepeatableSubfields = modeConfig.nonRepeatableSubfields || {}; >+ >+ return { >+ startState: function( prevState ) { >+ var state = prevState || {}; >+ >+ if ( !prevState ) { >+ state.seenTags = {}; >+ } >+ >+ state.indicatorNeeded = false; >+ state.subAllowed = true; >+ state.subfieldCode = undefined; >+ state.tagNumber = undefined; >+ state.seenSubfields = {}; >+ >+ return state; >+ }, >+ copyState: function( prevState ) { >+ var result = $.extend( {}, prevState ); >+ result.seenTags = $.extend( {}, prevState.seenTags ); >+ result.seenSubfields = $.extend( {}, prevState.seenSubfields ); >+ >+ return result; >+ }, >+ token: function( stream, state ) { >+ var match; >+ // First, try to match some kind of valid tag >+ if ( stream.sol() ) { >+ this.startState( state ); >+ if ( match = stream.match( /[0-9A-Za-z]+/ ) ) { >+ match = match[0]; >+ if ( match.length != 3 ) { >+ if ( stream.eol() && match.length < 3 ) { >+ // Don't show error for incomplete number >+ return 'tagnumber'; >+ } else { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ } >+ >+ state.tagNumber = match; >+ if ( state.tagNumber < '010' ) { >+ // Control field, no subfields or indicators >+ state.subAllowed = false; >+ } >+ >+ if ( state.seenTags[state.tagNumber] && modeConfig.nonRepeatableTags[state.tagNumber] ) { >+ return 'bad-tagnumber'; >+ } else { >+ state.seenTags[state.tagNumber] = true; >+ return 'tagnumber'; >+ } >+ } else { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ } >+ >+ // Don't need to do anything >+ if ( stream.eol() ) { >+ return; >+ } >+ >+ // Check for the correct space after the tag number for a control field >+ if ( !state.subAllowed && stream.pos == 3 ) { >+ if ( stream.next() == ' ' ) { >+ return 'required-space'; >+ } else { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ } >+ >+ // For a normal field, check for correct indicators and spacing >+ if ( stream.pos < 8 && state.subAllowed ) { >+ switch ( stream.pos ) { >+ case 3: >+ case 5: >+ case 7: >+ if ( stream.next() == ' ' ) { >+ return 'required-space'; >+ } else { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ case 4: >+ case 6: >+ if ( /[0-9A-Za-z_]/.test( stream.next() ) ) { >+ return 'indicator'; >+ } else { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ } >+ } >+ >+ // Otherwise, we're after the start of the line. >+ if ( state.subAllowed ) { >+ // If we don't have to match a subfield, try to consume text. >+ if ( stream.pos != 8 ) { >+ // Try to match space at the end of the line, then everything but spaces, and as >+ // a final fallback, only spaces. >+ // >+ // This is required to keep the contents matching from stepping on the end-space >+ // matching. >+ if ( stream.match( /[ \t]+$/ ) ) { >+ return 'end-space'; >+ } else if ( stream.match( /[^ \t$|Çâ¡]+/ ) || stream.match( /[ \t]+/ ) ) { >+ return; >+ } >+ } >+ >+ if ( stream.eat( /[$|Çâ¡]/ ) ) { >+ var subfieldCode; >+ if ( ( subfieldCode = stream.eat( /[a-z0-9%]/ ) ) && stream.eat( ' ' ) ) { >+ state.subfieldCode = subfieldCode; >+ if ( state.seenSubfields[state.subfieldCode] && ( modeConfig.nonRepeatableSubfields[state.tagNumber] || {} )[state.subfieldCode] ) { >+ return 'bad-subfieldcode'; >+ } else { >+ state.seenSubfields[state.subfieldCode] = true; >+ return 'subfieldcode'; >+ } >+ } >+ } >+ >+ if ( stream.pos < 11 && ( !stream.eol() || stream.pos == 8 ) ) { >+ stream.skipToEnd(); >+ return 'error'; >+ } >+ } else { >+ // Match space at end of line >+ if ( stream.match( /[ \t]+$/ ) ) { >+ return 'end-space'; >+ } else { >+ stream.match( /[ \t]+/ ); >+ } >+ >+ stream.match( /[^ \t]+/ ); >+ return; >+ } >+ } >+ }; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js >new file mode 100644 >index 0000000..f745533 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js >@@ -0,0 +1,370 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+/** >+ * Adapted and cleaned up from biblios.net, which is purportedly under the GPL. >+ * Source: http://git.librarypolice.com/?p=biblios.git;a=blob_plain;f=plugins/marc21editor/marcrecord.js;hb=master >+ * >+ * ISO2709 import/export is cribbed from marcjs, which is under the MIT license. >+ * Source: https://github.com/fredericd/marcjs/blob/master/lib/marcjs.js >+ */ >+ >+define( function() { >+ var MARC = {}; >+ >+ var _escape_map = { >+ "<": "<", >+ "&": "&", >+ "\"": """ >+ }; >+ >+ function _escape(str) { >+ return str.replace( /[<&"]/, function (c) { return _escape_map[c] } ); >+ } >+ >+ function _intpadded(i, digits) { >+ i = i + ''; >+ while (i.length < digits) { >+ i = '0' + i; >+ } >+ return i; >+ } >+ >+ MARC.Record = function (fieldlist) { >+ this._fieldlist = fieldlist || []; >+ } >+ >+ $.extend( MARC.Record.prototype, { >+ leader: function(val) { >+ var field = this.field('000'); >+ >+ if (val) { >+ if (field) { >+ field.subfield( '@', val ); >+ } else { >+ field = new MARC.Field( '000', '', '', [ [ '@', val ] ] ); >+ this.addFieldGrouped(field); >+ } >+ } else { >+ return ( field && field.subfield('@') ) || ' nam a22 7a 4500'; >+ } >+ }, >+ >+ /** >+ * If a tagnumber is given, returns all fields with that tagnumber. >+ * Otherwise, returns all fields. >+ */ >+ fields: function(fieldno) { >+ if (!fieldno) return this._fieldlist; >+ >+ var results = []; >+ for(var i=0; i<this._fieldlist.length; i++){ >+ if( this._fieldlist[i].tagnumber() == fieldno ) { >+ results.push(this._fieldlist[i]); >+ } >+ } >+ >+ return results; >+ }, >+ >+ /** >+ * Returns the first field with the given tagnumber, or false. >+ */ >+ field: function(fieldno) { >+ for(var i=0; i<this._fieldlist.length; i++){ >+ if( this._fieldlist[i].tagnumber() == fieldno ) { >+ return this._fieldlist[i]; >+ } >+ } >+ return false; >+ }, >+ >+ /** >+ * Adds the given MARC.Field to the record, at the end. >+ */ >+ addField: function(field) { >+ this._fieldlist.push(field); >+ return true; >+ }, >+ >+ /** >+ * Adds the given MARC.Field to the record, at the end of the matching >+ * x00 group. If a record has a 100, 245 and 300 field, for instance, a >+ * 260 field would be added after the 245 field. >+ */ >+ addFieldGrouped: function(field) { >+ for ( var i = this._fieldlist.length - 1; i >= 0; i-- ) { >+ if ( this._fieldlist[i].tagnumber()[0] <= field.tagnumber()[0] ) { >+ this._fieldlist.splice(i+1, 0, field); >+ return true; >+ } >+ } >+ this._fieldlist.push(field); >+ return true; >+ }, >+ >+ /** >+ * Removes the first field with the given tagnumber. Returns false if no >+ * such field was found. >+ */ >+ removeField: function(fieldno) { >+ for(var i=0; i<this._fieldlist.length; i++){ >+ if( this._fieldlist[i].tagnumber() == fieldno ) { >+ this._fieldlist.splice(i, 1); >+ return true; >+ } >+ } >+ return false; >+ }, >+ >+ /** >+ * Check to see if this record contains a field with the given >+ * tagnumber. >+ */ >+ hasField: function(fieldno) { >+ for(var i=0; i<this._fieldlist.length; i++){ >+ if( this._fieldlist[i].tagnumber() == fieldno ) { >+ return true; >+ } >+ } >+ return false; >+ }, >+ >+ toXML: function() { >+ var xml = '<record xmlns="http://www.loc.gov/MARC21/slim">'; >+ for(var i=0; i<this._fieldlist.length; i++){ >+ xml += this._fieldlist[i].toXML(); >+ } >+ xml += '</record>'; >+ return xml; >+ }, >+ >+ /** >+ * Truncates this record, and loads in the data from the given MARCXML >+ * document. >+ */ >+ loadMARCXML: function(xmldoc) { >+ var record = this; >+ record.xmlSource = xmldoc; >+ this._fieldlist.length = 0; >+ this.leader( $('leader', xmldoc).text() ); >+ $('controlfield', xmldoc).each( function(i) { >+ val = $(this).text(); >+ tagnum = $(this).attr('tag'); >+ record._fieldlist.push( new MARC.Field(tagnum, '', '', [ [ '@', val ] ]) ); >+ }); >+ $('datafield', xmldoc).each(function(i) { >+ var value = $(this).text(); >+ var tagnum = $(this).attr('tag'); >+ var ind1 = $(this).attr('ind1') || ' '; >+ var ind2 = $(this).attr('ind2') || ' '; >+ var subfields = new Array(); >+ $('subfield', this).each(function(j) { >+ var sfval = $(this).text(); >+ var sfcode = $(this).attr('code'); >+ subfields.push( [ sfcode, sfval ] ); >+ }); >+ record._fieldlist.push( new MARC.Field(tagnum, ind1, ind2, subfields) ); >+ }); >+ }, >+ >+ toISO2709: function() { >+ var FT = '\x1e', RT = '\x1d', DE = '\x1f'; >+ var directory = '', >+ from = 0, >+ chunks = ['', '']; >+ >+ $.each( this._fieldlist, function( undef, element ) { >+ var chunk = ''; >+ var tag = element.tagnumber(); >+ if (tag == '000') { >+ return; >+ } else if (tag < '010') { >+ chunk = element.subfields()[0][1]; >+ } else { >+ chunk = element.indicators().join(''); >+ $.each( element.subfields(), function( undef, subfield ) { >+ chunk += DE + subfield[0] + subfield[1]; >+ } ); >+ } >+ chunk += FT; >+ chunks.push(chunk); >+ directory += _intpadded(tag,3) + _intpadded(chunk.length,4) + _intpadded(from,5); >+ from += chunk.length; >+ }); >+ >+ chunks.push(RT); >+ directory += FT; >+ var offset = 24 + 12 * (this._fieldlist.length - 1) + 1; >+ var length = offset + from + 1; >+ var leader = this.leader(); >+ leader = _intpadded(length,5) + leader.substr(5,7) + _intpadded(offset,5) + >+ leader.substr(17); >+ chunks[0] = leader; >+ chunks[1] = directory; >+ return chunks.join(''); >+ }, >+ >+ loadISO2709: function(data) { >+ this._fieldlist.length = 0; >+ this.leader(data.substr(0, 24)); >+ var directory_len = parseInt(data.substring(12, 17), 0) - 25, >+ number_of_tag = directory_len / 12; >+ for (var i = 0; i < number_of_tag; i++) { >+ var off = 24 + i * 12, >+ tag = data.substring(off, off+3), >+ len = parseInt(data.substring(off+3, off+7), 0) - 1, >+ pos = parseInt(data.substring(off+7, off+12), 0) + 25 + directory_len, >+ value = data.substring(pos, pos+len); >+ if ( parseInt(tag) < 10 ) { >+ this.addField( new MARC.Field( tag, '', '', [ [ '@', value ] ] ) ); >+ } else { >+ if ( value.indexOf('\x1F') ) { // There are some letters >+ var ind1 = value.substr(0, 1), ind2 = value.substr(1, 1); >+ var subfields = []; >+ >+ $.each( value.substr(3).split('\x1f'), function( undef, v ) { >+ if (v.length < 2) return; >+ subfields.push([v.substr(0, 1), v.substr(1)]); >+ } ); >+ >+ this.addField( new MARC.Field( tag, ind1, ind2, subfields ) ); >+ } >+ } >+ } >+ } >+ } ); >+ >+ MARC.Field = function(tagnumber, indicator1, indicator2, subfields) { >+ this._tagnumber = tagnumber; >+ this._indicators = [ indicator1, indicator2 ]; >+ this._subfields = subfields; >+ }; >+ >+ $.extend( MARC.Field.prototype, { >+ tagnumber: function() { >+ return this._tagnumber; >+ }, >+ >+ isControlField: function() { >+ return this._tagnumber < '010'; >+ }, >+ >+ indicator: function(num, val) { >+ if( val != null ) { >+ this._indicators[num] = val; >+ } >+ return this._indicators[num]; >+ }, >+ >+ indicators: function() { >+ return this._indicators; >+ }, >+ >+ hasSubfield: function(code) { >+ for(var i = 0; i<this._subfields.length; i++) { >+ if( this._subfields[i][0] == code ) { >+ return true; >+ } >+ } >+ return false; >+ }, >+ >+ removeSubfield: function(code) { >+ for(var i = 0; i<this._subfields.length; i++) { >+ if( this._subfields[i][0] == code ) { >+ this._subfields.splice(i,1); >+ return true; >+ } >+ } >+ return false; >+ }, >+ >+ subfields: function() { >+ return this._subfields; >+ }, >+ >+ addSubfield: function(sf) { >+ this._subfields.push(sf); >+ return true; >+ }, >+ >+ addSubfieldGrouped: function(sf) { >+ function _kind( sc ) { >+ if ( /[a-z]/.test( sc ) ) { >+ return 0; >+ } else if ( /[0-9]/.test( sc ) ) { >+ return 1; >+ } else { >+ return 2; >+ } >+ } >+ >+ for ( var i = this._subfields.length - 1; i >= 0; i-- ) { >+ if ( i == 0 && _kind( sf[0] ) < _kind( this._subfields[i][0] ) ) { >+ this._subfields.splice( 0, 0, sf ); >+ return true; >+ } else if ( _kind( this._subfields[i][0] ) <= _kind( sf[0] ) ) { >+ this._subfields.splice( i + 1, 0, sf ); >+ return true; >+ } >+ } >+ >+ this._subfields.push(sf); >+ return true; >+ }, >+ >+ subfield: function(code, val) { >+ var sf = ''; >+ for(var i = 0; i<this._subfields.length; i++) { >+ if( this._subfields[i][0] == code ) { >+ sf = this._subfields[i]; >+ if( val != null ) { >+ sf[1] = val; >+ } >+ return sf[1]; >+ } >+ } >+ return false; >+ }, >+ >+ toXML: function() { >+ // decide if it's controlfield of datafield >+ if( this._tagnumber == '000') { >+ return '<leader>' + _escape( this._subfields[0][1] ) + '</leader>'; >+ } else if ( this._tagnumber < '010' ) { >+ return '<controlfield tag="' + this._tagnumber + '">' + _escape( this._subfields[0][1] ) + '</controlfield>'; >+ } else { >+ var result = '<datafield tag="' + this._tagnumber + '"'; >+ result += ' ind1="' + this._indicators[0] + '"'; >+ result += ' ind2="' + this._indicators[1] + '">'; >+ for( var i = 0; i< this._subfields.length; i++) { >+ result += '<subfield code="' + this._subfields[i][0] + '">'; >+ result += _escape( this._subfields[i][1] ); >+ result += '</subfield>'; >+ } >+ result += '</datafield>'; >+ >+ return result; >+ } >+ } >+ } ); >+ >+ return MARC; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js >new file mode 100644 >index 0000000..5cf81da >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js >@@ -0,0 +1,49 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( function() { >+ var Preferences = { >+ Load: function( borrowernumber ) { >+ if ( borrowernumber == null ) return; >+ >+ var saved_prefs; >+ try { >+ saved_prefs = JSON.parse( localStorage[ 'cateditor_preferences_' + borrowernumber ] ); >+ } catch (e) {} >+ >+ Preferences.user = $.extend( { >+ // Preference defaults >+ fieldWidgets: true, >+ font: 'monospace', >+ fontSize: '1em', >+ macros: {}, >+ selected_search_targets: {}, >+ }, saved_prefs ); >+ }, >+ >+ Save: function( borrowernumber ) { >+ if ( !borrowernumber ) return; >+ if ( !Preferences.user ) Preferences.Load(borrowernumber); >+ >+ localStorage[ 'cateditor_preferences_' + borrowernumber ] = JSON.stringify(Preferences.user); >+ }, >+ }; >+ >+ return Preferences; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js >new file mode 100644 >index 0000000..5be97d6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js >@@ -0,0 +1,38 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'module' ], function( module ) { >+ var _allResources = []; >+ >+ var Resources = { >+ GetAll: function() { >+ return $.when.call( null, _allResources ); >+ } >+ }; >+ >+ function _res( name, deferred ) { >+ Resources[name] = deferred; >+ _allResources.push(deferred); >+ } >+ >+ _res( 'marc21/xml/006', $.get( module.config().themelang + '/data/marc21_field_006.xml' ) ); >+ _res( 'marc21/xml/008', $.get( module.config().themelang + '/data/marc21_field_008.xml' ) ); >+ >+ return Resources; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js >new file mode 100644 >index 0000000..341da39 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js >@@ -0,0 +1,114 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'marc-record' ], function( MARC ) { >+ var _options; >+ var _records = {}; >+ var _last; >+ >+ var _pqfMapping = { >+ author: '1=1004', // s=al', >+ cn_dewey: '1=13', >+ cn_lc: '1=16', >+ date: '1=30', // r=r', >+ isbn: '1=7', >+ issn: '1=8', >+ lccn: '1=9', >+ local_number: '1=12', >+ music_identifier: '1=51', >+ standard_identifier: '1=1007', >+ subject: '1=21', // s=al', >+ term: '1=1016', // t=l,r s=al', >+ title: '1=4', // s=al', >+ } >+ >+ var Search = { >+ Init: function( options ) { >+ _options = options; >+ }, >+ JoinTerms: function( terms ) { >+ var q = ''; >+ >+ $.each( terms, function( i, term ) { >+ var term = '@attr ' + _pqfMapping[ term[0] ] + ' "' + term[1].replace( '"', '\\"' ) + '"' >+ >+ if ( q ) { >+ q = '@and ' + q + ' ' + term; >+ } else { >+ q = term; >+ } >+ } ); >+ >+ return q; >+ }, >+ Run: function( servers, q, options ) { >+ options = $.extend( { >+ offset: 0, >+ page_size: 20, >+ }, _options, options ); >+ >+ Search.includedServers = []; >+ _records = {}; >+ _last = { >+ servers: servers, >+ q: q, >+ options: options, >+ }; >+ >+ $.each( servers, function ( id, info ) { >+ if ( info.checked ) Search.includedServers.push( id ); >+ } ); >+ >+ $.get( >+ '/cgi-bin/koha/svc/cataloguing/metasearch', >+ { >+ q: q, >+ servers: Search.includedServers.join( ',' ), >+ offset: options.offset, >+ page_size: options.page_size, >+ sort_direction: options.sort_direction, >+ sort_key: options.sort_key, >+ resultset: options.resultset, >+ } >+ ) >+ .done( function( data ) { >+ _last.options.resultset = data.resultset; >+ $.each( data.hits, function( undef, hit ) { >+ var record = new MARC.Record(); >+ record.loadMARCXML( hit.record ); >+ hit.record = record; >+ } ); >+ >+ _options.onresults( data ); >+ } ) >+ .fail( function( error ) { >+ _options.onerror( error ); >+ } ); >+ >+ return true; >+ }, >+ Fetch: function( options ) { >+ if ( !_last ) return; >+ $.extend( _last.options, options ); >+ Search.Run( _last.servers, _last.q, _last.options ); >+ } >+ }; >+ >+ return Search; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js >new file mode 100644 >index 0000000..ba471e6 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js >@@ -0,0 +1,106 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'marc-record' ], function( MARC ) { >+ // Convert any characters for display >+ function _sanitize( text ) { >+ return text.replace( '$', '{dollar}' ); >+ } >+ >+ // Undo conversion >+ function _desanitize( text ) { >+ return text.replace( '{dollar}', '$' ); >+ } >+ return { >+ RecordToText: function( record ) { >+ var lines = []; >+ var fields = record.fields(); >+ >+ for ( var i = 0; i < fields.length; i++ ) { >+ var field = fields[i]; >+ >+ if ( field.isControlField() ) { >+ lines.push( field.tagnumber() + ' ' + _sanitize( field.subfield( '@' ) ) ); >+ } else { >+ var result = [ field.tagnumber() + ' ' ]; >+ >+ result.push( field.indicator(0) == ' ' ? '_' : field.indicator(0), ' ' ); >+ result.push( field.indicator(1) == ' ' ? '_' : field.indicator(1), ' ' ); >+ >+ $.each( field.subfields(), function( i, subfield ) { >+ result.push( '$' + subfield[0] + ' ' + _sanitize( subfield[1] ) ); >+ } ); >+ >+ lines.push( result.join('') ); >+ } >+ } >+ >+ return lines.join('\n'); >+ }, >+ >+ TextToRecord: function( text ) { >+ var record = new MARC.Record(); >+ var errors = []; >+ >+ $.each( text.split('\n'), function( i, line ) { >+ var tagNumber = line.match( /^([A-Za-z0-9]{3}) / ); >+ >+ if ( !tagNumber ) { >+ errors.push( { type: 'noTag', line: i } ); >+ return; >+ } >+ tagNumber = tagNumber[1]; >+ >+ if ( tagNumber < '010' ) { >+ var field = new MARC.Field( tagNumber, ' ', ' ', [ [ '@', _desanitize( line.substring( 4 ) ) ] ] ); >+ field.sourceLine = i; >+ record.addField( field ); >+ } else { >+ var indicators = line.match( /^... ([0-9A-Za-z_]) ([0-9A-Za-z_])/ ); >+ if ( !indicators ) { >+ errors.push( { type: 'noIndicators', line: i } ); >+ return; >+ } >+ >+ var field = new MARC.Field( tagNumber, ( indicators[1] == '_' ? ' ' : indicators[1] ), ( indicators[2] == '_' ? ' ' : indicators[2] ), [] ); >+ >+ var matcher = /[$|Çâ¡]([a-z0-9%]) /g; >+ var match; >+ >+ var subfields = []; >+ >+ while ( ( match = matcher.exec(line) ) ) { >+ subfields.push( { code: match[1], ch: match.index } ); >+ } >+ >+ $.each( subfields, function( i, subfield ) { >+ var next = subfields[ i + 1 ]; >+ >+ field.addSubfield( [ subfield.code, _desanitize( line.substring( subfield.ch + 3, next ? next.ch : line.length ) ) ] ); >+ } ); >+ >+ field.sourceLine = i; >+ record.addField( field ); >+ } >+ } ); >+ >+ return errors.length ? { errors: errors } : record; >+ } >+ }; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js >new file mode 100644 >index 0000000..35d4727 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js >@@ -0,0 +1,310 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'resources' ], function( Resources ) { >+ var _widgets = {}; >+ >+ var Widget = { >+ Register: function( tagfield, widget ) { >+ _widgets[tagfield] = widget; >+ }, >+ >+ PadNum: function( number, length ) { >+ var result = number.toString(); >+ >+ while ( result.length < length ) result = '0' + result; >+ >+ return result; >+ }, >+ >+ PadString: function( result, length ) { >+ while ( result.length < length ) result = ' ' + result; >+ >+ return result; >+ }, >+ >+ PadStringRight: function( result, length ) { >+ result = '' + result; >+ while ( result.length < length ) result += ' '; >+ >+ return result; >+ }, >+ >+ Base: { >+ // Marker utils >+ clearToText: function() { >+ var range = this.mark.find(); >+ if ( this.text == null ) throw new Error('Tried to clear widget with no text'); >+ this.mark.doc.replaceRange( this.text, range.from, range.to, 'widget.clearToText' ); >+ }, >+ >+ reCreate: function() { >+ this.postCreate( this.node, this.mark ); >+ }, >+ >+ // Fixed field utils >+ bindFixed: function( sel, start, end ) { >+ var $node = $( this.node ).find( sel ); >+ $node.val( this.getFixed( start, end ) ); >+ >+ var widget = this; >+ var $collapsed = $( '<span class="fixed-collapsed" title="' + $node.attr('title') + '">' + $node.val() + '</span>' ).insertAfter( $node ); >+ >+ function show() { >+ $collapsed.hide(); >+ $node.val( widget.getFixed( start, end ).replace(/\s+$/, '') ); >+ $node.show(); >+ $node[0].focus(); >+ } >+ >+ function hide() { >+ $node.hide(); >+ $collapsed.text( Widget.PadStringRight( $node.val(), end - start ) ).show(); >+ } >+ >+ $node.on( 'change keyup', function() { >+ widget.setFixed( start, end, $node.val(), '+input' ); >+ } ).focus( show ).blur( hide ); >+ >+ hide(); >+ >+ $collapsed.click( show ); >+ }, >+ >+ getFixed: function( start, end ) { >+ return this.text.substring( start, end ); >+ }, >+ >+ setFixed: function( start, end, value, source ) { >+ this.setText( this.text.substring( 0, start ) + Widget.PadStringRight( value.toString().substr( 0, end - start ), end - start ) + this.text.substring( end ), source ); >+ }, >+ >+ setText: function( text, source ) { >+ if ( source == '+input' ) this.mark.doc.cm.addLineClass( this.mark.find().from.line, 'wrapper', 'modified-line' ); >+ this.text = text; >+ this.editor.startNotify(); >+ }, >+ >+ createFromXML: function( resourceId ) { >+ var widget = this; >+ >+ Resources[resourceId].done( function( xml ) { >+ $(widget.node).find('.widget-loading').remove(); >+ var $matSelect = $('<select class="material-select"></select>').appendTo(widget.node); >+ var $contents = $('<span class="material-contents"/>').appendTo(widget.node); >+ var materialInfo = {}; >+ >+ $('Tagfield', xml).children('Material').each( function() { >+ $matSelect.append( '<option value="' + $(this).attr('id') + '">' + $(this).attr('id') + ' - ' + $(this).children('name').text() + '</option>' ); >+ >+ materialInfo[ $(this).attr('id') ] = this; >+ } ); >+ >+ $matSelect.change( function() { >+ widget.loadXMLMaterial( materialInfo[ $matSelect.val() ] ); >+ } ).change(); >+ } ); >+ }, >+ >+ loadXMLMaterial: function( materialInfo ) { >+ var $contents = $(this.node).children('.material-contents'); >+ $contents.empty(); >+ >+ var widget = this; >+ >+ $(materialInfo).children('Position').each( function() { >+ var match = $(this).attr('pos').match(/(\d+)(?:-(\d+))?/); >+ if (!match) return; >+ >+ var start = parseInt(match[1]); >+ var end = ( match[2] ? parseInt(match[2]) : start ) + 1; >+ var $input; >+ var $values = $(this).children('Value'); >+ >+ if ($values.length == 0) { >+ $contents.append( '<span title="' + $(this).children('name').text() + '">' + widget.getFixed(start, end) + '</span>' ); >+ return; >+ } >+ >+ if ( match[2] ) { >+ $input = $( '<input name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '" maxlength="' + (end - start) + '" />' ); >+ } else { >+ $input = $( '<select name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '"></select>' ); >+ >+ $values.each( function() { >+ $input.append( '<option value="' + $(this).attr('code') + '">' + $(this).attr('code') + ' - ' + $(this).children('description').text() + '</option>' ); >+ } ); >+ } >+ >+ $contents.append( $input ); >+ widget.bindFixed( $input, start, end ); >+ } ); >+ }, >+ >+ nodeChanged: function() { >+ this.mark.changed(); >+ var widget = this; >+ >+ var $inputs = $(this.node).find('input, select'); >+ if ( !$inputs.length ) return; >+ >+ $inputs.off('keydown.marc-tab'); >+ var editor = widget.editor; >+ >+ $inputs.each( function( i ) { >+ $(this).on( 'keydown.marc-tab', function( e ) { >+ if ( e.which != 9 ) return; // Tab >+ >+ var span = widget.mark.find(); >+ var cur = editor.cm.getCursor(); >+ >+ if ( e.shiftKey ) { >+ if ( i > 0 ) { >+ $inputs.eq(i - 1).trigger( 'focus' ); >+ } else { >+ editor.cm.setCursor( span.from ); >+ // FIXME: ugly hack >+ editor.cm.options.extraKeys['Shift-Tab']( editor.cm ); >+ editor.focus(); >+ } >+ } else { >+ if ( i < $inputs.length - 1 ) { >+ $inputs.eq(i + 1).trigger( 'focus' ); >+ } else { >+ editor.cm.setCursor( span.to ); >+ editor.focus(); >+ } >+ } >+ >+ return false; >+ } ); >+ } ); >+ }, >+ >+ // Template utils >+ insertTemplate: function( sel ) { >+ var wsOnly = /^\s*$/; >+ $( sel ).contents().clone().each( function() { >+ if ( this.nodeType == Node.TEXT_NODE ) { >+ this.data = this.data.replace( /^\s+|\s+$/g, '' ); >+ } >+ } ).appendTo( this.node ); >+ }, >+ }, >+ >+ ActivateAt: function( editor, cur, idx ) { >+ var marks = editor.findMarksAt( cur ); >+ if ( !marks.length ) return false; >+ >+ var $input = $(marks[0].widget.node).find('input, select').eq(idx || 0); >+ if ( !$input.length ) return false; >+ >+ $input.focus(); >+ return true; >+ }, >+ >+ Notify: function( editor ) { >+ $.each( editor.cm.getAllMarks(), function( undef, mark ) { >+ if ( mark.widget && mark.widget.notify ) mark.widget.notify(); >+ } ); >+ }, >+ >+ UpdateLine: function( editor, line ) { >+ var info = editor.getLineInfo( { line: line, ch: 0 } ); >+ var lineh = editor.cm.getLineHandle( line ); >+ if ( !lineh ) return; >+ >+ if ( !info ) { >+ if ( lineh.markedSpans ) { >+ $.each( lineh.markedSpans, function ( undef, span ) { >+ var mark = span.marker; >+ if ( !mark.widget ) return; >+ >+ mark.widget.clearToText(); >+ } ); >+ } >+ return; >+ } >+ >+ var subfields = []; >+ >+ var end = editor.cm.getLine( line ).length; >+ if ( info.tagNumber < '010' ) { >+ if ( end >= 4 ) subfields.push( { code: '@', from: 4, to: end } ); >+ } else { >+ for ( var i = 0; i < info.subfields.length; i++ ) { >+ var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end; >+ subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 3, to: next } ); >+ } >+ // If not a fixed field, and we didn't find any subfields, we need to throw in the >+ // '@' subfield so we can properly remove it >+ if ( subfields.length == 0 ) subfields.push( { code: '@', from: 4, to: end } ); >+ } >+ >+ $.each( subfields, function ( undef, subfield ) { >+ var id = info.tagNumber + subfield.code; >+ var marks = editor.cm.findMarksAt( { line: line, ch: subfield.from } ); >+ >+ if ( marks.length ) { >+ if ( marks[0].id == id ) { >+ return; >+ } else { >+ marks[0].widget.clearToText(); >+ } >+ } >+ >+ if ( !_widgets[id] ) return; >+ var fullBase = $.extend( Object.create( Widget.Base ), _widgets[id] ); >+ var widget = Object.create( fullBase ); >+ >+ if ( subfield.from == subfield.to ) { >+ editor.cm.replaceRange( widget.makeTemplate ? widget.makeTemplate() : '<empty>', { line: line, ch: subfield.from }, null, 'marcWidgetPrefill' ); >+ return; // We'll do the actual work when the change event is triggered again >+ } >+ >+ var text = editor.cm.getRange( { line: line, ch: subfield.from }, { line: line, ch: subfield.to } ); >+ >+ widget.text = text; >+ var node = widget.init(); >+ >+ var mark = editor.cm.markText( { line: line, ch: subfield.from }, { line: line, ch: subfield.to }, { >+ atomic: true, >+ inclusiveLeft: false, >+ inclusiveRight: false, >+ replacedWith: node, >+ } ); >+ >+ mark.id = id; >+ mark.widget = widget; >+ >+ widget.node = node; >+ widget.mark = mark; >+ widget.editor = editor; >+ >+ if ( widget.postCreate ) { >+ widget.postCreate(); >+ } >+ >+ widget.nodeChanged(); >+ } ); >+ }, >+ }; >+ >+ return Widget; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css >new file mode 100644 >index 0000000..04258a5 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css >@@ -0,0 +1,434 @@ >+/*> Infrastructure */ >+body { >+ padding: 0; >+} >+ >+#loading { >+ background-color: #FFF; >+ cursor: wait; >+ height: 100%; >+ left: 0; >+ opacity: .7; >+ position: fixed; >+ top: 0; >+ width: 100%; >+ z-index: 1000; >+} >+ >+#loading div { >+ background : transparent url(../../img/loading.gif) top left no-repeat; >+ font-size : 175%; >+ font-weight: bold; >+ height: 2em; >+ left: 50%; >+ margin: -1em 0 0 -2.5em; >+ padding-left : 50px; >+ position: absolute; >+ top: 50%; >+ width: 15em; >+} >+ >+#alerts-container { >+ font-size: 12px; >+} >+ >+#alerts-container h3 { >+ font-size: inherit; >+} >+ >+#alerts-container > ul { >+ padding: 0; >+} >+ >+#alerts-container > ul > li { >+ border-bottom: 1px solid #DDD; >+ display: block; >+ padding: 4px 0; >+} >+ >+#alerts-container > ul > li:first-child { >+ padding-top: 0; >+} >+ >+#alerts-container > ul > li:last-child { >+ border-bottom: none; >+ padding-bottom: 0; >+} >+ >+/*> MARC editor */ >+#editor .CodeMirror { >+ line-height: 1.2; >+} >+ >+.cm-tagnumber { >+ color: #080; >+ font-weight: bold; >+} >+ >+.cm-bad-tagnumber { >+ color: #A20; >+ font-weight: bold; >+} >+ >+.cm-indicator { >+ color: #884; >+} >+ >+.cm-subfieldcode { >+ background-color: #F4F4F4; >+ color: #187848; >+ border-radius: 3px 8px 8px 3px; >+ border-right: 2px solid white; >+ font-weight: bold; >+ margin-right: -2px; >+} >+ >+.cm-bad-subfieldcode { >+ background-color: #FFD9D9; >+ color: #482828; >+ border-radius: 3px 8px 8px 3px; >+ font-weight: bold; >+} >+ >+.cm-end-space { >+ background-color: #DDDDBB; >+} >+ >+#editor .modified-line-gutter { >+ width: 10px; >+} >+ >+#editor .modified-line { >+ background: #F8F8F8; >+ border-left: 5px solid black; >+ margin-left: -10px; >+ padding-left: 5px; >+} >+ >+#editor .CodeMirror-gutters { >+ background: transparent; >+ border-right: none; >+} >+ >+/*> MARC editor widgets */ >+ >+#editor .subfield-widget { >+ color: #538200; >+ border: solid 2px #538200; >+ border-radius: 6px; >+ font-family: inherit; >+ line-height: 2.75; >+ margin: 3px 0; >+ padding: 4px; >+} >+ >+#editor .subfield-widget select, #editor .subfield-widget input { >+ height: 1.5em; >+ vertical-align: middle; >+} >+ >+#editor .subfield-widget select:focus { >+ outline: 2px #83A230 solid; >+} >+ >+#editor .fixed-widget input { >+ width: 4em; >+} >+ >+#editor .fixed-widget select { >+ width: 3em; >+} >+ >+#editor .fixed-widget .material-select { >+ width: 4.5em; >+ margin-right: .5em; >+} >+ >+#editor .fixed-collapsed { >+ display: inline-block; >+ margin: 0 .25em; >+ text-align: center; >+ text-decoration: underline; >+} >+ >+#editor .hidden-widget { >+ color: #999999; >+ border: solid 2px #AAAAAA; >+ line-height: 2; >+ padding: 2px; >+} >+ >+.structure-error { >+ background: #FFEEEE; >+ font-size: 0.9em; >+ line-height: 1.5; >+ margin: .5em; >+ padding: 0 .5em; >+} >+ >+.structure-error i { >+ vertical-align: text-bottom; >+} >+ >+#statusbar { >+ background-color: #F4F8F9; >+ border: solid 2px #b9d8d9; >+ border-bottom-style: none; >+ border-radius: 6px 6px 0 0; >+ height: 18px; >+ margin-bottom: -32px; >+ overflow: auto; >+ padding: 4px; >+ padding-bottom: 0; >+} >+ >+#statusbar #status-tag-info, #statusbar #status-subfield-info { >+ float: left; >+ overflow: hidden; >+ padding-right: 2%; >+ width: 48%; >+} >+ >+#record-info .label { >+ float: none; >+} >+ >+#record-info .label + span { >+ display: block; >+ padding-left: 1em; >+} >+ >+/*> Search */ >+ >+#advanced-search-ui, #search-results-ui, #macro-ui { >+ padding: 5px; >+ width: 90%; >+} >+ >+.modal-body { >+ max-height: none; >+ padding: 0; >+} >+ >+#quicksearch-overlay { >+ background: rgba(255, 255, 255, .9); >+ border: 2px solid #CC8877; >+ border-radius: 5px; >+ -moz-box-sizing: border-box; >+ -webkit-box-sizing: border-box; >+ box-sizing: border-box; >+ color: #664444; >+ position: relative; >+ vertical-align: middle; >+} >+ >+#quicksearch-overlay h3 { >+ font-size: 1.5em%; >+ margin: 0; >+ text-align: center; >+ padding: 50px 5px; >+} >+ >+#quicksearch-overlay p { >+ bottom: 0; >+ font-size: .8em; >+ overflow: hidden; >+ padding: 8px 15px; >+ position: absolute; >+ text-align: center; >+} >+ >+#quicksearch input, #quicksearch a { >+ font-size: 1.2em; >+ padding: 3px 0; >+ width: 96%; /* I have no idea why this is necessary */ >+} >+ >+#show-advanced-search { >+ display: block; >+ margin-top: .3em; >+} >+ >+#advanced-search-fields { >+ -moz-column-width: 26em; >+ -webkit-column-width: 26em; >+ column-width: 26em; >+ margin: 0; >+ padding: 0; >+} >+ >+#advanced-search-fields li { >+ display: block; >+ list-style-type: none; >+} >+ >+#advanced-search-fields label { >+ display: inline-block; >+ font-weight: bold; >+ padding: 1em 1em 1em 0; >+ width: 10em; >+ text-align: right; >+} >+ >+#advanced-search-fields input { >+ display: inline-block; >+ margin: 0px auto; >+ width: 14em; >+} >+ >+.icon-loading { >+ display: inline-block; >+ height: 16px; >+ width: 16px; >+ background: transparent url("../../img/spinner-small.gif") top left no-repeat; >+ padding: -1px; >+ vertical-align: text-top; >+} >+ >+/*> Search results */ >+ >+#search-serversinfo li { >+ list-style-type: none; >+} >+ >+#search-serversinfo .search-toggle-server { >+ margin-right: 5px; >+} >+ >+#searchresults table { >+ width: 100%; >+} >+ >+.sourcecol { >+ width: 50px; >+} >+ >+.results-info { >+ height: 100px; >+ overflow: auto; >+} >+ >+.toolscol { >+ padding: 0; >+ width: 100px; >+} >+ >+.toolscol ul { >+ margin: 0; >+ padding: 0; >+} >+ >+#searchresults .toolscol li { >+ list-style-type: none; >+ list-style-image: none; >+} >+ >+.toolscol a { >+ border-bottom: 1px solid #BCBCBC; >+ display: block; >+ padding: 0 1em; >+ line-height: 24px; >+} >+ >+.marccol { >+ font-family: monospace; >+ height: auto; >+ white-space: pre-wrap; >+} >+ >+#searchresults { >+ position: relative; >+} >+ >+#search-overlay { >+ background: white; >+ bottom: 0; >+ font-size: 2em; >+ left: 0; >+ opacity: .7; >+ padding: 2em; >+ position: absolute; >+ right: 0; >+ text-align: center; >+ top: 0; >+ z-index: 9001; >+} >+ >+/*> Macros */ >+ >+#macro-ui .CodeMirror { >+ width: 100%; >+} >+ >+#macro-save-message { >+ color: #666; >+ font-size: 13px; >+ float: right; >+ line-height: 26px; >+} >+ >+#macro-list > li { >+ border: 2px solid #F0F0F0; >+ border-radius: 6px; >+ display: block; >+ font-size: 115%; >+} >+ >+#macro-list > li + li { >+ margin-top: -2px; >+} >+ >+#macro-list .active { >+ background: #EDF4F6; >+ border-color: none; >+} >+ >+#macro-list a { >+ display: block; >+ padding: 6px; >+} >+ >+#macro-list a:focus { >+ outline: none; >+} >+ >+.macro-info { >+ background-color: #F4F4F4; >+ display: none; >+ margin: 0; >+ padding: 10px; >+ text-align: right; >+} >+ >+.macro-info li { >+ color: #666; >+ font-size: 75%; >+ list-style-type: none; >+} >+ >+.macro-info .label { >+ clear: left; >+ font-weight: bold; >+ float: left; >+} >+ >+#macro-list .active .macro-info { >+ display: block; >+} >+ >+.btn-toolbar label, .btn-toolbar select { >+ font-size: 13px; >+ vertical-align: middle; >+} >+ >+.btn-toolbar label { >+ margin-left: 1em; >+} >+ >+.btn-toolbar select { >+ padding: 2px; >+} >+ >+#macro-editor .CodeMirror { >+ height: 100%; >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >index 23066ea..ff87531 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >+++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css >@@ -418,13 +418,18 @@ dd { > font-weight : normal; > } > >-div#toolbar { >+.btn-toolbar { > background-color : #EDF4F6; > padding: 5px 5px 5px 5px; > border-radius: 5px 5px 0 0; > border: 1px solid #E6F0F2; > } > >+.btn-toolbar .yui-menu-button button, >+.btn-toolbar .yui-button-button button { >+ line-height : 1.7em; >+} >+ > ul.toolbar { > padding-left : 0; > } >@@ -2412,8 +2417,8 @@ video { > background-position:-48px -166px; > } > >-#toolbar .btn, >-#toolbar .dropdown-menu { >+.btn-toolbar .btn, >+.btn-toolbar .dropdown-menu { > font-size: 13px; > } > a.btn:link, >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc >new file mode 100644 >index 0000000..89a11ec >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc >@@ -0,0 +1,1051 @@ >+<script src="/intranet-tmpl/lib/codemirror/codemirror-compressed.js"></script> >+<script src="/intranet-tmpl/lib/filesaver.js"></script> >+<script src="/intranet-tmpl/lib/koha/cateditor/marc-mode.js"></script> >+<script src="/intranet-tmpl/lib/require.js"></script> >+<script> >+require.config( { >+ baseUrl: '/intranet-tmpl/lib/koha/cateditor/', >+ config: { >+ resources: { >+ themelang: '[% themelang %]', >+ }, >+ }, >+ waitSeconds: 30, >+} ); >+</script> >+ >+[% IF marcflavour == 'MARC21' %] >+[% PROCESS 'cateditor-widgets-marc21.inc' %] >+[% ELSE %] >+<script>var editorWidgets = {};</script> >+[% END %] >+ >+<script> >+require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) { >+ var z3950Servers = { >+ 'koha:biblioserver': { >+ name: _("Local catalog"), >+ recordtype: 'biblio', >+ checked: false, >+ }, >+ [%- FOREACH server = z3950_servers -%] >+ [% server.id %]: { >+ name: '[% server.servername %]', >+ recordtype: '[% server.recordtype %]', >+ checked: [% server.checked ? 'true' : 'false' %], >+ }, >+ [%- END -%] >+ }; >+ >+ // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config >+ var z3950Labels = [ >+ [ "local_number", _("Local number") ], >+ [ "title", _("Title") ], >+ [ "series", _("Series title") ], >+ [ "author", _("Author") ], >+ [ "lccn", _("LCCN") ], >+ [ "isbn", _("ISBN") ], >+ [ "issn", _("ISSN") ], >+ [ "medium", _("Medium") ], >+ [ "edition", _("Edition") ], >+ [ "notes", _("Notes") ], >+ ]; >+ >+ var state = { >+ backend: '', >+ saveBackend: 'catalog', >+ recordID: undefined >+ }; >+ >+ var editor; >+ var macroEditor; >+ >+ function makeAuthorisedValueWidgets( frameworkCode ) { >+ $.each( KohaBackend.GetAllTagsInfo( frameworkCode ), function( tag, tagInfo ) { >+ $.each( tagInfo.subfields, function( subfield, subfieldInfo ) { >+ if ( !subfieldInfo.authorised_value ) return; >+ var authvals = KohaBackend.GetAuthorisedValues( subfieldInfo.authorised_value ); >+ if ( !authvals ) return; >+ >+ var defaultvalue = subfield.defaultvalue || authvals[0].value; >+ >+ Widget.Register( tag + subfield, { >+ init: function() { >+ var $result = $( '<span class="subfield-widget"></span>' ); >+ >+ return $result[0]; >+ }, >+ postCreate: function() { >+ this.setText( defaultvalue ); >+ >+ $( '<select></select>' ).appendTo( this.node ); >+ var $node = $( this.node ).find( 'select' ); >+ $.each( authvals, function( undef, authval ) { >+ $node.append( '<option value="' + authval.value + '"' + (authval.value == defaultvalue ? ' selected="selected"' : '') + '>' + authval.lib + '</option>' ); >+ } ); >+ $node.val( this.text ); >+ >+ $node.change( $.proxy( function() { >+ this.setText( $node.val() ); >+ }, this ) ); >+ }, >+ makeTemplate: function() { >+ return defaultvalue; >+ }, >+ } ); >+ } ); >+ } ); >+ } >+ >+ function bindGlobalKeys() { >+ shortcut.add( 'ctrl+s', function(event) { >+ $( '#save-record' ).click(); >+ >+ event.preventDefault(); >+ } ); >+ >+ shortcut.add( 'alt+ctrl+k', function(event) { >+ if ( Search.IsAvailable() ) $( '#search-by-keywords' ).focus(); >+ >+ return false; >+ } ); >+ >+ shortcut.add( 'alt+ctrl+a', function(event) { >+ if ( Search.IsAvailable() ) $( '#search-by-author' ).focus(); >+ >+ return false; >+ } ); >+ >+ shortcut.add( 'alt+ctrl+i', function(event) { >+ if ( Search.IsAvailable() ) $( '#search-by-isbn' ).focus(); >+ >+ return false; >+ } ); >+ >+ shortcut.add( 'alt+ctrl+t', function(event) { >+ if ( Search.IsAvailable() ) $( '#search-by-title' ).focus(); >+ >+ return false; >+ } ); >+ >+ shortcut.add( 'ctrl+h', function() { >+ var field = editor.getCurrentField(); >+ >+ if ( !field ) return; >+ >+ window.open( getFieldHelpURL( field.tag ) ); >+ } ); >+ >+ $('#quicksearch .search-box').each( function() { >+ shortcut.add( 'enter', $.proxy( function() { >+ var terms = []; >+ >+ $('#quicksearch .search-box').each( function() { >+ if ( !this.value ) return; >+ >+ terms.push( [ $(this).data('qualifier'), this.value ] ); >+ } ); >+ >+ if ( !terms.length ) return; >+ >+ if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) { >+ $("#search-overlay").show(); >+ showResultsBox(); >+ } >+ >+ return false; >+ }, this), { target: this, type: 'keypress' } ); >+ } ); >+ } >+ >+ function getFieldHelpURL( tag ) { >+ [% IF ( marcflavour == 'MARC21' ) %] >+ if ( tag == '000' ) { >+ return "http://www.loc.gov/marc/bibliographic/bdleader.html"; >+ } else if ( tag < '900' ) { >+ return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html"; >+ } else { >+ return "http://www.loc.gov/marc/bibliographic/bd9xx.html"; >+ } >+ [% ELSIF ( marcflavour == 'UNIMARC' ) %] >+ /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but >+ seems to be the only version available that can be linked to per tag. More recent >+ versions of the UNIMARC standard are available on the IFLA website only as >+ PDFs! >+ */ >+ if ( tag == '000' ) { >+ return "http://archive.ifla.org/VI/3/p1996-1/uni.htm"; >+ } else { >+ var first = tag[0]; >+ var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#"; >+ if ( first == '0' ) url += "b"; >+ if ( first != '9' ) url += field; >+ >+ return url; >+ } >+ [% END %] >+ } >+ >+ // Record loading >+ var backends = { >+ 'new': { >+ titleForRecord: _("Editing new record"), >+ get: function( id, callback ) { >+ record = new MARC.Record(); >+ KohaBackend.FillRecord( '', record ); >+ >+ callback( record ); >+ }, >+ }, >+ 'new-full': { >+ titleForRecord: _("Editing new full record"), >+ get: function( id, callback ) { >+ record = new MARC.Record(); >+ KohaBackend.FillRecord( '', record, true ); >+ >+ callback( record ); >+ }, >+ }, >+ 'catalog': { >+ titleForRecord: _("Editing catalog record #{ID}"), >+ links: [ >+ { title: _("view"), href: "/cgi-bin/koha/catalogue/detail.pl?biblionumber={ID}" }, >+ { title: _("edit items"), href: "/cgi-bin/koha/cataloguing/additem.pl?biblionumber={ID}" }, >+ ], >+ saveLabel: _("Save to catalog"), >+ get: function( id, callback ) { >+ if ( !id ) return false; >+ >+ KohaBackend.GetRecord( id, callback ); >+ }, >+ save: function( id, record, done ) { >+ function finishCb( data ) { >+ done( { error: data.error, newRecord: data.marcxml && data.marcxml[0], newId: data.biblionumber && [ 'catalog', data.biblionumber ] } ); >+ } >+ >+ if ( id ) { >+ KohaBackend.SaveRecord( id, record, finishCb ); >+ } else { >+ KohaBackend.CreateRecord( record, finishCb ); >+ } >+ } >+ }, >+ 'iso2709': { >+ saveLabel: _("Save as ISO2709 (.mrc) file"), >+ save: function( id, record, done ) { >+ saveAs( new Blob( [record.toISO2709()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.mrc' ); >+ >+ done( {} ); >+ } >+ }, >+ 'marcxml': { >+ saveLabel: _("Save as MARCXML (.xml) file"), >+ save: function( id, record, done ) { >+ saveAs( new Blob( [record.toXML()], { 'type': 'application/octet-stream;charset=utf-8' } ), 'record.xml' ); >+ >+ done( {} ); >+ } >+ }, >+ 'search': { >+ titleForRecord: _("Editing search result"), >+ get: function( id, callback ) { >+ if ( !id ) return false; >+ if ( !backends.search.records[ id ] ) { >+ callback( { error: _( "Invalid record" ) } ); >+ return false; >+ } >+ >+ callback( backends.search.records[ id ] ); >+ }, >+ records: {}, >+ }, >+ }; >+ >+ function setSource(parts) { >+ state.backend = parts[0]; >+ state.recordID = parts[1]; >+ state.canSave = backends[ state.backend ].save != null; >+ state.saveBackend = state.canSave ? state.backend : 'catalog'; >+ >+ var backend = backends[state.backend]; >+ >+ document.location.hash = '#' + parts[0] + '/' + parts[1]; >+ >+ $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) ); >+ >+ $.each( backend.links || [], function( i, link ) { >+ $('#title').append(' <a target="_blank" href="' + link.href.replace( '{ID}', parts[1] ) + '">(' + link.title + ')</a>' ); >+ } ); >+ $( 'title', document.head ).html( _("Koha › Cataloging › ") + backend.titleForRecord.replace( '{ID}', parts[1] ) ); >+ $('#save-record span').text( backends[ state.saveBackend ].saveLabel ); >+ } >+ >+ function saveRecord( recid, editor, callback ) { >+ var parts = recid.split('/'); >+ if ( parts.length != 2 ) return false; >+ >+ if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false; >+ >+ editor.removeErrors(); >+ var record = editor.getRecord(); >+ >+ if ( record.errors ) { >+ state.saving = false; >+ callback( { error: 'syntax', errors: record.errors } ); >+ return; >+ } >+ >+ var errors = KohaBackend.ValidateRecord( '', record ); >+ if ( errors.length ) { >+ state.saving = false; >+ callback( { error: 'invalid', errors: errors } ); >+ return; >+ } >+ >+ backends[ parts[0] ].save( parts[1], record, function(data) { >+ state.saving = false; >+ >+ if (data.newRecord) { >+ var record = new MARC.Record(); >+ record.loadMARCXML(data.newRecord); >+ editor.displayRecord( record ); >+ } >+ >+ if (data.newId) { >+ setSource(data.newId); >+ } else { >+ setSource( [ state.backend, state.recordID ] ); >+ } >+ >+ if (callback) callback( data ); >+ } ); >+ } >+ >+ function loadRecord( recid, editor, callback ) { >+ var parts = recid.split('/'); >+ if ( parts.length != 2 ) return false; >+ >+ if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false; >+ >+ backends[ parts[0] ].get( parts[1], function( record ) { >+ if ( !record.error ) { >+ editor.displayRecord( record ); >+ editor.focus(); >+ } >+ >+ if (callback) callback(record); >+ } ); >+ >+ return true; >+ } >+ >+ function openRecord( recid, editor, callback ) { >+ return loadRecord( recid, editor, function ( record ) { >+ setSource( recid.split('/') ); >+ >+ if (callback) callback( record ); >+ } ); >+ } >+ >+ // Search functions >+ function showAdvancedSearch() { >+ $('#advanced-search-servers').empty(); >+ $.each( z3950Servers, function( server_id, server ) { >+ $('#advanced-search-servers').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</li>' ); >+ } ); >+ $('#advanced-search-ui').modal('show'); >+ } >+ >+ function startAdvancedSearch() { >+ var terms = []; >+ >+ $('#advanced-search-ui .search-box').each( function() { >+ if ( !this.value ) return; >+ >+ terms.push( [ $(this).data('qualifier'), this.value ] ); >+ } ); >+ >+ if ( !terms.length ) return; >+ >+ if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) { >+ $('#advanced-search-ui').modal('hide'); >+ $("#search-overlay").show(); >+ showResultsBox(); >+ } >+ } >+ >+ function showResultsBox(data) { >+ $('#search-top-pages, #search-bottom-pages').find('.pagination').empty(); >+ $('#searchresults thead tr').empty(); >+ $('#searchresults tbody').empty(); >+ $('#search-serversinfo').empty().append('<li>' + _("Loading...") + '</li>'); >+ $('#search-results-ui').modal('show'); >+ } >+ >+ function showSearchSorting( sort_key, sort_direction ) { >+ var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]'); >+ $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' ); >+ >+ if ( sort_direction == 'asc' ) { >+ direction = 'asc'; >+ $th.attr( 'class', 'sorting_asc' ); >+ } else { >+ direction = 'desc'; >+ $th.attr( 'class', 'sorting_desc' ); >+ } >+ } >+ >+ function showSearchResults( editor, data ) { >+ backends.search.records = {}; >+ >+ $('#searchresults thead tr').empty(); >+ $('#searchresults tbody').empty(); >+ $('#search-serversinfo').empty(); >+ >+ $.each( z3950Servers, function( server_id, server ) { >+ var num_fetched = data.num_fetched[server_id]; >+ >+ if ( data.errors[server_id] ) { >+ num_fetched = data.errors[server_id]; >+ } else if ( num_fetched == null ) { >+ num_fetched = '-'; >+ } else if ( num_fetched < data.num_hits[server_id] ) { >+ num_fetched += '+'; >+ } >+ >+ $('#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>' ); >+ } ); >+ >+ var seenColumns = {}; >+ >+ $.each( data.hits, function( undef, hit ) { >+ $.each( hit.metadata, function(key) { >+ seenColumns[key] = true; >+ } ); >+ } ); >+ >+ $('#searchresults thead tr').append('<th>' + _("Source") + '</th>'); >+ >+ $.each( z3950Labels, function( undef, label ) { >+ if ( seenColumns[ label[0] ] ) { >+ $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' ); >+ } >+ } ); >+ >+ showSearchSorting( data.sort_key, data.sort_direction ); >+ >+ $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>'); >+ >+ $.each( data.hits, function( undef, hit ) { >+ backends.search.records[ hit.server + ':' + hit.index ] = hit.record; >+ hit.id = 'search/' + hit.server + ':' + hit.index; >+ >+ var result = '<tr>'; >+ result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>'; >+ >+ $.each( z3950Labels, function( undef, label ) { >+ if ( !seenColumns[ label[0] ] ) return; >+ >+ if ( hit.metadata[ label[0] ] ) { >+ result += '<td class="infocol">' + hit.metadata[ label[0] ] + '</td>'; >+ } else { >+ result += '<td class="infocol"> </td>'; >+ } >+ } ); >+ >+ result += '<td class="toolscol"><ul><li><a href="#" class="marc-link">' + _("View MARC") + '</a></li>'; >+ result += '<li><a href="#" class="open-link">' + _("Import") + '</a></li>'; >+ if ( state.canSave ) result += '<li><a href="#" class="substitute-link" title="' + _("Replace the current record's contents") + '">' + _("Substitute") + '</a></li>'; >+ result += '</ul></td></tr>'; >+ >+ var $tr = $( result ); >+ $tr.find( '.marc-link' ).click( function() { >+ var $info_columns = $tr.find( '.infocol' ); >+ var $marc_column = $tr.find( '.marccol' ); >+ >+ if ( !$marc_column.length ) { >+ $marc_column = $( '<td class="marccol" colspan="' + $info_columns.length + '"></td>' ).insertAfter( $info_columns.eq(-1) ).hide(); >+ CodeMirror.runMode( TextMARC.RecordToText( hit.record ), 'marc', $marc_column[0] ); >+ } >+ >+ if ( $marc_column.is(':visible') ) { >+ $tr.find('.marc-link').text( _("View MARC") ); >+ $info_columns.show(); >+ $marc_column.hide(); >+ } else { >+ $tr.find('.marc-link').text( _("Hide MARC") ); >+ $marc_column.show(); >+ $info_columns.hide(); >+ } >+ >+ return false; >+ } ); >+ $tr.find( '.open-link' ).click( function() { >+ $( '#search-results-ui' ).modal('hide'); >+ openRecord( hit.id, editor ); >+ >+ return false; >+ } ); >+ $tr.find( '.substitute-link' ).click( function() { >+ $( '#search-results-ui' ).modal('hide'); >+ loadRecord( hit.id, editor ); >+ >+ return false; >+ } ); >+ $('#searchresults tbody').append( $tr ); >+ } ); >+ >+ var pages = []; >+ var cur_page = data.offset / data.page_size; >+ var max_page = Math.ceil( data.total_fetched / data.page_size ) - 1; >+ >+ if ( cur_page != 0 ) { >+ pages.push( '<li><a class="search-nav" href="#" data-offset="' + (data.offset - data.page_size) + '">« ' + _("Previous") + '</a></li>' ); >+ } >+ >+ for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) { >+ if ( page == cur_page ) { >+ pages.push( ' <li class="active"><a href="#">' + ( page + 1 ) + '</a></li>' ); >+ } else { >+ pages.push( ' <li><a class="search-nav" href="#" data-offset="' + ( page * data.page_size ) + '">' + ( page + 1 ) + '</a></li>' ); >+ } >+ } >+ >+ if ( cur_page < max_page ) { >+ pages.push( ' <li><a class="search-nav" href="#" data-offset="' + (data.offset + data.page_size) + '">' + _("Next") + ' »</a></li>' ); >+ } >+ >+ if ( pages.length > 1 ) $( '#search-top-pages, #search-bottom-pages' ).find( '.pagination' ).html( '<ul>' + pages.join( '' ) + '</ul>'); >+ >+ var $overlay = $('#search-overlay'); >+ $overlay.find('span').text(_("Loading")); >+ $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } ); >+ >+ if ( data.activeclients ) { >+ $overlay.find('.bar').css( { display: 'block', width: 100 * ( 1 - data.activeclients / Search.includedServers.length ) + '%' } ); >+ $overlay.show(); >+ } else { >+ $overlay.find('.bar').css( { display: 'block', width: '100%' } ); >+ $overlay.fadeOut(); >+ } >+ } >+ >+ function invalidateSearchResults() { >+ var $overlay = $('#search-overlay'); >+ $overlay.find('span').text(_("Search expired, please try again")); >+ $overlay.find('.bar').css( { display: 'none' } ); >+ $overlay.show(); >+ } >+ >+ function handleSearchError(error) { >+ if (error.code == 1) { >+ invalidateSearchResults(); >+ Search.Reconnect(); >+ } else { >+ humanMsg.displayMsg( _("<h3>Internal search error</h3>") + '<p>' + error + '</p>' + _("<p>Please <b>refresh</b> the page and try again."), { className: 'humanError' } ); >+ } >+ } >+ >+ function handleSearchInitError(error) { >+ $('#quicksearch-overlay').fadeIn().find('p').text(error); >+ } >+ >+ // Preference functions >+ function showPreference( pref ) { >+ var value = Preferences.user[pref]; >+ >+ switch (pref) { >+ case 'fieldWidgets': >+ $( '#set-field-widgets' ).text( value ? _("Show fields verbatim") : _("Show helpers for fixed and coded fields") ); >+ break; >+ case 'font': >+ $( '#editor .CodeMirror' ).css( { fontFamily: value } ); >+ editor.refresh(); >+ break; >+ case 'fontSize': >+ $( '#editor .CodeMirror' ).css( { fontSize: value } ); >+ editor.refresh(); >+ break; >+ case 'macros': >+ showSavedMacros(); >+ break; >+ case 'selected_search_targets': >+ $.each( z3950Servers, function( server_id, server ) { >+ var saved_val = Preferences.user.selected_search_targets[server_id]; >+ >+ if ( saved_val != null ) server.checked = saved_val; >+ } ); >+ break; >+ } >+ } >+ >+ function bindPreference( editor, pref ) { >+ function _addHandler( sel, event, handler ) { >+ $( sel ).on( event, function (e) { >+ e.preventDefault(); >+ handler( e, Preferences.user[pref] ); >+ Preferences.Save( [% USER_INFO.0.borrowernumber %] ); >+ showPreference(pref); >+ } ); >+ } >+ >+ switch (pref) { >+ case 'fieldWidgets': >+ _addHandler( '#set-field-widgets', 'click', function( e, oldValue ) { >+ editor.setUseWidgets( Preferences.user.fieldWidgets = !Preferences.user.fieldWidgets ); >+ } ); >+ break; >+ case 'font': >+ _addHandler( '#prefs-menu .set-font', 'click', function( e, oldValue ) { >+ Preferences.user.font = $( e.target ).css( 'font-family' ); >+ } ); >+ break; >+ case 'fontSize': >+ _addHandler( '#prefs-menu .set-fontSize', 'click', function( e, oldValue ) { >+ Preferences.user.fontSize = $( e.target ).css( 'font-size' ); >+ } ); >+ break; >+ case 'selected_search_targets': >+ $( document ).on( 'change', 'input.search-toggle-server', function() { >+ var server_id = $( this ).parent().data('server-id'); >+ Preferences.user.selected_search_targets[server_id] = this.checked; >+ Preferences.Save( [% USER_INFO.0.borrowernumber %] ); >+ } ); >+ break; >+ } >+ } >+ >+ function displayPreferences( editor ) { >+ $.each( Preferences.user, function( pref, value ) { >+ showPreference( pref ); >+ bindPreference( editor, pref ); >+ } ); >+ } >+ >+ //> Macro functions >+ function loadMacro( name ) { >+ $( '#macro-list li' ).removeClass( 'active' ); >+ macroEditor.activeMacro = name; >+ >+ if ( !name ) { >+ macroEditor.setValue( '' ); >+ return; >+ } >+ >+ $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' ); >+ var macro = Preferences.user.macros[name]; >+ macroEditor.setValue( macro.contents ); >+ $( '#macro-format' ).val( macro.format || 'its' ); >+ if ( macro.history ) macroEditor.setHistory( macro.history ); >+ } >+ >+ function storeMacro( name, macro ) { >+ if ( macro ) { >+ Preferences.user.macros[name] = macro; >+ } else { >+ delete Preferences.user.macros[name]; >+ } >+ >+ Preferences.Save( [% USER_INFO.0.borrowernumber %] ); >+ } >+ >+ function showSavedMacros( macros ) { >+ var scrollTop = $('#macro-list').scrollTop(); >+ $( '#macro-list' ).empty(); >+ var macro_list = $.map( Preferences.user.macros, function( macro, name ) { >+ return $.extend( { name: name }, macro ); >+ } ); >+ macro_list.sort( function( a, b ) { >+ return a.name.localeCompare(b.name); >+ } ); >+ $.each( macro_list, function( undef, macro ) { >+ var $li = $( '<li data-name="' + macro.name + '"><a href="#">' + macro.name + '</a><ol class="macro-info"></ol></li>' ); >+ $li.click( function() { >+ loadMacro(macro.name); >+ return false; >+ } ); >+ if ( macro.name == macroEditor.activeMacro ) $li.addClass( 'active' ); >+ var modified = macro.modified && new Date(macro.modified); >+ $li.find( '.macro-info' ).append( >+ '<li><span class="label">' + _("Last changed:") + '</span>' + >+ ( modified ? modified.toLocaleFormat() : _("never") ) + '</li>' >+ ); >+ $('#macro-list').append($li); >+ } ); >+ var $new_li = $( '<li class="new-macro"><a href="#">' + _("New macro...") + '</a></li>' ); >+ $new_li.click( function() { >+ // TODO: make this a bit less retro >+ var name = prompt(_("Please enter the name for the new macro:")); >+ if (!name) return; >+ >+ if ( !Preferences.user.macros[name] ) storeMacro( name, { format: "rancor", contents: "" } ); >+ showSavedMacros(); >+ loadMacro( name ); >+ } ); >+ $('#macro-list').append($new_li); >+ $('#macro-list').scrollTop(scrollTop); >+ } >+ >+ function saveMacro() { >+ var name = macroEditor.activeMacro; >+ >+ if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return; >+ >+ macroEditor.savedGeneration = macroEditor.changeGeneration(); >+ storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } ); >+ $('#macro-save-message').text(_("Saved")); >+ showSavedMacros(); >+ } >+ >+ $(document).ready( function() { >+ // Editor setup >+ editor = new MARCEditor( { >+ onCursorActivity: function() { >+ $('#status-tag-info').empty(); >+ $('#status-subfield-info').empty(); >+ >+ var field = editor.getCurrentField(); >+ var cur = editor.getCursor(); >+ >+ if ( !field ) return; >+ >+ var taginfo = KohaBackend.GetTagInfo( '', field.tag ); >+ $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' ); >+ >+ if ( taginfo ) { >+ $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> ' + taginfo.lib ); >+ >+ var subfield = field.getSubfieldAt( cur.ch ); >+ if ( !subfield ) return; >+ >+ var subfieldinfo = taginfo.subfields[ subfield.code ]; >+ $('#status-subfield-info').html( '<strong>$' + subfield.code + ':</strong> ' ); >+ >+ if ( subfieldinfo ) { >+ $('#status-subfield-info').append( subfieldinfo.lib ); >+ } else { >+ $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' ); >+ } >+ } else { >+ $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' ); >+ } >+ }, >+ position: function (elt) { $(elt).insertAfter('#toolbar') }, >+ } ); >+ >+ macroEditor = CodeMirror( >+ $('#macro-editor')[0], >+ { >+ mode: 'null', >+ lineNumbers: true, >+ } >+ ); >+ >+ // Automatically detect resizes and change the height of the editor and position of modals. >+ var resizeTimer = null; >+ $( window ).resize( function() { >+ if ( resizeTimer == null ) resizeTimer = setTimeout( function() { >+ resizeTimer = null; >+ >+ var pos = $('#editor .CodeMirror').position(); >+ $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well >+ >+ $('.modal-body').each( function() { >+ $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() ); >+ } ); >+ }, 100); >+ >+ $("#advanced-search-ui, #search-results-ui, #macro-ui").css( { >+ marginLeft: function() { >+ return -($(this).width() / 2); >+ } >+ } ); >+ >+ } ).resize(); >+ >+ var saveableBackends = []; >+ $.each( backends, function( id, backend ) { >+ if ( backend.save ) saveableBackends.push( [ backend.saveLabel, id ] ); >+ } ); >+ saveableBackends.sort(); >+ $.each( saveableBackends, function( undef, backend ) { >+ $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' ); >+ } ); >+ >+ var macro_format_list = $.map( Macros.formats, function( format, name ) { >+ return $.extend( { name: name }, format ); >+ } ); >+ macro_format_list.sort( function( a, b ) { >+ return a.description.localeCompare(b.description); >+ } ); >+ $.each( macro_format_list, function() { >+ $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' ); >+ } ); >+ >+ // Click bindings >+ $( '#save-record, #save-dropdown a' ).click( function() { >+ $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") ); >+ >+ function finishCb(result) { >+ if ( result.error == 'syntax' ) { >+ humanMsg.displayAlert( _("Incorrect syntax, cannot save"), { className: 'humanError' } ); >+ } else if ( result.error == 'invalid' ) { >+ humanMsg.displayAlert( _("Record structure invalid, cannot save"), { className: 'humanError' } ); >+ } else if ( !result.error ) { >+ humanMsg.displayAlert( _("Record saved "), { className: 'humanSuccess' } ); >+ } >+ >+ $.each( result.errors || [], function( undef, error ) { >+ switch ( error.type ) { >+ case 'noTag': >+ editor.addError( error.line, _("Invalid tag number") ); >+ break; >+ case 'noIndicators': >+ editor.addError( error.line, _("Invalid indicators") ); >+ break; >+ case 'missingTag': >+ editor.addError( null, _("Missing mandatory tag: ") + error.tag ); >+ break; >+ case 'missingSubfield': >+ if ( error.subfield == '@' ) { >+ editor.addError( error.line, _("Missing control field contents") ); >+ } else { >+ editor.addError( error.line, _("Missing mandatory subfield: $") + error.subfield ); >+ } >+ break; >+ case 'unrepeatableTag': >+ editor.addError( error.line, _("Tag ") + error.tag + _(" cannot be repeated") ); >+ break; >+ case 'unrepeatableSubfield': >+ editor.addError( error.line, _("Subfield $") + error.subfield + _(" cannot be repeated") ); >+ break; >+ } >+ } ); >+ >+ $( '#save-record' ).find('i').attr( 'class', 'icon-hdd' ); >+ >+ if ( result.error ) { >+ // Reset backend info >+ setSource( [ state.backend, state.recordID ] ); >+ } >+ } >+ >+ var backend = $( this ).data( 'backend' ) || ( state.saveBackend ); >+ if ( state.backend == backend ) { >+ saveRecord( backend + '/' + state.recordID, editor, finishCb ); >+ } else { >+ saveRecord( backend + '/', editor, finishCb ); >+ } >+ >+ return false; >+ } ); >+ >+ $('#import-records').click( function() { >+ $('#import-records-input') >+ .off('change') >+ .change( function() { >+ if ( !this.files || !this.files.length ) return; >+ >+ var file = this.files[0]; >+ var reader = new FileReader(); >+ >+ reader.onload = function() { >+ var record = new MARC.Record(); >+ >+ if ( /\.mrc$/.test( file.name ) ) { >+ record.loadISO2709( reader.result ); >+ } else if ( /\.xml$/.test( file.name ) ) { >+ record.loadMARCXML( reader.result ); >+ } else { >+ humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } ); >+ return; >+ } >+ >+ editor.displayRecord( record ); >+ }; >+ >+ reader.readAsText( file ); >+ } ) >+ .click(); >+ >+ return false; >+ } ); >+ >+ $('#open-macros').click( function() { >+ $('#macro-ui').modal('show'); >+ >+ return false; >+ } ); >+ >+ $('#run-macro').click( function() { >+ var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() ); >+ >+ if ( !result.errors.length ) { >+ $('#macro-ui').modal('hide'); >+ return false; >+ } >+ >+ var errors = []; >+ $.each( result.errors, function() { >+ var error = '<b>' + _("Line ") + (this.line + 1) + ':</b> '; >+ >+ switch ( this.error ) { >+ case 'failed': error += _("failed to run"); break; >+ case 'unrecognized': error += _("unrecognized command"); break; >+ } >+ >+ errors.push(error); >+ } ); >+ >+ humanMsg.displayMsg( _("<h3>Failed to run macro:</h3>") + '<ul><li>' + errors.join('</li><li>') + '</li></ul>', { className: 'humanError' } ); >+ >+ return false; >+ } ); >+ >+ $('#delete-macro').click( function() { >+ if ( !macroEditor.activeMacro || !confirm( _("Are you sure you want to delete this macro?") ) ) return; >+ >+ storeMacro( macroEditor.activeMacro, undefined ); >+ showSavedMacros(); >+ loadMacro( undefined ); >+ >+ return false; >+ } ); >+ >+ var saveTimeout; >+ macroEditor.on( 'change', function( cm, change ) { >+ $('#macro-save-message').empty(); >+ if ( change.origin == 'setValue' ) return; >+ >+ if ( saveTimeout ) clearTimeout( saveTimeout ); >+ saveTimeout = setTimeout( function() { >+ saveMacro(); >+ >+ saveTimeout = null; >+ }, 500 ); >+ } ); >+ >+ $( '#switch-editor' ).click( function() { >+ if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return; >+ >+ $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'basic', { expires: 365, path: '/' } ); >+ >+ if ( state.backend == 'catalog' ) { >+ window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=' + state.recordID; >+ } else if ( state.backend == 'new' ) { >+ window.location = '/cgi-bin/koha/cataloguing/addbiblio.pl'; >+ } else { >+ humanMsg.displayAlert( _("Cannot open this record in the basic editor"), { className: 'humanError' } ); >+ } >+ } ); >+ >+ $( '#show-advanced-search' ).click( function() { >+ showAdvancedSearch(); >+ >+ return false; >+ } ); >+ >+ $('#advanced-search').submit( function() { >+ startAdvancedSearch(); >+ >+ return false; >+ } ); >+ >+ $( document ).on( 'click', 'a.search-nav', function() { >+ $("#search-overlay").show(); >+ Search.Fetch( { offset: $( this ).data( 'offset' ) } ); >+ return false; >+ }); >+ >+ $( document ).on( 'click', 'th[data-sort-label]', function() { >+ $("#search-overlay").show(); >+ var direction; >+ >+ if ( $( this ).hasClass( 'sorting_asc' ) ) { >+ direction = 'desc'; >+ } else { >+ direction = 'asc'; >+ } >+ >+ showSearchSorting( $( this ).data( 'sort-label' ), direction ); >+ >+ Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ); >+ return false; >+ }); >+ >+ $( document ).on( 'change', 'input.search-toggle-server', function() { >+ var server = z3950Servers[ $( this ).parent().data('server-id') ]; >+ server.checked = this.checked; >+ >+ if ( $('#search-results-ui').is( ':visible' ) ) { >+ $("#search-overlay").show(); >+ Search.Fetch(); >+ } >+ } ); >+ >+ // Key bindings >+ bindGlobalKeys(); >+ >+ // Setup UI >+ $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() { >+ $(this).modal({ show: false }); >+ } ); >+ >+ var $quicksearch = $('#quicksearch fieldset'); >+ $('<div id="quicksearch-overlay"><h3>' + _("Search unavailable") + '</h3> <p></p></div>').css({ >+ position: 'absolute', >+ top: $quicksearch.offset().top, >+ left: $quicksearch.offset().left, >+ height: $quicksearch.outerHeight(), >+ width: $quicksearch.outerWidth(), >+ }).appendTo(document.body).hide(); >+ >+ var prevAlerts = []; >+ humanMsg.logMsg = function(msg, options) { >+ $('#show-alerts').popover('hide'); >+ prevAlerts.unshift('<li>' + msg + '</li>'); >+ prevAlerts.splice(5, 999); // Truncate old messages >+ }; >+ >+ $('#show-alerts').popover({ >+ html: true, >+ placement: 'bottom', >+ content: function() { >+ return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>'; >+ }, >+ }); >+ $('#new-record' ).click( function() { >+ openRecord( 'new/', editor ); >+ return false; >+ } ); >+ >+ // Start editor >+ Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] ); >+ displayPreferences(editor); >+ makeAuthorisedValueWidgets( '' ); >+ Search.Init( { >+ page_size: 20, >+ onresults: function(data) { showSearchResults( editor, data ) }, >+ onerror: handleSearchError, >+ } ); >+ >+ function finishCb( data ) { >+ if ( data.error ) openRecord( 'new/', editor, finishCb ); >+ >+ Resources.GetAll().done( function() { >+ $("#loading").hide(); >+ editor.focus(); >+ } ); >+ } >+ >+ if ( "[% auth_forwarded_hash %]" ) { >+ document.location.hash = "[% auth_forwarded_hash %]"; >+ } >+ >+ if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) { >+ openRecord( 'new/', editor, finishCb ); >+ } >+ } ); >+} )(); >+ >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc >new file mode 100644 >index 0000000..bfba1ba >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc >@@ -0,0 +1,158 @@ >+<div id="editor-widget-templates" style="display:none"> >+ <div id="widget-leader"> >+ Leader: <span title="Record length (autogenerated)">#####</span> >+ <select name="f5" title="Record status"> >+ <option value="a">a - Increase in encoding level</option> >+ <option value="c">c - Corrected or revised</option> >+ <option value="d">d - Deleted</option> >+ <option value="n">n - New</option> >+ <option value="p">p - Increase in encoding level from prepublication</option> >+ </select> >+ <select name="f6" title="Type of record"> >+ <option value="a">a - Language material</option> >+ <option value="c">c - Notated music</option> >+ <option value="d">d - Manuscript notated music</option> >+ <option value="e">e - Cartographic material</option> >+ <option value="f">f - Manuscript cartographic material</option> >+ <option value="g">g - Projected medium</option> >+ <option value="i">i - Nonmusical sound recording</option> >+ <option value="j">j - Musical sound recording</option> >+ <option value="k">k - Two-dimensional nonprojectable graphic</option> >+ <option value="m">m - Computer file</option> >+ <option value="o">o - Kit</option> >+ <option value="p">p - Mixed materials</option> >+ <option value="r">r - Three-dimensional artifact or naturally occurring object</option> >+ <option value="t">t - Manuscript language material</option> >+ </select> >+ <select name="f7" title="Bibliographic level"> >+ <option value="a">a - Monographic component part</option> >+ <option value="b">b - Serial component part</option> >+ <option value="c">c - Collection</option> >+ <option value="d">d - Subunit</option> >+ <option value="i">i - Integrating resource</option> >+ <option value="m">m - Monograph/item</option> >+ <option value="s">s - Serial</option> >+ </select> >+ <select name="f8" title="Type of control"> >+ <option value=" ">_ - No specific type</option> >+ <option value="a">a - Archival</option> >+ </select> >+ <span title="Encoding (forced Unicode)">a</span> >+ <span title="Indicator/subfield lengths">22</span> >+ <span title="Data base address (autogenerated)">#####</span> >+ <select name="f17" title="Encoding level"> >+ <option value=" ">_ - Full level</option> >+ <option value="1">1 - Full level, material not examined</option> >+ <option value="2">2 - Less-than-full level, material not examined</option> >+ <option value="3">3 - Abbreviated level</option> >+ <option value="4">4 - Core level</option> >+ <option value="5">5 - Partial (preliminary) level</option> >+ <option value="7">7 - Minimal level</option> >+ <option value="8">8 - Prepublication level</option> >+ <option value="u">u - Unknown</option> >+ <option value="z">z - Not applicable</option> >+ </select> >+ <select name="f18" title="Descriptive cataloging form"> >+ <option value=" ">_ - Non-ISBD</option> >+ <option value="a">a - AACR 2</option> >+ <option value="c">c - ISBD punctuation omitted</option> >+ <option value="i">i - ISBD punctuation included</option> >+ <option value="u">u - Unknown</option> >+ </select> >+ <select name="f19" title="Multipart record resource level"> >+ <option value=" ">_ - Not specified or not applicable</option> >+ <option value="a">a - Set</option> >+ <option value="b">b - Part with independent title</option> >+ <option value="c">c - Part with dependent title</option> >+ </select> >+ <span title="Length of directory elements">4500</span> >+ </div> >+</div> >+ >+<script> >+ >+/** >+ * Each widget should provide one to three methods: >+ * init( text ): Returns the DOM node for this widget. >+ * postCreate( node, mark ): Optional, called once the mark has been created >+ * and the node shown. Bind event handlers here. >+ * makeTemplate(): Optional, should return some sane default contents for a >+ * newly created field/subfield. '<empty>' will be used if this >+ * method is unset. >+ * >+ * Following the Koha convention, control fields are defined as tags with a >+ * single subfield, '@'. >+ */ >+ >+require( [ 'widget' ], function( Widget ) { >+ Widget.Register( '000@', { >+ makeTemplate: function() { >+ return ' nam a22 7a 4500'; >+ }, >+ init: function() { >+ var $result = $( '<span class="subfield-widget fixed-widget"></span>' ); >+ >+ return $result[0]; >+ }, >+ postCreate: function() { >+ // Clear the length and directory start fields; these are unnecessary for MARCXML and will be filled in upon USMARC export >+ this.setFixed( 0, 5, ' ' ); >+ this.setFixed( 9, 17, 'a22 ' ); >+ this.setFixed( 20, 24, '4500' ); >+ >+ this.insertTemplate( '#widget-leader' ); >+ >+ this.bindFixed( '[name=f5]', 5, 6 ); >+ this.bindFixed( '[name=f6]', 6, 7 ); >+ this.bindFixed( '[name=f7]', 7, 8 ); >+ this.bindFixed( '[name=f8]', 8, 9 ); >+ this.bindFixed( '[name=f17]', 17, 18 ); >+ this.bindFixed( '[name=f18]', 18, 19 ); >+ this.bindFixed( '[name=f19]', 19, 20 ); >+ }, >+ } ); >+ >+ Widget.Register( '005@', { >+ init: function() { >+ var $result = $( '<span class="subfield-widget fixed-widget">Updated: </span>' ); >+ >+ return $result[0]; >+ }, >+ postCreate: function( node, mark ) { >+ var parts = this.text.match( /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d)/ ); >+ >+ if ( parts ) { >+ var dateVal = new Date( >+ parseInt( parts[1] ), // Year >+ parseInt( parts[2] ) - 1, // Month (0-11) >+ parseInt( parts[3] ), // Day >+ parseInt( parts[4] ), // Hour >+ parseInt( parts[5] ), // Minute >+ parseInt( parts[6] ), // Second >+ parseInt( parts[7] ) * 100 // Millisecond >+ ); >+ >+ $( this.node ).append( dateVal.toLocaleString() ); >+ } else { >+ $( this.node ).append( '<span class="hint">unset</span>' ); >+ } >+ } >+ } ); >+ >+ Widget.Register( '008@', { >+ makeTemplate: function() { >+ var now = new Date(); >+ return Widget.PadNum( now.getYear() % 100, 2 ) + Widget.PadNum( now.getMonth() + 1, 2 ) + Widget.PadNum( now.getDate(), 2 ) + "b xxu||||| |||| 00| 0 [% DefaultLanguageField008 %] d"; >+ }, >+ init: function() { >+ var $result = $( '<span class="subfield-widget fixed-widget">Fixed data:<span class="hint widget-loading">Loading...</span></span>' ); >+ >+ return $result[0]; >+ }, >+ postCreate: function( node, mark ) { >+ this.createFromXML( 'marc21/xml/008' ); >+ } >+ } ); >+} ); >+ >+</script> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >index 3b2e2f7..0482478 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >@@ -7,6 +7,7 @@ > [% IF ( circulation ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Circulation" href="/cgi-bin/koha/admin/preferences.pl?tab=circulation">Circulation</a></li> > [% 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> > [% 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> >+[% IF ( labs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Experimental features" href="/cgi-bin/koha/admin/preferences.pl?tab=labs">Labs</a></li> > [% IF ( local_use ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/admin/systempreferences.pl">Local use</a></li> > [% IF ( logs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Transaction logs" href="/cgi-bin/koha/admin/preferences.pl?tab=logs">Logs</a></li> > [% 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> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/labs.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/labs.pref >new file mode 100644 >index 0000000..c6dc285 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/labs.pref >@@ -0,0 +1,11 @@ >+Labs: >+ All: >+ - >+ - pref: EnableAdvancedCatalogingEditor >+ default: 0 >+ choices: >+ yes: Enable >+ no: "Don't enable" >+ - the advanced cataloging editor. >+ - "<br/> NOTE:" >+ - 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. >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >index 3bc00d5..c370034 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >@@ -105,7 +105,26 @@ > redirect("just_save", tab); > return false; > }); >- }); >+ >+ $( '#switcheditor' ).click( function() { >+ var breedingid = [% breedingid || "null" %]; >+ >+ if ( !confirm( breedingid ? _("This record cannot be transferred to the advanced editor. Continue?") : _("Any changes will not be saved. Continue?") ) ) return false; >+ >+ $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } ); >+ >+ var biblionumber = [% biblionumber || "null" %]; >+ >+ if ( biblionumber ) { >+ window.location = '/cgi-bin/koha/cataloguing/editor.pl#catalog:' + biblionumber; >+ } else { >+ window.location = '/cgi-bin/koha/cataloguing/editor.pl'; >+ } >+ >+ return false; >+ } ); >+ >+ }); > > function redirect(dest){ > $("#redirect").attr("value",dest); >@@ -462,6 +481,9 @@ function Changefwk(FwkList) { > > [% UNLESS (circborrowernumber) %][%# Hide in fast cataloging %] > <div class="btn-group"><a class="btn btn-small" href="#" id="z3950search"><i class="icon-search"></i> Z39.50/SRU search</a></div> >+ [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %] >+ <div class="btn-group"><a href="#" id="switcheditor" class="btn btn-small">Switch to advanced editor</a></div> >+ [% END %] > [% IF (biblionumber) %] > [% IF ( BiblioDefaultViewmarc ) %] > <div class="btn-group"> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >index affa0fe..461c2e7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >@@ -1,3 +1,4 @@ >+[% USE Koha %] > [% INCLUDE 'doc-head-open.inc' %] > <title>Koha › Cataloging</title> > [% INCLUDE 'greybox.inc' %] >@@ -22,6 +23,11 @@ > e.preventDefault(); > MergeItems(); > }); >+ >+ $("#useadvanced").click(function(){ >+ $.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } ); >+ return true; >+ }); > }); > > /* this function open a popup to search on z3950 server. */ >@@ -70,6 +76,9 @@ > > [% IF ( CAN_user_editcatalogue_edit_catalogue ) %] > <div id="toolbar" class="btn-toolbar"> >+ [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %] >+ <a id="useadvanced" href="/cgi-bin/koha/cataloguing/editor.pl" class="btn btn-small"><i class="icon-edit"></i> Advanced editor</a> >+ [% END %] > <div class="btn-group"> > <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-plus"></i> New record <span class="caret"></span></button> > <ul class="dropdown-menu"> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt >new file mode 100644 >index 0000000..0991dde >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt >@@ -0,0 +1,234 @@ >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Koha › Cataloging › Editor</title> >+[% INCLUDE 'doc-head-close.inc' %] >+<link rel="stylesheet" href="[% themelang %]/css/cateditor.css" /> >+<link rel="stylesheet" href="[% themelang %]/css/datatables.css" /> >+<link rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" /> >+<link rel="stylesheet" href="[% themelang %]/css/humanmsg.css" /> >+<script src="[% interface %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script> >+[% IF ( bidi ) %] >+ <link rel="stylesheet" type="text/css" href="[% themelang %]/css/right-to-left.css" /> >+[% END %] >+</head> >+<body id="cat_addbiblio" class="cat"> >+ >+ <div id="loading"> >+ <div>Loading, please wait...</div> >+ </div> >+ >+[% INCLUDE 'header.inc' %] >+ >+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a> › Editor</div> >+ >+<div id="doc3" class="yui-t2"> >+<div id="bd"> >+ >+<h1 id="title">Cataloging editor</h1> >+ >+<div id="yui-main"><div class="yui-b"> >+ >+<div id="editor"> >+ <input id="import-records-input" type="file" style="display: none"> >+ <div id="toolbar" class="btn-toolbar"> >+ <button class="btn btn-small" id="new-record" title="Open fresh record"><i class="icon-plus"></i> <span>New record</span></button> >+ <div class="btn-group"> >+ <button class="btn btn-small" id="save-record" title="Save current record (Ctrl-S)"><i class="icon-hdd"></i> <span>Save</span></button> >+ <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"> >+ <span class="caret"></span> >+ </button> >+ <ul class="dropdown-menu" id="save-dropdown"> >+ </ul> >+ </div> >+ <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> >+ <button class="btn btn-small" id="open-macros" title="Run and edit macros"><i class="icon-play"></i> <span>Macros...</span></button> >+ <div class="btn-group"> >+ <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i> Settings <span class="caret"></span></button> >+ <ul id="prefs-menu" class="dropdown-menu"> >+ <li><a id="switch-editor" href="#">Switch to basic editor</a></li> >+ <li><a id="set-field-widgets" href="#"></a></li> >+ <li class="divider"></li> >+ <li><a class="set-fontSize" style="font-size: .92em" href="#">Small text</a></li> >+ <li><a class="set-fontSize" style="font-size: 1em" href="#">Normal text</a></li> >+ <li><a class="set-fontSize" style="font-size: 1.08em" href="#">Large text</a></li> >+ <li><a class="set-fontSize" style="font-size: 1.18em" href="#">Huge text</a></li> >+ <li class="divider"></li> >+ <li><a class="set-font" style="font-family: monospace" href="#">Default font</a></li> >+ <li><a class="set-font" style="font-family: 'Courier New'" href="#">Courier New</a></li> >+ <li><a class="set-font" style="font-family: peep" href="#">peep</a></li> >+ </ul> >+ </div> >+ <button class="btn btn-small" id="show-alerts" title="Previous alerts"><i class="icon-info-sign"></i> Alerts <span class="caret"></span></button> >+ </div> >+ [%# CodeMirror instance will be inserted here %] >+ <div id="statusbar"> >+ <div id="status-tag-info"> >+ </div> >+ <div id="status-subfield-info"> >+ </div> >+ </div> >+</div> >+ >+</div></div> >+ >+<div class="yui-b" id="sidebar"> >+ >+<h3>Search</h3> >+<form id="quicksearch"> >+ <fieldset class="brief"> >+ <ol> >+ <li><label for="search-by-keywords">Keywords:</label></li> >+ <li><input class="search-box" data-qualifier="term" id="search-by-keywords" placeholder="(Ctrl-Alt-K)" /></li> >+ <li><label for="search-by-author">Author:</label></li> >+ <li><input class="search-box" data-qualifier="author" id="search-by-author" placeholder="(Ctrl-Alt-A)" /></li> >+ <li><label for="search-by-isbn">ISBN:</label></li> >+ <li><input class="search-box" data-qualifier="isbn" id="search-by-isbn" placeholder="(Ctrl-Alt-I)" /></li> >+ <li><label for="search-by-title">Title:</label></li> >+ <li><input class="search-box" data-qualifier="title" id="search-by-title" placeholder="(Ctrl-Alt-T)" /></li> >+ <li><a href="#" id="show-advanced-search" title="Show advanced search (Ctrl-Alt-S)">Advanced »</a></li> >+ </fieldset> >+</form> >+ >+</div> >+ >+</div> >+</div> >+ >+<div id="advanced-search-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="advanced-search-title" aria-hidden="true"> >+ >+<div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="advanced-search-title">Advanced search</h3> >+</div> >+ >+<form id="advanced-search" class="modal-body"> >+ <div class="span3"> >+ <div id="search-facets"> >+ <ul> >+ <li>Servers:<ul id="advanced-search-servers"></ul></li> >+ </ul> >+ </div> >+ </div> >+ <div class="span9"> >+ <div id="toolbar" class="btn-toolbar"> >+ <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button> >+ <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button> >+ </div> >+ <ul id="advanced-search-fields"> >+ <li> >+ <label for="advanced-search-by-author">Author:</label> >+ <input class="search-box" data-qualifier="author" id="advanced-search-by-author" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-control-number">Control number:</label> >+ <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-dewey">Dewey number:</label> >+ <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-isbn">ISBN:</label> >+ <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-issn">ISSN:</label> >+ <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-lccn">LCCN:</label> >+ <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-lc-number">LC call number:</label> >+ <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-publisher-number">Publisher number:</label> >+ <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-standard-number">Standard number:</label> >+ <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-subject">Subject:</label> >+ <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-publication-date">Publication date:</label> >+ <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-title">Title:</label> >+ <input class="search-box" data-qualifier="title" id="advanced-search-by-title" /> >+ </li> >+ </ul> >+ </div> >+</form> >+ >+</div> >+ >+<div id="search-results-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="search-results-title" aria-hidden="true"> >+ >+<div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="search-results-title">Results</h3> >+</div> >+ >+<div class="modal-body row-fluid"> >+ <div class="span3"> >+ <div id="search-facets"> >+ <ul> >+ <li>Servers:<ul id="search-serversinfo"></ul></li> >+ </ul> >+ </div> >+ </div> >+ <div class="span9"> >+ <div id="searchresults"> >+ <div id="search-top-pages"> >+ <div class="pagination pagination-small"> >+ </div> >+ </div> >+ >+ <table> >+ <thead> >+ <tr></tr> >+ </thead> >+ <tbody></tbody> >+ </table> >+ >+ <div id="search-bottom-pages"> >+ <div class="pagination pagination-small"> >+ </div> >+ </div> >+ </div> >+ </div> >+ <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div> >+</div> >+ >+</div> >+ >+<div id="macro-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="macro-title" aria-hidden="true"> >+ >+<div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="macro-title">Macros</h3> >+</div> >+ >+<div class="modal-body row-fluid"> >+ <div class="span3"><ul id="macro-list"></ul></div> >+ <div class="span9" id="macro-editor"> >+ <div id="macro-toolbar" class="btn-toolbar"> >+ <button class="btn btn-small" id="run-macro" title="Run and edit macros"><i class="icon-play"></i> Run macro</button> >+ <button class="btn btn-small" id="delete-macro" title="Delete macro"><i class="icon-remove"></i> Delete macro</button> >+ <label for="macro-format">Format: </label> <select id="macro-format"></select> >+ <div id="macro-save-message"></div> >+ </div> >+ </div> >+</div> >+ >+</div> >+ >+[% PROCESS 'cateditor-ui.inc' %] >+ >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/svc/cataloguing/framework b/svc/cataloguing/framework >new file mode 100755 >index 0000000..49d341d >--- /dev/null >+++ b/svc/cataloguing/framework >@@ -0,0 +1,77 @@ >+#!/usr/bin/perl >+ >+use Modern::Perl '2009'; >+ >+use CGI; >+use C4::Branch; >+use C4::ClassSource; >+use C4::Context; >+use C4::Biblio; >+use C4::Service; >+use Koha::Database; >+ >+my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' ); >+ >+my $frameworkcode = $query->param( 'frameworkcode' ) // ''; >+ >+my $tagslib = GetMarcStructure( 1, $frameworkcode ); >+ >+my @tags; >+ >+foreach my $tag ( sort keys %$tagslib ) { >+ my $taglib = $tagslib->{$tag}; >+ my $taginfo = { map { $_, $taglib->{$_} } grep { length $_ > 1 } keys %$taglib }; >+ $taginfo->{subfields} = [ map { [ $_, $taglib->{$_} ] } grep { length $_ == 1 } sort keys %$taglib ]; >+ >+ push @tags, [ $tag, $taginfo ]; >+} >+ >+my $schema = Koha::Database->new->schema; >+my $authorised_values = {}; >+ >+$authorised_values->{branches} = []; >+my $onlymine=C4::Context->preference('IndependentBranches') && >+ C4::Context->userenv && >+ C4::Context->userenv->{flags} % 2 == 0 && >+ C4::Context->userenv->{branch}; >+my $branches = GetBranches($onlymine); >+foreach my $thisbranch ( sort keys %$branches ) { >+ push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} }; >+} >+ >+$authorised_values->{itemtypes} = [ $schema->resultset( "Itemtype" )->search( undef, { >+ columns => [ { value => 'itemtype' }, { lib => "description" } ], >+ order_by => "description", >+ result_class => 'DBIx::Class::ResultClass::HashRefInflator' >+} ) ]; >+ >+my $class_sources = GetClassSources(); >+ >+my $default_source = C4::Context->preference("DefaultClassificationSource"); >+ >+foreach my $class_source (sort keys %$class_sources) { >+ next unless $class_sources->{$class_source}->{'used'} or >+ ($class_source eq $default_source); >+ push @{ $authorised_values->{cn_source} }, { value => $class_source, lib => $class_sources->{$class_source}->{'description'} }; >+} >+ >+my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; >+my $results; >+if( $branch_limit ) { >+ $results = $schema->resultset( "AuthorisedValue" )->search( >+ { "authorised_values_branches.branchcode" => { "=", [ $branch_limit, undef ] } }, >+ { join => "authorised_values_branches", order_by => "lib" } ); >+} else { >+ $results = $schema->resultset( "AuthorisedValue" )->search( >+ undef, >+ { order_by => "lib" } ); >+} >+ >+foreach my $result ( $results->all ) { >+ $authorised_values->{$result->category} ||= []; >+ push @{ $authorised_values->{$result->category} }, { value => $result->authorised_value, lib => $result->lib }; >+} >+ >+$response->param( framework => \@tags, authorised_values => $authorised_values ); >+ >+C4::Service->return_success( $response ); >diff --git a/svc/cataloguing/metasearch b/svc/cataloguing/metasearch >new file mode 100755 >index 0000000..110732f >--- /dev/null >+++ b/svc/cataloguing/metasearch >@@ -0,0 +1,76 @@ >+#!/usr/bin/perl >+# >+# Copyright 2014 ByWater Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Service; >+use Encode qw( encode_utf8 ); >+use Koha::MetaSearcher; >+ >+my ( $query, $response ) = C4::Service->init( catalogue => 1 ); >+ >+my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' ); >+ >+my $server_errors = {}; >+ >+my $sort_key = $query->param( 'sort_key' ) || 'title'; >+my $sort_direction = $query->param( 'sort_direction' ) || 'asc'; >+my $offset = $query->param( 'offset' ) || 0; >+my $page_size = $query->param( 'page_size' ) || 20; >+my $fetched = $query->param( 'fetched' ) || 100; >+ >+my $searcher = Koha::MetaSearcher->new( { >+ fetched => $fetched, >+ on_error => sub { >+ my ( $server, $exception ) = @_; >+ >+ $server_errors->{ $server->{id} } = $exception->message; >+ }, >+} ); >+ >+$searcher->resultset( $query->param('resultset') ) if ( $query->param('resultset') ); >+ >+my @server_ids = split( /,/, $servers ); >+my $stats = $searcher->search( \@server_ids, $query_string ); >+ >+$searcher->sort( $sort_key, $sort_direction eq 'desc' ? -1 : 1 ); >+ >+my @hits; >+ >+foreach my $hit ( $searcher->results( $offset, $page_size ) ) { >+ push @hits, { >+ server => $hit->{server}->{id}, >+ index => $hit->{index}, >+ record => $hit->{record}->as_xml_record(), >+ metadata => $hit->{metadata} >+ }; >+} >+ >+$response->param( >+ resultset => $searcher->resultset, >+ sort_key => $sort_key, >+ sort_direction => $sort_direction, >+ offset => $offset, >+ page_size => $page_size, >+ errors => $server_errors, >+ hits => \@hits, >+ %$stats >+); >+ >+C4::Service->return_success( $response ); >-- >2.6.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11559
:
26069
|
26070
|
26071
|
26104
|
26653
|
26654
|
27140
|
27141
|
27142
|
27757
|
27758
|
27759
|
27760
|
27761
|
27900
|
27901
|
27902
|
27993
|
27995
|
34458
|
34459
|
34460
|
34461
|
34462
|
34463
|
36243
|
36244
|
36245
|
36246
|
36247
|
36248
|
36455
|
38533
|
38534
|
38535
|
38536
|
38537
|
38538
|
38539
|
40153
|
40154
|
40155
|
40543
|
40555
|
40556
|
40557
|
40558
|
40811
|
40812
|
40813
|
40814
|
40815
|
41425
|
41453
|
41476
|
42373
|
43261
|
43547
|
43614
|
43615
|
43616
|
43617
|
43618
|
43619
|
43620
|
43621
|
43622
|
43623
|
43703
|
43704
|
43705
|
43706
|
43707
|
43708
|
43709
|
43710
|
43711
|
43712
|
43713
|
43772
|
43790
|
43883
|
43969
|
44016
|
44018
|
44022
|
44023
|
44024
|
44025
|
44026
|
44027
|
44028
|
44029
|
44030
|
44031
|
44032
|
44033
|
44034
|
44035
|
44036