Source: ccm.er_rel_trainer.js

"use strict";

/**
 * @overview <i>ccmjs</i>-based web component for ER-REL Trainer.
 * @author André Kless <andre.kless@h-brs.de> 2022
 * @copyright EILD.nrw 2022
 * @license The MIT License (MIT)
 */

( () => {

  /**
   * <i>ccmjs</i>-based web component for ER-REL Trainer.
   * @namespace WebComponent
   * @type {object}
   * @property {string} name - Unique identifier of the component.
   * @property {number[]} [version] - Version of the component according to Semantic Versioning 2.0 (default: latest version).
   * @property {string} ccm - URL of the (interchangeable) ccmjs version used at the time of publication.
   * @property {app_config} config - Default app configuration.
   * @property {Class} Instance - Class from which app instances are created.
   */
  const component = {
    name: 'er_rel_trainer',
    ccm: './libs/ccm/ccm.js',
    config: {
//    "anytime_finish": true,
//    "auto_arrows": true,
//    "auto_pk": true,
      "comments": {
        "input": true,
        "wrong": true,
        "correct": true
      },
      "correction": true,
      "css": [ "ccm.load",
        [  // is loaded serially (not in parallel)
          "./libs/bootstrap-5/css/bootstrap.css",
          "./resources/styles.css",
        ],
        { "url": "./libs/bootstrap-5/css/bootstrap-fonts.css", "context": "head" }
      ],
//    "data": { "store": [ "ccm.store" ] },
      "default": {
        "format": "svg",
        "images": [ "e", "1", "c", "n", "cn", "r", "s" ],
        "notation": "abrial",
        "path": "./resources/img/"
      },
      "feedback": true,
//    "fixed_notation": true,
      "helper": [ "ccm.load", { "url": "./libs/ccm/helper.js", "type": "module" } ],
//    "hide_own_fk": true,
      "html": [ "ccm.load", { "url": "./resources/templates.js", "type": "module" } ],
      "lang": [ "ccm.start", "./libs/lang/ccm.lang.js", {
        "translations": {
          "de": [ "ccm.load", { "url": "./configs.js#de", "type": "module" } ],
          "en": [ "ccm.load", { "url": "./configs.js#en", "type": "module" } ]
        }
      } ],
      "legend": true,
      "license": true,
      "logos": "./resources/img/logos/logos.jpg",
      "modal": {
        "attr": [ "ccm.start", "./libs/modal/ccm.modal.js", {
          "backdrop_close": true,
          "breakpoints": false,
          "buttons": [],
          "closed": true,
          "content": "",
          "css": [ "ccm.load",
            [  // serial
              "./libs/bootstrap-5/css/bootstrap.css",
              "./resources/modal.css"
            ],
            { "url": "./libs/bootstrap-5/css/bootstrap-fonts.css", "context": "head" }
          ]
        } ],
        "legend": [ "ccm.start", "./libs/modal/ccm.modal.js", {
          "backdrop_close": true,
          "buttons": "",
          "closed": true,
          "content": ""
        } ]
      },
      "notations": [ "ccm.load", { "url": "./configs.js#notations", "type": "module" } ],
//    "number": 5,
//    "onchange": event => console.log( event ),
      "onfinish": { "restart": true },
//    "onready": event => console.log( event ),
//    "onstart": event => console.log( event ),
      "phrases": [ "ccm.load", { "url": "./configs.js#phrases", "type": "module" } ],
      "show_solution": true,
      "shuffle": true,
      "skip": true,
      "text": [ "ccm.load", { "url": "./configs.js#de", "type": "module" } ],
//    "user": [ "ccm.start", "./user/ccm.user.js" ]
    },
    /**
     * @class
     * @memberOf WebComponent
     */
    Instance: function () {

      /**
       * Shortcut to helper functions
       * @private
       * @type {Object.<string,function>}
       */
      let $;

      /**
       * App state data
       * @private
       * @type {app_state}
       */
      let data;

      /**
       * When the instance is created, when all dependencies have been resolved and before the dependent sub-instances are initialized and ready. Allows dynamic post-configuration of the instance.
       * @async
       * @readonly
       * @function
       */
      this.init = async () => {

        // Merge all helper functions and offer them via a single variable.
        $ = Object.assign( {}, this.ccm.helper, this.helper ); $.use( this.ccm );

        // Are the phrases given as an associative field? => Convert the phrases to an array.
        if ( $.isObject( this.phrases ) ) this.phrases = Object.values( this.phrases );

        // By default, all phrases are asked.
        if ( !this.number ) this.number = this.phrases.length;

        // Unify notations data
        for ( const key in this.notations ) {
          let notation = this.notations[ key ];
          this.notations[ key ] = {
            key: notation.key,
            title: notation.title,
            swap: !!notation.swap,
            centered: !!notation.centered,
            mirrored: notation.mirrored || this.default.mirrored,
            images: ( notation.images || this.default.images ).map( image => image.includes( '.' ) ? image : ( notation.path || this.default.path ) + notation.key + '/' + image + '.' + ( notation.format || this.default.format ) ),
            comment: notation.comment
          };
        }

        // Has modal dialog instance for notation legend? => Set title of modal dialog
        if ( this.modal && this.modal.legend ) this.modal.legend.title = `<span data-lang="legend">${ this.text.legend }</span>`;

        // Update the table schema when closing the modal dialog for editing table attributes.
        this.modal.attr.onclose = () => render();

      };

      /**
       * When the instance is created and after all dependent sub-instances are initialized and ready. Allows the first official actions of the instance that should only happen once.
       * @async
       * @readonly
       * @function
       */
      this.ready = async () => {

        // Set content of modal dialog for legend of ER diagram notations.
        this.modal.legend && this.html.render( this.html.legend( this ), this.modal.legend.element.querySelector( 'main' ) );

        // Trigger 'ready' event
        this.onready && await this.onready( { instance: this } );

      };

      /**
       * Starts the app. The current app state is visualized in the webpage area.
       * @async
       * @readonly
       * @function
       */
      this.start = async () => {

        // Load app state data from source.
        data = await $.dataset( this.data );

        // Set initial app state data.
        const initial = {
          correct: 0,
          notation: data.notation || this.default.notation,
          phrases: null,
          results: []
        };

        // All phrases are finished? => Restart with initial state.
        if ( !data.results || !data.phrases || data.results.length >= data.phrases.length ) data = initial;

        // Restart from initial state?
        if ( !data.phrases ) {
          data.phrases = $.clone( this.phrases );               // Clone all original phrases.
          this.shuffle && $.shuffleArray( data.phrases );       // Phrases need to be mixed? => Shuffle phrases
          data.phrases = data.phrases.slice( 0, this.number );  // Use only required number of phrases.
        }

        // Render current phrase
        nextPhrase();

        // Render language selection and user login/logout area.
        const aside = this.element.querySelector( 'aside' );
        if ( aside ) {
          this.lang && !this.lang.getContext() && $.append( aside, this.lang.root );
          this.user && $.append( aside, this.user.root );
        }

        // Trigger 'start' event
        this.onstart && await this.onstart( { instance: this } );

      };

      /**
       * Returns the current app state.
       * @readonly
       * @function
       * @returns {app_state} A deep copy of the app state data.
       */
      this.getValue = () => $.clone( data );

      /**
       * Contains all event handlers.
       * @namespace AppEvents
       * @readonly
       * @type {Object.<string,function>}
       */
      this.events = {

        /**
         * When the notation used in the ER diagram is switched.
         * @function
         * @param {string} value - ID of the selected notation.
         * @param {boolean} [show_solution] - Correct solution is revealed.
         * @memberOf AppEvents
         */
        onNotation: ( value, show_solution ) => {

          // In the case of n-ary relationships, it is not possible to switch to a reverse-reading notation.
          if ( data.phrases[ data.results.length - 1 ].entities.length > 2 && this.notations[ value ].swap ) return;

          // Change notation in app state data
          data.notation = value;

          // Show ER diagram in new notation
          render( show_solution );

          // Trigger 'change' event because of notation change
          this.onchange && this.onchange( { event: 'notation', instance: this } );

        },

        /**
         * When the button to show the notation legend is clicked.
         * @function
         * @memberOf AppEvents
         */
        onLegend: () => {
          this.modal.legend.open();                                               // Open modal dialog for notation legend
          this.lang && this.lang.translate( this.modal.legend.element );          // Is multilingual app? => Translate content of modal dialog
          this.onchange && this.onchange( { event: 'legend', instance: this } );  // Trigger the 'change' event for calling the notation legend.
        },

        /**
         * When the button to add a table is clicked.
         * @function
         * @param {table_nr} nr - Table number
         * @memberOf AppEvents
         */
        onAddTable: nr => {

          // Get result data of current phrase
          const result_data = data.results[ data.results.length - 1 ];

          // A table cannot be created during the feedback.
          if ( result_data.solution ) return;

          // Create an empty table without any key attributes.
          result_data.input[ nr ] = Array( data.phrases[ data.results.length - 1 ].entities.length + 1 ).fill( 0 );

          // Should an artificial primary key be created automatically? => Add artificial primary key
          if ( this.auto_pk ) result_data.input[ nr ][ nr ] = 6;  // 6 = 4 (0100b => PK) + 2 (0010b => NOT NULL)

          // Open modal dialog for editing table attributes
          this.events.onEditTable( nr );

        },

        /**
         * When the button for editing the table attributes is clicked.
         * @function
         * @param {table_nr} nr - Table number
         * @memberOf AppEvents
         */
        onEditTable: nr => {

          // Table attributes can't be edited during the feedback.
          if ( data.results[ data.results.length - 1 ].solution ) return;

          // Set content of modal dialog for title, body and footer
          this.html.render( this.html.tableDialogTitle( this, nr ), this.modal.attr.element.querySelector( '#title' ) );
          this.html.render( this.html.tableDialogBody( this, nr ), this.modal.attr.element.querySelector( 'main' ) );
          this.html.render( this.html.tableDialogFooter( this ), this.modal.attr.element.querySelector( 'footer' ) );

          this.lang && this.lang.translate( this.modal.attr.element );  // Is multilingual app? => Translate content of modal dialog
          this.modal.attr.open();                                       // Open modal dialog for editing table attributes

        },

        /**
         * When the button for removing a table is clicked.
         * @function
         * @param {table_nr} nr - Table number
         * @memberOf AppEvents
         */
        onRemoveTable: nr => {

          const result_data = data.results[ data.results.length - 1 ];  // Get result data of current phrase
          if ( result_data.solution ) return;                           // A table can't be removed during the feedback.
          result_data.input[ nr ] = null;                         // Remove table in app state data

          // Remove all arrowheads on the table in app state data.
          result_data.input.forEach( table => table && ( table[ nr ] &= ~15872 ) );  // 15872 = (0011 1110 0000 0000b => arrow heads for FK0-FK4)

          render();  // Remove table in webpage area

        },

        /**
         * When a badge of a key attribute is clicked.
         * @function
         * @param {table_nr} table - Table number
         * @param {table_nr} attr - Table that references the attribute as a foreign key.
         * @param {number} badge_nr - 0: NULL, 1: NOT NULL, 2: PK, 3: AK, 4: FK0, 5: FK1, 6: FK2, 7: FK3, 8: FK4
         * @memberOf AppEvents
         */
        onToggleBadge: ( table, attr, badge_nr ) => {

          const phrase_data = data.phrases[ data.results.length - 1 ];              // Get data of the current phrase.
          const result_data = data.results[ data.results.length - 1 ];              // Get result data of the current phrase.
          const table_state = result_data.input[ table ];                           // Get state data of the table.
          const toggleBit = ( table_data, i, bit ) => table_data[ i ] ^= 1 << bit;  // Toggles a bit of an attribute value.

          // When a foreign key is removed, the associated arrowhead is also removed.
          if ( badge_nr >= 4 && badge_nr <= 8 && table_state[ attr ] & ( 1 << badge_nr ) )
            table_state[ attr ] &= ~( 1 << ( badge_nr + 5 ) );

          toggleBit( table_state, attr, badge_nr );  // Update the attribute value in the state data of the table.
          const value = table_state[ attr ];         // Get updated attribute value

          // A foreign key is changed and arrows should be set automatically? => Toggle the corresponding foreign key arrowhead.
          if ( badge_nr >= 4 && badge_nr <= 8 && this.auto_arrows && !( phrase_data.solution.length === 2 && phrase_data.entities[ 0 ] === phrase_data.entities[ 1 ] && table === 0 && badge_nr === 6 ) )
            toggleBit( table_state, attr, badge_nr + 5 );

          // An attribute cannot be optional (NULL) and mandatory (NOT NULL) at the same time.
          if ( badge_nr === 0 && value & 1 << 1 ) toggleBit( table_state, attr, 1 );
          if ( badge_nr === 1 && value & 1 << 0 ) toggleBit( table_state, attr, 0 );

          // Update the body section of the modal dialog for editing table attributes.
          this.html.render( this.html.tableDialogBody( this, table ), this.modal.attr.element.querySelector( 'main' ) );

        },

        /**
         * When the end point of a connection between two tables is changed via a selector box.
         * @function
         * @param {string} value - The selected value in the selector box ('', 'line' or 'arrow').
         * @param {table_nr} from - Number of the table from which the connection starts.
         * @param {table_nr} to - Number of the table to which the connection goes.
         * @memberOf AppEvents
         * @example onArrow( 'arrow', 1, 2 ) // Setting an arrow for the connection from entity table 1 to entity table 2 [E1]->[E2]
         */
        onArrow: ( value, from, to ) => {

          // Get the status data of the table that contains the associated foreign key.
          const table_state = data.results[ data.results.length - 1 ].input[ from ];

          // Set/Unset the arrowhead in the table state data for the appropriate foreign key.
          value === 'arrow' ? table_state[ to ] |= 1 << 9 + to : table_state[ to ] &= ~( 1 << 9 + to );  // 1 << 9-13 => arrow for FK0-FK4

          render();  // Update the changed table connection in the web page area.

        },

        /**
         * When the button is clicked that allows the user to submit a solution.
         * @function
         * @memberOf AppEvents
         */
        onSubmit: () => {

          const phrase_data = data.phrases[ data.results.length - 1 ];  // Get data of current phrase
          const result_data = data.results[ data.results.length - 1 ];  // Get result data of current phrase
          if ( result_data.solution ) return;                           // A user's solution can't be submitted during the feedback.

          // Determine what type of relationship is being modeled.
          const is_binary = phrase_data.solution.length === 2;                                        // Binary relationship with two entities.
          const is_recursive = is_binary && phrase_data.entities[ 0 ] === phrase_data.entities[ 1 ];  // Recursive relationship with a single entity.
          const is_multi = !phrase_data.solution.find( value => value !== 'cn' && value !== 'n' );    // N:M relationship with two or more entities.
          const is_hierarchy = !phrase_data.relation;                                                 // Specialization/Generalization

          // Define and determine all possible solutions for the current phrase.
          let solutions;
          if ( is_hierarchy )
            if ( phrase_data.entities.length < 4 )  // Specialization/Generalization with 3 entities (2 sub entities)
              solutions = [
                [ null, [ 0, 6, 0, 0 ], [ 0, 1066, 6, 0 ], [ 0, 1066, 0, 6 ] ],  // E1[PK] E2[PK,AK+FK1] E3[PK,AK+FK1]
                [ null, [ 0, 6, 0, 0 ], [ 0, 1062, 0, 0 ], [ 0, 1062, 0, 0 ] ]   // E1[PK] E2[PK+FK1] E3[PK+FK1]
              ];
            else                                    // Specialization/Generalization with 4 entities (3 sub entities)
              solutions = [
                [ null, [ 0, 6, 0, 0, 0 ], [ 0, 1066, 6, 0, 0 ], [ 0, 1066, 0, 6, 0 ], [ 0, 1066, 0, 0, 6 ] ],  // E1[PK] E2[PK,AK+FK1] E3[PK,AK+FK1] E4[PK,AK+FK1]
                [ null, [ 0, 6, 0, 0, 0 ], [ 0, 1062, 0, 0, 0 ], [ 0, 1062, 0, 0, 0 ], [ 0, 1062, 0, 0, 0 ] ]   // E1[PK] E2[PK+FK1] E3[PK+FK1] E4[PK+FK1]
              ];
          else if ( is_multi ) {
            if ( phrase_data.entities.length === 2 )  // N:M relationship with 2 entities
              if ( is_recursive )
                solutions = [
                  [ [ 0, 1062, 70 ], [ 0, 6, 0 ], null ],  // R[PK+FK1,PK+FK2] E1[PK]
                  [ [ 6, 1066, 74 ], [ 0, 6, 0 ], null ]   // R[PK,AK+FK1,AK+FK2] E1[PK]
                ];
              else
                solutions = [
                  [ [ 0, 1062, 2118 ], [ 0, 6, 0 ], [ 0, 0, 6 ] ],  // R[PK+FK1,PK+FK2] E1[PK] E2[PK]
                  [ [ 6, 1066, 2122 ], [ 0, 6, 0 ], [ 0, 0, 6 ] ]   // R[PK,AK+FK1,AK+FK2] E1[PK] E2[PK]
                ];
            if ( phrase_data.entities.length === 3 )  // N:M relationship with 3 entities
              solutions = [
                [ [ 0, 1062, 2118, 4230 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 0, 0, 0, 6 ] ],  // R[PK+FK1,PK+FK2,PK+FK3] E1[PK] E2[PK] E3[PK]
                [ [ 6, 1066, 2122, 4234 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 0, 0, 0, 6 ] ]   // R[PK,AK+FK1,AK+FK2,AK+FK3] E1[PK] E2[PK] E3[PK]
              ];
            if ( phrase_data.entities.length === 4 )  // N:M relationship with 4 entities
              solutions = [
                [ [ 0, 1062, 2118, 4230, 8454 ], [ 0, 6, 0, 0, 0 ], [ 0, 0, 6, 0, 0 ], [ 0, 0, 0, 6, 0 ], [ 0, 0, 0, 0, 6 ] ],  // R[PK+FK1,PK+FK2,PK+FK3,PK+FK4] E1[PK] E2[PK] E3[PK] E4[PK]
                [ [ 6, 1066, 2122, 4234, 8458 ], [ 0, 6, 0, 0, 0 ], [ 0, 0, 6, 0, 0 ], [ 0, 0, 0, 6, 0 ], [ 0, 0, 0, 0, 6 ] ]   // R[PK,AK+FK1,AK+FK2,AK+FK3,AK+F4] E1[PK] E2[PK] E3[PK] E4[PK]
              ];
          }
          else if ( is_binary ) {

            // Define the possible solutions for all combinations of cardinalities given a binary relationship, excluding N:M relationships.
            solutions = {
              '1': {
                '1': [
                  [ null, [ 0, 6, 2122 ], [ 0, 0, 6 ] ],  // E1[PK,AK+FK2] E2[PK]
                  [ null, [ 0, 0, 2118 ], [ 0, 0, 6 ] ],  // E1[PK+FK2] E2[PK]
                  [ null, [ 0, 6, 0 ], [ 0, 1066, 6 ] ],  // E1[PK] E2[PK,AK+FK1]
                  [ null, [ 0, 6, 0 ], [ 0, 1062, 0 ] ]   // E1[PK] E2[PK+FK1]
                ],
                'c': [
                  [ null, [ 0, 6, 2122 ], [ 0, 0, 6 ] ],  // E1[PK,AK+FK2] E2[PK]
                  [ null, [ 0, 0, 2118 ], [ 0, 0, 6 ] ]   // E1[PK+FK2] E2[PK]
                ],
                'cn': [
                  [ null, [ 0, 6, 2114 ], [ 0, 0, 6 ] ]   // E1[PK,FK2] E2[PK]
                ],
                'n': [
                  [ null, [ 0, 6, 2114 ], [ 0, 0, 6 ] ]   // E1[PK,FK2] E2[PK]
                ]
              },
              'c': {
                '1': [
                  [ null, [ 0, 6, 0 ], [ 0, 1066, 6 ] ],  // E1[PK] E2[PK,AK+FK1]
                  [ null, [ 0, 6, 0 ], [ 0, 1062, 0 ] ]   // E1[PK] E2[PK+FK1]
                ],
                'c': [
                  [ null, [ 0, 6, 2121 ], [ 0, 0, 6 ] ],  // E1[PK,AK+FK2+NULL] E2[PK]
                  [ null, [ 0, 6, 0 ], [ 0, 1065, 6 ] ]   // E1[PK] E2[PK,AK+FK1+NULL]
                ],
                'cn': [
                  [ null, [ 0, 6, 2113 ], [ 0, 0, 6 ] ]   // E1[PK,FK2+NULL] E2[PK]
                ],
                'n': [
                  [ null, [ 0, 6, 2113 ], [ 0, 0, 6 ] ]   // E1[PK,FK2+NULL] E2[PK]
                ]
              },
              'cn': {
                '1': [
                  [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ]   // E1[PK] E2[PK,F1]
                ],
                'c': [
                  [ null, [ 0, 6, 0 ], [ 0, 1057, 6 ] ]   // E1[PK] E2[PK,F1+NULL]
                ]
              },
              'n': {
                '1': [
                  [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ]   // E1[PK] E2[PK,FK1]
                ],
                'c': [
                  [ null, [ 0, 6, 0 ], [ 0, 1057, 6 ] ]   // E1[PK] E2[PK,FK1+NULL]
                ]
              }
            };

            solutions = solutions[ phrase_data.solution[ 0 ] ][ phrase_data.solution[ 1 ] ];  // Select the solutions for the binary relationship to be modeled.
            is_recursive && solutions.forEach( solution => solution[ 2 ] = null );            // In a recursive binary relationship, the second entity table in the solution must be removed.

          }
          else if ( phrase_data.solution.toString() === 'cn,n,1' || phrase_data.solution.toString() === 'n,n,1' )  // N:M:1 relationships.
            solutions = [
              [ null, [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 0, 1058, 2114, 6 ] ],               // E1[PK] E2[PK] E3[PK,FK1,FK2]
              [ [ 0, 1058, 2114, 6 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], null ],               // R[FK1,FK2,PK]
              [ [ 0, 1062, 2118, 4234 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 0, 0, 0, 6 ] ],  // R[PK+FK1,PK+FK2,AK+FK3] E1[PK] E2[PK] E3[PK]
              [ [ 0, 1062, 2118, 0 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 534, 0, 0, 0 ] ],   // R[PK+FK1,PK+FK2] E1[PK] E2[PK] E3[PK+FK0]
              [ [ 0, 1062, 2118, 0 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 538, 0, 0, 6 ] ],   // R[PK+FK1,PK+FK2] E1[PK] E2[PK] E3[PK,AK+FK0]
              [ [ 6, 1066, 2122, 0 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 538, 0, 0, 6 ] ],   // R[PK,AK+FK1,PK,AK+FK2] E1[PK] E2[PK] E3[PK,AK+FK0]
              [ [ 6, 1066, 2122, 0 ], [ 0, 6, 0, 0 ], [ 0, 0, 6, 0 ], [ 534, 0, 0, 0 ] ]    // R[PK,AK+FK1,PK,AK+FK2] E1[PK] E2[PK] E3[PK+FK0]
            ];

          // A user's solution can only be submitted if a correct solution for control could be determined.
          if ( !solutions ) return;

          // Compare the determined possible solutions with the user's solution.
          const solution = solutions.find( solution => JSON.stringify( result_data.input ) === JSON.stringify( solution ) );

          // Note in the phrase's status data whether the user's solution matches one of the possible solutions.
          result_data.correct = !!solution;

          // Note in the phrase status data whether a valid alternative solution was found.
          if ( result_data.correct && solution !== solutions[ 0 ] ) result_data.alternate_solution = solution;

          // Note the main solution in the status data of the phrase, which the feedback will refer to.
          result_data.solution = solutions[ 0 ];

          // Did the user find a valid solution? => Increase the number of correctly answered phrases in the app state data.
          result_data.correct && data.correct++;

          // Does the user get automated feedback on their solution?
          if ( this.feedback ) {
            this.element.classList.add( result_data.correct ? 'correct' : 'failed' );  // Enable visual feedback in the webpage area.
            render();                                                                   // Refresh the webpage area.
          }

          this.onchange && this.onchange( { event: 'submit', instance: this } );  // Trigger the 'change' event for submitting a solution.
          !this.feedback && this.events.onNext();                                 // No automated feedback? => Switch to the next phrase.

        },

        /**
         * When the button is clicked that allows the user to correct an incorrect solution.
         * @function
         * @memberOf AppEvents
         */
        onCorrection: () => {

          const result_data = data.results[ data.results.length - 1 ];  // Get result data of current phrase
          if ( !this.correction || !result_data.solution ) return;      // Check if the user is allowed to correct his solution.

          // Increment the counter for the number of corrections in the phrase state data.
          result_data.correction = result_data.correction ? result_data.correction + 1 : 1;

          delete result_data.correct; delete result_data.solution;                  // Remove the information about the correctness of the solution from the state data of the phrase.
          reset(); render();                                                          // Remove the visual feedback in the webpage area.
          this.onchange && this.onchange( { event: 'correction', instance: this } );  // Trigger the 'change' event because of the correction.

        },

        /**
         * When the button showing the sample solution for the current phrase is clicked.
         * @function
         * @memberOf AppEvents
         */
        onSolution: () => {

          if ( !this.show_solution || !data.results[ data.results.length - 1 ].solution ) return;  // Check if the user is allowed to reveal the sample solution.
          render( true );                                                                          // Reveal the sample solution in the webpage area.
          this.onchange && this.onchange( { event: 'solution', instance: this } );                 // Trigger the 'change' event due to the reveal of the sample solution.

        },

        /**
         * When the button that starts the next phrase is clicked.
         * @function
         * @memberOf AppEvents
         */
        onNext: () => {

          // Check if the user is allowed to start the next phrase.
          if ( !data.results[ data.results.length - 1 ].solution && !this.skip || data.results.length === this.number ) return;

          reset();       // Remove the visual feedback in the webpage area.
          nextPhrase();  // Switch to the next phrase.

          // Trigger the 'change' event due to the start of the next phrase.
          this.onchange && this.onchange( { event: 'next', instance: this, phrase: data.results.length } );

        },

        /**
         * When the button that finishes the app is clicked.
         * @function
         * @memberOf AppEvents
         */
        onFinish: () => {

          // Check if the user is allowed to finish the app.
          if ( !this.onfinish || !data.results[ data.results.length - 1 ].solution && !this.skip || data.results.length < this.number && !this.anytime_finish ) return;

          reset();             // Remove the visual feedback in the webpage area.
          $.onFinish( this );  // Perform finish actions

        }

      };

      /**
       * Starts the next phrase.
       * @private
       * @function
       */
      const nextPhrase = () => {

        // Set initial state data for the new phrase.
        data.results.push( {
          input: Array( data.phrases[ data.results.length ].entities.length + 1 ).fill( null ),
          solution: null
        } );

        // An n-ary relationship should not be displayed in a reverse-reading notation, since the semantics are different there. In this case, a forward-reading notation is used.
        if ( data.phrases[ data.results.length - 1 ].entities.length > 2 && this.notations[ data.notation ].swap )  // Check for an n-ary relationship displayed in a reverse-reading notation.
          data.notation = Object.values( this.notations ).find( notation => !notation.swap ).key;                   // => Switch to a forward-reading notation.

        // Display the new Phrase in the webpage area
        render();

      };

      /**
       * Updates the webpage area with the current app state data.
       * @private
       * @function
       * @param {boolean} [show_solution] - The correct solution should be revealed.
       */
      const render = show_solution => {

        // Update main HTML template
        this.html.render( this.html.main( this, show_solution ), this.element );

        // Multilingual app? => Translate content of webpage area.
        this.lang && this.lang.translate();

      };

      /**
       * Removes the visual feedback in the webpage area.
       * @private
       * @function
       */
      const reset = () => this.element.classList.remove( 'correct', 'failed' );

    }
  };
  let b="ccm."+component.name+(component.version?"-"+component.version.join("."):"")+".js";if(window.ccm&&null===window.ccm.files[b])return window.ccm.files[b]=component;(b=window.ccm&&window.ccm.components[component.name])&&b.ccm&&(component.ccm=b.ccm);"string"===typeof component.ccm&&(component.ccm={url:component.ccm});let c=(component.ccm.url.match(/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/)||[""])[0];if(window.ccm&&window.ccm[c])window.ccm[c].component(component);else{var a=document.createElement("script");document.head.appendChild(a);component.ccm.integrity&&a.setAttribute("integrity",component.ccm.integrity);component.ccm.crossorigin&&a.setAttribute("crossorigin",component.ccm.crossorigin);a.onload=function(){(c="latest"?window.ccm:window.ccm[c]).component(component);document.head.removeChild(a)};a.src=component.ccm.url}
} )();

/**
 * App configuration.
 * @typedef {object} app_config
 * @prop {boolean} [anytime_finish] - Finish button is always unlocked. You no longer have to go through all the phrases to use it.
 * @prop {boolean} [auto_arrows] - The arrows between tables are automatically set correctly and haven't to be selected manually.
 * @prop {boolean} [auto_pk] - When creating a table, an artificial primary key is created automatically.
 * @prop {object} [comments] - Show comments to support the user.
 * @prop {boolean} [comments.input] - Show comments during input (hints like "start creating tables" or "start to connect tables").
 * @prop {boolean} [comments.wrong] - Show comments as feedback on an incorrect solution.
 * @prop {boolean} [comments.correct] - Show comments as feedback on a correct solution.
 * @prop {boolean} [correction] - Allows the user to correct an incorrect solution.
 * @prop {array} css - CSS dependencies.
 * @prop {object} [data] - Source of app state data.
 * @prop {object} [default] - Default notations data.
 * @prop {boolean} [feedback] - Show visual feedback and any comments after submitting a solution.
 * @prop {boolean} [fixed_notation] - The notation used in the ER diagram cannot be changed.
 * @prop {array} helper - Dependency on helper functions.
 * @prop {boolean} [hide_own_fk] - No foreign key is offered for the table's own main attribute.
 * @prop {array} html - HTML template dependencies.
 * @prop {array} [lang] - Dependency on component for multilingualism.
 * @prop {boolean} [legend] - Button to display a legend for the different notations in the ER diagram.
 * @prop {boolean} [license] - Show license information in the bottom of the app.
 * @prop {string} [logos] - Show image of logos in the bottom of the app.
 * @prop {object} modal - Dependencies on component instances for modal dialogs.
 * @prop {array} modal.attr - Modal dialog for editing attributes of a table.
 * @prop {array} [modal.legend] - Modal dialog to display a legend for the different notations in the ER diagram.
 * @prop {Object.<string,notation_data>} notations - Data of the different notations in the ER diagram.
 * @prop {number} [number] - Number of phrases to be asked. By default, all phrases are asked.
 * @prop {function} [onchange] - When something changes in the app (notation change, show legend, submit, correction, show solution, next phrase).
 * @prop {function|object} [onfinish] - When the finish button is clicked. Sets the finish actions.
 * @prop {function} [onready] - Is called once before the first start of the app.
 * @prop {function} [onstart] - When the app has finished starting.
 * @prop {object} phrases - Data of the phrases.
 * @prop {boolean} [show_solution] - If the user solves the phrase incorrectly, he can reveal a correct solution. After that, the user can no longer correct his input.
 * @prop {boolean} [shuffle] - The phrases are shuffled, so the order in which the phrases are asked is different each time the app is started.
 * @prop {boolean} [skip] - Phrases can be skipped, so that not all phrases have to be answered.
 * @prop {boolean} text - Contains the static texts (e.g. task description, labeling of buttons, hints on feedback).
 * @prop {boolean} [user] - Dependency on component for user authentication.
 */

/**
 * App state data.
 * @typedef {object} app_state
 * @prop {number} correct - Number of correctly answered phrases.
 * @prop {string} notation - Current selected notation (e.g. 'abrial', 'arrow', 'chen', 'crow', 'mc', 'uml').
 * @prop {phrase_data[]} phrases - Phrases used in the order they are queried.
 * @prop {result_data[]} results - Result data of the phrases processed so far.
 * @example
 * {
 *   "correct": 1,
 *   "notation": "abrial",
 *   "phrases": [
 *     {
 *       "text": "Eine Bibliothek möchte die einzelnen Seiten ausgewählter Bücher digitalisieren.",
 *       "entities": [ "Buch", "Seite" ],
 *       "relation": "hat",
 *       "solution": [ "n", "1" ]
 *     },
 *     ...
 *   ]
 *   "results": [
 *     {
 *       "input": [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ],
 *       "correct": true,
 *       "solution": [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ]
 *     },
 *     ...
 *   ]
 * }
 */

/**
 * Notation data.
 * @typedef {object} notation_data
 * @prop {string} key - Unique key of the notation (e.g. 'abrial', 'arrow', 'chen', 'crow', 'mc', 'uml')
 * @prop {string} title - Title of the notation (used for the selection of the notation).
 * @prop {boolean} [swap] - Notation has a reverse reading order than Abrial notation.
 * @prop {boolean} [centered] - The relational verb is centered vertically in the diagram and not slightly higher above a connecting line.
 * @prop {boolean} [mirrored] - The notation is mirrored on the left side.
 * @prop {string} [comment] - Note that informs about special aspects of a notation.
 * @prop {string[]} images - Image URLs or filenames of the images without a file extension.
 * @prop {string} [format] - File extension of the image files (when using only filenames).
 * @prop {string} [path] - Image URLs without filename and file extension (when using only filenames).
 * @example
 * {
 *   "key": "arial",
 *   "title": "Abrial",
 *   "centered": true,
 *   "images": [
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/e.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/1.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/c.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/n.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/cn.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/r.svg",
 *     "https://ccmjs.github.io/eild/er_trainer/resources/img/abrial/s.svg"
 *   ]
 * }
 * @example
 * {
 *   "key": "arial",
 *   "title": "Abrial",
 *   "centered": true,
 *   "format": "svg",
 *   "images": [ "e", "1", "c", "n", "cn", "r", "s" ],
 *   "path": "https://ccmjs.github.io/eild/er_trainer/resources/img/"
 * }
 */

/**
 * Phrase data.
 * @typedef {object} phrase_data
 * @prop {string} text - Text of the phrase.
 * @prop {string[]} entities - Names of the entities.
 * @prop {string[]} [roles] - Role names of the entities (useful in recursive relationships).
 * @prop {string} [relation] - Name of the relation between the entities (default: has relation of a generalization/specialization).
 * @prop {string[]} solution - Solution of the phrase ('1': simple, 'c': conditional, 'n': multiple, 'cn': conditional multiple).
 * @example
 * {
 *   "text": "Eine Bibliothek möchte die einzelnen Seiten ausgewählter Bücher digitalisieren.",
 *   "entities": [ "Buch", "Seite" ],
 *   "relation": "hat",
 *   "solution": [ "n", "1" ]
 * }
 */

/**
 * Result data of a phrase.
 * @typedef {object} result_data
 * @prop {table_data[]} input - Input data of the tables.
 * @prop {boolean} [correct] - Phrase answered correctly.
 * @prop {table_data[]} solution - Solution data of the tables (main solution).
 * @prop {table_data[]} [alternate_solution] - Alternate solution found.
 * @example
 * {
 *   "correct": true,
 *   "input": [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ],
 *   "solution": [ null, [ 0, 6, 0 ], [ 0, 1058, 6 ] ]
 * }
 */

/**
 * Table data.<br>
 * null: not created<br>
 * [0]: ID attribute of relation table<br>
 * [1]: ID attribute of entity 1<br>
 * [2]: ID attribute of entity 2<br>
 * [3]: ID attribute of entity 3<br>
 * [4]: ID attribute of entity 4
 * @typedef {attr_value[]} table_data
 * @example
 * [ 0, 1058, 6 ]
 */

/**
 * Bit mask of a table attribute.<br>
 * 0: not set<br>
 * 2^0=1: is optional (NULL)<br>
 * 2^1=2: is mandatory (NOT NULL)<br>
 * 2^2=4: is part of primary key (PK)<br>
 * 2^3=8: is part of alternate key (AK)<br>
 * 2^4=16: is part of foreign key to relation table (FK0)<br>
 * 2^5=32: is part of foreign key to entity 1 (FK1)<br>
 * 2^6=64: is part of foreign key to entity 2 (FK2)<br>
 * 2^7=128: is part of foreign key to entity 3 (FK3)<br>
 * 2^8=256: is part of foreign key to entity 4 (FK4)<br>
 * 2^9=512: arrow for FK0 is set<br>
 * 2^10=1024: arrow for FK1 is set<br>
 * 2^11=2048: arrow for FK2 is set<br>
 * 2^12=4096: arrow for FK3 is set<br>
 * 2^13=8192: arrow for FK4 is set
 * @typedef {number} attr_value
 * @example
 * 1058 // = 1024 + 32 + 2
 * // => Foreign key to entity 1 (FK1) with set arrow and NOT NULL.
 */

/**
 * Number of an entity (1-N: entity 1-N).
 * @typedef {number} entity_nr
 */

/**
 * Number of a relation scheme table (0: extra table, 1-N: entity table 1-N).
 * @typedef {number} table_nr
 */