"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 */