Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
/* global ns */
2
/**
3
 * This file contains helper functions for the editor.
4
 */
5
 
6
// Grab common resources set in parent window, but avoid sharing back resources set in iframe)
7
window.ns = window.H5PEditor = H5P.jQuery.extend(false, {}, window.parent.H5PEditor);
8
ns.$ = H5P.jQuery;
9
 
10
// Load needed resources from parent.
11
H5PIntegration = H5P.jQuery.extend(false, {}, window.parent.H5PIntegration);
12
H5PIntegration.loadedJs = [];
13
H5PIntegration.loadedCss = [];
14
 
15
/**
16
 * Constants used within editor
17
 *
18
 * @type {{otherLibraries: string}}
19
 */
20
ns.constants = {
21
  otherLibraries: 'Other Libraries',
22
};
23
 
24
/**
25
 * Keep track of our widgets.
26
 */
27
ns.widgets = {};
28
 
29
/**
30
 * Caches library data (semantics, js and css)
31
 */
32
ns.libraryCache = {};
33
 
34
/**
35
 * Keeps track of callbacks to run once a library gets loaded.
36
 */
37
ns.loadedCallbacks = [];
38
 
39
/**
40
 * Keep track of which libraries have been loaded in the browser, i.e CSS is
41
 * added and JS have been run
42
 *
43
 * @type {Object}
44
 */
45
ns.libraryLoaded = {};
46
 
47
/**
48
 * Indiciates if the user is using Internet Explorer.
49
 */
50
ns.isIE = navigator.userAgent.match(/; MSIE \d+.\d+;/) !== null;
51
 
52
/**
53
 * Keep track of renderable common fields.
54
 *
55
 * @type {Object}
56
 */
57
ns.renderableCommonFields = {};
58
 
59
(() => {
60
  const loading = {}; // Map of callbacks for each src being loaded
61
 
62
  /**
63
   * Help load JavaScripts, prevents double loading.
64
   *
65
   * @param {string} src
66
   * @param {Function} done Callback
67
   */
68
  ns.loadJs = (src, done) => {
69
    if (H5P.jsLoaded(src)) {
70
      // Already loaded
71
      done();
72
      return;
73
    }
74
 
75
    if (loading[src] !== undefined) {
76
      // Loading in progress...
77
      loading[src].push(done);
78
      return;
79
    }
80
 
81
    loading[src] = [done];
82
 
83
    // Load using script tag
84
    var script = document.createElement('script');
85
    script.type = 'text/javascript';
86
    script.charset = 'UTF-8';
87
    script.async = false;
88
    script.onload = function () {
89
      H5PIntegration.loadedJs.push(src);
90
      loading[src].forEach(cb => cb());
91
      delete loading[src];
92
    };
93
    script.onerror = function (err) {
94
      loading[src].forEach(cb => cb(err));
95
      delete loading[src];
96
    };
97
    script.src = src;
98
    document.head.appendChild(script);
99
  };
100
})();
101
 
102
/**
103
 * Helper function invoked when a library is requested. Will add CSS and eval JS
104
 * if not already done.
105
 *
106
 * @private
107
 * @param {string} libraryName On the form "machineName majorVersion.minorVersion"
108
 * @param {Function} callback
109
 */
110
ns.libraryRequested = function (libraryName, callback) {
111
  var libraryData = ns.libraryCache[libraryName];
112
 
113
  if (!ns.libraryLoaded[libraryName]) {
114
    // Add CSS.
115
    if (libraryData.css !== undefined) {
116
      libraryData.css.forEach(function (path) {
117
        if (!H5P.cssLoaded(path)) {
118
          H5PIntegration.loadedCss.push(path);
119
          if (path) {
120
            ns.$('head').append('<link ' +
121
              'rel="stylesheet" ' +
122
              'href="' + path + '" ' +
123
              'type="text/css" ' +
124
              '/>');
125
          }
126
        }
127
      });
128
    }
129
 
130
    // Add JS
131
    var loadingJs = false;
132
    if (libraryData.javascript !== undefined && libraryData.javascript.length) {
133
      libraryData.javascript.forEach(function (path) {
134
        if (!H5P.jsLoaded(path)) {
135
          loadingJs = true;
136
          ns.loadJs(path, function (err) {
137
            if (err) {
138
              console.error('Error while loading script', err);
139
              return;
140
            }
141
 
142
            var isFinishedLoading = libraryData.javascript.reduce(function (hasLoaded, jsPath) {
143
              return hasLoaded && H5P.jsLoaded(jsPath);
144
            }, true);
145
 
146
            if (isFinishedLoading) {
147
              ns.libraryLoaded[libraryName] = true;
148
 
149
              // Need to set translations after all scripts have been loaded
150
              if (libraryData.translations) {
151
                for (var machineName in libraryData.translations) {
152
                  H5PEditor.language[machineName] = libraryData.translations[machineName];
153
                }
154
              }
155
 
156
              callback(ns.libraryCache[libraryName].semantics);
157
            }
158
          });
159
        }
160
      });
161
    }
162
    if (!loadingJs) {
163
      // Don't have to wait for any scripts, run callback
164
      ns.libraryLoaded[libraryName] = true;
165
      callback(ns.libraryCache[libraryName].semantics);
166
    }
167
  }
168
  else {
169
    // Already loaded, run callback
170
    callback(ns.libraryCache[libraryName].semantics);
171
  }
172
};
173
 
174
/**
175
 * Loads the given library, inserts any css and js and
176
 * then runs the callback with the samantics as an argument.
177
 *
178
 * @param {string} libraryName
179
 *  On the form machineName majorVersion.minorVersion
180
 * @param {function} callback
181
 * @returns {undefined}
182
 */
183
ns.loadLibrary = function (libraryName, callback) {
184
  switch (ns.libraryCache[libraryName]) {
185
    default:
186
      // Get semantics from cache.
187
      ns.libraryRequested(libraryName, callback);
188
      break;
189
 
190
    case 0:
191
      // Add to queue.
192
      if (ns.loadedCallbacks[libraryName] === undefined) {
193
        ns.loadedCallbacks[libraryName] = [];
194
      }
195
      ns.loadedCallbacks[libraryName].push(callback);
196
      break;
197
 
198
    case undefined:
199
      // Load semantics.
200
      ns.libraryCache[libraryName] = 0; // Indicates that others should queue.
201
      ns.loadedCallbacks[libraryName] = []; // Other callbacks to run once loaded.
202
      var library = ns.libraryFromString(libraryName);
203
 
204
      var url = ns.getAjaxUrl('libraries', library);
205
 
206
      // Add content language to URL
207
      if (ns.contentLanguage !== undefined) {
208
        url += (url.indexOf('?') === -1 ? '?' : '&') + 'language=' + ns.contentLanguage;
209
      }
210
      // Add common fields default lanuage to URL
211
      const defaultLanguage = ns.defaultLanguage; // Avoid changes after sending AJAX
212
      if (defaultLanguage !== undefined) {
213
        url += (url.indexOf('?') === -1 ? '?' : '&') + 'default-language=' + defaultLanguage;
214
      }
215
 
216
      // Fire away!
217
      ns.$.ajax({
218
        url: url,
219
        success: function (libraryData) {
220
          libraryData.translation = { // Used to cache all the translations
221
            en: libraryData.semantics
222
          };
223
          let languageSemantics = [];
224
          if (libraryData.language !== null) {
225
            languageSemantics = JSON.parse(libraryData.language).semantics;
226
            delete libraryData.language; // Avoid caching a lot of unused data
227
          }
228
          var semantics = ns.$.extend(true, [], libraryData.semantics, languageSemantics);
229
          if (libraryData.defaultLanguage !== null) {
230
            libraryData.translation[defaultLanguage] = JSON.parse(libraryData.defaultLanguage).semantics;
231
            delete libraryData.defaultLanguage; // Avoid caching a lot of unused data
232
            ns.updateCommonFieldsDefault(semantics, libraryData.translation[defaultLanguage]);
233
          }
234
          libraryData.semantics = semantics;
235
          ns.libraryCache[libraryName] = libraryData;
236
 
237
          ns.libraryRequested(libraryName, function (semantics) {
238
            callback(semantics);
239
 
240
            // Run queue.
241
            if (ns.loadedCallbacks[libraryName]) {
242
              for (var i = 0; i < ns.loadedCallbacks[libraryName].length; i++) {
243
                ns.loadedCallbacks[libraryName][i](semantics);
244
              }
245
            }
246
          });
247
        },
248
        error: function (jqXHR, textStatus, errorThrown) {
249
          if (window['console'] !== undefined) {
250
            console.warn('Ajax request failed');
251
            console.warn(jqXHR);
252
            console.warn(textStatus);
253
            console.warn(errorThrown);
254
          }
255
        },
256
        dataType: 'json'
257
      });
258
  }
259
};
260
 
261
/**
262
 * Update common fields default values for the given semantics.
263
 * Works by reference.
264
 *
265
 * @param {Array} semantics
266
 * @param {Array} translation
267
 * @param {boolean} [parentIsCommon] Used to indicated that one of the ancestors is a common field
268
 */
269
ns.updateCommonFieldsDefault = function (semantics, translation, parentIsCommon) {
270
  for (let i = 0; i < semantics.length; i++) {
271
    const isCommon = (semantics[i].common === true || parentIsCommon);
272
    if (isCommon && semantics[i].default !== undefined &&
273
        translation[i] !== undefined && translation[i].default !== undefined) {
274
      // Update value
275
      semantics[i].default = translation[i].default;
276
    }
277
    if (semantics[i].fields !== undefined && semantics[i].fields.length &&
278
        translation[i].fields !== undefined && translation[i].fields.length) {
279
      // Look into sub fields
280
      ns.updateCommonFieldsDefault(semantics[i].fields, translation[i].fields, isCommon);
281
    }
282
    if (semantics[i].field !== undefined && translation[i].field !== undefined ) {
283
      // Look into sub field
284
      ns.updateCommonFieldsDefault([semantics[i].field], [translation[i].field], isCommon);
285
    }
286
  }
287
};
288
 
289
/**
290
 * Reset loaded libraries - i.e removes CSS added previously.
291
 * @method
292
 * @return {[type]}
293
 */
294
ns.resetLoadedLibraries = function () {
295
  ns.$('head style.h5p-editor-style').remove();
296
  H5PIntegration.loadedCss = [];
297
  H5PIntegration.loadedJs = [];
298
  ns.loadedCallbacks = [];
299
  ns.libraryLoaded = {};
300
  ns.libraryCache = {};
301
};
302
 
303
/**
304
 * Render common fields of content type with given machine name
305
 *
306
 * @param {string} machineName Machine name of content type with common fields
307
 * @param {Array} [libraries] Library data for machine name
308
 */
309
ns.renderCommonField = function (machineName, libraries) {
310
  var commonFields = ns.renderableCommonFields[machineName].fields;
311
  var renderableCommonFields = [];
312
  var ancestor;
313
 
314
  commonFields.forEach(function (field) {
315
    if (!field.rendered) {
316
      var commonField = ns.addCommonField(
317
        field.field,
318
        field.parent,
319
        field.params,
320
        field.ancestor,
321
        true
322
      );
323
      if (commonField.setValues.length === 1) {
324
        renderableCommonFields.push({
325
          field: field,
326
          instance: commonField.instance
327
        });
328
        field.instance = commonField.instance;
329
      }
330
    }
331
    field.rendered = true;
332
  });
333
 
334
  // Render common fields if found
335
  if (renderableCommonFields.length) {
336
    var libraryName = machineName === ns.constants.otherLibraries ? machineName
337
      : (machineName.length ? machineName.split(' ')[0] : '');
338
    if (libraries.length && libraries[0].title) {
339
      libraryName = libraries[0].title;
340
    }
341
 
342
    // Create a library wrapper
343
    var hasLibraryWrapper = !!ns.renderableCommonFields[machineName].wrapper;
344
    var commonFieldsLibraryWrapper = ns.renderableCommonFields[machineName].wrapper;
345
    if (!hasLibraryWrapper) {
346
      commonFieldsLibraryWrapper = document.createElement('fieldset');
347
      var libraryWrapperClass = libraryName.replace(/\s+/g, '-').toLowerCase();
348
 
349
      commonFieldsLibraryWrapper.classList.add('common-fields-library-wrapper');
350
      commonFieldsLibraryWrapper.classList.add('common-fields-' + libraryWrapperClass);
351
 
352
      var libraryTitle = document.createElement('legend');
353
      libraryTitle.classList.add('common-field-legend');
354
      libraryTitle.textContent = libraryName;
355
      libraryTitle.tabIndex = '0';
356
      libraryTitle.setAttribute('role', 'button');
357
      libraryTitle.addEventListener('click', function () {
358
        commonFieldsLibraryWrapper.classList.toggle('expanded');
359
      });
360
      libraryTitle.addEventListener('keypress', function (e) {
361
        if (e.which === 32) {
362
          commonFieldsLibraryWrapper.classList.toggle('expanded');
363
        }
364
      });
365
      commonFieldsLibraryWrapper.appendChild(libraryTitle);
366
 
367
      ns.renderableCommonFields[machineName].wrapper = commonFieldsLibraryWrapper;
368
    }
369
 
370
    renderableCommonFields.forEach(function (commonField) {
371
      commonField.instance.appendTo(ns.$(commonFieldsLibraryWrapper));
372
      // Gather under a common ancestor
373
      if (commonField.field && commonField.field.ancestor) {
374
        ancestor = commonField.field.ancestor;
375
 
376
        // Ensure that params are updated after common field instance is
377
        // appended since this ensures that defaults are set for common fields
378
        const field = commonField.field;
379
        const library = field.parent.currentLibrary;
380
        const fieldName = field.field.name;
381
        const ancestorField = ancestor.commonFields[library][fieldName];
382
        ancestorField.params = field.params[fieldName];
383
      }
384
    });
385
 
386
    if (!hasLibraryWrapper && ancestor) {
387
      ancestor.$common[0].appendChild(commonFieldsLibraryWrapper);
388
    }
389
  }
390
};
391
 
392
/**
393
 * Recursively traverse parents to find the library our field belongs to
394
 *
395
 * @param parent
396
 * @returns {*}
397
 */
398
ns.getParentLibrary = function (parent) {
399
  if (!parent) {
400
    return null;
401
  }
402
 
403
  if (parent.currentLibrary) {
404
    return parent.currentLibrary;
405
  }
406
 
407
  return ns.getParentLibrary(parent.parent);
408
};
409
 
410
/**
411
 * Recursive processing of the semantics chunks.
412
 *
413
 * @param {array} semanticsChunk
414
 * @param {object} params
415
 * @param {jQuery} $wrapper
416
 * @param {mixed} parent
417
 * @param {string} [machineName] Machine name of library that is being processed
418
 * @returns {undefined}
419
 */
420
ns.processSemanticsChunk = function (semanticsChunk, params, $wrapper, parent, machineName) {
421
  var ancestor;
422
  parent.children = [];
423
 
424
  if (parent.passReadies === undefined) {
425
    throw 'Widget tried to run processSemanticsChunk without handling ready callbacks. [field:' + parent.field.type + ':' + parent.field.name + ']';
426
  }
427
 
428
  if (!parent.passReadies) {
429
    // If the parent can't pass ready callbacks we need to take care of them.
430
    parent.readies = [];
431
  }
432
 
433
  for (var i = 0; i < semanticsChunk.length; i++) {
434
    var field = semanticsChunk[i];
435
 
436
    // Check generic field properties.
437
    if (field.name === undefined) {
438
      throw ns.t('core', 'missingProperty', {':index': i, ':property': 'name'});
439
    }
440
    if (field.type === undefined) {
441
      throw ns.t('core', 'missingProperty', {':index': i, ':property': 'type'});
442
    }
443
 
444
    // Set default value.
445
    if (params[field.name] === undefined && field['default'] !== undefined) {
446
      params[field.name] = field['default'];
447
    }
448
 
449
    var widget = ns.getWidgetName(field);
450
 
451
    // TODO: Remove later, this is here for debugging purposes.
452
    if (ns.widgets[widget] === undefined) {
453
      $wrapper.append('<div>[field:' + field.type + ':' + widget + ':' + field.name + ']</div>');
454
      continue;
455
    }
456
 
457
    // Add common fields to bottom of form.
458
    if (field.common !== undefined && field.common) {
459
      if (ancestor === undefined) {
460
        ancestor = ns.findAncestor(parent);
461
      }
462
 
463
      var parentLibrary = ns.getParentLibrary(parent);
464
      var library = machineName ? machineName
465
        : (field.library ? field.library
466
          : (parentLibrary ? parentLibrary
467
            : ns.constants.otherLibraries));
468
      ns.renderableCommonFields[library] = ns.renderableCommonFields[library] || {};
469
      ns.renderableCommonFields[library].fields = ns.renderableCommonFields[library].fields || [];
470
 
471
      // Add renderable if it doesn't exist
472
      ns.renderableCommonFields[library].fields.push({
473
        field: field,
474
        parent: parent,
475
        params: params,
476
        ancestor: ancestor,
477
        rendered: false
478
      });
479
      continue;
480
    }
481
 
482
    var fieldInstance = new ns.widgets[widget](parent, field, params[field.name], function (field, value) {
483
      if (value === undefined) {
484
        delete params[field.name];
485
      }
486
      else {
487
        params[field.name] = value;
488
      }
489
    });
490
    fieldInstance.appendTo($wrapper);
491
    parent.children.push(fieldInstance);
492
  }
493
 
494
  // Render all gathered common field
495
  if (ns.renderableCommonFields) {
496
    for (var commonFieldMachineName in ns.renderableCommonFields) {
497
      if (commonFieldMachineName === ns.constants.otherLibraries) {
498
        // No need to grab library info
499
        ns.renderCommonField(commonFieldMachineName);
500
      }
501
      else {
502
        // Get title for common fields group
503
        H5PEditor.LibraryListCache.getLibraries(
504
          [commonFieldMachineName],
505
          ns.renderCommonField.bind(this, commonFieldMachineName)
506
        );
507
      }
508
    }
509
  }
510
 
511
  if (!parent.passReadies) {
512
    // Run ready callbacks.
513
    for (i = 0; i < parent.readies.length; i++) {
514
      parent.readies[i]();
515
    }
516
    delete parent.readies;
517
  }
518
};
519
 
520
/**
521
 * Attach ancestor of parent's common fields to a new wrapper
522
 *
523
 * @param {Object} parent Parent content type instance that common fields should be attached to
524
 * @param {HTMLElement} wrapper New wrapper of common fields
525
 */
526
ns.setCommonFieldsWrapper = function (parent, wrapper) {
527
  var ancestor = ns.findAncestor(parent);
528
  // Hide the ancestor whose children will be reattached elsewhere
529
  wrapper.appendChild(ancestor.$common[0]);
530
};
531
 
532
/**
533
 * Add a field to the common container.
534
 *
535
 * @param {object} field
536
 * @param {object} parent
537
 * @param {object} params
538
 * @param {object} ancestor
539
 * @param {boolean} [skipAppendTo] Skips appending the common field if set
540
 * @returns {undefined}
541
 */
542
ns.addCommonField = function (field, parent, params, ancestor, skipAppendTo) {
543
  var commonField;
544
 
545
  // Group all fields based on library name + version
546
  if (ancestor.commonFields[parent.currentLibrary] === undefined) {
547
    ancestor.commonFields[parent.currentLibrary] = {};
548
  }
549
 
550
  // Field name will have to be unique for library
551
  if (ancestor.commonFields[parent.currentLibrary][field.name] === undefined) {
552
    var widget = ns.getWidgetName(field);
553
    ancestor.commonFields[parent.currentLibrary][field.name] = {
554
      instance: new ns.widgets[widget](parent, field, params[field.name], function (field, value) {
555
        for (var i = 0; i < commonField.setValues.length; i++) {
556
          commonField.setValues[i](field, value);
557
        }
558
      }),
559
      setValues: [],
560
      parents: []
561
    };
562
  }
563
 
564
  commonField = ancestor.commonFields[parent.currentLibrary][field.name];
565
  commonField.parents.push(ns.findLibraryAncestor(parent));
566
  commonField.setValues.push(function (field, value) {
567
    if (value === undefined) {
568
      delete params[field.name];
569
    }
570
    else {
571
      params[field.name] = value;
572
    }
573
  });
574
 
575
  if (commonField.setValues.length === 1) {
576
    ancestor.$common.parent().removeClass('hidden');
577
    if (!skipAppendTo) {
578
      commonField.instance.appendTo(ancestor.$common);
579
    }
580
    commonField.params = params[field.name];
581
  }
582
  else {
583
    params[field.name] = commonField.params;
584
  }
585
 
586
  parent.children.push(commonField.instance);
587
  return commonField;
588
};
589
 
590
/**
591
 * Find the nearest library ancestor. Used when adding commonfields.
592
 *
593
 * @param {object} parent
594
 * @returns {ns.findLibraryAncestor.parent|@exp;ns@call;findLibraryAncestor}
595
 */
596
ns.findLibraryAncestor = function (parent) {
597
  if (parent.parent === undefined || parent.field.type === 'library') {
598
    return parent;
599
  }
600
  return ns.findLibraryAncestor(parent.parent);
601
};
602
 
603
/**
604
 * getParentZebra
605
 *
606
 * Alternate the background color of fields
607
 *
608
 * @param parent
609
 * @returns {string} to determine background color of callee
610
 */
611
ns.getParentZebra = function (parent) {
612
  if (parent.zebra) {
613
    return parent.zebra;
614
  }
615
  else {
616
    return ns.getParentZebra(parent.parent);
617
  }
618
};
619
 
620
/**
621
 * Find the nearest ancestor which handles commonFields.
622
 *
623
 * @param {type} parent
624
 * @returns {@exp;ns@call;findAncestor|ns.findAncestor.parent}
625
 */
626
ns.findAncestor = function (parent) {
627
  if (parent.commonFields === undefined) {
628
    return ns.findAncestor(parent.parent);
629
  }
630
  return parent;
631
};
632
 
633
/**
634
 * Call remove on the given children.
635
 *
636
 * @param {Array} children
637
 * @returns {unresolved}
638
 */
639
ns.removeChildren = function (children) {
640
  if (children === undefined) {
641
    return;
642
  }
643
 
644
  for (var i = 0; i < children.length; i++) {
645
    // Common fields will be removed by library.
646
    var isCommonField = (children[i].field === undefined ||
647
                         children[i].field.common === undefined ||
648
                         !children[i].field.common);
649
 
650
    var hasRemove = (children[i].remove instanceof Function ||
651
                     typeof children[i].remove === 'function');
652
 
653
    if (isCommonField && hasRemove) {
654
      children[i].remove();
655
    }
656
  }
657
};
658
 
659
/**
660
 * Find field from path.
661
 *
662
 * @param {String} path
663
 * @param {Object} parent
664
 * @returns {@exp;ns.Form@call;findField|Boolean}
665
 */
666
ns.findField = function (path, parent) {
667
  if (typeof path === 'string') {
668
    path = path.split('/');
669
  }
670
 
671
  if (path[0] === '..') {
672
    path.splice(0, 1);
673
    return ns.findField(path, parent.parent);
674
  }
675
  if (parent.children) {
676
    for (var i = 0; i < parent.children.length; i++) {
677
      if (parent.children[i].field.name === path[0]) {
678
        path.splice(0, 1);
679
        if (path.length) {
680
          return ns.findField(path, parent.children[i]);
681
        }
682
        else {
683
          return parent.children[i];
684
        }
685
      }
686
    }
687
  }
688
 
689
  return false;
690
};
691
 
692
/**
693
 * Find a semantics field in the semantics structure by name of the field
694
 * Will return the first found by depth first search if there are identically named fields
695
 *
696
 * @param {string} fieldName Name of the field we wish to find
697
 * @param {Object|Array} semanticsStructure Semantics we wish to find the field within
698
 * @returns {null|Object} Returns the field if found, otherwise null.
699
 */
700
ns.findSemanticsField = function (fieldName, semanticsStructure) {
701
  if (Array.isArray(semanticsStructure)) {
702
    for (let i = 0; i < semanticsStructure.length; i++) {
703
      var semanticsField = ns.findSemanticsField(fieldName, semanticsStructure[i]);
704
      if (semanticsField !== null) {
705
        // Return immediately if field is found
706
        return semanticsField;
707
      }
708
    }
709
    return null;
710
  }
711
  else if (semanticsStructure.name === fieldName) {
712
    return semanticsStructure;
713
  }
714
  else if (semanticsStructure.field) {
715
    // Process field
716
    return ns.findSemanticsField(fieldName, semanticsStructure.field);
717
  }
718
  else if (semanticsStructure.fields) {
719
    // Process fields
720
    return ns.findSemanticsField(fieldName, semanticsStructure.fields);
721
  }
722
  else {
723
    // No matching semantics found within known properties and list structures
724
    return null;
725
  }
726
};
727
 
728
/**
729
 * Follow a field and get all changes to its params.
730
 *
731
 * @param {Object} parent The parent object of the field.
732
 * @param {String} path Relative to parent object.
733
 * @param {Function} callback Gets called for params changes.
734
 * @returns {undefined}
735
 */
736
ns.followField = function (parent, path, callback) {
737
  if (path === undefined) {
738
    return;
739
  }
740
 
741
  // Find field when tree is ready.
742
  parent.ready(function () {
743
    var def;
744
 
745
    if (path instanceof Object) {
746
      // We have an object with default values
747
      def = H5P.cloneObject(path);
748
 
749
      if (path.field === undefined) {
750
        callback(path, null);
751
        return; // Exit if we have no field to follow.
752
      }
753
 
754
      path = def.field;
755
      delete def.field;
756
    }
757
 
758
    var field = ns.findField(path, parent);
759
 
760
    if (!field) {
761
      throw ns.t('core', 'unknownFieldPath', {':path': path});
762
    }
763
    if (field.changes === undefined) {
764
      throw ns.t('core', 'noFollow', {':path': path});
765
    }
766
 
767
    var params = (field.params === undefined ? def : field.params);
768
    callback(params, field.changes.length + 1);
769
 
770
    field.changes.push(function () {
771
      var params = (field.params === undefined ? def : field.params);
772
      callback(params);
773
    });
774
  });
775
};
776
 
777
/**
778
 * Create HTML wrapper for error messages.
779
 *
780
 * @param {String} message
781
 * @returns {String}
782
 */
783
ns.createError = function (message) {
784
  return '<p>' + message + '</p>';
785
};
786
 
787
/**
788
 * Turn a numbered importance into a string.
789
 *
790
 * @param {string} importance
791
 * @returns {String}
792
 */
793
ns.createImportance = function (importance) {
794
  return importance ? 'importance-' + importance : '';
795
};
796
 
797
/**
798
 * Create HTML wrapper for field items.
799
 * Makes sure the different elements are placed in an consistent order.
800
 *
801
 * @param {string} type
802
 * @param {string} [label]
803
 * @param {string} [description]
804
 * @param {string} [content]
805
 * @deprecated since version 1.12 (Jan. 2017, will be removed Jan. 2018). Use createFieldMarkup instead.
806
 * @see createFieldMarkup
807
 * @returns {string} HTML
808
 */
809
ns.createItem = function (type, label, description, content) {
810
  return '<div class="field ' + type + '">' +
811
           (label ? label : '') +
812
           (description ? '<div class="h5peditor-field-description">' + description + '</div>' : '') +
813
           (content ? content : '') +
814
           '<div class="h5p-errors"></div>' +
815
         '</div>';
816
};
817
 
818
/**
819
 * An object describing the semantics of a field
820
 * @typedef {Object} SemanticField
821
 * @property {string} name
822
 * @property {string} type
823
 * @property {string} label
824
 * @property {string} [importance]
825
 * @property {string} [description]
826
 * @property {string} [widget]
827
 * @property {boolean} [optional]
828
 */
829
 
830
/**
831
 * Create HTML wrapper for a field item.
832
 * Replacement for createItem()
833
 *
834
 * @since 1.12
835
 * @param  {SemanticField} field
836
 * @param  {string} content
837
 * @param  {string} [inputId]
838
 * @return {string}
839
 */
840
ns.createFieldMarkup = function (field, content, inputId) {
841
  content = content || '';
842
  var markup = this.createLabel(field, '', inputId) + this.createDescription(field.description, inputId) + content;
843
 
844
  return this.wrapFieldMarkup(field, markup);
845
};
846
 
847
/**
848
 * Create HTML wrapper for a boolean field item.
849
 *
850
 * @param  {SemanticField} field
851
 * @param  {string} content
852
 * @param  {string} [inputId]
853
 *
854
 * @return {string}
855
 */
856
ns.createBooleanFieldMarkup = function (field, content, inputId) {
857
  var markup = '<label class="h5peditor-label">' +
858
    content + (field.label || field.name || '') + '</label>' +
859
    this.createDescription(field.description, inputId);
860
 
861
  return this.wrapFieldMarkup(field, markup);
862
};
863
 
864
/**
865
 * Wraps a field with some metadata classes, and adds error field
866
 *
867
 * @param {SemanticField} field
868
 * @param {string} markup
869
 *
870
 * @private
871
 * @return {string}
872
 */
873
ns.wrapFieldMarkup = function (field, markup) {
874
  // removes undefined and joins
875
  var wrapperClasses = this.joinNonEmptyStrings(['field', 'field-name-' + field.name, field.type, ns.createImportance(field.importance), field.widget]);
876
 
877
  // wrap and return
878
  return '<div class="' + wrapperClasses + '">' +
879
    markup +
880
    '<div class="h5p-errors"></div>' +
881
    '</div>';
882
};
883
 
884
/**
885
 * Joins an array of strings if they are defined and non empty
886
 *
887
 * @param {string[]} arr
888
 * @param {string} [separator] Default is space
889
 * @return {string}
890
 */
891
ns.joinNonEmptyStrings = function (arr, separator) {
892
  separator = separator || ' ';
893
 
894
  return arr.filter(function (str) {
895
    return str !== undefined && str.length > 0;
896
  }).join(separator);
897
};
898
 
899
/**
900
 * Create HTML for select options.
901
 *
902
 * @param {String} value
903
 * @param {String} text
904
 * @param {Boolean} selected
905
 * @returns {String}
906
 */
907
ns.createOption = function (value, text, selected) {
908
  return '<option value="' + value + '"' + (selected !== undefined && selected ? ' selected="selected"' : '') + '>' + text + '</option>';
909
};
910
 
911
/**
912
 * Create HTML for text input.
913
 *
914
 * @param {String} value
915
 * @param {number} maxLength
916
 * @param {String} placeholder
917
 * @param {number} [id]
918
 * @param {number} [describedby]
919
 * @returns {String}
920
 */
921
ns.createText = function (value, maxLength, placeholder, id, describedby) {
922
  var html = '<input class="h5peditor-text" type="text"';
923
 
924
  if (id !== undefined) {
925
    html += ' id="' + id + '"';
926
  }
927
 
928
  if (describedby !== undefined) {
929
    html += ' aria-describedby="' + describedby + '"';
930
  }
931
 
932
  if (value !== undefined) {
933
    html += ' value="' + value + '"';
934
  }
935
 
936
  if (placeholder !== undefined) {
937
    html += ' placeholder="' + placeholder + '"';
938
  }
939
 
940
  html += ' maxlength="' + (maxLength === undefined ? 255 : maxLength) + '"/>';
941
 
942
  return html;
943
};
944
 
945
ns.getNextFieldId = (function (counter) {
946
  /**
947
   * Generates a consistent and unique field ID for the given field.
948
   *
949
   * @param {Object} field
950
   * @return {number}
951
   */
952
  return function (field) {
953
    return 'field-' + field.name.toLowerCase() +  '-' + (counter++);
954
  };
955
})(-1);
956
 
957
/**
958
 * Helps generates a consistent description ID across fields.
959
 *
960
 * @param {string} id
961
 * @return {string}
962
 */
963
ns.getDescriptionId = function (id) {
964
  return id + '-description';
965
};
966
 
967
/**
968
 * Create a label to wrap content in.
969
 *
970
 * @param {SemanticField} field
971
 * @param {String} [content]
972
 * @param {String} [inputId]
973
 * @returns {String}
974
 */
975
ns.createLabel = function (field, content, inputId) {
976
  // New items can be added next to the label within the flex-wrapper
977
  var html = '<label class="h5peditor-label-wrapper"';
978
 
979
  if (inputId !== undefined) {
980
    html += ' for="' + inputId + '"';
981
  }
982
  html+= '>'
983
 
984
  // Temporary fix for the old version of CoursePresentation's custom editor
985
  if (field.widget === 'coursepresentation' && field.name === 'presentation') {
986
    field.label = 0;
987
  }
988
 
989
  if (field.label !== 0) {
990
    html += '<span class="h5peditor-label' + (field.optional ? '' : ' h5peditor-required') + '">' + (field.label === undefined ? field.name : field.label) + '</span>';
991
  }
992
 
993
  return html + (content || '') + '</label>';
994
};
995
 
996
/**
997
 * Create a description
998
 * @param {String} description
999
 * @param {number} [inputId] Used to reference description from input
1000
 * @returns {string}
1001
 */
1002
ns.createDescription = function (description, inputId) {
1003
  var html = '';
1004
  if (description !== undefined) {
1005
    html += '<div class="h5peditor-field-description"';
1006
    if (inputId !== undefined) {
1007
      html += ' id="' + ns.getDescriptionId(inputId) + '"';
1008
    }
1009
    html += '>' + description + '</div>';
1010
  }
1011
  return html;
1012
};
1013
 
1014
/**
1015
 * Create an important description
1016
 * @param {Object} importantDescription
1017
 * @returns {String}
1018
 */
1019
ns.createImportantDescription = function (importantDescription) {
1020
  var html = '';
1021
 
1022
  if (importantDescription !== undefined) {
1023
    html += '<div class="h5peditor-field-important-description">' +
1024
              '<div class="important-description-tail">' +
1025
              '</div>' +
1026
              '<div class="important-description-close" role="button" tabindex="0" aria-label="' + ns.t('core', 'hideImportantInstructions') + '">' +
1027
                '<span>' +
1028
                   ns.t('core', 'hide') +
1029
                '</span>' +
1030
              '</div>' +
1031
              '<span class="h5p-info-icon">' +
1032
              '</span>' +
1033
              '<span class="important-description-title">' +
1034
                 ns.t('core', 'importantInstructions') +
1035
              '</span>';
1036
 
1037
    if (importantDescription.description !== undefined) {
1038
      html += '<div class="important-description-content">' +
1039
                 importantDescription.description +
1040
              '</div>';
1041
    }
1042
 
1043
    if (importantDescription.example !== undefined) {
1044
      html += '<div class="important-description-example">' +
1045
                '<div class="important-description-example-title">' +
1046
                  '<span>' +
1047
                     ns.t('core', 'example') +
1048
                  ':</span>' +
1049
                '</div>' +
1050
                '<div class="important-description-example-text">' +
1051
                  '<span>' +
1052
                     importantDescription.example +
1053
                  '</span>' +
1054
                '</div>' +
1055
              '</div>';
1056
    }
1057
 
1058
    html += '</div>' +
1059
            '<span class="important-description-show" role="button" tabindex="0">' +
1060
              ns.t('core', 'showImportantInstructions') +
1061
            '</span><span class="important-description-clear-right"></span>';
1062
  }
1063
 
1064
  return html;
1065
};
1066
 
1067
/**
1068
 * Bind events to important description
1069
 * @param {Object} widget
1070
 * @param {String} fieldName
1071
 * @param {Object} parent
1072
 */
1073
ns.bindImportantDescriptionEvents = function (widget, fieldName, parent) {
1074
  var context;
1075
 
1076
  if (!widget.field.important) {
1077
    return;
1078
  }
1079
 
1080
  // Generate a context string for using as referance in ex. localStorage.
1081
  var librarySelector = ns.findLibraryAncestor(parent);
1082
  if (librarySelector.currentLibrary !== undefined) {
1083
    var lib = librarySelector.currentLibrary.split(' ')[0];
1084
    context = (lib + '-' + fieldName).replace(/\.|_/g,'-') + '-important-description-open';
1085
  }
1086
 
1087
  // Set first occurance to visible
1088
  ns.storage.get(context, function (value) {
1089
    if (value === undefined || value === true) {
1090
      widget.$item.addClass('important-description-visible');
1091
    }
1092
  });
1093
 
1094
  widget.$item.addClass('has-important-description');
1095
 
1096
  // Bind events to toggle button and update aria-pressed
1097
  widget.$item.find('.important-description-show')
1098
    .click(function () {
1099
      widget.$item.addClass('important-description-visible');
1100
      ns.storage.set(context, true);
1101
    })
1102
    .keydown(function (event) {
1103
      if (event.which == 13 || event.which == 32) {
1104
        ns.$(this).trigger('click');
1105
        event.preventDefault();
1106
      }
1107
    });
1108
 
1109
  // Bind events to close button and update aria-pressed of toggle button
1110
  widget.$item.find('.important-description-close')
1111
    .click(function () {
1112
      widget.$item.removeClass('important-description-visible');
1113
      ns.storage.set(context, false);
1114
    })
1115
    .keydown(function (event) {
1116
      if (event.which == 13 || event.which == 32) {
1117
        ns.$(this).trigger('click');
1118
        event.preventDefault();
1119
      }
1120
    });
1121
};
1122
 
1123
/**
1124
 * Generate markup for the copy and paste buttons.
1125
 *
1126
 * @returns {string} HTML
1127
 */
1128
ns.createCopyPasteButtons = function () {
1129
  return '<div class="h5peditor-copypaste-wrap">' +
1130
           '<button class="h5peditor-copy-button disabled" title="' + H5PEditor.t('core', 'copyToClipboard') + '" disabled>' + ns.t('core', 'copyButton') + '</button>' +
1131
           '<button class="h5peditor-paste-button disabled" title="' + H5PEditor.t('core', 'pasteFromClipboard') + '" disabled>' + ns.t('core', 'pasteButton') + '</button>' +
1132
         '</div><div class="h5peditor-clearfix"></div>';
1133
};
1134
 
1135
/**
1136
 * Confirm replace if there is content selected
1137
 *
1138
 * @param {string} library Current selected library
1139
 * @param {number} top Offset
1140
 * @param {function} next Next callback
1141
 */
1142
ns.confirmReplace = function (library, top, next) {
1143
  if (library) {
1144
    // Confirm changing library
1145
    var confirmReplace = new H5P.ConfirmationDialog({
1146
      headerText: H5PEditor.t('core', 'pasteContent'),
1147
      dialogText: H5PEditor.t('core', 'confirmPasteContent'),
1148
      confirmText: H5PEditor.t('core', 'confirmPasteButtonText')
1149
    }).appendTo(document.body);
1150
    confirmReplace.on('confirmed', next);
1151
    confirmReplace.show(top);
1152
  }
1153
  else {
1154
    // No need to confirm
1155
    next();
1156
  }
1157
};
1158
 
1159
/**
1160
 * Check if any errors has been set.
1161
 *
1162
 * @param {jQuery} $errors
1163
 * @param {jQuery} $input
1164
 * @param {String} value
1165
 * @returns {mixed}
1166
 */
1167
ns.checkErrors = function ($errors, $input, value) {
1168
  if ($errors.children().length) {
1169
    $input.keyup(function (event) {
1170
      if (event.keyCode === 9) { // TAB
1171
        return;
1172
      }
1173
      $errors.html('');
1174
      $input.removeClass('error');
1175
      $input.unbind('keyup');
1176
    });
1177
 
1178
    return false;
1179
  }
1180
  return value;
1181
};
1182
 
1183
/**
1184
 * @param {object} library
1185
 *  with machineName, majorVersion and minorVersion params
1186
 * @returns {string}
1187
 *  Concatinated version of the library
1188
 */
1189
ns.libraryToString = function (library) {
1190
  return library.name + ' ' + library.majorVersion + '.' + library.minorVersion;
1191
};
1192
 
1193
/**
1194
 * TODO: Remove from here, and use from H5P instead(move this to the h5p.js...)
1195
 *
1196
 * @param {string} library
1197
 *  library in the format machineName majorVersion.minorVersion
1198
 * @returns
1199
 *  library as an object with machineName, majorVersion and minorVersion properties
1200
 *  return false if the library parameter is invalid
1201
 */
1202
ns.libraryFromString = function (library) {
1203
  var regExp = /(.+)\s(\d+)\.(\d+)$/g;
1204
  var res = regExp.exec(library);
1205
  if (res !== null) {
1206
    return {
1207
      'machineName': res[1],
1208
      'majorVersion': res[2],
1209
      'minorVersion': res[3]
1210
    };
1211
  }
1212
  else {
1213
    H5P.error('Invalid überName');
1214
    return false;
1215
  }
1216
};
1217
 
1218
/**
1219
 * Helper function for detecting field widget.
1220
 *
1221
 * @param {Object} field
1222
 * @returns {String} Widget name
1223
 */
1224
ns.getWidgetName = function (field) {
1225
  return (field.widget === undefined ? field.type : field.widget);
1226
};
1227
 
1228
/**
1229
 * Mimics how php's htmlspecialchars works (the way we uses it)
1230
 */
1231
ns.htmlspecialchars = function (string) {
1232
  return string.toString().replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/'/g, '&#039;').replace(/"/g, '&quot;');
1233
};
1234
 
1235
/**
1236
 * Makes it easier to add consistent buttons across the editor widget.
1237
 *
1238
 * @param {string} id Typical CSS class format
1239
 * @param {string} title Human readable format
1240
 * @param {function} handler Action handler when triggered
1241
 * @param {boolean} [displayTitle=false] Show button with text
1242
 * @return {H5P.jQuery}
1243
 */
1244
ns.createButton = function (id, title, handler, displayTitle) {
1245
  var options = {
1246
    class: 'h5peditor-button ' + (displayTitle ? 'h5peditor-button-textual ' : '') + id,
1247
    role: 'button',
1248
    tabIndex: 0,
1249
    'aria-disabled': 'false',
1250
    on: {
1251
      click: function () {
1252
        handler.call(this);
1253
      },
1254
      keydown: function (event) {
1255
        switch (event.which) {
1256
          case 13: // Enter
1257
          case 32: // Space
1258
            handler.call(this);
1259
            event.preventDefault();
1260
        }
1261
      }
1262
    }
1263
  };
1264
 
1265
  // Determine if we're a icon only button or have a textual label
1266
  options[displayTitle ? 'html' : 'aria-label'] = title;
1267
 
1268
  return ns.$('<div/>', options);
1269
};
1270
 
1271
/**
1272
 * Check if the current library is entitled for the metadata button. True by default.
1273
 *
1274
 * It will probably be okay to remove this check at some point in time when
1275
 * the majority of content types and plugins have been updated to a version
1276
 * that supports the metadata system.
1277
 *
1278
 * @param {string} library - Current library.
1279
 * @return {boolean} True, if form should have the metadata button.
1280
 */
1281
ns.enableMetadata = function (library) {
1282
 
1283
  if (!library || typeof library !== 'string') {
1284
    return false;
1285
  }
1286
 
1287
  library = H5P.libraryFromString(library);
1288
  if (!library) {
1289
    return false;
1290
  }
1291
 
1292
  // This list holds all old libraries (/older versions implicitly) that need an update for metadata
1293
  const blackList = [
1294
    // Should never have metadata because it does not make sense
1295
    'H5P.IVHotspot 1.2',
1296
    'H5P.Link 1.3',
1297
    'H5P.TwitterUserFeed 1.0',
1298
    'H5P.GoToQuestion 1.3',
1299
    'H5P.Nil 1.0',
1300
 
1301
    // Copyright information moved to metadata
1302
    'H5P.Audio 1.2',
1303
    'H5P.Video 1.4',
1304
    'H5P.Image 1.0',
1305
 
1306
    // Title moved to metadata
1307
    'H5P.DocumentExportPage 1.3',
1308
    'H5P.ExportableTextArea 1.2',
1309
    'H5P.GoalsAssessmentPage 1.3',
1310
    'H5P.GoalsPage 1.4',
1311
    'H5P.StandardPage 1.3',
1312
    'H5P.DragQuestion 1.12',
1313
    'H5P.ImageHotspotQuestion 1.7',
1314
 
1315
    // Custom editor changed
1316
    'H5P.CoursePresentation 1.19',
1317
    'H5P.InteractiveVideo 1.19'
1318
  ];
1319
 
1320
  let block = blackList.filter(function (item) {
1321
    // + ' ' makes sure to avoid partial matches
1322
    return item.indexOf(library.machineName + ' ') !== -1;
1323
  });
1324
  if (block.length === 0) {
1325
    return true;
1326
  }
1327
 
1328
  block = H5P.libraryFromString(block[0]);
1329
  if (library.majorVersion > block.majorVersion || library.majorVersion === block.majorVersion && library.minorVersion > block.minorVersion) {
1330
    return true;
1331
  }
1332
 
1333
  return false;
1334
};
1335
 
1336
// Backwards compatibilty
1337
ns.attachToastTo = H5P.attachToastTo;
1338
 
1339
/**
1340
 * Check if clipboard can be pasted.
1341
 *
1342
 * @param {Object} clipboard Clipboard data.
1343
 * @param {Object} libs Libraries to compare against.
1344
 * @return {boolean} True, if content can be pasted.
1345
 */
1346
ns.canPaste = function (clipboard, libs) {
1347
  return (this.canPastePlus(clipboard, libs)).canPaste;
1348
};
1349
 
1350
/**
1351
 * Check if clipboard can be pasted and give reason if not.
1352
 *
1353
 * @param {Object} clipboard Clipboard data.
1354
 * @param {Object} libs Libraries to compare against.
1355
 * @return {Object} Results. {canPaste: boolean, reason: string, description: string}.
1356
 */
1357
ns.canPastePlus = function (clipboard, libs) {
1358
  // Clipboard is empty
1359
  if (!clipboard || !clipboard.generic) {
1360
    return {
1361
      canPaste: false,
1362
      reason: 'pasteNoContent',
1363
      description: ns.t('core', 'pasteNoContent')
1364
    };
1365
  }
1366
 
1367
  // No libraries to compare to
1368
  if (libs === undefined) {
1369
    return {
1370
      canPaste: false,
1371
      reason: 'pasteError',
1372
      description: ns.t('core', 'pasteError')
1373
    };
1374
  }
1375
 
1376
  // Translate Hub format to common library format
1377
  if (libs.libraries !== undefined) {
1378
    libs = libs.libraries;
1379
    libs.forEach(function (lib) {
1380
      lib.name = lib.machineName;
1381
      lib.majorVersion = lib.localMajorVersion;
1382
      lib.minorVersion = lib.localMinorVersion;
1383
    });
1384
  }
1385
 
1386
  // Check if clipboard library type is available
1387
  const machineNameClip = clipboard.generic.library.split(' ')[0];
1388
  let candidates = libs.filter(function (library) {
1389
    return library.name === machineNameClip;
1390
  });
1391
  if (candidates.length === 0) {
1392
    return {
1393
      canPaste: false,
1394
      reason: 'pasteContentNotSupported',
1395
      description: ns.t('core', 'pasteContentNotSupported')
1396
    };
1397
  }
1398
 
1399
  // Check if clipboard library version is available
1400
  const versionClip = clipboard.generic.library.split(' ')[1];
1401
  for (let i = 0; i < candidates.length; i++) {
1402
    if (candidates[i].majorVersion + '.' + candidates[i].minorVersion === versionClip) {
1403
      if (candidates[i].restricted !== true) {
1404
        return {
1405
          canPaste: true
1406
        };
1407
      }
1408
      else {
1409
        return {
1410
          canPaste: false,
1411
          reason: 'pasteContentRestricted',
1412
          description: ns.t('core', 'pasteContentRestricted')
1413
        };
1414
      }
1415
    }
1416
  }
1417
 
1418
  // Sort remaining candidates by version number
1419
  candidates = candidates
1420
    .map(function (candidate) {
1421
      return '' + candidate.majorVersion + '.' + candidate.minorVersion;
1422
    })
1423
    .map(function (candidate) {
1424
      return candidate.replace(/\d+/g, function (d) {
1425
        return +d + 1000;
1426
      });
1427
    })
1428
    .sort()
1429
    .map(function (candidate) {
1430
      return candidate.replace(/\d+/g, function (d) {
1431
        return +d - 1000;
1432
      });
1433
    });
1434
 
1435
  // Clipboard library is newer than latest available local library
1436
  const candidateMax = candidates.slice(-1)[0];
1437
  if (+candidateMax.split('.')[0] < +versionClip.split('.')[0] ||
1438
      (+candidateMax.split('.')[0] === +versionClip.split('.')[0] &&
1439
      +candidateMax.split('.')[1] < +versionClip.split('.')[1])) {
1440
    return {
1441
      canPaste: false,
1442
      reason: 'pasteTooNew',
1443
      description: ns.t('core', 'pasteTooNew', {
1444
        ':clip': versionClip,
1445
        ':local': candidateMax
1446
      })
1447
    };
1448
  }
1449
 
1450
  // Clipboard library is older than latest available local library
1451
  const candidateMin = candidates.slice(0, 1)[0];
1452
  if (+candidateMin.split('.')[0] > +versionClip.split('.')[0] ||
1453
      (+candidateMin.split('.')[0] === +versionClip.split('.')[0] &&
1454
       +candidateMin.split('.')[1] > +versionClip.split('.')[1])) {
1455
    return {
1456
      canPaste: false,
1457
      reason: 'pasteTooOld',
1458
      description: ns.t('core', 'pasteTooOld', {
1459
        ':clip': versionClip,
1460
        ':local': candidateMin
1461
      })
1462
    };
1463
  }
1464
 
1465
  return {
1466
    canPaste: false,
1467
    reason: 'pasteError',
1468
    description: ns.t('core', 'pasteError')
1469
  };
1470
};
1471
 
1472
// Factory for creating storage instance
1473
ns.storage = (function () {
1474
  var instance = {
1475
    get: function (key, next) {
1476
      var value;
1477
 
1478
      // Get value from browser storage
1479
      if (window.localStorage !== undefined) {
1480
        value = !!window.localStorage.getItem(key);
1481
      }
1482
 
1483
      // Try to get a better value from user data storage
1484
      try {
1485
        H5P.getUserData(0, key, function (err, result) {
1486
          if (!err) {
1487
            value = result;
1488
          }
1489
          next(value);
1490
        });
1491
      }
1492
      catch (err) {
1493
        next(value);
1494
      }
1495
    },
1496
    set: function (key, value) {
1497
 
1498
      // Store in browser
1499
      if (window.localStorage !== undefined) {
1500
        window.localStorage.setItem(key, value);
1501
      }
1502
 
1503
      // Try to store in user data storage
1504
      try {
1505
        H5P.setUserData(0, key, value);
1506
      }
1507
      catch (err) { /*Intentionally left empty*/ }
1508
    }
1509
  };
1510
  return instance;
1511
})();
1512
 
1513
/**
1514
 * Small helper class for library data.
1515
 *
1516
 * @class
1517
 * @param {string} nameVersionString
1518
 */
1519
ns.ContentType = function ContentType(nameVersionString) {
1520
  const libraryNameSplit = nameVersionString.split(' ');
1521
  const libraryVersionSplit = libraryNameSplit[1].split('.');
1522
 
1523
  this.machineName = libraryNameSplit[0];
1524
  this.majorVersion = libraryVersionSplit[0];
1525
  this.minorVersion = libraryVersionSplit[1];
1526
};
1527
 
1528
/**
1529
 * Look for the best possible upgrade for the given library
1530
 *
1531
 * @param {ns.ContentType} library
1532
 * @param {Array} libraries Where to look
1533
 */
1534
ns.ContentType.getPossibleUpgrade = function (library, libraries) {
1535
  let possibleUpgrade;
1536
 
1537
  for (let i = 0; i < libraries.length; i++) {
1538
    const candiate = libraries[i];
1539
    if (candiate.installed !== false && ns.ContentType.hasSameName(candiate, library) && ns.ContentType.isHigherVersion(candiate, library)) {
1540
 
1541
      // Check if the upgrade is better than the previous upgrade we found
1542
      if (!possibleUpgrade || ns.ContentType.isHigherVersion(candiate, possibleUpgrade)) {
1543
        possibleUpgrade = candiate;
1544
      }
1545
    }
1546
  }
1547
 
1548
  return possibleUpgrade;
1549
};
1550
 
1551
/**
1552
 * Check if candiate is a higher version than original.
1553
 *
1554
 * @param {Object} candiate Library object
1555
 * @param {Object} original Library object
1556
 * @returns {boolean}
1557
 */
1558
ns.ContentType.isHigherVersion = function (candiate, original) {
1559
  return (ns.ContentType.getMajorVersion(candiate) > ns.ContentType.getMajorVersion(original) ||
1560
    (ns.ContentType.getMajorVersion(candiate) == ns.ContentType.getMajorVersion(original) &&
1561
     ns.ContentType.getMinorVersion(candiate) > ns.ContentType.getMinorVersion(original)));
1562
};
1563
 
1564
/**
1565
 * Check if candiate has same name as original.
1566
 *
1567
 * @param {Object} candiate Library object
1568
 * @param {Object} original Library object
1569
 * @returns {boolean}
1570
 */
1571
ns.ContentType.hasSameName = function (candiate, original) {
1572
  return (ns.ContentType.getName(candiate) === ns.ContentType.getName(original));
1573
};
1574
 
1575
/**
1576
 * Check if candiate has same name as original.
1577
 *
1578
 * @param {Object} candiate Library object
1579
 * @param {Object} original Library object
1580
 * @returns {string}
1581
 */
1582
ns.ContentType.getNameVersionString = function (library) {
1583
  return ns.ContentType.getName(library) + ' ' + ns.ContentType.getMajorVersion(library) + '.' + ns.ContentType.getMinorVersion(library);
1584
};
1585
 
1586
/**
1587
 * Get the major version from a library object.
1588
 *
1589
 * @param {Object} library
1590
 * @returns {number}
1591
 */
1592
ns.ContentType.getMajorVersion = function (library) {
1593
  return parseInt((library.localMajorVersion !== undefined ? library.localMajorVersion : library.majorVersion));
1594
};
1595
 
1596
/**
1597
 * Get the minor version from a library object.
1598
 *
1599
 * @param {Object} library
1600
 * @returns {number}
1601
 */
1602
ns.ContentType.getMinorVersion = function (library) {
1603
  return parseInt((library.localMinorVersion !== undefined ? library.localMinorVersion : library.minorVersion));
1604
};
1605
 
1606
/**
1607
 * Get the name from a library object.
1608
 *
1609
 * @param {Object} library
1610
 * @returns {string}
1611
 */
1612
ns.ContentType.getName = function (library) {
1613
  return (library.machineName !== undefined ? library.machineName : library.name);
1614
};
1615
 
1616
 
1617
ns.upgradeContent = (function () {
1618
 
1619
  /**
1620
   * A wrapper for loading library data for the content upgrade scripts.
1621
   *
1622
   * @param {string} name Library name
1623
   * @param {H5P.Version} version
1624
   * @param {Function} next Callback
1625
   */
1626
  const loadLibrary = function (name, version, next) {
1627
    const library = name + ' ' + version.major + '.' + version.minor;
1628
    ns.loadLibrary(library, function () {
1629
      next(null, ns.libraryCache[library]);
1630
    });
1631
  };
1632
 
1633
  return function contentUpgrade(fromLibrary, toLibrary, parameters, done) {
1634
    ns.loadJs(H5PIntegration.libraryUrl + '/h5p-version.js' + H5PIntegration.pluginCacheBuster, function (err) {
1635
      ns.loadJs(H5PIntegration.libraryUrl + '/h5p-content-upgrade-process.js' + H5PIntegration.pluginCacheBuster, function (err) {
1636
        // TODO: Avoid stringify the parameters
1637
        new H5P.ContentUpgradeProcess(ns.ContentType.getName(fromLibrary), new H5P.Version(fromLibrary), new H5P.Version(toLibrary), JSON.stringify(parameters), 1, function (name, version, next) {
1638
          loadLibrary(name, version, function (err, library) {
1639
            if (library.upgradesScript) {
1640
              ns.loadJs(library.upgradesScript, function (err) {
1641
                if (err) {
1642
                  err = 'Error loading upgrades ' + name + ' ' + version;
1643
                }
1644
                next(err, library);
1645
              });
1646
            }
1647
            else {
1648
              next(null, library);
1649
            }
1650
          });
1651
 
1652
        }, function (err, result) {
1653
          if (err) {
1654
            let header = 'Failed';
1655
            let message = 'Could not upgrade content';
1656
            switch (err.type) {
1657
              case 'errorTooHighVersion':
1658
                message += ': ' + ns.t('core', 'errorTooHighVersion', {'%used': err.used, '%supported': err.supported});
1659
                break;
1660
 
1661
              case 'errorNotSupported':
1662
                message += ': ' + ns.t('core', 'errorNotSupported', {'%used': err.used});
1663
                break;
1664
 
1665
              case 'errorParamsBroken':
1666
                message += ': ' + ns.t('core', 'errorParamsBroken');
1667
                break;
1668
 
1669
              case 'libraryMissing':
1670
                message += ': ' +  ns.t('core', 'libraryMissing', {'%lib': err.library});
1671
                break;
1672
 
1673
              case 'scriptMissing':
1674
                message += ': ' + ns.t('core', 'scriptMissing', {'%lib': err.library});
1675
                break;
1676
            }
1677
 
1678
            var confirmErrorDialog = new H5P.ConfirmationDialog({
1679
              headerText: header,
1680
              dialogText: message,
1681
              confirmText: 'Continue'
1682
            }).appendTo(document.body);
1683
            confirmErrorDialog.show();
1684
          }
1685
          done(err, result);
1686
        });
1687
      });
1688
    });
1689
  };
1690
})();
1691
 
1692
// List of language code mappings used by the editor
1693
ns.supportedLanguages = {
1694
  'aa': 'Afar',
1695
  'ab': 'Abkhazian (аҧсуа бызшәа)',
1696
  'ae': 'Avestan',
1697
  'af': 'Afrikaans',
1698
  'ak': 'Akan',
1699
  'am': 'Amharic (አማርኛ)',
1700
  'ar': 'Arabic (العربية)',
1701
  'as': 'Assamese',
1702
  'ast': 'Asturian',
1703
  'av': 'Avar',
1704
  'ay': 'Aymara',
1705
  'az': 'Azerbaijani (azərbaycan)',
1706
  'ba': 'Bashkir',
1707
  'be': 'Belarusian (Беларуская)',
1708
  'bg': 'Bulgarian (Български)',
1709
  'bh': 'Bihari',
1710
  'bi': 'Bislama',
1711
  'bm': 'Bambara (Bamanankan)',
1712
  'bn': 'Bengali',
1713
  'bo': 'Tibetan',
1714
  'br': 'Breton',
1715
  'bs': 'Bosnian (Bosanski)',
1716
  'ca': 'Catalan (Català)',
1717
  'ce': 'Chechen',
1718
  'ch': 'Chamorro',
1719
  'co': 'Corsican',
1720
  'cr': 'Cree',
1721
  'cs': 'Czech (Čeština)',
1722
  'cu': 'Old Slavonic',
1723
  'cv': 'Chuvash',
1724
  'cy': 'Welsh (Cymraeg)',
1725
  'da': 'Danish (Dansk)',
1726
  'de': 'German (Deutsch)',
1727
  'dv': 'Maldivian',
1728
  'dz': 'Bhutani',
1729
  'ee': 'Ewe (Ɛʋɛ)',
1730
  'el': 'Greek (Ελληνικά)',
1731
  'en': 'English',
1732
  'en-gb': 'English, British',
1733
  'eo': 'Esperanto',
1734
  'es': 'Spanish (Español)',
1735
  'es-mx': 'Spanish, Mexican',
1736
  'et': 'Estonian (Eesti)',
1737
  'eu': 'Basque (Euskera)',
1738
  'fa': 'Persian (فارسی)',
1739
  'ff': 'Fulah (Fulfulde)',
1740
  'fi': 'Finnish (Suomi)',
1741
  'fil': 'Filipino',
1742
  'fj': 'Fiji',
1743
  'fo': 'Faeroese',
1744
  'fr': 'French (Français)',
1745
  'fy': 'Frisian (Frysk)',
1746
  'ga': 'Irish (Gaeilge)',
1747
  'gd': 'Scots Gaelic',
1748
  'gl': 'Galician (Galego)',
1749
  'gn': 'Guarani',
1750
  'gsw-berne': 'Swiss German',
1751
  'gu': 'Gujarati',
1752
  'gv': 'Manx',
1753
  'ha': 'Hausa',
1754
  'he': 'Hebrew (עברית)',
1755
  'hi': 'Hindi (हिन्दी)',
1756
  'ho': 'Hiri Motu',
1757
  'hr': 'Croatian (Hrvatski)',
1758
  'hsb': 'Upper Sorbian (hornjoserbšćina)',
1759
  'ht': 'Haitian Creole',
1760
  'hu': 'Hungarian (Magyar)',
1761
  'hy': 'Armenian (Õ€Õ¡ÕµÕ¥Ö€Õ¥Õ¶)',
1762
  'hz': 'Herero',
1763
  'ia': 'Interlingua',
1764
  'id': 'Indonesian (Bahasa Indonesia)',
1765
  'ie': 'Interlingue',
1766
  'ig': 'Igbo',
1767
  'ik': 'Inupiak',
1768
  'is': 'Icelandic (Íslenska)',
1769
  'it': 'Italian (Italiano)',
1770
  'iu': 'Inuktitut',
1771
  'ja': 'Japanese (日本語)',
1772
  'jv': 'Javanese',
1773
  'ka': 'Georgian',
1774
  'kg': 'Kongo',
1775
  'ki': 'Kikuyu',
1776
  'kj': 'Kwanyama',
1777
  'kk': 'Kazakh (Қазақ)',
1778
  'kl': 'Greenlandic',
1779
  'km': 'Cambodian',
1780
  'kn': 'Kannada (ಕನ್ನಡ)',
1781
  'ko': 'Korean (한국어)',
1782
  'kr': 'Kanuri',
1783
  'ks': 'Kashmiri',
1784
  'ku': 'Kurdish (Kurdî)',
1785
  'kv': 'Komi',
1786
  'kw': 'Cornish',
1787
  'ky': 'Kyrgyz (Кыргызча)',
1788
  'la': 'Latin (Latina)',
1789
  'lb': 'Luxembourgish',
1790
  'lg': 'Luganda',
1791
  'ln': 'Lingala',
1792
  'lo': 'Laothian',
1793
  'lt': 'Lithuanian (Lietuvių)',
1794
  'lv': 'Latvian (Latviešu)',
1795
  'mg': 'Malagasy',
1796
  'mh': 'Marshallese',
1797
  'mi': 'Māori',
1798
  'mk': 'Macedonian (Македонски)',
1799
  'ml': 'Malayalam (മലയാളം)',
1800
  'mn': 'Mongolian',
1801
  'mo': 'Moldavian',
1802
  'mr': 'Marathi',
1803
  'ms': 'Malay (Bahasa Melayu)',
1804
  'mt': 'Maltese (Malti)',
1805
  'my': 'Burmese',
1806
  'na': 'Nauru',
1807
  'nd': 'North Ndebele',
1808
  'ne': 'Nepali',
1809
  'ng': 'Ndonga',
1810
  'nl': 'Dutch (Nederlands)',
1811
  'nb': 'Norwegian Bokmål (Bokmål)',
1812
  'nn': 'Norwegian Nynorsk (Nynorsk)',
1813
  'nr': 'South Ndebele',
1814
  'nv': 'Navajo',
1815
  'ny': 'Chichewa',
1816
  'oc': 'Occitan',
1817
  'om': 'Oromo',
1818
  'or': 'Oriya',
1819
  'os': 'Ossetian',
1820
  'pa': 'Punjabi',
1821
  'pi': 'Pali',
1822
  'pl': 'Polish (Polski)',
1823
  'ps': 'Pashto (پښتو)',
1824
  'pt': 'Portuguese, International',
1825
  'pt-pt': 'Portuguese, Portugal (Português)',
1826
  'pt-br': 'Portuguese, Brazil (Português)',
1827
  'qu': 'Quechua',
1828
  'rm': 'Rhaeto-Romance',
1829
  'rn': 'Kirundi',
1830
  'ro': 'Romanian (Română)',
1831
  'ru': 'Russian (Русский)',
1832
  'rw': 'Kinyarwanda',
1833
  'sa': 'Sanskrit',
1834
  'sc': 'Sardinian',
1835
  'sco': 'Scots',
1836
  'sd': 'Sindhi',
1837
  'se': 'Northern Sami',
1838
  'sg': 'Sango',
1839
  'sh': 'Serbo-Croatian',
1840
  'si': 'Sinhala (සිංහල)',
1841
  'sk': 'Slovak (Slovenčina)',
1842
  'sl': 'Slovenian (Slovenščina)',
1843
  'sm': 'Samoan',
1844
  'sma': 'Sámi (Southern)',
1845
  'sme': 'Sámi (Northern)',
1846
  'smj': 'Sámi (Lule)',
1847
  'sn': 'Shona',
1848
  'so': 'Somali',
1849
  'sq': 'Albanian (Shqip)',
1850
  'sr': 'Serbian (Српски)',
1851
  'ss': 'Siswati',
1852
  'st': 'Sesotho',
1853
  'su': 'Sudanese',
1854
  'sv': 'Swedish (Svenska)',
1855
  'sw': 'Swahili (Kiswahili)',
1856
  'ta': 'Tamil (தமிழ்)',
1857
  'te': 'Telugu (తెలుగు)',
1858
  'tg': 'Tajik',
1859
  'th': 'Thai (ภาษาไทย)',
1860
  'ti': 'Tigrinya',
1861
  'tk': 'Turkmen',
1862
  'tl': 'Tagalog',
1863
  'tn': 'Setswana',
1864
  'to': 'Tonga',
1865
  'tr': 'Turkish (Türkçe)',
1866
  'ts': 'Tsonga',
1867
  'tt': 'Tatar (Tatarça)',
1868
  'tw': 'Twi',
1869
  'ty': 'Tahitian',
1870
  'ug': 'Uyghur',
1871
  'uk': 'Ukrainian (Українська)',
1872
  'ur': 'Urdu (اردو)',
1873
  'uz': "Uzbek (o'zbek)",
1874
  've': 'Venda',
1875
  'vi': 'Vietnamese (Tiếng Việt)',
1876
  'wo': 'Wolof',
1877
  'xh': 'Xhosa (isiXhosa)',
1878
  'xx-lolspeak': 'Lolspeak)',
1879
  'yi': 'Yiddish',
1880
  'yo': 'Yoruba (Yorùbá)',
1881
  'za': 'Zhuang',
1882
  'zh': 'Chinese',
1883
  'zh-hans': 'Chinese, Simplified (简体中文)',
1884
  'zh-hant': 'Chinese, Traditional (繁體中文)',
1885
  'zh-tw': 'Chinese, Taiwan, Traditional',
1886
  'zu': 'Zulu (isiZulu)'
1887
};