Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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