Bug 11559: (followup) fix first-character deletion, small usability issues
[koha.git] / koha-tmpl / intranet-tmpl / lib / koha / cateditor / widget.js
1 /**
2  * Copyright 2015 ByWater Solutions
3  *
4  * This file is part of Koha.
5  *
6  * Koha is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Koha is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Koha; if not, see <http://www.gnu.org/licenses>.
18  */
19
20 define( [ 'resources' ], function( Resources ) {
21     var _widgets = {};
22
23     var Widget = {
24         Register: function( tagfield, widget ) {
25             _widgets[tagfield] = widget;
26         },
27
28         PadNum: function( number, length ) {
29             var result = number.toString();
30
31             while ( result.length < length ) result = '0' + result;
32
33             return result;
34         },
35
36         PadString: function( result, length ) {
37             while ( result.length < length ) result = ' ' + result;
38
39             return result;
40         },
41
42         PadStringRight: function( result, length ) {
43             result = '' + result;
44             while ( result.length < length ) result += ' ';
45
46             return result;
47         },
48
49         Base: {
50             // Marker utils
51             clearToText: function() {
52                 var range = this.mark.find();
53                 if ( this.text == null ) throw new Error('Tried to clear widget with no text');
54                 this.mark.doc.replaceRange( this.text, range.from, range.to, 'widget.clearToText' );
55             },
56
57             reCreate: function() {
58                 this.postCreate( this.node, this.mark );
59             },
60
61             // Fixed field utils
62             bindFixed: function( sel, start, end ) {
63                 var $node = $( this.node ).find( sel );
64                 $node.val( this.getFixed( start, end ) );
65
66                 var widget = this;
67                 var $collapsed = $( '<span class="fixed-collapsed" title="' + $node.attr('title') + '">' + $node.val() + '</span>' ).insertAfter( $node );
68
69                 function show() {
70                     $collapsed.hide();
71                     $node.val( widget.getFixed( start, end ).replace(/\s+$/, '') );
72                     $node.show();
73                     $node[0].focus();
74                 }
75
76                 function hide() {
77                     $node.hide();
78                     $collapsed.text( Widget.PadStringRight( $node.val(), end - start ) ).show();
79                 }
80
81                 $node.on( 'change keyup', function() {
82                     widget.setFixed( start, end, $node.val(), '+input' );
83                 } ).focus( show ).blur( hide );
84
85                 hide();
86
87                 $collapsed.click( show );
88             },
89
90             getFixed: function( start, end ) {
91                 return this.text.substring( start, end );
92             },
93
94             setFixed: function( start, end, value, source ) {
95                 this.setText( this.text.substring( 0, start ) + Widget.PadStringRight( value.toString().substr( 0, end - start ), end - start ) + this.text.substring( end ), source );
96             },
97
98             setText: function( text, source ) {
99                 if ( source == '+input' ) this.mark.doc.cm.addLineClass( this.mark.find().from.line, 'wrapper', 'modified-line' );
100                 this.text = text;
101                 this.editor.startNotify();
102             },
103
104             createFromXML: function( resourceId ) {
105                 var widget = this;
106
107                 Resources[resourceId].done( function( xml ) {
108                     $(widget.node).find('.widget-loading').remove();
109                     var $matSelect = $('<select class="material-select"></select>').appendTo(widget.node);
110                     var $contents = $('<span class="material-contents"/>').appendTo(widget.node);
111                     var materialInfo = {};
112
113                     $('Tagfield', xml).children('Material').each( function() {
114                         $matSelect.append( '<option value="' + $(this).attr('id') + '">' + $(this).attr('id') + ' - ' + $(this).children('name').text() + '</option>' );
115
116                         materialInfo[ $(this).attr('id') ] = this;
117                     } );
118
119                     $matSelect.change( function() {
120                         widget.loadXMLMaterial( materialInfo[ $matSelect.val() ] );
121                     } ).change();
122                 } );
123             },
124
125             loadXMLMaterial: function( materialInfo ) {
126                 var $contents = $(this.node).children('.material-contents');
127                 $contents.empty();
128
129                 var widget = this;
130
131                 $(materialInfo).children('Position').each( function() {
132                     var match = $(this).attr('pos').match(/(\d+)(?:-(\d+))?/);
133                     if (!match) return;
134
135                     var start = parseInt(match[1]);
136                     var end = ( match[2] ? parseInt(match[2]) : start ) + 1;
137                     var $input;
138                     var $values = $(this).children('Value');
139
140                     if ($values.length == 0) {
141                         $contents.append( '<span title="' + $(this).children('name').text() + '">' + widget.getFixed(start, end) + '</span>' );
142                         return;
143                     }
144
145                     if ( match[2] ) {
146                         $input = $( '<input name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '" maxlength="' + (end - start) + '" />' );
147                     } else {
148                         $input = $( '<select name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '"></select>' );
149
150                         $values.each( function() {
151                             $input.append( '<option value="' + $(this).attr('code') + '">' + $(this).attr('code') + ' - ' + $(this).children('description').text() + '</option>' );
152                         } );
153                     }
154
155                     $contents.append( $input );
156                     widget.bindFixed( $input, start, end );
157                 } );
158             },
159
160             nodeChanged: function() {
161                 this.mark.changed();
162                 var widget = this;
163
164                 var $inputs = $(this.node).find('input, select');
165                 if ( !$inputs.length ) return;
166
167                 $inputs.off('keydown.marc-tab');
168                 var editor = widget.editor;
169
170                 $inputs.each( function( i ) {
171                     $(this).on( 'keydown.marc-tab', function( e ) {
172                         // Cheap hack to disable backspace and special keys
173                         if ( ( this.nodeName.toLowerCase() == 'select' && e.which == 9 ) || e.ctrlKey ) {
174                             e.preventDefault();
175                             return;
176                         } else if ( e.which != 9 ) { // Tab
177                             return;
178                         }
179
180                         var span = widget.mark.find();
181                         var cur = editor.cm.getCursor();
182
183                         if ( e.shiftKey ) {
184                             if ( i > 0 ) {
185                                 $inputs.eq(i - 1).trigger( 'focus' );
186                             } else {
187                                 editor.cm.setCursor( span.from );
188                                 // FIXME: ugly hack
189                                 editor.cm.options.extraKeys['Shift-Tab']( editor.cm );
190                                 editor.focus();
191                             }
192                         } else {
193                             if ( i < $inputs.length - 1 ) {
194                                 $inputs.eq(i + 1).trigger( 'focus' );
195                             } else {
196                                 editor.cm.setCursor( span.to );
197                                 editor.focus();
198                             }
199                         }
200
201                         return false;
202                     } );
203                 } );
204             },
205
206             // Template utils
207             insertTemplate: function( sel ) {
208                 var wsOnly = /^\s*$/;
209                 $( sel ).contents().clone().each( function() {
210                     if ( this.nodeType == Node.TEXT_NODE ) {
211                         this.data = this.data.replace( /^\s+|\s+$/g, '' );
212                     }
213                 } ).appendTo( this.node );
214             },
215         },
216
217         ActivateAt: function( editor, cur, idx ) {
218             var marks = editor.findMarksAt( cur );
219             if ( !marks.length || !marks[0].widget ) return false;
220
221             var $input = $(marks[0].widget.node).find('input, select').eq(idx || 0);
222             if ( !$input.length ) return false;
223
224             $input.focus();
225             return true;
226         },
227
228         Notify: function( editor ) {
229             $.each( editor.cm.getAllMarks(), function( undef, mark ) {
230                 if ( mark.widget && mark.widget.notify ) mark.widget.notify();
231             } );
232         },
233
234         UpdateLine: function( editor, line ) {
235             var info = editor.getLineInfo( { line: line, ch: 0 } );
236             var lineh = editor.cm.getLineHandle( line );
237             if ( !lineh ) return;
238
239             if ( !info ) {
240                 if ( lineh.markedSpans ) {
241                     $.each( lineh.markedSpans, function ( undef, span ) {
242                         var mark = span.marker;
243                         if ( !mark.widget ) return;
244
245                         mark.widget.clearToText();
246                     } );
247                 }
248                 return;
249             }
250
251             var subfields = [];
252
253             var end = editor.cm.getLine( line ).length;
254             if ( info.tagNumber < '010' ) {
255                 if ( end >= 4 ) subfields.push( { code: '@', from: 4, to: end } );
256             } else {
257                 for ( var i = 0; i < info.subfields.length; i++ ) {
258                     var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end;
259                     subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 2, to: next } );
260                 }
261                 // If not a fixed field, and we didn't find any subfields, we need to throw in the
262                 // '@' subfield so we can properly remove it
263                 if ( subfields.length == 0 ) subfields.push( { code: '@', from: 4, to: end } );
264             }
265
266             $.each( subfields, function ( undef, subfield ) {
267                 var id = info.tagNumber + subfield.code;
268                 var marks = editor.cm.findMarksAt( { line: line, ch: subfield.from } );
269
270                 if ( marks.length ) {
271                     if ( marks[0].id == id ) {
272                         return;
273                     } else if ( marks[0].widget ) {
274                         marks[0].widget.clearToText();
275                     }
276                 }
277
278                 if ( !_widgets[id] ) return;
279                 var fullBase = $.extend( Object.create( Widget.Base ), _widgets[id] );
280                 var widget = Object.create( fullBase );
281
282                 if ( subfield.from == subfield.to ) {
283                     editor.cm.replaceRange( widget.makeTemplate ? widget.makeTemplate() : '<empty>', { line: line, ch: subfield.from }, null, 'marcWidgetPrefill' );
284                     return; // We'll do the actual work when the change event is triggered again
285                 }
286
287                 var text = editor.cm.getRange( { line: line, ch: subfield.from }, { line: line, ch: subfield.to } );
288
289                 widget.text = text;
290                 var node = widget.init();
291
292                 var mark = editor.cm.markText( { line: line, ch: subfield.from }, { line: line, ch: subfield.to }, {
293                     atomic: true,
294                     inclusiveLeft: false,
295                     inclusiveRight: false,
296                     replacedWith: node,
297                 } );
298
299                 mark.id = id;
300                 mark.widget = widget;
301
302                 widget.node = node;
303                 widget.mark = mark;
304                 widget.editor = editor;
305
306                 if ( widget.postCreate ) {
307                     widget.postCreate();
308                 }
309
310                 widget.nodeChanged();
311             } );
312         },
313     };
314
315     return Widget;
316 } );