From a1e15a5e437c832927e083f69675af4cf237d7ea Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 23 Sep 2025 16:52:55 +0100 Subject: [PATCH 001/100] iss1171 - reactive start --- amd/build/metadata/container.min.js | 12 +++ amd/build/metadata/container.min.js.map | 1 + amd/build/metadata/contributors.min.js | 12 +++ amd/build/metadata/contributors.min.js.map | 1 + amd/build/metadata/events.min.js | 12 +++ amd/build/metadata/events.min.js.map | 1 + amd/build/metadata/exporter.min.js | 13 ++++ amd/build/metadata/exporter.min.js.map | 1 + amd/build/metadata/main.min.js | 12 +++ amd/build/metadata/main.min.js.map | 1 + amd/build/metadata/metadata.min.js | 3 + amd/build/metadata/metadata.min.js.map | 1 + amd/build/metadata/mutations.min.js | 12 +++ amd/build/metadata/mutations.min.js.map | 1 + amd/src/metadata/container.js | 88 ++++++++++++++++++++++ amd/src/metadata/contributors.js | 53 +++++++++++++ amd/src/metadata/events.js | 56 ++++++++++++++ amd/src/metadata/exporter.js | 40 ++++++++++ amd/src/metadata/metadata.js | 76 +++++++++++++++++++ amd/src/metadata/mutations.js | 79 +++++++++++++++++++ classes/output/metadata.php | 67 ++++++++++++++++ edit_stack_form.php | 7 +- templates/metadata.mustache | 32 ++++++++ templates/metadata/contributors.mustache | 45 +++++++++++ 24 files changed, 623 insertions(+), 3 deletions(-) create mode 100644 amd/build/metadata/container.min.js create mode 100644 amd/build/metadata/container.min.js.map create mode 100644 amd/build/metadata/contributors.min.js create mode 100644 amd/build/metadata/contributors.min.js.map create mode 100644 amd/build/metadata/events.min.js create mode 100644 amd/build/metadata/events.min.js.map create mode 100644 amd/build/metadata/exporter.min.js create mode 100644 amd/build/metadata/exporter.min.js.map create mode 100644 amd/build/metadata/main.min.js create mode 100644 amd/build/metadata/main.min.js.map create mode 100644 amd/build/metadata/metadata.min.js create mode 100644 amd/build/metadata/metadata.min.js.map create mode 100644 amd/build/metadata/mutations.min.js create mode 100644 amd/build/metadata/mutations.min.js.map create mode 100644 amd/src/metadata/container.js create mode 100644 amd/src/metadata/contributors.js create mode 100644 amd/src/metadata/events.js create mode 100644 amd/src/metadata/exporter.js create mode 100644 amd/src/metadata/metadata.js create mode 100644 amd/src/metadata/mutations.js create mode 100644 classes/output/metadata.php create mode 100644 templates/metadata.mustache create mode 100644 templates/metadata/contributors.mustache diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js new file mode 100644 index 00000000000..1b33b5152df --- /dev/null +++ b/amd/build/metadata/container.min.js @@ -0,0 +1,12 @@ +define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * The beginner main app component. + * + * @module mod_nosferatu/local/beginner + * @class mod_nosferatu/local/beginner + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-app-container",this.selectors={METADATACONTAINER:"[data-for='metadata-contrib']"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){this._reloadCityComponent({state:state})}async _reloadCityComponent(_ref){let{state:state}=_ref;const data={people:[]};state.people.forEach((person=>{data.people.push({...person})})),data.haspeople=0!=data.people.length;const citiyContainer=this.getElement(this.selectors.METADATACONTAINER);if(!citiyContainer)throw new Error("Missing city container.");window.console.log(data),this.cityComponent=await this.renderComponent(citiyContainer,"qtype_stack/metadata/contributors",data)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map new file mode 100644 index 00000000000..f03e4e554ec --- /dev/null +++ b/amd/build/metadata/container.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-app-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='metadata-contrib']`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * Note in this case we want our stateReady to be async.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n this._reloadCityComponent({state});\n }\n\n async _reloadCityComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object. In the intermediate level you will learng how to centralize this\n // kind of operations to keep your components cleaner.\n const data = {\n people: [],\n };\n state.people.forEach(person => {\n data.people.push({...person});\n });\n data.haspeople = (data.people.length != 0);\n\n // To render a child component we need a container.\n const citiyContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!citiyContainer) {\n throw new Error('Missing city container.');\n }\n window.console.log(data);\n // We store the new content into an attribute in case we want to remove it in the future.\n this.cityComponent = await this.renderComponent(citiyContainer, 'qtype_stack/metadata/contributors', data);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","target","this","element","document","querySelector","reactive","metadata","state","_reloadCityComponent","data","people","forEach","person","push","haspeople","length","citiyContainer","getElement","Error","window","console","log","cityComponent","renderComponent"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,oCACPC,UAAY,CACbC,+DAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,6BAWSS,YACRC,qBAAqB,CAACD,MAAAA,6CAGJA,MAACA,kBAIlBE,KAAO,CACTC,OAAQ,IAEZH,MAAMG,OAAOC,SAAQC,SACjBH,KAAKC,OAAOG,KAAK,IAAID,YAEzBH,KAAKK,UAAmC,GAAtBL,KAAKC,OAAOK,aAGxBC,eAAiBf,KAAKgB,WAAWhB,KAAKH,UAAUC,uBACjDiB,qBACK,IAAIE,MAAM,2BAEpBC,OAAOC,QAAQC,IAAIZ,WAEda,oBAAsBrB,KAAKsB,gBAAgBP,eAAgB,oCAAqCP"} \ No newline at end of file diff --git a/amd/build/metadata/contributors.min.js b/amd/build/metadata/contributors.min.js new file mode 100644 index 00000000000..16e5b177c18 --- /dev/null +++ b/amd/build/metadata/contributors.min.js @@ -0,0 +1,12 @@ +define("qtype_stack/metadata/contributors",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * The beginner main app component. + * + * @module mod_nosferatu/local/beginner + * @class mod_nosferatu/local/beginner + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-contrib"}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=contributors.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/contributors.min.js.map b/amd/build/metadata/contributors.min.js.map new file mode 100644 index 00000000000..894ceb5f75e --- /dev/null +++ b/amd/build/metadata/contributors.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contributors.min.js","sources":["../../src/metadata/contributors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-contrib';\n\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n}"],"names":["BaseComponent","create","name","target","selectors","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,qCAWJC,OAAQC,kBACT,IAAIC,KAAK,CACZC,QAASC,SAASC,cAAcL,QAChCM,SAAUC,mBACVN,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/events.min.js b/amd/build/metadata/events.min.js new file mode 100644 index 00000000000..2dd87572f43 --- /dev/null +++ b/amd/build/metadata/events.min.js @@ -0,0 +1,12 @@ +define("qtype_stack/metadata/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyQtypeStackStateUpdated=_exports.eventTypes=void 0; +/** + * Javascript events for the `mod_nosferatu` activity. + * + * @module mod_nosferatu/events + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.0 + */ +const eventTypes={qtypeStackStateUpdated:"qtype_stack/stateUpdated"};_exports.eventTypes=eventTypes;_exports.notifyQtypeStackStateUpdated=(detail,container)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.qtypeStackStateUpdated,detail,container)})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/events.min.js.map b/amd/build/metadata/events.min.js.map new file mode 100644 index 00000000000..89e91e6c631 --- /dev/null +++ b/amd/build/metadata/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for the `mod_nosferatu` activity.\n *\n * @module mod_nosferatu/events\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\n/**\n * Events for the `mod_nosferatu` activity.\n *\n * @constant\n * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event modNosferatuStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyModNosferatuStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires modNosferatuStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;;MAiCaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file diff --git a/amd/build/metadata/exporter.min.js b/amd/build/metadata/exporter.min.js new file mode 100644 index 00000000000..f9b2ec197bb --- /dev/null +++ b/amd/build/metadata/exporter.min.js @@ -0,0 +1,13 @@ +define("qtype_stack/metadata/exporter",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default= +/** + * Module to export parts of the state and transform them to be used in templates + * and as draggable data. + * + * @module mod_nosferatu/local/intermediate/exporter + * @class mod_nosferatu/local/intermediate/exporter + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class{constructor(reactive){this.reactive=reactive}metadata(state){const data={people:[]};return state.people.forEach((person=>{data.people.push({...person})})),data.haspeople=0!=data.people.length,data}},_exports.default})); + +//# sourceMappingURL=exporter.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/exporter.min.js.map b/amd/build/metadata/exporter.min.js.map new file mode 100644 index 00000000000..6b374ffc987 --- /dev/null +++ b/amd/build/metadata/exporter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"exporter.min.js","sources":["../../src/metadata/exporter.js"],"sourcesContent":["// along with Moodle. If not, see .\n\n/**\n * Module to export parts of the state and transform them to be used in templates\n * and as draggable data.\n *\n * @module mod_nosferatu/local/intermediate/exporter\n * @class mod_nosferatu/local/intermediate/exporter\n * @copyright 2022 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class {\n\n /**\n * Class constructor.\n * @param {Reactive} reactive the course editor object\n */\n constructor(reactive) {\n this.reactive = reactive;\n }\n\n /**\n * Generate the people export data from the state.\n *\n * @param {Object} state the current state.\n * @returns {Object}\n */\n metadata(state) {\n // Collect section information from the state.\n const data = {\n // State stores maps of key-values instead of simple arrays.\n people: [],\n };\n state.people.forEach(person => {\n data.people.push({...person});\n });\n data.haspeople = (data.people.length != 0);\n return data;\n }\n}"],"names":["constructor","reactive","metadata","state","data","people","forEach","person","push","haspeople","length"],"mappings":";;;;;;;;;;MAiBIA,YAAYC,eACHA,SAAWA,SASpBC,SAASC,aAECC,KAAO,CAETC,OAAQ,WAEZF,MAAME,OAAOC,SAAQC,SACjBH,KAAKC,OAAOG,KAAK,IAAID,YAEzBH,KAAKK,UAAmC,GAAtBL,KAAKC,OAAOK,OACvBN"} \ No newline at end of file diff --git a/amd/build/metadata/main.min.js b/amd/build/metadata/main.min.js new file mode 100644 index 00000000000..f8d9e9051b2 --- /dev/null +++ b/amd/build/metadata/main.min.js @@ -0,0 +1,12 @@ +define("qtype_stack/metadata/main",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * The beginner main app component. + * + * @module mod_nosferatu/local/beginner + * @class mod_nosferatu/local/beginner + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-app-container",this.selectors={METADATACONTAINER:"[data-for='metadata']"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){this._reloadCityComponent({state:state})}async _reloadCityComponent(_ref){let{state:state}=_ref;const data={people:[]};state.people.forEach((person=>{data.people.push({...person})})),data.haspeople=0!=data.people.length;const citiyContainer=this.getElement(this.selectors.METADATACONTAINER);if(!citiyContainer)throw new Error("Missing city container.");window.console.log(data),this.cityComponent=await this.renderComponent(citiyContainer,"qtype_stack/metadata",data)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=main.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/main.min.js.map b/amd/build/metadata/main.min.js.map new file mode 100644 index 00000000000..15be14c93ef --- /dev/null +++ b/amd/build/metadata/main.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.min.js","sources":["../../src/metadata/main.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-app-container';\n\n this.selectors = {\n METADATACONTAINER: `[data-for='metadata']`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * Note in this case we want our stateReady to be async.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n this._reloadCityComponent({state});\n }\n\n async _reloadCityComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object. In the intermediate level you will learng how to centralize this\n // kind of operations to keep your components cleaner.\n const data = {\n people: [],\n };\n state.people.forEach(person => {\n data.people.push({...person});\n });\n data.haspeople = (data.people.length != 0);\n\n // To render a child component we need a container.\n const citiyContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!citiyContainer) {\n throw new Error('Missing city container.');\n }\n window.console.log(data);\n // We store the new content into an attribute in case we want to remove it in the future.\n this.cityComponent = await this.renderComponent(citiyContainer, 'qtype_stack/metadata', data);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","target","this","element","document","querySelector","reactive","metadata","state","_reloadCityComponent","data","people","forEach","person","push","haspeople","length","citiyContainer","getElement","Error","window","console","log","cityComponent","renderComponent"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,oCAEPC,UAAY,CACbC,uDAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,6BAWSS,YACRC,qBAAqB,CAACD,MAAAA,6CAGJA,MAACA,kBAIlBE,KAAO,CACTC,OAAQ,IAEZH,MAAMG,OAAOC,SAAQC,SACjBH,KAAKC,OAAOG,KAAK,IAAID,YAEzBH,KAAKK,UAAmC,GAAtBL,KAAKC,OAAOK,aAGxBC,eAAiBf,KAAKgB,WAAWhB,KAAKH,UAAUC,uBACjDiB,qBACK,IAAIE,MAAM,2BAEpBC,OAAOC,QAAQC,IAAIZ,WAEda,oBAAsBrB,KAAKsB,gBAAgBP,eAAgB,uBAAwBP"} \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js b/amd/build/metadata/metadata.min.js new file mode 100644 index 00000000000..2db6fb5c501 --- /dev/null +++ b/amd/build/metadata/metadata.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=_exports.init=void 0;class StackMetadata extends _reactive.Reactive{}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,state:{people:[{id:1,name:"Carlos",bitten:!1},{id:2,name:"Amaia",bitten:!1},{id:3,name:"Sara",bitten:!1},{id:4,name:"Ilya",bitten:!0},{id:5,name:"Ferran",bitten:!1}]},mutations:_mutations.mutations});_exports.metadata=metadata;_exports.init=()=>{}})); + +//# sourceMappingURL=metadata.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map new file mode 100644 index 00000000000..e87322e0389 --- /dev/null +++ b/amd/build/metadata/metadata.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nconst state = {\n 'people': [\n {\n id: 1,\n name: 'Carlos',\n bitten: false,\n },\n {\n id: 2,\n name: 'Amaia',\n bitten: false,\n },\n {\n id: 3,\n name: 'Sara',\n bitten: false,\n },\n {\n id: 4,\n name: 'Ilya',\n bitten: true,\n },\n {\n id: 5,\n name: 'Ferran',\n bitten: false,\n },\n ],\n};\n\nclass StackMetadata extends Reactive {\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n state,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n //state.metadata = JSON.parse(metadata);\n};"],"names":["StackMetadata","Reactive","metadata","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","state","id","bitten","mutations"],"mappings":"wRAwDMA,sBAAsBC,0BAMfC,SAAW,IAAIF,cAAc,CACtCG,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,MAxCU,QACA,CACN,CACIC,GAAI,EACJP,KAAM,SACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,QACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,SACNQ,QAAQ,KAgBhBC,UAAAA,gEAMgB"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js new file mode 100644 index 00000000000..df36d6b1103 --- /dev/null +++ b/amd/build/metadata/mutations.min.js @@ -0,0 +1,12 @@ +define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.mutations=void 0;const mutations=new +/** + * Default mutation manager + * + * @module mod_nosferatu/local/beginner/mutations + * @class mod_nosferatu/local/beginner/mutations + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class{bite(stateManager,personId){const state=stateManager.state;stateManager.setReadOnly(!1),state.people.get(personId).bitten=!0,stateManager.setReadOnly(!0)}cureAll(stateManager){const result=this._callCureAll(stateManager.state);stateManager.processUpdates(result)}_callCureAll(state){const result=[];return state.people.forEach((person=>{result.push({name:"people",action:"update",fields:{...person,bitten:!1}})})),result}};_exports.mutations=mutations})); + +//# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map new file mode 100644 index 00000000000..3c42e492b92 --- /dev/null +++ b/amd/build/metadata/mutations.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module mod_nosferatu/local/beginner/mutations\n * @class mod_nosferatu/local/beginner/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass Mutations {\n /**\n * Bite a person.\n *\n * All mutations recive a StateManager object as a first parameter. Whith this object the mutation\n * can acces the state (stateManager.state) but also set the read mode (statemanager.setReadOnly(true|false)).\n * In next steps we will see some other stateManager features. But for now you don't need them.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Number} personId the person id to bite\n */\n bite(stateManager, personId) {\n // The first thing we need to do is get the current state.\n const state = stateManager.state;\n // State is always on read mode. To change any value first we need to unlock it.\n stateManager.setReadOnly(false);\n // Now we do as many state changes as we need.\n state.people.get(personId).bitten = true;\n // All mutations should restore the read mode. This will trigger all the reactive events.\n stateManager.setReadOnly(true);\n }\n\n /**\n * The cureAll mutation.\n *\n * @param {StateManager} stateManager the current state manager\n */\n cureAll(stateManager) {\n // We call our hipotetical webservice.\n const result = this._callCureAll(stateManager.state);\n // And now we send the results to the stateManager.\n stateManager.processUpdates(result);\n }\n /**\n * Ok. we don't have a webservice yet, so we fake it.\n *\n * @param {object} state if this was a real webservice we probably won't need the full state.\n * @returns {array} the state updates object.\n */\n _callCureAll(state) {\n const result = [];\n state.people.forEach(person => {\n result.push({\n name: 'people',\n action: 'update',\n fields: {\n ...person,\n bitten: false,\n }\n });\n });\n return result;\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","bite","stateManager","personId","state","setReadOnly","people","get","bitten","cureAll","result","this","_callCureAll","processUpdates","forEach","person","push","name","action","fields"],"mappings":"gKA8EaA,UAAY;;;;;;;;;MA5CrBC,KAAKC,aAAcC,gBAETC,MAAQF,aAAaE,MAE3BF,aAAaG,aAAY,GAEzBD,MAAME,OAAOC,IAAIJ,UAAUK,QAAS,EAEpCN,aAAaG,aAAY,GAQ7BI,QAAQP,oBAEEQ,OAASC,KAAKC,aAAaV,aAAaE,OAE9CF,aAAaW,eAAeH,QAQhCE,aAAaR,aACHM,OAAS,UACfN,MAAME,OAAOQ,SAAQC,SACjBL,OAAOM,KAAK,CACRC,KAAM,SACNC,OAAQ,SACRC,OAAQ,IACDJ,OACHP,QAAQ,QAIbE"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js new file mode 100644 index 00000000000..817e1bd2867 --- /dev/null +++ b/amd/src/metadata/container.js @@ -0,0 +1,88 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * The beginner main app component. + * + * @module mod_nosferatu/local/beginner + * @class mod_nosferatu/local/beginner + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; + +export default class extends BaseComponent { + + /** + * All the component definition should be initialized on the "create" method. + */ + create() { + // This is an optional name for the debugging messages. + this.name = 'stack-metadata-app-container'; + this.selectors = { + METADATACONTAINER: `[data-for='metadata-contrib']`, + }; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } + + /** + * Initial state ready method. + * + * Note in this case we want our stateReady to be async. + * + * @param {object} state the initial state + */ + async stateReady(state) { + this._reloadCityComponent({state}); + } + + async _reloadCityComponent({state}) { + // Mustache data is not fully compatible with state object so we need to convert it + // into a plain object. In the intermediate level you will learng how to centralize this + // kind of operations to keep your components cleaner. + const data = { + people: [], + }; + state.people.forEach(person => { + data.people.push({...person}); + }); + data.haspeople = (data.people.length != 0); + + // To render a child component we need a container. + const citiyContainer = this.getElement(this.selectors.METADATACONTAINER); + if (!citiyContainer) { + throw new Error('Missing city container.'); + } + window.console.log(data); + // We store the new content into an attribute in case we want to remove it in the future. + this.cityComponent = await this.renderComponent(citiyContainer, 'qtype_stack/metadata/contributors', data); + } +} \ No newline at end of file diff --git a/amd/src/metadata/contributors.js b/amd/src/metadata/contributors.js new file mode 100644 index 00000000000..0491319b7d7 --- /dev/null +++ b/amd/src/metadata/contributors.js @@ -0,0 +1,53 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * The beginner main app component. + * + * @module mod_nosferatu/local/beginner + * @class mod_nosferatu/local/beginner + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; + +export default class extends BaseComponent { + + /** + * All the component definition should be initialized on the "create" method. + */ + create() { + // This is an optional name for the debugging messages. + this.name = 'stack-metadata-contrib'; + + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } +} \ No newline at end of file diff --git a/amd/src/metadata/events.js b/amd/src/metadata/events.js new file mode 100644 index 00000000000..2a9b3d82630 --- /dev/null +++ b/amd/src/metadata/events.js @@ -0,0 +1,56 @@ + +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +import {dispatchEvent} from 'core/event_dispatcher'; + +/** + * Javascript events for the `mod_nosferatu` activity. + * + * @module mod_nosferatu/events + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.0 + */ + +/** + * Events for the `mod_nosferatu` activity. + * + * @constant + * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated} + */ +export const eventTypes = { + /** + * Event triggered when the activity reactive state is updated. + * + * @event modNosferatuStateUpdated + * @type {CustomEvent} + * @property {Array} nodes The list of parent nodes which were updated + */ + qtypeStackStateUpdated: 'qtype_stack/stateUpdated', +}; + +/** + * Trigger an event to indicate that the activity state is updated. + * + * @method notifyModNosferatuStateUpdated + * @param {object} detail the full state + * @param {HTMLElement} container the custom event target (document if none provided) + * @returns {CustomEvent} + * @fires modNosferatuStateUpdated + */ +export const notifyQtypeStackStateUpdated = (detail, container) => { + return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container); +}; \ No newline at end of file diff --git a/amd/src/metadata/exporter.js b/amd/src/metadata/exporter.js new file mode 100644 index 00000000000..d41e5b12b24 --- /dev/null +++ b/amd/src/metadata/exporter.js @@ -0,0 +1,40 @@ +// along with Moodle. If not, see . + +/** + * Module to export parts of the state and transform them to be used in templates + * and as draggable data. + * + * @module mod_nosferatu/local/intermediate/exporter + * @class mod_nosferatu/local/intermediate/exporter + * @copyright 2022 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +export default class { + + /** + * Class constructor. + * @param {Reactive} reactive the course editor object + */ + constructor(reactive) { + this.reactive = reactive; + } + + /** + * Generate the people export data from the state. + * + * @param {Object} state the current state. + * @returns {Object} + */ + metadata(state) { + // Collect section information from the state. + const data = { + // State stores maps of key-values instead of simple arrays. + people: [], + }; + state.people.forEach(person => { + data.people.push({...person}); + }); + data.haspeople = (data.people.length != 0); + return data; + } +} \ No newline at end of file diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js new file mode 100644 index 00000000000..f93f57f2cc8 --- /dev/null +++ b/amd/src/metadata/metadata.js @@ -0,0 +1,76 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Metadata entry reactive component + * + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {Reactive} from 'core/reactive'; +import {mutations} from 'qtype_stack/metadata/mutations'; +import {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events'; + +const state = { + 'people': [ + { + id: 1, + name: 'Carlos', + bitten: false, + }, + { + id: 2, + name: 'Amaia', + bitten: false, + }, + { + id: 3, + name: 'Sara', + bitten: false, + }, + { + id: 4, + name: 'Ilya', + bitten: true, + }, + { + id: 5, + name: 'Ferran', + bitten: false, + }, + ], +}; + +class StackMetadata extends Reactive { +} + +/** + * The metadata state instance. + */ +export const metadata = new StackMetadata({ + name: 'qtype_stack_metadata', + eventName: eventTypes.qtypeStackStateUpdated, + eventDispatch: notifyQtypeStackStateUpdated, + state, + mutations, +}); + +/** + * Load the initial state. + */ +export const init = () => { + //state.metadata = JSON.parse(metadata); +}; \ No newline at end of file diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js new file mode 100644 index 00000000000..fecbcdda9e0 --- /dev/null +++ b/amd/src/metadata/mutations.js @@ -0,0 +1,79 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Default mutation manager + * + * @module mod_nosferatu/local/beginner/mutations + * @class mod_nosferatu/local/beginner/mutations + * @copyright 2021 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class Mutations { + /** + * Bite a person. + * + * All mutations recive a StateManager object as a first parameter. Whith this object the mutation + * can acces the state (stateManager.state) but also set the read mode (statemanager.setReadOnly(true|false)). + * In next steps we will see some other stateManager features. But for now you don't need them. + * + * @param {StateManager} stateManager the current state manager + * @param {Number} personId the person id to bite + */ + bite(stateManager, personId) { + // The first thing we need to do is get the current state. + const state = stateManager.state; + // State is always on read mode. To change any value first we need to unlock it. + stateManager.setReadOnly(false); + // Now we do as many state changes as we need. + state.people.get(personId).bitten = true; + // All mutations should restore the read mode. This will trigger all the reactive events. + stateManager.setReadOnly(true); + } + + /** + * The cureAll mutation. + * + * @param {StateManager} stateManager the current state manager + */ + cureAll(stateManager) { + // We call our hipotetical webservice. + const result = this._callCureAll(stateManager.state); + // And now we send the results to the stateManager. + stateManager.processUpdates(result); + } + /** + * Ok. we don't have a webservice yet, so we fake it. + * + * @param {object} state if this was a real webservice we probably won't need the full state. + * @returns {array} the state updates object. + */ + _callCureAll(state) { + const result = []; + state.people.forEach(person => { + result.push({ + name: 'people', + action: 'update', + fields: { + ...person, + bitten: false, + } + }); + }); + return result; + } +} + +export const mutations = new Mutations(); \ No newline at end of file diff --git a/classes/output/metadata.php b/classes/output/metadata.php new file mode 100644 index 00000000000..89350a46b30 --- /dev/null +++ b/classes/output/metadata.php @@ -0,0 +1,67 @@ +. + +/** + * Contains class mod_nosferatu\output\reportlink + * + * @package mod_nosferatu + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_stack\output; + +defined('MOODLE_INTERNAL') || die(); + +use qtype_stack\metadatamanager; +use renderable; +use templatable; +use renderer_base; +use stdClass; + +/** + * Class to help display report link in mod_nosferatu. + * + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class metadata implements renderable, templatable { + + + /** + * Constructor. + * + */ + public function __construct() { + global $PAGE; + + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadata', 'init'); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * Initial data shown. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + + $data = ['haspeople' => true, 'people' => ['name' => 'bob']]; + + return $data; + } +} \ No newline at end of file diff --git a/edit_stack_form.php b/edit_stack_form.php index 54d786766bc..330f66a122b 100644 --- a/edit_stack_form.php +++ b/edit_stack_form.php @@ -255,7 +255,8 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { $warnings = ($warnings) ? $warnings . '
' : $warnings; $warnings .= '' . stack_string('usetextarea'); } - + $widget = new qtype_stack\output\metadata(); + echo $OUTPUT->render($widget); // Note that for the editor elements, we are using $mform->getElement('prtincorrect')->setValue(...); instead // of setDefault, because setDefault does not work for editors. @@ -298,8 +299,8 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { if ($courseid = optional_param('courseid', 0, PARAM_INT)) { $liburlparams['courseid'] = $courseid; } - if ($cmid = optional_param('returnurl', null, PARAM_LOCALURL)) { - $liburlparams['returnurl'] = $cmid; + if ($returnurl = optional_param('returnurl', null, PARAM_LOCALURL)) { + $liburlparams['returnurl'] = $returnurl; } $qlibrarylink = html_writer::link(new moodle_url('/question/type/stack/questionlibrary.php', $liburlparams), $out, []) . ' ' . $OUTPUT->help_icon('stack_library', 'qtype_stack'); diff --git a/templates/metadata.mustache b/templates/metadata.mustache new file mode 100644 index 00000000000..60ff868df53 --- /dev/null +++ b/templates/metadata.mustache @@ -0,0 +1,32 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_nosferatu/local/beginner/city + + This component renders all people in the city. + + Example context (json): + { + } +}} +
+
+ {{> qtype_stack/metadata/contributors}} +
+
+{{#js}} +require(['qtype_stack/metadata/container'], function(component) { + component.init('#qtype-stack-metadata-main'); +}); +{{/js}} \ No newline at end of file diff --git a/templates/metadata/contributors.mustache b/templates/metadata/contributors.mustache new file mode 100644 index 00000000000..19a42134ff1 --- /dev/null +++ b/templates/metadata/contributors.mustache @@ -0,0 +1,45 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_nosferatu/local/beginner/city + + This component renders all people in the city. + + Example context (json): + { + } +}} +
+ {{^haspeople}} +
+ No contributors. +
+ {{/haspeople}} + {{#haspeople}} +
    + {{#people}} +
  • +

    + {{name}} +

    +
  • + {{/people}} +
+ {{/haspeople}} +
+{{#js}} +require(['qtype_stack/metadata/contributors'], function(component) { + component.init('#qtype-stack-metadata-contributors-{{uniqid}}'); +}); +{{/js}} \ No newline at end of file From a34f275406306cc57e21a6b4d9cb5043d821d3b6 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 24 Sep 2025 12:51:44 +0100 Subject: [PATCH 002/100] iss1171 - modal --- amd/build/metadata/metadata.min.js.map | 2 +- amd/build/metadata/metadatalaunch.min.js | 3 + amd/build/metadata/metadatalaunch.min.js.map | 1 + amd/build/metadata/metadatamodal.min.js | 3 + amd/build/metadata/metadatamodal.min.js.map | 1 + amd/build/metadatalaunch.min.js | 3 + amd/build/metadatalaunch.min.js.map | 1 + amd/src/metadata/metadata.js | 3 +- amd/src/metadata/metadatalaunch.js | 18 ++++++ amd/src/metadata/metadatamodal.js | 13 ++++ classes/output/metadatamodal.php | 67 ++++++++++++++++++++ edit_stack_form.php | 7 +- lang/en/qtype_stack.php | 1 + stackmetadatamodal.php | 0 templates/metadata.mustache | 2 +- templates/metadata/contributors.mustache | 4 +- templates/metadatamodal.mustache | 40 ++++++++++++ 17 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 amd/build/metadata/metadatalaunch.min.js create mode 100644 amd/build/metadata/metadatalaunch.min.js.map create mode 100644 amd/build/metadata/metadatamodal.min.js create mode 100644 amd/build/metadata/metadatamodal.min.js.map create mode 100644 amd/build/metadatalaunch.min.js create mode 100644 amd/build/metadatalaunch.min.js.map create mode 100644 amd/src/metadata/metadatalaunch.js create mode 100644 amd/src/metadata/metadatamodal.js create mode 100644 classes/output/metadatamodal.php create mode 100644 stackmetadatamodal.php create mode 100644 templates/metadatamodal.mustache diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map index e87322e0389..c1bbd5af8cc 100644 --- a/amd/build/metadata/metadata.min.js.map +++ b/amd/build/metadata/metadata.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nconst state = {\n 'people': [\n {\n id: 1,\n name: 'Carlos',\n bitten: false,\n },\n {\n id: 2,\n name: 'Amaia',\n bitten: false,\n },\n {\n id: 3,\n name: 'Sara',\n bitten: false,\n },\n {\n id: 4,\n name: 'Ilya',\n bitten: true,\n },\n {\n id: 5,\n name: 'Ferran',\n bitten: false,\n },\n ],\n};\n\nclass StackMetadata extends Reactive {\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n state,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n //state.metadata = JSON.parse(metadata);\n};"],"names":["StackMetadata","Reactive","metadata","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","state","id","bitten","mutations"],"mappings":"wRAwDMA,sBAAsBC,0BAMfC,SAAW,IAAIF,cAAc,CACtCG,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,MAxCU,QACA,CACN,CACIC,GAAI,EACJP,KAAM,SACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,QACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,SACNQ,QAAQ,KAgBhBC,UAAAA,gEAMgB"} \ No newline at end of file +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nconst state = {\n 'people': [\n {\n id: 1,\n name: 'Carlos',\n bitten: false,\n },\n {\n id: 2,\n name: 'Amaia',\n bitten: false,\n },\n {\n id: 3,\n name: 'Sara',\n bitten: false,\n },\n {\n id: 4,\n name: 'Ilya',\n bitten: true,\n },\n {\n id: 5,\n name: 'Ferran',\n bitten: false,\n },\n ],\n};\n\nclass StackMetadata extends Reactive {\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n state,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n //state.metadata = JSON.parse(metadata);\n};\n\n"],"names":["StackMetadata","Reactive","metadata","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","state","id","bitten","mutations"],"mappings":"wRAwDMA,sBAAsBC,0BAMfC,SAAW,IAAIF,cAAc,CACtCG,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,MAxCU,QACA,CACN,CACIC,GAAI,EACJP,KAAM,SACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,QACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,SACNQ,QAAQ,KAgBhBC,UAAAA,gEAMgB"} \ No newline at end of file diff --git a/amd/build/metadata/metadatalaunch.min.js b/amd/build/metadata/metadatalaunch.min.js new file mode 100644 index 00000000000..96350ce73c0 --- /dev/null +++ b/amd/build/metadata/metadatalaunch.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadata/metadatalaunch",["exports","core/modal_factory","./metadatamodal"],(function(_exports,_modal_factory,_metadatamodal){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_metadatamodal=_interopRequireDefault(_metadatamodal);async function openModal(){(await _modal_factory.default.create({type:_metadatamodal.default.TYPE})).show()}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal)}})); + +//# sourceMappingURL=metadatalaunch.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatalaunch.min.js.map b/amd/build/metadata/metadatalaunch.min.js.map new file mode 100644 index 00000000000..86b64c22fe8 --- /dev/null +++ b/amd/build/metadata/metadatalaunch.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatalaunch.min.js","sources":["../../src/metadata/metadatalaunch.js"],"sourcesContent":["import ModalFactory from 'core/modal_factory';\nimport MetadataModal from './metadatamodal';\n\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n};\n\n/**\n * Open the metadata modal.\n */\nasync function openModal() {\n // ...\n const modal = await ModalFactory.create({\n type: MetadataModal.TYPE,\n });\n\n modal.show();\n}"],"names":["openModal","ModalFactory","create","type","MetadataModal","TYPE","show","document","querySelector","addEventListener"],"mappings":"6aAUeA,mBAESC,uBAAaC,OAAO,CACpCC,KAAMC,uBAAcC,QAGlBC,sBAbW,6DAClBC,SAASC,cAAc,6EAAsBC,iBAAiB,QAAST"} \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js b/amd/build/metadata/metadatamodal.min.js new file mode 100644 index 00000000000..fd3839a61f6 --- /dev/null +++ b/amd/build/metadata/metadatamodal.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadata/metadatamodal",["exports","core/modal","core/modal_registry"],(function(_exports,_modal,_modal_registry){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry);class MetadataModal extends _modal.default{}_exports.default=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadatamodal");let registered=!1;return registered||(_modal_registry.default.register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE),registered=!0),_exports.default})); + +//# sourceMappingURL=metadatamodal.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js.map b/amd/build/metadata/metadatamodal.min.js.map new file mode 100644 index 00000000000..b93ae74d8ea --- /dev/null +++ b/amd/build/metadata/metadatamodal.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["import Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\n\nexport default class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadatamodal\";\n}\n\nlet registered = false;\nif (!registered) {\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n registered = true;\n}"],"names":["MetadataModal","Modal","registered","register","TYPE","TEMPLATE"],"mappings":"gjBAGqBA,sBAAsBC,+DAAtBD,qBACH,6CADGA,yBAEC,iCAGlBE,YAAa,SACZA,qCACaC,SAASH,cAAcI,KAAMJ,cAAeA,cAAcK,UACxEH,YAAa"} \ No newline at end of file diff --git a/amd/build/metadatalaunch.min.js b/amd/build/metadatalaunch.min.js new file mode 100644 index 00000000000..7075a6e851c --- /dev/null +++ b/amd/build/metadatalaunch.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadatalaunch",["exports","core/modal_factory","mod_example/my_modal"],(function(_exports,_modal_factory,_my_modal){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=void 0,_modal_factory=_interopRequireDefault(_modal_factory),_my_modal=_interopRequireDefault(_my_modal);async function openModal(){(await _modal_factory.default.create({type:_my_modal.default.TYPE})).show()}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal)}})); + +//# sourceMappingURL=metadatalaunch.min.js.map \ No newline at end of file diff --git a/amd/build/metadatalaunch.min.js.map b/amd/build/metadatalaunch.min.js.map new file mode 100644 index 00000000000..6d832e112de --- /dev/null +++ b/amd/build/metadatalaunch.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatalaunch.min.js","sources":["../src/metadatalaunch.js"],"sourcesContent":["import ModalFactory from 'core/modal_factory';\nimport MyModal from 'mod_example/my_modal';\n\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n};\n\n/**\n * Open the metadata modal.\n */\nasync function openModal() {\n // ...\n const modal = await ModalFactory.create({\n type: MyModal.TYPE,\n });\n\n modal.show();\n}"],"names":["openModal","ModalFactory","create","type","MyModal","TYPE","show","document","querySelector","addEventListener"],"mappings":"0ZAUeA,mBAESC,uBAAaC,OAAO,CACpCC,KAAMC,kBAAQC,QAGZC,sBAbW,6DAClBC,SAASC,cAAc,6EAAsBC,iBAAiB,QAAST"} \ No newline at end of file diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js index f93f57f2cc8..91febe1c741 100644 --- a/amd/src/metadata/metadata.js +++ b/amd/src/metadata/metadata.js @@ -73,4 +73,5 @@ export const metadata = new StackMetadata({ */ export const init = () => { //state.metadata = JSON.parse(metadata); -}; \ No newline at end of file +}; + diff --git a/amd/src/metadata/metadatalaunch.js b/amd/src/metadata/metadatalaunch.js new file mode 100644 index 00000000000..d7a64948326 --- /dev/null +++ b/amd/src/metadata/metadatalaunch.js @@ -0,0 +1,18 @@ +import ModalFactory from 'core/modal_factory'; +import MetadataModal from './metadatamodal'; + +export const setup = () => { + document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal); +}; + +/** + * Open the metadata modal. + */ +async function openModal() { + // ... + const modal = await ModalFactory.create({ + type: MetadataModal.TYPE, + }); + + modal.show(); +} \ No newline at end of file diff --git a/amd/src/metadata/metadatamodal.js b/amd/src/metadata/metadatamodal.js new file mode 100644 index 00000000000..1943c7bec72 --- /dev/null +++ b/amd/src/metadata/metadatamodal.js @@ -0,0 +1,13 @@ +import Modal from 'core/modal'; +import ModalRegistry from 'core/modal_registry'; + +export default class MetadataModal extends Modal { + static TYPE = "qtype_stack/metadatamodal"; + static TEMPLATE = "qtype_stack/metadatamodal"; +} + +let registered = false; +if (!registered) { + ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE); + registered = true; +} \ No newline at end of file diff --git a/classes/output/metadatamodal.php b/classes/output/metadatamodal.php new file mode 100644 index 00000000000..5e1ce78ec99 --- /dev/null +++ b/classes/output/metadatamodal.php @@ -0,0 +1,67 @@ +. + +/** + * Contains class mod_nosferatu\output\reportlink + * + * @package mod_nosferatu + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_stack\output; + +defined('MOODLE_INTERNAL') || die(); + +use qtype_stack\metadatamanager; +use renderable; +use templatable; +use renderer_base; +use stdClass; + +/** + * Class to help display report link in mod_nosferatu. + * + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class metadatamodal implements renderable, templatable { + + + /** + * Constructor. + * + */ + public function __construct() { + global $PAGE; + + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatamodal', 'init'); + } + + /** + * Export this data so it can be used as the context for a mustache template. + * Initial data shown. + * + * @param renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + + $data = ['haspeople' => true, 'people' => ['name' => 'bob']]; + + return $data; + } +} \ No newline at end of file diff --git a/edit_stack_form.php b/edit_stack_form.php index 330f66a122b..1babbe0f257 100644 --- a/edit_stack_form.php +++ b/edit_stack_form.php @@ -209,7 +209,7 @@ protected function definition() { // phpcs:ignore moodle.Commenting.MissingDocblock.Function protected function definition_inner(/* MoodleQuickForm */ $mform) { - global $OUTPUT; + global $OUTPUT, $PAGE; // Load the configuration. $this->stackconfig = stack_utils::get_config(); @@ -255,8 +255,9 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { $warnings = ($warnings) ? $warnings . '
' : $warnings; $warnings .= '' . stack_string('usetextarea'); } - $widget = new qtype_stack\output\metadata(); - echo $OUTPUT->render($widget); + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatalaunch', 'setup'); + $mform->addElement('button', 'metadatamodal', stack_string("editmetadata")); + // Note that for the editor elements, we are using $mform->getElement('prtincorrect')->setValue(...); instead // of setDefault, because setDefault does not work for editors. diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index 954a11579e3..8d2aff06d2e 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -409,6 +409,7 @@ $string['mustconfirm'] = 'You have changes to confirm.'; $string['moodleerrors'] = 'You have errors related to Moodle\'s basic question setup.'; $string['stackerrors'] = 'You have errors in your question.'; +$string['editmetadata'] = 'Edit metadata'; // Strings used by input elements. $string['studentinputtoolong'] = 'Your input is longer than permitted by STACK.'; diff --git a/stackmetadatamodal.php b/stackmetadatamodal.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/templates/metadata.mustache b/templates/metadata.mustache index 60ff868df53..2cc381b1b2d 100644 --- a/templates/metadata.mustache +++ b/templates/metadata.mustache @@ -22,7 +22,7 @@ }}
- {{> qtype_stack/metadata/contributors}} +
{{#js}} diff --git a/templates/metadata/contributors.mustache b/templates/metadata/contributors.mustache index 19a42134ff1..aa3b5d8cef4 100644 --- a/templates/metadata/contributors.mustache +++ b/templates/metadata/contributors.mustache @@ -20,7 +20,7 @@ { } }} -
+
{{^haspeople}}
No contributors. @@ -40,6 +40,6 @@
{{#js}} require(['qtype_stack/metadata/contributors'], function(component) { - component.init('#qtype-stack-metadata-contributors-{{uniqid}}'); + component.init('#qtype-stack-metadata-contributors-main'); }); {{/js}} \ No newline at end of file diff --git a/templates/metadatamodal.mustache b/templates/metadatamodal.mustache new file mode 100644 index 00000000000..e49da4fade0 --- /dev/null +++ b/templates/metadatamodal.mustache @@ -0,0 +1,40 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/modal_copytoclipboard + + This template renders a modal. The body contains a readonly input text field. + The Footer contains a button "Copy to clipboard" that copies the content + of the text field into the clipboard. + The purpose of the dialog element is to copy a permalink of a section into + the clipboard. Thus, the header of the modal dialog contains the label "Permalink". + + Example context (json): + { + "text": "content for the input field that is being copied to the clipboard" + } +}} +{{< core/modal }} + {{$title}}{{#str}} login {{/str}}{{/title}} + {{$body}} +
+ {{> qtype_stack/metadata}} +
+ {{/body}} + {{$footer}} + {{/footer}} +{{/ core/modal }} \ No newline at end of file From 575863248d4a06983cafaae26695984d6a1b7916 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 6 Nov 2025 11:59:07 +0000 Subject: [PATCH 003/100] iss1171 - Working basic display --- amd/build/metadata/container.min.js | 8 +-- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/contributors.min.js.map | 2 +- amd/build/metadata/events.min.js.map | 2 +- amd/build/metadata/metadata.min.js | 9 ++- amd/build/metadata/metadata.min.js.map | 2 +- amd/build/metadata/metadatamodal.min.js | 2 +- amd/build/metadata/metadatamodal.min.js.map | 2 +- amd/src/metadata/container.js | 56 ++++++++--------- amd/src/metadata/contributors.js | 53 ---------------- amd/src/metadata/events.js | 9 ++- amd/src/metadata/exporter.js | 40 ------------ amd/src/metadata/metadata.js | 42 ++----------- amd/src/metadata/metadatalaunch.js | 18 ------ amd/src/metadata/metadatamodal.js | 36 ++++++++++- amd/src/metadata/sample.json | 24 ++++++++ classes/output/metadata.php | 67 --------------------- classes/output/metadatamodal.php | 1 - edit_stack_form.php | 7 ++- stackmetadatamodal.php | 0 templates/metadata.mustache | 22 +++---- templates/metadata/contributors.mustache | 45 -------------- templates/metadatamodal.mustache | 13 +++- 23 files changed, 135 insertions(+), 327 deletions(-) delete mode 100644 amd/src/metadata/contributors.js delete mode 100644 amd/src/metadata/exporter.js delete mode 100644 amd/src/metadata/metadatalaunch.js create mode 100644 amd/src/metadata/sample.json delete mode 100644 classes/output/metadata.php delete mode 100644 stackmetadatamodal.php delete mode 100644 templates/metadata/contributors.mustache diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 1b33b5152df..39d0514cadc 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -1,12 +1,10 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; /** - * The beginner main app component. + * Main STACK metadata component * - * @module mod_nosferatu/local/beginner - * @class mod_nosferatu/local/beginner - * @copyright 2020 Ferran Recio + * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-app-container",this.selectors={METADATACONTAINER:"[data-for='metadata-contrib']"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){this._reloadCityComponent({state:state})}async _reloadCityComponent(_ref){let{state:state}=_ref;const data={people:[]};state.people.forEach((person=>{data.people.push({...person})})),data.haspeople=0!=data.people.length;const citiyContainer=this.getElement(this.selectors.METADATACONTAINER);if(!citiyContainer)throw new Error("Missing city container.");window.console.log(data),this.cityComponent=await this.renderComponent(citiyContainer,"qtype_stack/metadata/contributors",data)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}stateReady(state){this.reloadContainerComponent({state:state})}reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:"",isPartOf:"",additional:[]};state.contributor.forEach((contributor=>{data.contributor.push({...contributor})}));const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");this.renderComponent(metadataContainer,"qtype_stack/metadata",data)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index f03e4e554ec..e6ea2ecd476 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-app-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='metadata-contrib']`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * Note in this case we want our stateReady to be async.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n this._reloadCityComponent({state});\n }\n\n async _reloadCityComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object. In the intermediate level you will learng how to centralize this\n // kind of operations to keep your components cleaner.\n const data = {\n people: [],\n };\n state.people.forEach(person => {\n data.people.push({...person});\n });\n data.haspeople = (data.people.length != 0);\n\n // To render a child component we need a container.\n const citiyContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!citiyContainer) {\n throw new Error('Missing city container.');\n }\n window.console.log(data);\n // We store the new content into an attribute in case we want to remove it in the future.\n this.cityComponent = await this.renderComponent(citiyContainer, 'qtype_stack/metadata/contributors', data);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","target","this","element","document","querySelector","reactive","metadata","state","_reloadCityComponent","data","people","forEach","person","push","haspeople","length","citiyContainer","getElement","Error","window","console","log","cityComponent","renderComponent"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,oCACPC,UAAY,CACbC,+DAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,6BAWSS,YACRC,qBAAqB,CAACD,MAAAA,6CAGJA,MAACA,kBAIlBE,KAAO,CACTC,OAAQ,IAEZH,MAAMG,OAAOC,SAAQC,SACjBH,KAAKC,OAAOG,KAAK,IAAID,YAEzBH,KAAKK,UAAmC,GAAtBL,KAAKC,OAAOK,aAGxBC,eAAiBf,KAAKgB,WAAWhB,KAAKH,UAAUC,uBACjDiB,qBACK,IAAIE,MAAM,2BAEpBC,OAAOC,QAAQC,IAAIZ,WAEda,oBAAsBrB,KAAKsB,gBAAgBP,eAAgB,oCAAqCP"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this.reloadContainerComponent({state});\n }\n\n reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: '',\n isPartOf: '',\n additional: []\n };\n state.contributor.forEach(contributor => {\n data.contributor.push({...contributor});\n });\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n this.renderComponent(metadataContainer, 'qtype_stack/metadata', data);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","target","this","element","document","querySelector","reactive","metadata","stateReady","state","reloadContainerComponent","data","creator","contributor","language","license","isPartOf","additional","forEach","push","metadataContainer","getElement","Error","renderComponent"],"mappings":";;;;;;;uBAyB6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,mEAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,YASRS,WAAWC,YACFC,yBAAyB,CAACD,MAAAA,QAGnCC,mCAAyBD,MAACA,kBAGhBE,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAAS,GACTC,SAAU,GACVC,WAAY,IAEhBR,MAAMI,YAAYK,SAAQL,cACtBF,KAAKE,YAAYM,KAAK,IAAIN,uBAIxBO,kBAAoBlB,KAAKmB,WAAWnB,KAAKH,UAAUC,uBACpDoB,wBACK,IAAIE,MAAM,oCAEfC,gBAAgBH,kBAAmB,uBAAwBT"} \ No newline at end of file diff --git a/amd/build/metadata/contributors.min.js.map b/amd/build/metadata/contributors.min.js.map index 894ceb5f75e..f77da20d7bc 100644 --- a/amd/build/metadata/contributors.min.js.map +++ b/amd/build/metadata/contributors.min.js.map @@ -1 +1 @@ -{"version":3,"file":"contributors.min.js","sources":["../../src/metadata/contributors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-contrib';\n\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n}"],"names":["BaseComponent","create","name","target","selectors","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,qCAWJC,OAAQC,kBACT,IAAIC,KAAK,CACZC,QAASC,SAASC,cAAcL,QAChCM,SAAUC,mBACVN,UAAAA"} \ No newline at end of file +{"version":3,"file":"contributors.min.js","sources":["../../src/metadata/contributors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The beginner main app component.\n *\n * @module mod_nosferatu/local/beginner\n * @class mod_nosferatu/local/beginner\n * @copyright 2020 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n\n /**\n * All the component definition should be initialized on the \"create\" method.\n */\n create() {\n // This is an optional name for the debugging messages.\n this.name = 'stack-metadata-contrib';\n\n }\n\n /**\n * Static method to create a component instance from the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n}"],"names":["BaseComponent","create","name","target","selectors","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;;uBA2B6BA,wBAKzBC,cAESC,KAAO,qCAWJC,OAAQC,kBACT,IAAIC,KAAK,CACZC,QAASC,SAASC,cAAcL,QAChCM,SAAUC,mBACVN,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/events.min.js.map b/amd/build/metadata/events.min.js.map index 89e91e6c631..0b1d94739af 100644 --- a/amd/build/metadata/events.min.js.map +++ b/amd/build/metadata/events.min.js.map @@ -1 +1 @@ -{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["\n// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for the `mod_nosferatu` activity.\n *\n * @module mod_nosferatu/events\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\n/**\n * Events for the `mod_nosferatu` activity.\n *\n * @constant\n * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event modNosferatuStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyModNosferatuStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires modNosferatuStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;;MAiCaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file +{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for the `mod_nosferatu` activity.\n *\n * @module mod_nosferatu/events\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\n/**\n * Events for the `mod_nosferatu` activity.\n *\n * @constant\n * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event modNosferatuStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyModNosferatuStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires modNosferatuStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;;MAgCaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js b/amd/build/metadata/metadata.min.js index 2db6fb5c501..d6c68e19e37 100644 --- a/amd/build/metadata/metadata.min.js +++ b/amd/build/metadata/metadata.min.js @@ -1,3 +1,10 @@ -define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=_exports.init=void 0;class StackMetadata extends _reactive.Reactive{}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,state:{people:[{id:1,name:"Carlos",bitten:!1},{id:2,name:"Amaia",bitten:!1},{id:3,name:"Sara",bitten:!1},{id:4,name:"Ilya",bitten:!0},{id:5,name:"Ferran",bitten:!1}]},mutations:_mutations.mutations});_exports.metadata=metadata;_exports.init=()=>{}})); +define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=void 0; +/** + * Metadata entry reactive component + * + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class StackMetadata extends _reactive.Reactive{loadState(){var _metadata;let metadata=document.querySelector('input[name="metadata"]');metadata=JSON.parse(null===(_metadata=metadata)||void 0===_metadata?void 0:_metadata.value),this.setInitialState(metadata)}}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,mutations:_mutations.mutations});_exports.metadata=metadata})); //# sourceMappingURL=metadata.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map index c1bbd5af8cc..a8dcf5b5bf7 100644 --- a/amd/build/metadata/metadata.min.js.map +++ b/amd/build/metadata/metadata.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nconst state = {\n 'people': [\n {\n id: 1,\n name: 'Carlos',\n bitten: false,\n },\n {\n id: 2,\n name: 'Amaia',\n bitten: false,\n },\n {\n id: 3,\n name: 'Sara',\n bitten: false,\n },\n {\n id: 4,\n name: 'Ilya',\n bitten: true,\n },\n {\n id: 5,\n name: 'Ferran',\n bitten: false,\n },\n ],\n};\n\nclass StackMetadata extends Reactive {\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n state,\n mutations,\n});\n\n/**\n * Load the initial state.\n */\nexport const init = () => {\n //state.metadata = JSON.parse(metadata);\n};\n\n"],"names":["StackMetadata","Reactive","metadata","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","state","id","bitten","mutations"],"mappings":"wRAwDMA,sBAAsBC,0BAMfC,SAAW,IAAIF,cAAc,CACtCG,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,MAxCU,QACA,CACN,CACIC,GAAI,EACJP,KAAM,SACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,QACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,OACNQ,QAAQ,GAEZ,CACID,GAAI,EACJP,KAAM,SACNQ,QAAQ,KAgBhBC,UAAAA,gEAMgB"} \ No newline at end of file +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n metadata = JSON.parse(metadata?.value);\n this.setInitialState(metadata);\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n\n\n"],"names":["StackMetadata","Reactive","loadState","metadata","document","querySelector","JSON","parse","_metadata","value","setInitialState","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":";;;;;;;MA0BMA,sBAAsBC,mBACxBC,8BACQC,SAAWC,SAASC,cAAc,0BACtCF,SAAWG,KAAKC,wBAAMJ,qCAAAK,UAAUC,YAC3BC,gBAAgBP,iBAOhBA,SAAW,IAAIH,cAAc,CACtCW,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js b/amd/build/metadata/metadatamodal.min.js index fd3839a61f6..dcc18e63f67 100644 --- a/amd/build/metadata/metadatamodal.min.js +++ b/amd/build/metadata/metadatamodal.min.js @@ -1,3 +1,3 @@ -define("qtype_stack/metadata/metadatamodal",["exports","core/modal","core/modal_registry"],(function(_exports,_modal,_modal_registry){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry);class MetadataModal extends _modal.default{}_exports.default=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadatamodal");let registered=!1;return registered||(_modal_registry.default.register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE),registered=!0),_exports.default})); +define("qtype_stack/metadata/metadatamodal",["exports","core/modal","core/modal_registry","core/modal_factory","qtype_stack/metadata/metadata"],(function(_exports,_modal,_modal_registry,_modal_factory,_metadata){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=_exports.MetadataModal=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry),_modal_factory=_interopRequireDefault(_modal_factory);class MetadataModal extends _modal.default{}_exports.MetadataModal=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadatamodal");let registered=!1;registered||(_modal_registry.default.register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE),registered=!0);async function openModal(){(await _modal_factory.default.create({type:MetadataModal.TYPE})).show()}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal),_metadata.metadata.loadState()}})); //# sourceMappingURL=metadatamodal.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js.map b/amd/build/metadata/metadatamodal.min.js.map index b93ae74d8ea..1824a48b8d0 100644 --- a/amd/build/metadata/metadatamodal.min.js.map +++ b/amd/build/metadata/metadatamodal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["import Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\n\nexport default class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadatamodal\";\n}\n\nlet registered = false;\nif (!registered) {\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n registered = true;\n}"],"names":["MetadataModal","Modal","registered","register","TYPE","TEMPLATE"],"mappings":"gjBAGqBA,sBAAsBC,+DAAtBD,qBACH,6CADGA,yBAEC,iCAGlBE,YAAa,SACZA,qCACaC,SAASH,cAAcI,KAAMJ,cAAeA,cAAcK,UACxEH,YAAa"} \ No newline at end of file +{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\nimport ModalFactory from 'core/modal_factory';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadatamodal\";\n}\n\nlet registered = false;\nif (!registered) {\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n registered = true;\n}\n\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n metadata.loadState();\n};\n\n/**\n * Open the metadata modal.\n */\nasync function openModal() {\n // ...\n const modal = await ModalFactory.create({\n type: MetadataModal.TYPE,\n });\n\n modal.show();\n}"],"names":["MetadataModal","Modal","registered","register","TYPE","TEMPLATE","openModal","ModalFactory","create","type","show","document","querySelector","addEventListener","loadState"],"mappings":"ysBAoBaA,sBAAsBC,qEAAtBD,qBACK,6CADLA,yBAES,iCAGlBE,YAAa,EACZA,qCACaC,SAASH,cAAcI,KAAMJ,cAAeA,cAAcK,UACxEH,YAAa,kBAWFI,mBAESC,uBAAaC,OAAO,CACpCC,KAAMT,cAAcI,QAGlBM,sBAdW,6DAClBC,SAASC,cAAc,6EAAsBC,iBAAiB,QAASP,8BAC9DQ"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 817e1bd2867..252d62e0004 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -1,24 +1,22 @@ -// This file is part of Moodle - http://moodle.org/ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ // -// Moodle is free software: you can redistribute it and/or modify +// Stack is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // -// Moodle is distributed in the hope that it will be useful, +// Stack is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . +// along with Stack. If not, see . /** - * The beginner main app component. + * Main STACK metadata component * - * @module mod_nosferatu/local/beginner - * @class mod_nosferatu/local/beginner - * @copyright 2020 Ferran Recio + * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -26,15 +24,10 @@ import {BaseComponent} from 'core/reactive'; import {metadata} from 'qtype_stack/metadata/metadata'; export default class extends BaseComponent { - - /** - * All the component definition should be initialized on the "create" method. - */ create() { - // This is an optional name for the debugging messages. - this.name = 'stack-metadata-app-container'; + this.name = 'stack-metadata-container'; this.selectors = { - METADATACONTAINER: `[data-for='metadata-contrib']`, + METADATACONTAINER: `[data-for='qtype-stack-metadata']`, }; } @@ -56,33 +49,32 @@ export default class extends BaseComponent { /** * Initial state ready method. * - * Note in this case we want our stateReady to be async. - * * @param {object} state the initial state */ - async stateReady(state) { - this._reloadCityComponent({state}); + stateReady(state) { + this.reloadContainerComponent({state}); } - async _reloadCityComponent({state}) { + reloadContainerComponent({state}) { // Mustache data is not fully compatible with state object so we need to convert it - // into a plain object. In the intermediate level you will learng how to centralize this - // kind of operations to keep your components cleaner. + // into a plain object. const data = { - people: [], + creator: {}, + contributor: [], + language: [], + license: '', + isPartOf: '', + additional: [] }; - state.people.forEach(person => { - data.people.push({...person}); + state.contributor.forEach(contributor => { + data.contributor.push({...contributor}); }); - data.haspeople = (data.people.length != 0); // To render a child component we need a container. - const citiyContainer = this.getElement(this.selectors.METADATACONTAINER); - if (!citiyContainer) { - throw new Error('Missing city container.'); + const metadataContainer = this.getElement(this.selectors.METADATACONTAINER); + if (!metadataContainer) { + throw new Error('Missing metadata container.'); } - window.console.log(data); - // We store the new content into an attribute in case we want to remove it in the future. - this.cityComponent = await this.renderComponent(citiyContainer, 'qtype_stack/metadata/contributors', data); + this.renderComponent(metadataContainer, 'qtype_stack/metadata', data); } } \ No newline at end of file diff --git a/amd/src/metadata/contributors.js b/amd/src/metadata/contributors.js deleted file mode 100644 index 0491319b7d7..00000000000 --- a/amd/src/metadata/contributors.js +++ /dev/null @@ -1,53 +0,0 @@ -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * The beginner main app component. - * - * @module mod_nosferatu/local/beginner - * @class mod_nosferatu/local/beginner - * @copyright 2020 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -import {BaseComponent} from 'core/reactive'; -import {metadata} from 'qtype_stack/metadata/metadata'; - -export default class extends BaseComponent { - - /** - * All the component definition should be initialized on the "create" method. - */ - create() { - // This is an optional name for the debugging messages. - this.name = 'stack-metadata-contrib'; - - } - - /** - * Static method to create a component instance form the mustache template. - * - * @param {string} target the DOM main element or its ID - * @param {object} selectors optional css selector overrides - * @return {Component} - */ - static init(target, selectors) { - return new this({ - element: document.querySelector(target), - reactive: metadata, - selectors, - }); - } -} \ No newline at end of file diff --git a/amd/src/metadata/events.js b/amd/src/metadata/events.js index 2a9b3d82630..a1fe5fe91d8 100644 --- a/amd/src/metadata/events.js +++ b/amd/src/metadata/events.js @@ -1,18 +1,17 @@ - -// This file is part of Moodle - http://moodle.org/ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ // -// Moodle is free software: you can redistribute it and/or modify +// Stack is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // -// Moodle is distributed in the hope that it will be useful, +// Stack is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . +// along with Stack. If not, see . import {dispatchEvent} from 'core/event_dispatcher'; diff --git a/amd/src/metadata/exporter.js b/amd/src/metadata/exporter.js deleted file mode 100644 index d41e5b12b24..00000000000 --- a/amd/src/metadata/exporter.js +++ /dev/null @@ -1,40 +0,0 @@ -// along with Moodle. If not, see . - -/** - * Module to export parts of the state and transform them to be used in templates - * and as draggable data. - * - * @module mod_nosferatu/local/intermediate/exporter - * @class mod_nosferatu/local/intermediate/exporter - * @copyright 2022 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -export default class { - - /** - * Class constructor. - * @param {Reactive} reactive the course editor object - */ - constructor(reactive) { - this.reactive = reactive; - } - - /** - * Generate the people export data from the state. - * - * @param {Object} state the current state. - * @returns {Object} - */ - metadata(state) { - // Collect section information from the state. - const data = { - // State stores maps of key-values instead of simple arrays. - people: [], - }; - state.people.forEach(person => { - data.people.push({...person}); - }); - data.haspeople = (data.people.length != 0); - return data; - } -} \ No newline at end of file diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js index 91febe1c741..c94d757e3a7 100644 --- a/amd/src/metadata/metadata.js +++ b/amd/src/metadata/metadata.js @@ -24,37 +24,12 @@ import {Reactive} from 'core/reactive'; import {mutations} from 'qtype_stack/metadata/mutations'; import {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events'; -const state = { - 'people': [ - { - id: 1, - name: 'Carlos', - bitten: false, - }, - { - id: 2, - name: 'Amaia', - bitten: false, - }, - { - id: 3, - name: 'Sara', - bitten: false, - }, - { - id: 4, - name: 'Ilya', - bitten: true, - }, - { - id: 5, - name: 'Ferran', - bitten: false, - }, - ], -}; - class StackMetadata extends Reactive { + loadState() { + let metadata = document.querySelector('input[name="metadata"]'); + metadata = JSON.parse(metadata?.value); + this.setInitialState(metadata); + } } /** @@ -64,14 +39,7 @@ export const metadata = new StackMetadata({ name: 'qtype_stack_metadata', eventName: eventTypes.qtypeStackStateUpdated, eventDispatch: notifyQtypeStackStateUpdated, - state, mutations, }); -/** - * Load the initial state. - */ -export const init = () => { - //state.metadata = JSON.parse(metadata); -}; diff --git a/amd/src/metadata/metadatalaunch.js b/amd/src/metadata/metadatalaunch.js deleted file mode 100644 index d7a64948326..00000000000 --- a/amd/src/metadata/metadatalaunch.js +++ /dev/null @@ -1,18 +0,0 @@ -import ModalFactory from 'core/modal_factory'; -import MetadataModal from './metadatamodal'; - -export const setup = () => { - document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal); -}; - -/** - * Open the metadata modal. - */ -async function openModal() { - // ... - const modal = await ModalFactory.create({ - type: MetadataModal.TYPE, - }); - - modal.show(); -} \ No newline at end of file diff --git a/amd/src/metadata/metadatamodal.js b/amd/src/metadata/metadatamodal.js index 1943c7bec72..1a778efa446 100644 --- a/amd/src/metadata/metadatamodal.js +++ b/amd/src/metadata/metadatamodal.js @@ -1,7 +1,24 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + import Modal from 'core/modal'; import ModalRegistry from 'core/modal_registry'; +import ModalFactory from 'core/modal_factory'; +import {metadata} from 'qtype_stack/metadata/metadata'; -export default class MetadataModal extends Modal { +export class MetadataModal extends Modal { static TYPE = "qtype_stack/metadatamodal"; static TEMPLATE = "qtype_stack/metadatamodal"; } @@ -10,4 +27,21 @@ let registered = false; if (!registered) { ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE); registered = true; +} + +export const setup = () => { + document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal); + metadata.loadState(); +}; + +/** + * Open the metadata modal. + */ +async function openModal() { + // ... + const modal = await ModalFactory.create({ + type: MetadataModal.TYPE, + }); + + modal.show(); } \ No newline at end of file diff --git a/amd/src/metadata/sample.json b/amd/src/metadata/sample.json new file mode 100644 index 00000000000..df5d7b61b65 --- /dev/null +++ b/amd/src/metadata/sample.json @@ -0,0 +1,24 @@ +{ + "creator": { + "firstName": "Edmund", + "lastName": "Farrow", + "institution": "University of Edinburgh", + "year": 2024 + }, + "contributor": [ + { + "id": 1, + "firstName": "Chris", + "lastName": "Andrews", + "institution": "University of Mars", + "year": 2025 + }, + { + "id": 2, + "firstName": "Bob", + "lastName": "Smith", + "institution": "MIT", + "year": 2025 + } + ] +} \ No newline at end of file diff --git a/classes/output/metadata.php b/classes/output/metadata.php deleted file mode 100644 index 89350a46b30..00000000000 --- a/classes/output/metadata.php +++ /dev/null @@ -1,67 +0,0 @@ -. - -/** - * Contains class mod_nosferatu\output\reportlink - * - * @package mod_nosferatu - * @copyright 2020 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace qtype_stack\output; - -defined('MOODLE_INTERNAL') || die(); - -use qtype_stack\metadatamanager; -use renderable; -use templatable; -use renderer_base; -use stdClass; - -/** - * Class to help display report link in mod_nosferatu. - * - * @copyright 2020 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class metadata implements renderable, templatable { - - - /** - * Constructor. - * - */ - public function __construct() { - global $PAGE; - - $PAGE->requires->js_call_amd('qtype_stack/metadata/metadata', 'init'); - } - - /** - * Export this data so it can be used as the context for a mustache template. - * Initial data shown. - * - * @param renderer_base $output - * @return stdClass - */ - public function export_for_template(renderer_base $output) { - - $data = ['haspeople' => true, 'people' => ['name' => 'bob']]; - - return $data; - } -} \ No newline at end of file diff --git a/classes/output/metadatamodal.php b/classes/output/metadatamodal.php index 5e1ce78ec99..b84607dcbb2 100644 --- a/classes/output/metadatamodal.php +++ b/classes/output/metadatamodal.php @@ -47,7 +47,6 @@ class metadatamodal implements renderable, templatable { */ public function __construct() { global $PAGE; - $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatamodal', 'init'); } diff --git a/edit_stack_form.php b/edit_stack_form.php index 1babbe0f257..bac2424bc37 100644 --- a/edit_stack_form.php +++ b/edit_stack_form.php @@ -255,8 +255,13 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { $warnings = ($warnings) ? $warnings . '
' : $warnings; $warnings .= '' . stack_string('usetextarea'); } - $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatalaunch', 'setup'); + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatamodal', 'setup'); $mform->addElement('button', 'metadatamodal', stack_string("editmetadata")); + global $CFG; + $data = file_get_contents($CFG->dirroot . '/question/type/stack/amd/src/metadata/sample.json'); + $md = $mform->createElement('hidden', 'metadata', $data); + $mform->insertElementBefore($md, 'metadatamodal'); + $mform->setType('metadata', PARAM_RAW); // Note that for the editor elements, we are using $mform->getElement('prtincorrect')->setValue(...); instead // of setDefault, because setDefault does not work for editors. diff --git a/stackmetadatamodal.php b/stackmetadatamodal.php deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/templates/metadata.mustache b/templates/metadata.mustache index 2cc381b1b2d..fa7097820ac 100644 --- a/templates/metadata.mustache +++ b/templates/metadata.mustache @@ -12,21 +12,19 @@ along with Moodle. If not, see . }} {{! - @template mod_nosferatu/local/beginner/city + @template qtype_stack/metadata - This component renders all people in the city. Example context (json): { } }} -
-
- -
-
-{{#js}} -require(['qtype_stack/metadata/container'], function(component) { - component.init('#qtype-stack-metadata-main'); -}); -{{/js}} \ No newline at end of file +
+
    + {{#contributor}} +
  • + {{firstName}} {{lastName}} +
  • + {{/contributor}} +
+
\ No newline at end of file diff --git a/templates/metadata/contributors.mustache b/templates/metadata/contributors.mustache deleted file mode 100644 index aa3b5d8cef4..00000000000 --- a/templates/metadata/contributors.mustache +++ /dev/null @@ -1,45 +0,0 @@ -{{! - This file is part of Stack - http://stack.maths.ed.ac.uk/ - Moodle is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - Moodle is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Moodle. If not, see . -}} -{{! - @template mod_nosferatu/local/beginner/city - - This component renders all people in the city. - - Example context (json): - { - } -}} -
- {{^haspeople}} -
- No contributors. -
- {{/haspeople}} - {{#haspeople}} -
    - {{#people}} -
  • -

    - {{name}} -

    -
  • - {{/people}} -
- {{/haspeople}} -
-{{#js}} -require(['qtype_stack/metadata/contributors'], function(component) { - component.init('#qtype-stack-metadata-contributors-main'); -}); -{{/js}} \ No newline at end of file diff --git a/templates/metadatamodal.mustache b/templates/metadatamodal.mustache index e49da4fade0..b87ae053f77 100644 --- a/templates/metadatamodal.mustache +++ b/templates/metadatamodal.mustache @@ -31,10 +31,17 @@ {{< core/modal }} {{$title}}{{#str}} login {{/str}}{{/title}} {{$body}} -
- {{> qtype_stack/metadata}} +
+
+ +
{{/body}} {{$footer}} {{/footer}} -{{/ core/modal }} \ No newline at end of file +{{/ core/modal }} +{{#js}} +require(['qtype_stack/metadata/container'], function(component) { + component.init('#qtype-stack-metadata-main'); +}); +{{/js}} \ No newline at end of file From 65f120ca11324fcfd380908e85ff8f4b4fed1bcc Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 1 Dec 2025 10:56:51 +0000 Subject: [PATCH 004/100] iss1171 - bump version --- stack/maxima/stackmaxima.mac | 10 +++++----- version.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index e9595120562..0a4a28e2141 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -223,7 +223,7 @@ make_complexJ(OPT_COMPLEXJ) := block( /* Choose the symbol for the multiplication sign. */ make_multsgn(OPT_MULTSGN) := block( - if ev(not(elementp(OPT_MULTSGN, {"cross", "dot", "onum", "none", "space"})), simp) + if ev(not(elementp(OPT_MULTSGN, {"cross", "dot", "onum", "none", "space"})), simp) then error("make_multsgn received un-supported option: ", OPT_MULTSGN), if OPT_MULTSGN = "cross" then (texput("*", "\\times ", nary), texput("nounmul", "\\times ", nary)), @@ -1227,7 +1227,7 @@ ATAlgEquivfun(SA, SB) := block([SApoly, SBpoly, keepfloat, RawMark, FeedBack, A /* Are we dealing with lists? */ if listp(SB) then if listp(SA)#true then - if is(stackfltsep=".") then + if is(stackfltsep=".") then return(StackBasicReturn(false, false, "ATAlgEquiv_SA_not_list")) else return([false, false, StackAddNote("", "ATAlgEquiv_SA_not_list"), StackAddFeedback("", "ATAlgEquiv_SA_not_list_semi")]) @@ -1236,7 +1236,7 @@ ATAlgEquivfun(SA, SB) := block([SApoly, SBpoly, keepfloat, RawMark, FeedBack, A /* Are we dealing with sets? */ if safe_setp(SB) then if safe_setp(SA)=false then - if is(stackfltsep=".") then + if is(stackfltsep=".") then return(StackBasicReturn(false, false, "ATAlgEquiv_SA_not_set")) else return([false, false, StackAddNote("", "ATAlgEquiv_SA_not_set"), StackAddFeedback("", "ATAlgEquiv_SA_not_set_semi")]) @@ -1655,7 +1655,7 @@ ATSets(SA, SB) := block([RawMark, FeedBack, AnswerNote, SAsimp, SBsimp], if not(safe_setp(SB)) then return(StackBasicReturn(false, false, "ATSets_SB_not_set")), if not(safe_setp(SA)) then - if is(stackfltsep=".") then + if is(stackfltsep=".") then return(StackBasicReturn(false, false, "ATSets_SA_not_set")) else return([false, false, StackAddNote("", "ATSets_SA_not_set"), StackAddFeedback("", "ATSets_SA_not_set_semi")]), @@ -3523,4 +3523,4 @@ is_lang(code):=ev(is(%_STACK_LANG=code),simp=true)$ /* Stack expects some output with the version number the output happens at */ /* maximalocal.mac after additional library loading */ -stackmaximaversion:2025080100$ +stackmaximaversion:2025102200$ diff --git a/version.php b/version.php index 20846b419d8..2cbd4aa4dcb 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025080100; +$plugin->version = 2025102200; $plugin->requires = 2022041900; $plugin->cron = 0; $plugin->component = 'qtype_stack'; From acfc5c8daa2cc0a3402c2a110b907ed6decf9851 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 1 Dec 2025 15:12:33 +0000 Subject: [PATCH 005/100] iss1171 - Working reaction --- amd/build/metadata/container.min.js | 5 +- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/events.min.js | 7 +-- amd/build/metadata/events.min.js.map | 2 +- amd/build/metadata/metadata.min.js | 3 +- amd/build/metadata/metadata.min.js.map | 2 +- amd/build/metadata/metadatacontent.min.js | 11 ++++ amd/build/metadata/metadatacontent.min.js.map | 1 + amd/build/metadata/metadatamodal.min.js | 2 +- amd/build/metadata/metadatamodal.min.js.map | 2 +- amd/build/metadata/mutations.min.js | 9 ++- amd/build/metadata/mutations.min.js.map | 2 +- amd/src/metadata/container.js | 38 ++++++++++-- amd/src/metadata/events.js | 7 +-- amd/src/metadata/metadata.js | 3 +- amd/src/metadata/metadatacontent.js | 51 ++++++++++++++++ amd/src/metadata/metadatamodal.js | 19 ++++-- amd/src/metadata/mutations.js | 59 +++---------------- ...data.mustache => metadatacontent.mustache} | 18 +++++- templates/metadatamodal.mustache | 1 - 20 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 amd/build/metadata/metadatacontent.min.js create mode 100644 amd/build/metadata/metadatacontent.min.js.map create mode 100644 amd/src/metadata/metadatacontent.js rename templates/{metadata.mustache => metadatacontent.mustache} (59%) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 39d0514cadc..e317640a3f7 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -2,9 +2,10 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ /** * Main STACK metadata component * + * @module qtype_stack/metadata * @copyright 2025 University of Edinburgh - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}stateReady(state){this.reloadContainerComponent({state:state})}reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:"",isPartOf:"",additional:[]};state.contributor.forEach((contributor=>{data.contributor.push({...contributor})}));const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");this.renderComponent(metadataContainer,"qtype_stack/metadata",data)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state}),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}getWatchers(){return[{watch:"contributor:updated",handler:this.reloadContainerComponent}]}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:"",isPartOf:"",additional:[]};state.contributor.forEach((contributor=>{data.contributor.push({...contributor})}));const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data)}update(event){event.preventDefault();const first=this.getElement("#contributor-1-firstName").value,last=this.getElement("#contributor-1-lastName").value;this.reactive.dispatch("updateContributor",1,first,last)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index e6ea2ecd476..86d64e7f1d5 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n stateReady(state) {\n this.reloadContainerComponent({state});\n }\n\n reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: '',\n isPartOf: '',\n additional: []\n };\n state.contributor.forEach(contributor => {\n data.contributor.push({...contributor});\n });\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n this.renderComponent(metadataContainer, 'qtype_stack/metadata', data);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","target","this","element","document","querySelector","reactive","metadata","stateReady","state","reloadContainerComponent","data","creator","contributor","language","license","isPartOf","additional","forEach","push","metadataContainer","getElement","Error","renderComponent"],"mappings":";;;;;;;uBAyB6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,mEAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,YASRS,WAAWC,YACFC,yBAAyB,CAACD,MAAAA,QAGnCC,mCAAyBD,MAACA,kBAGhBE,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAAS,GACTC,SAAU,GACVC,WAAY,IAEhBR,MAAMI,YAAYK,SAAQL,cACtBF,KAAKE,YAAYM,KAAK,IAAIN,uBAIxBO,kBAAoBlB,KAAKmB,WAAWnB,KAAKH,UAAUC,uBACpDoB,wBACK,IAAIE,MAAM,oCAEfC,gBAAgBH,kBAAmB,uBAAwBT"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n getWatchers() {\n return [\n {watch: `contributor:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: '',\n isPartOf: '',\n additional: []\n };\n state.contributor.forEach(contributor => {\n data.contributor.push({...contributor});\n });\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n // Get the selected person id.\n const first = this.getElement('#contributor-1-firstName').value;\n const last = this.getElement('#contributor-1-lastName').value;\n this.reactive.dispatch('updateContributor', 1, first, last);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","addEventListener","getElement","update","getWatchers","watch","handler","data","creator","contributor","language","license","isPartOf","additional","forEach","push","metadataContainer","Error","renderComponent","event","preventDefault","first","value","last","dispatch"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,aAChCE,iBACDR,KAAKS,WAAWT,KAAKJ,UAAUE,QAC/B,QACAE,KAAKU,QAIbC,oBACW,CACH,CAACC,4BAA8BC,QAASb,KAAKO,oEAItBD,MAACA,kBAGtBQ,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAAS,GACTC,SAAU,GACVC,WAAY,IAEhBd,MAAMU,YAAYK,SAAQL,cACtBF,KAAKE,YAAYM,KAAK,IAAIN,uBAIxBO,kBAAoBvB,KAAKS,WAAWT,KAAKJ,UAAUC,uBACpD0B,wBACK,IAAIC,MAAM,qCAGdxB,KAAKyB,gBAAgBF,kBAAmB,8BAA+BT,MAQjFJ,OAAOgB,OAEHA,MAAMC,uBAEAC,MAAQ5B,KAAKS,WAAW,4BAA4BoB,MACpDC,KAAO9B,KAAKS,WAAW,2BAA2BoB,WACnDzB,SAAS2B,SAAS,oBAAqB,EAAGH,MAAOE"} \ No newline at end of file diff --git a/amd/build/metadata/events.min.js b/amd/build/metadata/events.min.js index 2dd87572f43..142e506d20d 100644 --- a/amd/build/metadata/events.min.js +++ b/amd/build/metadata/events.min.js @@ -2,10 +2,9 @@ define("qtype_stack/metadata/events",["exports","core/event_dispatcher"],(functi /** * Javascript events for the `mod_nosferatu` activity. * - * @module mod_nosferatu/events - * @copyright 2021 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since 4.0 + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ const eventTypes={qtypeStackStateUpdated:"qtype_stack/stateUpdated"};_exports.eventTypes=eventTypes;_exports.notifyQtypeStackStateUpdated=(detail,container)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.qtypeStackStateUpdated,detail,container)})); diff --git a/amd/build/metadata/events.min.js.map b/amd/build/metadata/events.min.js.map index 0b1d94739af..699adc1d796 100644 --- a/amd/build/metadata/events.min.js.map +++ b/amd/build/metadata/events.min.js.map @@ -1 +1 @@ -{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for the `mod_nosferatu` activity.\n *\n * @module mod_nosferatu/events\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.0\n */\n\n/**\n * Events for the `mod_nosferatu` activity.\n *\n * @constant\n * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event modNosferatuStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyModNosferatuStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires modNosferatuStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;;MAgCaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file +{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for the `mod_nosferatu` activity.\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\n/**\n * Events for the `mod_nosferatu` activity.\n *\n * @constant\n * @property {String} modNosferatuStateUpdated See {@link event:modNosferatuStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event modNosferatuStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method notifyModNosferatuStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires modNosferatuStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;MA+BaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js b/amd/build/metadata/metadata.min.js index d6c68e19e37..1a19118a889 100644 --- a/amd/build/metadata/metadata.min.js +++ b/amd/build/metadata/metadata.min.js @@ -2,8 +2,9 @@ define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/m /** * Metadata entry reactive component * + * @module qtype_stack/metadata * @copyright 2025 University of Edinburgh - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class StackMetadata extends _reactive.Reactive{loadState(){var _metadata;let metadata=document.querySelector('input[name="metadata"]');metadata=JSON.parse(null===(_metadata=metadata)||void 0===_metadata?void 0:_metadata.value),this.setInitialState(metadata)}}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,mutations:_mutations.mutations});_exports.metadata=metadata})); diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map index a8dcf5b5bf7..a4de12e6866 100644 --- a/amd/build/metadata/metadata.min.js.map +++ b/amd/build/metadata/metadata.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n metadata = JSON.parse(metadata?.value);\n this.setInitialState(metadata);\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n\n\n"],"names":["StackMetadata","Reactive","loadState","metadata","document","querySelector","JSON","parse","_metadata","value","setInitialState","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":";;;;;;;MA0BMA,sBAAsBC,mBACxBC,8BACQC,SAAWC,SAASC,cAAc,0BACtCF,SAAWG,KAAKC,wBAAMJ,qCAAAK,UAAUC,YAC3BC,gBAAgBP,iBAOhBA,SAAW,IAAIH,cAAc,CACtCW,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,UAAAA"} \ No newline at end of file +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n metadata = JSON.parse(metadata?.value);\n this.setInitialState(metadata);\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n\n\n"],"names":["StackMetadata","Reactive","loadState","metadata","document","querySelector","JSON","parse","_metadata","value","setInitialState","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":";;;;;;;;MA2BMA,sBAAsBC,mBACxBC,8BACQC,SAAWC,SAASC,cAAc,0BACtCF,SAAWG,KAAKC,wBAAMJ,qCAAAK,UAAUC,YAC3BC,gBAAgBP,iBAOhBA,SAAW,IAAIH,cAAc,CACtCW,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js b/amd/build/metadata/metadatacontent.min.js new file mode 100644 index 00000000000..e22cdaefce3 --- /dev/null +++ b/amd/build/metadata/metadatacontent.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/metadatacontent",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=metadatacontent.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js.map b/amd/build/metadata/metadatacontent.min.js.map new file mode 100644 index 00000000000..8e23b4bc6d9 --- /dev/null +++ b/amd/build/metadata/metadatacontent.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js b/amd/build/metadata/metadatamodal.min.js index dcc18e63f67..a008f6b53f6 100644 --- a/amd/build/metadata/metadatamodal.min.js +++ b/amd/build/metadata/metadatamodal.min.js @@ -1,3 +1,3 @@ -define("qtype_stack/metadata/metadatamodal",["exports","core/modal","core/modal_registry","core/modal_factory","qtype_stack/metadata/metadata"],(function(_exports,_modal,_modal_registry,_modal_factory,_metadata){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=_exports.MetadataModal=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry),_modal_factory=_interopRequireDefault(_modal_factory);class MetadataModal extends _modal.default{}_exports.MetadataModal=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadatamodal");let registered=!1;registered||(_modal_registry.default.register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE),registered=!0);async function openModal(){(await _modal_factory.default.create({type:MetadataModal.TYPE})).show()}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal),_metadata.metadata.loadState()}})); +define("qtype_stack/metadata/metadatamodal",["exports","core/modal","core/modal_registry","core/modal_factory","qtype_stack/metadata/metadata"],(function(_exports,_modal,_modal_registry,_modal_factory,_metadata){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=_exports.MetadataModal=void 0,_modal=_interopRequireDefault(_modal),_modal_registry=_interopRequireDefault(_modal_registry),_modal_factory=_interopRequireDefault(_modal_factory);class MetadataModal extends _modal.default{}_exports.MetadataModal=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadatamodal");let registered=!1;registered||(_modal_registry.default.register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE),registered=!0);let modal=null;async function openModal(){modal||(modal=await _modal_factory.default.create({type:MetadataModal.TYPE})),modal.show()}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal),_metadata.metadata.loadState()}})); //# sourceMappingURL=metadatamodal.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js.map b/amd/build/metadata/metadatamodal.min.js.map index 1824a48b8d0..b747d54cef7 100644 --- a/amd/build/metadata/metadatamodal.min.js.map +++ b/amd/build/metadata/metadatamodal.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\nimport ModalFactory from 'core/modal_factory';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadatamodal\";\n}\n\nlet registered = false;\nif (!registered) {\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n registered = true;\n}\n\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n metadata.loadState();\n};\n\n/**\n * Open the metadata modal.\n */\nasync function openModal() {\n // ...\n const modal = await ModalFactory.create({\n type: MetadataModal.TYPE,\n });\n\n modal.show();\n}"],"names":["MetadataModal","Modal","registered","register","TYPE","TEMPLATE","openModal","ModalFactory","create","type","show","document","querySelector","addEventListener","loadState"],"mappings":"ysBAoBaA,sBAAsBC,qEAAtBD,qBACK,6CADLA,yBAES,iCAGlBE,YAAa,EACZA,qCACaC,SAASH,cAAcI,KAAMJ,cAAeA,cAAcK,UACxEH,YAAa,kBAWFI,mBAESC,uBAAaC,OAAO,CACpCC,KAAMT,cAAcI,QAGlBM,sBAdW,6DAClBC,SAASC,cAAc,6EAAsBC,iBAAiB,QAASP,8BAC9DQ"} \ No newline at end of file +{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * STACK metadata modal setup\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport Modal from 'core/modal';\nimport ModalRegistry from 'core/modal_registry';\nimport ModalFactory from 'core/modal_factory';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadatamodal\";\n}\n\nlet registered = false;\nif (!registered) {\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n registered = true;\n}\n\nlet modal = null;\n\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n metadata.loadState();\n};\n\n/**\n * Open the metadata modal.\n */\nasync function openModal() {\n if (!modal) {\n modal = await ModalFactory.create({\n type: MetadataModal.TYPE,\n });\n }\n\n modal.show();\n}"],"names":["MetadataModal","Modal","registered","register","TYPE","TEMPLATE","modal","openModal","ModalFactory","create","type","show","document","querySelector","addEventListener","loadState"],"mappings":"ysBA4BaA,sBAAsBC,qEAAtBD,qBACK,6CADLA,yBAES,iCAGlBE,YAAa,EACZA,qCACaC,SAASH,cAAcI,KAAMJ,cAAeA,cAAcK,UACxEH,YAAa,OAGbI,MAAQ,oBAUGC,YACND,QACDA,YAAcE,uBAAaC,OAAO,CAC9BC,KAAMV,cAAcI,QAI5BE,MAAMK,sBAfW,6DAClBC,SAASC,cAAc,6EAAsBC,iBAAiB,QAASP,8BAC9DQ"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js index df36d6b1103..4ae817100ad 100644 --- a/amd/build/metadata/mutations.min.js +++ b/amd/build/metadata/mutations.min.js @@ -2,11 +2,10 @@ define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.d /** * Default mutation manager * - * @module mod_nosferatu/local/beginner/mutations - * @class mod_nosferatu/local/beginner/mutations - * @copyright 2021 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class{bite(stateManager,personId){const state=stateManager.state;stateManager.setReadOnly(!1),state.people.get(personId).bitten=!0,stateManager.setReadOnly(!0)}cureAll(stateManager){const result=this._callCureAll(stateManager.state);stateManager.processUpdates(result)}_callCureAll(state){const result=[];return state.people.forEach((person=>{result.push({name:"people",action:"update",fields:{...person,bitten:!1}})})),result}};_exports.mutations=mutations})); +class{updateContributor(stateManager,id,firstName,LastName){const state=stateManager.state;stateManager.setReadOnly(!1),state.contributor.get(id).firstName=firstName,state.contributor.get(id).LastName=LastName,stateManager.setReadOnly(!0),console.log(state)}};_exports.mutations=mutations})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map index 3c42e492b92..1f911254e81 100644 --- a/amd/build/metadata/mutations.min.js.map +++ b/amd/build/metadata/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module mod_nosferatu/local/beginner/mutations\n * @class mod_nosferatu/local/beginner/mutations\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass Mutations {\n /**\n * Bite a person.\n *\n * All mutations recive a StateManager object as a first parameter. Whith this object the mutation\n * can acces the state (stateManager.state) but also set the read mode (statemanager.setReadOnly(true|false)).\n * In next steps we will see some other stateManager features. But for now you don't need them.\n *\n * @param {StateManager} stateManager the current state manager\n * @param {Number} personId the person id to bite\n */\n bite(stateManager, personId) {\n // The first thing we need to do is get the current state.\n const state = stateManager.state;\n // State is always on read mode. To change any value first we need to unlock it.\n stateManager.setReadOnly(false);\n // Now we do as many state changes as we need.\n state.people.get(personId).bitten = true;\n // All mutations should restore the read mode. This will trigger all the reactive events.\n stateManager.setReadOnly(true);\n }\n\n /**\n * The cureAll mutation.\n *\n * @param {StateManager} stateManager the current state manager\n */\n cureAll(stateManager) {\n // We call our hipotetical webservice.\n const result = this._callCureAll(stateManager.state);\n // And now we send the results to the stateManager.\n stateManager.processUpdates(result);\n }\n /**\n * Ok. we don't have a webservice yet, so we fake it.\n *\n * @param {object} state if this was a real webservice we probably won't need the full state.\n * @returns {array} the state updates object.\n */\n _callCureAll(state) {\n const result = [];\n state.people.forEach(person => {\n result.push({\n name: 'people',\n action: 'update',\n fields: {\n ...person,\n bitten: false,\n }\n });\n });\n return result;\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","bite","stateManager","personId","state","setReadOnly","people","get","bitten","cureAll","result","this","_callCureAll","processUpdates","forEach","person","push","name","action","fields"],"mappings":"gKA8EaA,UAAY;;;;;;;;;MA5CrBC,KAAKC,aAAcC,gBAETC,MAAQF,aAAaE,MAE3BF,aAAaG,aAAY,GAEzBD,MAAME,OAAOC,IAAIJ,UAAUK,QAAS,EAEpCN,aAAaG,aAAY,GAQ7BI,QAAQP,oBAEEQ,OAASC,KAAKC,aAAaV,aAAaE,OAE9CF,aAAaW,eAAeH,QAQhCE,aAAaR,aACHM,OAAS,UACfN,MAAME,OAAOQ,SAAQC,SACjBL,OAAOM,KAAK,CACRC,KAAM,SACNC,OAAQ,SACRC,OAAQ,IACDJ,OACHP,QAAQ,QAIbE"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateContributor(stateManager, id, firstName, LastName) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.contributor.get(id).firstName = firstName;\n state.contributor.get(id).LastName = LastName;\n stateManager.setReadOnly(true);\n console.log(state);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateContributor","stateManager","id","firstName","LastName","state","setReadOnly","contributor","get","console","log"],"mappings":"gKAiCaA,UAAY;;;;;;;;MAVrBC,kBAAkBC,aAAcC,GAAIC,UAAWC,gBACrCC,MAAQJ,aAAaI,MAC3BJ,aAAaK,aAAY,GACzBD,MAAME,YAAYC,IAAIN,IAAIC,UAAYA,UACtCE,MAAME,YAAYC,IAAIN,IAAIE,SAAWA,SACrCH,aAAaK,aAAY,GACzBG,QAAQC,IAAIL"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 252d62e0004..1885f08b900 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -16,8 +16,9 @@ /** * Main STACK metadata component * + * @module qtype_stack/metadata * @copyright 2025 University of Edinburgh - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ import {BaseComponent} from 'core/reactive'; @@ -28,6 +29,7 @@ export default class extends BaseComponent { this.name = 'stack-metadata-container'; this.selectors = { METADATACONTAINER: `[data-for='qtype-stack-metadata']`, + SUBMIT: `#stack-metadata-update`, }; } @@ -51,11 +53,22 @@ export default class extends BaseComponent { * * @param {object} state the initial state */ - stateReady(state) { - this.reloadContainerComponent({state}); + async stateReady(state) { + await this.reloadContainerComponent({state}); + this.addEventListener( + this.getElement(this.selectors.SUBMIT), + 'click', + this.update + ); } - reloadContainerComponent({state}) { + getWatchers() { + return [ + {watch: `contributor:updated`, handler: this.reloadContainerComponent}, + ]; + } + + async reloadContainerComponent({state}) { // Mustache data is not fully compatible with state object so we need to convert it // into a plain object. const data = { @@ -75,6 +88,21 @@ export default class extends BaseComponent { if (!metadataContainer) { throw new Error('Missing metadata container.'); } - this.renderComponent(metadataContainer, 'qtype_stack/metadata', data); + + await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data); + } + + /** + * Our submit handler. + * + * @param {Event} event the click event + */ + update(event) { + // We don't want to submit the form. + event.preventDefault(); + // Get the selected person id. + const first = this.getElement('#contributor-1-firstName').value; + const last = this.getElement('#contributor-1-lastName').value; + this.reactive.dispatch('updateContributor', 1, first, last); } } \ No newline at end of file diff --git a/amd/src/metadata/events.js b/amd/src/metadata/events.js index a1fe5fe91d8..8b5d9b049f6 100644 --- a/amd/src/metadata/events.js +++ b/amd/src/metadata/events.js @@ -18,10 +18,9 @@ import {dispatchEvent} from 'core/event_dispatcher'; /** * Javascript events for the `mod_nosferatu` activity. * - * @module mod_nosferatu/events - * @copyright 2021 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since 4.0 + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ /** diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js index c94d757e3a7..a8077775b8d 100644 --- a/amd/src/metadata/metadata.js +++ b/amd/src/metadata/metadata.js @@ -16,8 +16,9 @@ /** * Metadata entry reactive component * + * @module qtype_stack/metadata * @copyright 2025 University of Edinburgh - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ import {Reactive} from 'core/reactive'; diff --git a/amd/src/metadata/metadatacontent.js b/amd/src/metadata/metadatacontent.js new file mode 100644 index 00000000000..7caddf56840 --- /dev/null +++ b/amd/src/metadata/metadatacontent.js @@ -0,0 +1,51 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; + +export default class extends BaseComponent { + create() { + this.name = 'stack-metadata-content'; + this.selectors = { + METADATACONTAINER: `[data-for='qtype-stack-metadata']`, + SUBMIT: `#stack-metadata-update`, + }; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } + +} \ No newline at end of file diff --git a/amd/src/metadata/metadatamodal.js b/amd/src/metadata/metadatamodal.js index 1a778efa446..1b7c7aaa968 100644 --- a/amd/src/metadata/metadatamodal.js +++ b/amd/src/metadata/metadatamodal.js @@ -13,6 +13,14 @@ // You should have received a copy of the GNU General Public License // along with Stack. If not, see . +/** + * STACK metadata modal setup + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + import Modal from 'core/modal'; import ModalRegistry from 'core/modal_registry'; import ModalFactory from 'core/modal_factory'; @@ -29,6 +37,8 @@ if (!registered) { registered = true; } +let modal = null; + export const setup = () => { document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal); metadata.loadState(); @@ -38,10 +48,11 @@ export const setup = () => { * Open the metadata modal. */ async function openModal() { - // ... - const modal = await ModalFactory.create({ - type: MetadataModal.TYPE, - }); + if (!modal) { + modal = await ModalFactory.create({ + type: MetadataModal.TYPE, + }); + } modal.show(); } \ No newline at end of file diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js index fecbcdda9e0..245ecab03be 100644 --- a/amd/src/metadata/mutations.js +++ b/amd/src/metadata/mutations.js @@ -16,63 +16,18 @@ /** * Default mutation manager * - * @module mod_nosferatu/local/beginner/mutations - * @class mod_nosferatu/local/beginner/mutations - * @copyright 2021 Ferran Recio - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class Mutations { - /** - * Bite a person. - * - * All mutations recive a StateManager object as a first parameter. Whith this object the mutation - * can acces the state (stateManager.state) but also set the read mode (statemanager.setReadOnly(true|false)). - * In next steps we will see some other stateManager features. But for now you don't need them. - * - * @param {StateManager} stateManager the current state manager - * @param {Number} personId the person id to bite - */ - bite(stateManager, personId) { - // The first thing we need to do is get the current state. + updateContributor(stateManager, id, firstName, LastName) { const state = stateManager.state; - // State is always on read mode. To change any value first we need to unlock it. stateManager.setReadOnly(false); - // Now we do as many state changes as we need. - state.people.get(personId).bitten = true; - // All mutations should restore the read mode. This will trigger all the reactive events. + state.contributor.get(id).firstName = firstName; + state.contributor.get(id).LastName = LastName; stateManager.setReadOnly(true); - } - - /** - * The cureAll mutation. - * - * @param {StateManager} stateManager the current state manager - */ - cureAll(stateManager) { - // We call our hipotetical webservice. - const result = this._callCureAll(stateManager.state); - // And now we send the results to the stateManager. - stateManager.processUpdates(result); - } - /** - * Ok. we don't have a webservice yet, so we fake it. - * - * @param {object} state if this was a real webservice we probably won't need the full state. - * @returns {array} the state updates object. - */ - _callCureAll(state) { - const result = []; - state.people.forEach(person => { - result.push({ - name: 'people', - action: 'update', - fields: { - ...person, - bitten: false, - } - }); - }); - return result; + console.log(state); } } diff --git a/templates/metadata.mustache b/templates/metadatacontent.mustache similarity index 59% rename from templates/metadata.mustache rename to templates/metadatacontent.mustache index fa7097820ac..2173e6da9a1 100644 --- a/templates/metadata.mustache +++ b/templates/metadatacontent.mustache @@ -19,12 +19,26 @@ { } }} -
+
    {{#contributor}}
  • {{firstName}} {{lastName}} + +
  • {{/contributor}}
-
\ No newline at end of file +
+
+ +
+
+
+{{#js}} +require(['qtype_stack/metadata/metadatacontent'], function(component) { + component.init('#qtype-stack-metadata-content'); +}); +{{/js}} \ No newline at end of file diff --git a/templates/metadatamodal.mustache b/templates/metadatamodal.mustache index b87ae053f77..628f5cf5f4b 100644 --- a/templates/metadatamodal.mustache +++ b/templates/metadatamodal.mustache @@ -33,7 +33,6 @@ {{$body}}
-
{{/body}} From 87bcb1b765187e912f66627119c58fdf61cae23b Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 1 Dec 2025 17:16:36 +0000 Subject: [PATCH 006/100] iss1171 - Display --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/mutations.min.js | 2 +- amd/build/metadata/mutations.min.js.map | 2 +- amd/src/metadata/container.js | 14 +++++++---- amd/src/metadata/mutations.js | 1 - amd/src/metadata/sample.json | 24 +++++++++++++++++++ lang/en/qtype_stack.php | 7 ++++++ styles.css | 9 ++++++- templates/metadatacontent.mustache | 32 ++++++++++++++++++------- templates/metadatamodal.mustache | 3 ++- 11 files changed, 78 insertions(+), 20 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index e317640a3f7..ce024847f72 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state}),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}getWatchers(){return[{watch:"contributor:updated",handler:this.reloadContainerComponent}]}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:"",isPartOf:"",additional:[]};state.contributor.forEach((contributor=>{data.contributor.push({...contributor})}));const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data)}update(event){event.preventDefault();const first=this.getElement("#contributor-1-firstName").value,last=this.getElement("#contributor-1-lastName").value;this.reactive.dispatch("updateContributor",1,first,last)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state}),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}getWatchers(){return[{watch:"*:updated",handler:this.reloadContainerComponent}]}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:state.creator,contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{data.contributor.push({...contributor})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})}));const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data)}update(event){event.preventDefault();const first=this.getElement("#contributor-1-firstName").value,last=this.getElement("#contributor-1-lastName").value;this.reactive.dispatch("updateContributor",1,first,last)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index 86d64e7f1d5..bd8bbd91e5b 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n getWatchers() {\n return [\n {watch: `contributor:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: '',\n isPartOf: '',\n additional: []\n };\n state.contributor.forEach(contributor => {\n data.contributor.push({...contributor});\n });\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n // Get the selected person id.\n const first = this.getElement('#contributor-1-firstName').value;\n const last = this.getElement('#contributor-1-lastName').value;\n this.reactive.dispatch('updateContributor', 1, first, last);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","addEventListener","getElement","update","getWatchers","watch","handler","data","creator","contributor","language","license","isPartOf","additional","forEach","push","metadataContainer","Error","renderComponent","event","preventDefault","first","value","last","dispatch"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,aAChCE,iBACDR,KAAKS,WAAWT,KAAKJ,UAAUE,QAC/B,QACAE,KAAKU,QAIbC,oBACW,CACH,CAACC,4BAA8BC,QAASb,KAAKO,oEAItBD,MAACA,kBAGtBQ,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAAS,GACTC,SAAU,GACVC,WAAY,IAEhBd,MAAMU,YAAYK,SAAQL,cACtBF,KAAKE,YAAYM,KAAK,IAAIN,uBAIxBO,kBAAoBvB,KAAKS,WAAWT,KAAKJ,UAAUC,uBACpD0B,wBACK,IAAIC,MAAM,qCAGdxB,KAAKyB,gBAAgBF,kBAAmB,8BAA+BT,MAQjFJ,OAAOgB,OAEHA,MAAMC,uBAEAC,MAAQ5B,KAAKS,WAAW,4BAA4BoB,MACpDC,KAAO9B,KAAKS,WAAW,2BAA2BoB,WACnDzB,SAAS2B,SAAS,oBAAqB,EAAGH,MAAOE"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n getWatchers() {\n return [\n {watch: `*:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: state.creator,\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n data.contributor.push({...contributor});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n // Get the selected person id.\n const first = this.getElement('#contributor-1-firstName').value;\n const last = this.getElement('#contributor-1-lastName').value;\n this.reactive.dispatch('updateContributor', 1, first, last);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","addEventListener","getElement","update","getWatchers","watch","handler","data","creator","contributor","language","license","isPartOf","additional","forEach","push","metadataContainer","Error","renderComponent","event","preventDefault","first","value","last","dispatch"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,aAChCE,iBACDR,KAAKS,WAAWT,KAAKJ,UAAUE,QAC/B,QACAE,KAAKU,QAIbC,oBACW,CACH,CAACC,kBAAoBC,QAASb,KAAKO,oEAIZD,MAACA,kBAGtBQ,KAAO,CACTC,QAAST,MAAMS,QACfC,YAAa,GACbC,SAAU,GACVC,QAASZ,MAAMY,QACfC,SAAUb,MAAMa,SAChBC,WAAY,IAEhBd,MAAMU,YAAYK,SAAQL,cACtBF,KAAKE,YAAYM,KAAK,IAAIN,iBAE9BV,MAAMW,SAASI,SAAQJ,WACnBH,KAAKG,SAASK,KAAK,IAAIL,cAE3BX,MAAMc,WAAWC,SAAQD,aACrBN,KAAKM,WAAWE,KAAK,IAAIF,sBAIvBG,kBAAoBvB,KAAKS,WAAWT,KAAKJ,UAAUC,uBACpD0B,wBACK,IAAIC,MAAM,qCAGdxB,KAAKyB,gBAAgBF,kBAAmB,8BAA+BT,MAQjFJ,OAAOgB,OAEHA,MAAMC,uBAEAC,MAAQ5B,KAAKS,WAAW,4BAA4BoB,MACpDC,KAAO9B,KAAKS,WAAW,2BAA2BoB,WACnDzB,SAAS2B,SAAS,oBAAqB,EAAGH,MAAOE"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js index 4ae817100ad..a1c0a8226b0 100644 --- a/amd/build/metadata/mutations.min.js +++ b/amd/build/metadata/mutations.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.d * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class{updateContributor(stateManager,id,firstName,LastName){const state=stateManager.state;stateManager.setReadOnly(!1),state.contributor.get(id).firstName=firstName,state.contributor.get(id).LastName=LastName,stateManager.setReadOnly(!0),console.log(state)}};_exports.mutations=mutations})); +class{updateContributor(stateManager,id,firstName,LastName){const state=stateManager.state;stateManager.setReadOnly(!1),state.contributor.get(id).firstName=firstName,state.contributor.get(id).LastName=LastName,stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map index 1f911254e81..a2b19b4d18a 100644 --- a/amd/build/metadata/mutations.min.js.map +++ b/amd/build/metadata/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateContributor(stateManager, id, firstName, LastName) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.contributor.get(id).firstName = firstName;\n state.contributor.get(id).LastName = LastName;\n stateManager.setReadOnly(true);\n console.log(state);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateContributor","stateManager","id","firstName","LastName","state","setReadOnly","contributor","get","console","log"],"mappings":"gKAiCaA,UAAY;;;;;;;;MAVrBC,kBAAkBC,aAAcC,GAAIC,UAAWC,gBACrCC,MAAQJ,aAAaI,MAC3BJ,aAAaK,aAAY,GACzBD,MAAME,YAAYC,IAAIN,IAAIC,UAAYA,UACtCE,MAAME,YAAYC,IAAIN,IAAIE,SAAWA,SACrCH,aAAaK,aAAY,GACzBG,QAAQC,IAAIL"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateContributor(stateManager, id, firstName, LastName) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.contributor.get(id).firstName = firstName;\n state.contributor.get(id).LastName = LastName;\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateContributor","stateManager","id","firstName","LastName","state","setReadOnly","contributor","get"],"mappings":"gKAgCaA,UAAY;;;;;;;;MATrBC,kBAAkBC,aAAcC,GAAIC,UAAWC,gBACrCC,MAAQJ,aAAaI,MAC3BJ,aAAaK,aAAY,GACzBD,MAAME,YAAYC,IAAIN,IAAIC,UAAYA,UACtCE,MAAME,YAAYC,IAAIN,IAAIE,SAAWA,SACrCH,aAAaK,aAAY"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 1885f08b900..26a0d91ec3d 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -64,7 +64,7 @@ export default class extends BaseComponent { getWatchers() { return [ - {watch: `contributor:updated`, handler: this.reloadContainerComponent}, + {watch: `*:updated`, handler: this.reloadContainerComponent}, ]; } @@ -72,16 +72,22 @@ export default class extends BaseComponent { // Mustache data is not fully compatible with state object so we need to convert it // into a plain object. const data = { - creator: {}, + creator: state.creator, contributor: [], language: [], - license: '', - isPartOf: '', + license: state.license, + isPartOf: state.isPartOf, additional: [] }; state.contributor.forEach(contributor => { data.contributor.push({...contributor}); }); + state.language.forEach(language => { + data.language.push({...language}); + }); + state.additional.forEach(additional => { + data.additional.push({...additional}); + }); // To render a child component we need a container. const metadataContainer = this.getElement(this.selectors.METADATACONTAINER); diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js index 245ecab03be..c477bacfd0f 100644 --- a/amd/src/metadata/mutations.js +++ b/amd/src/metadata/mutations.js @@ -27,7 +27,6 @@ class Mutations { state.contributor.get(id).firstName = firstName; state.contributor.get(id).LastName = LastName; stateManager.setReadOnly(true); - console.log(state); } } diff --git a/amd/src/metadata/sample.json b/amd/src/metadata/sample.json index df5d7b61b65..73d47bec60e 100644 --- a/amd/src/metadata/sample.json +++ b/amd/src/metadata/sample.json @@ -20,5 +20,29 @@ "institution": "MIT", "year": 2025 } + ], + "language": [ + { + "id": "en" + }, + { + "id": "fr" + } + ], + "license": { + "value": "Creative Commons - NoDerivatives 4.0 International" + }, + "isPartOf": { + "value": "HELM" + }, + "additional": [ + { + "id": 1, + "scope": "EdmundsData", + "property": "tags", + "qualifier": "colour", + "value": "red" + } ] + } \ No newline at end of file diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index 8d2aff06d2e..93be28029e7 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -410,6 +410,13 @@ $string['moodleerrors'] = 'You have errors related to Moodle\'s basic question setup.'; $string['stackerrors'] = 'You have errors in your question.'; $string['editmetadata'] = 'Edit metadata'; +$string['metadata'] = 'Metadata'; +$string['firstname'] = 'First name'; +$string['lastname'] = 'Last name'; +$string['institution'] = 'Institution'; +$string['year'] = 'Year'; +$string['contributor'] = 'Contributor'; +$string['creator'] = 'Creator'; // Strings used by input elements. $string['studentinputtoolong'] = 'Your input is longer than permitted by STACK.'; diff --git a/styles.css b/styles.css index fc2ae0809a3..2ae565c74ca 100644 --- a/styles.css +++ b/styles.css @@ -1278,4 +1278,11 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. .que .outcome, .que .comment { color: #44331b; -} \ No newline at end of file +} +.stack-metadata input { + width: 98%; +} +.stack-metadata .inputcol { + padding-left: 0px; + padding-right: 0px; +} diff --git a/templates/metadatacontent.mustache b/templates/metadatacontent.mustache index 2173e6da9a1..b1548e95131 100644 --- a/templates/metadatacontent.mustache +++ b/templates/metadatacontent.mustache @@ -20,15 +20,29 @@ } }}
-
    - {{#contributor}} -
  • - {{firstName}} {{lastName}} - - -
  • - {{/contributor}} -
+
+
+
{{#str}} firstname, qtype_stack {{/str}}
+
{{#str}} lastname, qtype_stack {{/str}}
+
{{#str}} institution, qtype_stack {{/str}}
+
{{#str}} year, qtype_stack {{/str}}
+
+
+
{{#str}} creator, qtype_stack {{/str}}
+
+
+
+
+
+ {{#contributor}} +
+
{{#str}} contributor, qtype_stack {{/str}}
+
+
+
+
+
+ {{/contributor}}
{{/contributor}} -
-
- +
+
+
+
+
+
+
+ +
-
+
+
{{#str}} contributor, qtype_stack {{/str}}
+
{{#json}}{{> core_form/element-textarea }}{{/json}}
+
+ {{#js}} require(['qtype_stack/metadata/metadatacontent'], function(component) { component.init('#qtype-stack-metadata-content'); From bb06d38aff7becbff5830eab9174eaf6a87b00b5 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 3 Dec 2025 16:12:13 +0000 Subject: [PATCH 008/100] iss1171 - Update multiple --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/mutations.min.js | 2 +- amd/build/metadata/mutations.min.js.map | 2 +- amd/src/metadata/container.js | 107 ++++++++---------------- amd/src/metadata/mutations.js | 22 +++-- lang/en/qtype_stack.php | 1 + templates/metadatacontent.mustache | 41 ++++++--- 8 files changed, 89 insertions(+), 90 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index e982c320176..1304426691c 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state}),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}getWatchers(){return[{watch:"contributor:updated",handler:this.reloadContainerComponent}]}async reloadContainerComponent(_ref){let{state:state}=_ref;console.log("Refresh");const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[],element:[]};state.contributor.forEach((contributor=>{const element={firstname:{required:!1,element:{hiddenlabel:"Contributor first name",value:contributor.firstName,wrapperid:"fitem_"+contributor.id+"_confirstname",id:"id_"+contributor.id+"_confirstname",name:contributor.id+"_confirstname"}},lastname:{required:!0,element:{hiddenlabel:"Contributor last name",value:contributor.lastName,wrapperid:"fitem_"+contributor.id+"_conlastname",id:"id_"+contributor.id+"_conlastname",name:contributor.id+"_conlastname"}},institution:{required:!1,element:{hiddenlabel:"Contributor institution",value:contributor.institution,wrapperid:"fitem_"+contributor.id+"_coninstitution",id:"id_"+contributor.id+"_coninstitution",name:contributor.id+"_coninstitution"}},year:{required:!1,element:{hiddenlabel:"Contributor year",value:contributor.year,wrapperid:"fitem_"+contributor.id+"_conyear",id:"id_"+contributor.id+"_conyear",name:contributor.id+"_conyear"}}};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.element.push({hiddenlabel:"Course full name",required:!0,element:{wrapperid:"fitem_id_fullname",id:"id_fullname",name:"fullname"}}),data.creator=data.contributor[0],data.json={required:!0,element:{hiddenlabel:"JSON metadata",value:JSON.stringify(state),wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data)}update(event){event.preventDefault();const elements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of elements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const first=this.getElement("#id_2_confirstname").value,last=this.getElement("#id_2_conlastname").value;this.reactive.dispatch("updateContributor",2,first,last)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!0,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year)};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!0,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:JSON.stringify(state),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}update(event){event.preventDefault();const requiredElements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of requiredElements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");let inputElements=this.getElements('#qtype-stack-metadata-content input[id^="smdi"]');inputElements=Array.from(inputElements).map((el=>[el.id,el.value])),this.reactive.dispatch("updateAll",inputElements)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index 2d244fb2e62..af9cda7c680 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n getWatchers() {\n return [\n {watch: `contributor:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n async reloadContainerComponent({state}) {\n console.log('Refresh');\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const element = {\n hiddenlabel: \"Course full name\",\n required: true,\n element: {\n wrapperid: \"fitem_id_fullname\",\n id: \"id_fullname\",\n name: \"fullname\"\n }\n };\n\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: [],\n element: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: {\n required: false,\n element: {\n hiddenlabel: \"Contributor first name\",\n value: contributor.firstName,\n wrapperid: \"fitem_\" + contributor.id + \"_confirstname\",\n id: 'id_' + contributor.id + \"_confirstname\",\n name: contributor.id + \"_confirstname\",\n }\n },\n lastname: {\n required: true,\n element: {\n hiddenlabel: \"Contributor last name\",\n value: contributor.lastName,\n wrapperid: \"fitem_\" + contributor.id + \"_conlastname\",\n id: 'id_' + contributor.id + \"_conlastname\",\n name: contributor.id + \"_conlastname\",\n }\n },\n institution: {\n required: false,\n element: {\n hiddenlabel: \"Contributor institution\",\n value: contributor.institution,\n wrapperid: \"fitem_\" + contributor.id + \"_coninstitution\",\n id: 'id_' + contributor.id + \"_coninstitution\",\n name: contributor.id + \"_coninstitution\",\n }\n },\n year: {\n required: false,\n element: {\n hiddenlabel: \"Contributor year\",\n value: contributor.year,\n wrapperid: \"fitem_\" + contributor.id + \"_conyear\",\n id: 'id_' + contributor.id + \"_conyear\",\n name: contributor.id + \"_conyear\",\n }\n }\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.element.push(element);\n data.creator = data.contributor[0];\n data.json = {\n required: true,\n element: {\n hiddenlabel: \"JSON metadata\",\n value: JSON.stringify(state),\n wrapperid: \"fitem_metadata_json\",\n id: \"id_metadata_json\",\n name: \"metadata_json\",\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const elements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of elements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n // Get the selected person id.\n const first = this.getElement('#id_2_confirstname').value;\n const last = this.getElement('#id_2_conlastname').value;\n this.reactive.dispatch('updateContributor', 2, first, last);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","addEventListener","getElement","update","getWatchers","watch","handler","console","log","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","required","hiddenlabel","value","firstName","wrapperid","id","lastname","lastName","institution","year","push","json","JSON","stringify","metadataContainer","Error","renderComponent","event","preventDefault","elements","getElements","classList","contains","first","last","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,aAChCE,iBACDR,KAAKS,WAAWT,KAAKJ,UAAUE,QAC/B,QACAE,KAAKU,QAIbC,oBACW,CACH,CAACC,4BAA8BC,QAASb,KAAKO,oEAItBD,MAACA,YAC5BQ,QAAQC,IAAI,iBAaNC,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASd,MAAMc,QACfC,SAAUf,MAAMe,SAChBC,WAAY,GACZrB,QAAS,IAEbK,MAAMY,YAAYK,SAAQL,oBAChBjB,QAAU,CACZuB,UAAW,CACPC,UAAU,EACVxB,QAAS,CACLyB,YAAa,yBACbC,MAAOT,YAAYU,UACnBC,UAAW,SAAWX,YAAYY,GAAK,gBACvCA,GAAI,MAAQZ,YAAYY,GAAK,gBAC7BnC,KAAMuB,YAAYY,GAAK,kBAG/BC,SAAU,CACNN,UAAU,EACVxB,QAAS,CACLyB,YAAa,wBACbC,MAAOT,YAAYc,SACnBH,UAAW,SAAWX,YAAYY,GAAK,eACvCA,GAAI,MAAQZ,YAAYY,GAAK,eAC7BnC,KAAMuB,YAAYY,GAAK,iBAG/BG,YAAa,CACTR,UAAU,EACVxB,QAAS,CACLyB,YAAa,0BACbC,MAAOT,YAAYe,YACnBJ,UAAW,SAAWX,YAAYY,GAAK,kBACvCA,GAAI,MAAQZ,YAAYY,GAAK,kBAC7BnC,KAAMuB,YAAYY,GAAK,oBAG/BI,KAAM,CACFT,UAAU,EACVxB,QAAS,CACLyB,YAAa,mBACbC,MAAOT,YAAYgB,KACnBL,UAAW,SAAWX,YAAYY,GAAK,WACvCA,GAAI,MAAQZ,YAAYY,GAAK,WAC7BnC,KAAMuB,YAAYY,GAAK,cAInCd,KAAKE,YAAYiB,KAAK,IAAIlC,aAE9BK,MAAMa,SAASI,SAAQJ,WACnBH,KAAKG,SAASgB,KAAK,IAAIhB,cAE3Bb,MAAMgB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWa,KAAK,IAAIb,gBAE7BN,KAAKf,QAAQkC,KAtEG,CACZT,YAAa,mBACbD,UAAU,EACVxB,QAAS,CACL4B,UAAW,oBACXC,GAAI,cACJnC,KAAM,cAiEdqB,KAAKC,QAAUD,KAAKE,YAAY,GAChCF,KAAKoB,KAAO,CACRX,UAAU,EACVxB,QAAS,CACLyB,YAAa,gBACbC,MAAOU,KAAKC,UAAUhC,OACtBuB,UAAW,sBACXC,GAAI,mBACJnC,KAAM,wBAKR4C,kBAAoBvC,KAAKS,WAAWT,KAAKJ,UAAUC,uBACpD0C,wBACK,IAAIC,MAAM,qCAGdxC,KAAKyC,gBAAgBF,kBAAmB,8BAA+BvB,MAQjFN,OAAOgC,OAEHA,MAAMC,uBACAC,SAAW5C,KAAK6C,YAAY,iEAC7B,MAAM5C,WAAW2C,SACI,KAAlB3C,QAAQ0B,+CACqB1B,QAAS,YAC/BA,QAAQ6C,UAAUC,SAAS,wDACL9C,QAAS,UAIxC+C,MAAQhD,KAAKS,WAAW,sBAAsBkB,MAC9CsB,KAAOjD,KAAKS,WAAW,qBAAqBkB,WAC7CvB,SAAS8C,SAAS,oBAAqB,EAAGF,MAAOC"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n data.json = {\n required: true,\n element: {\n value: JSON.stringify(state),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of requiredElements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n let inputElements = this.getElements('#qtype-stack-metadata-content input[id^=\"smdi\"]');\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n this.reactive.dispatch('updateAll', inputElements);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","firstName","lastname","lastName","institution","year","push","json","JSON","stringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","event","preventDefault","requiredElements","getElements","classList","contains","inputElements","Array","from","map","el","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,QAGzCE,oBACW,CACH,CAACC,sBAAwBC,QAASV,KAAKO,2BAI/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzBnB,KAAM,QAAUkB,GAAK,IAAMC,+CAKRR,MAACA,kBAGtBW,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASf,MAAMe,QACfC,SAAUhB,MAAMgB,SAChBC,WAAY,IAEhBjB,MAAMa,YAAYK,SAAQL,oBAChBlB,QAAU,CACZwB,UAAWzB,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,wBAAyBM,YAAYO,WAC9FC,SAAU3B,KAAKW,mBAAkB,EAAMQ,YAAYN,GAAI,uBAAwBM,YAAYS,UAC3FC,YAAa7B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,0BAA2BM,YAAYU,aAClGC,KAAM9B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,mBAAoBM,YAAYW,OAExFb,KAAKE,YAAYY,KAAK,IAAI9B,aAE9BK,MAAMc,SAASI,SAAQJ,WACnBH,KAAKG,SAASW,KAAK,IAAIX,cAE3Bd,MAAMiB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWQ,KAAK,IAAIR,gBAE7BN,KAAKC,QAAU,CACXO,UAAWzB,KAAKW,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMY,QAAQQ,WAC/EC,SAAU3B,KAAKW,mBAAkB,EAAM,EAAG,mBAAoBL,MAAMY,QAAQU,UAC5EC,YAAa7B,KAAKW,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMY,QAAQW,aACnFC,KAAM9B,KAAKW,mBAAkB,EAAO,EAAG,eAAgBL,MAAMY,QAAQY,OAEzEb,KAAKe,KAAO,CACRpB,UAAU,EACVX,QAAS,CACLc,MAAOkB,KAAKC,UAAU5B,OACtB6B,WAAY,YACZnB,UAAW,sBACXH,GAAI,mBACJlB,KAAM,wBAKRyC,kBAAoBpC,KAAKqC,WAAWrC,KAAKJ,UAAUC,uBACpDuC,wBACK,IAAIE,MAAM,qCAGdtC,KAAKuC,gBAAgBH,kBAAmB,8BAA+BnB,WACxEuB,iBACDxC,KAAKqC,WAAWrC,KAAKJ,UAAUE,QAC/B,QACAE,KAAKyC,QASbA,OAAOC,OAEHA,MAAMC,uBACAC,iBAAmB5C,KAAK6C,YAAY,iEACrC,MAAM5C,WAAW2C,iBACI,KAAlB3C,QAAQc,+CACqBd,QAAS,YAC/BA,QAAQ6C,UAAUC,SAAS,wDACL9C,QAAS,QAG1C+C,cAAgBhD,KAAK6C,YAAY,mDACrCG,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGvC,GAAIuC,GAAGrC,cAC5DX,SAASiD,SAAS,YAAaL"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js index 27dc88e8e00..8613a030762 100644 --- a/amd/build/metadata/mutations.min.js +++ b/amd/build/metadata/mutations.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.d * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class{updateContributor(stateManager,id,firstName,lastName){const state=stateManager.state;stateManager.setReadOnly(!1),state.contributor.get(id).firstName=firstName,state.contributor.get(id).lastName=lastName,stateManager.setReadOnly(!0),console.log(state.contributor.get(id).firstName),console.log(state.contributor.get(id).lastName),console.log(state)}};_exports.mutations=mutations})); +class{updateAll(stateManager,inputArray){const state=stateManager.state;stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if(console.log(property,subproperty,id),0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map index ea89a28c2ad..54baf5976a9 100644 --- a/amd/build/metadata/mutations.min.js.map +++ b/amd/build/metadata/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateContributor(stateManager, id, firstName, lastName) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.contributor.get(id).firstName = firstName;\n state.contributor.get(id).lastName = lastName;\n stateManager.setReadOnly(true);\n console.log(state.contributor.get(id).firstName);\n console.log(state.contributor.get(id).lastName);\n console.log(state);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateContributor","stateManager","id","firstName","lastName","state","setReadOnly","contributor","get","console","log"],"mappings":"gKAmCaA,UAAY;;;;;;;;MAZrBC,kBAAkBC,aAAcC,GAAIC,UAAWC,gBACrCC,MAAQJ,aAAaI,MAC3BJ,aAAaK,aAAY,GACzBD,MAAME,YAAYC,IAAIN,IAAIC,UAAYA,UACtCE,MAAME,YAAYC,IAAIN,IAAIE,SAAWA,SACrCH,aAAaK,aAAY,GACzBG,QAAQC,IAAIL,MAAME,YAAYC,IAAIN,IAAIC,WACtCM,QAAQC,IAAIL,MAAME,YAAYC,IAAIN,IAAIE,UACtCK,QAAQC,IAAIL"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateAll(stateManager, inputArray) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n console.log(property, subproperty, id);\n if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateAll","stateManager","inputArray","state","setReadOnly","field","parts","split","id","property","subproperty","console","log","existing","get"],"mappings":"gKA6CaA,UAAY;;;;;;;;MAtBrBC,UAAUC,aAAcC,kBACdC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,OACpB,MAAMC,SAASH,WAAY,OACtBI,MAAQD,MAAM,GAAGE,MAAM,KACvBC,GAAKF,MAAM,GACXG,SAAWH,MAAM,GACjBI,YAAcJ,MAAM,MAC1BK,QAAQC,IAAIH,SAAUC,YAAaF,IACzB,GAANA,GAAS,OACHK,SAAWV,MAAMM,UAAUK,IAAIN,IACjCK,WACAA,SAASH,aAAeL,MAAM,SAGlCF,MAAMM,UAAUC,aAAeL,MAAM,GAG7CJ,aAAaG,aAAY"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 826a23bd8eb..118ceb5179a 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -56,84 +56,43 @@ export default class extends BaseComponent { */ async stateReady(state) { await this.reloadContainerComponent({state}); - this.addEventListener( - this.getElement(this.selectors.SUBMIT), - 'click', - this.update - ); } getWatchers() { return [ - {watch: `contributor:updated`, handler: this.reloadContainerComponent}, + {watch: `state:updated`, handler: this.reloadContainerComponent}, ]; } - async reloadContainerComponent({state}) { - console.log('Refresh'); - // Mustache data is not fully compatible with state object so we need to convert it - // into a plain object. - const element = { - hiddenlabel: "Course full name", - required: true, + createDataElement(required, id, tag, value) { + return { + required: required, element: { - wrapperid: "fitem_id_fullname", - id: "id_fullname", - name: "fullname" + value: value, + wrapperid: 'fitem_smdi_' + id + '_' + tag, + id: 'smdi_' + id + '_' + tag, + name: 'smdi_' + id + '_' + tag, } }; + } + async reloadContainerComponent({state}) { + // Mustache data is not fully compatible with state object so we need to convert it + // into a plain object. const data = { creator: {}, contributor: [], language: [], license: state.license, isPartOf: state.isPartOf, - additional: [], - element: [] + additional: [] }; state.contributor.forEach(contributor => { const element = { - firstname: { - required: false, - element: { - hiddenlabel: "Contributor first name", - value: contributor.firstName, - wrapperid: "fitem_" + contributor.id + "_confirstname", - id: 'id_' + contributor.id + "_confirstname", - name: contributor.id + "_confirstname", - } - }, - lastname: { - required: true, - element: { - hiddenlabel: "Contributor last name", - value: contributor.lastName, - wrapperid: "fitem_" + contributor.id + "_conlastname", - id: 'id_' + contributor.id + "_conlastname", - name: contributor.id + "_conlastname", - } - }, - institution: { - required: false, - element: { - hiddenlabel: "Contributor institution", - value: contributor.institution, - wrapperid: "fitem_" + contributor.id + "_coninstitution", - id: 'id_' + contributor.id + "_coninstitution", - name: contributor.id + "_coninstitution", - } - }, - year: { - required: false, - element: { - hiddenlabel: "Contributor year", - value: contributor.year, - wrapperid: "fitem_" + contributor.id + "_conyear", - id: 'id_' + contributor.id + "_conyear", - name: contributor.id + "_conyear", - } - } + firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName), + lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName), + institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution), + year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year), }; data.contributor.push({...element}); }); @@ -143,16 +102,20 @@ export default class extends BaseComponent { state.additional.forEach(additional => { data.additional.push({...additional}); }); - data.element.push(element); - data.creator = data.contributor[0]; + data.creator = { + firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName), + lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName), + institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution), + year: this.createDataElement(false, 0, 'creator_year', state.creator.year), + }; data.json = { required: true, element: { - hiddenlabel: "JSON metadata", value: JSON.stringify(state), - wrapperid: "fitem_metadata_json", - id: "id_metadata_json", - name: "metadata_json", + attributes: 'rows="10"', + wrapperid: 'fitem_metadata_json', + id: 'id_metadata_json', + name: 'metadata_json', } }; @@ -163,6 +126,11 @@ export default class extends BaseComponent { } await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data); + this.addEventListener( + this.getElement(this.selectors.SUBMIT), + 'click', + this.update + ); } /** @@ -173,17 +141,16 @@ export default class extends BaseComponent { update(event) { // We don't want to submit the form. event.preventDefault(); - const elements = this.getElements('#qtype-stack-metadata-content input[aria-required="true"]'); - for (const element of elements) { + const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required="true"]'); + for (const element of requiredElements) { if (element.value === '') { notifyFieldValidationFailure(element, 'Required'); } else if (element.classList.contains('is-invalid')) { notifyFieldValidationFailure(element, ''); } } - // Get the selected person id. - const first = this.getElement('#id_2_confirstname').value; - const last = this.getElement('#id_2_conlastname').value; - this.reactive.dispatch('updateContributor', 2, first, last); + let inputElements = this.getElements('#qtype-stack-metadata-content input[id^="smdi"]'); + inputElements = Array.from(inputElements).map((el) => [el.id, el.value]); + this.reactive.dispatch('updateAll', inputElements); } } \ No newline at end of file diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js index d1a82b04471..b7a4e09cbdc 100644 --- a/amd/src/metadata/mutations.js +++ b/amd/src/metadata/mutations.js @@ -21,15 +21,25 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ class Mutations { - updateContributor(stateManager, id, firstName, lastName) { + updateAll(stateManager, inputArray) { const state = stateManager.state; stateManager.setReadOnly(false); - state.contributor.get(id).firstName = firstName; - state.contributor.get(id).lastName = lastName; + for (const field of inputArray) { + const parts = field[0].split('_'); + const id = parts[1]; + const property = parts[2]; + const subproperty = parts[3]; + console.log(property, subproperty, id); + if (id != 0) { + const existing = state[property].get(id); + if (existing) { + existing[subproperty] = field[1]; + } + } else { + state[property][subproperty] = field[1]; + } + } stateManager.setReadOnly(true); - console.log(state.contributor.get(id).firstName); - console.log(state.contributor.get(id).lastName); - console.log(state); } } diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index f5abc02f02f..2683eaaf5c3 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -419,6 +419,7 @@ $string['creator'] = 'Creator'; $string['addcontributor'] = 'Add contributor'; $string['updateJSON'] = 'Update JSON'; +$string['JSONmetadata'] = 'JSON metadata'; // Strings used by input elements. $string['studentinputtoolong'] = 'Your input is longer than permitted by STACK.'; diff --git a/templates/metadatacontent.mustache b/templates/metadatacontent.mustache index 7161dd0fa1a..ddecdf6f16b 100644 --- a/templates/metadatacontent.mustache +++ b/templates/metadatacontent.mustache @@ -20,29 +20,29 @@ } }}
-
+
{{#str}} firstname, qtype_stack {{/str}}
{{#str}} lastname, qtype_stack {{/str}}
{{#str}} institution, qtype_stack {{/str}}
-
{{#str}} year, qtype_stack {{/str}}
+
{{#str}} year, qtype_stack {{/str}}
{{#creator}}
-
{{#str}} creator, qtype_stack {{/str}}
+
{{#str}} creator, qtype_stack {{/str}}
{{#firstname}}{{> core_form/element-text-inline }}{{/firstname}}
{{#lastname}}{{> core_form/element-text-inline }}{{/lastname}}
{{#institution}}{{> core_form/element-text-inline }}{{/institution}}
-
{{#year}}{{> core_form/element-text-inline }}{{/year}}
+
{{#year}}{{> core_form/element-text-inline }}{{/year}}
{{/creator}} {{#contributor}}
-
{{#str}} contributor, qtype_stack {{/str}}
+
{{#str}} contributor, qtype_stack {{/str}}
{{#firstname}}{{> core_form/element-text-inline }}{{/firstname}}
{{#lastname}}{{> core_form/element-text-inline }}{{/lastname}}
{{#institution}}{{> core_form/element-text-inline }}{{/institution}}
-
{{#year}}{{> core_form/element-text-inline }}{{/year}}
+
{{#year}}{{> core_form/element-text-inline }}{{/year}}
{{/contributor}} @@ -50,18 +50,39 @@
+
-
-
+
+
+
+
-
{{#str}} contributor, qtype_stack {{/str}}
-
{{#json}}{{> core_form/element-textarea }}{{/json}}
+
{{#str}} JSONmetadata, qtype_stack {{/str}}
+
+ {{#json}} + {{< core_form/element-template-inline }} + {{$element}} + + {{/element}} + {{/ core_form/element-template-inline }} + {{/json}} +
{{#js}} From 8acee22dab33908c06b8b61e3c4a3ebba7930201 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Wed, 3 Dec 2025 16:56:39 +0000 Subject: [PATCH 009/100] iss1171 - Sub component --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/contributor.min.js | 11 ++++ amd/build/metadata/contributor.min.js.map | 1 + amd/build/metadata/metadatacontent.min.js | 2 +- amd/build/metadata/metadatacontent.min.js.map | 2 +- amd/build/metadata/mutations.min.js | 2 +- amd/build/metadata/mutations.min.js.map | 2 +- amd/src/metadata/container.js | 1 + amd/src/metadata/contributor.js | 51 +++++++++++++++++++ amd/src/metadata/metadatacontent.js | 13 ++++- amd/src/metadata/mutations.js | 1 - templates/metadatacontent.mustache | 9 +--- templates/metadatacontributor.mustache | 34 +++++++++++++ 14 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 amd/build/metadata/contributor.min.js create mode 100644 amd/build/metadata/contributor.min.js.map create mode 100644 amd/src/metadata/contributor.js create mode 100644 templates/metadatacontributor.mustache diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 1304426691c..0849fdbc7a5 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!0,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year)};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!0,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:JSON.stringify(state),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}update(event){event.preventDefault();const requiredElements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of requiredElements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");let inputElements=this.getElements('#qtype-stack-metadata-content input[id^="smdi"]');inputElements=Array.from(inputElements).map((el=>[el.id,el.value])),this.reactive.dispatch("updateAll",inputElements)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!0,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year),id:contributor.id};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!0,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:JSON.stringify(state),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}update(event){event.preventDefault();const requiredElements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of requiredElements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");let inputElements=this.getElements('#qtype-stack-metadata-content input[id^="smdi"]');inputElements=Array.from(inputElements).map((el=>[el.id,el.value])),this.reactive.dispatch("updateAll",inputElements)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index af9cda7c680..c1d2f965485 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n data.json = {\n required: true,\n element: {\n value: JSON.stringify(state),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of requiredElements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n let inputElements = this.getElements('#qtype-stack-metadata-content input[id^=\"smdi\"]');\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n this.reactive.dispatch('updateAll', inputElements);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","firstName","lastname","lastName","institution","year","push","json","JSON","stringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","event","preventDefault","requiredElements","getElements","classList","contains","inputElements","Array","from","map","el","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,QAGzCE,oBACW,CACH,CAACC,sBAAwBC,QAASV,KAAKO,2BAI/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzBnB,KAAM,QAAUkB,GAAK,IAAMC,+CAKRR,MAACA,kBAGtBW,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASf,MAAMe,QACfC,SAAUhB,MAAMgB,SAChBC,WAAY,IAEhBjB,MAAMa,YAAYK,SAAQL,oBAChBlB,QAAU,CACZwB,UAAWzB,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,wBAAyBM,YAAYO,WAC9FC,SAAU3B,KAAKW,mBAAkB,EAAMQ,YAAYN,GAAI,uBAAwBM,YAAYS,UAC3FC,YAAa7B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,0BAA2BM,YAAYU,aAClGC,KAAM9B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,mBAAoBM,YAAYW,OAExFb,KAAKE,YAAYY,KAAK,IAAI9B,aAE9BK,MAAMc,SAASI,SAAQJ,WACnBH,KAAKG,SAASW,KAAK,IAAIX,cAE3Bd,MAAMiB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWQ,KAAK,IAAIR,gBAE7BN,KAAKC,QAAU,CACXO,UAAWzB,KAAKW,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMY,QAAQQ,WAC/EC,SAAU3B,KAAKW,mBAAkB,EAAM,EAAG,mBAAoBL,MAAMY,QAAQU,UAC5EC,YAAa7B,KAAKW,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMY,QAAQW,aACnFC,KAAM9B,KAAKW,mBAAkB,EAAO,EAAG,eAAgBL,MAAMY,QAAQY,OAEzEb,KAAKe,KAAO,CACRpB,UAAU,EACVX,QAAS,CACLc,MAAOkB,KAAKC,UAAU5B,OACtB6B,WAAY,YACZnB,UAAW,sBACXH,GAAI,mBACJlB,KAAM,wBAKRyC,kBAAoBpC,KAAKqC,WAAWrC,KAAKJ,UAAUC,uBACpDuC,wBACK,IAAIE,MAAM,qCAGdtC,KAAKuC,gBAAgBH,kBAAmB,8BAA+BnB,WACxEuB,iBACDxC,KAAKqC,WAAWrC,KAAKJ,UAAUE,QAC/B,QACAE,KAAKyC,QASbA,OAAOC,OAEHA,MAAMC,uBACAC,iBAAmB5C,KAAK6C,YAAY,iEACrC,MAAM5C,WAAW2C,iBACI,KAAlB3C,QAAQc,+CACqBd,QAAS,YAC/BA,QAAQ6C,UAAUC,SAAS,wDACL9C,QAAS,QAG1C+C,cAAgBhD,KAAK6C,YAAY,mDACrCG,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGvC,GAAIuC,GAAGrC,cAC5DX,SAASiD,SAAS,YAAaL"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n id: contributor.id,\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n data.json = {\n required: true,\n element: {\n value: JSON.stringify(state),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of requiredElements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n let inputElements = this.getElements('#qtype-stack-metadata-content input[id^=\"smdi\"]');\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n this.reactive.dispatch('updateAll', inputElements);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","firstName","lastname","lastName","institution","year","push","json","JSON","stringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","event","preventDefault","requiredElements","getElements","classList","contains","inputElements","Array","from","map","el","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,QAGzCE,oBACW,CACH,CAACC,sBAAwBC,QAASV,KAAKO,2BAI/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzBnB,KAAM,QAAUkB,GAAK,IAAMC,+CAKRR,MAACA,kBAGtBW,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASf,MAAMe,QACfC,SAAUhB,MAAMgB,SAChBC,WAAY,IAEhBjB,MAAMa,YAAYK,SAAQL,oBAChBlB,QAAU,CACZwB,UAAWzB,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,wBAAyBM,YAAYO,WAC9FC,SAAU3B,KAAKW,mBAAkB,EAAMQ,YAAYN,GAAI,uBAAwBM,YAAYS,UAC3FC,YAAa7B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,0BAA2BM,YAAYU,aAClGC,KAAM9B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,mBAAoBM,YAAYW,MACpFjB,GAAIM,YAAYN,IAEpBI,KAAKE,YAAYY,KAAK,IAAI9B,aAE9BK,MAAMc,SAASI,SAAQJ,WACnBH,KAAKG,SAASW,KAAK,IAAIX,cAE3Bd,MAAMiB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWQ,KAAK,IAAIR,gBAE7BN,KAAKC,QAAU,CACXO,UAAWzB,KAAKW,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMY,QAAQQ,WAC/EC,SAAU3B,KAAKW,mBAAkB,EAAM,EAAG,mBAAoBL,MAAMY,QAAQU,UAC5EC,YAAa7B,KAAKW,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMY,QAAQW,aACnFC,KAAM9B,KAAKW,mBAAkB,EAAO,EAAG,eAAgBL,MAAMY,QAAQY,OAEzEb,KAAKe,KAAO,CACRpB,UAAU,EACVX,QAAS,CACLc,MAAOkB,KAAKC,UAAU5B,OACtB6B,WAAY,YACZnB,UAAW,sBACXH,GAAI,mBACJlB,KAAM,wBAKRyC,kBAAoBpC,KAAKqC,WAAWrC,KAAKJ,UAAUC,uBACpDuC,wBACK,IAAIE,MAAM,qCAGdtC,KAAKuC,gBAAgBH,kBAAmB,8BAA+BnB,WACxEuB,iBACDxC,KAAKqC,WAAWrC,KAAKJ,UAAUE,QAC/B,QACAE,KAAKyC,QASbA,OAAOC,OAEHA,MAAMC,uBACAC,iBAAmB5C,KAAK6C,YAAY,iEACrC,MAAM5C,WAAW2C,iBACI,KAAlB3C,QAAQc,+CACqBd,QAAS,YAC/BA,QAAQ6C,UAAUC,SAAS,wDACL9C,QAAS,QAG1C+C,cAAgBhD,KAAK6C,YAAY,mDACrCG,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGvC,GAAIuC,GAAGrC,cAC5DX,SAASiD,SAAS,YAAaL"} \ No newline at end of file diff --git a/amd/build/metadata/contributor.min.js b/amd/build/metadata/contributor.min.js new file mode 100644 index 00000000000..13231b971a4 --- /dev/null +++ b/amd/build/metadata/contributor.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/contributor",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-contributor",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=contributor.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/contributor.min.js.map b/amd/build/metadata/contributor.min.js.map new file mode 100644 index 00000000000..c3573674ff1 --- /dev/null +++ b/amd/build/metadata/contributor.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contributor.min.js","sources":["../../src/metadata/contributor.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-contributor';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,kCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js b/amd/build/metadata/metadatacontent.min.js index e22cdaefce3..4c14e535036 100644 --- a/amd/build/metadata/metadatacontent.min.js +++ b/amd/build/metadata/metadatacontent.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/metadatacontent",["exports","core/reactive","qtype_ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",DELETE:"#smd_1_delete"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}stateReady(){this.addEventListener(this.getElement(this.selectors.DELETE),"click",this.delete)}delete(){console.log("Here")}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=metadatacontent.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js.map b/amd/build/metadata/metadatacontent.min.js.map index 8e23b4bc6d9..34cd4b20103 100644 --- a/amd/build/metadata/metadatacontent.min.js.map +++ b/amd/build/metadata/metadatacontent.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file +{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n DELETE: `#smd_1_delete`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n stateReady() {\n this.addEventListener(\n this.getElement(this.selectors.DELETE),\n 'click',\n this.delete\n );\n }\n\n delete() {\n console.log('Here');\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","DELETE","target","this","element","document","querySelector","reactive","metadata","stateReady","addEventListener","getElement","delete","console","log"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,oCAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,YAIRU,kBACSC,iBACDP,KAAKQ,WAAWR,KAAKJ,UAAUE,QAC/B,QACAE,KAAKS,QAIbA,SACIC,QAAQC,IAAI"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js index 8613a030762..6dd53b0b09c 100644 --- a/amd/build/metadata/mutations.min.js +++ b/amd/build/metadata/mutations.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.d * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class{updateAll(stateManager,inputArray){const state=stateManager.state;stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if(console.log(property,subproperty,id),0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); +class{updateAll(stateManager,inputArray){const state=stateManager.state;stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if(0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map index 54baf5976a9..a613a7d985d 100644 --- a/amd/build/metadata/mutations.min.js.map +++ b/amd/build/metadata/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateAll(stateManager, inputArray) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n console.log(property, subproperty, id);\n if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateAll","stateManager","inputArray","state","setReadOnly","field","parts","split","id","property","subproperty","console","log","existing","get"],"mappings":"gKA6CaA,UAAY;;;;;;;;MAtBrBC,UAAUC,aAAcC,kBACdC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,OACpB,MAAMC,SAASH,WAAY,OACtBI,MAAQD,MAAM,GAAGE,MAAM,KACvBC,GAAKF,MAAM,GACXG,SAAWH,MAAM,GACjBI,YAAcJ,MAAM,MAC1BK,QAAQC,IAAIH,SAAUC,YAAaF,IACzB,GAANA,GAAS,OACHK,SAAWV,MAAMM,UAAUK,IAAIN,IACjCK,WACAA,SAASH,aAAeL,MAAM,SAGlCF,MAAMM,UAAUC,aAAeL,MAAM,GAG7CJ,aAAaG,aAAY"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateAll(stateManager, inputArray) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateAll","stateManager","inputArray","state","setReadOnly","field","parts","split","id","property","subproperty","existing","get"],"mappings":"gKA4CaA,UAAY;;;;;;;;MArBrBC,UAAUC,aAAcC,kBACdC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,OACpB,MAAMC,SAASH,WAAY,OACtBI,MAAQD,MAAM,GAAGE,MAAM,KACvBC,GAAKF,MAAM,GACXG,SAAWH,MAAM,GACjBI,YAAcJ,MAAM,MAChB,GAANE,GAAS,OACHG,SAAWR,MAAMM,UAAUG,IAAIJ,IACjCG,WACAA,SAASD,aAAeL,MAAM,SAGlCF,MAAMM,UAAUC,aAAeL,MAAM,GAG7CJ,aAAaG,aAAY"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 118ceb5179a..11044b5a40d 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -93,6 +93,7 @@ export default class extends BaseComponent { lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName), institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution), year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year), + id: contributor.id, }; data.contributor.push({...element}); }); diff --git a/amd/src/metadata/contributor.js b/amd/src/metadata/contributor.js new file mode 100644 index 00000000000..d9e7d49bfe4 --- /dev/null +++ b/amd/src/metadata/contributor.js @@ -0,0 +1,51 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; + +export default class extends BaseComponent { + create() { + this.name = 'stack-metadata-contributor'; + this.selectors = { + METADATACONTAINER: `[data-for='qtype-stack-metadata']`, + SUBMIT: `#stack-metadata-update`, + }; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } + +} \ No newline at end of file diff --git a/amd/src/metadata/metadatacontent.js b/amd/src/metadata/metadatacontent.js index 7caddf56840..d085f8fd213 100644 --- a/amd/src/metadata/metadatacontent.js +++ b/amd/src/metadata/metadatacontent.js @@ -29,7 +29,7 @@ export default class extends BaseComponent { this.name = 'stack-metadata-content'; this.selectors = { METADATACONTAINER: `[data-for='qtype-stack-metadata']`, - SUBMIT: `#stack-metadata-update`, + DELETE: `#smd_1_delete`, }; } @@ -48,4 +48,15 @@ export default class extends BaseComponent { }); } + stateReady() { + this.addEventListener( + this.getElement(this.selectors.DELETE), + 'click', + this.delete + ); + } + + delete() { + console.log('Here'); + } } \ No newline at end of file diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js index b7a4e09cbdc..c5594c389e1 100644 --- a/amd/src/metadata/mutations.js +++ b/amd/src/metadata/mutations.js @@ -29,7 +29,6 @@ class Mutations { const id = parts[1]; const property = parts[2]; const subproperty = parts[3]; - console.log(property, subproperty, id); if (id != 0) { const existing = state[property].get(id); if (existing) { diff --git a/templates/metadatacontent.mustache b/templates/metadatacontent.mustache index ddecdf6f16b..6f8cc2393e6 100644 --- a/templates/metadatacontent.mustache +++ b/templates/metadatacontent.mustache @@ -37,14 +37,7 @@
{{/creator}} {{#contributor}} -
-
{{#str}} contributor, qtype_stack {{/str}}
-
{{#firstname}}{{> core_form/element-text-inline }}{{/firstname}}
-
{{#lastname}}{{> core_form/element-text-inline }}{{/lastname}}
-
{{#institution}}{{> core_form/element-text-inline }}{{/institution}}
-
{{#year}}{{> core_form/element-text-inline }}{{/year}}
-
-
+ {{>qtype_stack/metadatacontributor}} {{/contributor}}
diff --git a/templates/metadatacontributor.mustache b/templates/metadatacontributor.mustache new file mode 100644 index 00000000000..8d7a1e6bf53 --- /dev/null +++ b/templates/metadatacontributor.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata + + + Example context (json): + { + } +}} +
+
{{#str}} contributor, qtype_stack {{/str}}
+
{{#firstname}}{{> core_form/element-text-inline }}{{/firstname}}
+
{{#lastname}}{{> core_form/element-text-inline }}{{/lastname}}
+
{{#institution}}{{> core_form/element-text-inline }}{{/institution}}
+
{{#year}}{{> core_form/element-text-inline }}{{/year}}
+
+
+{{#js}} +require(['qtype_stack/metadata/metadatacontent'], function(component) { + component.init('#qtype-stack-metadata-contributor'); +}); +{{/js}} \ No newline at end of file From 161a6156075b7da66e6ced7b645988d6b9016497 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 4 Dec 2025 12:18:52 +0000 Subject: [PATCH 010/100] iss1171 - Add/delete contributor. Basic update from JSON. --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/build/metadata/contributor.min.js | 2 +- amd/build/metadata/contributor.min.js.map | 2 +- amd/build/metadata/metadatacontent.min.js | 2 +- amd/build/metadata/metadatacontent.min.js.map | 2 +- amd/build/metadata/mutations.min.js | 2 +- amd/build/metadata/mutations.min.js.map | 2 +- amd/src/metadata/container.js | 26 ++++++++++++-- amd/src/metadata/contributor.js | 15 ++++++-- amd/src/metadata/metadatacontent.js | 14 +------- amd/src/metadata/mutations.js | 35 +++++++++++++++++++ lang/en/qtype_stack.php | 1 + templates/metadatacontent.mustache | 6 +++- templates/metadatacontributor.mustache | 8 ++--- 15 files changed, 90 insertions(+), 31 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 0849fdbc7a5..5c71d50a485 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!0,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year),id:contributor.id};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!0,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:JSON.stringify(state),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.SUBMIT),"click",this.update)}update(event){event.preventDefault();const requiredElements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of requiredElements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");let inputElements=this.getElements('#qtype-stack-metadata-content input[id^="smdi"]');inputElements=Array.from(inputElements).map((el=>[el.id,el.value])),this.reactive.dispatch("updateAll",inputElements)}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",ADDCONTRIB:"#stack-metadata-add-contrib"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:state.license,isPartOf:state.isPartOf,additional:[]};state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!0,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year),id:contributor.id};data.contributor.push({...element})})),state.language.forEach((language=>{data.language.push({...language})})),state.additional.forEach((additional=>{data.additional.push({...additional})})),data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!0,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:JSON.stringify(state,null,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",this.update),this.addEventListener(this.getElement(this.selectors.ADDCONTRIB),"click",this.addContrib),this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",this.updateInputs)}update(event){event.preventDefault();const requiredElements=this.getElements('#qtype-stack-metadata-content input[aria-required="true"]');for(const element of requiredElements)""===element.value?(0,_events.notifyFieldValidationFailure)(element,"Required"):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");let inputElements=this.getElements('#qtype-stack-metadata-content input[id^="smdi"]');inputElements=Array.from(inputElements).map((el=>[el.id,el.value])),this.reactive.dispatch("updateAll",inputElements)}addContrib(){this.reactive.dispatch("addContributor")}updateInputs(){this.reactive.dispatch("updateFromJson",this.getElement("#id_metadata_json").value)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index c1d2f965485..42f77c48c28 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n id: contributor.id,\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n data.json = {\n required: true,\n element: {\n value: JSON.stringify(state),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n this.addEventListener(\n this.getElement(this.selectors.SUBMIT),\n 'click',\n this.update\n );\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of requiredElements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n let inputElements = this.getElements('#qtype-stack-metadata-content input[id^=\"smdi\"]');\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n this.reactive.dispatch('updateAll', inputElements);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","firstName","lastname","lastName","institution","year","push","json","JSON","stringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","event","preventDefault","requiredElements","getElements","classList","contains","inputElements","Array","from","map","el","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,6BASSU,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,QAGzCE,oBACW,CACH,CAACC,sBAAwBC,QAASV,KAAKO,2BAI/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzBnB,KAAM,QAAUkB,GAAK,IAAMC,+CAKRR,MAACA,kBAGtBW,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASf,MAAMe,QACfC,SAAUhB,MAAMgB,SAChBC,WAAY,IAEhBjB,MAAMa,YAAYK,SAAQL,oBAChBlB,QAAU,CACZwB,UAAWzB,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,wBAAyBM,YAAYO,WAC9FC,SAAU3B,KAAKW,mBAAkB,EAAMQ,YAAYN,GAAI,uBAAwBM,YAAYS,UAC3FC,YAAa7B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,0BAA2BM,YAAYU,aAClGC,KAAM9B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,mBAAoBM,YAAYW,MACpFjB,GAAIM,YAAYN,IAEpBI,KAAKE,YAAYY,KAAK,IAAI9B,aAE9BK,MAAMc,SAASI,SAAQJ,WACnBH,KAAKG,SAASW,KAAK,IAAIX,cAE3Bd,MAAMiB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWQ,KAAK,IAAIR,gBAE7BN,KAAKC,QAAU,CACXO,UAAWzB,KAAKW,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMY,QAAQQ,WAC/EC,SAAU3B,KAAKW,mBAAkB,EAAM,EAAG,mBAAoBL,MAAMY,QAAQU,UAC5EC,YAAa7B,KAAKW,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMY,QAAQW,aACnFC,KAAM9B,KAAKW,mBAAkB,EAAO,EAAG,eAAgBL,MAAMY,QAAQY,OAEzEb,KAAKe,KAAO,CACRpB,UAAU,EACVX,QAAS,CACLc,MAAOkB,KAAKC,UAAU5B,OACtB6B,WAAY,YACZnB,UAAW,sBACXH,GAAI,mBACJlB,KAAM,wBAKRyC,kBAAoBpC,KAAKqC,WAAWrC,KAAKJ,UAAUC,uBACpDuC,wBACK,IAAIE,MAAM,qCAGdtC,KAAKuC,gBAAgBH,kBAAmB,8BAA+BnB,WACxEuB,iBACDxC,KAAKqC,WAAWrC,KAAKJ,UAAUE,QAC/B,QACAE,KAAKyC,QASbA,OAAOC,OAEHA,MAAMC,uBACAC,iBAAmB5C,KAAK6C,YAAY,iEACrC,MAAM5C,WAAW2C,iBACI,KAAlB3C,QAAQc,+CACqBd,QAAS,YAC/BA,QAAQ6C,UAAUC,SAAS,wDACL9C,QAAS,QAG1C+C,cAAgBhD,KAAK6C,YAAY,mDACrCG,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGvC,GAAIuC,GAAGrC,cAC5DX,SAASiD,SAAS,YAAaL"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n ADDCONTRIB: `#stack-metadata-add-contrib`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: state.license,\n isPartOf: state.isPartOf,\n additional: []\n };\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(true, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n id: contributor.id,\n };\n data.contributor.push({...element});\n });\n state.language.forEach(language => {\n data.language.push({...language});\n });\n state.additional.forEach(additional => {\n data.additional.push({...additional});\n });\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(true, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n data.json = {\n required: true,\n element: {\n value: JSON.stringify(state, null, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data);\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n this.update\n );\n this.addEventListener(\n this.getElement(this.selectors.ADDCONTRIB),\n 'click',\n this.addContrib\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n this.updateInputs\n );\n }\n\n /**\n * Our submit handler.\n *\n * @param {Event} event the click event\n */\n update(event) {\n // We don't want to submit the form.\n event.preventDefault();\n const requiredElements = this.getElements('#qtype-stack-metadata-content input[aria-required=\"true\"]');\n for (const element of requiredElements) {\n if (element.value === '') {\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n notifyFieldValidationFailure(element, '');\n }\n }\n let inputElements = this.getElements('#qtype-stack-metadata-content input[id^=\"smdi\"]');\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n this.reactive.dispatch('updateAll', inputElements);\n }\n\n addContrib() {\n this.reactive.dispatch('addContributor');\n }\n\n updateInputs() {\n this.reactive.dispatch('updateFromJson', this.getElement('#id_metadata_json').value);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","ADDCONTRIB","target","this","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","data","creator","contributor","language","license","isPartOf","additional","forEach","firstname","firstName","lastname","lastName","institution","year","push","json","JSON","stringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","addContrib","updateInputs","event","preventDefault","requiredElements","getElements","classList","contains","inputElements","Array","from","map","el","dispatch"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,oCACAC,6CACAC,sDAWIC,OAAQL,kBACT,IAAIM,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVX,UAAAA,6BASSY,aACPN,KAAKO,yBAAyB,CAACD,MAAAA,QAGzCE,oBACW,CACH,CAACC,sBAAwBC,QAASV,KAAKO,2BAI/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzBrB,KAAM,QAAUoB,GAAK,IAAMC,+CAKRR,MAACA,kBAGtBW,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASf,MAAMe,QACfC,SAAUhB,MAAMgB,SAChBC,WAAY,IAEhBjB,MAAMa,YAAYK,SAAQL,oBAChBlB,QAAU,CACZwB,UAAWzB,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,wBAAyBM,YAAYO,WAC9FC,SAAU3B,KAAKW,mBAAkB,EAAMQ,YAAYN,GAAI,uBAAwBM,YAAYS,UAC3FC,YAAa7B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,0BAA2BM,YAAYU,aAClGC,KAAM9B,KAAKW,mBAAkB,EAAOQ,YAAYN,GAAI,mBAAoBM,YAAYW,MACpFjB,GAAIM,YAAYN,IAEpBI,KAAKE,YAAYY,KAAK,IAAI9B,aAE9BK,MAAMc,SAASI,SAAQJ,WACnBH,KAAKG,SAASW,KAAK,IAAIX,cAE3Bd,MAAMiB,WAAWC,SAAQD,aACrBN,KAAKM,WAAWQ,KAAK,IAAIR,gBAE7BN,KAAKC,QAAU,CACXO,UAAWzB,KAAKW,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMY,QAAQQ,WAC/EC,SAAU3B,KAAKW,mBAAkB,EAAM,EAAG,mBAAoBL,MAAMY,QAAQU,UAC5EC,YAAa7B,KAAKW,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMY,QAAQW,aACnFC,KAAM9B,KAAKW,mBAAkB,EAAO,EAAG,eAAgBL,MAAMY,QAAQY,OAEzEb,KAAKe,KAAO,CACRpB,UAAU,EACVX,QAAS,CACLc,MAAOkB,KAAKC,UAAU5B,MAAO,KAAM,GACnC6B,WAAY,YACZnB,UAAW,sBACXH,GAAI,mBACJpB,KAAM,wBAKR2C,kBAAoBpC,KAAKqC,WAAWrC,KAAKN,UAAUC,uBACpDyC,wBACK,IAAIE,MAAM,qCAGdtC,KAAKuC,gBAAgBH,kBAAmB,8BAA+BnB,WACxEuB,iBACDxC,KAAKqC,WAAWrC,KAAKN,UAAUE,YAC/B,QACAI,KAAKyC,aAEJD,iBACDxC,KAAKqC,WAAWrC,KAAKN,UAAUI,YAC/B,QACAE,KAAK0C,iBAEJF,iBACDxC,KAAKqC,WAAWrC,KAAKN,UAAUG,cAC/B,QACAG,KAAK2C,cASbF,OAAOG,OAEHA,MAAMC,uBACAC,iBAAmB9C,KAAK+C,YAAY,iEACrC,MAAM9C,WAAW6C,iBACI,KAAlB7C,QAAQc,+CACqBd,QAAS,YAC/BA,QAAQ+C,UAAUC,SAAS,wDACLhD,QAAS,QAG1CiD,cAAgBlD,KAAK+C,YAAY,mDACrCG,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGzC,GAAIyC,GAAGvC,cAC5DX,SAASmD,SAAS,YAAaL,eAGxCR,kBACStC,SAASmD,SAAS,kBAG3BZ,oBACSvC,SAASmD,SAAS,iBAAkBvD,KAAKqC,WAAW,qBAAqBtB"} \ No newline at end of file diff --git a/amd/build/metadata/contributor.min.js b/amd/build/metadata/contributor.min.js index 13231b971a4..bf05385b359 100644 --- a/amd/build/metadata/contributor.min.js +++ b/amd/build/metadata/contributor.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/contributor",["exports","core/reactive","qtype_stac * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-contributor",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-contributor",this.selectors={DELETE:'[name="smd_delete"]'}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}stateReady(){this.addEventListener(this.getElement(this.selectors.DELETE),"click",this.delete)}delete(event){const id=event.target.id.split("_")[1];this.reactive.dispatch("deleteContributor",id)}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=contributor.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/contributor.min.js.map b/amd/build/metadata/contributor.min.js.map index c3573674ff1..82ae4cb7a1a 100644 --- a/amd/build/metadata/contributor.min.js.map +++ b/amd/build/metadata/contributor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"contributor.min.js","sources":["../../src/metadata/contributor.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-contributor';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,kCACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file +{"version":3,"file":"contributor.min.js","sources":["../../src/metadata/contributor.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-contributor';\n this.selectors = {\n DELETE: `[name=\"smd_delete\"]`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n stateReady() {\n this.addEventListener(\n this.getElement(this.selectors.DELETE),\n 'click',\n this.delete\n );\n }\n\n delete(event) {\n const id = event.target.id.split('_')[1];\n this.reactive.dispatch('deleteContributor', id);\n }\n}"],"names":["BaseComponent","create","name","selectors","DELETE","target","this","element","document","querySelector","reactive","metadata","stateReady","addEventListener","getElement","delete","event","id","split","dispatch"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,kCACPC,UAAY,CACbC,0CAWIC,OAAQF,kBACT,IAAIG,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVR,UAAAA,YAIRS,kBACSC,iBACDP,KAAKQ,WAAWR,KAAKH,UAAUC,QAC/B,QACAE,KAAKS,QAIbA,OAAOC,aACGC,GAAKD,MAAMX,OAAOY,GAAGC,MAAM,KAAK,QACjCR,SAASS,SAAS,oBAAqBF"} \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js b/amd/build/metadata/metadatacontent.min.js index 4c14e535036..e22cdaefce3 100644 --- a/amd/build/metadata/metadatacontent.min.js +++ b/amd/build/metadata/metadatacontent.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/metadatacontent",["exports","core/reactive","qtype_ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",DELETE:"#smd_1_delete"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}stateReady(){this.addEventListener(this.getElement(this.selectors.DELETE),"click",this.delete)}delete(){console.log("Here")}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=metadatacontent.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js.map b/amd/build/metadata/metadatacontent.min.js.map index 34cd4b20103..9097b1acb22 100644 --- a/amd/build/metadata/metadatacontent.min.js.map +++ b/amd/build/metadata/metadatacontent.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n DELETE: `#smd_1_delete`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n stateReady() {\n this.addEventListener(\n this.getElement(this.selectors.DELETE),\n 'click',\n this.delete\n );\n }\n\n delete() {\n console.log('Here');\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","DELETE","target","this","element","document","querySelector","reactive","metadata","stateReady","addEventListener","getElement","delete","console","log"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,oCAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA,YAIRU,kBACSC,iBACDP,KAAKQ,WAAWR,KAAKJ,UAAUE,QAC/B,QACAE,KAAKS,QAIbA,SACIC,QAAQC,IAAI"} \ No newline at end of file +{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js index 6dd53b0b09c..7428e1cb022 100644 --- a/amd/build/metadata/mutations.min.js +++ b/amd/build/metadata/mutations.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/mutations",["exports"],(function(_exports){Object.d * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class{updateAll(stateManager,inputArray){const state=stateManager.state;stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if(0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); +class{updateAll(stateManager,inputArray){const state=stateManager.state;stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if(0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}stateManager.setReadOnly(!0)}deleteContributor(stateManager,id){const state=stateManager.state;stateManager.setReadOnly(!1),state.contributor.delete(id),stateManager.setReadOnly(!0)}addContributor(stateManager){const state=stateManager.state,keys=Array.from(state.contributor);keys.sort(((a,b)=>b[0]-a[0]));const newCon={id:1+parseInt(keys[0][0]),firstName:"",lastName:"",institution:"",year:2025};console.log(keys),stateManager.setReadOnly(!1),state.contributor.add(newCon),stateManager.setReadOnly(!0),console.log(state.contributor)}updateFromJson(stateManager,data){const state=stateManager.state;data=JSON.parse(data),stateManager.setReadOnly(!1);for(const prop in data)state[prop]=data[prop];stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); //# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map index a613a7d985d..cf0af30d9eb 100644 --- a/amd/build/metadata/mutations.min.js.map +++ b/amd/build/metadata/mutations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateAll(stateManager, inputArray) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateAll","stateManager","inputArray","state","setReadOnly","field","parts","split","id","property","subproperty","existing","get"],"mappings":"gKA4CaA,UAAY;;;;;;;;MArBrBC,UAAUC,aAAcC,kBACdC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,OACpB,MAAMC,SAASH,WAAY,OACtBI,MAAQD,MAAM,GAAGE,MAAM,KACvBC,GAAKF,MAAM,GACXG,SAAWH,MAAM,GACjBI,YAAcJ,MAAM,MAChB,GAANE,GAAS,OACHG,SAAWR,MAAMM,UAAUG,IAAIJ,IACjCG,WACAA,SAASD,aAAeL,MAAM,SAGlCF,MAAMM,UAAUC,aAAeL,MAAM,GAG7CJ,aAAaG,aAAY"} \ No newline at end of file +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nclass Mutations {\n updateAll(stateManager, inputArray) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n stateManager.setReadOnly(true);\n }\n deleteContributor(stateManager, id) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n state.contributor.delete(id);\n stateManager.setReadOnly(true);\n }\n\n addContributor(stateManager) {\n const state = stateManager.state;\n const keys = Array.from(state.contributor);\n keys.sort((a, b) => b[0] - a[0]);\n\n const newCon = {\n id: 1 + parseInt(keys[0][0]),\n firstName: \"\",\n lastName: \"\",\n institution: \"\",\n year: 2025\n };\n console.log(keys);\n stateManager.setReadOnly(false);\n state.contributor.add(newCon);\n stateManager.setReadOnly(true);\n console.log(state.contributor);\n }\n\n updateFromJson(stateManager, data) {\n const state = stateManager.state;\n data = JSON.parse(data);\n stateManager.setReadOnly(false);\n for (const prop in data) {\n state[prop] = data[prop];\n }\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","updateAll","stateManager","inputArray","state","setReadOnly","field","parts","split","id","property","subproperty","existing","get","deleteContributor","contributor","delete","addContributor","keys","Array","from","sort","a","b","newCon","parseInt","firstName","lastName","institution","year","console","log","add","updateFromJson","data","JSON","parse","prop"],"mappings":"gKA+EaA,UAAY;;;;;;;;MAxDrBC,UAAUC,aAAcC,kBACdC,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,OACpB,MAAMC,SAASH,WAAY,OACtBI,MAAQD,MAAM,GAAGE,MAAM,KACvBC,GAAKF,MAAM,GACXG,SAAWH,MAAM,GACjBI,YAAcJ,MAAM,MAChB,GAANE,GAAS,OACHG,SAAWR,MAAMM,UAAUG,IAAIJ,IACjCG,WACAA,SAASD,aAAeL,MAAM,SAGlCF,MAAMM,UAAUC,aAAeL,MAAM,GAG7CJ,aAAaG,aAAY,GAE7BS,kBAAkBZ,aAAcO,UACtBL,MAAQF,aAAaE,MAC3BF,aAAaG,aAAY,GACzBD,MAAMW,YAAYC,OAAOP,IACzBP,aAAaG,aAAY,GAG7BY,eAAef,oBACLE,MAAQF,aAAaE,MACrBc,KAAOC,MAAMC,KAAKhB,MAAMW,aAC9BG,KAAKG,MAAK,CAACC,EAAGC,IAAMA,EAAE,GAAKD,EAAE,WAEvBE,OAAS,CACXf,GAAI,EAAIgB,SAASP,KAAK,GAAG,IACzBQ,UAAW,GACXC,SAAU,GACVC,YAAa,GACbC,KAAM,MAEVC,QAAQC,IAAIb,MACZhB,aAAaG,aAAY,GACzBD,MAAMW,YAAYiB,IAAIR,QACtBtB,aAAaG,aAAY,GACzByB,QAAQC,IAAI3B,MAAMW,aAGtBkB,eAAe/B,aAAcgC,YACnB9B,MAAQF,aAAaE,MAC3B8B,KAAOC,KAAKC,MAAMF,MAClBhC,aAAaG,aAAY,OACpB,MAAMgC,QAAQH,KACf9B,MAAMiC,MAAQH,KAAKG,MAEvBnC,aAAaG,aAAY"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 11044b5a40d..1fa6c04add9 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -30,7 +30,9 @@ export default class extends BaseComponent { this.name = 'stack-metadata-container'; this.selectors = { METADATACONTAINER: `[data-for='qtype-stack-metadata']`, - SUBMIT: `#stack-metadata-update`, + UPDATEJSON: `#stack-metadata-update`, + UPDATEINPUTS: `#stack-metadata-update-inputs`, + ADDCONTRIB: `#stack-metadata-add-contrib`, }; } @@ -112,7 +114,7 @@ export default class extends BaseComponent { data.json = { required: true, element: { - value: JSON.stringify(state), + value: JSON.stringify(state, null, 4), attributes: 'rows="10"', wrapperid: 'fitem_metadata_json', id: 'id_metadata_json', @@ -128,10 +130,20 @@ export default class extends BaseComponent { await this.renderComponent(metadataContainer, 'qtype_stack/metadatacontent', data); this.addEventListener( - this.getElement(this.selectors.SUBMIT), + this.getElement(this.selectors.UPDATEJSON), 'click', this.update ); + this.addEventListener( + this.getElement(this.selectors.ADDCONTRIB), + 'click', + this.addContrib + ); + this.addEventListener( + this.getElement(this.selectors.UPDATEINPUTS), + 'click', + this.updateInputs + ); } /** @@ -154,4 +166,12 @@ export default class extends BaseComponent { inputElements = Array.from(inputElements).map((el) => [el.id, el.value]); this.reactive.dispatch('updateAll', inputElements); } + + addContrib() { + this.reactive.dispatch('addContributor'); + } + + updateInputs() { + this.reactive.dispatch('updateFromJson', this.getElement('#id_metadata_json').value); + } } \ No newline at end of file diff --git a/amd/src/metadata/contributor.js b/amd/src/metadata/contributor.js index d9e7d49bfe4..1c29f301b7e 100644 --- a/amd/src/metadata/contributor.js +++ b/amd/src/metadata/contributor.js @@ -28,8 +28,7 @@ export default class extends BaseComponent { create() { this.name = 'stack-metadata-contributor'; this.selectors = { - METADATACONTAINER: `[data-for='qtype-stack-metadata']`, - SUBMIT: `#stack-metadata-update`, + DELETE: `[name="smd_delete"]`, }; } @@ -48,4 +47,16 @@ export default class extends BaseComponent { }); } + stateReady() { + this.addEventListener( + this.getElement(this.selectors.DELETE), + 'click', + this.delete + ); + } + + delete(event) { + const id = event.target.id.split('_')[1]; + this.reactive.dispatch('deleteContributor', id); + } } \ No newline at end of file diff --git a/amd/src/metadata/metadatacontent.js b/amd/src/metadata/metadatacontent.js index d085f8fd213..1f457a48a3c 100644 --- a/amd/src/metadata/metadatacontent.js +++ b/amd/src/metadata/metadatacontent.js @@ -29,7 +29,7 @@ export default class extends BaseComponent { this.name = 'stack-metadata-content'; this.selectors = { METADATACONTAINER: `[data-for='qtype-stack-metadata']`, - DELETE: `#smd_1_delete`, + SUBMIT: `#stack-metadata-update`, }; } @@ -47,16 +47,4 @@ export default class extends BaseComponent { selectors, }); } - - stateReady() { - this.addEventListener( - this.getElement(this.selectors.DELETE), - 'click', - this.delete - ); - } - - delete() { - console.log('Here'); - } } \ No newline at end of file diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js index c5594c389e1..94ddbfb25d8 100644 --- a/amd/src/metadata/mutations.js +++ b/amd/src/metadata/mutations.js @@ -40,6 +40,41 @@ class Mutations { } stateManager.setReadOnly(true); } + deleteContributor(stateManager, id) { + const state = stateManager.state; + stateManager.setReadOnly(false); + state.contributor.delete(id); + stateManager.setReadOnly(true); + } + + addContributor(stateManager) { + const state = stateManager.state; + const keys = Array.from(state.contributor); + keys.sort((a, b) => b[0] - a[0]); + + const newCon = { + id: 1 + parseInt(keys[0][0]), + firstName: "", + lastName: "", + institution: "", + year: 2025 + }; + console.log(keys); + stateManager.setReadOnly(false); + state.contributor.add(newCon); + stateManager.setReadOnly(true); + console.log(state.contributor); + } + + updateFromJson(stateManager, data) { + const state = stateManager.state; + data = JSON.parse(data); + stateManager.setReadOnly(false); + for (const prop in data) { + state[prop] = data[prop]; + } + stateManager.setReadOnly(true); + } } export const mutations = new Mutations(); \ No newline at end of file diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index 2683eaaf5c3..607a6273604 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -419,6 +419,7 @@ $string['creator'] = 'Creator'; $string['addcontributor'] = 'Add contributor'; $string['updateJSON'] = 'Update JSON'; +$string['updateinputs'] = 'Update inputs'; $string['JSONmetadata'] = 'JSON metadata'; // Strings used by input elements. diff --git a/templates/metadatacontent.mustache b/templates/metadatacontent.mustache index 6f8cc2393e6..88f8d9df583 100644 --- a/templates/metadatacontent.mustache +++ b/templates/metadatacontent.mustache @@ -41,7 +41,7 @@ {{/contributor}}
-
+

@@ -51,6 +51,9 @@ +
@@ -62,6 +65,7 @@ {{< core_form/element-template-inline }} {{$element}} + {{/element}} + {{/ core_form/element-template-inline }} + {{/freeform}} +
+
+
-
\ No newline at end of file +
diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index 22899f62117..67e26a9d644 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -17,88 +17,46 @@ Example context (json): { - "creator": { - "firstname": { - "required": false, - "element": { - "value": "Edmund", - "wrapperid": "fitem_smdi_0_creator_firstName", - "id": "smdi_0_creator_firstName", - "name": "smdi_0_creator_firstName", - "iderror": "smde_0_creator_firstName_error" - } - }, - "lastname": { - "required": true, - "element": { - "value": "Farrow", - "wrapperid": "fitem_smdi_0_creator_lastName", - "id": "smdi_0_creator_lastName", - "name": "smdi_0_creator_lastName", - "iderror": "smde_0_creator_lastName_error" - } - }, - "institution": { - "required": false, - "element": { - "value": "UoE", - "wrapperid": "fitem_smdi_0_creator_institution", - "id": "smdi_0_creator_institution", - "name": "smdi_0_creator_institution", - "iderror": "smde_0_creator_institution_error" - } - }, - "year": { - "required": false, - "element": { - "value": "2025", - "wrapperid": "fitem_smdi_0_creator_year", - "id": "smdi_0_creator_year", - "name": "smdi_0_creator_year", - "iderror": "smde_0_creator_year_error" - } - } - }, - "contributor": [ + "author": [ { "firstname": { "required": false, "element": { "value": "a", - "wrapperid": "fitem_smdi_1_contributor_firstName", - "id": "smdi_1_contributor_firstName", - "name": "smdi_1_contributor_firstName", - "iderror": "smde_1_contributor_firstName_error" + "wrapperid": "fitem_smdi_1_author_firstName", + "id": "smdi_1_author_firstName", + "name": "smdi_1_author_firstName", + "iderror": "smde_1_author_firstName_error" } }, "lastname": { "required": true, "element": { "value": "b", - "wrapperid": "fitem_smdi_1_contributor_lastName", - "id": "smdi_1_contributor_lastName", - "name": "smdi_1_contributor_lastName", - "iderror": "smde_1_contributor_lastName_error" + "wrapperid": "fitem_smdi_1_author_lastName", + "id": "smdi_1_author_lastName", + "name": "smdi_1_author_lastName", + "iderror": "smde_1_author_lastName_error" } }, "institution": { "required": false, "element": { "value": "c", - "wrapperid": "fitem_smdi_1_contributor_institution", - "id": "smdi_1_contributor_institution", - "name": "smdi_1_contributor_institution", - "iderror": "smde_1_contributor_institution_error" + "wrapperid": "fitem_smdi_1_author_institution", + "id": "smdi_1_author_institution", + "name": "smdi_1_author_institution", + "iderror": "smde_1_author_institution_error" } }, "year": { "required": false, "element": { "value": "2025", - "wrapperid": "fitem_smdi_1_contributor_year", - "id": "smdi_1_contributor_year", - "name": "smdi_1_contributor_year", - "iderror": "smde_1_contributor_year_error" + "wrapperid": "fitem_smdi_1_author_year", + "id": "smdi_1_author_year", + "name": "smdi_1_author_year", + "iderror": "smde_1_author_year_error" } }, "id": "1" @@ -366,7 +324,7 @@ "json": { "required": true, "element": { - "value": "{\n \"creator\": {\n \"firstName\": \"Edmund\",\n ... ... ... }", + "value": "{\n \"author\": [\n {\n \"firstName\": \"Edmund\",\n ... ... ... }", "attributes": "rows=\"10\"", "wrapperid": "fitem_metadata_json", "id": "id_metadata_json", @@ -385,57 +343,17 @@
{{#str}} institution, qtype_stack {{/str}}
{{#str}} year, qtype_stack {{/str}}
- {{#creator}} -
-
{{#str}} creator, qtype_stack {{/str}}
-
- {{#firstname}} - - {{> core_form/element-text-inline }} - {{/firstname}} -
-
- {{#lastname}} - - {{> core_form/element-text-inline }} - {{/lastname}} -
-
- {{#institution}} - - {{> core_form/element-text-inline }} - {{/institution}} -
-
- {{#year}} - - {{>qtype_stack/metadata/metadatayear}} - {{/year}} -
-
- {{/creator}} - {{#contributor}} - {{>qtype_stack/metadata/metadatacontributor}} - {{/contributor}} + {{#author}} + {{>qtype_stack/metadata/metadataauthor}} + {{/author}}
- - -
@@ -554,4 +472,4 @@ require(['qtype_stack/metadata/metadatacontent'], function(component) { component.init('#qtype-stack-metadata-content'); }); -{{/js}} \ No newline at end of file +{{/js}} diff --git a/templates/metadata/metadatayear.mustache b/templates/metadata/metadatayear.mustache index 5952d366468..3ea810f03d2 100644 --- a/templates/metadata/metadatayear.mustache +++ b/templates/metadata/metadatayear.mustache @@ -18,10 +18,10 @@ "required": false, "element": { "value": "2025", - "wrapperid": "fitem_smdi_1_contributor_year", - "id": "smdi_1_contributor_year", - "name": "smdi_1_contributor_year", - "iderror": "smde_1_contributor_year_error" + "wrapperid": "fitem_smdi_1_author_year", + "id": "smdi_1_author_year", + "name": "smdi_1_author_year", + "iderror": "smde_1_author_year_error" } } }} @@ -45,4 +45,4 @@ {{/required}} {{{element.attributes}}}> {{/element}} -{{/ core_form/element-template-inline }} \ No newline at end of file +{{/ core_form/element-template-inline }} diff --git a/tests/behat/metadata.feature b/tests/behat/metadata.feature index d774a10ff8e..f7b1bdb0cb7 100644 --- a/tests/behat/metadata.feature +++ b/tests/behat/metadata.feature @@ -27,16 +27,18 @@ Feature: Create and edit STACK metadata When I am on the "Algebraic input" "core_question > edit" page logged in as teacher And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." - And I set the field "smdi_0_creator_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Edmund" - And I set the field "smdi_0_creator_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Farrow" - And I set the field "smdi_0_creator_institution" in the "#qtype-stack-metadata-content" "css_element" to "UoE" - And I set the field "smdi_0_creator_year" in the "#qtype-stack-metadata-content" "css_element" to "2025" - And I click on "Add contributor" "button" - And I wait until "smdi_1_contributor_firstName" "field" exists - And I set the field "smdi_1_contributor_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Bob" - And I set the field "smdi_1_contributor_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Smith" - And I set the field "smdi_1_contributor_institution" in the "#qtype-stack-metadata-content" "css_element" to "MIT" - And I set the field "smdi_1_contributor_year" in the "#qtype-stack-metadata-content" "css_element" to "2026" + And I click on "Add author" "button" + And I wait until "smdi_1_author_firstName" "field" exists + And I set the field "smdi_1_author_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Edmund" + And I set the field "smdi_1_author_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Farrow" + And I set the field "smdi_1_author_institution" in the "#qtype-stack-metadata-content" "css_element" to "UoE" + And I set the field "smdi_1_author_year" in the "#qtype-stack-metadata-content" "css_element" to "2025" + And I click on "Add author" "button" + And I wait until "smdi_2_author_firstName" "field" exists + And I set the field "smdi_2_author_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Bob" + And I set the field "smdi_2_author_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Smith" + And I set the field "smdi_2_author_institution" in the "#qtype-stack-metadata-content" "css_element" to "MIT" + And I set the field "smdi_2_author_year" in the "#qtype-stack-metadata-content" "css_element" to "2026" And I click on "Add language" "button" And I wait until "smdi_1_language_value" "field" exists And I set the field "smdi_1_language_value" in the "#qtype-stack-metadata-content" "css_element" to "en" @@ -51,10 +53,10 @@ Feature: Create and edit STACK metadata And I set the field "smdi_1_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Breed" And I set the field "smdi_1_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Al$%&^" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." - And I click on "Add me as a contributor" "button" + And I click on "Add me as an author" "button" And I click on "Add language" "button" And I wait until "smdi_2_language_value" "field" exists And I set the field "smdi_2_language_value" in the "#qtype-stack-metadata-content" "css_element" to "fr" @@ -75,14 +77,14 @@ Feature: Create and edit STACK metadata And I set the field "smdi_4_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Things" And I set the field "smdi_4_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "AAA" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"More data":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"More data":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I set the field "smdi_3_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Changed" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." - And I click on "smd_contributor_1_delete" "button" + And I click on "smd_author_2_delete" "button" And I should not see "Smith" And I set the field "smdi_1_language_value" in the "#qtype-stack-metadata-content" "css_element" to "en-del" And I click on "smd_language_1_delete" "button" @@ -93,7 +95,7 @@ Feature: Create and edit STACK metadata And I wait until "smdi_3_language_value" "field" exists And I set the field "smdi_3_language_value" in the "#qtype-stack-metadata-content" "css_element" to "it" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I click on "smd_scope_1_delete" "button" @@ -104,7 +106,7 @@ Feature: Create and edit STACK metadata And I should see "Required" in the "#smde_5_additional_property_error" "css_element" And I click on "smd_additional_5_delete" "button" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I click on "Add scope" "button" @@ -129,19 +131,19 @@ Feature: Create and edit STACK metadata And I set the field "smdi_8_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "" And I set the field "smdi_8_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "BBB3" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}},"Another Scope":{"Question2":{"Type2":"MC2"},"More data":{"Things2":"AAA2","Type2":"BBB2"},"No obj":"BBB3"}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}},"Another Scope":{"Question2":{"Type2":"MC2"},"More data":{"Things2":"AAA2","Type2":"BBB2"},"No obj":"BBB3"}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I set the field "id_metadata_json" to multiline: """ { - "creator": { - "firstName": "Bob", - "lastName": "Smith", - "institution": "MIT", - "year": "2024" - }, - "contributor": [ + "author": [ + { + "firstName": "Bob", + "lastName": "Smith", + "institution": "MIT", + "year": "2024" + }, { "firstName": "Mike", "lastName": "Jones", @@ -177,7 +179,7 @@ Feature: Create and edit STACK metadata """ And I click on "Update inputs from JSON" "button" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I click on "smd_property_5_add" "button" @@ -191,7 +193,7 @@ Feature: Create and edit STACK metadata And I set the field "smdi_7_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "" And I set the field "smdi_7_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Champion" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I click on "smd_property_1_add" "button" @@ -204,13 +206,12 @@ Feature: Create and edit STACK metadata And I set the field "id_metadata_json" to multiline: """ { - "creator": { - "firstName": "Bo1b", - "lastName": "Smi1th", - "institution": "MI1T", - "year": "2024" - }, - "contributor": [ + "author": { + "firstName": "Bo1b", + "lastName": "Smi1th", + "institution": "MI1T", + "year": "2024" + }, { "firstName": "Mi1ke", "lastName": "Jon1es", @@ -248,18 +249,18 @@ Feature: Create and edit STACK metadata } """ And I click on "Update inputs from JSON" "button" - And I wait until "smdi_2_contributor_firstName" "field" exists + And I wait until "smdi_2_author_firstName" "field" exists And I click on "Revert current changes" "button" And I should not see "Lowell" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' And I press "id_updatebutton" Given the site is running Moodle version 4.6 or lower Then I should see "Version 2" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' Given the site is running Moodle version 5.0 or higher Then I should see "v2 (latest)" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' @javascript Scenario: Create and edit STACK freeform metadata @@ -269,13 +270,13 @@ Feature: Create and edit STACK metadata And I set the field "id_metadata_json" to multiline: """ { - "creator": { - "firstName": "Bob", - "lastName": "Smith", - "institution": "MIT", - "year": "2024" - }, - "contributor": [ + "author": [ + { + "firstName": "Bob", + "lastName": "Smith", + "institution": "MIT", + "year": "2024" + }, { "firstName": "Mike", "lastName": "Jones", @@ -337,7 +338,7 @@ Feature: Create and edit STACK metadata And I click on "Update inputs from JSON" "button" And I should see "{\"license\":{\"Cat\":{\"Breed\":\"Al$%&^\"},\"Horse\":\"Dobbin\",\"Dog\":{\"Teeth\":\"50\",\"Tails\":\"1\"}},\"Freeform too\":{\"Fish\":{\"Gills\":\"2\"}}}" And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"license":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Freeform too":{"Fish":{"Gills":"2"}}}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"license":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Freeform too":{"Fish":{"Gills":"2"}}}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I set the field "smdi_0_freeform_value" to multiline: @@ -345,7 +346,7 @@ Feature: Create and edit STACK metadata {"x":[{"additional":"b"},{"license":"d"}]} """ And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"x":[{"additional":"b"},{"license":"d"}]}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"x":[{"additional":"b"},{"license":"d"}]}}' @javascript Scenario: New question metadata @@ -356,4 +357,4 @@ Feature: Create and edit STACK metadata And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." And I click on "Validate and close" "button" - And I check the hidden input "metadata" is '{"creator":{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"},"contributor":[],"language":["en"],"license":"unknown","isPartOf":"","additional":{},"freeform":{}}' + And I check the hidden input "metadata" is '{"author":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en"],"license":"unknown","isPartOf":"","additional":{},"freeform":{}}' diff --git a/tests/jest/metadata.container.test.js b/tests/jest/metadata.container.test.js index 363b3c3990d..7fdf6fdef0f 100644 --- a/tests/jest/metadata.container.test.js +++ b/tests/jest/metadata.container.test.js @@ -60,35 +60,17 @@ describe('createDataElement', () => { test('element IDs are composed from id and tag', () => { const {instance} = makeInstance(); - const el = instance.createDataElement(false, 3, 'contributor_lastName', 'Smith'); - expect(el.element.id).toBe('smdi_3_contributor_lastName'); - expect(el.element.name).toBe('smdi_3_contributor_lastName'); - expect(el.element.wrapperid).toBe('fitem_smdi_3_contributor_lastName'); - expect(el.element.iderror).toBe('smde_3_contributor_lastName_error'); + const el = instance.createDataElement(false, 3, 'author_lastName', 'Smith'); + expect(el.element.id).toBe('smdi_3_author_lastName'); + expect(el.element.name).toBe('smdi_3_author_lastName'); + expect(el.element.wrapperid).toBe('fitem_smdi_3_author_lastName'); + expect(el.element.iderror).toBe('smde_3_author_lastName_error'); }); test('works with id=0 for single-instance elements', () => { const {instance} = makeInstance(); - const el = instance.createDataElement(true, 0, 'creator_firstName', 'Alice'); - expect(el.element.id).toBe('smdi_0_creator_firstName'); - }); -}); - -// ── makeCreator ──────────────────────────────────────────────────────────────── - -describe('makeCreator', () => { - test('populates all four creator fields from metadata.lib.user and current year', () => { - const {instance} = makeInstance(); - const fields = {}; - instance.getElement.mockImplementation(sel => { - fields[sel] = {value: ''}; - return fields[sel]; - }); - instance.makeCreator(); - expect(fields['#smdi_0_creator_firstName'].value).toBe('Jane'); - expect(fields['#smdi_0_creator_lastName'].value).toBe('Doe'); - expect(fields['#smdi_0_creator_institution'].value).toBe('Test University'); - expect(fields['#smdi_0_creator_year'].value).toBe(new Date().getFullYear()); + const el = instance.createDataElement(true, 0, 'license_value', 'MIT'); + expect(el.element.id).toBe('smdi_0_license_value'); }); }); @@ -107,7 +89,9 @@ describe('update', () => { const {instance, reactive} = makeInstance(); const allInputs = [makeInputEl('smdi_0_license_value', 'MIT')]; instance.getElements.mockImplementation(sel => { - if (sel === instance.selectors.ALLINPUTS) return allInputs; + if (sel === instance.selectors.ALLINPUTS) { + return allInputs; + } return []; }); reactive.dispatch.mockResolvedValue(undefined); @@ -118,9 +102,11 @@ describe('update', () => { test('returns false and notifies when a required field is empty', async () => { const {instance, reactive} = makeInstance(); - const emptyEl = makeInputEl('smdi_0_creator_lastName', ''); + const emptyEl = makeInputEl('smdi_1_language_value', ''); instance.getElements.mockImplementation(sel => { - if (sel === instance.selectors.REQUIREDINPUTS) return [emptyEl]; + if (sel === instance.selectors.REQUIREDINPUTS) { + return [emptyEl]; + } return []; }); const result = await instance.update(true); @@ -131,10 +117,14 @@ describe('update', () => { test('clears error notification for an is-invalid field that now has a value', async () => { const {instance, reactive} = makeInstance(); - const fixedEl = makeInputEl('smdi_0_creator_lastName', 'Smith', ['is-invalid']); + const fixedEl = makeInputEl('smdi_1_language_value', 'en', ['is-invalid']); instance.getElements.mockImplementation(sel => { - if (sel === instance.selectors.REQUIREDINPUTS) return [fixedEl]; - if (sel === instance.selectors.ALLINPUTS) return [fixedEl]; + if (sel === instance.selectors.REQUIREDINPUTS) { + return [fixedEl]; + } + if (sel === instance.selectors.ALLINPUTS) { + return [fixedEl]; + } return []; }); reactive.dispatch.mockResolvedValue(undefined); @@ -147,7 +137,9 @@ describe('update', () => { const freeformEl = {value: '{bad json', classList: {contains: () => false}}; instance.getElements.mockReturnValue([]); instance.getElement.mockImplementation(sel => { - if (sel === '#smdi_0_freeform_value') return freeformEl; + if (sel === '#smdi_0_freeform_value') { + return freeformEl; + } return null; }); const result = await instance.update(true); @@ -159,13 +151,20 @@ describe('update', () => { test('clears freeform error and dispatches when freeform JSON is valid', async () => { const {instance, reactive} = makeInstance(); const freeformEl = {value: '{"ok":true}', classList: {contains: () => false}}; + const authorEl = makeInputEl('smdi_1_author_firstName', 'Alice'); instance.getElements.mockImplementation(sel => { - if (sel === instance.selectors.REQUIREDINPUTS) return []; - if (sel === instance.selectors.ALLINPUTS) return []; + if (sel === instance.selectors.REQUIREDINPUTS) { + return []; + } + if (sel === instance.selectors.ALLINPUTS) { + return [authorEl]; + } return []; }); instance.getElement.mockImplementation(sel => { - if (sel === '#smdi_0_freeform_value') return freeformEl; + if (sel === '#smdi_0_freeform_value') { + return freeformEl; + } return null; }); reactive.dispatch.mockResolvedValue(undefined); @@ -177,7 +176,6 @@ describe('update', () => { test('returns false and notifies per-element when dispatch throws comma-separated IDs', async () => { const {instance, reactive} = makeInstance(); instance.getElements.mockReturnValue([]); - instance.getElement.mockReturnValue(null); reactive.dispatch.mockRejectedValue('2,5'); const errorEl = {value: ''}; instance.getElement.mockImplementation(sel => { @@ -198,7 +196,7 @@ describe('addItem', () => { test('does not dispatch when update() returns false', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(false); - await instance.addItem({target: {id: 'smd_add_1_language'}}); + await instance.addItem({target: {id: 'smd_language_0_add'}}); expect(reactive.dispatch).not.toHaveBeenCalled(); }); @@ -206,9 +204,9 @@ describe('addItem', () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(true); reactive.dispatch.mockResolvedValue(undefined); - await instance.addItem({target: {id: 'smd_add_contributor_user'}}); - // id.split('_') → ['smd', 'add', 'contributor', 'user']; parts[1]='add', parts[2]='contributor' - expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'add', 'contributor'); + await instance.addItem({target: {id: 'smd_add_author_user'}}); + // id.split('_') → ['smd', 'add', 'author', 'user']; parts[1]='add', parts[2]='author' + expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'add', 'author'); }); }); @@ -218,7 +216,7 @@ describe('deleteItem', () => { test('does not dispatch when update() returns false', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(false); - await instance.deleteItem({target: {id: 'smd_delete_1_language'}}); + await instance.deleteItem({target: {id: 'smd_author_1_delete'}}); expect(reactive.dispatch).not.toHaveBeenCalled(); }); @@ -226,27 +224,27 @@ describe('deleteItem', () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(true); reactive.dispatch.mockResolvedValue(undefined); - await instance.deleteItem({target: {id: 'smd_delete_1_language'}}); - expect(reactive.dispatch).toHaveBeenCalledWith('deleteRow', 'delete', '1'); + await instance.deleteItem({target: {id: 'smd_author_1_delete'}}); + expect(reactive.dispatch).toHaveBeenCalledWith('deleteRow', 'author', '1'); }); }); -// ── makeContributor ──────────────────────────────────────────────────────────── +// ── makeAuthor ──────────────────────────────────────────────────────────────── -describe('makeContributor', () => { +describe('makeAuthor', () => { test('does not dispatch when update() returns false', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(false); - await instance.makeContributor(); + await instance.makeAuthor(); expect(reactive.dispatch).not.toHaveBeenCalled(); }); - test('dispatches addItem for contributor/user when update succeeds', async () => { + test('dispatches addItem for author/user when update succeeds', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(true); reactive.dispatch.mockResolvedValue(undefined); - await instance.makeContributor(); - expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'contributor', 'user'); + await instance.makeAuthor(); + expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'author', 'user'); }); }); @@ -306,7 +304,9 @@ describe('revert', () => { metadata.jsonToState.mockImplementation(() => { // First call: parsing the saved (invalid) JSON → throws. // Second call: inside the catch block for the dispatch argument (valid '{}') → succeeds. - if (callCount++ === 0) throw parseError; + if (callCount++ === 0) { + throw parseError; + } return {}; }); reactive.dispatch.mockResolvedValue(undefined); @@ -324,9 +324,8 @@ describe('reloadContainerComponent', () => { function makeState(overrides = {}) { const items = (arr) => ({forEach: (cb) => arr.forEach(cb)}); return { - creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + author: items([{id: 1, firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}]), language: items([{id: 1, value: 'en'}]), - contributor: items([{id: 1, firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}]), additional: items([]), license: {value: 'cc-by'}, isPartOf: {value: 'course1'}, @@ -401,37 +400,58 @@ describe('reloadContainerComponent', () => { expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.deleteItem); }); - // ── data.creator ───────────────────────────────────────────────────────────── + // ── data.author ───────────────────────────────────────────────────────────── - describe('data.creator', () => { - test('maps all four creator fields with correct values', async () => { + describe('data.author', () => { + test('maps all four author fields with correct values', async () => { const {instance} = makeInstance(); setupForRender(instance); const data = await captureData(instance, makeState()); - expect(data.creator.firstname.element.value).toBe('Alice'); - expect(data.creator.lastname.element.value).toBe('Smith'); - expect(data.creator.institution.element.value).toBe('Uni'); - expect(data.creator.year.element.value).toBe('2025'); + expect(data.author[0].firstname.element.value).toBe('Alice'); + expect(data.author[0].lastname.element.value).toBe('Smith'); + expect(data.author[0].institution.element.value).toBe('Uni'); + expect(data.author[0].year.element.value).toBe('2025'); }); test('lastname is not required; firstname, institution and year are not', async () => { const {instance} = makeInstance(); setupForRender(instance); const data = await captureData(instance, makeState()); - expect(data.creator.lastname.required).toBe(false); - expect(data.creator.firstname.required).toBe(false); - expect(data.creator.institution.required).toBe(false); - expect(data.creator.year.required).toBe(false); + expect(data.author[0].lastname.required).toBe(false); + expect(data.author[0].firstname.required).toBe(false); + expect(data.author[0].institution.required).toBe(false); + expect(data.author[0].year.required).toBe(false); }); - test('element IDs follow the creator_* naming convention', async () => { + test('element IDs follow the author_* naming convention', async () => { const {instance} = makeInstance(); setupForRender(instance); const data = await captureData(instance, makeState()); - expect(data.creator.firstname.element.id).toBe('smdi_0_creator_firstName'); - expect(data.creator.lastname.element.id).toBe('smdi_0_creator_lastName'); - expect(data.creator.institution.element.id).toBe('smdi_0_creator_institution'); - expect(data.creator.year.element.id).toBe('smdi_0_creator_year'); + expect(data.author[0].firstname.element.id).toBe('smdi_1_author_firstName'); + expect(data.author[0].lastname.element.id).toBe('smdi_1_author_lastName'); + expect(data.author[0].institution.element.id).toBe('smdi_1_author_institution'); + expect(data.author[0].year.element.id).toBe('smdi_1_author_year'); + }); + + test('multiple authors produce multiple entries', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + author: rows([ + {id: 1, firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + {id: 2, firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.author).toHaveLength(2); + expect(data.author[1].firstname.element.value).toBe('Bob'); + }); + + test('empty author list produces an empty array', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({author: rows([])})); + expect(data.author).toEqual([]); }); }); @@ -465,39 +485,6 @@ describe('reloadContainerComponent', () => { }); }); - // ── data.contributor ────────────────────────────────────────────────────────── - - describe('data.contributor', () => { - test('single contributor has all four fields mapped', async () => { - const {instance} = makeInstance(); - setupForRender(instance); - const data = await captureData(instance, makeState()); - expect(data.contributor).toHaveLength(1); - expect(data.contributor[0].id).toBe(1); - expect(data.contributor[0].firstname.element.value).toBe('Bob'); - expect(data.contributor[0].lastname.element.value).toBe('Jones'); - expect(data.contributor[0].institution.element.value).toBe('Uni2'); - expect(data.contributor[0].year.element.value).toBe('2026'); - }); - - test('contributor lastname is not required; firstname, institution and year are not', async () => { - const {instance} = makeInstance(); - setupForRender(instance); - const data = await captureData(instance, makeState()); - expect(data.contributor[0].lastname.required).toBe(false); - expect(data.contributor[0].firstname.required).toBe(false); - expect(data.contributor[0].institution.required).toBe(false); - expect(data.contributor[0].year.required).toBe(false); - }); - - test('empty contributor list produces an empty array', async () => { - const {instance} = makeInstance(); - setupForRender(instance); - const data = await captureData(instance, makeState({contributor: rows([])})); - expect(data.contributor).toEqual([]); - }); - }); - // ── data.license ────────────────────────────────────────────────────────────── describe('data.license', () => { @@ -754,7 +741,9 @@ describe('reloadContainerComponent', () => { const jsonInputEl = {value: ''}; instance.renderComponent.mockResolvedValue(undefined); instance.getElement.mockImplementation(sel => { - if (sel === instance.selectors.JSONINPUT) return jsonInputEl; + if (sel === instance.selectors.JSONINPUT) { + return jsonInputEl; + } return fakeContainer; }); instance.getElements.mockReturnValue([]); diff --git a/tests/jest/metadata.mutations.test.js b/tests/jest/metadata.mutations.test.js index 543159db544..68198d25ed6 100644 --- a/tests/jest/metadata.mutations.test.js +++ b/tests/jest/metadata.mutations.test.js @@ -18,8 +18,7 @@ class MockStateCollection { function makeState(overrides = {}) { return { - creator: {firstName: '', lastName: '', institution: '', year: ''}, - contributor: new MockStateCollection(), + author: new MockStateCollection(), language: new MockStateCollection(), license: {id: '', value: ''}, isPartOf: {id: '', value: ''}, @@ -41,10 +40,10 @@ describe('updateFromJson', () => { const state = makeState(); const stateManager = makeStateManager(state); mutations.updateFromJson(stateManager, { - creator: {firstName: 'Alice'}, + author: [{firstName: 'Alice'}], license: {id: '', value: 'MIT'}, }); - expect(state.creator).toEqual({firstName: 'Alice'}); + expect(state.author).toEqual([{firstName: 'Alice'}]); expect(state.license).toEqual({id: '', value: 'MIT'}); }); @@ -65,15 +64,15 @@ describe('updateFromJson', () => { // ── deleteRow ────────────────────────────────────────────────────────────────── describe('deleteRow', () => { - test('removes the specified contributor entry and leaves others intact', () => { - const contributor = new MockStateCollection([ + test('removes the specified author entry and leaves others intact', () => { + const author = new MockStateCollection([ {id: 1, firstName: 'Alice'}, {id: 2, firstName: 'Bob'}, ]); - const stateManager = makeStateManager(makeState({contributor})); - mutations.deleteRow(stateManager, 'contributor', '1'); - expect(stateManager.state.contributor.get('1')).toBeUndefined(); - expect(stateManager.state.contributor.get('2')).toBeDefined(); + const stateManager = makeStateManager(makeState({author})); + mutations.deleteRow(stateManager, 'author', '1'); + expect(stateManager.state.author.get('1')).toBeUndefined(); + expect(stateManager.state.author.get('2')).toBeDefined(); }); test('removes the specified language entry and leaves others intact', () => { @@ -136,27 +135,60 @@ describe('addItem', () => { expect(stateManager.state.language.get('2')).toMatchObject({value: '', id: 2}); }); - test('contributor: adds a blank entry when id is not "user"', () => { - const contributor = new MockStateCollection(); - const stateManager = makeStateManager(makeState({contributor})); - mutations.addItem(stateManager, 'contributor', 'blank'); - const item = stateManager.state.contributor.get('1'); + test('author: adds a blank entry when id is not "user"', () => { + const author = new MockStateCollection(); + const stateManager = makeStateManager(makeState({author})); + mutations.addItem(stateManager, 'author', 'blank'); + const item = stateManager.state.author.get('1'); expect(item.firstName).toBe(''); expect(item.lastName).toBe(''); expect(item.institution).toBe(''); expect(item.year).toBe(String(new Date().getFullYear())); }); - test('contributor: prefills user data when id is "user"', () => { - const contributor = new MockStateCollection(); - const stateManager = makeStateManager(makeState({contributor})); - mutations.addItem(stateManager, 'contributor', 'user'); - const item = stateManager.state.contributor.get('1'); + test('author: prefills user data when id is "user"', () => { + const author = new MockStateCollection(); + const stateManager = makeStateManager(makeState({author})); + mutations.addItem(stateManager, 'author', 'user'); + const item = stateManager.state.author.get('1'); expect(item.firstName).toBe('Jane'); expect(item.lastName).toBe('Doe'); expect(item.institution).toBe('Test University'); }); + test('author: does not add a second blank entry while one already exists', () => { + const author = new MockStateCollection([ + {id: 1, firstName: 'Alice', lastName: 'Author', institution: 'Uni', year: '2024'}, + {id: 2, firstName: ' ', lastName: '', institution: '', year: '2025'}, + ]); + const stateManager = makeStateManager(makeState({author})); + mutations.addItem(stateManager, 'author', 'blank'); + expect(stateManager.state.author.get('2')).toEqual({ + id: 2, + firstName: ' ', + lastName: '', + institution: '', + year: '2025', + }); + expect(stateManager.state.author.get('3')).toBeUndefined(); + }); + + test('author: reuses a blank entry for "user" instead of adding a new one', () => { + const author = new MockStateCollection([ + {id: 1, firstName: ' ', lastName: '', institution: '', year: '2023'}, + ]); + const stateManager = makeStateManager(makeState({author})); + mutations.addItem(stateManager, 'author', 'user'); + expect(stateManager.state.author.get('1')).toEqual({ + id: 1, + firstName: 'Jane', + lastName: 'Doe', + institution: 'Test University', + year: String(new Date().getFullYear()), + }); + expect(stateManager.state.author.get('2')).toBeUndefined(); + }); + test('scope: adds a blank additional entry with all empty fields', () => { const additional = new MockStateCollection(); const stateManager = makeStateManager(makeState({additional})); @@ -194,8 +226,8 @@ describe('addItem', () => { describe('updateAll', () => { test('updates a scalar field (id=0) directly on the state property', async () => { const state = makeState(); - await mutations.updateAll(makeStateManager(state), [['smdi_0_creator_firstName', 'Alice']]); - expect(state.creator.firstName).toBe('Alice'); + await mutations.updateAll(makeStateManager(state), [['smdi_0_license_value', 'MIT']]); + expect(state.license.value).toBe('MIT'); }); test('updates a collection item by id', async () => { @@ -324,13 +356,14 @@ describe('updateAll', () => { }); test('processes multiple fields in a single call', async () => { - const state = makeState(); + const author = new MockStateCollection([{id: 1, firstName: '', lastName: '', institution: '', year: ''}]); + const state = makeState({author}); await mutations.updateAll(makeStateManager(state), [ - ['smdi_0_creator_firstName', 'Alice'], - ['smdi_0_creator_lastName', 'Smith'], + ['smdi_1_author_firstName', 'Alice'], + ['smdi_1_author_lastName', 'Smith'], ]); - expect(state.creator.firstName).toBe('Alice'); - expect(state.creator.lastName).toBe('Smith'); + expect(state.author.get('1').firstName).toBe('Alice'); + expect(state.author.get('1').lastName).toBe('Smith'); }); test('opens and closes read-only around the update', async () => { diff --git a/tests/jest/metadata.test.js b/tests/jest/metadata.test.js index e8cfe0a33ea..0003ae65959 100644 --- a/tests/jest/metadata.test.js +++ b/tests/jest/metadata.test.js @@ -3,14 +3,20 @@ const {metadata} = require('../../amd/src/metadata/metadata.js'); // ── reviver ──────────────────────────────────────────────────────────────────── describe('reviver', () => { - test('contributor: assigns sequential ids to each item', () => { + test('author: assigns sequential ids to each item', () => { const input = [{firstName: 'Alice'}, {firstName: 'Bob'}]; - expect(metadata.reviver('contributor', input)).toEqual([ + expect(metadata.reviver('author', input)).toEqual([ {firstName: 'Alice', id: 1}, {firstName: 'Bob', id: 2}, ]); }); + test('author: wraps a single object and assigns an id', () => { + expect(metadata.reviver('author', {firstName: 'Alice'})).toEqual([ + {firstName: 'Alice', id: 1}, + ]); + }); + test('language: converts array of strings to [{id, value}] objects', () => { expect(metadata.reviver('language', ['en', 'fr'])).toEqual([ {id: 1, value: 'en'}, @@ -190,25 +196,32 @@ describe('tidyObject', () => { describe('jsonToState', () => { test('empty JSON produces a fully-defaulted state', () => { expect(metadata.jsonToState('{}')).toEqual({ - creator: {firstName: '', lastName: '', institution: '', year: ''}, - contributor: [], - language: [], - license: {id: '', value: ''}, - isPartOf: {id: '', value: ''}, - additional: [], - freeform: {id: '', value: ''}, + author: [{id: '', firstName: '', lastName: '', institution: '', year: ''}], + language: [], + license: {id: '', value: ''}, + isPartOf: {id: '', value: ''}, + additional: [], + freeform: {id: '', value: ''}, + }); + }); + + test('parses author fields and coerces values to strings', () => { + const input = JSON.stringify({ + author: [{firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: 2025}], }); + expect(metadata.jsonToState(input).author) + .toEqual([{id: '1', firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}]); }); - test('parses creator fields and coerces values to strings', () => { - const input = JSON.stringify({creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: 2025}}); - expect(metadata.jsonToState(input).creator) - .toEqual({firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}); + test('parses single author object into an array with one item', () => { + const input = JSON.stringify({author: {firstName: 'Alice', lastName: 'Smith'}}); + expect(metadata.jsonToState(input).author) + .toEqual([{id: '1', firstName: 'Alice', lastName: 'Smith', institution: '', year: ''}]); }); - test('strips unrecognised creator fields', () => { - const input = JSON.stringify({creator: {firstName: 'Alice', unknownField: 'ignored'}}); - expect(metadata.jsonToState(input).creator).not.toHaveProperty('unknownField'); + test('strips unrecognised author fields', () => { + const input = JSON.stringify({author: [{firstName: 'Alice', unknownField: 'ignored'}]}); + expect(metadata.jsonToState(input).author[0]).not.toHaveProperty('unknownField'); }); test('parses language strings into [{id, value}] objects', () => { @@ -217,15 +230,6 @@ describe('jsonToState', () => { .toEqual([{id: '1', value: 'en'}, {id: '2', value: 'fr'}]); }); - test('parses contributors and assigns sequential string ids', () => { - const input = JSON.stringify({contributor: [{firstName: 'Bob', lastName: 'Jones'},{firstName: 'Dave', lastName: 'Smith'}]}); - expect(metadata.jsonToState(input).contributor) - .toEqual([ - {id: '1', firstName: 'Bob', lastName: 'Jones', institution: '', year: ''}, - {id: '2', firstName: 'Dave', lastName: 'Smith', institution: '', year: ''} - ]); - }); - test('parses license string into {id, value}', () => { const input = JSON.stringify({license: 'MIT'}); expect(metadata.jsonToState(input).license).toEqual({id: '', value: 'MIT'}); @@ -242,20 +246,20 @@ describe('jsonToState', () => { }); test('parses additional unqualified entries in array form', () => { - const input = JSON.stringify({additional: {dc: {subject: ['math','physics']}}}); + const input = JSON.stringify({additional: {dc: {subject: ['math', 'physics']}}}); expect(metadata.jsonToState(input).additional) .toEqual([ - {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, - {id: 2, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, ]); }); test('parses additional qualified entries in array form', () => { - const input = JSON.stringify({additional: {dc: {subject: {qual: ['math','physics']}}}}); + const input = JSON.stringify({additional: {dc: {subject: {qual: ['math', 'physics']}}}}); expect(metadata.jsonToState(input).additional) .toEqual([ - {id: 1, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'math'}, - {id: 2, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'physics'} + {id: 1, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'physics'}, ]); }); @@ -283,7 +287,7 @@ describe('jsonToState', () => { .toEqual({id: '', value: '{"custom":"data"}'}); }); - test('parses freeform object containing key waords into {id, value} where value is JSON string', () => { + test('parses freeform object containing key words into {id, value}', () => { const input = JSON.stringify({freeform: {additional: 'data'}}); expect(metadata.jsonToState(input).freeform) .toEqual({id: '', value: '{"additional":"data"}'}); @@ -294,9 +298,11 @@ describe('jsonToState', () => { describe('jsonStringify', () => { const baseState = { - creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, - contributor: [{firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}], - language: [{id: 1, value: 'en'},{id: 2, value: 'fr'}], + author: [ + {id: '1', firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + {id: '2', firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}, + ], + language: [{id: 1, value: 'en'}, {id: 2, value: 'fr'}], license: {id: 0, value: 'MIT'}, isPartOf: {id: 0, value: 'course1'}, additional: [ @@ -306,10 +312,12 @@ describe('jsonStringify', () => { freeform: {id: '', value: '{"times":[1,2,3]}'}, }; - test('round-trips creator fields unchanged', () => { + test('round-trips author fields unchanged and strips ids', () => { const result = JSON.parse(metadata.jsonStringify(baseState)); - expect(result.creator).toEqual({firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}); - expect(result.contributor).toEqual([{firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}]); + expect(result.author).toEqual([ + {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + {firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}, + ]); }); test('converts language objects back to string array', () => { @@ -355,7 +363,7 @@ describe('jsonStringify', () => { test('respects spacing parameter for pretty-printing', () => { const result = metadata.jsonStringify(baseState, 2); expect(result).toContain('\n'); - expect(result).toContain('\n "creator'); + expect(result).toContain('\n "author"'); }); test('produces compact output with no spacing', () => { From 1b4a08fc80b31949dc0e0b1add39a920cdec0b43 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 16 Jun 2026 11:10:04 +0100 Subject: [PATCH 068/100] iss1171 - Fix initial metadata display --- amd/build/metadata/metadata.min.js | 2 +- amd/build/metadata/metadata.min.js.map | 2 +- amd/src/metadata/metadata.js | 2 +- templates/metadata/metadatacontent.mustache | 16 +++++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/amd/build/metadata/metadata.min.js b/amd/build/metadata/metadata.min.js index a07c6981eaa..95cec59f5fa 100644 --- a/amd/build/metadata/metadata.min.js +++ b/amd/build/metadata/metadata.min.js @@ -1,3 +1,3 @@ -define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=void 0;class StackMetadata extends _reactive.Reactive{constructor(){var obj,key,value;super(...arguments),value={languages:["en"],user:{firstname:"",lastname:"",institution:"",year:""},licenses:[{value:"unknown",text:"unknown"}],placeholder:""},(key="lib")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}loadState(){var _metadata$value;let metadata=document.querySelector('input[name="metadata"]');const metadataJSON=null!==(_metadata$value=metadata.value)&&void 0!==_metadata$value?_metadata$value:null;try{this.lib=JSON.parse(metadata.dataset.lib),this.lib.user.year=(new Date).getFullYear();let languages=new Set(this.lib.languages);this.lib.languages=languages.difference(new Set([null,void 0,""])),this.lib.languages=Array.from(this.lib.languages)}catch(e){}try{metadata=this.jsonToState(metadataJSON)}catch(e){this.lib.brokenMetadata=e.message,metadata=this.jsonToState("{}")}metadata.metadataTicker={value:1},this.setInitialState(metadata)}replacer(key,value){const languages=[],additional={};switch(key){case"metadataTicker":case"id":return;case"language":for(const lang of value)languages.push(lang.value);return languages;case"license":case"isPartOf":return value.value;case"freeform":if(!value.value)return"{}";try{return value.value}catch(e){return}case"additional":for(const item of value){item.scope in additional==!1&&(additional[item.scope]={}),item.property in additional[item.scope]==!1&&item.qualifier&&(additional[item.scope][item.property]={});let currentValue=null;currentValue=""===item.qualifier?additional[item.scope][item.property]:additional[item.scope][item.property][item.qualifier];let value=null;value=currentValue?Array.isArray(currentValue)?currentValue.concat([item.value]):[currentValue,item.value]:item.value,""===item.qualifier?additional[item.scope][item.property]=value:additional[item.scope][item.property][item.qualifier]=value}return JSON.stringify(additional);default:return value}}jsonStringify(state,spacing){let output=JSON.stringify(state,this.replacer);return output=JSON.parse(output),output.freeform=JSON.parse(output.freeform),output.additional=JSON.parse(output.additional),output=JSON.stringify(output,null,spacing),output}reviver(key,value){const holder=[];let id=1;switch(key){case"author":for(const current of value)current.id=id,holder.push(current),id++;return holder;case"language":for(const lang of value)holder.push({id:id,value:lang}),id++;return holder;case"license":case"isPartOf":return{value:value};case"freeform":return{value:JSON.stringify(value)};default:return value}}jsonToState(data){data=JSON.parse(data);for(let property in data)data[property]=this.reviver(property,data[property]);const authorFields=["id","firstName","lastName","institution","year"],standardFields=["id","value"];(data=this.stripFields(data,["author","language","license","isPartOf","additional","freeform"])).author=Array.isArray(data.author)?data.author:[this.tidyObject({id:1},authorFields)];const authorHolder=[];for(let author of data.author)author=this.tidyObject(author,authorFields),authorHolder.push(author);data.author=authorHolder,data.language=Array.isArray(data.language)?data.language:[];const langHolder=[];for(let lang of data.language)lang=this.tidyObject(lang,standardFields),langHolder.push(lang);data.language=langHolder,data.isPartOf=this.tidyObject(data.isPartOf,standardFields),data.license=this.tidyObject(data.license,standardFields),data.additional=data.additional&&"object"==typeof data.additional?data.additional:{};const addHolder=[];let addId=1;for(const addScope in data.additional)for(const addProperty in data.additional[addScope])if(data.additional[addScope][addProperty]&&"object"==typeof data.additional[addScope][addProperty]&&!Array.isArray(data.additional[addScope][addProperty]))for(const addQualifier in data.additional[addScope][addProperty]){let values=data.additional[addScope][addProperty][addQualifier];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:addQualifier,value:value};addHolder.push(add),addId++}}else{let values=data.additional[addScope][addProperty];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:"",value:value};addHolder.push(add),addId++}}return data.additional=addHolder,data.freeform=this.tidyObject(data.freeform,standardFields),data}stripFields(obj,fields){const result={};for(const suppliedField in obj)fields.includes(suppliedField)&&(result[suppliedField]=obj[suppliedField]);return result}addFields(obj,fields){for(const field of fields)Object.hasOwn(obj,field)?obj[field]=String(obj[field]):obj[field]="";return obj}tidyObject(obj,fields){return obj=obj&&"object"==typeof obj?obj:{},obj=this.stripFields(obj,fields),obj=this.addFields(obj,fields)}}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,mutations:_mutations.mutations});_exports.metadata=metadata})); +define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=void 0;class StackMetadata extends _reactive.Reactive{constructor(){var obj,key,value;super(...arguments),value={languages:["en"],user:{firstname:"",lastname:"",institution:"",year:""},licenses:[{value:"unknown",text:"unknown"}],placeholder:""},(key="lib")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}loadState(){var _metadata$value;let metadata=document.querySelector('input[name="metadata"]');const metadataJSON=null!==(_metadata$value=metadata.value)&&void 0!==_metadata$value?_metadata$value:null;try{this.lib=JSON.parse(metadata.dataset.lib),this.lib.user.year=(new Date).getFullYear();let languages=new Set(this.lib.languages);this.lib.languages=languages.difference(new Set([null,void 0,""])),this.lib.languages=Array.from(this.lib.languages)}catch(e){}try{metadata=this.jsonToState(metadataJSON)}catch(e){this.lib.brokenMetadata=e.message,metadata=this.jsonToState("{}")}metadata.metadataTicker={value:1},this.setInitialState(metadata)}replacer(key,value){const languages=[],additional={};switch(key){case"metadataTicker":case"id":return;case"language":for(const lang of value)languages.push(lang.value);return languages;case"license":case"isPartOf":return value.value;case"freeform":if(!value.value)return"{}";try{return value.value}catch(e){return}case"additional":for(const item of value){item.scope in additional==!1&&(additional[item.scope]={}),item.property in additional[item.scope]==!1&&item.qualifier&&(additional[item.scope][item.property]={});let currentValue=null;currentValue=""===item.qualifier?additional[item.scope][item.property]:additional[item.scope][item.property][item.qualifier];let value=null;value=currentValue?Array.isArray(currentValue)?currentValue.concat([item.value]):[currentValue,item.value]:item.value,""===item.qualifier?additional[item.scope][item.property]=value:additional[item.scope][item.property][item.qualifier]=value}return JSON.stringify(additional);default:return value}}jsonStringify(state,spacing){let output=JSON.stringify(state,this.replacer);return output=JSON.parse(output),output.freeform=JSON.parse(output.freeform),output.additional=JSON.parse(output.additional),output=JSON.stringify(output,null,spacing),output}reviver(key,value){const holder=[];let id=1;switch(key){case"author":for(const current of value)current.id=id,holder.push(current),id++;return holder;case"language":for(const lang of value)holder.push({id:id,value:lang}),id++;return holder;case"license":case"isPartOf":return{value:value};case"freeform":return{value:JSON.stringify(value)};default:return value}}jsonToState(data){data=JSON.parse(data);for(let property in data)data[property]=this.reviver(property,data[property]);const authorFields=["id","firstName","lastName","institution","year"],standardFields=["id","value"];(data=this.stripFields(data,["author","language","license","isPartOf","additional","freeform"])).author=Array.isArray(data.author)?data.author:[];const authorHolder=[];for(let author of data.author)author=this.tidyObject(author,authorFields),authorHolder.push(author);data.author=authorHolder,data.language=Array.isArray(data.language)?data.language:[];const langHolder=[];for(let lang of data.language)lang=this.tidyObject(lang,standardFields),langHolder.push(lang);data.language=langHolder,data.isPartOf=this.tidyObject(data.isPartOf,standardFields),data.license=this.tidyObject(data.license,standardFields),data.additional=data.additional&&"object"==typeof data.additional?data.additional:{};const addHolder=[];let addId=1;for(const addScope in data.additional)for(const addProperty in data.additional[addScope])if(data.additional[addScope][addProperty]&&"object"==typeof data.additional[addScope][addProperty]&&!Array.isArray(data.additional[addScope][addProperty]))for(const addQualifier in data.additional[addScope][addProperty]){let values=data.additional[addScope][addProperty][addQualifier];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:addQualifier,value:value};addHolder.push(add),addId++}}else{let values=data.additional[addScope][addProperty];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:"",value:value};addHolder.push(add),addId++}}return data.additional=addHolder,data.freeform=this.tidyObject(data.freeform,standardFields),data}stripFields(obj,fields){const result={};for(const suppliedField in obj)fields.includes(suppliedField)&&(result[suppliedField]=obj[suppliedField]);return result}addFields(obj,fields){for(const field of fields)Object.hasOwn(obj,field)?obj[field]=String(obj[field]):obj[field]="";return obj}tidyObject(obj,fields){return obj=obj&&"object"==typeof obj?obj:{},obj=this.stripFields(obj,fields),obj=this.addFields(obj,fields)}}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,mutations:_mutations.mutations});_exports.metadata=metadata})); //# sourceMappingURL=metadata.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map index 85a1847cf37..336037aa448 100644 --- a/amd/build/metadata/metadata.min.js.map +++ b/amd/build/metadata/metadata.min.js.map @@ -1 +1 @@ -{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n // Default and config data passed through from Moodle.\n lib = {\n languages: ['en'],\n user: {\n firstname: '',\n lastname: '',\n institution: '',\n year: ''\n },\n licenses: [{value: 'unknown', text: 'unknown'}],\n placeholder: ''\n };\n\n /**\n * Load initial value of state from value on form.\n */\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n const metadataJSON = metadata.value ?? null;\n try {\n this.lib = JSON.parse(metadata.dataset.lib);\n this.lib.user.year = new Date().getFullYear();\n // Weed out duplicates and falsy values.\n let languages = new Set(this.lib.languages);\n this.lib.languages = languages.difference(new Set([null, undefined, \"\"]));\n this.lib.languages = Array.from(this.lib.languages);\n } catch (e) {\n // Lib will be set to defaults.\n }\n try {\n metadata = this.jsonToState(metadataJSON);\n } catch (e) {\n // If the saved data is broken, show empty inputs and save error message for display in modal.\n this.lib.brokenMetadata = e.message;\n metadata = this.jsonToState('{}');\n }\n metadata.metadataTicker = {value: 1};\n this.setInitialState(metadata);\n }\n\n /**\n * Replacer function for JSON stringify of state.\n * Removed unwanted properties and converts some objects to plain values.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n replacer(key, value) {\n const languages = [];\n const additional = {};\n switch(key) {\n case 'metadataTicker':\n return undefined;\n case 'id':\n return undefined;\n case 'language':\n for (const lang of value) {\n languages.push(lang.value);\n }\n return languages;\n case 'license':\n case 'isPartOf':\n return value.value;\n case 'freeform':\n if (!value.value) {\n return '{}';\n }\n try {\n // If we parse the value here then the replacer\n // works on it recursively which gets messed up\n // if someone uses property values to match our own.\n return value.value;\n } catch(e) {\n return undefined;\n }\n case 'additional':\n for (const item of value) {\n if (item.scope in additional === false) {\n additional[item.scope] = {};\n }\n if (item.property in additional[item.scope] === false && item.qualifier) {\n additional[item.scope][item.property] = {};\n }\n let currentValue = null;\n if (item.qualifier === '') {\n currentValue = additional[item.scope][item.property];\n } else {\n currentValue = additional[item.scope][item.property][item.qualifier];\n }\n let value = null;\n // If we have multiple values, we need to convert to an array.\n if (!currentValue) {\n value = item.value;\n } else if (!Array.isArray(currentValue)) {\n value = [currentValue, item.value];\n } else {\n value = currentValue.concat([item.value]);\n }\n if (item.qualifier === '') {\n additional[item.scope][item.property] = value;\n } else {\n additional[item.scope][item.property][item.qualifier] = value;\n }\n }\n return JSON.stringify(additional);\n default:\n return value;\n }\n\n }\n\n /**\n * Convert state into a JSON string.\n * We can't simply stringify because of the freeform and additional properties.\n * We have to deal with them as strings before this to prevent the replacer\n * acting recursively.\n *\n * @param {*} state\n * @param {*} spacing\n * @returns\n */\n jsonStringify(state, spacing) {\n let output = JSON.stringify(state, this.replacer);\n output = JSON.parse(output);\n output.freeform = JSON.parse(output.freeform);\n output.additional = JSON.parse(output.additional);\n output = JSON.stringify(output, null, spacing);\n return output;\n }\n\n /**\n * Reviver function for JSON parsing to feed into state.\n * Adds id values and converts strings to obj.value.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n reviver(key, value) {\n const holder = [];\n let id = 1;\n switch(key) {\n case 'author':\n for (const current of value) {\n current.id = id;\n holder.push(current);\n id++;\n }\n return holder;\n case 'language':\n for (const lang of value) {\n holder.push({id: id, value: lang});\n id++;\n }\n return holder;\n case 'license':\n case 'isPartOf':\n return {value: value};\n case 'freeform':\n return {value: JSON.stringify(value)};\n default:\n return value;\n }\n }\n\n /**\n * Convert JSON to state format ready for updateFromJson mutation.\n * Strips out extraneous fields; adds in missing fields with blank values.\n *\n * @param {*} data\n * @returns\n */\n jsonToState(data) {\n data = JSON.parse(data);\n for (let property in data) {\n // We use the reviver once rather than recursively as it works from the bottom\n // up and that causes issues if someone has re-used one of our property names.\n data[property] = this.reviver(property, data[property]);\n }\n const fields = ['author', 'language', 'license', 'isPartOf', 'additional', 'freeform'];\n data = this.stripFields(data, fields);\n const authorFields = ['id', 'firstName', 'lastName', 'institution', 'year'];\n const standardFields = ['id', 'value'];\n\n data.author = (Array.isArray(data.author)) ? data.author : [this.tidyObject({id: 1}, authorFields)];\n const authorHolder = [];\n for (let author of data.author) {\n author = this.tidyObject(author, authorFields);\n authorHolder.push(author);\n }\n data.author = authorHolder;\n data.language = (Array.isArray(data.language)) ? data.language : [];\n const langHolder = [];\n for (let lang of data.language) {\n lang = this.tidyObject(lang, standardFields);\n langHolder.push(lang);\n }\n data.language = langHolder;\n data.isPartOf = this.tidyObject(data.isPartOf, standardFields);\n data.license = this.tidyObject(data.license, standardFields);\n data.additional = (data.additional && typeof data.additional === 'object') ? data.additional : {};\n const addHolder = [];\n let addId = 1;\n for (const addScope in data.additional) {\n for (const addProperty in data.additional[addScope]) {\n if (\n data.additional[addScope][addProperty] &&\n typeof data.additional[addScope][addProperty] === 'object' &&\n !Array.isArray(data.additional[addScope][addProperty])\n ) {\n for (const addQualifier in data.additional[addScope][addProperty]) {\n let values = data.additional[addScope][addProperty][addQualifier];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: addQualifier,\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n } else {\n let values = data.additional[addScope][addProperty];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: '',\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n }\n }\n data.additional = addHolder;\n data.freeform = this.tidyObject(data.freeform, standardFields);\n\n return data;\n }\n\n /**\n * Remove any properties from an object that are not in a supplied array of property names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns {object}\n */\n stripFields(obj, fields) {\n const result = {};\n for (const suppliedField in obj) {\n if (fields.includes(suppliedField)) {\n result[suppliedField] = obj[suppliedField];\n }\n }\n return result;\n }\n\n /**\n * Add any missing properties to an object from a supplied array of field names and set to ''.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n addFields(obj, fields) {\n for (const field of fields) {\n if (!Object.hasOwn(obj, field)) {\n obj[field] = '';\n } else {\n obj[field] = String(obj[field]);\n }\n }\n return obj;\n }\n\n /**\n * Set properties of an object to those from a supplied array of field names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n tidyObject(obj, fields) {\n obj = (obj && typeof obj === 'object') ? obj : {};\n obj = this.stripFields(obj, fields);\n obj = this.addFields(obj, fields);\n return obj;\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n"],"names":["StackMetadata","Reactive","languages","user","firstname","lastname","institution","year","licenses","value","text","placeholder","loadState","metadata","document","querySelector","metadataJSON","lib","JSON","parse","dataset","Date","getFullYear","Set","this","difference","undefined","Array","from","e","jsonToState","brokenMetadata","message","metadataTicker","setInitialState","replacer","key","additional","lang","push","item","scope","property","qualifier","currentValue","isArray","concat","stringify","jsonStringify","state","spacing","output","freeform","reviver","holder","id","current","data","authorFields","standardFields","stripFields","author","tidyObject","authorHolder","language","langHolder","isPartOf","license","addHolder","addId","addScope","addProperty","addQualifier","values","add","obj","fields","result","suppliedField","includes","addFields","field","Object","hasOwn","String","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n // Default and config data passed through from Moodle.\n lib = {\n languages: ['en'],\n user: {\n firstname: '',\n lastname: '',\n institution: '',\n year: ''\n },\n licenses: [{value: 'unknown', text: 'unknown'}],\n placeholder: ''\n };\n\n /**\n * Load initial value of state from value on form.\n */\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n const metadataJSON = metadata.value ?? null;\n try {\n this.lib = JSON.parse(metadata.dataset.lib);\n this.lib.user.year = new Date().getFullYear();\n // Weed out duplicates and falsy values.\n let languages = new Set(this.lib.languages);\n this.lib.languages = languages.difference(new Set([null, undefined, \"\"]));\n this.lib.languages = Array.from(this.lib.languages);\n } catch (e) {\n // Lib will be set to defaults.\n }\n try {\n metadata = this.jsonToState(metadataJSON);\n } catch (e) {\n // If the saved data is broken, show empty inputs and save error message for display in modal.\n this.lib.brokenMetadata = e.message;\n metadata = this.jsonToState('{}');\n }\n metadata.metadataTicker = {value: 1};\n this.setInitialState(metadata);\n }\n\n /**\n * Replacer function for JSON stringify of state.\n * Removed unwanted properties and converts some objects to plain values.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n replacer(key, value) {\n const languages = [];\n const additional = {};\n switch(key) {\n case 'metadataTicker':\n return undefined;\n case 'id':\n return undefined;\n case 'language':\n for (const lang of value) {\n languages.push(lang.value);\n }\n return languages;\n case 'license':\n case 'isPartOf':\n return value.value;\n case 'freeform':\n if (!value.value) {\n return '{}';\n }\n try {\n // If we parse the value here then the replacer\n // works on it recursively which gets messed up\n // if someone uses property values to match our own.\n return value.value;\n } catch(e) {\n return undefined;\n }\n case 'additional':\n for (const item of value) {\n if (item.scope in additional === false) {\n additional[item.scope] = {};\n }\n if (item.property in additional[item.scope] === false && item.qualifier) {\n additional[item.scope][item.property] = {};\n }\n let currentValue = null;\n if (item.qualifier === '') {\n currentValue = additional[item.scope][item.property];\n } else {\n currentValue = additional[item.scope][item.property][item.qualifier];\n }\n let value = null;\n // If we have multiple values, we need to convert to an array.\n if (!currentValue) {\n value = item.value;\n } else if (!Array.isArray(currentValue)) {\n value = [currentValue, item.value];\n } else {\n value = currentValue.concat([item.value]);\n }\n if (item.qualifier === '') {\n additional[item.scope][item.property] = value;\n } else {\n additional[item.scope][item.property][item.qualifier] = value;\n }\n }\n return JSON.stringify(additional);\n default:\n return value;\n }\n\n }\n\n /**\n * Convert state into a JSON string.\n * We can't simply stringify because of the freeform and additional properties.\n * We have to deal with them as strings before this to prevent the replacer\n * acting recursively.\n *\n * @param {*} state\n * @param {*} spacing\n * @returns\n */\n jsonStringify(state, spacing) {\n let output = JSON.stringify(state, this.replacer);\n output = JSON.parse(output);\n output.freeform = JSON.parse(output.freeform);\n output.additional = JSON.parse(output.additional);\n output = JSON.stringify(output, null, spacing);\n return output;\n }\n\n /**\n * Reviver function for JSON parsing to feed into state.\n * Adds id values and converts strings to obj.value.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n reviver(key, value) {\n const holder = [];\n let id = 1;\n switch(key) {\n case 'author':\n for (const current of value) {\n current.id = id;\n holder.push(current);\n id++;\n }\n return holder;\n case 'language':\n for (const lang of value) {\n holder.push({id: id, value: lang});\n id++;\n }\n return holder;\n case 'license':\n case 'isPartOf':\n return {value: value};\n case 'freeform':\n return {value: JSON.stringify(value)};\n default:\n return value;\n }\n }\n\n /**\n * Convert JSON to state format ready for updateFromJson mutation.\n * Strips out extraneous fields; adds in missing fields with blank values.\n *\n * @param {*} data\n * @returns\n */\n jsonToState(data) {\n data = JSON.parse(data);\n for (let property in data) {\n // We use the reviver once rather than recursively as it works from the bottom\n // up and that causes issues if someone has re-used one of our property names.\n data[property] = this.reviver(property, data[property]);\n }\n const fields = ['author', 'language', 'license', 'isPartOf', 'additional', 'freeform'];\n data = this.stripFields(data, fields);\n const authorFields = ['id', 'firstName', 'lastName', 'institution', 'year'];\n const standardFields = ['id', 'value'];\n\n data.author = (Array.isArray(data.author)) ? data.author : [];\n const authorHolder = [];\n for (let author of data.author) {\n author = this.tidyObject(author, authorFields);\n authorHolder.push(author);\n }\n data.author = authorHolder;\n data.language = (Array.isArray(data.language)) ? data.language : [];\n const langHolder = [];\n for (let lang of data.language) {\n lang = this.tidyObject(lang, standardFields);\n langHolder.push(lang);\n }\n data.language = langHolder;\n data.isPartOf = this.tidyObject(data.isPartOf, standardFields);\n data.license = this.tidyObject(data.license, standardFields);\n data.additional = (data.additional && typeof data.additional === 'object') ? data.additional : {};\n const addHolder = [];\n let addId = 1;\n for (const addScope in data.additional) {\n for (const addProperty in data.additional[addScope]) {\n if (\n data.additional[addScope][addProperty] &&\n typeof data.additional[addScope][addProperty] === 'object' &&\n !Array.isArray(data.additional[addScope][addProperty])\n ) {\n for (const addQualifier in data.additional[addScope][addProperty]) {\n let values = data.additional[addScope][addProperty][addQualifier];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: addQualifier,\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n } else {\n let values = data.additional[addScope][addProperty];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: '',\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n }\n }\n data.additional = addHolder;\n data.freeform = this.tidyObject(data.freeform, standardFields);\n\n return data;\n }\n\n /**\n * Remove any properties from an object that are not in a supplied array of property names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns {object}\n */\n stripFields(obj, fields) {\n const result = {};\n for (const suppliedField in obj) {\n if (fields.includes(suppliedField)) {\n result[suppliedField] = obj[suppliedField];\n }\n }\n return result;\n }\n\n /**\n * Add any missing properties to an object from a supplied array of field names and set to ''.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n addFields(obj, fields) {\n for (const field of fields) {\n if (!Object.hasOwn(obj, field)) {\n obj[field] = '';\n } else {\n obj[field] = String(obj[field]);\n }\n }\n return obj;\n }\n\n /**\n * Set properties of an object to those from a supplied array of field names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n tidyObject(obj, fields) {\n obj = (obj && typeof obj === 'object') ? obj : {};\n obj = this.stripFields(obj, fields);\n obj = this.addFields(obj, fields);\n return obj;\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n"],"names":["StackMetadata","Reactive","languages","user","firstname","lastname","institution","year","licenses","value","text","placeholder","loadState","metadata","document","querySelector","metadataJSON","lib","JSON","parse","dataset","Date","getFullYear","Set","this","difference","undefined","Array","from","e","jsonToState","brokenMetadata","message","metadataTicker","setInitialState","replacer","key","additional","lang","push","item","scope","property","qualifier","currentValue","isArray","concat","stringify","jsonStringify","state","spacing","output","freeform","reviver","holder","id","current","data","authorFields","standardFields","stripFields","author","authorHolder","tidyObject","language","langHolder","isPartOf","license","addHolder","addId","addScope","addProperty","addQualifier","values","add","obj","fields","result","suppliedField","includes","addFields","field","Object","hasOwn","String","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js index f5000a2b460..f6cf6aa7ec7 100644 --- a/amd/src/metadata/metadata.js +++ b/amd/src/metadata/metadata.js @@ -211,7 +211,7 @@ class StackMetadata extends Reactive { const authorFields = ['id', 'firstName', 'lastName', 'institution', 'year']; const standardFields = ['id', 'value']; - data.author = (Array.isArray(data.author)) ? data.author : [this.tidyObject({id: 1}, authorFields)]; + data.author = (Array.isArray(data.author)) ? data.author : []; const authorHolder = []; for (let author of data.author) { author = this.tidyObject(author, authorFields); diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index 67e26a9d644..f7c4cfea7e4 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -336,13 +336,15 @@ }}
-
-
-
{{#str}} firstname, qtype_stack {{/str}}
-
{{#str}} lastname, qtype_stack {{/str}}
-
{{#str}} institution, qtype_stack {{/str}}
-
{{#str}} year, qtype_stack {{/str}}
-
+ {{#author.0}} +
+
+
{{#str}} firstname, qtype_stack {{/str}}
+
{{#str}} lastname, qtype_stack {{/str}}
+
{{#str}} institution, qtype_stack {{/str}}
+
{{#str}} year, qtype_stack {{/str}}
+
+ {{/author.0}} {{#author}} {{>qtype_stack/metadata/metadataauthor}} {{/author}} From cc7c9876f7aefb23ffc00bbdb7d1740f03d711a9 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 16 Jun 2026 14:17:05 +0100 Subject: [PATCH 069/100] iss1171 - Fix tests --- tests/jest/metadata.test.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/jest/metadata.test.js b/tests/jest/metadata.test.js index 0003ae65959..be76d2e2c28 100644 --- a/tests/jest/metadata.test.js +++ b/tests/jest/metadata.test.js @@ -11,12 +11,6 @@ describe('reviver', () => { ]); }); - test('author: wraps a single object and assigns an id', () => { - expect(metadata.reviver('author', {firstName: 'Alice'})).toEqual([ - {firstName: 'Alice', id: 1}, - ]); - }); - test('language: converts array of strings to [{id, value}] objects', () => { expect(metadata.reviver('language', ['en', 'fr'])).toEqual([ {id: 1, value: 'en'}, @@ -196,7 +190,7 @@ describe('tidyObject', () => { describe('jsonToState', () => { test('empty JSON produces a fully-defaulted state', () => { expect(metadata.jsonToState('{}')).toEqual({ - author: [{id: '', firstName: '', lastName: '', institution: '', year: ''}], + author: [], language: [], license: {id: '', value: ''}, isPartOf: {id: '', value: ''}, @@ -213,12 +207,6 @@ describe('jsonToState', () => { .toEqual([{id: '1', firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}]); }); - test('parses single author object into an array with one item', () => { - const input = JSON.stringify({author: {firstName: 'Alice', lastName: 'Smith'}}); - expect(metadata.jsonToState(input).author) - .toEqual([{id: '1', firstName: 'Alice', lastName: 'Smith', institution: '', year: ''}]); - }); - test('strips unrecognised author fields', () => { const input = JSON.stringify({author: [{firstName: 'Alice', unknownField: 'ignored'}]}); expect(metadata.jsonToState(input).author[0]).not.toHaveProperty('unknownField'); From 5f7b15971eb554142db78f81990da481d547f3a8 Mon Sep 17 00:00:00 2001 From: Michael Kallweit Date: Thu, 25 Jun 2026 10:50:39 +0200 Subject: [PATCH 070/100] metadata edit form ui overhaul --- amd/src/metadata/container.js | 4 +- amd/src/metadata/metadatamodal.js | 1 + styles.css | 109 +++++++++- .../metadata/metadataadditional.mustache | 11 +- templates/metadata/metadataauthor.mustache | 26 +-- templates/metadata/metadatacontent.mustache | 199 +++++++++--------- templates/metadata/metadatalanguage.mustache | 14 +- templates/metadata/metadatamodal.mustache | 3 +- templates/metadata/metadatascope.mustache | 45 ++-- 9 files changed, 266 insertions(+), 146 deletions(-) diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 46bdfe7dc3a..9fb6c4812c4 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -304,7 +304,7 @@ export default class extends BaseComponent { async addItem(event) { const result = await this.update(true); if (result) { - const parts = event.target.id.split('_'); + const parts = event.currentTarget.id.split('_'); this.reactive.dispatch('addItem', parts[1], parts[2]); } } @@ -318,7 +318,7 @@ export default class extends BaseComponent { async deleteItem(event) { const result = await this.update(false); if (result) { - const parts = event.target.id.split('_'); + const parts = event.currentTarget.id.split('_'); this.reactive.dispatch('deleteRow', parts[1], parts[2]); } } diff --git a/amd/src/metadata/metadatamodal.js b/amd/src/metadata/metadatamodal.js index 6ef463a6eec..d59d40eb9ab 100644 --- a/amd/src/metadata/metadatamodal.js +++ b/amd/src/metadata/metadatamodal.js @@ -116,6 +116,7 @@ async function openModal() { } if (addListener) { document.querySelector('#stackmetadata_cancel').addEventListener('click', closeModal); + document.querySelector('#stackmetadata_save').addEventListener('click', () => modal.hide()); } } diff --git a/styles.css b/styles.css index 10ca5835440..52e47fc0de5 100644 --- a/styles.css +++ b/styles.css @@ -1307,11 +1307,108 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. padding-left: 0; padding-right: 0; } -.stack-metadata .scope { - padding: 20px 10px 0 10px; + +/* --- Metadata modal: section layout ---------------------------------------- */ +.stack-metadata .smd-section { + border-left: 3px solid #0f6cbf; + background: rgba(15, 108, 191, 0.03); + border-radius: 0 4px 4px 0; + padding: 0.75rem 1rem 0.75rem 1.25rem; + margin-bottom: 1rem; +} +.stack-metadata .smd-section-label { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.09em; + color: #6c757d; + margin-bottom: 0.6rem; } -.stack-metadata .btn-danger { - background-color: pink; - border-color: pink; - color: black; +.stack-metadata .smd-col-label { + font-size: 0.75rem; + font-weight: 600; + color: #495057; + display: block; + margin-bottom: 0.25rem; +} +.stack-metadata .smd-delete { + display: flex; + align-items: center; +} +.stack-metadata [name="smd_delete"] i, +.stack-metadata [name="smd_add"] i { + pointer-events: none; +} +.stack-metadata details.smd-section > summary { + list-style: none; + cursor: pointer; + user-select: none; +} +.stack-metadata details.smd-section > summary::marker, +.stack-metadata details.smd-section > summary::-webkit-details-marker { + display: none; +} + +/* --- Scope cards within Additional Metadata -------------------------------- */ +.stack-metadata .smd-scope-card { + border: 1px solid #dee2e6; + border-radius: 4px; + margin-bottom: 0.75rem; + overflow: hidden; +} +.stack-metadata .smd-scope-header { + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + gap: 0.75rem; + border-bottom: 1px solid #dee2e6; +} +.stack-metadata .smd-scope-header .smd-col-label { + white-space: nowrap; + margin-bottom: 0; + flex-shrink: 0; +} +.stack-metadata .smd-scope-header .fitem, +.stack-metadata .smd-scope-header .felement { + flex: 1; + margin: 0 !important; + padding: 0 !important; +} +/*
/ collapse for scope cards */ +.stack-metadata .smd-scope-card > summary { + cursor: pointer; + user-select: none; +} +.stack-metadata .smd-scope-card > summary::marker, +.stack-metadata .smd-scope-card > summary::-webkit-details-marker { + display: none; +} +.stack-metadata .smd-scope-card > summary input { + user-select: text; +} +.stack-metadata .smd-scope-chevron { + flex-shrink: 0; + color: #6c757d; + pointer-events: none; + transition: transform 0.2s ease; + margin-right: 0.25rem; +} +.stack-metadata .smd-scope-card:not([open]) .smd-scope-chevron { + transform: rotate(-90deg); +} +.stack-metadata .smd-scope-body { + padding: 0.75rem; +} +/* First field in scope rows gets left breathing room */ +.stack-metadata .smd-scope-body .row > :first-child { + padding-left: 0.5rem !important; +} + +/* Zero out Moodle form-wrapper spacing in all aligned rows */ +.stack-metadata .smd-section .row .fitem, +.stack-metadata .smd-section .row .felement, +.stack-metadata .smd-scope-body .row .fitem, +.stack-metadata .smd-scope-body .row .felement { + margin: 0 !important; + padding: 0 !important; } diff --git a/templates/metadata/metadataadditional.mustache b/templates/metadata/metadataadditional.mustache index c858ef19099..7ced972f51c 100644 --- a/templates/metadata/metadataadditional.mustache +++ b/templates/metadata/metadataadditional.mustache @@ -48,7 +48,7 @@ "id": "2" } }} -
+
{{#property}}
-
-
\ No newline at end of file diff --git a/templates/metadata/metadataauthor.mustache b/templates/metadata/metadataauthor.mustache index b4575732a6d..d3acb3b2575 100644 --- a/templates/metadata/metadataauthor.mustache +++ b/templates/metadata/metadataauthor.mustache @@ -58,20 +58,19 @@ "id": "1" } }} -
-
{{#str}} author, qtype_stack {{/str}}
-
+
+
{{#firstname}} {{> core_form/element-text-inline }} {{/firstname}}
-
+
{{#lastname}} {{> core_form/element-text-inline }} {{/lastname}} @@ -79,22 +78,25 @@
{{#institution}} {{> core_form/element-text-inline }} {{/institution}}
-
+
{{#year}} {{>qtype_stack/metadata/metadatayear}} {{/year}}
-
-
diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index f7c4cfea7e4..d5edb83aea8 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -336,116 +336,124 @@ }} - {{#author.0}} -
-
-
{{#str}} firstname, qtype_stack {{/str}}
-
{{#str}} lastname, qtype_stack {{/str}}
-
{{#str}} institution, qtype_stack {{/str}}
-
{{#str}} year, qtype_stack {{/str}}
+ + {{! Authors }} +
+ + {{#author.0}} +
+
{{#str}} firstname, qtype_stack {{/str}}
+
{{#str}} lastname, qtype_stack {{/str}}
+
{{#str}} institution, qtype_stack {{/str}}
+
{{#str}} year, qtype_stack {{/str}}
- {{/author.0}} - {{#author}} - {{>qtype_stack/metadata/metadataauthor}} - {{/author}} -
-
-
- -
-
- {{#language}} - {{>qtype_stack/metadata/metadatalanguage}} - {{/language}} -
-
-
-
-
-
-
{{#str}} ispartof, qtype_stack {{/str}}
-
- {{#isPartOf}} - - {{> core_form/element-text-inline}} - {{/isPartOf}} -
-
-
-
{{#str}} license {{/str}}
-
- {{#license}} - - {{> core_form/element-autocomplete-inline}} - {{/license}} + + {{! Language }} +
+ + {{#language}} + {{>qtype_stack/metadata/metadatalanguage}} + {{/language}} +
+
-
{{#str}} additionalmetadata, qtype_stack {{/str}}
- {{#scope}} - {{>qtype_stack/metadata/metadatascope}} - {{/scope}} -
-
-
-
-
-
-
{{#str}} freeformmetadata, qtype_stack {{/str}}
-
- {{#freeform}} - - {{< core_form/element-template-inline }} - {{$element}} - - {{/element}} - {{/ core_form/element-template-inline }} - {{/freeform}} + + {{! License + isPartOf }} +
+
+
+ + {{#license}} + + {{> core_form/element-autocomplete-inline}} + {{/license}} +
+
+ + {{#isPartOf}} + + {{> core_form/element-text-inline}} + {{/isPartOf}} +
-
-
-
- - - -
-
-

+ + {{! Freeform metadata }} +

+ + {{#freeform}} + + {{< core_form/element-template-inline }} + {{$element}} + + {{/element}} + {{/ core_form/element-template-inline }} + {{/freeform}} +
+ + {{! Utility buttons }} +
+ + + +
+ +

{{#str}} metadataexplanation, qtype_stack {{/str}}

-
-
{{#str}} JSONmetadata, qtype_stack {{/str}}
-
+ + {{! JSON metadata — collapsible }} +
+ +
{{#json}}
-
+
+ {{#js}} require(['qtype_stack/metadata/metadatacontent'], function(component) { diff --git a/templates/metadata/metadatalanguage.mustache b/templates/metadata/metadatalanguage.mustache index 6ee3dfea213..561e169a09c 100644 --- a/templates/metadata/metadatalanguage.mustache +++ b/templates/metadata/metadatalanguage.mustache @@ -29,9 +29,8 @@ } } }} -
-
{{#str}} language, core {{/str}}
-
+
+
{{#lang}}
-
-
\ No newline at end of file diff --git a/templates/metadata/metadatamodal.mustache b/templates/metadata/metadatamodal.mustache index f5276190fe6..ffe98be46f9 100644 --- a/templates/metadata/metadatamodal.mustache +++ b/templates/metadata/metadatamodal.mustache @@ -27,9 +27,10 @@
- {{/body}} {{$footer}} + + {{/footer}} {{/ core/modal }} {{#js}} diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache index fd8fd406031..594783b3d28 100644 --- a/templates/metadata/metadatascope.mustache +++ b/templates/metadata/metadatascope.mustache @@ -130,10 +130,11 @@ } } }} -
-
-
{{#str}} scope, qtype_stack {{/str}}
-
+
+ + + {{#str}} scope, qtype_stack {{/str}} +
{{#input}}
-
- +
+
+
+
{{#str}} property, qtype_stack {{/str}}
+
{{#str}} qualifier, qtype_stack {{/str}}
+
{{#str}} value, qtype_stack {{/str}}
+
+ {{#properties}} + {{>qtype_stack/metadata/metadataadditional}} + {{/properties}} +
+
-
-
{{#str}} property, qtype_stack {{/str}}
-
{{#str}} qualifier, qtype_stack {{/str}}
-
{{#str}} value, qtype_stack {{/str}}
-
- {{#properties}} - {{>qtype_stack/metadata/metadataadditional}} - {{/properties}} -
-
-
-
-
\ No newline at end of file + \ No newline at end of file From cb7fa2fb118653d520bcd1c096380d4c4136403f Mon Sep 17 00:00:00 2001 From: Michael Kallweit Date: Thu, 25 Jun 2026 11:10:57 +0200 Subject: [PATCH 071/100] metadata edit scope pencil icon --- styles.css | 48 +++++++++++++++++++++ templates/metadata/metadatacontent.mustache | 41 ++++++++++++++++++ templates/metadata/metadatascope.mustache | 8 +++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/styles.css b/styles.css index 52e47fc0de5..6ea7ccb19f2 100644 --- a/styles.css +++ b/styles.css @@ -1383,6 +1383,54 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. .stack-metadata .smd-scope-card > summary::-webkit-details-marker { display: none; } +/* Scope name: inline edit with pencil */ +.stack-metadata .smd-scope-name { + display: flex; + align-items: center; + gap: 0.25rem; + min-width: 0; +} +.stack-metadata .smd-scope-name-text { + font-weight: 600; + font-size: 0.95rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +/* Pencil: always in pointer flow, just invisible until hover */ +.stack-metadata .smd-scope-pencil { + color: #6c757d; + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; +} +.stack-metadata .smd-scope-pencil i { + pointer-events: none; +} +.stack-metadata .smd-scope-header:hover .smd-scope-pencil { + opacity: 0.6; +} +/* Read mode: fitem collapses to zero, stays in-flow so focus() never scrolls */ +.stack-metadata .smd-scope-name .fitem { + flex: 0 0 0 !important; + max-width: 0 !important; + max-height: 0 !important; + overflow: hidden; + opacity: 0; +} +/* Edit mode: input has focus */ +.stack-metadata .smd-scope-name:has(input:focus) .fitem { + flex: 1 !important; + max-width: none !important; + max-height: none !important; + overflow: visible; + opacity: 1; +} +.stack-metadata .smd-scope-name:has(input:focus) .smd-scope-name-text, +.stack-metadata .smd-scope-name:has(input:focus) .smd-scope-pencil { + display: none; +} +/* Allow text selection in the scope name input */ .stack-metadata .smd-scope-card > summary input { user-select: text; } diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index d5edb83aea8..f74321d2d02 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -483,4 +483,45 @@ require(['qtype_stack/metadata/metadatacontent'], function(component) { component.init('#qtype-stack-metadata-content'); }); + +// Scope name inline-edit: pencil click → focus input; Enter/blur → confirm; update display span. +// Listeners are added once per page load (flag guards against re-render duplicates). +if (!document._stackScopeEditInit) { + document._stackScopeEditInit = true; + document.addEventListener('click', function(e) { + if (e.target.closest('.smd-scope-pencil')) { + e.preventDefault(); // prevent
from toggling + var nameEl = e.target.closest('.smd-scope-name'); + if (nameEl) { + var inp = nameEl.querySelector('input'); + if (inp) { inp.focus({preventScroll: true}); inp.select(); } + } + } + }); + document.addEventListener('keydown', function(e) { + if (e.key === 'Enter' && e.target.matches && e.target.matches('.smd-scope-name input')) { + e.preventDefault(); + e.target.blur(); + } + }); + document.addEventListener('focusout', function(e) { + if (e.target.matches && e.target.matches('.smd-scope-name input')) { + var nameEl = e.target.closest('.smd-scope-name'); + if (nameEl) { + var span = nameEl.querySelector('.smd-scope-name-text'); + if (span) { span.textContent = e.target.value || ''; } + } + } + }); +} + +// Auto-focus scope name input when name is empty (newly added scope). +(function() { + document.querySelectorAll('.smd-scope-name-text').forEach(function(span) { + if (!span.textContent.trim()) { + var inp = span.closest('.smd-scope-name').querySelector('input'); + if (inp) { setTimeout(function() { inp.focus(); }, 50); } + } + }); +}()); {{/js}} diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache index 594783b3d28..ca5058c57c6 100644 --- a/templates/metadata/metadatascope.mustache +++ b/templates/metadata/metadatascope.mustache @@ -134,7 +134,13 @@ {{#str}} scope, qtype_stack {{/str}} -
+
+ {{name}} + {{#input}}
- +
@@ -177,7 +172,7 @@ {{/properties}}
-
+
- \ No newline at end of file + diff --git a/tests/behat/metadata.feature b/tests/behat/metadata.feature index f7b1bdb0cb7..f575f292ab2 100644 --- a/tests/behat/metadata.feature +++ b/tests/behat/metadata.feature @@ -27,6 +27,7 @@ Feature: Create and edit STACK metadata When I am on the "Algebraic input" "core_question > edit" page logged in as teacher And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "Add author" "button" And I wait until "smdi_1_author_firstName" "field" exists And I set the field "smdi_1_author_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Edmund" @@ -56,6 +57,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "Add me as an author" "button" And I click on "Add language" "button" And I wait until "smdi_2_language_value" "field" exists @@ -79,11 +81,13 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"More data":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" + And I click on "stack-metadata-edit-switch" "checkbox" And I set the field "smdi_3_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Changed" And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_author_2_delete" "button" And I should not see "Smith" And I set the field "smdi_1_language_value" in the "#qtype-stack-metadata-content" "css_element" to "en-del" @@ -98,6 +102,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_scope_1_delete" "button" And I should not see "Added data" And I click on "smd_property_3_add" "button" @@ -109,6 +114,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "Add scope" "button" And I wait until "smdi_5_additional_scope" "field" exists And I set the field "smdi_5_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Another Scope" @@ -134,6 +140,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}},"Another Scope":{"Question2":{"Type2":"MC2"},"More data":{"Things2":"AAA2","Type2":"BBB2"},"No obj":"BBB3"}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I set the field "id_metadata_json" to multiline: """ { @@ -182,6 +189,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_property_5_add" "button" And I wait until "smdi_6_additional_property" "field" exists And I set the field "smdi_6_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Fish" @@ -196,6 +204,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_property_1_add" "button" And I wait until "smdi_8_additional_property" "field" exists And I set the field "smdi_8_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Horse" @@ -267,6 +276,7 @@ Feature: Create and edit STACK metadata When I am on the "Algebraic input" "core_question > edit" page logged in as teacher And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I set the field "id_metadata_json" to multiline: """ { @@ -341,6 +351,7 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"license":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Freeform too":{"Fish":{"Gills":"2"}}}}' And I click on "View and edit full metadata" "button" And I should see "STACK metadata is stored as a JSON object." + And I click on "stack-metadata-edit-switch" "checkbox" And I set the field "smdi_0_freeform_value" to multiline: """ {"x":[{"additional":"b"},{"license":"d"}]} diff --git a/tests/jest/metadata.container.test.js b/tests/jest/metadata.container.test.js index 7fdf6fdef0f..e3a8e33cf42 100644 --- a/tests/jest/metadata.container.test.js +++ b/tests/jest/metadata.container.test.js @@ -196,17 +196,16 @@ describe('addItem', () => { test('does not dispatch when update() returns false', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(false); - await instance.addItem({target: {id: 'smd_language_0_add'}}); + await instance.addItem({currentTarget: {id: 'smd_language_0_add'}}); expect(reactive.dispatch).not.toHaveBeenCalled(); }); - test('dispatches addItem with type derived from event.target.id when update succeeds', async () => { + test('dispatches addItem with type derived from event.currentTarget.id when update succeeds', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(true); reactive.dispatch.mockResolvedValue(undefined); - await instance.addItem({target: {id: 'smd_add_author_user'}}); - // id.split('_') → ['smd', 'add', 'author', 'user']; parts[1]='add', parts[2]='author' - expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'add', 'author'); + await instance.addItem({currentTarget: {id: 'smd_author_0_add'}}); + expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'author', '0'); }); }); @@ -216,19 +215,124 @@ describe('deleteItem', () => { test('does not dispatch when update() returns false', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(false); - await instance.deleteItem({target: {id: 'smd_author_1_delete'}}); + await instance.deleteItem({currentTarget: {id: 'smd_author_1_delete'}}); expect(reactive.dispatch).not.toHaveBeenCalled(); }); - test('dispatches deleteRow with id parts from event.target.id when update succeeds', async () => { + test('dispatches deleteRow with id parts from event.currentTarget.id when update succeeds', async () => { const {instance, reactive} = makeInstance(); jest.spyOn(instance, 'update').mockResolvedValue(true); reactive.dispatch.mockResolvedValue(undefined); - await instance.deleteItem({target: {id: 'smd_author_1_delete'}}); + await instance.deleteItem({currentTarget: {id: 'smd_author_1_delete'}}); expect(reactive.dispatch).toHaveBeenCalledWith('deleteRow', 'author', '1'); }); }); +// ── enableEditing ─────────────────────────────────────────────────────────────── + +describe('enableEditing', () => { + test('sets edit mode and adds the edit class to the rendered form', () => { + const {instance} = makeInstance(); + const event = {preventDefault: jest.fn()}; + const form = {classList: {add: jest.fn()}}; + instance.getElement.mockImplementation(sel => { + if (sel === instance.selectors.FORM) { + return form; + } + return null; + }); + instance.enableEditing(event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(instance.isEditing).toBe(true); + expect(form.classList.add).toHaveBeenCalledWith('smd-editing'); + }); + + test('still records edit mode when the form element is missing', () => { + const {instance} = makeInstance(); + instance.getElement.mockReturnValue(null); + expect(() => instance.enableEditing()).not.toThrow(); + expect(instance.isEditing).toBe(true); + }); +}); + +// ── toggleEditing ─────────────────────────────────────────────────────────────── + +describe('toggleEditing', () => { + test('enables editing when the switch is checked', async () => { + const {instance} = makeInstance(); + const event = { + preventDefault: jest.fn(), + currentTarget: {checked: true}, + }; + jest.spyOn(instance, 'enableEditing'); + await instance.toggleEditing(event); + expect(instance.enableEditing).toHaveBeenCalledWith(event); + }); + + test('validates and updates in read mode when the switch is unchecked', async () => { + const {instance} = makeInstance(); + const event = {currentTarget: {checked: false}}; + instance.isEditing = true; + jest.spyOn(instance, 'validateInputs').mockReturnValue(true); + jest.spyOn(instance, 'update').mockResolvedValue(true); + jest.spyOn(instance, 'syncEditToggle').mockImplementation(() => {}); + await instance.toggleEditing(event); + expect(instance.validateInputs).toHaveBeenCalled(); + expect(instance.update).toHaveBeenCalledWith(false); + expect(instance.isEditing).toBe(false); + expect(instance.syncEditToggle).toHaveBeenCalled(); + expect(event.currentTarget.checked).toBe(false); + }); + + test('keeps edit mode switched on when validation fails', async () => { + const {instance} = makeInstance(); + const event = {currentTarget: {checked: false}}; + jest.spyOn(instance, 'validateInputs').mockReturnValue(false); + jest.spyOn(instance, 'update').mockResolvedValue(true); + await instance.toggleEditing(event); + expect(instance.update).not.toHaveBeenCalled(); + expect(event.currentTarget.checked).toBe(true); + }); + + test('keeps edit mode switched on when update fails after validation', async () => { + const {instance} = makeInstance(); + const event = {currentTarget: {checked: false}}; + instance.isEditing = true; + jest.spyOn(instance, 'validateInputs').mockReturnValue(true); + jest.spyOn(instance, 'update').mockResolvedValue(false); + await instance.toggleEditing(event); + expect(instance.isEditing).toBe(true); + expect(event.currentTarget.checked).toBe(true); + }); +}); + +// ── disableEditing ────────────────────────────────────────────────────────────── + +describe('disableEditing', () => { + test('clears edit mode and removes the edit class from the rendered form', () => { + const {instance} = makeInstance(); + const form = {classList: {remove: jest.fn()}}; + instance.isEditing = true; + instance.getElement.mockImplementation(sel => { + if (sel === instance.selectors.FORM) { + return form; + } + return null; + }); + instance.disableEditing(); + expect(instance.isEditing).toBe(false); + expect(form.classList.remove).toHaveBeenCalledWith('smd-editing'); + }); + + test('still clears edit mode when the form element is missing', () => { + const {instance} = makeInstance(); + instance.isEditing = true; + instance.getElement.mockReturnValue(null); + expect(() => instance.disableEditing()).not.toThrow(); + expect(instance.isEditing).toBe(false); + }); +}); + // ── makeAuthor ──────────────────────────────────────────────────────────────── describe('makeAuthor', () => { @@ -381,14 +485,25 @@ describe('reloadContainerComponent', () => { await expect(instance.reloadContainerComponent({state})).rejects.toThrow('Missing metadata container'); }); - test('registers click listeners for add and delete buttons after render', async () => { + test('registers change listener for edit switch and click listeners for add and delete buttons after render', async () => { const {instance} = makeInstance(); const state = makeState(); metadata.jsonStringify.mockReturnValue('{}'); const fakeContainer = {}; const fakeButton = {}; - mockQuerySelector.mockReturnValue({value: '{}'}); - instance.getElement.mockReturnValue(fakeContainer); + const fakeToggle = {}; + mockQuerySelector.mockImplementation(sel => { + if (sel === instance.selectors.EDITTOGGLE) { + return fakeToggle; + } + return {value: '{}'}; + }); + instance.getElement.mockImplementation(sel => { + if (sel === instance.selectors.METADATACONTAINER) { + return fakeContainer; + } + return null; + }); instance.getElements.mockImplementation(sel => { if (sel === instance.selectors.ADDITEM || sel === instance.selectors.DELETEITEM) { return [fakeButton]; @@ -396,10 +511,21 @@ describe('reloadContainerComponent', () => { return []; }); await instance.reloadContainerComponent({state}); + expect(instance.addEventListener).toHaveBeenCalledWith(fakeToggle, 'change', instance.toggleEditing); expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.addItem); expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.deleteItem); }); + test('passes the current edit state to the template data', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + let data = await captureData(instance, makeState()); + expect(data.isEditing).toBe(false); + instance.isEditing = true; + data = await captureData(instance, makeState()); + expect(data.isEditing).toBe(true); + }); + // ── data.author ───────────────────────────────────────────────────────────── describe('data.author', () => { From cc1dbd4651fd8943cd10bd661c26ab07e04e4be8 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 28 Jul 2026 10:45:33 +0100 Subject: [PATCH 078/100] iss1171 - Fix mustache validation --- lang/en/qtype_stack.php | 1 + styles.css | 13 ++++++---- .../metadata/metadataadditional.mustache | 9 ++++++- templates/metadata/metadatascope.mustache | 26 ++++++++++++------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index 6278180916f..d822abe3502 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -443,6 +443,7 @@ $string['JSONbroken'] = 'Metadata is invalid JSON.'; $string['metadataexplanation'] = 'STACK metadata is stored as a JSON object. You can edit metadata in the inputs above or directly edit the JSON below and then click \'Update inputs from JSON\'. On closing this metadata window, the inputs will be validated and, if this is successful you will be returned to the main question edit form. You will still need to save the question as normal to save the metadata.'; $string['scope'] = 'Scope'; +$string['scopename'] = 'Scope name'; $string['property'] = 'Property'; $string['qualifier'] = 'Qualifier'; $string['value'] = 'Value'; diff --git a/styles.css b/styles.css index 4a5ec8b1833..3bb400c4de9 100644 --- a/styles.css +++ b/styles.css @@ -1552,19 +1552,22 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. white-space: nowrap; } -.stack-metadata .smd-editing .smd-scope-name-text, .stack-metadata .smd-editing .smd-license-display-text, .stack-metadata .smd-editing .smd-ispartof-display-text, .stack-metadata .smd-editing .smd-freeform-display-text { display: none; } -.stack-metadata .smd-editing .smd-scope-name .fitem { - flex: 1; +.stack-metadata .smd-scope-name-table { + display: none; +} + +.stack-metadata .smd-editing .smd-scope-name-table { + display: table; } -.stack-metadata .smd-scope-card > summary input { - user-select: text; +.stack-metadata .smd-prop-table.smd-scope-name-table { + width: 24%; } .stack-metadata .smd-scope-chevron { diff --git a/templates/metadata/metadataadditional.mustache b/templates/metadata/metadataadditional.mustache index f9aec6c06ea..727d714234d 100644 --- a/templates/metadata/metadataadditional.mustache +++ b/templates/metadata/metadataadditional.mustache @@ -45,9 +45,13 @@ "iderror": "smde_2_additional_value_error" } }, - "id": "2" + "id": "2", + "istest": true } }} +{{#istest}} + +{{/istest}} +{{#istest}} +
{{property.element.value}} @@ -85,3 +89,6 @@
+{{/istest}} diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache index bb7b84adc31..7c13ebf4c92 100644 --- a/templates/metadata/metadatascope.mustache +++ b/templates/metadata/metadatascope.mustache @@ -134,7 +134,7 @@
{{#str}} scope, qtype_stack {{/str}} -
+ {{name}} - {{#input}} - - {{> core_form/element-text-inline }} - {{/input}} -
+
- +
+ + + + + +
+ {{#input}} + + {{> core_form/element-text-inline }} + {{/input}} +
+ From 9c5e40c4852ef513316fdd839efbd2c570025ee0 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Tue, 28 Jul 2026 12:18:34 +0100 Subject: [PATCH 079/100] iss1171 - Tweaks for layout, consistency, accessibility --- styles.css | 147 +++++++++++------- .../metadata/metadataadditional.mustache | 29 ++-- templates/metadata/metadataauthor.mustache | 16 +- templates/metadata/metadatacontent.mustache | 11 +- templates/metadata/metadatascope.mustache | 62 +++----- 5 files changed, 137 insertions(+), 128 deletions(-) diff --git a/styles.css b/styles.css index 3bb400c4de9..7be07f9962d 100644 --- a/styles.css +++ b/styles.css @@ -1456,18 +1456,18 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. } .stack-metadata .smd-section-label { - font-size: 0.7rem; + font-size: 0.9rem; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.09em; - color: #6c757d; + letter-spacing: 0; + color: #343a40; margin-bottom: 0.6rem; } .stack-metadata .smd-col-label { - font-size: 0.75rem; - font-weight: 600; - color: #495057; + font-size: 0.9rem; + font-weight: 700; + color: #343a40; display: block; margin-bottom: 0.25rem; } @@ -1532,6 +1532,17 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. border-bottom: 1px solid #dee2e6; } +.stack-metadata .smd-scope-card[open] > .smd-scope-header { + background: rgba(15, 108, 191, 0.08); + box-shadow: inset 4px 0 0 #0f6cbf; +} + +.stack-metadata .smd-scope-card > .smd-scope-header:focus-visible { + outline: 3px solid #0f6cbf; + outline-offset: -3px; + background: rgba(15, 108, 191, 0.14); +} + .stack-metadata .smd-scope-header .smd-col-label { white-space: nowrap; margin-bottom: 0; @@ -1558,16 +1569,45 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. display: none; } -.stack-metadata .smd-scope-name-table { +.stack-metadata .smd-field-grid { + display: grid; + column-gap: 0.5rem; + row-gap: 0.1rem; + align-items: flex-start; + width: 100%; + margin-bottom: 0.1rem; +} + +.stack-metadata .smd-author-grid { + grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 3fr) minmax(4.5rem, 1fr) 2.25rem; +} + +.stack-metadata .smd-additional-grid { + grid-template-columns: minmax(0, 24%) minmax(0, 20%) minmax(0, 1fr) 2.25rem; +} + +.stack-metadata .smd-scope-name-grid { + grid-template-columns: minmax(0, 24%) 2.25rem; +} + +.stack-metadata .smd-field-header { + margin-bottom: 0.3rem; +} + +.stack-metadata .smd-field-header .smd-col-label { + margin-bottom: 0; +} + +.stack-metadata .smd-scope-name-editor { display: none; } -.stack-metadata .smd-editing .smd-scope-name-table { - display: table; +.stack-metadata .smd-editing .smd-scope-name-editor { + display: grid; } -.stack-metadata .smd-prop-table.smd-scope-name-table { - width: 24%; +.stack-metadata .smd-scope-name-editor .smd-row-delete { + margin-top: 1.75rem; } .stack-metadata .smd-scope-chevron { @@ -1588,30 +1628,17 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. .stack-metadata .smd-section .row .fitem, .stack-metadata .smd-section .row .felement, +.stack-metadata .smd-field-grid .fitem, +.stack-metadata .smd-field-grid .felement, .stack-metadata .smd-scope-body .fitem, .stack-metadata .smd-scope-body .felement { margin: 0; padding: 0; } -.stack-metadata .smd-prop-table { - width: 100%; - border-collapse: collapse; - border-spacing: 0; - table-layout: fixed; - margin-bottom: 0.25rem; - border: 0; - border-color: transparent; - outline: 0; - --bs-table-border-color: transparent; -} - -.stack-metadata .smd-prop-table > :not(caption) > * > *, -.stack-metadata .smd-prop-table thead, -.stack-metadata .smd-prop-table tbody, -.stack-metadata .smd-prop-table tr, -.stack-metadata .smd-prop-table th, -.stack-metadata .smd-prop-table td { +.stack-metadata .smd-field-row, +.stack-metadata .smd-cell, +.stack-metadata .smd-cell-actions { border: 0; border-color: transparent; border-width: 0; @@ -1619,33 +1646,11 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. outline: 0; } -.stack-metadata .smd-prop-table th { - font-size: 0.75rem; - font-weight: 600; - color: #495057; - padding: 0 0.5rem 0.3rem 0; - text-align: left; - white-space: nowrap; - background: transparent; -} - -.stack-metadata .smd-prop-table td, .stack-metadata .smd-cell, .stack-metadata .smd-cell-actions { - padding: 0.1rem 0.5rem 0.1rem 0; + padding: 0.05rem 0; vertical-align: top; background: transparent; - border: 0; - border-color: transparent; - border-width: 0; - box-shadow: none; - outline: 0; -} - -.stack-metadata .smd-prop-row { - border: 0; - box-shadow: none; - outline: 0; } .stack-metadata .smd-cell-text, @@ -1669,16 +1674,15 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. } .stack-metadata .smd-cell-actions { - width: 2rem; + width: 2.25rem; text-align: right; vertical-align: top; - padding-right: 0; } .stack-metadata .smd-editing .smd-cell .fitem { border: 0; box-shadow: none; - min-height: 3.6rem; + min-height: 0; } .stack-metadata .smd-editing .smd-cell .felement { @@ -1704,7 +1708,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. .stack-metadata .smd-editing .smd-cell .invalid-feedback, .stack-metadata .smd-editing .smd-cell .form-control-feedback { margin-top: 0.2rem; - min-height: 1rem; + min-height: 0; } .stack-metadata .smd-author-display, @@ -1717,6 +1721,10 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. min-width: 0; } +.stack-metadata .smd-author-display { + grid-column: 1 / -2; +} + .stack-metadata .smd-license-wrap, .stack-metadata .smd-ispartof-wrap { flex-wrap: wrap; @@ -1744,14 +1752,28 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. } .stack-metadata .smd-editing .smd-author-col-headers { - display: flex; - flex-wrap: wrap; + display: grid; +} + +.stack-metadata .smd-editing .smd-author-row .smd-row-actions { + align-self: flex-start; } .stack-metadata .smd-row-delete { color: #dc3545; display: none; + align-items: center; + justify-content: center; flex-shrink: 0; + width: 2.25rem; + height: 2.25rem; + min-width: 2.25rem; + line-height: 1; +} + +.stack-metadata .smd-row-delete i { + font-size: 1.1rem; + line-height: 1; } .stack-metadata .smd-editing .smd-row-delete { @@ -1759,6 +1781,8 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. } .stack-metadata .smd-add-btn { + color: #343a40; + border-color: #343a40; width: 1.75rem; height: 1.75rem; padding: 0; @@ -1769,6 +1793,13 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. flex-shrink: 0; } +.stack-metadata .smd-add-btn:hover, +.stack-metadata .smd-add-btn:focus-visible { + color: #fff; + background-color: #343a40; + border-color: #343a40; +} + .stack-metadata .smd-col-divider { border-left: 1px solid #dee2e6; padding-left: 1.25rem; diff --git a/templates/metadata/metadataadditional.mustache b/templates/metadata/metadataadditional.mustache index 727d714234d..0a7c289c309 100644 --- a/templates/metadata/metadataadditional.mustache +++ b/templates/metadata/metadataadditional.mustache @@ -45,15 +45,11 @@ "iderror": "smde_2_additional_value_error" } }, - "id": "2", - "istest": true + "id": "2" } }} -{{#istest}} -
-{{/istest}} - - - - - - -{{#istest}} -
+
+
{{property.element.value}} {{#property}} {{> core_form/element-text-inline }} {{/property}} -
+ +
{{qualifier.element.value}} {{#qualifier}} {{> core_form/element-text-inline }} {{/qualifier}} -
+ +
{{value.element.value}} {{#value}} {{> core_form/element-text-inline }} {{/value}} -
+ +
-
-{{/istest}} +
+
diff --git a/templates/metadata/metadataauthor.mustache b/templates/metadata/metadataauthor.mustache index 11f68f74e1e..fa66d4420af 100644 --- a/templates/metadata/metadataauthor.mustache +++ b/templates/metadata/metadataauthor.mustache @@ -22,39 +22,39 @@ "id": "1" } }} -
+
{{! Read mode: compact one-line display + delete }} -
+
{{firstname.element.value}} {{lastname.element.value}}{{#institution.element.value}}, {{institution.element.value}}{{/institution.element.value}}{{#year.element.value}} ({{year.element.value}}){{/year.element.value}}
{{! Edit mode: four input columns }} -
+
{{#firstname}} {{> core_form/element-text-inline }} {{/firstname}}
-
+
{{#lastname}} {{> core_form/element-text-inline }} {{/lastname}}
-
+
{{#institution}} {{> core_form/element-text-inline }} {{/institution}}
-
+
{{#year}} {{>qtype_stack/metadata/metadatayear}} {{/year}}
-
+
+
+
+
+ {{#input}} + + {{> core_form/element-text-inline }} + {{/input}} +
- - -
- - - - - - -
- {{#input}} - - {{> core_form/element-text-inline }} - {{/input}} -
- - - - - - - - - - - - - - - - - {{#properties}} - {{>qtype_stack/metadata/metadataadditional}} - {{/properties}} - -
{{#str}} property, qtype_stack {{/str}}{{#str}} qualifier, qtype_stack {{/str}}{{#str}} value, qtype_stack {{/str}}
+
+
+
{{#str}} property, qtype_stack {{/str}}
+
{{#str}} qualifier, qtype_stack {{/str}}
+
{{#str}} value, qtype_stack {{/str}}
+
+
+ {{#properties}} + {{>qtype_stack/metadata/metadataadditional}} + {{/properties}}
@@ -465,7 +465,7 @@

{{! JSON metadata — collapsible }} -
+
-
+
{{#input}}
- +
+ +
-
-
{{#str}} property, qtype_stack {{/str}}
-
{{#str}} qualifier, qtype_stack {{/str}}
-
{{#str}} value, qtype_stack {{/str}}
-
+
+
{{#str}} property, qtype_stack {{/str}}
+
{{#str}} qualifier, qtype_stack {{/str}}
+
{{#str}} value, qtype_stack {{/str}}
+
{{#properties}} {{>qtype_stack/metadata/metadataadditional}} {{/properties}}
-
-

+

{{#str}} metadataexplanation, qtype_stack {{/str}}

From 8c850bcd480a6fa8728cb05e01cdd3a37e6f27a9 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 30 Jul 2026 12:15:20 +0100 Subject: [PATCH 092/100] iss1171 - Fix screen reader issues --- templates/metadata/metadataauthor.mustache | 16 ++++++++++++---- templates/metadata/metadatacontent.mustache | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/templates/metadata/metadataauthor.mustache b/templates/metadata/metadataauthor.mustache index 082c00386ea..1448bbd802f 100644 --- a/templates/metadata/metadataauthor.mustache +++ b/templates/metadata/metadataauthor.mustache @@ -30,25 +30,33 @@ {{! Edit mode: four input columns }}
{{#firstname}} - + {{> core_form/element-text-inline }} {{/firstname}}
{{#lastname}} - + {{> core_form/element-text-inline }} {{/lastname}}
{{#institution}} - + {{> core_form/element-text-inline }} {{/institution}}
{{#year}} - + {{>qtype_stack/metadata/metadatayear}} {{/year}}
diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index 2ef20af862d..521c5ffcb40 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -385,8 +385,8 @@ {{! License + isPartOf }}
-
- +
+
{{#license.element.options}}{{#selected}}{{text}}{{/selected}}{{/license.element.options}} {{#license}} From eab391d971ce5f350d3f1bc1f50c65b983536f81 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 30 Jul 2026 14:49:19 +0100 Subject: [PATCH 093/100] iss1171 - Fix Behat and lint --- templates/metadata/metadatacontent.mustache | 2 ++ templates/metadata/metadatascope.mustache | 1 + tests/behat/metadata.feature | 25 ++++++++++----------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache index 521c5ffcb40..0b5cdd04d19 100644 --- a/templates/metadata/metadatacontent.mustache +++ b/templates/metadata/metadatacontent.mustache @@ -157,6 +157,7 @@ }, "scope": [ { + "id": "qtype-stack-metadata-scope-1", "name": "d", "firstProp": "1", "properties": [ @@ -272,6 +273,7 @@ } }, { + "id": "qtype-stack-metadata-scope-4", "name": "n", "firstProp": "4", "properties": [ diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache index 327564e5ea1..fa483e327bd 100644 --- a/templates/metadata/metadatascope.mustache +++ b/templates/metadata/metadatascope.mustache @@ -15,6 +15,7 @@ @template qtype_stack/metadata/metadatascope Example context (json): { + "id": "qtype-stack-metadata-scope-1", "name": "d", "firstProp": "1", "properties": [ diff --git a/tests/behat/metadata.feature b/tests/behat/metadata.feature index fe8f518c3b2..5f6230dab10 100644 --- a/tests/behat/metadata.feature +++ b/tests/behat/metadata.feature @@ -26,7 +26,7 @@ Feature: Create and edit STACK metadata Scenario: Create and edit STACK metadata When I am on the "Algebraic input" "core_question > edit" page logged in as teacher And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_author_0_add" "button" And I wait until "smdi_1_author_firstName" "field" exists @@ -60,7 +60,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "stack-metadata-make-author" "button" And I wait until "smdi_3_author_firstName" "field" exists @@ -95,7 +95,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_author_2_delete" "button" And the focused element should be "#smd_author_0_add" "css_element" @@ -114,7 +114,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_scope_1_delete" "button" And the focused element should be "#smd_scope_0_add" "css_element" @@ -127,7 +127,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_scope_0_add" "button" And I wait until "smdi_5_additional_scope" "field" exists @@ -153,7 +153,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}},"Another Scope":{"Question2":{"Type2":"MC2"},"More data":{"Things2":"AAA2","Type2":"BBB2"},"No obj":"BBB3"}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "#qtype-stack-metadata-json-section > summary" "css_element" And I set the field "id_metadata_json" to multiline: @@ -204,7 +204,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_property_5_add" "button" And I wait until "smdi_6_additional_property" "field" exists @@ -219,7 +219,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "smd_property_1_add" "button" And I wait until "smdi_8_additional_property" "field" exists @@ -228,7 +228,6 @@ Feature: Create and edit STACK metadata And I set the field "smdi_8_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Nessie" And I click on "Validate and close" "button" And I should see "Required" in the "#smde_7_additional_qualifier_error" "css_element" - And I click on "#qtype-stack-metadata-json-section > summary" "css_element" And I set the field "id_metadata_json" to multiline: """ { @@ -290,10 +289,10 @@ Feature: Create and edit STACK metadata And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' @javascript - Scenario: Create and edit STACK freeform metadata + Scenario: Create and edit STACK Freeform metadata When I am on the "Algebraic input" "core_question > edit" page logged in as teacher And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I click on "#qtype-stack-metadata-json-section > summary" "css_element" And I set the field "id_metadata_json" to multiline: @@ -370,7 +369,7 @@ Feature: Create and edit STACK metadata And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"license":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Freeform too":{"Fish":{"Gills":"2"}}}}' And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "stack-metadata-edit-switch" "checkbox" And I set the field "smdi_0_freeform_value" to multiline: """ @@ -386,6 +385,6 @@ Feature: Create and edit STACK metadata And I set the field "item_qtype_stack" to "1" And I press "submitbutton" And I click on "View and edit full metadata" "button" - And I should see "STACK metadata is stored as a JSON object." + And I should see "Freeform metadata" And I click on "Validate and close" "button" And I check the hidden input "metadata" is '{"author":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en"],"license":"unknown","isPartOf":"","additional":{},"freeform":{}}' From cc4c6390288690a058f6cabb79529f0f533193f4 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 30 Jul 2026 16:28:53 +0100 Subject: [PATCH 094/100] iss1171 - Add tests. Fix Grunt lint. --- styles.css | 6 +- templates/metadata/metadatascope.mustache | 2 +- tests/jest/metadata.container.test.js | 77 ++++++++++++++++++++--- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/styles.css b/styles.css index 55ac66ae69f..5834017665a 100644 --- a/styles.css +++ b/styles.css @@ -1462,8 +1462,12 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. white-space: nowrap; } +.stack-metadata .smd-scope-header { + border-bottom: 1px solid var(--smd-border); +} + .stack-metadata .smd-scope-header:is(:focus, :focus-visible) { - border: 3px solid var(--smd-primary) !important; + border: 3px solid var(--smd-primary); outline: 0; } diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache index fa483e327bd..b2fc7775a52 100644 --- a/templates/metadata/metadatascope.mustache +++ b/templates/metadata/metadatascope.mustache @@ -132,7 +132,7 @@ } }}
- + {{#str}} scope, qtype_stack {{/str}} diff --git a/tests/jest/metadata.container.test.js b/tests/jest/metadata.container.test.js index 14b76baa7cf..bf5a7d9e79e 100644 --- a/tests/jest/metadata.container.test.js +++ b/tests/jest/metadata.container.test.js @@ -233,7 +233,11 @@ describe('deleteItem', () => { describe('focus handling', () => { test('restorePendingFocus moves focus to the first input in the last added row', () => { const {instance} = makeInstance(); - const oldInput = {id: 'smdi_1_language_value', focus: jest.fn(), select: jest.fn()}; + const oldInput = { + id: 'smdi_1_language_value', + focus: jest.fn(), + select: jest.fn(), + }; const newInput = { id: 'smdi_2_language_value', focus: jest.fn(), @@ -251,6 +255,8 @@ describe('focus handling', () => { expect(newInput.focus).toHaveBeenCalled(); expect(newInput.select).toHaveBeenCalled(); + expect(oldInput.focus).not.toHaveBeenCalled(); + expect(oldInput.select).not.toHaveBeenCalled(); }); test('restorePendingFocus moves focus to the corresponding add button after deletion', () => { @@ -298,10 +304,10 @@ describe('focus handling', () => { expect(add.focus).not.toHaveBeenCalled(); }); - test('restorePendingFocus uses the scope add button for deleted additional metadata', () => { + test('restorePendingFocus uses the property add button after deleting additional metadata', () => { const {instance} = makeInstance(); const add = {id: 'smd_property_1_add', focus: jest.fn()}; - const oldScopeCard = { + const sourceScopeCard = { querySelector: jest.fn(sel => { if (sel === instance.selectors.ADDSCOPE) { return {value: 'dc'}; @@ -309,7 +315,7 @@ describe('focus handling', () => { return null; }), }; - const newScopeCard = { + const renderedScopeCard = { id: 'qtype-stack-metadata-scope-2', querySelector: jest.fn(sel => { if (sel === instance.selectors.ADDSCOPE) { @@ -321,22 +327,73 @@ describe('focus handling', () => { return null; }), }; - const deleted = { - closest: jest.fn(() => oldScopeCard), + const deletedPropertyButton = { + closest: jest.fn(() => sourceScopeCard), }; instance.getElements.mockImplementation(sel => { if (sel === instance.selectors.SCOPECARD) { - return [newScopeCard]; + return [renderedScopeCard]; } return []; }); - instance.queueFocus('delete', 'additional', deleted); + instance.queueFocus('delete', 'additional', deletedPropertyButton); instance.restorePendingFocus(); expect(add.focus).toHaveBeenCalled(); }); + test('restorePendingFocus moves focus to the add property button within the same scope', () => { + const {instance} = makeInstance(); + const wrongScopeAdd = { + id: 'smd_property_1_add', + focus: jest.fn(), + matches: jest.fn(sel => sel === instance.selectors.ADDBUTTONS), + }; + const matchingScopeAdd = { + id: 'smd_property_3_add', + focus: jest.fn(), + matches: jest.fn(sel => sel === instance.selectors.ADDBUTTONS), + }; + const wrongScopeCard = { + querySelector: jest.fn(sel => { + if (sel === instance.selectors.ADDSCOPE) { + return {value: 'dc'}; + } + if (sel === instance.selectors.DELETEADDITIONAL) { + return wrongScopeAdd; + } + return null; + }), + }; + const matchingScopeCard = { + querySelector: jest.fn(sel => { + if (sel === instance.selectors.ADDSCOPE) { + return {value: 'lom'}; + } + if (sel === instance.selectors.DELETEADDITIONAL) { + return matchingScopeAdd; + } + return null; + }), + }; + const deletedPropertyButton = { + closest: jest.fn(() => matchingScopeCard), + }; + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.SCOPECARD) { + return [wrongScopeCard, matchingScopeCard]; + } + return []; + }); + + instance.queueFocus('delete', 'additional', deletedPropertyButton); + instance.restorePendingFocus(); + + expect(matchingScopeAdd.focus).toHaveBeenCalledWith({focusVisible: true}); + expect(wrongScopeAdd.focus).not.toHaveBeenCalled(); + }); + test('setDetailsState records scope open state', () => { const {instance} = makeInstance(); const detail = { @@ -580,7 +637,7 @@ describe('reloadContainerComponent', () => { expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.deleteItem); }); - test('update inputs button focuses itself and applies JSON changes', async () => { + test('update inputs button queues focus and calls updateInputs', async () => { const {instance} = makeInstance(); const state = makeState(); metadata.jsonStringify.mockReturnValue('{}'); @@ -606,7 +663,7 @@ describe('reloadContainerComponent', () => { )[2]; updateInputsListener(); - expect(instance.pendingFocus).toBe(instance.selectors.UPDATEINPUTS); + expect(instance.pendingFocus).toEqual({selector: instance.selectors.UPDATEINPUTS}); expect(instance.updateInputs).toHaveBeenCalled(); }); From fd88fbb0e5336049148bc60b0795127d11729922 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 30 Jul 2026 16:35:13 +0100 Subject: [PATCH 095/100] iss1171 - Update for Moodle 4.2 --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/src/metadata/container.js | 20 +++++++-------- tests/jest/metadata.container.test.js | 33 ++++++++++++++++--------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 6998fdccb83..780e7613277 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(()=>{this.pendingFocus={selector:this.selectors.REVERT},this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){var _focusTarget,_focusTarget3;if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;if(this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length)return;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},await this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},await this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(async()=>{this.pendingFocus={selector:this.selectors.REVERT},await this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}async updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),await this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}async revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),await this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){var _focusTarget,_focusTarget3;if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;if(this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length)return;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index d3e698fb940..0c70e53a5b3 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n () => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n () => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n () => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index 97dd4a19c70..ebf72ddfd11 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -232,9 +232,9 @@ export default class extends BaseComponent { this.addEventListener( this.getElement(this.selectors.UPDATEJSON), 'click', - () => { + async() => { this.pendingFocus = {selector: this.selectors.UPDATEJSON}; - this.update(); + await this.update(); } ); const addButtons = this.getElements(this.selectors.ADDITEM); @@ -256,9 +256,9 @@ export default class extends BaseComponent { this.addEventListener( this.getElement(this.selectors.UPDATEINPUTS), 'click', - () => { + async() => { this.pendingFocus = {selector: this.selectors.UPDATEINPUTS}; - this.updateInputs(); + await this.updateInputs(); } ); this.addEventListener( @@ -269,9 +269,9 @@ export default class extends BaseComponent { this.addEventListener( this.getElement(this.selectors.REVERT), 'click', - () => { + async() => { this.pendingFocus = {selector: this.selectors.REVERT}; - this.revert(); + await this.revert(); } ); @@ -409,7 +409,7 @@ export default class extends BaseComponent { /** * Update state from the currently entered JSON if JSON is valid. */ - updateInputs() { + async updateInputs() { const jsonElement = this.getElement(this.selectors.JSONINPUT); let data = null; try { @@ -420,7 +420,7 @@ export default class extends BaseComponent { return; } jsonElement.value = metadata.jsonStringify(data, 4); - this.reactive.dispatch('updateFromJson', data); + await this.reactive.dispatch('updateFromJson', data); } /** @@ -440,7 +440,7 @@ export default class extends BaseComponent { * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure * in metadata.js. */ - revert() { + async revert() { const jsonElement = this.getElement(this.selectors.JSONINPUT); let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null; let previousdata = null; @@ -454,7 +454,7 @@ export default class extends BaseComponent { return; } jsonElement.value = metadata.jsonStringify(previousdata, 4); - this.reactive.dispatch('updateFromJson', previousdata); + await this.reactive.dispatch('updateFromJson', previousdata); } /** diff --git a/tests/jest/metadata.container.test.js b/tests/jest/metadata.container.test.js index bf5a7d9e79e..100258a7247 100644 --- a/tests/jest/metadata.container.test.js +++ b/tests/jest/metadata.container.test.js @@ -481,17 +481,17 @@ describe('makeAuthor', () => { // ── updateInputs ─────────────────────────────────────────────────────────────── describe('updateInputs', () => { - test('notifies validation failure and returns early when JSON is invalid', () => { + test('notifies validation failure and returns early when JSON is invalid', async () => { const {instance, reactive} = makeInstance(); const jsonEl = {value: '{invalid json'}; instance.getElement.mockReturnValue(jsonEl); metadata.jsonToState.mockImplementation(() => { throw new SyntaxError('Unexpected token'); }); - instance.updateInputs(); + await instance.updateInputs(); expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, expect.any(String)); expect(reactive.dispatch).not.toHaveBeenCalled(); }); - test('clears error, updates element value and dispatches updateFromJson on valid JSON', () => { + test('clears error, updates element value and dispatches updateFromJson on valid JSON', async () => { const {instance, reactive} = makeInstance(); const jsonEl = {value: '{"language":[{"id":1,"value":"en"}]}'}; instance.getElement.mockReturnValue(jsonEl); @@ -499,7 +499,7 @@ describe('updateInputs', () => { metadata.jsonToState.mockReturnValue(parsed); metadata.jsonStringify.mockReturnValue('prettified'); reactive.dispatch.mockResolvedValue(undefined); - instance.updateInputs(); + await instance.updateInputs(); expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, ''); expect(jsonEl.value).toBe('prettified'); expect(reactive.dispatch).toHaveBeenCalledWith('updateFromJson', parsed); @@ -509,7 +509,7 @@ describe('updateInputs', () => { // ── revert ───────────────────────────────────────────────────────────────────── describe('revert', () => { - test('restores JSON and dispatches updateFromJson when saved value is valid', () => { + test('restores JSON and dispatches updateFromJson when saved value is valid', async () => { const {instance, reactive} = makeInstance(); const jsonEl = {value: ''}; instance.getElement.mockReturnValue(jsonEl); @@ -519,12 +519,12 @@ describe('revert', () => { metadata.jsonToState.mockReturnValue(parsed); metadata.jsonStringify.mockReturnValue('pretty'); reactive.dispatch.mockResolvedValue(undefined); - instance.revert(); + await instance.revert(); expect(jsonEl.value).toBe('pretty'); expect(reactive.dispatch).toHaveBeenCalledWith('updateFromJson', parsed); }); - test('notifies validation failure and restores broken JSON when saved value is invalid', () => { + test('notifies validation failure and restores broken JSON when saved value is invalid', async () => { const {instance, reactive} = makeInstance(); const jsonEl = {value: ''}; instance.getElement.mockReturnValue(jsonEl); @@ -534,7 +534,7 @@ describe('revert', () => { throw parseError; }); reactive.dispatch.mockResolvedValue(undefined); - instance.revert(); + await instance.revert(); expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, parseError.message); expect(jsonEl.value).toBe('{broken'); expect(metadata.lib.brokenMetadata).toBe(parseError.message); @@ -637,13 +637,16 @@ describe('reloadContainerComponent', () => { expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.deleteItem); }); - test('update inputs button queues focus and calls updateInputs', async () => { + test('update inputs button queues focus and awaits updateInputs', async () => { const {instance} = makeInstance(); const state = makeState(); metadata.jsonStringify.mockReturnValue('{}'); const fakeContainer = {}; const fakeUpdateInputs = {}; - jest.spyOn(instance, 'updateInputs').mockImplementation(() => {}); + let resolveUpdateInputs = null; + jest.spyOn(instance, 'updateInputs').mockImplementation(() => new Promise(resolve => { + resolveUpdateInputs = resolve; + })); mockQuerySelector.mockReturnValue({value: '{}'}); instance.getElement.mockImplementation(sel => { if (sel === instance.selectors.METADATACONTAINER) { @@ -661,10 +664,18 @@ describe('reloadContainerComponent', () => { const updateInputsListener = instance.addEventListener.mock.calls.find( call => call[0] === fakeUpdateInputs && call[1] === 'click' )[2]; - updateInputsListener(); + let completed = false; + const listenerPromise = updateInputsListener().then(() => { + completed = true; + }); expect(instance.pendingFocus).toEqual({selector: instance.selectors.UPDATEINPUTS}); expect(instance.updateInputs).toHaveBeenCalled(); + await Promise.resolve(); + expect(completed).toBe(false); + resolveUpdateInputs(); + await listenerPromise; + expect(completed).toBe(true); }); test('passes the current edit state to the template data', async () => { From f4f40425e70c93cd199579ac49813eb53fe0898d Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 31 Jul 2026 09:06:29 +0100 Subject: [PATCH 096/100] iss1171 - Behat fix. Update screenshot. --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/src/metadata/container.js | 13 +++++++++++++ doc/content/metadata.png | Bin 73197 -> 59257 bytes doc/en/Authoring/Metadata.md | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index 780e7613277..cb2c893b949 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},await this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},await this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(async()=>{this.pendingFocus={selector:this.selectors.REVERT},await this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}async updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),await this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}async revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),await this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){var _focusTarget,_focusTarget3;if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;if(this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length)return;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},await this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},await this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(async()=>{this.pendingFocus={selector:this.selectors.REVERT},await this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}async updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),await this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}async revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),await this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length||window.setTimeout((()=>{this.focusPendingTarget(pendingFocus)}),0)}focusPendingTarget(pendingFocus){var _focusTarget,_focusTarget3;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index 0c70e53a5b3..4663ffc2f6f 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n\n window.setTimeout(() => {\n this.focusPendingTarget(pendingFocus);\n }, 0);\n }\n\n /**\n * Find and focus the element requested by a stored focus instruction.\n *\n * @param {object} pendingFocus\n */\n focusPendingTarget(pendingFocus) {\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","window","setTimeout","focusPendingTarget","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index ebf72ddfd11..a6cb0c8d11c 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -510,6 +510,19 @@ export default class extends BaseComponent { if (this.getElements(this.selectors.VALIDATIONERRORS).length) { return; } + + // Defer to deal with Moodle 4.2 issue. + window.setTimeout(() => { + this.focusPendingTarget(pendingFocus); + }, 0); + } + + /** + * Find and focus the element requested by a stored focus instruction. + * + * @param {object} pendingFocus + */ + focusPendingTarget(pendingFocus) { let focusTarget = null; const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null; diff --git a/doc/content/metadata.png b/doc/content/metadata.png index 0609f6d0081be683b2447d8579213179290e6341..5c62503f5602245b088dba860cd626615c8ae06e 100644 GIT binary patch literal 59257 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 z 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 Wajdw3vN{U>yC)?tnR#3L<^KTcC~7$X literal 73197 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%0D2H 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 J(NhDz{{c>pUwQxl diff --git a/doc/en/Authoring/Metadata.md b/doc/en/Authoring/Metadata.md index da6a0228f23..0678b6fd17b 100644 --- a/doc/en/Authoring/Metadata.md +++ b/doc/en/Authoring/Metadata.md @@ -85,7 +85,7 @@ All information in the question `metadata` field can be updated via the button ' ![Metadata popup in action](../../content/metadata.png) -Update the input boxes and click 'Validate inputs and update JSON' to display the JSON output. Update the JSON and click 'Update inputs from JSON' to fill in the input boxes from the JSON. 'Validate and close' will take the contents of the input boxes, validate them and then create and store JSON ready to be saved as part of the STACK question. The question edit form must still be saved normally once the pop-up has closed in order to save this JSON to the question. +Switch the modal to edit, update the input boxes and click 'Validate inputs and update JSON' to display the JSON output. Update the JSON and click 'Update inputs from JSON' to fill in the input boxes from the JSON. 'Validate and close' will take the contents of the input boxes, validate them and then create and store JSON ready to be saved as part of the STACK question. The question edit form must still be saved normally once the pop-up has closed in order to save this JSON to the question. Please note that although you can update either the JSON or the input boxes, the form saves the contents of the input boxes. If you update the JSON you must click 'Update inputs from JSON' without a JSON error being displayed before clicking 'Validate and close'. From 49ff9ae4c5e1beae44a53662e17d01355ea04b7a Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 31 Jul 2026 09:59:46 +0100 Subject: [PATCH 097/100] iss1171 - Update map file --- amd/build/metadata/container.min.js.map | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index 4663ffc2f6f..b3c8c0b47fa 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n\n window.setTimeout(() => {\n this.focusPendingTarget(pendingFocus);\n }, 0);\n }\n\n /**\n * Find and focus the element requested by a stored focus instruction.\n *\n * @param {object} pendingFocus\n */\n focusPendingTarget(pendingFocus) {\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","window","setTimeout","focusPendingTarget","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n\n // Defer to deal with Moodle 4.2 issue.\n window.setTimeout(() => {\n this.focusPendingTarget(pendingFocus);\n }, 0);\n }\n\n /**\n * Find and focus the element requested by a stored focus instruction.\n *\n * @param {object} pendingFocus\n */\n focusPendingTarget(pendingFocus) {\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","window","setTimeout","focusPendingTarget","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file From ca5e492bca46718cb054bbb048239ae63e2ee334 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Fri, 31 Jul 2026 11:23:12 +0100 Subject: [PATCH 098/100] iss1171 - Fix JEST tests --- amd/build/metadata/container.min.js | 2 +- amd/build/metadata/container.min.js.map | 2 +- amd/src/metadata/container.js | 14 ++++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js index cb2c893b949..b722e8328b6 100644 --- a/amd/build/metadata/container.min.js +++ b/amd/build/metadata/container.min.js @@ -6,6 +6,6 @@ define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/ * @copyright 2025 University of Edinburgh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},await this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},await this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(async()=>{this.pendingFocus={selector:this.selectors.REVERT},await this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}async updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),await this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}async revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),await this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length||window.setTimeout((()=>{this.focusPendingTarget(pendingFocus)}),0)}focusPendingTarget(pendingFocus){var _focusTarget,_focusTarget3;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",EDITTOGGLE:"#stack-metadata-edit-switch",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKEAUTHOR:"#stack-metadata-make-author",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',FORM:"#qtype-stack-metadata-content",JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',VALIDATIONERRORS:"#qtype-stack-metadata-content .is-invalid",ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]',DETAILS:"#qtype-stack-metadata-content details[id]",ADDAUTHOR:'[id^="smdi_"][id$="_author_firstName"]',ADDLANGUAGE:'[id^="smdi_"][id$="_language_value"]',ADDPROPERTY:'[id^="smdi_"][id$="_additional_property"]',ADDSCOPE:'[id^="smdi_"][id$="_additional_scope"]',ADDBUTTONS:'[name="smd_add"], #stack-metadata-make-author',SCOPECARD:".smd-scope-card",DELETEADDITIONAL:'[id^="smd_property_"][id$="_add"]',DELETEAUTHOR:"#smd_author_0_add",DELETELANGUAGE:"#smd_language_0_add",DELETESCOPE:"#smd_scope_0_add"},this.isEditing=!1,this.detailsOpen={},this.pendingFocus=null,_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){var _this$detailsOpen$qty;let{state:state}=_ref;const details=this.getElements(this.selectors.DETAILS);for(const detail of details)this.detailsOpen[detail.id]=detail.open;const data={author:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}"),isEditing:this.isEditing,isJsonOpen:null!==(_this$detailsOpen$qty=this.detailsOpen["qtype-stack-metadata-json-section"])&&void 0!==_this$detailsOpen$qty&&_this$detailsOpen$qty};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.author.forEach((author=>{const element={firstname:this.createDataElement(!1,author.id,"author_firstName",author.firstName),lastname:this.createDataElement(!1,author.id,"author_lastName",author.lastName),institution:this.createDataElement(!1,author.id,"author_institution",author.institution),year:this.createDataElement(!1,author.id,"author_year",author.year),id:author.id};data.author.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){var _this$detailsOpen$sco;const scopeId="qtype-stack-metadata-scope-"+scopeHolder[scope][0].id,current={id:scopeId,name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope),isOpen:null===(_this$detailsOpen$sco=this.detailsOpen[scopeId])||void 0===_this$detailsOpen$sco||_this$detailsOpen$sco};data.scope.push(current)}data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data);const detailSections=this.getElements(this.selectors.DETAILS);for(const detailSection of detailSections)this.addEventListener(detailSection,"toggle",this.setDetailsState);this.addEventListener(document.querySelector(this.selectors.EDITTOGGLE),"change",this.toggleEditing),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEJSON},await this.update()}));const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",(async()=>{this.pendingFocus={selector:this.selectors.UPDATEINPUTS},await this.updateInputs()})),this.addEventListener(this.getElement(this.selectors.MAKEAUTHOR),"click",this.makeAuthor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",(async()=>{this.pendingFocus={selector:this.selectors.REVERT},await this.revert()})),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}this.restorePendingFocus()}async toggleEditing(event){if(event.currentTarget.checked)return this.isEditing=!0,void await this.reloadContainerComponent({state:this.reactive.state});if(!this.validateInputs())return void(event.currentTarget.checked=!0);this.isEditing=!1;await this.update(!1)||(this.isEditing=!0,event.currentTarget.checked=!0,await this.reloadContainerComponent({state:this.reactive.state}))}validateInputs(){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}return!isError}async update(){if((!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!this.validateInputs())return!1;let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.currentTarget.id.split("_");this.queueFocus("add",parts[1],event.currentTarget),await this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.currentTarget.id.split("_");this.queueFocus("delete",parts[1],event.currentTarget),await this.reactive.dispatch("deleteRow",parts[1],parts[2])}}async updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),await this.reactive.dispatch("updateFromJson",data)}async makeAuthor(){await this.update(!1)&&(this.queueFocus("add","author",this.getElement(this.selectors.MAKEAUTHOR)),await this.reactive.dispatch("addItem","author","user"))}async revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,void(_metadata.metadata.lib.brokenMetadata=e.message)}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),await this.reactive.dispatch("updateFromJson",previousdata)}setDetailsState(event){const detail=event.currentTarget;this.detailsOpen[detail.id]=detail.open}queueFocus(action,category,target){this.pendingFocus=null;const selectorName=(action+category).toUpperCase(),selector=this.selectors[selectorName];if(selector){switch(action){case"add":this.pendingFocus={selector:selector,selection:"last"};break;case"delete":this.pendingFocus={selector:selector,fallbackSelector:"DELETEADDITIONAL"===selectorName?this.selectors.DELETESCOPE:null,selection:"scope"}}if("ADDPROPERTY"===selectorName||"DELETEADDITIONAL"===selectorName){var _target$closest,_scopeCard$querySelec,_scopeCard$querySelec2,_scopeCard$querySelec3;const scopeCard=null==target||null===(_target$closest=target.closest)||void 0===_target$closest?void 0:_target$closest.call(target,this.selectors.SCOPECARD);this.pendingFocus.scopeName=null!==(_scopeCard$querySelec=null==scopeCard||null===(_scopeCard$querySelec2=scopeCard.querySelector)||void 0===_scopeCard$querySelec2||null===(_scopeCard$querySelec3=_scopeCard$querySelec2.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec3?void 0:_scopeCard$querySelec3.value)&&void 0!==_scopeCard$querySelec?_scopeCard$querySelec:null}}}restorePendingFocus(){if(!this.pendingFocus)return;const pendingFocus=this.pendingFocus;this.pendingFocus=null,this.getElements(this.selectors.VALIDATIONERRORS).length||("undefined"!=typeof window&&window.setTimeout?window.setTimeout((()=>{this.focusPendingTarget(pendingFocus)}),0):this.focusPendingTarget(pendingFocus))}focusPendingTarget(pendingFocus){var _focusTarget,_focusTarget3;let focusTarget=null;const scopeCard=pendingFocus.scopeName?this.getScopeCard(pendingFocus.scopeName):null;if("last"===pendingFocus.selection){var _scopeCard$querySelec4;const elements=Array.from((null==scopeCard||null===(_scopeCard$querySelec4=scopeCard.querySelectorAll)||void 0===_scopeCard$querySelec4?void 0:_scopeCard$querySelec4.call(scopeCard,pendingFocus.selector))||this.getElements(pendingFocus.selector));focusTarget=elements[elements.length-1]||null}else if("scope"===pendingFocus.selection){var _scopeCard$querySelec5;focusTarget=(null==scopeCard||null===(_scopeCard$querySelec5=scopeCard.querySelector)||void 0===_scopeCard$querySelec5?void 0:_scopeCard$querySelec5.call(scopeCard,pendingFocus.selector))||this.getElement(pendingFocus.fallbackSelector||pendingFocus.selector)}else focusTarget=this.getElements(pendingFocus.selector)[0];var _focusTarget$matches,_focusTarget2;if(null!==(_focusTarget=focusTarget)&&void 0!==_focusTarget&&_focusTarget.focus)if(null!==(_focusTarget$matches=(_focusTarget2=focusTarget).matches)&&void 0!==_focusTarget$matches&&_focusTarget$matches.call(_focusTarget2,this.selectors.ADDBUTTONS))try{focusTarget.focus({focusVisible:!0})}catch{focusTarget.focus()}else focusTarget.focus();null!==(_focusTarget3=focusTarget)&&void 0!==_focusTarget3&&_focusTarget3.select&&focusTarget.select()}getScopeCard(scopeName){var _Array$from$find;return null!==(_Array$from$find=Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard=>{var _scopeCard$querySelec6,_scopeCard$querySelec7;return(null===(_scopeCard$querySelec6=scopeCard.querySelector)||void 0===_scopeCard$querySelec6||null===(_scopeCard$querySelec7=_scopeCard$querySelec6.call(scopeCard,this.selectors.ADDSCOPE))||void 0===_scopeCard$querySelec7?void 0:_scopeCard$querySelec7.value)===scopeName})))&&void 0!==_Array$from$find?_Array$from$find:null}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map index b3c8c0b47fa..72dc129df29 100644 --- a/amd/build/metadata/container.min.js.map +++ b/amd/build/metadata/container.min.js.map @@ -1 +1 @@ -{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n\n // Defer to deal with Moodle 4.2 issue.\n window.setTimeout(() => {\n this.focusPendingTarget(pendingFocus);\n }, 0);\n }\n\n /**\n * Find and focus the element requested by a stored focus instruction.\n *\n * @param {object} pendingFocus\n */\n focusPendingTarget(pendingFocus) {\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","window","setTimeout","focusPendingTarget","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n EDITTOGGLE: `#stack-metadata-edit-switch`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKEAUTHOR: `#stack-metadata-make-author`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n FORM: '#qtype-stack-metadata-content',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n VALIDATIONERRORS: '#qtype-stack-metadata-content .is-invalid',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n DETAILS: '#qtype-stack-metadata-content details[id]',\n ADDAUTHOR: '[id^=\"smdi_\"][id$=\"_author_firstName\"]',\n ADDLANGUAGE: '[id^=\"smdi_\"][id$=\"_language_value\"]',\n ADDPROPERTY: '[id^=\"smdi_\"][id$=\"_additional_property\"]',\n ADDSCOPE: '[id^=\"smdi_\"][id$=\"_additional_scope\"]',\n ADDBUTTONS: '[name=\"smd_add\"], #stack-metadata-make-author',\n SCOPECARD: '.smd-scope-card',\n DELETEADDITIONAL: '[id^=\"smd_property_\"][id$=\"_add\"]',\n DELETEAUTHOR: '#smd_author_0_add',\n DELETELANGUAGE: '#smd_language_0_add',\n DELETESCOPE: '#smd_scope_0_add',\n };\n this.isEditing = false;\n this.detailsOpen = {};\n this.pendingFocus = null;\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n const details = this.getElements(this.selectors.DETAILS);\n for (const detail of details) {\n this.detailsOpen[detail.id] = detail.open;\n }\n\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n author: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n isEditing: this.isEditing,\n isJsonOpen: this.detailsOpen['qtype-stack-metadata-json-section'] ?? false,\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.author.forEach(author => {\n const element = {\n firstname: this.createDataElement(false, author.id, 'author_firstName', author.firstName),\n lastname: this.createDataElement(false, author.id, 'author_lastName', author.lastName),\n institution: this.createDataElement(false, author.id, 'author_institution', author.institution),\n year: this.createDataElement(false, author.id, 'author_year', author.year),\n id: author.id,\n };\n data.author.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const scopeId = 'qtype-stack-metadata-scope-' + scopeHolder[scope][0].id;\n const current = {\n id: scopeId,\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope),\n isOpen: this.detailsOpen[scopeId] ?? true,\n };\n data.scope.push(current);\n }\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n const detailSections = this.getElements(this.selectors.DETAILS);\n for (const detailSection of detailSections) {\n this.addEventListener(\n detailSection,\n 'toggle',\n this.setDetailsState\n );\n }\n this.addEventListener(\n document.querySelector(this.selectors.EDITTOGGLE),\n 'change',\n this.toggleEditing\n );\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEJSON};\n await this.update();\n }\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.UPDATEINPUTS};\n await this.updateInputs();\n }\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKEAUTHOR),\n 'click',\n this.makeAuthor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n async() => {\n this.pendingFocus = {selector: this.selectors.REVERT};\n await this.revert();\n }\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n\n this.restorePendingFocus();\n }\n\n /**\n * Toggle edit mode. Turning edit mode off validates and updates the state first.\n *\n * @param {Event} event\n */\n async toggleEditing(event) {\n if (event.currentTarget.checked) {\n this.isEditing = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n return;\n }\n if (!this.validateInputs()) {\n event.currentTarget.checked = true;\n return;\n }\n this.isEditing = false;\n const result = await this.update(false);\n if (!result) {\n this.isEditing = true;\n event.currentTarget.checked = true;\n await this.reloadContainerComponent({state: this.reactive.state});\n }\n }\n\n /**\n * Validate visible metadata inputs without dispatching state changes.\n *\n * @returns {bool} Returns false on validation error.\n */\n validateInputs() {\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n return !isError;\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding an author. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate && !this.validateInputs()) {\n return false;\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_author_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('add', parts[1], event.currentTarget);\n await this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.currentTarget.id.split('_');\n this.queueFocus('delete', parts[1], event.currentTarget);\n await this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n async updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n await this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as an author.\n */\n async makeAuthor() {\n const result = await this.update(false);\n if (result) {\n this.queueFocus('add', 'author', this.getElement(this.selectors.MAKEAUTHOR));\n await this.reactive.dispatch('addItem', 'author', 'user');\n }\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n async revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n await this.reactive.dispatch('updateFromJson', previousdata);\n }\n\n /**\n * Update stored state when a collapsible section is toggled.\n *\n * @param {Event} event\n */\n setDetailsState(event) {\n const detail = event.currentTarget;\n this.detailsOpen[detail.id] = detail.open;\n }\n\n /**\n * Remember where focus should move after adding or deleting an item.\n *\n * @param {string} action\n * @param {string} category\n * @param {HTMLElement} target\n */\n queueFocus(action, category, target) {\n this.pendingFocus = null;\n const selectorName = (action + category).toUpperCase();\n const selector = this.selectors[selectorName];\n if (!selector) {\n return;\n }\n\n switch (action) {\n case 'add':\n this.pendingFocus = {selector, selection: 'last'};\n break;\n case 'delete':\n this.pendingFocus = {selector, fallbackSelector: selectorName === 'DELETEADDITIONAL' ?\n this.selectors.DELETESCOPE : null, selection: 'scope'};\n break;\n }\n if (selectorName === 'ADDPROPERTY' || selectorName === 'DELETEADDITIONAL') {\n const scopeCard = target?.closest?.(this.selectors.SCOPECARD);\n this.pendingFocus.scopeName = scopeCard?.querySelector?.(this.selectors.ADDSCOPE)?.value ?? null;\n }\n }\n\n /**\n * Restore focus after reactive re-rendering.\n */\n restorePendingFocus() {\n if (!this.pendingFocus) {\n return;\n }\n\n const pendingFocus = this.pendingFocus;\n this.pendingFocus = null;\n if (this.getElements(this.selectors.VALIDATIONERRORS).length) {\n return;\n }\n\n // Defer in the browser to deal with Moodle 4.2 issue.\n if (typeof window !== 'undefined' && window.setTimeout) {\n window.setTimeout(() => {\n this.focusPendingTarget(pendingFocus);\n }, 0);\n return;\n }\n\n // JEST test ends up here.\n this.focusPendingTarget(pendingFocus);\n }\n\n /**\n * Find and focus the element requested by a stored focus instruction.\n *\n * @param {object} pendingFocus\n */\n focusPendingTarget(pendingFocus) {\n let focusTarget = null;\n\n const scopeCard = pendingFocus.scopeName ? this.getScopeCard(pendingFocus.scopeName) : null;\n if (pendingFocus.selection === 'last') {\n const elements = Array.from(\n scopeCard?.querySelectorAll?.(pendingFocus.selector) || this.getElements(pendingFocus.selector)\n );\n focusTarget = elements[elements.length - 1] || null;\n } else if (pendingFocus.selection === 'scope') {\n focusTarget = scopeCard?.querySelector?.(pendingFocus.selector) ||\n this.getElement(pendingFocus.fallbackSelector || pendingFocus.selector);\n } else {\n focusTarget = this.getElements(pendingFocus.selector)[0];\n }\n\n if (focusTarget?.focus) {\n if (!focusTarget.matches?.(this.selectors.ADDBUTTONS)) {\n focusTarget.focus();\n } else {\n try {\n focusTarget.focus({focusVisible: true});\n } catch {\n focusTarget.focus();\n }\n }\n }\n if (focusTarget?.select) {\n focusTarget.select();\n }\n }\n\n /**\n * Get a scope card from its scope name.\n *\n * @param {string} scopeName\n * @returns {HTMLElement|null}\n */\n getScopeCard(scopeName) {\n return Array.from(this.getElements(this.selectors.SCOPECARD)).find((scopeCard) => {\n return scopeCard.querySelector?.(this.selectors.ADDSCOPE)?.value === scopeName;\n }) ?? null;\n }\n}\n"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","EDITTOGGLE","ADDITEM","DELETEITEM","MAKEAUTHOR","REVERT","FORMJSON","FORM","JSONINPUT","REQUIREDINPUTS","VALIDATIONERRORS","ALLINPUTS","DETAILS","ADDAUTHOR","ADDLANGUAGE","ADDPROPERTY","ADDSCOPE","ADDBUTTONS","SCOPECARD","DELETEADDITIONAL","DELETEAUTHOR","DELETELANGUAGE","DELETESCOPE","isEditing","detailsOpen","pendingFocus","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","details","getElements","detail","open","data","author","language","license","isPartOf","scope","freeform","isJsonOpen","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","scopeId","current","firstProp","properties","input","isOpen","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","detailSections","detailSection","addEventListener","setDetailsState","toggleEditing","async","selector","update","addButtons","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeAuthor","revert","brokenMetadata","jsonElement","restorePendingFocus","event","currentTarget","checked","validateInputs","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","parts","queueFocus","jsonToState","previousdataJSON","previousdata","action","category","selectorName","toUpperCase","selection","fallbackSelector","scopeCard","closest","_target$closest","scopeName","_scopeCard$querySelec2","_scopeCard$querySelec3","length","window","setTimeout","focusPendingTarget","focusTarget","getScopeCard","elements","querySelectorAll","_focusTarget","focus","matches","_focusTarget$matches","focusVisible","_focusTarget3","select"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js index a6cb0c8d11c..4cb0aa7e6ef 100644 --- a/amd/src/metadata/container.js +++ b/amd/src/metadata/container.js @@ -511,10 +511,16 @@ export default class extends BaseComponent { return; } - // Defer to deal with Moodle 4.2 issue. - window.setTimeout(() => { - this.focusPendingTarget(pendingFocus); - }, 0); + // Defer in the browser to deal with Moodle 4.2 issue. + if (typeof window !== 'undefined' && window.setTimeout) { + window.setTimeout(() => { + this.focusPendingTarget(pendingFocus); + }, 0); + return; + } + + // JEST test ends up here. + this.focusPendingTarget(pendingFocus); } /** From 14329c8555530ba0946f01fbcdfce1a4f0bdf299 Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Mon, 3 Aug 2026 13:25:17 +0100 Subject: [PATCH 099/100] iss1171 - Fix author display if institution only. --- templates/metadata/metadataauthor.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/metadata/metadataauthor.mustache b/templates/metadata/metadataauthor.mustache index 1448bbd802f..e9b1fd379a4 100644 --- a/templates/metadata/metadataauthor.mustache +++ b/templates/metadata/metadataauthor.mustache @@ -25,7 +25,7 @@
{{! Read mode: compact one-line display + delete }}
- {{firstname.element.value}} {{lastname.element.value}}{{#institution.element.value}}, {{institution.element.value}}{{/institution.element.value}}{{#year.element.value}} ({{year.element.value}}){{/year.element.value}} + {{firstname.element.value}} {{lastname.element.value}}{{#institution.element.value}}{{#lastname.element.value}}, {{/lastname.element.value}}{{institution.element.value}}{{/institution.element.value}}{{#year.element.value}} ({{year.element.value}}){{/year.element.value}}
{{! Edit mode: four input columns }}
From 962d41154312eecb65615ab9474e8b7a47d51b9a Mon Sep 17 00:00:00 2001 From: Edmund Farrow Date: Thu, 6 Aug 2026 14:00:56 +0100 Subject: [PATCH 100/100] iss1171 - Moodle 4 toggle fix --- styles.css | 2 +- templates/metadata/metadatamodal.mustache | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/styles.css b/styles.css index 5834017665a..6893c1bbc9f 100644 --- a/styles.css +++ b/styles.css @@ -1372,7 +1372,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. --smd-radius: 4px; } -.stack-metadata :is(input, textarea) { +.stack-metadata :is(input:not([type="checkbox"]):not([type="radio"]), textarea) { width: 100%; } diff --git a/templates/metadata/metadatamodal.mustache b/templates/metadata/metadatamodal.mustache index c8607e37b2a..b791e9dea91 100644 --- a/templates/metadata/metadatamodal.mustache +++ b/templates/metadata/metadatamodal.mustache @@ -24,12 +24,12 @@ -
-