From edd64d30188fef2b3ecb314ceeb1f613e2217cc2 Mon Sep 17 00:00:00 2001 From: Jesse Weaver 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. Signed-off-by: Nick Clemens Signed-off-by: Katrin Fischer Signed-off-by: Tomas Cohen Arazi --- Koha/MetaSearcher.pm | 351 ++++++ Koha/MetadataRecord.pm | 25 + cataloguing/addbiblio.pl | 6 + cataloguing/editor.pl | 67 ++ ...EnableAdvancedCatalogingEditor_syspref.sql | 1 + installer/data/mysql/sysprefs.sql | 1 + .../lib/koha/cateditor/koha-backend.js | 220 ++++ .../lib/koha/cateditor/macros.js | 38 + .../lib/koha/cateditor/macros/its.js | 208 ++++ .../lib/koha/cateditor/macros/rancor.js | 277 +++++ .../lib/koha/cateditor/marc-editor.js | 671 +++++++++++ .../lib/koha/cateditor/marc-mode.js | 168 +++ .../lib/koha/cateditor/marc-record.js | 370 ++++++ .../lib/koha/cateditor/preferences.js | 49 + .../lib/koha/cateditor/resources.js | 38 + .../lib/koha/cateditor/search.js | 114 ++ .../lib/koha/cateditor/text-marc.js | 106 ++ .../lib/koha/cateditor/widget.js | 310 +++++ .../intranet-tmpl/prog/en/css/cateditor.css | 434 +++++++ .../prog/en/css/staff-global.css | 11 +- .../prog/en/includes/cateditor-ui.inc | 1051 +++++++++++++++++ .../en/includes/cateditor-widgets-marc21.inc | 158 +++ .../prog/en/includes/prefs-menu.inc | 1 + .../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 0000000000..7c4c87a63d --- /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 . + +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 8b0d8a45eb..1577cd04d5 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 890db0f946..8cf3b6ab4e 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 0000000000..6d0fd0d65a --- /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 0000000000..c7f0fac4f3 --- /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 578ab79249..b7d33027c8 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -123,6 +123,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 0000000000..7827f60c69 --- /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 . + */ + +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 0000000000..4d747f944f --- /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 . + */ + +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 0000000000..07bfbeb6d0 --- /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 . + */ + +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 0000000000..e9b484ef60 --- /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 . + */ + +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 0000000000..bc79eb6ed0 --- /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 . + */ + +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 = $( '
' + error + '
' )[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 0000000000..f9bf862f07 --- /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 . + */ + +// 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 0000000000..f745533f61 --- /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 . + */ + +/** + * 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= 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= 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' + _escape( this._subfields[0][1] ) + ''; + } else if ( this._tagnumber < '010' ) { + return '' + _escape( this._subfields[0][1] ) + ''; + } else { + var result = ''; + for( var i = 0; i< this._subfields.length; i++) { + result += ''; + result += _escape( this._subfields[i][1] ); + result += ''; + } + result += ''; + + 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 0000000000..5cf81dace6 --- /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 . + */ + +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 0000000000..5be97d60e1 --- /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 . + */ + +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 0000000000..341da39b7f --- /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 . + */ + +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 0000000000..ba471e657c --- /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 . + */ + +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 0000000000..35d47270a5 --- /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 . + */ + +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 = $( '' + $node.val() + '' ).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 = $('').appendTo(widget.node); + var $contents = $('').appendTo(widget.node); + var materialInfo = {}; + + $('Tagfield', xml).children('Material').each( function() { + $matSelect.append( '' ); + + 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( '' + widget.getFixed(start, end) + '' ); + return; + } + + if ( match[2] ) { + $input = $( '' ); + } else { + $input = $( '' ); + + $values.each( function() { + $input.append( '' ); + } ); + } + + $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() : '', { 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 0000000000..04258a5686 --- /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 d33580ab1d..11781bce78 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 0000000000..89a11ec0b0 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc @@ -0,0 +1,1051 @@ + + + + + + +[% IF marcflavour == 'MARC21' %] +[% PROCESS 'cateditor-widgets-marc21.inc' %] +[% ELSE %] + +[% END %] + + 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 0000000000..bfba1baede --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc @@ -0,0 +1,158 @@ + + + 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 3b2e2f75ed..0482478821 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 ) %]
  • [% ELSE %]
  • [% END %]Circulation
  • [% IF ( enhanced_content ) %]
  • [% ELSE %]
  • [% END %]Enhanced content
  • [% IF ( i18n_l10n ) %]
  • [% ELSE %]
  • [% END %]I18N/L10N
  • +[% IF ( labs ) %]
  • [% ELSE %]
  • [% END %]Labs
  • [% IF ( local_use ) %]
  • [% ELSE %]
  • [% END %]Local use
  • [% IF ( logs ) %]
  • [% ELSE %]
  • [% END %]Logs
  • [% IF ( opac ) %]
  • [% ELSE %]
  • [% END %]OPAC
  • 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 0000000000..c6dc285de1 --- /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. + - "
    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 f1bcc331fc..32d3d062e1 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 %] + [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %] + + [% END %] [% IF (biblionumber) %] [% IF ( BiblioDefaultViewmarc ) %]
    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 35f35e9805..9ef3543af8 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' %] Koha › Cataloging [% 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 ) %]
    + [% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %] + Advanced editor + [% END %]