From 4d02c0027fcdad7766415486252ac9e0ff1630a1 Mon Sep 17 00:00:00 2001 From: Jesse Weaver Date: Tue, 11 Mar 2014 11:28:40 +0100 Subject: [PATCH] Bug 11559 - Rancor: professional cataloging interface Test plan: 1. Apply patches for bug 10486 and this bug. 2. Reinstall patches for pazpar2. 3. Switch back and forth between basic and advanced editor, and verify that preference is saved (by workstation, not user. 4. Enter in a record (with all required fields). 5. Save to ISO2709, MARCXML and catalog, verify that all three match. 6. Import a MARCXML and ISO2709 record, and verify that the record is correct after import. 7. Run keyword and advanced searches with a variety of Z39.50 servers to check that searching is correct, and try to both import records as new records and substitute them for the contents of existing records. 8. Create, edit and delete macros, verifying that they are autosaved. 9. Try running macros (example follows). 10. Try enabling and disabling fixed-field widgets and customizing fonts, and test the editor afterwards. Example macro: goto field 003 copy field data insert new field 040 data= goto subfield end insert new subfield a data= goto subfield a paste --- cataloguing/addbiblio.pl | 6 + cataloguing/editor.pl | 70 ++ .../lib/koha/cateditor/koha-backend.js | 201 ++++++ .../intranet-tmpl/lib/koha/cateditor/macros.js | 196 +++++ .../lib/koha/cateditor/marc-editor.js | 358 +++++++++ .../intranet-tmpl/lib/koha/cateditor/marc-mode.js | 129 ++++ .../lib/koha/cateditor/marc-record.js | 351 +++++++++ .../lib/koha/cateditor/preferences.js | 28 + .../intranet-tmpl/lib/koha/cateditor/search.js | 87 +++ .../intranet-tmpl/lib/koha/cateditor/text-marc.js | 78 ++ .../intranet-tmpl/lib/koha/cateditor/widget.js | 169 +++++ koha-tmpl/intranet-tmpl/lib/koha/cateditor/xslt.js | 68 ++ koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css | 332 +++++++++ .../prog/en/includes/cateditor-ui.inc | 804 +++++++++++++++++++++ .../prog/en/includes/cateditor-widgets-marc21.inc | 155 ++++ .../prog/en/modules/cataloguing/addbiblio.tt | 15 + .../prog/en/modules/cataloguing/editor.tt | 201 ++++++ svc/cataloguing/framework | 73 ++ 18 files changed, 3321 insertions(+) create mode 100755 cataloguing/editor.pl 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/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/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/lib/koha/cateditor/xslt.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/cataloguing/editor.tt create mode 100755 svc/cataloguing/framework diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl index 118bfc2..0168597 100755 --- a/cataloguing/addbiblio.pl +++ b/cataloguing/addbiblio.pl @@ -767,8 +767,14 @@ if ($frameworkcode eq 'FA'){ 'stickyduedate' => $fa_stickyduedate, 'duedatespec' => $fa_duedatespec, ); +} elsif ( $input->cookie( 'catalogue_editor_' . $loggedinuser ) eq 'advanced' ) { + # Only use the advanced editor for non-fast-cataloging. + # breedingid is not handled because those would only come off a Z39.50 + # search initiated by the basic editor. + print $input->redirect( '/cgi-bin/koha/cataloguing/editor.pl' . ( $biblionumber && ( '#catalog:' . $biblionumber ) ) ); } + # Getting the list of all frameworks # get framework list my $frameworks = getframeworks; diff --git a/cataloguing/editor.pl b/cataloguing/editor.pl new file mode 100755 index 0000000..1dc4b4e --- /dev/null +++ b/cataloguing/editor.pl @@ -0,0 +1,70 @@ +#!/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 C4::XSLT qw( XSLTGetFilename ); + +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' }, + } +); + +# 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_targets} = $dbh->selectall_arrayref( q{ + SELECT * FROM z3950servers + ORDER BY name +}, { Slice => {} } ); + +my @xsltResultStylesheets; +my @xsltDetailStylesheets; + +foreach my $syntax ( qw( MARC21 UNIMARC NORMARC ) ) { + if ( XSLTGetFilename( $syntax, 'XSLTResultsDisplay' ) =~ m,/intranet-tmpl/.*|^https:?.*, ) { + push @xsltResultStylesheets, { syntax => $syntax, url => $& }; + } + + if ( XSLTGetFilename( $syntax, 'XSLTDetailsDisplay' ) =~ m,/intranet-tmpl/.*|^https:?.*, ) { + push @xsltDetailStylesheets, { syntax => $syntax, url => $& }; + } +} + +$template->{VARS}->{xslt_result_stylesheets} = \@xsltResultStylesheets; +$template->{VARS}->{xslt_detail_stylesheets} = \@xsltDetailStylesheets; + +output_html_with_http_headers $input, $cookie, $template->output; diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js new file mode 100644 index 0000000..c70c3bf --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js @@ -0,0 +1,201 @@ +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( { data: { error: data } } ); + } ); + }, + + CreateRecord: function( record, callback ) { + $.ajax( { + type: 'POST', + url: '/cgi-bin/koha/svc/new_bib', + data: record.toXML(), + contentType: 'text/xml' + } ).done( function( data ) { + callback( _fromXMLStruct( data ) ); + } ).fail( function( data ) { + callback( { error: data } ); + } ); + }, + + SaveRecord: function( id, record, callback ) { + $.ajax( { + type: 'POST', + url: '/cgi-bin/koha/svc/bib/' + id, + data: record.toXML(), + contentType: 'text/xml' + } ).done( function( data ) { + callback( _fromXMLStruct( data ) ); + } ).fail( function( data ) { + callback( { data: { error: data } } ); + } ); + }, + + GetTagsBy: function( frameworkcode, field, value ) { + var result = {}; + + $.each( _frameworks[frameworkcode], function( undef, tag ) { + var tagnum = tag[0], taginfo = tag[1]; + + if ( taginfo[field] == value ) result[tagnum] = true; + } ); + + return result; + }, + + GetSubfieldsBy: function( frameworkcode, field, value ) { + var result = {}; + + $.each( _frameworks[frameworkcode], function( undef, tag ) { + var tagnum = tag[0], taginfo = tag[1]; + + $.each( taginfo.subfields, function( undef, subfield ) { + var subfieldcode = subfield[0], subfieldinfo = subfield[1]; + + if ( subfieldinfo[field] == value ) { + if ( !result[tagnum] ) result[tagnum] = {}; + + result[tagnum][subfieldcode] = true; + } + } ); + } ); + + return result; + }, + + FillRecord: function( frameworkcode, record, allTags ) { + $.each( _frameworks[frameworkcode], function( undef, tag ) { + var tagnum = tag[0], taginfo = tag[1]; + + if ( taginfo.mandatory != "1" && !allTags ) return; + + var fields = record.fields(tagnum); + + if ( fields.length == 0 ) { + var newField = new MARC.Field( tagnum, ' ', ' ', [] ); + fields.push( newField ); + record.addFieldGrouped( newField ); + + if ( tagnum < '010' ) { + newField.addSubfield( [ '@', '' ] ); + return; + } + } + + $.each( taginfo.subfields, function( undef, subfield ) { + var subfieldcode = subfield[0], subfieldinfo = subfield[1]; + + if ( subfieldinfo.mandatory != "1" && !allTags ) return; + + $.each( fields, function( undef, field ) { + if ( !field.hasSubfield(subfieldcode) ) field.addSubfieldGrouped( [ subfieldcode, '' ] ); + } ); + } ); + } ); + }, + + ValidateRecord: function( frameworkcode, record ) { + var errors = []; + + var mandatoryTags = KohaBackend.GetTagsBy( '', 'mandatory', '1' ); + var mandatorySubfields = KohaBackend.GetSubfieldsBy( '', 'mandatory', '1' ); + var nonRepeatableTags = KohaBackend.GetTagsBy( '', 'repeatable', '0' ); + var nonRepeatableSubfields = KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' ); + + $.each( mandatoryTags, function( tag ) { + if ( !record.hasField( tag ) ) errors.push( { type: 'missingTag', tag: tag } ); + } ); + + var seenTags = {}; + + $.each( record.fields(), function( undef, field ) { + if ( seenTags[ field.tagnumber() ] && nonRepeatableTags[ field.tagnumber() ] ) { + errors.push( { type: 'unrepeatableTag', line: field.sourceLine, tag: field.tagnumber() } ); + return; + } + + seenTags[ field.tagnumber() ] = true; + + var seenSubfields = {}; + + $.each( field.subfields(), function( undef, subfield ) { + if ( seenSubfields[ subfield[0] ] != null && nonRepeatableSubfields[ field.tagnumber() ][ subfield[0] ] ) { + errors.push( { type: 'unrepeatableSubfield', subfield: subfield[0], line: field.sourceLine } ); + } else { + seenSubfields[ subfield[0] ] = subfield[1]; + } + } ); + + $.each( mandatorySubfields[ field.tagnumber() ] || {}, function( subfield ) { + if ( !seenSubfields[ subfield ] ) { + errors.push( { type: 'missingSubfield', subfield: subfield[0], line: field.sourceLine } ); + } + } ); + } ); + + return errors; + }, + }; + + return KohaBackend; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js new file mode 100644 index 0000000..8aaa2f3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js @@ -0,0 +1,196 @@ +define( [ 'widget' ], function( Widget ) { + function _setIndicators( editor, ind1, ind2 ) { + var info = editor.getLineInfo( editor.cm.getCursor() ); + if (!info || !info.subfields) return false; + + var cur = editor.cm.getCursor(); + + var indicators = [ ind1 || info.contents.substring(4, 5) || '_', ind2 || info.contents.substring(6, 7) || '_' ]; + + editor.cm.replaceRange( + info.tagNumber + ' ' + indicators.join(' ') + ' ' + info.contents.substring(8), + { line: cur.line, ch: 0 }, + { line: cur.line }, + 'marcAware' + ); + + return true; + } + + var _commandGenerators = [ + [ /^copy field data$/i, function() { + return function( editor, state ) { + var info = editor.getLineInfo( editor.cm.getCursor() ); + if (!info) return false; + + if (info.subfields) { + state.clipboard = info.contents.substring(4); + } else { + state.clipboard = info.contents.substring(8); + } + }; + } ], + [ /^copy subfield data$/i, function() { + return function( editor, state ) { + var info = editor.getLineInfo( editor.cm.getCursor() ); + if (!info) return false; + + var cur = editor.cm.getCursor(); + + if (info.subfields) { + for (var i = 0; i < info.subfields.length; i++) { + var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; + if (cur.ch > end) continue; + + state.clipboard = info.contents.substring(info.subfields[i].ch + 3, end); + return; + } + } + + return false; + } + } ], + [ /^delete field$/i, function() { + return function( editor, state ) { + var cur = editor.cm.getCursor(); + + editor.cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' ); + } + } ], + [ /^goto field end$/i, function() { + return function( editor, state ) { + editor.cm.setCursor( { line: editor.cm.lastLine() } ); + } + } ], + [ /^goto field (\w{3})$/i, function(field) { + var matcher = new RegExp('^' + field + ' '); + return function( editor, state ) { + for ( var line = 0, contents; (contents = editor.cm.getLine(line)); line++ ) { + if ( matcher.exec( contents ) ) { + editor.cm.setCursor( { line: line, ch: 0 } ); + return; + } + } + + return false; + } + } ], + [ /^goto subfield end$/i, function() { + return function( editor, state ) { + var cur = editor.cm.getCursor(); + + editor.cm.setCursor( { line: cur.line } ); + } + } ], + [ /^goto subfield (\w)$/i, function( code ) { + return function( editor, state ) { + var info = editor.getLineInfo( editor.cm.getCursor() ); + if (!info || !info.subfields) return false; + + var cur = editor.cm.getCursor(); + + for (var i = 0; i < info.subfields.length; i++) { + if ( info.subfields[i].code != code ) continue; + + var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; + editor.cm.setCursor( { line: cur.line, ch: end } ); + return; + } + + return false; + } + } ], + [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, field, data) { + var new_contents = field + ( field < '100' ? ' ' : ' _ _ ' ) + data.replace(/\\([0-9a-z])/g, '$$$1 '); + return function( editor, state ) { + var line, contents; + + for ( line = 0; (contents = editor.cm.getLine(line)); line++ ) { + if ( contents && contents[0] > field[0] ) break; + } + + if ( line > editor.cm.lastLine() ) { + new_contents = '\n' + new_contents; + } else { + new_contents = new_contents + '\n'; + } + + editor.cm.replaceRange( new_contents, { line: line, ch: 0 }, null, 'marcAware' ); + editor.cm.setCursor( { line: line, ch: 0 } ); + } + } ], + [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, subfield, data) { + return function( editor, state ) { + editor.cm.replaceRange( '$' + subfield + ' ' + data, { line: editor.cm.getCursor().line }, null, 'marcAware' ); + } + } ], + [ /^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 ) { + return ind == '1' ? _setIndicators( editor, ind1, null ) : _setIndicators( editor, null, ind2 ); + } + } ], + [ /^set indicators=([ _0-9])([ _0-9])$/i, function( ind1, ind2 ) { + return function( editor, state ) { + return _setIndicators( editor, ind1, ind2 ); + } + } ], + ]; + + var Macros = { + 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 = Macros.Compile(macro); + if ( compiled.errors.length ) return { errors: compiled.errors }; + var state = { + clipboard: '', + }; + + var result = { errors: [] }; + + editor.cm.operation( function() { + $.each( compiled.commands, function( undef, command ) { + if ( command.func( editor, state ) === false ) { + result.errors.push( { line: command.line, error: 'failed' } ); + return false; + } + } ); + } ); + + return result; + }, + }; + + return Macros; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js new file mode 100644 index 0000000..3c6992b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js @@ -0,0 +1,358 @@ +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; + if ( editor.textMode ) return; + + $('#status-tag-info').empty(); + $('#status-subfield-info').empty(); + + var info = editor.getLineInfo( cm.getCursor() ); + + if ( !info ) return; // No tag at all on this line + + var taginfo = KohaBackend.GetTagInfo( '', info.tagNumber ); + $('#status-tag-info').html( '' + info.tagNumber + ': ' ); + + if ( taginfo ) { + $('#status-tag-info').append( taginfo.lib ); + + if ( !info.currentSubfield ) return; // No current subfield + + var subfieldinfo = taginfo.subfields[info.currentSubfield]; + $('#status-subfield-info').html( '$' + info.currentSubfield + ': ' ); + + if ( subfieldinfo ) { + $('#status-subfield-info').append( subfieldinfo.lib ); + } else { + $('#status-subfield-info').append( '' + _("Unknown subfield") + '' ); + } + } else { + $('#status-tag-info').append( '' + _("Unknown tag") + '' ); + } + } + + function editorBeforeChange( cm, change ) { + var editor = cm.marceditor; + if ( editor.textMode || change.origin == 'marcAware' ) 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 editorChange( cm, change ) { + var editor = cm.marceditor; + if ( editor.textMode ) return; + + var updatedLines = {}; + do { + var from = change.from; + var to = change.to; + if ( to.line < from.line || to.ch < from.ch ) { + var temp = from; + from = to; + to = temp; + } + + var startLine, endLine; + if ( change.text.length == 2 && from.line == to.line && from.ch == to.ch) { + if ( from.ch == 0 ) { + startLine = endLine = from.line; + } else if ( from.ch == cm.getLine(from.line).length ){ + startLine = endLine = from.line + 1; + } + } else { + startLine = (from.ch == cm.getLine(from.line).length && from.line < to.line) ? Math.min(cm.lastLine(), from.line + 1) : from.line; + endLine = ((to.ch == 0 && from.line < to.line) ? Math.max(to.line - 1, 0) : to.line) + change.text.length - 1; + } + + for ( var line = startLine; line <= endLine; line++ ) { + if ( updatedLines[line] ) continue; + + if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( cm.marceditor, line ); + if ( change.origin != 'setValue' && change.origin != 'marcWidgetPrefill' ) cm.addLineClass( line, 'wrapper', 'modified-line' ); + updatedLines[line] = true; + } + } while ( change = change.next ) + + Widget.ActivateAt( cm, cm.getCursor() ); + cm.marceditor.startNotify(); + } + + // Editor helper functions + function activateTabPosition( cm, cur, idx ) { + cm.setCursor( cur ); + Widget.ActivateAt( cm, cur, idx ); + } + + function getTabPositions( editor, cur ) { + var info = editor.getLineInfo( cur || editor.cm.getCursor() ); + + if ( info ) { + if ( info.subfields ) { + var positions = [ 0, 4, 6 ]; + + $.each( info.subfields, function( undef, subfield ) { + positions.push( subfield.ch + 3 ); + } ); + + return positions; + } else { + return [ 0, 4 ]; + } + } 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 } ); + }, + + 'Shift-Enter': function( cm ) { + var cursor = cm.getCursor(); + cm.replaceRange( '\n', { line: cursor.line, ch: 0 }, null, 'marcAware' ); + cm.setCursor( { line: cursor.line, ch: 0 } ); + }, + + 'Ctrl-X': function( cm ) { + // Delete subfield (or cut) + if ( cm.somethingSelected() ) return true; + + var cur = cm.getCursor(); + var info = cm.marceditor.getLineInfo( cur ); + if ( !info || !info.subfields ) return true; + + for (var i = 0; i < info.subfields.length; i++) { + var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; + if (cur.ch > end) continue; + + cm.replaceRange( "", { line: cur.line, ch: info.subfields[i].ch }, { line: cur.line, ch: end }, 'marcAware' ); + return; + } + }, + + 'Shift-Ctrl-X': function( cm ) { + // Delete line + var cur = cm.getCursor(); + + cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' ); + }, + + 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] > cur.ch ) { + activateTabPosition( cm, { line: cur.line, ch: 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] < cur.ch ) { + activateTabPosition( cm, { line: cur.line, ch: positions[i] } ); + return false; + } + } + + if ( cur.line == 0 ) return; + + var prevPositions = getTabPositions( cm, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } ); + + if ( prevPositions.length ) { + activateTabPosition( cm, { line: cur.line - 1, ch: prevPositions[ prevPositions.length - 1 ] }, -1 ); + } else { + cm.setCursor( { line: cur.line - 1, ch: 0 } ); + } + }, + }; + + function MARCEditor( position ) { + this.cm = CodeMirror( + 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( 'change', editorChange ); + this.cm.on( 'cursorActivity', editorCursorActivity ); + + this.subscribers = []; + this.subscribe( function( marceditor ) { + Widget.Notify( marceditor ); + } ); + } + + 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(); + } ); + } + }; + + MARCEditor.prototype.focus = function() { + this.cm.focus(); + }; + + MARCEditor.prototype.refresh = function() { + this.cm.refresh(); + }; + + MARCEditor.prototype.displayRecord = function( record ) { + this.cm.setValue( TextMARC.RecordToText(record) ); + }; + + MARCEditor.prototype.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; + }; + + MARCEditor.prototype.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 }; + }; + + MARCEditor.prototype.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; + }; + + MARCEditor.prototype.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(); + } ); + } + }; + + MARCEditor.prototype.getFixedField = function(field) { + field += ' '; + for ( var line = 0; line < this.cm.lineCount(); line++ ) { + var contents = this.cm.getLine(line); + if ( contents.substr( 0, 4 ) != field ) continue; + + var marks = this.cm.findMarksAt( { line: line, ch: 4 } ); + if ( marks[0] && marks[0].widget ) { + return marks[0].widget.text; + } else { + return contents.substr(4); + } + } + + return null; + }; + + MARCEditor.prototype.startNotify = function() { + if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout ); + this.notifyTimeout = setTimeout( $.proxy( function() { + this.notifyAll(); + + this.notifyTimeout = null; + }, this ), NOTIFY_TIMEOUT ); + }; + + MARCEditor.prototype.notifyAll = function() { + $.each( this.subscribers, $.proxy( function( undef, subscriber ) { + subscriber(this); + }, this ) ); + }; + + MARCEditor.prototype.subscribe = function( subscriber ) { + this.subscribers.push( subscriber ); + }; + + return MARCEditor; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js new file mode 100644 index 0000000..bfa0174 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js @@ -0,0 +1,129 @@ +/** + * Textual MARC mode for CodeMirror. + * Copyright (c) 2013 ByWater + */ + +// 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; + 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 + 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'; + } + } + + if ( stream.eol() ) { + return; + } + + if ( !state.subAllowed && stream.pos == 3 ) { + if ( stream.next() == ' ' ) { + return 'reqspace'; + } else { + stream.skipToEnd(); + return 'error'; + } + } + + if ( stream.pos < 8 && state.subAllowed ) { + switch ( stream.pos ) { + case 3: + case 5: + case 7: + if ( stream.next() == ' ' ) { + return 'reqspace'; + } else { + stream.skipToEnd(); + return 'error'; + } + case 4: + case 6: + if ( /[0-9A-Za-z_]/.test( stream.next() ) ) { + return 'indicator'; + } else { + stream.skipToEnd(); + return 'error'; + } + } + } + + if ( state.subAllowed ) { + if ( stream.pos != 8 && stream.match( /[^$|ǂ‡]+/ ) ) 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 { + stream.skipToEnd(); + } + } + }; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js new file mode 100644 index 0000000..95adf85 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js @@ -0,0 +1,351 @@ +/** + * 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 0000000..1372dbf --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js @@ -0,0 +1,28 @@ +define( function() { + var Preferences = { + Load: function( borrowernumber ) { + if ( !borrowernumber ) 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: {}, + }, 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/search.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js new file mode 100644 index 0000000..06dd8fc --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js @@ -0,0 +1,87 @@ +define( [ 'marc-record', 'pz2' ], function( MARC, Pazpar2 ) { + //var _pz; + var _onresults; + var _recordCache = {}; + var _options; + + var Search = { + Init: function( targets, options ) { + var initOpts = {}; + + _onresults = options.onresults; + + $.each( targets, function ( url, info ) { + initOpts[ 'pz:name[' + url + ']' ] = info.name; + initOpts[ 'pz:queryencoding[' + url + ']' ] = info.encoding; + initOpts[ 'pz:xslt[' + url + ']' ] = info.kohasyntax.toLowerCase() + '-work-groups.xsl'; + initOpts[ 'pz:requestsyntax[' + url + ']' ] = info.syntax; + + // Load in default CCL mappings + // Pazpar2 seems to have a bug where wildcard cclmaps are ignored. + // What an incredible surprise. + initOpts[ 'pz:cclmap:term[' + url + ']' ] = 'u=1016 t=l,r s=al'; + initOpts[ 'pz:cclmap:Author-name[' + url + ']' ] = 'u=1004 s=al'; + initOpts[ 'pz:cclmap:Classification-Dewey[' + url + ']' ] = 'u=13'; + initOpts[ 'pz:cclmap:Classification-LC[' + url + ']' ] = 'u=16'; + initOpts[ 'pz:cclmap:Date[' + url + ']' ] = 'u=30 r=r'; + initOpts[ 'pz:cclmap:Identifier-ISBN[' + url + ']' ] = 'u=7'; + initOpts[ 'pz:cclmap:Identifier-ISSN[' + url + ']' ] = 'u=8'; + initOpts[ 'pz:cclmap:Identifier-publisher-for-music[' + url + ']' ] = 'u=51'; + initOpts[ 'pz:cclmap:Identifier-standard[' + url + ']' ] = 'u=1007'; + initOpts[ 'pz:cclmap:LC-card-number[' + url + ']' ] = 'u=9'; + initOpts[ 'pz:cclmap:Local-number[' + url + ']' ] = 'u=12'; + initOpts[ 'pz:cclmap:Subject[' + url + ']' ] = 'u=21 s=al'; + initOpts[ 'pz:cclmap:Title[' + url + ']' ] = 'u=4 s=al'; + + if ( info.authentication ) initOpts[ 'pz:authentication[' + url + ']' ] = info.authentication; + } ); + + _options = $.extend( { + initopts: initOpts, + onshow: Search._onshow, + errorhandler: options.onerror, + }, options ); + + _pz = new Pazpar2( _options ); + }, + Reconnect: function() { + _pz.reset(); + _pz = new Pazpar2( _options ); + }, + Start: function( targets, q, limit ) { + Search.includedTargets = []; + recordcache = {}; + + $.each( targets, function ( url, info ) { + if ( !info.disabled ) Search.includedTargets.push( url ); + } ); + + _pz.search( q, limit, 'relevance:0', 'pz:id=' + Search.includedTargets.join( '|' ) ); + }, + Fetch: function( offset ) { + _pz.show( offset ); + }, + GetDetailedRecord: function( recid, callback ) { + if ( _recordCache[recid] ) { + callback( _recordCache[recid] ); + return; + } + + _pz.record( recid, 0, undefined, { callback: function(data) { + var record = _recordCache[recid] = new MARC.Record(); + record.loadMARCXML(data.xmlDoc); + + callback(record); + } } ); + }, + _onshow: function( data ) { + $.each( data.hits, function( undef, hit ) { + hit.id = 'search:' + encodeURIComponent( hit.recid[0] ); + } ); + + _onresults( data ); + }, + }; + + return Search; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js new file mode 100644 index 0000000..5c76c62 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js @@ -0,0 +1,78 @@ +define( [ 'marc-record' ], function( MARC ) { + 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() + ' ' + 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] + ' ' + 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, ' ', ' ', [ [ '@', 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, line.substring( subfield.ch + 3, next ? next.ch : line.length ) ] ); + } ); + + field.sourceLine = i; + record.addField( field ); + } + } ); + + return errors.length ? { errors: errors } : record; + } + }; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js new file mode 100644 index 0000000..08122ef --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js @@ -0,0 +1,169 @@ +define( function() { + var Widget = { + Base: { + // Marker utils + clearToText: function() { + var range = this.mark.find(); + this.mark.doc.replaceRange( this.text, range.from, range.to, 'marcAware' ); + }, + + 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 ) ); + + $node.change( $.proxy( function() { + this.setFixed( start, end, $node.val(), '+input' ); + }, this ) ); + }, + + getFixed: function( start, end ) { + return this.text.substring( start, end ); + }, + + setFixed: function( start, end, value, source ) { + this.setText( this.text.substring( 0, start ) + this.padString( 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(); + }, + + // 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 ); + }, + + 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; + } + }, + + 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 } ); + } + } + + $.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 ( !editorWidgets[id] ) return; + var fullBase = $.extend( Object.create( Widget.Base ), editorWidgets[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 }, { + 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(); + mark.changed(); + } + + var $lastInput = $(widget.node).find('input, select').eq(-1); + if ( $lastInput.length ) { + $lastInput.bind( 'keypress', 'tab', function() { + var cur = editor.cm.getCursor(); + editor.cm.setCursor( { line: cur.line } ); + // FIXME: ugly hack + editor.cm.options.extraKeys.Tab( editor.cm ); + editor.focus(); + return false; + } ); + } + } ); + }, + }; + + return Widget; +} ); diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/xslt.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/xslt.js new file mode 100644 index 0000000..fc9b9a7 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/xslt.js @@ -0,0 +1,68 @@ +define( function() { + var XSLT = { + TransformToFragment: function( xmlDoc, xslDoc ) { + if ( window.XSLTProcessor ) { + var proc = new XSLTProcessor(); + proc.importStylesheet( xslDoc ); + proc.setParameter( null, 'showAvailability', false ); + return (new XMLSerializer).serializeToString( proc.transformToFragment( xmlDoc, document ) ); + } else if ( window.ActiveXObject ) { + var xslt = new ActiveXObject( "Msxml2.XSLTemplate" ); + xslt.stylesheet = xslDoc; + var xslProc = xslt.createProcessor(); + xslProc.input = xmlDoc; + xslProc.addParameter( 'showAvailability', false ); + xslProc.transform(); + return xslProc.output; + } else { + return null; + } + }, + + Get: ( function() { + // Horrible browser hack, but required due to the following hard-to-detect and long-unfixed bug: + // https://bugs.webkit.org/show_bug.cgi?id=60276 + var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); + var isSafari = /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor); + + if ( !isChrome && !isSafari ) return $.get; + + return function( url ) { + var result = new jQuery.Deferred(); + var basepath = url.match( /(.*\/)*/ )[0]; + + $.get( url ).done( function( xslDoc ) { + var xslImports = xslDoc.getElementsByTagNameNS( 'http://www.w3.org/1999/XSL/Transform', 'import' ); + var importsRemaining = xslImports.length; + + if ( importsRemaining == 0 ) { + result.resolve( xslDoc ); + return; + } + + $.each( xslImports, function( i, importElem ) { + var path = $( importElem ).attr( 'href' ); + if ( !/^(\/|https?:)/.test( path ) ) path = basepath + path; + + XSLT.Get( path ).done( function( subDoc ) { + importsRemaining--; + $( importElem ).replaceWith( subDoc.documentElement.childNodes ); + + if ( importsRemaining == 0 ) result.resolve( xslDoc ); + } ).fail( function() { + importsRemaining = -1; + + result.reject(); + } ); + } ); + } ).fail( function() { + result.reject(); + } ); + + return result; + }; + } )(), + }; + + return XSLT; +} ); diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css new file mode 100644 index 0000000..33cfb58 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css @@ -0,0 +1,332 @@ +/*> 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; +} + +/*> 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; +} + +#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 { + height: 1.5em; + vertical-align: middle; +} + +#editor .subfield-widget select:focus { + outline: 1px #83A230 solid; +} + +#editor .fixed-widget select { + width: 3em; +} + +#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 { + background-color: white; + border: solid 2px #444; + border-radius: 6px 6px 6px 6px; + height: 80%; + overflow: auto; + padding: 10px; + width: 90%; +} + +#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 { + margin: 0; + padding: 0; +} + +#advanced-search-fields li { + display: inline-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 */ + +#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; +} + +.results-marc { + 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 { + height: 400px; + 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; +} diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc new file mode 100644 index 0000000..a831c3b --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc @@ -0,0 +1,804 @@ + + + + + + + + +[% 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 0000000..ef6035d --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-widgets-marc21.inc @@ -0,0 +1,155 @@ + + + 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 cbe0a39..0e7393f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt @@ -74,6 +74,20 @@ return false; }); + $( '#switcheditor' ).click( function() { + if ( !confirm( _("Any changes will not be saved. Continue?") ) ) return; + + $.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'; + } + } ); + }); function redirect(dest){ @@ -402,6 +416,7 @@ function Changefwk(FwkList) { [% UNLESS (circborrowernumber) %][%# Hide in fast cataloging %] + [% IF (biblionumber) %] [% IF ( BiblioDefaultViewmarc ) %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt new file mode 100644 index 0000000..6cd5e56 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt @@ -0,0 +1,201 @@ +[% INCLUDE 'doc-head-open.inc' %] +Koha › Cataloging › Editor +[% INCLUDE 'doc-head-close.inc' %] + + + + + +[% IF ( bidi ) %] + +[% END %] + + + +
+
Loading, please wait...
+
+ +[% INCLUDE 'header.inc' %] + + + +
+
+ +

Cataloging editor

+ +
+ +
+ +
+
+ + + +
+ + + +
+ [%# CodeMirror instance will be inserted here %] +
+
+
+
+
+
+
+ +
+ + + +
+
+ + + + + + + +[% PROCESS 'cateditor-ui.inc' %] + +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/svc/cataloguing/framework b/svc/cataloguing/framework new file mode 100755 index 0000000..8213de6 --- /dev/null +++ b/svc/cataloguing/framework @@ -0,0 +1,73 @@ +#!/usr/bin/perl + +use Modern::Perl '2009'; + +use CGI; +use C4::Branch; +use C4::ClassSource; +use C4::Context; +use C4::Biblio; +use C4::Service; + +my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' ); + +my ( $frameworkcode ) = C4::Service->require_params( 'frameworkcode' ); + +my $tagslib = GetMarcStructure( 1, $frameworkcode ); + +my @tags; + +foreach my $tag ( sort keys %$tagslib ) { + my $taglib = $tagslib->{$tag}; + my $taginfo = { map { $_, $taglib->{$_} } grep { length $_ > 1 } keys %$taglib }; + $taginfo->{subfields} = [ map { [ $_, $taglib->{$_} ] } grep { length $_ == 1 } sort keys %$taglib ]; + + push @tags, [ $tag, $taginfo ]; +} + +my $dbh = C4::Context->dbh; +my $authorised_values = {}; + +$authorised_values->{branches} = []; +my $onlymine=C4::Context->preference('IndependentBranches') && + C4::Context->userenv && + C4::Context->userenv->{flags} % 2 == 0 && + C4::Context->userenv->{branch}; +my $branches = GetBranches($onlymine); +foreach my $thisbranch ( sort keys %$branches ) { + push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} }; +} + +$authorised_values->{itemtypes} = $dbh->selectall_arrayref( q{ + SELECT itemtype AS value, description AS lib FROM itemtypes ORDER BY description +}, { Slice => {} } ); + +my $class_sources = GetClassSources(); + +my $default_source = C4::Context->preference("DefaultClassificationSource"); + +foreach my $class_source (sort keys %$class_sources) { + next unless $class_sources->{$class_source}->{'used'} or + ($class_source eq $default_source); + push @{ $authorised_values->{cn_source} }, { value => $class_source, lib => $class_sources->{$class_source}->{'description'} }; +} + +my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; +my $auth_query = "SELECT category, authorised_value, lib + FROM authorised_values"; +$auth_query .= qq{ LEFT JOIN authorised_values_branches ON ( id = av_id )} if $branch_limit; +$auth_query .= " AND ( branchcode = ? OR branchcode IS NULL )" if $branch_limit; +$auth_query .= " GROUP BY lib ORDER BY lib, lib_opac"; +my $authorised_values_sth = $dbh->prepare( $auth_query ); +$authorised_values_sth->execute( + $branch_limit ? $branch_limit : (), +); + +while ( my ( $category, $value, $lib ) = $authorised_values_sth->fetchrow_array ) { + $authorised_values->{$category} ||= []; + push @{ $authorised_values->{$category} }, { value => $lib, lib => $lib }; +} + +$response->param( framework => \@tags, authorised_values => $authorised_values ); + +C4::Service->return_success( $response ); -- 1.9.0