From 806808706086858ce32de311e35872df55620e21 Mon Sep 17 00:00:00 2001 From: Artem Okladov Date: Tue, 18 Aug 2026 13:34:24 +0300 Subject: [PATCH 1/4] fix: Remove Global Badge Support; Add Error Messages' Display --- README.md | 3 +- amd/build/bundle_warnings.min.js | 17 +++ amd/build/bundle_warnings.min.js.map | 1 + amd/build/constants.min.js | 3 + amd/build/constants.min.js.map | 1 + amd/build/environment_atto.min.js | 4 +- amd/build/environment_atto.min.js.map | 2 +- amd/build/environment_textarea.min.js | 9 +- amd/build/environment_textarea.min.js.map | 2 +- amd/build/environment_tinymce.min.js | 2 +- amd/build/environment_tinymce.min.js.map | 2 +- amd/build/init.min.js | 4 +- amd/build/init.min.js.map | 2 +- amd/build/notify.min.js | 24 +++ amd/build/notify.min.js.map | 1 + amd/build/proofreader_config.min.js | 2 +- amd/build/proofreader_config.min.js.map | 2 +- amd/src/bundle_warnings.js | 70 +++++++++ amd/src/constants.js | 44 ++++++ amd/src/environment_atto.js | 17 ++- amd/src/environment_textarea.js | 92 ++++++++++-- amd/src/environment_tinymce.js | 17 ++- amd/src/init.js | 5 + amd/src/notify.js | 173 ++++++++++++++++++++++ amd/src/proofreader_config.js | 8 +- classes/local/config_builder.php | 18 +-- lang/en/local_wproofreader.php | 10 +- settings.php | 11 -- styles.css | 29 ++++ 29 files changed, 507 insertions(+), 68 deletions(-) create mode 100644 amd/build/bundle_warnings.min.js create mode 100644 amd/build/bundle_warnings.min.js.map create mode 100644 amd/build/constants.min.js create mode 100644 amd/build/constants.min.js.map create mode 100644 amd/build/notify.min.js create mode 100644 amd/build/notify.min.js.map create mode 100644 amd/src/bundle_warnings.js create mode 100644 amd/src/constants.js create mode 100644 amd/src/notify.js create mode 100644 styles.css diff --git a/README.md b/README.md index ad5e344..f146c38 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,7 @@ All settings live under *Site administration > Plugins > Local plugins > WProofr |----------------------|-------------|-------------| | License key | empty | Paste the license key delivered with your Moodle Marketplace purchase. Leave empty to use the free version. | | Default language | Auto | Initial proofreading language. *Auto* lets the WebSpellChecker service detect the language from the content being checked; pick a specific language to pin it. The dropdown is refreshed live from the service when the settings page loads. | -| Show badge button | enabled | Toggles the orange WProofreader badge on or off. | -| Badge placement | Page corner | Where to render the badge. "Page corner" shows a single floating badge in the bottom-right corner of the page that controls all editors. "Per editor" attaches a separate badge to each editor. | +| Show badge button | enabled | Toggles the orange WProofreader badge on or off. A separate badge attaches to each editor on the page. | ### Proofreading features diff --git a/amd/build/bundle_warnings.min.js b/amd/build/bundle_warnings.min.js new file mode 100644 index 0000000..699737e --- /dev/null +++ b/amd/build/bundle_warnings.min.js @@ -0,0 +1,17 @@ +define("local_wproofreader/bundle_warnings",["exports","local_wproofreader/notify"],(function(_exports,_notify){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.install=void 0; +/** + * Surfaces the WProofreader bundle's own runtime console.warn calls as + * visible warnings. + * + * The bundle reports some runtime conditions (an unsupported configured + * language, the service being unreachable mid-session) only via + * console.warn, with no exception thrown and no subscribable event, so + * there is no other integration point to hook into. + * + * @module local_wproofreader/bundle_warnings + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +let installed=!1;const MATCHERS=[{pattern:/is unsupported or doesn't exist/,key:"languageUnsupported"},{pattern:/language code is not available for your Service ID/,key:"languageUnsupported"},{pattern:/language list is not defined/,key:"languageUnsupported"},{pattern:/WebSpellChecker Service is currently unavailable/,key:"serviceUnavailable"},{pattern:/CORS response parsing error/,key:"serviceUnavailable"}];_exports.install=config=>{if(installed||!window.console||"function"!=typeof window.console.warn)return;installed=!0;const messages={languageUnsupported:config.runtimeLanguageUnsupportedMessage,serviceUnavailable:config.runtimeServiceUnavailableMessage},originalWarn=window.console.warn.bind(window.console);window.console.warn=function(){originalWarn(...arguments);const text=String((arguments.length<=0?void 0:arguments[0])||""),matcher=MATCHERS.find((candidate=>candidate.pattern.test(text)));matcher&&messages[matcher.key]&&(0,_notify.notifyAllAttachedFields)(messages[matcher.key])}}})); + +//# sourceMappingURL=bundle_warnings.min.js.map \ No newline at end of file diff --git a/amd/build/bundle_warnings.min.js.map b/amd/build/bundle_warnings.min.js.map new file mode 100644 index 0000000..399e8db --- /dev/null +++ b/amd/build/bundle_warnings.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bundle_warnings.min.js","sources":["../src/bundle_warnings.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Surfaces the WProofreader bundle's own runtime console.warn calls as\n * visible warnings.\n *\n * The bundle reports some runtime conditions (an unsupported configured\n * language, the service being unreachable mid-session) only via\n * console.warn, with no exception thrown and no subscribable event, so\n * there is no other integration point to hook into.\n *\n * @module local_wproofreader/bundle_warnings\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyAllAttachedFields} from 'local_wproofreader/notify';\n\nlet installed = false;\n\nconst MATCHERS = [\n {pattern: /is unsupported or doesn't exist/, key: 'languageUnsupported'},\n {pattern: /language code is not available for your Service ID/, key: 'languageUnsupported'},\n {pattern: /language list is not defined/, key: 'languageUnsupported'},\n {pattern: /WebSpellChecker Service is currently unavailable/, key: 'serviceUnavailable'},\n {pattern: /CORS response parsing error/, key: 'serviceUnavailable'},\n];\n\n/**\n * Wrap window.console.warn once to catch the bundle's known runtime warnings.\n *\n * @param {Object} config Page configuration carrying the notification messages.\n */\nexport const install = (config) => {\n if (installed || !window.console || typeof window.console.warn !== 'function') {\n return;\n }\n installed = true;\n\n const messages = {\n languageUnsupported: config.runtimeLanguageUnsupportedMessage,\n serviceUnavailable: config.runtimeServiceUnavailableMessage,\n };\n const originalWarn = window.console.warn.bind(window.console);\n\n window.console.warn = (...args) => {\n originalWarn(...args);\n\n const text = String(args[0] || '');\n const matcher = MATCHERS.find((candidate) => candidate.pattern.test(text));\n if (!matcher || !messages[matcher.key]) {\n return;\n }\n\n notifyAllAttachedFields(messages[matcher.key]);\n };\n};\n"],"names":["installed","MATCHERS","pattern","key","config","window","console","warn","messages","languageUnsupported","runtimeLanguageUnsupportedMessage","serviceUnavailable","runtimeServiceUnavailableMessage","originalWarn","bind","text","String","matcher","find","candidate","test"],"mappings":";;;;;;;;;;;;;;IA+BIA,WAAY,QAEVC,SAAW,CACb,CAACC,QAAS,kCAAmCC,IAAK,uBAClD,CAACD,QAAS,qDAAsDC,IAAK,uBACrE,CAACD,QAAS,+BAAgCC,IAAK,uBAC/C,CAACD,QAAS,mDAAoDC,IAAK,sBACnE,CAACD,QAAS,8BAA+BC,IAAK,wCAQ1BC,YAChBJ,YAAcK,OAAOC,SAA0C,mBAAxBD,OAAOC,QAAQC,YAG1DP,WAAY,QAENQ,SAAW,CACbC,oBAAqBL,OAAOM,kCAC5BC,mBAAoBP,OAAOQ,kCAEzBC,aAAeR,OAAOC,QAAQC,KAAKO,KAAKT,OAAOC,SAErDD,OAAOC,QAAQC,KAAO,WAClBM,iCAEME,KAAOC,kDAAkB,IACzBC,QAAUhB,SAASiB,MAAMC,WAAcA,UAAUjB,QAAQkB,KAAKL,QAC/DE,SAAYT,SAASS,QAAQd,0CAIVK,SAASS,QAAQd"} \ No newline at end of file diff --git a/amd/build/constants.min.js b/amd/build/constants.min.js new file mode 100644 index 0000000..99f6885 --- /dev/null +++ b/amd/build/constants.min.js @@ -0,0 +1,3 @@ +define("local_wproofreader/constants",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.INSTANCE_ATTR_VALUE=_exports.INSTANCE_ATTR=_exports.ATTACHED_ATTR=void 0;_exports.INSTANCE_ATTR="data-wsc-instance";_exports.INSTANCE_ATTR_VALUE="true";_exports.ATTACHED_ATTR="data-wsc-attached"})); + +//# sourceMappingURL=constants.min.js.map \ No newline at end of file diff --git a/amd/build/constants.min.js.map b/amd/build/constants.min.js.map new file mode 100644 index 0000000..598cf82 --- /dev/null +++ b/amd/build/constants.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.min.js","sources":["../src/constants.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Shared constants for the WProofreader AMD modules.\n *\n * @module local_wproofreader/constants\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * The bundle's own `instance_attribute` option (confirmed in wscbundle.js's\n * OptionsManager defaults). It sets this to \"true\" on the container it\n * directly manages once an instance is attached. For a plain textarea\n * (autoSearch-attached, container === the textarea) that is a reliable\n * outer-page signal. For TinyMCE the container we pass is the outer\n * iframe, but the bundle tags something inside that iframe's own document\n * instead, so this never appears on the iframe element itself - Atto and\n * TinyMCE rely on ATTACHED_ATTR below instead.\n */\nexport const INSTANCE_ATTR = 'data-wsc-instance';\nexport const INSTANCE_ATTR_VALUE = 'true';\n\n/**\n * Our own marker, set immediately before calling WEBSPELLCHECKER.init() and\n * kept afterwards on success (only removed if that call throws). Used both\n * to stop the MutationObserver re-attempting an attach already in flight,\n * and - for Atto/TinyMCE specifically - as the outer-page-queryable \"this\n * instance is attached\" signal that INSTANCE_ATTR cannot reliably provide.\n */\nexport const ATTACHED_ATTR = 'data-wsc-attached';\n"],"names":[],"mappings":"uOAiC6B,iDACM,8BASN"} \ No newline at end of file diff --git a/amd/build/environment_atto.min.js b/amd/build/environment_atto.min.js index ee5ad9a..a5b53aa 100644 --- a/amd/build/environment_atto.min.js +++ b/amd/build/environment_atto.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/environment_atto",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +define("local_wproofreader/environment_atto",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Atto editor integration. * @@ -6,6 +6,6 @@ define("local_wproofreader/environment_atto",["exports"],(function(_exports){Obj * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const SELECTORS=['.editor_atto_content[contenteditable="true"]','.editor_atto [contenteditable="true"]'];let observer=null,hookInstalled=!1;const createInstance=element=>{if(element&&element.isContentEditable&&!(element=>element.hasAttribute("data-wsc-instance"))(element)&&window.WEBSPELLCHECKER){element.setAttribute("data-wsc-instance","1");try{window.WEBSPELLCHECKER.init({container:element})}catch(e){element.removeAttribute("data-wsc-instance"),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to Atto editor",e)}}},scanAndInit=()=>{(()=>{const seen=new Set;return SELECTORS.forEach((selector=>{document.querySelectorAll(selector).forEach((element=>seen.add(element)))})),Array.from(seen)})().forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=()=>{hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["contenteditable"]})),scanAndInit()}})); +const SELECTORS=['.editor_atto_content[contenteditable="true"]','.editor_atto [contenteditable="true"]'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const createInstance=element=>{if(element&&element.isContentEditable&&!(element=>element.hasAttribute(_constants.ATTACHED_ATTR))(element)&&window.WEBSPELLCHECKER){element.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:element})}catch(e){element.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to Atto editor",e),(0,_notify.notifyField)(element,attachErrorMessage)}}},scanAndInit=()=>{(()=>{const seen=new Set;return SELECTORS.forEach((selector=>{document.querySelectorAll(selector).forEach((element=>seen.add(element)))})),Array.from(seen)})().forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["contenteditable"]})),scanAndInit()}})); //# sourceMappingURL=environment_atto.min.js.map \ No newline at end of file diff --git a/amd/build/environment_atto.min.js.map b/amd/build/environment_atto.min.js.map index 5172adb..e8f14ef 100644 --- a/amd/build/environment_atto.min.js.map +++ b/amd/build/environment_atto.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_atto.min.js","sources":["../src/environment_atto.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Atto editor integration.\n *\n * @module local_wproofreader/environment_atto\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = [\n '.editor_atto_content[contenteditable=\"true\"]',\n '.editor_atto [contenteditable=\"true\"]',\n];\nconst INSTANCE_ATTR = 'data-wsc-instance';\n\nlet observer = null;\nlet hookInstalled = false;\n\nconst findEditors = () => {\n const seen = new Set();\n SELECTORS.forEach((selector) => {\n document.querySelectorAll(selector).forEach((element) => seen.add(element));\n });\n return Array.from(seen);\n};\n\nconst isInstanceCreated = (element) => element.hasAttribute(INSTANCE_ATTR);\n\nconst createInstance = (element) => {\n if (!element || !element.isContentEditable || isInstanceCreated(element) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n element.setAttribute(INSTANCE_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: element});\n } catch (e) {\n element.removeAttribute(INSTANCE_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to Atto editor', e);\n }\n }\n};\n\nconst scanAndInit = () => {\n findEditors().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['contenteditable'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the Atto editor environment.\n */\nexport const init = () => {\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["SELECTORS","observer","hookInstalled","createInstance","element","isContentEditable","hasAttribute","isInstanceCreated","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","seen","Set","forEach","selector","document","querySelectorAll","add","Array","from","findEditors","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter"],"mappings":";;;;;;;;MAuBMA,UAAY,CACd,+CACA,6CAIAC,SAAW,KACXC,eAAgB,QAYdC,eAAkBC,aACfA,SAAYA,QAAQC,oBAHFD,CAAAA,SAAYA,QAAQE,aAbzB,qBAgB4BC,CAAkBH,UAAaI,OAAOC,iBAIpFL,QAAQM,aApBU,oBAoBkB,SAGhCF,OAAOC,gBAAgBE,KAAK,CAACC,UAAWR,UAC1C,MAAOS,GACLT,QAAQU,gBAzBM,qBA0BVN,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,gDAAiDH,MAK3EI,YAAc,KA3BA,YACVC,KAAO,IAAIC,WACjBnB,UAAUoB,SAASC,WACfC,SAASC,iBAAiBF,UAAUD,SAAShB,SAAYc,KAAKM,IAAIpB,cAE/DqB,MAAMC,KAAKR,OAuBlBS,GAAcP,QAAQjB,iBAoBpByB,gBAAkB,QAChB1B,qBAGJA,eAAgB,QAEV2B,SAAWrB,OAAOsB,6BACxBtB,OAAOsB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOpB,IAIbI,8BAOY,KAChBW,kBAvCI3B,UAAwC,oBAArBiC,mBAIvBjC,SAAW,IAAIiC,kBAAiB,KAC5BjB,iBAGJhB,SAASkC,QAAQb,SAASc,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,sBA6BtBvB"} \ No newline at end of file +{"version":3,"file":"environment_atto.min.js","sources":["../src/environment_atto.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Atto editor integration.\n *\n * @module local_wproofreader/environment_atto\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTORS = [\n '.editor_atto_content[contenteditable=\"true\"]',\n '.editor_atto [contenteditable=\"true\"]',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findEditors = () => {\n const seen = new Set();\n SELECTORS.forEach((selector) => {\n document.querySelectorAll(selector).forEach((element) => seen.add(element));\n });\n return Array.from(seen);\n};\n\nconst isInstanceCreated = (element) => element.hasAttribute(ATTACHED_ATTR);\n\nconst createInstance = (element) => {\n if (!element || !element.isContentEditable || isInstanceCreated(element) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n element.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: element});\n } catch (e) {\n element.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to Atto editor', e);\n }\n notifyField(element, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findEditors().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['contenteditable'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the Atto editor environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["SELECTORS","observer","hookInstalled","attachErrorMessage","createInstance","element","isContentEditable","hasAttribute","ATTACHED_ATTR","isInstanceCreated","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","seen","Set","forEach","selector","document","querySelectorAll","add","Array","from","findEditors","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter"],"mappings":";;;;;;;;MA0BMA,UAAY,CACd,+CACA,6CAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAYnBC,eAAkBC,aACfA,SAAYA,QAAQC,oBAHFD,CAAAA,SAAYA,QAAQE,aAAaC,0BAGVC,CAAkBJ,UAAaK,OAAOC,iBAIpFN,QAAQO,aAAaJ,yBAAe,SAGhCE,OAAOC,gBAAgBE,KAAK,CAACC,UAAWT,UAC1C,MAAOU,GACLV,QAAQW,gBAAgBR,0BACpBE,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,gDAAiDH,2BAE7DV,QAASF,uBAIvBgB,YAAc,KA5BA,YACVC,KAAO,IAAIC,WACjBrB,UAAUsB,SAASC,WACfC,SAASC,iBAAiBF,UAAUD,SAASjB,SAAYe,KAAKM,IAAIrB,cAE/DsB,MAAMC,KAAKR,OAwBlBS,GAAcP,QAAQlB,iBAoBpB0B,gBAAkB,QAChB5B,qBAGJA,eAAgB,QAEV6B,SAAWrB,OAAOsB,6BACxBtB,OAAOsB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOpB,IAIbI,8BASaiB,SACjBjC,mBAAqBiC,QAAUA,OAAOC,0BAA4B,KAClEP,kBA1CI7B,UAAwC,oBAArBqC,mBAIvBrC,SAAW,IAAIqC,kBAAiB,KAC5BnB,iBAGJlB,SAASsC,QAAQf,SAASgB,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,sBAgCtBzB"} \ No newline at end of file diff --git a/amd/build/environment_textarea.min.js b/amd/build/environment_textarea.min.js index 0948c73..37d2594 100644 --- a/amd/build/environment_textarea.min.js +++ b/amd/build/environment_textarea.min.js @@ -1,11 +1,16 @@ -define("local_wproofreader/environment_textarea",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +define("local_wproofreader/environment_textarea",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Plain HTML textarea integration. * + * Attaches explicitly instead of relying on the bundle's own autoSearch + * (disabled for textareas in proofreader_config.js), since autoSearch only + * scans once at load and misses fields revealed later, such as an mform + * "Show more" advanced section. + * * @module local_wproofreader/environment_textarea * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const CODE_FIELD_HINTS=['id$="customcss"','id$="custommenuitems"','name="customcss"','name="s_theme_boost_customcss"','name$="[customcss]"'];_exports.init=config=>{if((()=>{const selectors=CODE_FIELD_HINTS.map((hint=>"textarea[".concat(hint,"]"))).join(",");document.querySelectorAll(selectors).forEach((textarea=>{textarea.classList.add("wsc-skip-autosearch")}))})(),window.WEBSPELLCHECKER_CONFIG&&Array.isArray(window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn)){const skipSelector=".".concat("wsc-skip-autosearch");window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.includes(skipSelector)||window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.push(skipSelector)}}})); +const CODE_FIELD_HINTS=['id$="customcss"','id$="custommenuitems"','name="customcss"','name="s_theme_boost_customcss"','name$="[customcss]"'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const createInstance=textarea=>{if(textarea&&!textarea.hasAttribute(_constants.ATTACHED_ATTR)&&((element=textarea).offsetWidth||element.offsetHeight||element.getClientRects().length)&&window.WEBSPELLCHECKER){var element;textarea.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:textarea})}catch(e){textarea.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to textarea",e),(0,_notify.notifyField)(textarea,attachErrorMessage)}}},scanAndInit=()=>{Array.from(document.querySelectorAll("textarea")).filter((textarea=>!textarea.classList.contains("wsc-skip-autosearch"))).forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,(()=>{const selectors=CODE_FIELD_HINTS.map((hint=>"textarea[".concat(hint,"]"))).join(",");document.querySelectorAll(selectors).forEach((textarea=>{textarea.classList.add("wsc-skip-autosearch")}))})(),hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["class"]})),scanAndInit()}})); //# sourceMappingURL=environment_textarea.min.js.map \ No newline at end of file diff --git a/amd/build/environment_textarea.min.js.map b/amd/build/environment_textarea.min.js.map index 9626913..3276a30 100644 --- a/amd/build/environment_textarea.min.js.map +++ b/amd/build/environment_textarea.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_textarea.min.js","sources":["../src/environment_textarea.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Plain HTML textarea integration.\n *\n * @module local_wproofreader/environment_textarea\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SKIP_CLASS = 'wsc-skip-autosearch';\nconst CODE_FIELD_HINTS = [\n 'id$=\"customcss\"',\n 'id$=\"custommenuitems\"',\n 'name=\"customcss\"',\n 'name=\"s_theme_boost_customcss\"',\n 'name$=\"[customcss]\"',\n];\n\nconst tagSkippableTextareas = () => {\n const selectors = CODE_FIELD_HINTS.map((hint) => `textarea[${hint}]`).join(',');\n\n document.querySelectorAll(selectors).forEach((textarea) => {\n textarea.classList.add(SKIP_CLASS);\n });\n};\n\n/**\n * Initialize the textarea environment.\n *\n * @param {Object} config Page configuration (mutated to extend disableAutoSearchIn).\n */\nexport const init = (config) => {\n tagSkippableTextareas();\n\n if (window.WEBSPELLCHECKER_CONFIG && Array.isArray(window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn)) {\n const skipSelector = `.${SKIP_CLASS}`;\n if (!window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.includes(skipSelector)) {\n window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.push(skipSelector);\n }\n }\n\n // Keep the parameter signature consistent for the init dispatcher even though\n // we do not currently use the rest of the config here.\n void config;\n};\n"],"names":["CODE_FIELD_HINTS","config","selectors","map","hint","join","document","querySelectorAll","forEach","textarea","classList","add","tagSkippableTextareas","window","WEBSPELLCHECKER_CONFIG","Array","isArray","disableAutoSearchIn","skipSelector","includes","push"],"mappings":";;;;;;;;MAwBMA,iBAAmB,CACrB,kBACA,wBACA,mBACA,iCACA,qCAgBiBC,YAbS,YACpBC,UAAYF,iBAAiBG,KAAKC,yBAAqBA,YAASC,KAAK,KAE3EC,SAASC,iBAAiBL,WAAWM,SAASC,WAC1CA,SAASC,UAAUC,IAbR,2BAuBfC,GAEIC,OAAOC,wBAA0BC,MAAMC,QAAQH,OAAOC,uBAAuBG,qBAAsB,OAC7FC,wBA1BK,uBA2BNL,OAAOC,uBAAuBG,oBAAoBE,SAASD,eAC5DL,OAAOC,uBAAuBG,oBAAoBG,KAAKF"} \ No newline at end of file +{"version":3,"file":"environment_textarea.min.js","sources":["../src/environment_textarea.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Plain HTML textarea integration.\n *\n * Attaches explicitly instead of relying on the bundle's own autoSearch\n * (disabled for textareas in proofreader_config.js), since autoSearch only\n * scans once at load and misses fields revealed later, such as an mform\n * \"Show more\" advanced section.\n *\n * @module local_wproofreader/environment_textarea\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SKIP_CLASS = 'wsc-skip-autosearch';\nconst CODE_FIELD_HINTS = [\n 'id$=\"customcss\"',\n 'id$=\"custommenuitems\"',\n 'name=\"customcss\"',\n 'name=\"s_theme_boost_customcss\"',\n 'name$=\"[customcss]\"',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst tagSkippableTextareas = () => {\n const selectors = CODE_FIELD_HINTS.map((hint) => `textarea[${hint}]`).join(',');\n\n document.querySelectorAll(selectors).forEach((textarea) => {\n textarea.classList.add(SKIP_CLASS);\n });\n};\n\nconst isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);\n\nconst findTextareas = () => Array.from(document.querySelectorAll('textarea'))\n .filter((textarea) => !textarea.classList.contains(SKIP_CLASS));\n\nconst createInstance = (textarea) => {\n if (!textarea || textarea.hasAttribute(ATTACHED_ATTR) || !isVisible(textarea) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n textarea.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: textarea});\n } catch (e) {\n textarea.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to textarea', e);\n }\n notifyField(textarea, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findTextareas().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['class'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the textarea environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n tagSkippableTextareas();\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["CODE_FIELD_HINTS","observer","hookInstalled","attachErrorMessage","createInstance","textarea","hasAttribute","ATTACHED_ATTR","element","offsetWidth","offsetHeight","getClientRects","length","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","Array","from","document","querySelectorAll","filter","classList","contains","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","selectors","map","hint","join","add","tagSkippableTextareas","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter"],"mappings":";;;;;;;;;;;;;MAgCMA,iBAAmB,CACrB,kBACA,wBACA,mBACA,iCACA,2BAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAenBC,eAAkBC,cACfA,WAAYA,SAASC,aAAaC,6BANxBC,QAMqDH,UAN9BI,aAAeD,QAAQE,cAAgBF,QAAQG,iBAAiBC,SAMpBC,OAAOC,iBAN1EN,IAAAA,QAUfH,SAASU,aAAaR,yBAAe,SAGjCM,OAAOC,gBAAgBE,KAAK,CAACC,UAAWZ,WAC1C,MAAOa,GACLb,SAASc,gBAAgBZ,0BACrBM,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,6CAA8CH,2BAE1Db,SAAUF,uBAIxBmB,YAAc,KArBQC,MAAMC,KAAKC,SAASC,iBAAiB,aAC5DC,QAAQtB,WAAcA,SAASuB,UAAUC,SAxB3B,yBA6CCC,QAAQ1B,iBAoBtB2B,gBAAkB,QAChB7B,qBAGJA,eAAgB,QAEV8B,SAAWnB,OAAOoB,6BACxBpB,OAAOoB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOlB,IAIbI,8BASae,SACjBlC,mBAAqBkC,QAAUA,OAAOC,0BAA4B,KA7ExC,YACpBC,UAAYvC,iBAAiBwC,KAAKC,yBAAqBA,YAASC,KAAK,KAE3EjB,SAASC,iBAAiBa,WAAWT,SAASzB,WAC1CA,SAASuB,UAAUe,IAjBR,2BA2FfC,GACAb,kBA3CI9B,UAAwC,oBAArB4C,mBAIvB5C,SAAW,IAAI4C,kBAAiB,KAC5BvB,iBAGJrB,SAAS6C,QAAQrB,SAASsB,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,YAiCtB7B"} \ No newline at end of file diff --git a/amd/build/environment_tinymce.min.js b/amd/build/environment_tinymce.min.js index 787f11d..1d13893 100644 --- a/amd/build/environment_tinymce.min.js +++ b/amd/build/environment_tinymce.min.js @@ -1,3 +1,3 @@ -define("local_wproofreader/environment_tinymce",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;let observer=null,hookInstalled=!1;const unmark=iframe=>iframe.removeAttribute("data-wsc-instance"),initInstance=iframe=>{const doc=iframe.contentDocument;if(doc&&doc.body)try{window.WEBSPELLCHECKER.init(Object.assign({},window.WEBSPELLCHECKER_CONFIG,{container:iframe}))}catch(e){unmark(iframe),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to TinyMCE editor",e)}else unmark(iframe)},attach=iframe=>{if(!iframe||(iframe=>iframe.hasAttribute("data-wsc-instance"))(iframe)||!window.WEBSPELLCHECKER||!window.WEBSPELLCHECKER_CONFIG)return;(iframe=>{iframe.setAttribute("data-wsc-instance","1")})(iframe);const editor=(iframe=>window.tinymce&&"function"==typeof window.tinymce.get&&(window.tinymce.get()||[]).find((editor=>editor&&editor.iframeElement===iframe))||null)(iframe);!editor||editor.initialized||"function"!=typeof editor.on?initInstance(iframe):editor.on("init",(()=>initInstance(iframe)))},scanAndInit=()=>{Array.from(document.querySelectorAll("iframe.tox-edit-area__iframe")).forEach(attach)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=()=>{hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0})),scanAndInit()}})); +define("local_wproofreader/environment_tinymce",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;let observer=null,hookInstalled=!1,attachErrorMessage=null;const unmark=iframe=>iframe.removeAttribute(_constants.ATTACHED_ATTR),initInstance=iframe=>{const doc=iframe.contentDocument;if(doc&&doc.body)try{window.WEBSPELLCHECKER.init(Object.assign({},window.WEBSPELLCHECKER_CONFIG,{container:iframe}))}catch(e){unmark(iframe),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to TinyMCE editor",e),(0,_notify.notifyField)(iframe,attachErrorMessage)}else unmark(iframe)},attach=iframe=>{if(!iframe||(iframe=>iframe.hasAttribute(_constants.ATTACHED_ATTR))(iframe)||!window.WEBSPELLCHECKER||!window.WEBSPELLCHECKER_CONFIG)return;(iframe=>{iframe.setAttribute(_constants.ATTACHED_ATTR,"1")})(iframe);const editor=(iframe=>window.tinymce&&"function"==typeof window.tinymce.get&&(window.tinymce.get()||[]).find((editor=>editor&&editor.iframeElement===iframe))||null)(iframe);!editor||editor.initialized||"function"!=typeof editor.on?initInstance(iframe):editor.on("init",(()=>initInstance(iframe)))},scanAndInit=()=>{Array.from(document.querySelectorAll("iframe.tox-edit-area__iframe")).forEach(attach)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0})),scanAndInit()}})); //# sourceMappingURL=environment_tinymce.min.js.map \ No newline at end of file diff --git a/amd/build/environment_tinymce.min.js.map b/amd/build/environment_tinymce.min.js.map index b936e84..edf8260 100644 --- a/amd/build/environment_tinymce.min.js.map +++ b/amd/build/environment_tinymce.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_tinymce.min.js","sources":["../src/environment_tinymce.js"],"sourcesContent":["// This file is part of Moodle - https://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 * TinyMCE 6 editor integration.\n *\n * @module local_wproofreader/environment_tinymce\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTOR = 'iframe.tox-edit-area__iframe';\nconst INSTANCE_ATTR = 'data-wsc-instance';\n\nlet observer = null;\nlet hookInstalled = false;\n\nconst findIframes = () => Array.from(document.querySelectorAll(SELECTOR));\n\nconst isMarked = (iframe) => iframe.hasAttribute(INSTANCE_ATTR);\nconst mark = (iframe) => iframe.setAttribute(INSTANCE_ATTR, '1');\nconst unmark = (iframe) => iframe.removeAttribute(INSTANCE_ATTR);\n\n\nconst findEditor = (iframe) => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return null;\n }\n const editors = window.tinymce.get() || [];\n return editors.find((editor) => editor && editor.iframeElement === iframe) || null;\n};\n\nconst initInstance = (iframe) => {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n unmark(iframe);\n return;\n }\n\n try {\n window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, {\n container: iframe,\n }));\n } catch (e) {\n unmark(iframe);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to TinyMCE editor', e);\n }\n }\n};\n\n/**\n * Decide when to attach to a single iframe: now, or on the editor's init event.\n *\n * @param {HTMLIFrameElement} iframe\n */\nconst attach = (iframe) => {\n if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {\n return;\n }\n\n // Mark up front so repeated observer fires do not register duplicate hooks;\n // initInstance unmarks again if the editor turns out not to be ready.\n mark(iframe);\n\n const editor = findEditor(iframe);\n if (editor && !editor.initialized && typeof editor.on === 'function') {\n editor.on('init', () => initInstance(iframe));\n return;\n }\n\n initInstance(iframe);\n};\n\nconst scanAndInit = () => {\n findIframes().forEach(attach);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the TinyMCE environment.\n */\nexport const init = () => {\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["observer","hookInstalled","unmark","iframe","removeAttribute","initInstance","doc","contentDocument","body","window","WEBSPELLCHECKER","init","Object","assign","WEBSPELLCHECKER_CONFIG","container","e","console","warn","attach","hasAttribute","isMarked","setAttribute","mark","editor","tinymce","get","find","iframeElement","findEditor","initialized","on","scanAndInit","Array","from","document","querySelectorAll","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","MutationObserver","observe","childList","subtree"],"mappings":"iKA0BIA,SAAW,KACXC,eAAgB,QAMdC,OAAUC,QAAWA,OAAOC,gBATZ,qBAoBhBC,aAAgBF,eACZG,IAAMH,OAAOI,mBACdD,KAAQA,IAAIE,SAMbC,OAAOC,gBAAgBC,KAAKC,OAAOC,OAAO,GAAIJ,OAAOK,uBAAwB,CACzEC,UAAWZ,UAEjB,MAAOa,GACLd,OAAOC,QACHM,OAAOQ,SAAWR,OAAOQ,QAAQC,MACjCT,OAAOQ,QAAQC,KAAK,mDAAoDF,QAX5Ed,OAAOC,SAqBTgB,OAAUhB,aACPA,QAtCSA,CAAAA,QAAWA,OAAOiB,aAPd,qBA6CHC,CAASlB,UAAYM,OAAOC,kBAAoBD,OAAOK,8BArC5DX,CAAAA,SAAWA,OAAOmB,aARV,oBAQsC,MA2CxDC,CAAKpB,cAECqB,OAzCUrB,CAAAA,QACXM,OAAOgB,SAAyC,mBAAvBhB,OAAOgB,QAAQC,MAG7BjB,OAAOgB,QAAQC,OAAS,IACzBC,MAAMH,QAAWA,QAAUA,OAAOI,gBAAkBzB,UAHxD,KAuCI0B,CAAW1B,SACtBqB,QAAWA,OAAOM,aAAoC,mBAAdN,OAAOO,GAKnD1B,aAAaF,QAJTqB,OAAOO,GAAG,QAAQ,IAAM1B,aAAaF,WAOvC6B,YAAc,KAzDMC,MAAMC,KAAKC,SAASC,iBAN7B,iCAgECC,QAAQlB,SAkBpBmB,gBAAkB,QAChBrC,qBAGJA,eAAgB,QAEVsC,SAAW9B,OAAO+B,6BACxB/B,OAAO+B,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAO3B,IAIbgB,8BAOY,KAChBM,kBArCItC,UAAwC,oBAArB4C,mBAIvB5C,SAAW,IAAI4C,kBAAiB,KAC5BZ,iBAGJhC,SAAS6C,QAAQV,SAAS3B,KAAM,CAC5BsC,WAAW,EACXC,SAAS,KA6Bbf"} \ No newline at end of file +{"version":3,"file":"environment_tinymce.min.js","sources":["../src/environment_tinymce.js"],"sourcesContent":["// This file is part of Moodle - https://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 * TinyMCE 6 editor integration.\n *\n * @module local_wproofreader/environment_tinymce\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTOR = 'iframe.tox-edit-area__iframe';\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findIframes = () => Array.from(document.querySelectorAll(SELECTOR));\n\nconst isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR);\nconst mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1');\nconst unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR);\n\n\nconst findEditor = (iframe) => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return null;\n }\n const editors = window.tinymce.get() || [];\n return editors.find((editor) => editor && editor.iframeElement === iframe) || null;\n};\n\nconst initInstance = (iframe) => {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n unmark(iframe);\n return;\n }\n\n try {\n window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, {\n container: iframe,\n }));\n } catch (e) {\n unmark(iframe);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to TinyMCE editor', e);\n }\n notifyField(iframe, attachErrorMessage);\n }\n};\n\n/**\n * Decide when to attach to a single iframe: now, or on the editor's init event.\n *\n * @param {HTMLIFrameElement} iframe\n */\nconst attach = (iframe) => {\n if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {\n return;\n }\n\n // Mark up front so repeated observer fires do not register duplicate hooks;\n // initInstance unmarks again if the editor turns out not to be ready.\n mark(iframe);\n\n const editor = findEditor(iframe);\n if (editor && !editor.initialized && typeof editor.on === 'function') {\n editor.on('init', () => initInstance(iframe));\n return;\n }\n\n initInstance(iframe);\n};\n\nconst scanAndInit = () => {\n findIframes().forEach(attach);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the TinyMCE environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["observer","hookInstalled","attachErrorMessage","unmark","iframe","removeAttribute","ATTACHED_ATTR","initInstance","doc","contentDocument","body","window","WEBSPELLCHECKER","init","Object","assign","WEBSPELLCHECKER_CONFIG","container","e","console","warn","attach","hasAttribute","isMarked","setAttribute","mark","editor","tinymce","get","find","iframeElement","findEditor","initialized","on","scanAndInit","Array","from","document","querySelectorAll","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","childList","subtree"],"mappings":"+OA4BIA,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAMnBC,OAAUC,QAAWA,OAAOC,gBAAgBC,0BAW5CC,aAAgBH,eACZI,IAAMJ,OAAOK,mBACdD,KAAQA,IAAIE,SAMbC,OAAOC,gBAAgBC,KAAKC,OAAOC,OAAO,GAAIJ,OAAOK,uBAAwB,CACzEC,UAAWb,UAEjB,MAAOc,GACLf,OAAOC,QACHO,OAAOQ,SAAWR,OAAOQ,QAAQC,MACjCT,OAAOQ,QAAQC,KAAK,mDAAoDF,2BAEhEd,OAAQF,yBAbpBC,OAAOC,SAsBTiB,OAAUjB,aACPA,QAvCSA,CAAAA,QAAWA,OAAOkB,aAAahB,0BAuC9BiB,CAASnB,UAAYO,OAAOC,kBAAoBD,OAAOK,8BAtC5DZ,CAAAA,SAAWA,OAAOoB,aAAalB,yBAAe,MA4CxDmB,CAAKrB,cAECsB,OA1CUtB,CAAAA,QACXO,OAAOgB,SAAyC,mBAAvBhB,OAAOgB,QAAQC,MAG7BjB,OAAOgB,QAAQC,OAAS,IACzBC,MAAMH,QAAWA,QAAUA,OAAOI,gBAAkB1B,UAHxD,KAwCI2B,CAAW3B,SACtBsB,QAAWA,OAAOM,aAAoC,mBAAdN,OAAOO,GAKnD1B,aAAaH,QAJTsB,OAAOO,GAAG,QAAQ,IAAM1B,aAAaH,WAOvC8B,YAAc,KA1DMC,MAAMC,KAAKC,SAASC,iBAN7B,iCAiECC,QAAQlB,SAkBpBmB,gBAAkB,QAChBvC,qBAGJA,eAAgB,QAEVwC,SAAW9B,OAAO+B,6BACxB/B,OAAO+B,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAO3B,IAIbgB,8BASaY,SACjB5C,mBAAqB4C,QAAUA,OAAOC,0BAA4B,KAClEP,kBAxCIxC,UAAwC,oBAArBgD,mBAIvBhD,SAAW,IAAIgD,kBAAiB,KAC5Bd,iBAGJlC,SAASiD,QAAQZ,SAAS3B,KAAM,CAC5BwC,WAAW,EACXC,SAAS,KAgCbjB"} \ No newline at end of file diff --git a/amd/build/init.min.js b/amd/build/init.min.js index 1832773..d437eec 100644 --- a/amd/build/init.min.js +++ b/amd/build/init.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/init",["exports","local_wproofreader/proofreader_config","local_wproofreader/environment_atto","local_wproofreader/environment_tinymce","local_wproofreader/environment_textarea","local_wproofreader/bundle_loader"],(function(_exports,Config,AttoEnv,TinyMceEnv,TextareaEnv,_bundle_loader){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +define("local_wproofreader/init",["exports","local_wproofreader/notify","local_wproofreader/proofreader_config","local_wproofreader/environment_atto","local_wproofreader/environment_tinymce","local_wproofreader/environment_textarea","local_wproofreader/bundle_loader","local_wproofreader/bundle_warnings"],(function(_exports,_notify,Config,AttoEnv,TinyMceEnv,TextareaEnv,_bundle_loader,_bundle_warnings){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * WProofreader main entry point. * @@ -8,6 +8,6 @@ define("local_wproofreader/init",["exports","local_wproofreader/proofreader_conf * @module local_wproofreader/init * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Config=_interopRequireWildcard(Config),AttoEnv=_interopRequireWildcard(AttoEnv),TinyMceEnv=_interopRequireWildcard(TinyMceEnv),TextareaEnv=_interopRequireWildcard(TextareaEnv);_exports.init=async()=>{const config=window.WPROOFREADER_BOOTSTRAP;config&&"object"==typeof config&&(Config.apply(config),await(0,_bundle_loader.loadBundle)(config.bundleUrl),window.WEBSPELLCHECKER&&(TextareaEnv.init(config),AttoEnv.init(config),TinyMceEnv.init(config)))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Config=_interopRequireWildcard(Config),AttoEnv=_interopRequireWildcard(AttoEnv),TinyMceEnv=_interopRequireWildcard(TinyMceEnv),TextareaEnv=_interopRequireWildcard(TextareaEnv);_exports.init=async()=>{const config=window.WPROOFREADER_BOOTSTRAP;config&&"object"==typeof config&&(Config.apply(config),await(0,_bundle_loader.loadBundle)(config.bundleUrl),window.WEBSPELLCHECKER?((0,_bundle_warnings.install)(config),TextareaEnv.init(config),AttoEnv.init(config),TinyMceEnv.init(config)):(0,_notify.showWarning)(config.bundleLoadErrorMessage))}})); //# sourceMappingURL=init.min.js.map \ No newline at end of file diff --git a/amd/build/init.min.js.map b/amd/build/init.min.js.map index a1db0de..be84be6 100644 --- a/amd/build/init.min.js.map +++ b/amd/build/init.min.js.map @@ -1 +1 @@ -{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - https://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 * WProofreader main entry point.\n *\n * Applies global configuration, dynamically loads the WProofreader JS library,\n * and dispatches to the editor-specific environments once the bundle is ready.\n *\n * @module local_wproofreader/init\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Config from 'local_wproofreader/proofreader_config';\nimport * as AttoEnv from 'local_wproofreader/environment_atto';\nimport * as TinyMceEnv from 'local_wproofreader/environment_tinymce';\nimport * as TextareaEnv from 'local_wproofreader/environment_textarea';\nimport {loadBundle} from 'local_wproofreader/bundle_loader';\n\n/**\n * Boot WProofreader on the current page.\n *\n * @returns {Promise}\n */\nexport const init = async() => {\n const config = window.WPROOFREADER_BOOTSTRAP;\n if (!config || typeof config !== 'object') {\n return;\n }\n\n Config.apply(config);\n await loadBundle(config.bundleUrl);\n\n if (!window.WEBSPELLCHECKER) {\n return;\n }\n\n TextareaEnv.init(config);\n AttoEnv.init(config);\n TinyMceEnv.init(config);\n};\n"],"names":["async","config","window","WPROOFREADER_BOOTSTRAP","Config","apply","bundleUrl","WEBSPELLCHECKER","TextareaEnv","init","AttoEnv","TinyMceEnv"],"mappings":";;;;;;;;;;gRAqCoBA,gBACVC,OAASC,OAAOC,uBACjBF,QAA4B,iBAAXA,SAItBG,OAAOC,MAAMJ,cACP,6BAAWA,OAAOK,WAEnBJ,OAAOK,kBAIZC,YAAYC,KAAKR,QACjBS,QAAQD,KAAKR,QACbU,WAAWF,KAAKR"} \ No newline at end of file +{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - https://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 * WProofreader main entry point.\n *\n * Applies global configuration, dynamically loads the WProofreader JS library,\n * and dispatches to the editor-specific environments once the bundle is ready.\n *\n * @module local_wproofreader/init\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {showWarning} from 'local_wproofreader/notify';\nimport * as Config from 'local_wproofreader/proofreader_config';\nimport * as AttoEnv from 'local_wproofreader/environment_atto';\nimport * as TinyMceEnv from 'local_wproofreader/environment_tinymce';\nimport * as TextareaEnv from 'local_wproofreader/environment_textarea';\nimport {loadBundle} from 'local_wproofreader/bundle_loader';\nimport {install as installBundleWarnings} from 'local_wproofreader/bundle_warnings';\n\n/**\n * Boot WProofreader on the current page.\n *\n * @returns {Promise}\n */\nexport const init = async() => {\n const config = window.WPROOFREADER_BOOTSTRAP;\n if (!config || typeof config !== 'object') {\n return;\n }\n\n Config.apply(config);\n await loadBundle(config.bundleUrl);\n\n if (!window.WEBSPELLCHECKER) {\n showWarning(config.bundleLoadErrorMessage);\n return;\n }\n\n installBundleWarnings(config);\n\n TextareaEnv.init(config);\n AttoEnv.init(config);\n TinyMceEnv.init(config);\n};\n"],"names":["async","config","window","WPROOFREADER_BOOTSTRAP","Config","apply","bundleUrl","WEBSPELLCHECKER","TextareaEnv","init","AttoEnv","TinyMceEnv","bundleLoadErrorMessage"],"mappings":";;;;;;;;;;gRAuCoBA,gBACVC,OAASC,OAAOC,uBACjBF,QAA4B,iBAAXA,SAItBG,OAAOC,MAAMJ,cACP,6BAAWA,OAAOK,WAEnBJ,OAAOK,8CAKUN,QAEtBO,YAAYC,KAAKR,QACjBS,QAAQD,KAAKR,QACbU,WAAWF,KAAKR,iCARAA,OAAOW"} \ No newline at end of file diff --git a/amd/build/notify.min.js b/amd/build/notify.min.js new file mode 100644 index 0000000..ed7b495 --- /dev/null +++ b/amd/build/notify.min.js @@ -0,0 +1,24 @@ +define("local_wproofreader/notify",["exports","core/notification","local_wproofreader/constants"],(function(_exports,_notification,_constants){var obj; +/** + * Shared warning display for WProofreader runtime notices. + * + * core/notification.addNotification() always renders into the page-level + * #user-notifications region. When the editor triggering a warning lives + * inside an open Moodle modal (e.g. the calendar's "New event" dialog), that + * region sits behind the modal's backdrop and the notification is never + * seen, so render inside the open modal's own body in that case instead. + * + * Where the warning can be tied to a specific editor field, show it next to + * that field directly, styled as an informational (orange, bold) note in the + * same spot as Moodle's own per-field validation error - but not using + * core_form/events' notifyFieldValidationFailure() itself: that sets + * .is-invalid/.has-danger, which mform's own client-side validation checks + * on submit and blocks the form on. This is an FYI about proofreading, not a + * reason to stop the user submitting their content. + * + * @module local_wproofreader/notify + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showWarning=_exports.notifyField=_exports.notifyAllAttachedFields=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const ATTACHED_INSTANCE_SELECTOR="[".concat(_constants.INSTANCE_ATTR,'="').concat(_constants.INSTANCE_ATTR_VALUE,'"], [').concat(_constants.ATTACHED_ATTR,"]"),showWarning=message=>{if(!message)return;const modalBody=(()=>{const modal=document.querySelector(".modal.show");return modal?modal.querySelector(".modal-body")||modal:null})();if(!modalBody)return void _notification.default.addNotification({message:message,type:"warning"}).then((()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#user-notifications"))||void 0===_document$querySelect||_document$querySelect.scrollIntoView({behavior:"smooth",block:"center"})}));const alert=document.createElement("div");alert.className="alert alert-warning",alert.setAttribute("role","alert"),alert.textContent=message,modalBody.prepend(alert),alert.scrollIntoView({behavior:"smooth",block:"nearest"})};_exports.showWarning=showWarning;const notifyField=(container,message)=>{if(!message)return;const field=(container=>{if(!container)return null;if("IFRAME"===container.tagName&&window.tinymce&&"function"==typeof window.tinymce.get){const editor=window.tinymce.get().find((candidate=>candidate&&candidate.iframeElement===container));return editor&&"function"==typeof editor.getElement?editor.getElement():null}const wrapper=container.closest(".felement, .fitem");return wrapper?wrapper.querySelector("textarea"):null})(container);field?((field,message)=>{const wrapper=field.closest(".felement")||field.parentElement;if(!wrapper)return void showWarning(message);let note=wrapper.querySelector(".".concat("wsc-field-warning"));note||(note=document.createElement("div"),note.className="wsc-field-warning",note.setAttribute("role","status"),wrapper.appendChild(note)),note.textContent=message})(field,message):showWarning(message)};_exports.notifyField=notifyField;_exports.notifyAllAttachedFields=message=>{message&&document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container=>notifyField(container,message)))}})); + +//# sourceMappingURL=notify.min.js.map \ No newline at end of file diff --git a/amd/build/notify.min.js.map b/amd/build/notify.min.js.map new file mode 100644 index 0000000..1c0cbaf --- /dev/null +++ b/amd/build/notify.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"notify.min.js","sources":["../src/notify.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Shared warning display for WProofreader runtime notices.\n *\n * core/notification.addNotification() always renders into the page-level\n * #user-notifications region. When the editor triggering a warning lives\n * inside an open Moodle modal (e.g. the calendar's \"New event\" dialog), that\n * region sits behind the modal's backdrop and the notification is never\n * seen, so render inside the open modal's own body in that case instead.\n *\n * Where the warning can be tied to a specific editor field, show it next to\n * that field directly, styled as an informational (orange, bold) note in the\n * same spot as Moodle's own per-field validation error - but not using\n * core_form/events' notifyFieldValidationFailure() itself: that sets\n * .is-invalid/.has-danger, which mform's own client-side validation checks\n * on submit and blocks the form on. This is an FYI about proofreading, not a\n * reason to stop the user submitting their content.\n *\n * @module local_wproofreader/notify\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {INSTANCE_ATTR, INSTANCE_ATTR_VALUE, ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst OPEN_MODAL_SELECTOR = '.modal.show';\nconst FIELD_WARNING_CLASS = 'wsc-field-warning';\n// Plain textareas only carry the bundle's own INSTANCE_ATTR; Atto/TinyMCE\n// only carry our own ATTACHED_ATTR (see constants.js for why neither alone\n// covers all three editor types).\nconst ATTACHED_INSTANCE_SELECTOR = `[${INSTANCE_ATTR}=\"${INSTANCE_ATTR_VALUE}\"], [${ATTACHED_ATTR}]`;\n\nconst findOpenModalBody = () => {\n const modal = document.querySelector(OPEN_MODAL_SELECTOR);\n if (!modal) {\n return null;\n }\n return modal.querySelector('.modal-body') || modal;\n};\n\n/**\n * Resolve a WProofreader attach container back to the original mform field\n * it replaced, so the warning can be anchored in the right place: an Atto\n * content div or a TinyMCE iframe both replace an original textarea, while\n * a plain autoSearch-attached textarea is already that field itself.\n *\n * @param {HTMLElement} container The element WEBSPELLCHECKER attached an instance to.\n * @returns {HTMLElement|null}\n */\nconst findOriginalField = (container) => {\n if (!container) {\n return null;\n }\n\n if (container.tagName === 'IFRAME' && window.tinymce && typeof window.tinymce.get === 'function') {\n const editor = window.tinymce.get().find((candidate) => candidate && candidate.iframeElement === container);\n if (editor && typeof editor.getElement === 'function') {\n return editor.getElement();\n }\n return null;\n }\n\n // Atto hides the original textarea and inserts its editable wrapper as a\n // sibling within the same mform field wrapper; a plain textarea is its\n // own only match within that same wrapper.\n const wrapper = container.closest('.felement, .fitem');\n return wrapper ? wrapper.querySelector('textarea') : null;\n};\n\n/**\n * Show a warning, inside the open modal if there is one, else page-wide.\n *\n * @param {string} message Warning text to display.\n */\nexport const showWarning = (message) => {\n if (!message) {\n return;\n }\n\n const modalBody = findOpenModalBody();\n if (!modalBody) {\n Notification.addNotification({message, type: 'warning'}).then(() => {\n document.querySelector('#user-notifications')?.scrollIntoView({behavior: 'smooth', block: 'center'});\n return;\n });\n return;\n }\n\n const alert = document.createElement('div');\n alert.className = 'alert alert-warning';\n alert.setAttribute('role', 'alert');\n alert.textContent = message;\n modalBody.prepend(alert);\n alert.scrollIntoView({behavior: 'smooth', block: 'nearest'});\n};\n\n/**\n * Insert or update the informational warning note beside a field, without\n * touching the field's own validity state.\n *\n * @param {HTMLElement} field The mform field to anchor the note to.\n * @param {string} message Warning text to display.\n */\nconst showFieldWarning = (field, message) => {\n const wrapper = field.closest('.felement') || field.parentElement;\n if (!wrapper) {\n showWarning(message);\n return;\n }\n\n let note = wrapper.querySelector(`.${FIELD_WARNING_CLASS}`);\n if (!note) {\n note = document.createElement('div');\n note.className = FIELD_WARNING_CLASS;\n note.setAttribute('role', 'status');\n wrapper.appendChild(note);\n }\n note.textContent = message;\n};\n\n/**\n * Show a warning next to the specific editor field that triggered it, as an\n * informational note rather than a form validation error - it must never\n * block form submission. Falls back to showWarning() if the container\n * cannot be resolved back to a real mform field.\n *\n * @param {HTMLElement} container The element passed to WEBSPELLCHECKER.init().\n * @param {string} message Warning text to display.\n */\nexport const notifyField = (container, message) => {\n if (!message) {\n return;\n }\n\n const field = findOriginalField(container);\n if (!field) {\n showWarning(message);\n return;\n }\n\n showFieldWarning(field, message);\n};\n\n/**\n * Show a warning against every currently attached WProofreader instance on\n * the page. Used for warnings that stem from shared configuration (e.g. an\n * unsupported language), which affects every attached instance equally\n * rather than one specific field.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyAllAttachedFields = (message) => {\n if (!message) {\n return;\n }\n\n document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container) => notifyField(container, message));\n};\n"],"names":["ATTACHED_INSTANCE_SELECTOR","INSTANCE_ATTR","INSTANCE_ATTR_VALUE","ATTACHED_ATTR","showWarning","message","modalBody","modal","document","querySelector","findOpenModalBody","addNotification","type","then","scrollIntoView","behavior","block","alert","createElement","className","setAttribute","textContent","prepend","notifyField","container","field","tagName","window","tinymce","get","editor","find","candidate","iframeElement","getElement","wrapper","closest","findOriginalField","parentElement","note","appendChild","showFieldWarning","querySelectorAll","forEach"],"mappings":";;;;;;;;;;;;;;;;;;;;;yNA6CMA,sCAAiCC,sCAAkBC,+CAA2BC,8BA4CvEC,YAAeC,cACnBA,qBAICC,UA/CgB,YAChBC,MAAQC,SAASC,cARC,sBASnBF,MAGEA,MAAME,cAAc,gBAAkBF,MAFlC,MA4COG,OACbJ,4CACYK,gBAAgB,CAACN,QAAAA,QAASO,KAAM,YAAYC,MAAK,6DAC1DL,SAASC,cAAc,+EAAwBK,eAAe,CAACC,SAAU,SAAUC,MAAO,oBAM5FC,MAAQT,SAASU,cAAc,OACrCD,MAAME,UAAY,sBAClBF,MAAMG,aAAa,OAAQ,SAC3BH,MAAMI,YAAchB,QACpBC,UAAUgB,QAAQL,OAClBA,MAAMH,eAAe,CAACC,SAAU,SAAUC,MAAO,oDAoCxCO,YAAc,CAACC,UAAWnB,eAC9BA,qBAICoB,MArFiBD,CAAAA,gBAClBA,iBACM,QAGe,WAAtBA,UAAUE,SAAwBC,OAAOC,SAAyC,mBAAvBD,OAAOC,QAAQC,IAAoB,OACxFC,OAASH,OAAOC,QAAQC,MAAME,MAAMC,WAAcA,WAAaA,UAAUC,gBAAkBT,mBAC7FM,QAAuC,mBAAtBA,OAAOI,WACjBJ,OAAOI,aAEX,WAMLC,QAAUX,UAAUY,QAAQ,4BAC3BD,QAAUA,QAAQ1B,cAAc,YAAc,MAoEvC4B,CAAkBb,WAC3BC,MAhCgB,EAACA,MAAOpB,iBACvB8B,QAAUV,MAAMW,QAAQ,cAAgBX,MAAMa,kBAC/CH,oBACD/B,YAAYC,aAIZkC,KAAOJ,QAAQ1B,yBApFK,sBAqFnB8B,OACDA,KAAO/B,SAASU,cAAc,OAC9BqB,KAAKpB,UAvFe,oBAwFpBoB,KAAKnB,aAAa,OAAQ,UAC1Be,QAAQK,YAAYD,OAExBA,KAAKlB,YAAchB,SAuBnBoC,CAAiBhB,MAAOpB,SAJpBD,YAAYC,4EAeoBA,UAC/BA,SAILG,SAASkC,iBAAiB1C,4BAA4B2C,SAASnB,WAAcD,YAAYC,UAAWnB"} \ No newline at end of file diff --git a/amd/build/proofreader_config.min.js b/amd/build/proofreader_config.min.js index 375d4ba..8d3b2d0 100644 --- a/amd/build/proofreader_config.min.js +++ b/amd/build/proofreader_config.min.js @@ -6,6 +6,6 @@ define("local_wproofreader/proofreader_config",["exports"],(function(_exports){O * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const toBoolean=(value,fallback)=>!0===value||"true"===value||!1!==value&&"false"!==value&&fallback,toArray=value=>Array.isArray(value)?value:[];_exports.apply=config=>{const isBadgeEnabled=toBoolean(config.enableBadgeButton,!0),defaultBadgeActions=isBadgeEnabled?["addWord","ignoreAll","settings","toggle","proofreadDialog"]:["addWord","ignoreAll","settings","proofreadDialog"],badgeActions=toArray(config.actionItems).length?config.actionItems:defaultBadgeActions;window.WEBSPELLCHECKER_CONFIG={autoSearch:toBoolean(config.autoSearch,!0),appType:config.appType,serviceProtocol:config.serviceProtocol||"https",serviceHost:config.serviceHost||"svc.webspellchecker.net",servicePath:config.servicePath||"api",servicePort:config.servicePort||"443",enableGrammar:toBoolean(config.enableGrammar,!1),aiWritingAssistant:toBoolean(config.aiWritingAssistant,!1),settingsSections:toArray(config.settingsSections),serviceId:config.serviceId,lang:config.lang,enableBadgeButton:isBadgeEnabled,actionItems:badgeActions,disableAutoSearchIn:toArray(config.disableAutoSearchIn),disableOptionsStorage:toArray(config.disableOptionsStorage),disableDictionariesPreferences:toBoolean(config.disableDictionariesPreferences,!1),autocomplete:toBoolean(config.autocomplete,!1),autocorrect:toBoolean(config.autocorrect,!1),spellingSuggestions:toBoolean(config.spellingSuggestions,!0),grammarSuggestions:toBoolean(config.grammarSuggestions,!1),styleGuideSuggestions:toBoolean(config.styleGuideSuggestions,!0),ignoreAllCapsWords:toBoolean(config.ignoreAllCapsWords,!0),ignoreDomainNames:toBoolean(config.ignoreDomainNames,!0),ignoreWordsWithMixedCases:toBoolean(config.ignoreWordsWithMixedCases,!0),ignoreWordsWithNumbers:toBoolean(config.ignoreWordsWithNumbers,!0),globalBadge:toBoolean(config.globalBadge,!0),compactBadge:toBoolean(config.compactBadge,!0),allSuggestionsMode:toBoolean(config.allSuggestionsMode,!0),onLoad:function(){const instance=this;try{this.subscribe("replaceProblem",(()=>{try{instance.getContainerNode().dispatchEvent(new Event("input",{bubbles:!0}))}catch(e){}}))}catch(e){}}},Array.isArray(config.generalOptions)&&config.generalOptions.length&&(window.WEBSPELLCHECKER_CONFIG.generalOptions=config.generalOptions)}})); +const toBoolean=(value,fallback)=>!0===value||"true"===value||!1!==value&&"false"!==value&&fallback,toArray=value=>Array.isArray(value)?value:[];_exports.apply=config=>{const isBadgeEnabled=toBoolean(config.enableBadgeButton,!0),defaultBadgeActions=isBadgeEnabled?["addWord","ignoreAll","settings","toggle","proofreadDialog"]:["addWord","ignoreAll","settings","proofreadDialog"],badgeActions=toArray(config.actionItems).length?config.actionItems:defaultBadgeActions;window.WEBSPELLCHECKER_CONFIG={autoSearch:toBoolean(config.autoSearch,!0),appType:config.appType,serviceProtocol:config.serviceProtocol||"https",serviceHost:config.serviceHost||"svc.webspellchecker.net",servicePath:config.servicePath||"api",servicePort:config.servicePort||"443",enableGrammar:toBoolean(config.enableGrammar,!1),aiWritingAssistant:toBoolean(config.aiWritingAssistant,!1),settingsSections:toArray(config.settingsSections),serviceId:config.serviceId,lang:config.lang,enableBadgeButton:isBadgeEnabled,actionItems:badgeActions,disableAutoSearchIn:[...toArray(config.disableAutoSearchIn),"textarea"],disableOptionsStorage:toArray(config.disableOptionsStorage),disableDictionariesPreferences:toBoolean(config.disableDictionariesPreferences,!1),autocomplete:toBoolean(config.autocomplete,!1),autocorrect:toBoolean(config.autocorrect,!1),spellingSuggestions:toBoolean(config.spellingSuggestions,!0),grammarSuggestions:toBoolean(config.grammarSuggestions,!1),styleGuideSuggestions:toBoolean(config.styleGuideSuggestions,!0),ignoreAllCapsWords:toBoolean(config.ignoreAllCapsWords,!0),ignoreDomainNames:toBoolean(config.ignoreDomainNames,!0),ignoreWordsWithMixedCases:toBoolean(config.ignoreWordsWithMixedCases,!0),ignoreWordsWithNumbers:toBoolean(config.ignoreWordsWithNumbers,!0),globalBadge:toBoolean(config.globalBadge,!1),compactBadge:toBoolean(config.compactBadge,!0),allSuggestionsMode:toBoolean(config.allSuggestionsMode,!0),onLoad:function(){const instance=this;try{this.subscribe("replaceProblem",(()=>{try{instance.getContainerNode().dispatchEvent(new Event("input",{bubbles:!0}))}catch(e){}}))}catch(e){}}},Array.isArray(config.generalOptions)&&config.generalOptions.length&&(window.WEBSPELLCHECKER_CONFIG.generalOptions=config.generalOptions)}})); //# sourceMappingURL=proofreader_config.min.js.map \ No newline at end of file diff --git a/amd/build/proofreader_config.min.js.map b/amd/build/proofreader_config.min.js.map index 15206cf..a43b31d 100644 --- a/amd/build/proofreader_config.min.js.map +++ b/amd/build/proofreader_config.min.js.map @@ -1 +1 @@ -{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n disableAutoSearchIn: toArray(config.disableAutoSearchIn),\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, true),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","generalOptions"],"mappings":";;;;;;;;MAuBMA,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aACbgB,oBAAqBvB,QAAQG,OAAOoB,qBACpCC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,OAMb7C,MAAMC,QAAQC,OAAO4C,iBAAmB5C,OAAO4C,eAAetC,SAC9DC,OAAOC,uBAAuBoC,eAAiB5C,OAAO4C"} \ No newline at end of file +{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n // Plain textareas are attached explicitly by\n // local_wproofreader/environment_textarea instead of the bundle's\n // own autoSearch, which only scans once at load and misses fields\n // revealed later (e.g. an mform \"Show more\" advanced section).\n disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'],\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, false),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","generalOptions"],"mappings":";;;;;;;;MAuBMA,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aAKbgB,oBAAqB,IAAIvB,QAAQG,OAAOoB,qBAAsB,YAC9DC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,OAMb7C,MAAMC,QAAQC,OAAO4C,iBAAmB5C,OAAO4C,eAAetC,SAC9DC,OAAOC,uBAAuBoC,eAAiB5C,OAAO4C"} \ No newline at end of file diff --git a/amd/src/bundle_warnings.js b/amd/src/bundle_warnings.js new file mode 100644 index 0000000..c37777d --- /dev/null +++ b/amd/src/bundle_warnings.js @@ -0,0 +1,70 @@ +// This file is part of Moodle - https://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 . + +/** + * Surfaces the WProofreader bundle's own runtime console.warn calls as + * visible warnings. + * + * The bundle reports some runtime conditions (an unsupported configured + * language, the service being unreachable mid-session) only via + * console.warn, with no exception thrown and no subscribable event, so + * there is no other integration point to hook into. + * + * @module local_wproofreader/bundle_warnings + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {notifyAllAttachedFields} from 'local_wproofreader/notify'; + +let installed = false; + +const MATCHERS = [ + {pattern: /is unsupported or doesn't exist/, key: 'languageUnsupported'}, + {pattern: /language code is not available for your Service ID/, key: 'languageUnsupported'}, + {pattern: /language list is not defined/, key: 'languageUnsupported'}, + {pattern: /WebSpellChecker Service is currently unavailable/, key: 'serviceUnavailable'}, + {pattern: /CORS response parsing error/, key: 'serviceUnavailable'}, +]; + +/** + * Wrap window.console.warn once to catch the bundle's known runtime warnings. + * + * @param {Object} config Page configuration carrying the notification messages. + */ +export const install = (config) => { + if (installed || !window.console || typeof window.console.warn !== 'function') { + return; + } + installed = true; + + const messages = { + languageUnsupported: config.runtimeLanguageUnsupportedMessage, + serviceUnavailable: config.runtimeServiceUnavailableMessage, + }; + const originalWarn = window.console.warn.bind(window.console); + + window.console.warn = (...args) => { + originalWarn(...args); + + const text = String(args[0] || ''); + const matcher = MATCHERS.find((candidate) => candidate.pattern.test(text)); + if (!matcher || !messages[matcher.key]) { + return; + } + + notifyAllAttachedFields(messages[matcher.key]); + }; +}; diff --git a/amd/src/constants.js b/amd/src/constants.js new file mode 100644 index 0000000..33cf0af --- /dev/null +++ b/amd/src/constants.js @@ -0,0 +1,44 @@ +// This file is part of Moodle - https://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 . + +/** + * Shared constants for the WProofreader AMD modules. + * + * @module local_wproofreader/constants + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The bundle's own `instance_attribute` option (confirmed in wscbundle.js's + * OptionsManager defaults). It sets this to "true" on the container it + * directly manages once an instance is attached. For a plain textarea + * (autoSearch-attached, container === the textarea) that is a reliable + * outer-page signal. For TinyMCE the container we pass is the outer + * iframe, but the bundle tags something inside that iframe's own document + * instead, so this never appears on the iframe element itself - Atto and + * TinyMCE rely on ATTACHED_ATTR below instead. + */ +export const INSTANCE_ATTR = 'data-wsc-instance'; +export const INSTANCE_ATTR_VALUE = 'true'; + +/** + * Our own marker, set immediately before calling WEBSPELLCHECKER.init() and + * kept afterwards on success (only removed if that call throws). Used both + * to stop the MutationObserver re-attempting an attach already in flight, + * and - for Atto/TinyMCE specifically - as the outer-page-queryable "this + * instance is attached" signal that INSTANCE_ATTR cannot reliably provide. + */ +export const ATTACHED_ATTR = 'data-wsc-attached'; diff --git a/amd/src/environment_atto.js b/amd/src/environment_atto.js index 5a01eab..17a5ec0 100644 --- a/amd/src/environment_atto.js +++ b/amd/src/environment_atto.js @@ -21,14 +21,17 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import {notifyField} from 'local_wproofreader/notify'; +import {ATTACHED_ATTR} from 'local_wproofreader/constants'; + const SELECTORS = [ '.editor_atto_content[contenteditable="true"]', '.editor_atto [contenteditable="true"]', ]; -const INSTANCE_ATTR = 'data-wsc-instance'; let observer = null; let hookInstalled = false; +let attachErrorMessage = null; const findEditors = () => { const seen = new Set(); @@ -38,22 +41,23 @@ const findEditors = () => { return Array.from(seen); }; -const isInstanceCreated = (element) => element.hasAttribute(INSTANCE_ATTR); +const isInstanceCreated = (element) => element.hasAttribute(ATTACHED_ATTR); const createInstance = (element) => { if (!element || !element.isContentEditable || isInstanceCreated(element) || !window.WEBSPELLCHECKER) { return; } - element.setAttribute(INSTANCE_ATTR, '1'); + element.setAttribute(ATTACHED_ATTR, '1'); try { window.WEBSPELLCHECKER.init({container: element}); } catch (e) { - element.removeAttribute(INSTANCE_ATTR); + element.removeAttribute(ATTACHED_ATTR); if (window.console && window.console.warn) { window.console.warn('WProofreader: failed to attach to Atto editor', e); } + notifyField(element, attachErrorMessage); } }; @@ -99,8 +103,11 @@ const hookBundleReady = () => { /** * Initialize the Atto editor environment. + * + * @param {Object} config Page configuration. */ -export const init = () => { +export const init = (config) => { + attachErrorMessage = config && config.editorAttachErrorMessage || null; hookBundleReady(); startObserver(); scanAndInit(); diff --git a/amd/src/environment_textarea.js b/amd/src/environment_textarea.js index f015c20..4bdc54a 100644 --- a/amd/src/environment_textarea.js +++ b/amd/src/environment_textarea.js @@ -16,11 +16,19 @@ /** * Plain HTML textarea integration. * + * Attaches explicitly instead of relying on the bundle's own autoSearch + * (disabled for textareas in proofreader_config.js), since autoSearch only + * scans once at load and misses fields revealed later, such as an mform + * "Show more" advanced section. + * * @module local_wproofreader/environment_textarea * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import {notifyField} from 'local_wproofreader/notify'; +import {ATTACHED_ATTR} from 'local_wproofreader/constants'; + const SKIP_CLASS = 'wsc-skip-autosearch'; const CODE_FIELD_HINTS = [ 'id$="customcss"', @@ -30,6 +38,10 @@ const CODE_FIELD_HINTS = [ 'name$="[customcss]"', ]; +let observer = null; +let hookInstalled = false; +let attachErrorMessage = null; + const tagSkippableTextareas = () => { const selectors = CODE_FIELD_HINTS.map((hint) => `textarea[${hint}]`).join(','); @@ -38,22 +50,78 @@ const tagSkippableTextareas = () => { }); }; +const isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); + +const findTextareas = () => Array.from(document.querySelectorAll('textarea')) + .filter((textarea) => !textarea.classList.contains(SKIP_CLASS)); + +const createInstance = (textarea) => { + if (!textarea || textarea.hasAttribute(ATTACHED_ATTR) || !isVisible(textarea) || !window.WEBSPELLCHECKER) { + return; + } + + textarea.setAttribute(ATTACHED_ATTR, '1'); + + try { + window.WEBSPELLCHECKER.init({container: textarea}); + } catch (e) { + textarea.removeAttribute(ATTACHED_ATTR); + if (window.console && window.console.warn) { + window.console.warn('WProofreader: failed to attach to textarea', e); + } + notifyField(textarea, attachErrorMessage); + } +}; + +const scanAndInit = () => { + findTextareas().forEach(createInstance); +}; + +const startObserver = () => { + if (observer || typeof MutationObserver === 'undefined') { + return; + } + + observer = new MutationObserver(() => { + scanAndInit(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'], + }); +}; + +const hookBundleReady = () => { + if (hookInstalled) { + return; + } + hookInstalled = true; + + const previous = window.webspellcheckerAlreadyLoaded; + window.webspellcheckerAlreadyLoaded = function() { + if (typeof previous === 'function') { + try { + previous.apply(this, arguments); + } catch (e) { + // Preserve original callback contract on failure. + } + } + scanAndInit(); + }; +}; + /** * Initialize the textarea environment. * - * @param {Object} config Page configuration (mutated to extend disableAutoSearchIn). + * @param {Object} config Page configuration. */ export const init = (config) => { + attachErrorMessage = config && config.editorAttachErrorMessage || null; tagSkippableTextareas(); - - if (window.WEBSPELLCHECKER_CONFIG && Array.isArray(window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn)) { - const skipSelector = `.${SKIP_CLASS}`; - if (!window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.includes(skipSelector)) { - window.WEBSPELLCHECKER_CONFIG.disableAutoSearchIn.push(skipSelector); - } - } - - // Keep the parameter signature consistent for the init dispatcher even though - // we do not currently use the rest of the config here. - void config; + hookBundleReady(); + startObserver(); + scanAndInit(); }; diff --git a/amd/src/environment_tinymce.js b/amd/src/environment_tinymce.js index b5ac4f8..a37bac0 100644 --- a/amd/src/environment_tinymce.js +++ b/amd/src/environment_tinymce.js @@ -21,17 +21,20 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import {notifyField} from 'local_wproofreader/notify'; +import {ATTACHED_ATTR} from 'local_wproofreader/constants'; + const SELECTOR = 'iframe.tox-edit-area__iframe'; -const INSTANCE_ATTR = 'data-wsc-instance'; let observer = null; let hookInstalled = false; +let attachErrorMessage = null; const findIframes = () => Array.from(document.querySelectorAll(SELECTOR)); -const isMarked = (iframe) => iframe.hasAttribute(INSTANCE_ATTR); -const mark = (iframe) => iframe.setAttribute(INSTANCE_ATTR, '1'); -const unmark = (iframe) => iframe.removeAttribute(INSTANCE_ATTR); +const isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR); +const mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1'); +const unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR); const findEditor = (iframe) => { @@ -58,6 +61,7 @@ const initInstance = (iframe) => { if (window.console && window.console.warn) { window.console.warn('WProofreader: failed to attach to TinyMCE editor', e); } + notifyField(iframe, attachErrorMessage); } }; @@ -124,8 +128,11 @@ const hookBundleReady = () => { /** * Initialize the TinyMCE environment. + * + * @param {Object} config Page configuration. */ -export const init = () => { +export const init = (config) => { + attachErrorMessage = config && config.editorAttachErrorMessage || null; hookBundleReady(); startObserver(); scanAndInit(); diff --git a/amd/src/init.js b/amd/src/init.js index 33ec64a..48a0503 100644 --- a/amd/src/init.js +++ b/amd/src/init.js @@ -24,11 +24,13 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import {showWarning} from 'local_wproofreader/notify'; import * as Config from 'local_wproofreader/proofreader_config'; import * as AttoEnv from 'local_wproofreader/environment_atto'; import * as TinyMceEnv from 'local_wproofreader/environment_tinymce'; import * as TextareaEnv from 'local_wproofreader/environment_textarea'; import {loadBundle} from 'local_wproofreader/bundle_loader'; +import {install as installBundleWarnings} from 'local_wproofreader/bundle_warnings'; /** * Boot WProofreader on the current page. @@ -45,9 +47,12 @@ export const init = async() => { await loadBundle(config.bundleUrl); if (!window.WEBSPELLCHECKER) { + showWarning(config.bundleLoadErrorMessage); return; } + installBundleWarnings(config); + TextareaEnv.init(config); AttoEnv.init(config); TinyMceEnv.init(config); diff --git a/amd/src/notify.js b/amd/src/notify.js new file mode 100644 index 0000000..e47ab6f --- /dev/null +++ b/amd/src/notify.js @@ -0,0 +1,173 @@ +// This file is part of Moodle - https://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 . + +/** + * Shared warning display for WProofreader runtime notices. + * + * core/notification.addNotification() always renders into the page-level + * #user-notifications region. When the editor triggering a warning lives + * inside an open Moodle modal (e.g. the calendar's "New event" dialog), that + * region sits behind the modal's backdrop and the notification is never + * seen, so render inside the open modal's own body in that case instead. + * + * Where the warning can be tied to a specific editor field, show it next to + * that field directly, styled as an informational (orange, bold) note in the + * same spot as Moodle's own per-field validation error - but not using + * core_form/events' notifyFieldValidationFailure() itself: that sets + * .is-invalid/.has-danger, which mform's own client-side validation checks + * on submit and blocks the form on. This is an FYI about proofreading, not a + * reason to stop the user submitting their content. + * + * @module local_wproofreader/notify + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Notification from 'core/notification'; +import {INSTANCE_ATTR, INSTANCE_ATTR_VALUE, ATTACHED_ATTR} from 'local_wproofreader/constants'; + +const OPEN_MODAL_SELECTOR = '.modal.show'; +const FIELD_WARNING_CLASS = 'wsc-field-warning'; +// Plain textareas only carry the bundle's own INSTANCE_ATTR; Atto/TinyMCE +// only carry our own ATTACHED_ATTR (see constants.js for why neither alone +// covers all three editor types). +const ATTACHED_INSTANCE_SELECTOR = `[${INSTANCE_ATTR}="${INSTANCE_ATTR_VALUE}"], [${ATTACHED_ATTR}]`; + +const findOpenModalBody = () => { + const modal = document.querySelector(OPEN_MODAL_SELECTOR); + if (!modal) { + return null; + } + return modal.querySelector('.modal-body') || modal; +}; + +/** + * Resolve a WProofreader attach container back to the original mform field + * it replaced, so the warning can be anchored in the right place: an Atto + * content div or a TinyMCE iframe both replace an original textarea, while + * a plain autoSearch-attached textarea is already that field itself. + * + * @param {HTMLElement} container The element WEBSPELLCHECKER attached an instance to. + * @returns {HTMLElement|null} + */ +const findOriginalField = (container) => { + if (!container) { + return null; + } + + if (container.tagName === 'IFRAME' && window.tinymce && typeof window.tinymce.get === 'function') { + const editor = window.tinymce.get().find((candidate) => candidate && candidate.iframeElement === container); + if (editor && typeof editor.getElement === 'function') { + return editor.getElement(); + } + return null; + } + + // Atto hides the original textarea and inserts its editable wrapper as a + // sibling within the same mform field wrapper; a plain textarea is its + // own only match within that same wrapper. + const wrapper = container.closest('.felement, .fitem'); + return wrapper ? wrapper.querySelector('textarea') : null; +}; + +/** + * Show a warning, inside the open modal if there is one, else page-wide. + * + * @param {string} message Warning text to display. + */ +export const showWarning = (message) => { + if (!message) { + return; + } + + const modalBody = findOpenModalBody(); + if (!modalBody) { + Notification.addNotification({message, type: 'warning'}).then(() => { + document.querySelector('#user-notifications')?.scrollIntoView({behavior: 'smooth', block: 'center'}); + return; + }); + return; + } + + const alert = document.createElement('div'); + alert.className = 'alert alert-warning'; + alert.setAttribute('role', 'alert'); + alert.textContent = message; + modalBody.prepend(alert); + alert.scrollIntoView({behavior: 'smooth', block: 'nearest'}); +}; + +/** + * Insert or update the informational warning note beside a field, without + * touching the field's own validity state. + * + * @param {HTMLElement} field The mform field to anchor the note to. + * @param {string} message Warning text to display. + */ +const showFieldWarning = (field, message) => { + const wrapper = field.closest('.felement') || field.parentElement; + if (!wrapper) { + showWarning(message); + return; + } + + let note = wrapper.querySelector(`.${FIELD_WARNING_CLASS}`); + if (!note) { + note = document.createElement('div'); + note.className = FIELD_WARNING_CLASS; + note.setAttribute('role', 'status'); + wrapper.appendChild(note); + } + note.textContent = message; +}; + +/** + * Show a warning next to the specific editor field that triggered it, as an + * informational note rather than a form validation error - it must never + * block form submission. Falls back to showWarning() if the container + * cannot be resolved back to a real mform field. + * + * @param {HTMLElement} container The element passed to WEBSPELLCHECKER.init(). + * @param {string} message Warning text to display. + */ +export const notifyField = (container, message) => { + if (!message) { + return; + } + + const field = findOriginalField(container); + if (!field) { + showWarning(message); + return; + } + + showFieldWarning(field, message); +}; + +/** + * Show a warning against every currently attached WProofreader instance on + * the page. Used for warnings that stem from shared configuration (e.g. an + * unsupported language), which affects every attached instance equally + * rather than one specific field. + * + * @param {string} message Warning text to display. + */ +export const notifyAllAttachedFields = (message) => { + if (!message) { + return; + } + + document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container) => notifyField(container, message)); +}; diff --git a/amd/src/proofreader_config.js b/amd/src/proofreader_config.js index 870950c..7bdda0a 100644 --- a/amd/src/proofreader_config.js +++ b/amd/src/proofreader_config.js @@ -61,7 +61,11 @@ export const apply = (config) => { lang: config.lang, enableBadgeButton: isBadgeEnabled, actionItems: badgeActions, - disableAutoSearchIn: toArray(config.disableAutoSearchIn), + // Plain textareas are attached explicitly by + // local_wproofreader/environment_textarea instead of the bundle's + // own autoSearch, which only scans once at load and misses fields + // revealed later (e.g. an mform "Show more" advanced section). + disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'], disableOptionsStorage: toArray(config.disableOptionsStorage), disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false), autocomplete: toBoolean(config.autocomplete, false), @@ -73,7 +77,7 @@ export const apply = (config) => { ignoreDomainNames: toBoolean(config.ignoreDomainNames, true), ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true), ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true), - globalBadge: toBoolean(config.globalBadge, true), + globalBadge: toBoolean(config.globalBadge, false), compactBadge: toBoolean(config.compactBadge, true), allSuggestionsMode: toBoolean(config.allSuggestionsMode, true), onLoad: function() { diff --git a/classes/local/config_builder.php b/classes/local/config_builder.php index 6a40169..c917cb4 100644 --- a/classes/local/config_builder.php +++ b/classes/local/config_builder.php @@ -68,7 +68,7 @@ public static function build(): array { 'enableGrammar' => $grammar, 'aiWritingAssistant' => $aiassistant, 'enableBadgeButton' => $badgeenabled, - 'globalBadge' => self::is_page_corner_badge(), + 'globalBadge' => false, 'compactBadge' => true, 'autocomplete' => $autocomplete, 'allSuggestionsMode' => false, @@ -89,6 +89,10 @@ public static function build(): array { ? ['addWord', 'ignoreAll', 'toggle', 'proofreadDialog'] : ['addWord', 'ignoreAll', 'proofreadDialog'], 'disableAutoSearchIn' => self::excluded_selectors(), + 'bundleLoadErrorMessage' => get_string('bundle_load_error', 'local_wproofreader'), + 'editorAttachErrorMessage' => get_string('editor_attach_error', 'local_wproofreader'), + 'runtimeServiceUnavailableMessage' => get_string('runtime_service_unavailable', 'local_wproofreader'), + 'runtimeLanguageUnsupportedMessage' => get_string('runtime_language_unsupported', 'local_wproofreader'), ]; if ($isfree) { @@ -151,18 +155,6 @@ public static function is_free_edition(?string $serviceid = null): bool { return $serviceid === self::TRIAL_SERVICE_ID; } - /** - * Whether the badge should render as a single page-corner element. - * - * Defaults to true so existing installs that have not yet seen the - * `badge_placement` setting keep their previous behavior. - * - * @return bool - */ - public static function is_page_corner_badge(): bool { - return (string) get_config('local_wproofreader', 'badge_placement') !== 'per_editor'; - } - /** * Whether the WProofreader badge should be shown. * diff --git a/lang/en/local_wproofreader.php b/lang/en/local_wproofreader.php index 611c358..f6914b4 100644 --- a/lang/en/local_wproofreader.php +++ b/lang/en/local_wproofreader.php @@ -24,10 +24,8 @@ defined('MOODLE_INTERNAL') || die(); -$string['badge_placement'] = 'Badge placement'; -$string['badge_placement_desc'] = 'Where to render the WProofreader badge. "Page corner" shows a single floating badge in the bottom-right corner of the page that controls all editors. "Per editor" attaches a separate badge to each editor.'; -$string['badge_placement_page_corner'] = 'Page corner'; -$string['badge_placement_per_editor'] = 'Per editor'; +$string['bundle_load_error'] = '⚠️ WProofreader service is temporarily unavailable. Reload the page or try again later.'; +$string['editor_attach_error'] = '⚠️ WProofreader could not start on this editor. Reload the page or try again later.'; $string['enable_ai_writing_assistant'] = 'AI writing assistant'; $string['enable_ai_writing_assistant_desc'] = 'Offer rephrasing and tone suggestions powered by the AI writing assistant. Requires a paid license.'; $string['enable_autocomplete'] = 'Text autocomplete'; @@ -71,6 +69,8 @@ $string['privacy:metadata:wproofreader_service:language'] = 'The language code selected for proofreading.'; $string['privacy:metadata:wproofreader_service:useragent'] = 'Browser user agent string sent with each request to the service.'; $string['privacy:metadata:wproofreader_service:userip'] = 'The IP address from which the browser contacts the WebSpellChecker service.'; +$string['runtime_language_unsupported'] = '⚠️ WProofreader: selected language is not supported. Please report this issue to the site administrator.'; +$string['runtime_service_unavailable'] = '⚠️ WProofreader service is temporarily unavailable. Try again in a moment.'; $string['service_id'] = 'License key'; $string['service_id_desc'] = 'Paste the license key delivered with your Moodle Marketplace purchase. Leave empty to use the free version. The license key lifts the daily usage limitation, unlocks enhanced grammar checking, the AI writing assistant, access to custom dictionaries and style guide functionality.'; $string['service_id_invalid'] = '⚠️ Provided license key is invalid. Contact WProofreader support for assistance.'; @@ -85,6 +85,6 @@ $string['settings_ignore'] = 'Spelling ignore options'; $string['settings_ignore_desc'] = 'Skip certain word patterns during spell checking. These defaults apply site-wide; users can adjust them per editor from the badge menu.'; $string['show_badge_button'] = 'Show badge button'; -$string['show_badge_button_desc'] = 'Display the orange WProofreader badge. The badge gives quick access to settings and the proofreading dialog. Use the "Badge placement" setting below to control where it appears.'; +$string['show_badge_button_desc'] = 'Display the orange WProofreader badge. The badge gives quick access to settings and the proofreading dialog.'; $string['usage_limit_exceeded'] = '⚠️ The WProofreader usage limit for your license key has been exceeded. Contact WProofreader support for assistance.'; $string['wproofreader:use'] = 'Use WProofreader on editor content'; diff --git a/settings.php b/settings.php index d0fa16f..9dcc5a8 100644 --- a/settings.php +++ b/settings.php @@ -64,17 +64,6 @@ 1 )); - $settings->add(new admin_setting_configselect( - 'local_wproofreader/badge_placement', - get_string('badge_placement', 'local_wproofreader'), - get_string('badge_placement_desc', 'local_wproofreader'), - 'page_corner', - [ - 'page_corner' => get_string('badge_placement_page_corner', 'local_wproofreader'), - 'per_editor' => get_string('badge_placement_per_editor', 'local_wproofreader'), - ] - )); - // Proofreading features section. $settings->add(new admin_setting_heading( 'local_wproofreader/heading_features', diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..420f3e6 --- /dev/null +++ b/styles.css @@ -0,0 +1,29 @@ +/* This file is part of Moodle - https://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 . + */ + +/* + * Informational per-field warning note, styled like Moodle's own + * .invalid-feedback but in orange rather than red, since it is not a form + * validation error and must not read as one. + */ +.wsc-field-warning { + display: block; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + font-weight: bold; + color: #f0ad4e; +} From 19623848160ca4fba75d5e5873f6e7d3b13ff570 Mon Sep 17 00:00:00 2001 From: Artem Okladov Date: Thu, 20 Aug 2026 11:31:33 +0300 Subject: [PATCH 2/4] feat: Add support for Moodle 4.1-4.4, Add display of HTTP errors --- README.md | 24 +-- amd/build/environment_atto.min.js | 4 +- amd/build/environment_atto.min.js.map | 2 +- amd/build/environment_textarea.min.js | 4 +- amd/build/environment_textarea.min.js.map | 2 +- amd/build/environment_tinymce.min.js | 2 +- amd/build/environment_tinymce.min.js.map | 2 +- amd/build/environment_tinymce_legacy.min.js | 17 ++ .../environment_tinymce_legacy.min.js.map | 1 + amd/build/init.min.js | 4 +- amd/build/init.min.js.map | 2 +- amd/build/notify.min.js | 27 ++- amd/build/notify.min.js.map | 2 +- amd/build/proofreader_config.min.js | 4 +- amd/build/proofreader_config.min.js.map | 2 +- amd/src/environment_atto.js | 12 +- amd/src/environment_textarea.js | 22 ++- amd/src/environment_tinymce.js | 12 +- amd/src/environment_tinymce_legacy.js | 159 ++++++++++++++++++ amd/src/init.js | 8 +- amd/src/notify.js | 124 ++++++++------ amd/src/proofreader_config.js | 29 ++++ classes/external/save_languages.php | 30 ++-- classes/local/config_builder.php | 5 + lang/en/local_wproofreader.php | 7 +- version.php | 4 +- 26 files changed, 390 insertions(+), 121 deletions(-) create mode 100644 amd/build/environment_tinymce_legacy.min.js create mode 100644 amd/build/environment_tinymce_legacy.min.js.map create mode 100644 amd/src/environment_tinymce_legacy.js diff --git a/README.md b/README.md index f146c38..0d71e12 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # WProofreader plugin for Moodle WProofreader is a real-time spelling, grammar, and style checker for editor -content in Moodle. It works inside the Atto editor (Moodle 4.5 LTS), TinyMCE 6, -and plain HTML textareas, with no per-editor configuration required. +content in Moodle. It works inside the Atto editor (Moodle 4.1 through 4.5 LTS), +TinyMCE 6, the legacy TinyMCE editor (Moodle 4.1 only), and plain HTML +textareas, with no per-editor configuration required. ## Features @@ -33,19 +34,20 @@ WProofreader > License key*. ## Supported Moodle versions -* Moodle 4.5 LTS (2024100700) and any later release up to the 5.2 branch -* Atto support is only relevant on 4.5. From 5.0 onward Atto is removed and the - Atto integration becomes a no-op automatically. +* Moodle 4.1 (2022112800) through the 5.2 branch +* Atto is relevant on 4.1 through 4.5 LTS. From 5.0 onward Atto is removed from + core and the Atto integration becomes a no-op automatically. * On Moodle 5.x the plugin installs under `public/local/wproofreader/` (Moodle's - 5.0+ web-root split). On 4.5 it installs under `local/wproofreader/`. + 5.0+ web-root split). On 4.1 through 4.5 it installs under `local/wproofreader/`. ## Supported editors -| Editor | How it is detected | -|-----------------|------------------------------------------------------------------| -| Atto | `.editor_atto_content[contenteditable="true"]` in the main DOM | -| TinyMCE 6 | `window.tinymce.editors`, hooked on each editor's `init` event | -| Plain textareas | Bundle's `autoSearch` plus a small list of code-field exclusions | +| Editor | How it is detected | +|---------------------|-----------------------------------------------------------------------| +| Atto | `.editor_atto_content[contenteditable="true"]` in the main DOM | +| TinyMCE 6 | `window.tinymce.editors`, hooked on each editor's `init` event | +| Legacy TinyMCE (4.1 only) | `window.tinymce.editors` (same global as TinyMCE 6, told apart by API shape), hooked on each editor's `onInit` event | +| Plain textareas | Attached explicitly (autoSearch disabled for textareas); a small list of code-field exclusions is skipped, along with any textarea already managed by a TinyMCE instance | ## Installation diff --git a/amd/build/environment_atto.min.js b/amd/build/environment_atto.min.js index a5b53aa..cff2c7f 100644 --- a/amd/build/environment_atto.min.js +++ b/amd/build/environment_atto.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/environment_atto",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +define("local_wproofreader/environment_atto",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyUnavailable=_exports.init=void 0; /** * Atto editor integration. * @@ -6,6 +6,6 @@ define("local_wproofreader/environment_atto",["exports","local_wproofreader/noti * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const SELECTORS=['.editor_atto_content[contenteditable="true"]','.editor_atto [contenteditable="true"]'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const createInstance=element=>{if(element&&element.isContentEditable&&!(element=>element.hasAttribute(_constants.ATTACHED_ATTR))(element)&&window.WEBSPELLCHECKER){element.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:element})}catch(e){element.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to Atto editor",e),(0,_notify.notifyField)(element,attachErrorMessage)}}},scanAndInit=()=>{(()=>{const seen=new Set;return SELECTORS.forEach((selector=>{document.querySelectorAll(selector).forEach((element=>seen.add(element)))})),Array.from(seen)})().forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["contenteditable"]})),scanAndInit()}})); +const SELECTORS=['.editor_atto_content[contenteditable="true"]','.editor_atto [contenteditable="true"]'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const findEditors=()=>{const seen=new Set;return SELECTORS.forEach((selector=>{document.querySelectorAll(selector).forEach((element=>seen.add(element)))})),Array.from(seen)},createInstance=element=>{if(element&&element.isContentEditable&&!(element=>element.hasAttribute(_constants.ATTACHED_ATTR))(element)&&window.WEBSPELLCHECKER){element.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:element})}catch(e){element.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to Atto editor",e),(0,_notify.notifyField)(element,attachErrorMessage)}}},scanAndInit=()=>{findEditors().forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["contenteditable"]})),scanAndInit()};_exports.notifyUnavailable=message=>(0,_notify.observeUnavailable)(findEditors,message)})); //# sourceMappingURL=environment_atto.min.js.map \ No newline at end of file diff --git a/amd/build/environment_atto.min.js.map b/amd/build/environment_atto.min.js.map index e8f14ef..1853a00 100644 --- a/amd/build/environment_atto.min.js.map +++ b/amd/build/environment_atto.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_atto.min.js","sources":["../src/environment_atto.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Atto editor integration.\n *\n * @module local_wproofreader/environment_atto\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTORS = [\n '.editor_atto_content[contenteditable=\"true\"]',\n '.editor_atto [contenteditable=\"true\"]',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findEditors = () => {\n const seen = new Set();\n SELECTORS.forEach((selector) => {\n document.querySelectorAll(selector).forEach((element) => seen.add(element));\n });\n return Array.from(seen);\n};\n\nconst isInstanceCreated = (element) => element.hasAttribute(ATTACHED_ATTR);\n\nconst createInstance = (element) => {\n if (!element || !element.isContentEditable || isInstanceCreated(element) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n element.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: element});\n } catch (e) {\n element.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to Atto editor', e);\n }\n notifyField(element, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findEditors().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['contenteditable'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the Atto editor environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["SELECTORS","observer","hookInstalled","attachErrorMessage","createInstance","element","isContentEditable","hasAttribute","ATTACHED_ATTR","isInstanceCreated","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","seen","Set","forEach","selector","document","querySelectorAll","add","Array","from","findEditors","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter"],"mappings":";;;;;;;;MA0BMA,UAAY,CACd,+CACA,6CAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAYnBC,eAAkBC,aACfA,SAAYA,QAAQC,oBAHFD,CAAAA,SAAYA,QAAQE,aAAaC,0BAGVC,CAAkBJ,UAAaK,OAAOC,iBAIpFN,QAAQO,aAAaJ,yBAAe,SAGhCE,OAAOC,gBAAgBE,KAAK,CAACC,UAAWT,UAC1C,MAAOU,GACLV,QAAQW,gBAAgBR,0BACpBE,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,gDAAiDH,2BAE7DV,QAASF,uBAIvBgB,YAAc,KA5BA,YACVC,KAAO,IAAIC,WACjBrB,UAAUsB,SAASC,WACfC,SAASC,iBAAiBF,UAAUD,SAASjB,SAAYe,KAAKM,IAAIrB,cAE/DsB,MAAMC,KAAKR,OAwBlBS,GAAcP,QAAQlB,iBAoBpB0B,gBAAkB,QAChB5B,qBAGJA,eAAgB,QAEV6B,SAAWrB,OAAOsB,6BACxBtB,OAAOsB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOpB,IAIbI,8BASaiB,SACjBjC,mBAAqBiC,QAAUA,OAAOC,0BAA4B,KAClEP,kBA1CI7B,UAAwC,oBAArBqC,mBAIvBrC,SAAW,IAAIqC,kBAAiB,KAC5BnB,iBAGJlB,SAASsC,QAAQf,SAASgB,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,sBAgCtBzB"} \ No newline at end of file +{"version":3,"file":"environment_atto.min.js","sources":["../src/environment_atto.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Atto editor integration.\n *\n * @module local_wproofreader/environment_atto\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField, observeUnavailable} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTORS = [\n '.editor_atto_content[contenteditable=\"true\"]',\n '.editor_atto [contenteditable=\"true\"]',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findEditors = () => {\n const seen = new Set();\n SELECTORS.forEach((selector) => {\n document.querySelectorAll(selector).forEach((element) => seen.add(element));\n });\n return Array.from(seen);\n};\n\nconst isInstanceCreated = (element) => element.hasAttribute(ATTACHED_ATTR);\n\nconst createInstance = (element) => {\n if (!element || !element.isContentEditable || isInstanceCreated(element) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n element.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: element});\n } catch (e) {\n element.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to Atto editor', e);\n }\n notifyField(element, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findEditors().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['contenteditable'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the Atto editor environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n\n/**\n * Show a warning next to every Atto editor on the page, now and for any that\n * appear later, without attaching an instance. Used when the WProofreader\n * bundle itself failed to load, so no instance can ever attach to report\n * per-field problems itself.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyUnavailable = (message) => observeUnavailable(findEditors, message);\n"],"names":["SELECTORS","observer","hookInstalled","attachErrorMessage","findEditors","seen","Set","forEach","selector","document","querySelectorAll","element","add","Array","from","createInstance","isContentEditable","hasAttribute","ATTACHED_ATTR","isInstanceCreated","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter","message"],"mappings":";;;;;;;;MA0BMA,UAAY,CACd,+CACA,6CAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAEnBC,YAAc,WACVC,KAAO,IAAIC,WACjBN,UAAUO,SAASC,WACfC,SAASC,iBAAiBF,UAAUD,SAASI,SAAYN,KAAKO,IAAID,cAE/DE,MAAMC,KAAKT,OAKhBU,eAAkBJ,aACfA,SAAYA,QAAQK,oBAHFL,CAAAA,SAAYA,QAAQM,aAAaC,0BAGVC,CAAkBR,UAAaS,OAAOC,iBAIpFV,QAAQW,aAAaJ,yBAAe,SAGhCE,OAAOC,gBAAgBE,KAAK,CAACC,UAAWb,UAC1C,MAAOc,GACLd,QAAQe,gBAAgBR,0BACpBE,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,gDAAiDH,2BAE7Dd,QAASR,uBAIvB0B,YAAc,KAChBzB,cAAcG,QAAQQ,iBAoBpBe,gBAAkB,QAChB5B,qBAGJA,eAAgB,QAEV6B,SAAWX,OAAOY,6BACxBZ,OAAOY,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOV,IAIbI,8BASaO,SACjBjC,mBAAqBiC,QAAUA,OAAOC,0BAA4B,KAClEP,kBA1CI7B,UAAwC,oBAArBqC,mBAIvBrC,SAAW,IAAIqC,kBAAiB,KAC5BT,iBAGJ5B,SAASsC,QAAQ9B,SAAS+B,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,sBAgCtBf,0CAW8BgB,UAAY,8BAAmBzC,YAAayC"} \ No newline at end of file diff --git a/amd/build/environment_textarea.min.js b/amd/build/environment_textarea.min.js index 37d2594..7c4e76f 100644 --- a/amd/build/environment_textarea.min.js +++ b/amd/build/environment_textarea.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/environment_textarea",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +define("local_wproofreader/environment_textarea",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyUnavailable=_exports.init=void 0; /** * Plain HTML textarea integration. * @@ -11,6 +11,6 @@ define("local_wproofreader/environment_textarea",["exports","local_wproofreader/ * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const CODE_FIELD_HINTS=['id$="customcss"','id$="custommenuitems"','name="customcss"','name="s_theme_boost_customcss"','name$="[customcss]"'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const createInstance=textarea=>{if(textarea&&!textarea.hasAttribute(_constants.ATTACHED_ATTR)&&((element=textarea).offsetWidth||element.offsetHeight||element.getClientRects().length)&&window.WEBSPELLCHECKER){var element;textarea.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:textarea})}catch(e){textarea.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to textarea",e),(0,_notify.notifyField)(textarea,attachErrorMessage)}}},scanAndInit=()=>{Array.from(document.querySelectorAll("textarea")).filter((textarea=>!textarea.classList.contains("wsc-skip-autosearch"))).forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,(()=>{const selectors=CODE_FIELD_HINTS.map((hint=>"textarea[".concat(hint,"]"))).join(",");document.querySelectorAll(selectors).forEach((textarea=>{textarea.classList.add("wsc-skip-autosearch")}))})(),hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["class"]})),scanAndInit()}})); +const CODE_FIELD_HINTS=['id$="customcss"','id$="custommenuitems"','name="customcss"','name="s_theme_boost_customcss"','name$="[customcss]"'];let observer=null,hookInstalled=!1,attachErrorMessage=null;const tagSkippableTextareas=()=>{const selectors=CODE_FIELD_HINTS.map((hint=>"textarea[".concat(hint,"]"))).join(",");document.querySelectorAll(selectors).forEach((textarea=>{textarea.classList.add("wsc-skip-autosearch")}))},findTextareas=()=>Array.from(document.querySelectorAll("textarea")).filter((textarea=>!textarea.classList.contains("wsc-skip-autosearch")&&!(textarea=>!!textarea.id&&!!window.tinymce&&"function"==typeof window.tinymce.get&&!!window.tinymce.get(textarea.id))(textarea))),createInstance=textarea=>{if(textarea&&!textarea.hasAttribute(_constants.ATTACHED_ATTR)&&((element=textarea).offsetWidth||element.offsetHeight||element.getClientRects().length)&&window.WEBSPELLCHECKER){var element;textarea.setAttribute(_constants.ATTACHED_ATTR,"1");try{window.WEBSPELLCHECKER.init({container:textarea})}catch(e){textarea.removeAttribute(_constants.ATTACHED_ATTR),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to textarea",e),(0,_notify.notifyField)(textarea,attachErrorMessage)}}},scanAndInit=()=>{findTextareas().forEach(createInstance)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,tagSkippableTextareas(),hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["class"]})),scanAndInit()};_exports.notifyUnavailable=message=>{tagSkippableTextareas(),(0,_notify.observeUnavailable)(findTextareas,message)}})); //# sourceMappingURL=environment_textarea.min.js.map \ No newline at end of file diff --git a/amd/build/environment_textarea.min.js.map b/amd/build/environment_textarea.min.js.map index 3276a30..c8da47b 100644 --- a/amd/build/environment_textarea.min.js.map +++ b/amd/build/environment_textarea.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_textarea.min.js","sources":["../src/environment_textarea.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Plain HTML textarea integration.\n *\n * Attaches explicitly instead of relying on the bundle's own autoSearch\n * (disabled for textareas in proofreader_config.js), since autoSearch only\n * scans once at load and misses fields revealed later, such as an mform\n * \"Show more\" advanced section.\n *\n * @module local_wproofreader/environment_textarea\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SKIP_CLASS = 'wsc-skip-autosearch';\nconst CODE_FIELD_HINTS = [\n 'id$=\"customcss\"',\n 'id$=\"custommenuitems\"',\n 'name=\"customcss\"',\n 'name=\"s_theme_boost_customcss\"',\n 'name$=\"[customcss]\"',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst tagSkippableTextareas = () => {\n const selectors = CODE_FIELD_HINTS.map((hint) => `textarea[${hint}]`).join(',');\n\n document.querySelectorAll(selectors).forEach((textarea) => {\n textarea.classList.add(SKIP_CLASS);\n });\n};\n\nconst isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);\n\nconst findTextareas = () => Array.from(document.querySelectorAll('textarea'))\n .filter((textarea) => !textarea.classList.contains(SKIP_CLASS));\n\nconst createInstance = (textarea) => {\n if (!textarea || textarea.hasAttribute(ATTACHED_ATTR) || !isVisible(textarea) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n textarea.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: textarea});\n } catch (e) {\n textarea.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to textarea', e);\n }\n notifyField(textarea, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findTextareas().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['class'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the textarea environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n tagSkippableTextareas();\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["CODE_FIELD_HINTS","observer","hookInstalled","attachErrorMessage","createInstance","textarea","hasAttribute","ATTACHED_ATTR","element","offsetWidth","offsetHeight","getClientRects","length","window","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","Array","from","document","querySelectorAll","filter","classList","contains","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","selectors","map","hint","join","add","tagSkippableTextareas","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter"],"mappings":";;;;;;;;;;;;;MAgCMA,iBAAmB,CACrB,kBACA,wBACA,mBACA,iCACA,2BAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAenBC,eAAkBC,cACfA,WAAYA,SAASC,aAAaC,6BANxBC,QAMqDH,UAN9BI,aAAeD,QAAQE,cAAgBF,QAAQG,iBAAiBC,SAMpBC,OAAOC,iBAN1EN,IAAAA,QAUfH,SAASU,aAAaR,yBAAe,SAGjCM,OAAOC,gBAAgBE,KAAK,CAACC,UAAWZ,WAC1C,MAAOa,GACLb,SAASc,gBAAgBZ,0BACrBM,OAAOO,SAAWP,OAAOO,QAAQC,MACjCR,OAAOO,QAAQC,KAAK,6CAA8CH,2BAE1Db,SAAUF,uBAIxBmB,YAAc,KArBQC,MAAMC,KAAKC,SAASC,iBAAiB,aAC5DC,QAAQtB,WAAcA,SAASuB,UAAUC,SAxB3B,yBA6CCC,QAAQ1B,iBAoBtB2B,gBAAkB,QAChB7B,qBAGJA,eAAgB,QAEV8B,SAAWnB,OAAOoB,6BACxBpB,OAAOoB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOlB,IAIbI,8BASae,SACjBlC,mBAAqBkC,QAAUA,OAAOC,0BAA4B,KA7ExC,YACpBC,UAAYvC,iBAAiBwC,KAAKC,yBAAqBA,YAASC,KAAK,KAE3EjB,SAASC,iBAAiBa,WAAWT,SAASzB,WAC1CA,SAASuB,UAAUe,IAjBR,2BA2FfC,GACAb,kBA3CI9B,UAAwC,oBAArB4C,mBAIvB5C,SAAW,IAAI4C,kBAAiB,KAC5BvB,iBAGJrB,SAAS6C,QAAQrB,SAASsB,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,YAiCtB7B"} \ No newline at end of file +{"version":3,"file":"environment_textarea.min.js","sources":["../src/environment_textarea.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Plain HTML textarea integration.\n *\n * Attaches explicitly instead of relying on the bundle's own autoSearch\n * (disabled for textareas in proofreader_config.js), since autoSearch only\n * scans once at load and misses fields revealed later, such as an mform\n * \"Show more\" advanced section.\n *\n * @module local_wproofreader/environment_textarea\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField, observeUnavailable} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SKIP_CLASS = 'wsc-skip-autosearch';\nconst CODE_FIELD_HINTS = [\n 'id$=\"customcss\"',\n 'id$=\"custommenuitems\"',\n 'name=\"customcss\"',\n 'name=\"s_theme_boost_customcss\"',\n 'name$=\"[customcss]\"',\n];\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst tagSkippableTextareas = () => {\n const selectors = CODE_FIELD_HINTS.map((hint) => `textarea[${hint}]`).join(',');\n\n document.querySelectorAll(selectors).forEach((textarea) => {\n textarea.classList.add(SKIP_CLASS);\n });\n};\n\nconst isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);\n\nconst isManagedByTinymce = (textarea) => !!textarea.id\n && !!window.tinymce\n && typeof window.tinymce.get === 'function'\n && !!window.tinymce.get(textarea.id);\n\nconst findTextareas = () => Array.from(document.querySelectorAll('textarea'))\n .filter((textarea) => !textarea.classList.contains(SKIP_CLASS) && !isManagedByTinymce(textarea));\n\nconst createInstance = (textarea) => {\n if (!textarea || textarea.hasAttribute(ATTACHED_ATTR) || !isVisible(textarea) || !window.WEBSPELLCHECKER) {\n return;\n }\n\n textarea.setAttribute(ATTACHED_ATTR, '1');\n\n try {\n window.WEBSPELLCHECKER.init({container: textarea});\n } catch (e) {\n textarea.removeAttribute(ATTACHED_ATTR);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to textarea', e);\n }\n notifyField(textarea, attachErrorMessage);\n }\n};\n\nconst scanAndInit = () => {\n findTextareas().forEach(createInstance);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['class'],\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the textarea environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n tagSkippableTextareas();\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n\n/**\n * Show a warning next to every plain textarea on the page, now and for any\n * that appear later, without attaching an instance. Used when the\n * WProofreader bundle itself failed to load, so no instance can ever attach\n * to report per-field problems itself.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyUnavailable = (message) => {\n tagSkippableTextareas();\n observeUnavailable(findTextareas, message);\n};\n"],"names":["CODE_FIELD_HINTS","observer","hookInstalled","attachErrorMessage","tagSkippableTextareas","selectors","map","hint","join","document","querySelectorAll","forEach","textarea","classList","add","findTextareas","Array","from","filter","contains","id","window","tinymce","get","isManagedByTinymce","createInstance","hasAttribute","ATTACHED_ATTR","element","offsetWidth","offsetHeight","getClientRects","length","WEBSPELLCHECKER","setAttribute","init","container","e","removeAttribute","console","warn","scanAndInit","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","body","childList","subtree","attributes","attributeFilter","message"],"mappings":";;;;;;;;;;;;;MAgCMA,iBAAmB,CACrB,kBACA,wBACA,mBACA,iCACA,2BAGAC,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAEnBC,sBAAwB,WACpBC,UAAYL,iBAAiBM,KAAKC,yBAAqBA,YAASC,KAAK,KAE3EC,SAASC,iBAAiBL,WAAWM,SAASC,WAC1CA,SAASC,UAAUC,IAjBR,2BA4BbC,cAAgB,IAAMC,MAAMC,KAAKR,SAASC,iBAAiB,aAC5DQ,QAAQN,WAAcA,SAASC,UAAUM,SA7B3B,yBAuBSP,CAAAA,YAAeA,SAASQ,MAC3CC,OAAOC,SACqB,mBAAvBD,OAAOC,QAAQC,OACpBF,OAAOC,QAAQC,IAAIX,SAASQ,IAGkCI,CAAmBZ,YAEpFa,eAAkBb,cACfA,WAAYA,SAASc,aAAaC,6BAXxBC,QAWqDhB,UAX9BiB,aAAeD,QAAQE,cAAgBF,QAAQG,iBAAiBC,SAWpBX,OAAOY,iBAX1EL,IAAAA,QAefhB,SAASsB,aAAaP,yBAAe,SAGjCN,OAAOY,gBAAgBE,KAAK,CAACC,UAAWxB,WAC1C,MAAOyB,GACLzB,SAAS0B,gBAAgBX,0BACrBN,OAAOkB,SAAWlB,OAAOkB,QAAQC,MACjCnB,OAAOkB,QAAQC,KAAK,6CAA8CH,2BAE1DzB,SAAUT,uBAIxBsC,YAAc,KAChB1B,gBAAgBJ,QAAQc,iBAoBtBiB,gBAAkB,QAChBxC,qBAGJA,eAAgB,QAEVyC,SAAWtB,OAAOuB,6BACxBvB,OAAOuB,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOV,IAIbI,8BASaO,SACjB7C,mBAAqB6C,QAAUA,OAAOC,0BAA4B,KAClE7C,wBACAsC,kBA3CIzC,UAAwC,oBAArBiD,mBAIvBjD,SAAW,IAAIiD,kBAAiB,KAC5BT,iBAGJxC,SAASkD,QAAQ1C,SAAS2C,KAAM,CAC5BC,WAAW,EACXC,SAAS,EACTC,YAAY,EACZC,gBAAiB,CAAC,YAiCtBf,0CAW8BgB,UAC9BrD,uDACmBW,cAAe0C"} \ No newline at end of file diff --git a/amd/build/environment_tinymce.min.js b/amd/build/environment_tinymce.min.js index 1d13893..5ecae32 100644 --- a/amd/build/environment_tinymce.min.js +++ b/amd/build/environment_tinymce.min.js @@ -1,3 +1,3 @@ -define("local_wproofreader/environment_tinymce",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;let observer=null,hookInstalled=!1,attachErrorMessage=null;const unmark=iframe=>iframe.removeAttribute(_constants.ATTACHED_ATTR),initInstance=iframe=>{const doc=iframe.contentDocument;if(doc&&doc.body)try{window.WEBSPELLCHECKER.init(Object.assign({},window.WEBSPELLCHECKER_CONFIG,{container:iframe}))}catch(e){unmark(iframe),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to TinyMCE editor",e),(0,_notify.notifyField)(iframe,attachErrorMessage)}else unmark(iframe)},attach=iframe=>{if(!iframe||(iframe=>iframe.hasAttribute(_constants.ATTACHED_ATTR))(iframe)||!window.WEBSPELLCHECKER||!window.WEBSPELLCHECKER_CONFIG)return;(iframe=>{iframe.setAttribute(_constants.ATTACHED_ATTR,"1")})(iframe);const editor=(iframe=>window.tinymce&&"function"==typeof window.tinymce.get&&(window.tinymce.get()||[]).find((editor=>editor&&editor.iframeElement===iframe))||null)(iframe);!editor||editor.initialized||"function"!=typeof editor.on?initInstance(iframe):editor.on("init",(()=>initInstance(iframe)))},scanAndInit=()=>{Array.from(document.querySelectorAll("iframe.tox-edit-area__iframe")).forEach(attach)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0})),scanAndInit()}})); +define("local_wproofreader/environment_tinymce",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyUnavailable=_exports.init=void 0;let observer=null,hookInstalled=!1,attachErrorMessage=null;const findIframes=()=>Array.from(document.querySelectorAll("iframe.tox-edit-area__iframe")),unmark=iframe=>iframe.removeAttribute(_constants.ATTACHED_ATTR),initInstance=iframe=>{const doc=iframe.contentDocument;if(doc&&doc.body)try{window.WEBSPELLCHECKER.init(Object.assign({},window.WEBSPELLCHECKER_CONFIG,{container:iframe}))}catch(e){unmark(iframe),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to TinyMCE editor",e),(0,_notify.notifyField)(iframe,attachErrorMessage)}else unmark(iframe)},attach=iframe=>{if(!iframe||(iframe=>iframe.hasAttribute(_constants.ATTACHED_ATTR))(iframe)||!window.WEBSPELLCHECKER||!window.WEBSPELLCHECKER_CONFIG)return;(iframe=>{iframe.setAttribute(_constants.ATTACHED_ATTR,"1")})(iframe);const editor=(iframe=>window.tinymce&&"function"==typeof window.tinymce.get&&(window.tinymce.get()||[]).find((editor=>editor&&editor.iframeElement===iframe))||null)(iframe);!editor||editor.initialized||"function"!=typeof editor.on?initInstance(iframe):editor.on("init",(()=>initInstance(iframe)))},scanAndInit=()=>{findIframes().forEach(attach)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0})),scanAndInit()};_exports.notifyUnavailable=message=>(0,_notify.observeUnavailable)(findIframes,message)})); //# sourceMappingURL=environment_tinymce.min.js.map \ No newline at end of file diff --git a/amd/build/environment_tinymce.min.js.map b/amd/build/environment_tinymce.min.js.map index edf8260..6484400 100644 --- a/amd/build/environment_tinymce.min.js.map +++ b/amd/build/environment_tinymce.min.js.map @@ -1 +1 @@ -{"version":3,"file":"environment_tinymce.min.js","sources":["../src/environment_tinymce.js"],"sourcesContent":["// This file is part of Moodle - https://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 * TinyMCE 6 editor integration.\n *\n * @module local_wproofreader/environment_tinymce\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTOR = 'iframe.tox-edit-area__iframe';\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findIframes = () => Array.from(document.querySelectorAll(SELECTOR));\n\nconst isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR);\nconst mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1');\nconst unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR);\n\n\nconst findEditor = (iframe) => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return null;\n }\n const editors = window.tinymce.get() || [];\n return editors.find((editor) => editor && editor.iframeElement === iframe) || null;\n};\n\nconst initInstance = (iframe) => {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n unmark(iframe);\n return;\n }\n\n try {\n window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, {\n container: iframe,\n }));\n } catch (e) {\n unmark(iframe);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to TinyMCE editor', e);\n }\n notifyField(iframe, attachErrorMessage);\n }\n};\n\n/**\n * Decide when to attach to a single iframe: now, or on the editor's init event.\n *\n * @param {HTMLIFrameElement} iframe\n */\nconst attach = (iframe) => {\n if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {\n return;\n }\n\n // Mark up front so repeated observer fires do not register duplicate hooks;\n // initInstance unmarks again if the editor turns out not to be ready.\n mark(iframe);\n\n const editor = findEditor(iframe);\n if (editor && !editor.initialized && typeof editor.on === 'function') {\n editor.on('init', () => initInstance(iframe));\n return;\n }\n\n initInstance(iframe);\n};\n\nconst scanAndInit = () => {\n findIframes().forEach(attach);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the TinyMCE environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n"],"names":["observer","hookInstalled","attachErrorMessage","unmark","iframe","removeAttribute","ATTACHED_ATTR","initInstance","doc","contentDocument","body","window","WEBSPELLCHECKER","init","Object","assign","WEBSPELLCHECKER_CONFIG","container","e","console","warn","attach","hasAttribute","isMarked","setAttribute","mark","editor","tinymce","get","find","iframeElement","findEditor","initialized","on","scanAndInit","Array","from","document","querySelectorAll","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","childList","subtree"],"mappings":"+OA4BIA,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAMnBC,OAAUC,QAAWA,OAAOC,gBAAgBC,0BAW5CC,aAAgBH,eACZI,IAAMJ,OAAOK,mBACdD,KAAQA,IAAIE,SAMbC,OAAOC,gBAAgBC,KAAKC,OAAOC,OAAO,GAAIJ,OAAOK,uBAAwB,CACzEC,UAAWb,UAEjB,MAAOc,GACLf,OAAOC,QACHO,OAAOQ,SAAWR,OAAOQ,QAAQC,MACjCT,OAAOQ,QAAQC,KAAK,mDAAoDF,2BAEhEd,OAAQF,yBAbpBC,OAAOC,SAsBTiB,OAAUjB,aACPA,QAvCSA,CAAAA,QAAWA,OAAOkB,aAAahB,0BAuC9BiB,CAASnB,UAAYO,OAAOC,kBAAoBD,OAAOK,8BAtC5DZ,CAAAA,SAAWA,OAAOoB,aAAalB,yBAAe,MA4CxDmB,CAAKrB,cAECsB,OA1CUtB,CAAAA,QACXO,OAAOgB,SAAyC,mBAAvBhB,OAAOgB,QAAQC,MAG7BjB,OAAOgB,QAAQC,OAAS,IACzBC,MAAMH,QAAWA,QAAUA,OAAOI,gBAAkB1B,UAHxD,KAwCI2B,CAAW3B,SACtBsB,QAAWA,OAAOM,aAAoC,mBAAdN,OAAOO,GAKnD1B,aAAaH,QAJTsB,OAAOO,GAAG,QAAQ,IAAM1B,aAAaH,WAOvC8B,YAAc,KA1DMC,MAAMC,KAAKC,SAASC,iBAN7B,iCAiECC,QAAQlB,SAkBpBmB,gBAAkB,QAChBvC,qBAGJA,eAAgB,QAEVwC,SAAW9B,OAAO+B,6BACxB/B,OAAO+B,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAO3B,IAIbgB,8BASaY,SACjB5C,mBAAqB4C,QAAUA,OAAOC,0BAA4B,KAClEP,kBAxCIxC,UAAwC,oBAArBgD,mBAIvBhD,SAAW,IAAIgD,kBAAiB,KAC5Bd,iBAGJlC,SAASiD,QAAQZ,SAAS3B,KAAM,CAC5BwC,WAAW,EACXC,SAAS,KAgCbjB"} \ No newline at end of file +{"version":3,"file":"environment_tinymce.min.js","sources":["../src/environment_tinymce.js"],"sourcesContent":["// This file is part of Moodle - https://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 * TinyMCE 6 editor integration.\n *\n * @module local_wproofreader/environment_tinymce\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField, observeUnavailable} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst SELECTOR = 'iframe.tox-edit-area__iframe';\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst findIframes = () => Array.from(document.querySelectorAll(SELECTOR));\n\nconst isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR);\nconst mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1');\nconst unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR);\n\n\nconst findEditor = (iframe) => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return null;\n }\n const editors = window.tinymce.get() || [];\n return editors.find((editor) => editor && editor.iframeElement === iframe) || null;\n};\n\nconst initInstance = (iframe) => {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n unmark(iframe);\n return;\n }\n\n try {\n window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, {\n container: iframe,\n }));\n } catch (e) {\n unmark(iframe);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to TinyMCE editor', e);\n }\n notifyField(iframe, attachErrorMessage);\n }\n};\n\n/**\n * Decide when to attach to a single iframe: now, or on the editor's init event.\n *\n * @param {HTMLIFrameElement} iframe\n */\nconst attach = (iframe) => {\n if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {\n return;\n }\n\n // Mark up front so repeated observer fires do not register duplicate hooks;\n // initInstance unmarks again if the editor turns out not to be ready.\n mark(iframe);\n\n const editor = findEditor(iframe);\n if (editor && !editor.initialized && typeof editor.on === 'function') {\n editor.on('init', () => initInstance(iframe));\n return;\n }\n\n initInstance(iframe);\n};\n\nconst scanAndInit = () => {\n findIframes().forEach(attach);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the TinyMCE environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n\n/**\n * Show a warning next to every TinyMCE 6 editor on the page, now and for any\n * that appear later, without attaching an instance. Used when the\n * WProofreader bundle itself failed to load, so no instance can ever attach\n * to report per-field problems itself.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyUnavailable = (message) => observeUnavailable(findIframes, message);\n"],"names":["observer","hookInstalled","attachErrorMessage","findIframes","Array","from","document","querySelectorAll","unmark","iframe","removeAttribute","ATTACHED_ATTR","initInstance","doc","contentDocument","body","window","WEBSPELLCHECKER","init","Object","assign","WEBSPELLCHECKER_CONFIG","container","e","console","warn","attach","hasAttribute","isMarked","setAttribute","mark","editor","tinymce","get","find","iframeElement","findEditor","initialized","on","scanAndInit","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","childList","subtree","message"],"mappings":"0QA4BIA,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAEnBC,YAAc,IAAMC,MAAMC,KAAKC,SAASC,iBAN7B,iCAUXC,OAAUC,QAAWA,OAAOC,gBAAgBC,0BAW5CC,aAAgBH,eACZI,IAAMJ,OAAOK,mBACdD,KAAQA,IAAIE,SAMbC,OAAOC,gBAAgBC,KAAKC,OAAOC,OAAO,GAAIJ,OAAOK,uBAAwB,CACzEC,UAAWb,UAEjB,MAAOc,GACLf,OAAOC,QACHO,OAAOQ,SAAWR,OAAOQ,QAAQC,MACjCT,OAAOQ,QAAQC,KAAK,mDAAoDF,2BAEhEd,OAAQP,yBAbpBM,OAAOC,SAsBTiB,OAAUjB,aACPA,QAvCSA,CAAAA,QAAWA,OAAOkB,aAAahB,0BAuC9BiB,CAASnB,UAAYO,OAAOC,kBAAoBD,OAAOK,8BAtC5DZ,CAAAA,SAAWA,OAAOoB,aAAalB,yBAAe,MA4CxDmB,CAAKrB,cAECsB,OA1CUtB,CAAAA,QACXO,OAAOgB,SAAyC,mBAAvBhB,OAAOgB,QAAQC,MAG7BjB,OAAOgB,QAAQC,OAAS,IACzBC,MAAMH,QAAWA,QAAUA,OAAOI,gBAAkB1B,UAHxD,KAwCI2B,CAAW3B,SACtBsB,QAAWA,OAAOM,aAAoC,mBAAdN,OAAOO,GAKnD1B,aAAaH,QAJTsB,OAAOO,GAAG,QAAQ,IAAM1B,aAAaH,WAOvC8B,YAAc,KAChBpC,cAAcqC,QAAQd,SAkBpBe,gBAAkB,QAChBxC,qBAGJA,eAAgB,QAEVyC,SAAW1B,OAAO2B,6BACxB3B,OAAO2B,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOvB,IAIbgB,8BASaQ,SACjB7C,mBAAqB6C,QAAUA,OAAOC,0BAA4B,KAClEP,kBAxCIzC,UAAwC,oBAArBiD,mBAIvBjD,SAAW,IAAIiD,kBAAiB,KAC5BV,iBAGJvC,SAASkD,QAAQ5C,SAASS,KAAM,CAC5BoC,WAAW,EACXC,SAAS,KAgCbb,0CAW8Bc,UAAY,8BAAmBlD,YAAakD"} \ No newline at end of file diff --git a/amd/build/environment_tinymce_legacy.min.js b/amd/build/environment_tinymce_legacy.min.js new file mode 100644 index 0000000..d6486e9 --- /dev/null +++ b/amd/build/environment_tinymce_legacy.min.js @@ -0,0 +1,17 @@ +define("local_wproofreader/environment_tinymce_legacy",["exports","local_wproofreader/notify","local_wproofreader/constants"],(function(_exports,_notify,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyUnavailable=_exports.init=void 0; +/** + * Legacy TinyMCE editor integration (editor_tinymce, TinyMCE 3.5.x, Moodle's + * "TinyMCE HTML editor (legacy)"). Distinct from environment_tinymce.js, + * which targets the newer TinyMCE 6 "tiny" editor plugin - the two ship + * different iframe markup and editor APIs, and both publish themselves as + * window.tinymce, so editor instances are told apart by feature (an + * .iframeElement property only the newer editor exposes), not by a global + * version flag. + * + * @module local_wproofreader/environment_tinymce_legacy + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +let observer=null,hookInstalled=!1,attachErrorMessage=null;const unmark=iframe=>iframe.removeAttribute(_constants.ATTACHED_ATTR),isLegacyEditor=editor=>!!editor&&!editor.iframeElement&&editor.onInit&&"function"==typeof editor.onInit.add,findLegacyEditors=()=>{if(!window.tinymce||"function"!=typeof window.tinymce.get)return[];const all=window.tinymce.get()||[];return(Array.isArray(all)?all:[]).filter(isLegacyEditor)},initInstance=iframe=>{const doc=iframe.contentDocument;if(doc&&doc.body)try{window.WEBSPELLCHECKER.init(Object.assign({},window.WEBSPELLCHECKER_CONFIG,{container:iframe}))}catch(e){unmark(iframe),window.console&&window.console.warn&&window.console.warn("WProofreader: failed to attach to legacy TinyMCE editor",e),(0,_notify.notifyField)(iframe,attachErrorMessage)}else unmark(iframe)},attach=editor=>{const iframe=document.getElementById("".concat(editor.id,"_ifr"));iframe&&!(iframe=>iframe.hasAttribute(_constants.ATTACHED_ATTR))(iframe)&&window.WEBSPELLCHECKER&&window.WEBSPELLCHECKER_CONFIG&&((iframe=>{iframe.setAttribute(_constants.ATTACHED_ATTR,"1")})(iframe),editor.initialized?initInstance(iframe):editor.onInit.add((()=>initInstance(iframe))))},scanAndInit=()=>{findLegacyEditors().forEach(attach)},hookBundleReady=()=>{if(hookInstalled)return;hookInstalled=!0;const previous=window.webspellcheckerAlreadyLoaded;window.webspellcheckerAlreadyLoaded=function(){if("function"==typeof previous)try{previous.apply(this,arguments)}catch(e){}scanAndInit()}};_exports.init=config=>{attachErrorMessage=config&&config.editorAttachErrorMessage||null,hookBundleReady(),observer||"undefined"==typeof MutationObserver||(observer=new MutationObserver((()=>{scanAndInit()})),observer.observe(document.body,{childList:!0,subtree:!0})),scanAndInit()};const findLegacyIframes=()=>findLegacyEditors().map((editor=>document.getElementById("".concat(editor.id,"_ifr")))).filter(Boolean);_exports.notifyUnavailable=message=>(0,_notify.observeUnavailable)(findLegacyIframes,message)})); + +//# sourceMappingURL=environment_tinymce_legacy.min.js.map \ No newline at end of file diff --git a/amd/build/environment_tinymce_legacy.min.js.map b/amd/build/environment_tinymce_legacy.min.js.map new file mode 100644 index 0000000..9201df4 --- /dev/null +++ b/amd/build/environment_tinymce_legacy.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"environment_tinymce_legacy.min.js","sources":["../src/environment_tinymce_legacy.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Legacy TinyMCE editor integration (editor_tinymce, TinyMCE 3.5.x, Moodle's\n * \"TinyMCE HTML editor (legacy)\"). Distinct from environment_tinymce.js,\n * which targets the newer TinyMCE 6 \"tiny\" editor plugin - the two ship\n * different iframe markup and editor APIs, and both publish themselves as\n * window.tinymce, so editor instances are told apart by feature (an\n * .iframeElement property only the newer editor exposes), not by a global\n * version flag.\n *\n * @module local_wproofreader/environment_tinymce_legacy\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField, observeUnavailable} from 'local_wproofreader/notify';\nimport {ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nlet observer = null;\nlet hookInstalled = false;\nlet attachErrorMessage = null;\n\nconst isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR);\nconst mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1');\nconst unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR);\n\nconst isLegacyEditor = (editor) => !!editor\n && !editor.iframeElement\n && editor.onInit && typeof editor.onInit.add === 'function';\n\nconst findLegacyEditors = () => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return [];\n }\n const all = window.tinymce.get() || [];\n return (Array.isArray(all) ? all : []).filter(isLegacyEditor);\n};\n\nconst initInstance = (iframe) => {\n const doc = iframe.contentDocument;\n if (!doc || !doc.body) {\n unmark(iframe);\n return;\n }\n\n try {\n window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, {\n container: iframe,\n }));\n } catch (e) {\n unmark(iframe);\n if (window.console && window.console.warn) {\n window.console.warn('WProofreader: failed to attach to legacy TinyMCE editor', e);\n }\n notifyField(iframe, attachErrorMessage);\n }\n};\n\n/**\n * Decide when to attach to a single legacy editor instance: now, or once it\n * finishes initializing.\n *\n * @param {Object} editor A tinymce.Editor instance (legacy API).\n */\nconst attach = (editor) => {\n const iframe = document.getElementById(`${editor.id}_ifr`);\n if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) {\n return;\n }\n\n // Mark up front so repeated observer fires do not register duplicate hooks;\n // initInstance unmarks again if the editor turns out not to be ready.\n mark(iframe);\n\n if (!editor.initialized) {\n editor.onInit.add(() => initInstance(iframe));\n return;\n }\n\n initInstance(iframe);\n};\n\nconst scanAndInit = () => {\n findLegacyEditors().forEach(attach);\n};\n\nconst startObserver = () => {\n if (observer || typeof MutationObserver === 'undefined') {\n return;\n }\n\n observer = new MutationObserver(() => {\n scanAndInit();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n });\n};\n\nconst hookBundleReady = () => {\n if (hookInstalled) {\n return;\n }\n hookInstalled = true;\n\n const previous = window.webspellcheckerAlreadyLoaded;\n window.webspellcheckerAlreadyLoaded = function() {\n if (typeof previous === 'function') {\n try {\n previous.apply(this, arguments);\n } catch (e) {\n // Preserve original callback contract on failure.\n }\n }\n scanAndInit();\n };\n};\n\n/**\n * Initialize the legacy TinyMCE environment.\n *\n * @param {Object} config Page configuration.\n */\nexport const init = (config) => {\n attachErrorMessage = config && config.editorAttachErrorMessage || null;\n hookBundleReady();\n startObserver();\n scanAndInit();\n};\n\nconst findLegacyIframes = () => findLegacyEditors()\n .map((editor) => document.getElementById(`${editor.id}_ifr`))\n .filter(Boolean);\n\n/**\n * Show a warning next to every legacy TinyMCE editor on the page, now and\n * for any that appear later, without attaching an instance. Used when the\n * WProofreader bundle itself failed to load, so no instance can ever attach\n * to report per-field problems itself.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyUnavailable = (message) => observeUnavailable(findLegacyIframes, message);\n"],"names":["observer","hookInstalled","attachErrorMessage","unmark","iframe","removeAttribute","ATTACHED_ATTR","isLegacyEditor","editor","iframeElement","onInit","add","findLegacyEditors","window","tinymce","get","all","Array","isArray","filter","initInstance","doc","contentDocument","body","WEBSPELLCHECKER","init","Object","assign","WEBSPELLCHECKER_CONFIG","container","e","console","warn","attach","document","getElementById","id","hasAttribute","isMarked","setAttribute","mark","initialized","scanAndInit","forEach","hookBundleReady","previous","webspellcheckerAlreadyLoaded","apply","this","arguments","config","editorAttachErrorMessage","MutationObserver","observe","childList","subtree","findLegacyIframes","map","Boolean","message"],"mappings":";;;;;;;;;;;;;;IAgCIA,SAAW,KACXC,eAAgB,EAChBC,mBAAqB,WAInBC,OAAUC,QAAWA,OAAOC,gBAAgBC,0BAE5CC,eAAkBC,UAAaA,SAC7BA,OAAOC,eACRD,OAAOE,QAAuC,mBAAtBF,OAAOE,OAAOC,IAEvCC,kBAAoB,SACjBC,OAAOC,SAAyC,mBAAvBD,OAAOC,QAAQC,UAClC,SAELC,IAAMH,OAAOC,QAAQC,OAAS,UAC5BE,MAAMC,QAAQF,KAAOA,IAAM,IAAIG,OAAOZ,iBAG5Ca,aAAgBhB,eACZiB,IAAMjB,OAAOkB,mBACdD,KAAQA,IAAIE,SAMbV,OAAOW,gBAAgBC,KAAKC,OAAOC,OAAO,GAAId,OAAOe,uBAAwB,CACzEC,UAAWzB,UAEjB,MAAO0B,GACL3B,OAAOC,QACHS,OAAOkB,SAAWlB,OAAOkB,QAAQC,MACjCnB,OAAOkB,QAAQC,KAAK,0DAA2DF,2BAEvE1B,OAAQF,yBAbpBC,OAAOC,SAuBT6B,OAAUzB,eACNJ,OAAS8B,SAASC,yBAAkB3B,OAAO4B,YAC5ChC,SA5CSA,CAAAA,QAAWA,OAAOiC,aAAa/B,0BA4C9BgC,CAASlC,SAAYS,OAAOW,iBAAoBX,OAAOe,yBA3C5DxB,CAAAA,SAAWA,OAAOmC,aAAajC,yBAAe,MAiDxDkC,CAAKpC,QAEAI,OAAOiC,YAKZrB,aAAahB,QAJTI,OAAOE,OAAOC,KAAI,IAAMS,aAAahB,YAOvCsC,YAAc,KAChB9B,oBAAoB+B,QAAQV,SAkB1BW,gBAAkB,QAChB3C,qBAGJA,eAAgB,QAEV4C,SAAWhC,OAAOiC,6BACxBjC,OAAOiC,6BAA+B,cACV,mBAAbD,aAEHA,SAASE,MAAMC,KAAMC,WACvB,MAAOnB,IAIbY,8BASaQ,SACjBhD,mBAAqBgD,QAAUA,OAAOC,0BAA4B,KAClEP,kBAxCI5C,UAAwC,oBAArBoD,mBAIvBpD,SAAW,IAAIoD,kBAAiB,KAC5BV,iBAGJ1C,SAASqD,QAAQnB,SAASX,KAAM,CAC5B+B,WAAW,EACXC,SAAS,KAgCbb,qBAGEc,kBAAoB,IAAM5C,oBAC3B6C,KAAKjD,QAAW0B,SAASC,yBAAkB3B,OAAO4B,cAClDjB,OAAOuC,oCAUsBC,UAAY,8BAAmBH,kBAAmBG"} \ No newline at end of file diff --git a/amd/build/init.min.js b/amd/build/init.min.js index d437eec..39b41d4 100644 --- a/amd/build/init.min.js +++ b/amd/build/init.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/init",["exports","local_wproofreader/notify","local_wproofreader/proofreader_config","local_wproofreader/environment_atto","local_wproofreader/environment_tinymce","local_wproofreader/environment_textarea","local_wproofreader/bundle_loader","local_wproofreader/bundle_warnings"],(function(_exports,_notify,Config,AttoEnv,TinyMceEnv,TextareaEnv,_bundle_loader,_bundle_warnings){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +define("local_wproofreader/init",["exports","local_wproofreader/proofreader_config","local_wproofreader/environment_atto","local_wproofreader/environment_tinymce","local_wproofreader/environment_tinymce_legacy","local_wproofreader/environment_textarea","local_wproofreader/bundle_loader","local_wproofreader/bundle_warnings"],(function(_exports,Config,AttoEnv,TinyMceEnv,TinyMceLegacyEnv,TextareaEnv,_bundle_loader,_bundle_warnings){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} /** * WProofreader main entry point. * @@ -8,6 +8,6 @@ define("local_wproofreader/init",["exports","local_wproofreader/notify","local_w * @module local_wproofreader/init * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Config=_interopRequireWildcard(Config),AttoEnv=_interopRequireWildcard(AttoEnv),TinyMceEnv=_interopRequireWildcard(TinyMceEnv),TextareaEnv=_interopRequireWildcard(TextareaEnv);_exports.init=async()=>{const config=window.WPROOFREADER_BOOTSTRAP;config&&"object"==typeof config&&(Config.apply(config),await(0,_bundle_loader.loadBundle)(config.bundleUrl),window.WEBSPELLCHECKER?((0,_bundle_warnings.install)(config),TextareaEnv.init(config),AttoEnv.init(config),TinyMceEnv.init(config)):(0,_notify.showWarning)(config.bundleLoadErrorMessage))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Config=_interopRequireWildcard(Config),AttoEnv=_interopRequireWildcard(AttoEnv),TinyMceEnv=_interopRequireWildcard(TinyMceEnv),TinyMceLegacyEnv=_interopRequireWildcard(TinyMceLegacyEnv),TextareaEnv=_interopRequireWildcard(TextareaEnv);_exports.init=async()=>{const config=window.WPROOFREADER_BOOTSTRAP;if(config&&"object"==typeof config){if(Config.apply(config),await(0,_bundle_loader.loadBundle)(config.bundleUrl),!window.WEBSPELLCHECKER)return TextareaEnv.notifyUnavailable(config.bundleLoadErrorMessage),AttoEnv.notifyUnavailable(config.bundleLoadErrorMessage),TinyMceEnv.notifyUnavailable(config.bundleLoadErrorMessage),void TinyMceLegacyEnv.notifyUnavailable(config.bundleLoadErrorMessage);(0,_bundle_warnings.install)(config),TextareaEnv.init(config),AttoEnv.init(config),TinyMceEnv.init(config),TinyMceLegacyEnv.init(config)}}})); //# sourceMappingURL=init.min.js.map \ No newline at end of file diff --git a/amd/build/init.min.js.map b/amd/build/init.min.js.map index be84be6..f076cf9 100644 --- a/amd/build/init.min.js.map +++ b/amd/build/init.min.js.map @@ -1 +1 @@ -{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - https://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 * WProofreader main entry point.\n *\n * Applies global configuration, dynamically loads the WProofreader JS library,\n * and dispatches to the editor-specific environments once the bundle is ready.\n *\n * @module local_wproofreader/init\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {showWarning} from 'local_wproofreader/notify';\nimport * as Config from 'local_wproofreader/proofreader_config';\nimport * as AttoEnv from 'local_wproofreader/environment_atto';\nimport * as TinyMceEnv from 'local_wproofreader/environment_tinymce';\nimport * as TextareaEnv from 'local_wproofreader/environment_textarea';\nimport {loadBundle} from 'local_wproofreader/bundle_loader';\nimport {install as installBundleWarnings} from 'local_wproofreader/bundle_warnings';\n\n/**\n * Boot WProofreader on the current page.\n *\n * @returns {Promise}\n */\nexport const init = async() => {\n const config = window.WPROOFREADER_BOOTSTRAP;\n if (!config || typeof config !== 'object') {\n return;\n }\n\n Config.apply(config);\n await loadBundle(config.bundleUrl);\n\n if (!window.WEBSPELLCHECKER) {\n showWarning(config.bundleLoadErrorMessage);\n return;\n }\n\n installBundleWarnings(config);\n\n TextareaEnv.init(config);\n AttoEnv.init(config);\n TinyMceEnv.init(config);\n};\n"],"names":["async","config","window","WPROOFREADER_BOOTSTRAP","Config","apply","bundleUrl","WEBSPELLCHECKER","TextareaEnv","init","AttoEnv","TinyMceEnv","bundleLoadErrorMessage"],"mappings":";;;;;;;;;;gRAuCoBA,gBACVC,OAASC,OAAOC,uBACjBF,QAA4B,iBAAXA,SAItBG,OAAOC,MAAMJ,cACP,6BAAWA,OAAOK,WAEnBJ,OAAOK,8CAKUN,QAEtBO,YAAYC,KAAKR,QACjBS,QAAQD,KAAKR,QACbU,WAAWF,KAAKR,iCARAA,OAAOW"} \ No newline at end of file +{"version":3,"file":"init.min.js","sources":["../src/init.js"],"sourcesContent":["// This file is part of Moodle - https://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 * WProofreader main entry point.\n *\n * Applies global configuration, dynamically loads the WProofreader JS library,\n * and dispatches to the editor-specific environments once the bundle is ready.\n *\n * @module local_wproofreader/init\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Config from 'local_wproofreader/proofreader_config';\nimport * as AttoEnv from 'local_wproofreader/environment_atto';\nimport * as TinyMceEnv from 'local_wproofreader/environment_tinymce';\nimport * as TinyMceLegacyEnv from 'local_wproofreader/environment_tinymce_legacy';\nimport * as TextareaEnv from 'local_wproofreader/environment_textarea';\nimport {loadBundle} from 'local_wproofreader/bundle_loader';\nimport {install as installBundleWarnings} from 'local_wproofreader/bundle_warnings';\n\n/**\n * Boot WProofreader on the current page.\n *\n * @returns {Promise}\n */\nexport const init = async() => {\n const config = window.WPROOFREADER_BOOTSTRAP;\n if (!config || typeof config !== 'object') {\n return;\n }\n\n Config.apply(config);\n await loadBundle(config.bundleUrl);\n\n if (!window.WEBSPELLCHECKER) {\n TextareaEnv.notifyUnavailable(config.bundleLoadErrorMessage);\n AttoEnv.notifyUnavailable(config.bundleLoadErrorMessage);\n TinyMceEnv.notifyUnavailable(config.bundleLoadErrorMessage);\n TinyMceLegacyEnv.notifyUnavailable(config.bundleLoadErrorMessage);\n return;\n }\n\n installBundleWarnings(config);\n\n TextareaEnv.init(config);\n AttoEnv.init(config);\n TinyMceEnv.init(config);\n TinyMceLegacyEnv.init(config);\n};\n"],"names":["async","config","window","WPROOFREADER_BOOTSTRAP","Config","apply","bundleUrl","WEBSPELLCHECKER","TextareaEnv","notifyUnavailable","bundleLoadErrorMessage","AttoEnv","TinyMceEnv","TinyMceLegacyEnv","init"],"mappings":";;;;;;;;;;2UAuCoBA,gBACVC,OAASC,OAAOC,0BACjBF,QAA4B,iBAAXA,WAItBG,OAAOC,MAAMJ,cACP,6BAAWA,OAAOK,YAEnBJ,OAAOK,uBACRC,YAAYC,kBAAkBR,OAAOS,wBACrCC,QAAQF,kBAAkBR,OAAOS,wBACjCE,WAAWH,kBAAkBR,OAAOS,6BACpCG,iBAAiBJ,kBAAkBR,OAAOS,qDAIxBT,QAEtBO,YAAYM,KAAKb,QACjBU,QAAQG,KAAKb,QACbW,WAAWE,KAAKb,QAChBY,iBAAiBC,KAAKb"} \ No newline at end of file diff --git a/amd/build/notify.min.js b/amd/build/notify.min.js index ed7b495..450104d 100644 --- a/amd/build/notify.min.js +++ b/amd/build/notify.min.js @@ -1,24 +1,21 @@ -define("local_wproofreader/notify",["exports","core/notification","local_wproofreader/constants"],(function(_exports,_notification,_constants){var obj; +define("local_wproofreader/notify",["exports","local_wproofreader/constants"],(function(_exports,_constants){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.observeUnavailable=_exports.notifyField=_exports.notifyAllAttachedFields=void 0; /** * Shared warning display for WProofreader runtime notices. * - * core/notification.addNotification() always renders into the page-level - * #user-notifications region. When the editor triggering a warning lives - * inside an open Moodle modal (e.g. the calendar's "New event" dialog), that - * region sits behind the modal's backdrop and the notification is never - * seen, so render inside the open modal's own body in that case instead. - * - * Where the warning can be tied to a specific editor field, show it next to - * that field directly, styled as an informational (orange, bold) note in the - * same spot as Moodle's own per-field validation error - but not using - * core_form/events' notifyFieldValidationFailure() itself: that sets - * .is-invalid/.has-danger, which mform's own client-side validation checks - * on submit and blocks the form on. This is an FYI about proofreading, not a - * reason to stop the user submitting their content. + * Warnings are always shown next to the specific editor field they concern, + * styled as an informational (orange, bold) note in the same spot as + * Moodle's own per-field validation error - but not using core_form/events' + * notifyFieldValidationFailure() itself: that sets .is-invalid/.has-danger, + * which mform's own client-side validation checks on submit and blocks the + * form on. This is an FYI about proofreading, not a reason to stop the user + * submitting their content. There is no page-level or modal-level fallback: + * a warning that cannot be tied to a specific field is dropped rather than + * shown anywhere else. * * @module local_wproofreader/notify * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showWarning=_exports.notifyField=_exports.notifyAllAttachedFields=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const ATTACHED_INSTANCE_SELECTOR="[".concat(_constants.INSTANCE_ATTR,'="').concat(_constants.INSTANCE_ATTR_VALUE,'"], [').concat(_constants.ATTACHED_ATTR,"]"),showWarning=message=>{if(!message)return;const modalBody=(()=>{const modal=document.querySelector(".modal.show");return modal?modal.querySelector(".modal-body")||modal:null})();if(!modalBody)return void _notification.default.addNotification({message:message,type:"warning"}).then((()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#user-notifications"))||void 0===_document$querySelect||_document$querySelect.scrollIntoView({behavior:"smooth",block:"center"})}));const alert=document.createElement("div");alert.className="alert alert-warning",alert.setAttribute("role","alert"),alert.textContent=message,modalBody.prepend(alert),alert.scrollIntoView({behavior:"smooth",block:"nearest"})};_exports.showWarning=showWarning;const notifyField=(container,message)=>{if(!message)return;const field=(container=>{if(!container)return null;if("IFRAME"===container.tagName&&window.tinymce&&"function"==typeof window.tinymce.get){const editor=window.tinymce.get().find((candidate=>candidate&&candidate.iframeElement===container));return editor&&"function"==typeof editor.getElement?editor.getElement():null}const wrapper=container.closest(".felement, .fitem");return wrapper?wrapper.querySelector("textarea"):null})(container);field?((field,message)=>{const wrapper=field.closest(".felement")||field.parentElement;if(!wrapper)return void showWarning(message);let note=wrapper.querySelector(".".concat("wsc-field-warning"));note||(note=document.createElement("div"),note.className="wsc-field-warning",note.setAttribute("role","status"),wrapper.appendChild(note)),note.textContent=message})(field,message):showWarning(message)};_exports.notifyField=notifyField;_exports.notifyAllAttachedFields=message=>{message&&document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container=>notifyField(container,message)))}})); + */ +const ATTACHED_INSTANCE_SELECTOR="[".concat(_constants.INSTANCE_ATTR,'="').concat(_constants.INSTANCE_ATTR_VALUE,'"], [').concat(_constants.ATTACHED_ATTR,"]"),findOriginalField=container=>{if(!container)return null;if("IFRAME"===container.tagName||"BODY"===container.tagName){const editor=(container=>{if(!window.tinymce||"function"!=typeof window.tinymce.get)return null;const editors=window.tinymce.get()||[];return(Array.isArray(editors)?editors:[]).find((candidate=>{if(!candidate)return!1;const iframe=candidate.iframeElement||document.getElementById("".concat(candidate.id,"_ifr"));return iframe===container||iframe&&iframe.contentDocument&&iframe.contentDocument.body===container}))||null})(container);return editor&&"function"==typeof editor.getElement?editor.getElement():null}const wrapper=container.closest(".felement, .fitem");return wrapper?wrapper.querySelector("textarea"):null},notifyField=(container,message)=>{if(!message)return;const field=findOriginalField(container);field&&((field,message)=>{const wrapper=field.closest(".felement")||field.parentElement;if(!wrapper)return;let note=wrapper.querySelector(".".concat("wsc-field-warning"));note||(note=document.createElement("div"),note.className="wsc-field-warning",note.setAttribute("role","status"),wrapper.appendChild(note)),note.textContent=message})(field,message)};_exports.notifyField=notifyField;_exports.notifyAllAttachedFields=message=>{message&&document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container=>notifyField(container,message)))};_exports.observeUnavailable=(findContainers,message)=>{if(!message)return;const scan=()=>{findContainers().forEach((container=>{container.hasAttribute(_constants.ATTACHED_ATTR)||(container.setAttribute(_constants.ATTACHED_ATTR,"1"),notifyField(container,message))}))};scan(),"undefined"!=typeof MutationObserver&&new MutationObserver(scan).observe(document.body,{childList:!0,subtree:!0})}})); //# sourceMappingURL=notify.min.js.map \ No newline at end of file diff --git a/amd/build/notify.min.js.map b/amd/build/notify.min.js.map index 1c0cbaf..faa55be 100644 --- a/amd/build/notify.min.js.map +++ b/amd/build/notify.min.js.map @@ -1 +1 @@ -{"version":3,"file":"notify.min.js","sources":["../src/notify.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Shared warning display for WProofreader runtime notices.\n *\n * core/notification.addNotification() always renders into the page-level\n * #user-notifications region. When the editor triggering a warning lives\n * inside an open Moodle modal (e.g. the calendar's \"New event\" dialog), that\n * region sits behind the modal's backdrop and the notification is never\n * seen, so render inside the open modal's own body in that case instead.\n *\n * Where the warning can be tied to a specific editor field, show it next to\n * that field directly, styled as an informational (orange, bold) note in the\n * same spot as Moodle's own per-field validation error - but not using\n * core_form/events' notifyFieldValidationFailure() itself: that sets\n * .is-invalid/.has-danger, which mform's own client-side validation checks\n * on submit and blocks the form on. This is an FYI about proofreading, not a\n * reason to stop the user submitting their content.\n *\n * @module local_wproofreader/notify\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {INSTANCE_ATTR, INSTANCE_ATTR_VALUE, ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst OPEN_MODAL_SELECTOR = '.modal.show';\nconst FIELD_WARNING_CLASS = 'wsc-field-warning';\n// Plain textareas only carry the bundle's own INSTANCE_ATTR; Atto/TinyMCE\n// only carry our own ATTACHED_ATTR (see constants.js for why neither alone\n// covers all three editor types).\nconst ATTACHED_INSTANCE_SELECTOR = `[${INSTANCE_ATTR}=\"${INSTANCE_ATTR_VALUE}\"], [${ATTACHED_ATTR}]`;\n\nconst findOpenModalBody = () => {\n const modal = document.querySelector(OPEN_MODAL_SELECTOR);\n if (!modal) {\n return null;\n }\n return modal.querySelector('.modal-body') || modal;\n};\n\n/**\n * Resolve a WProofreader attach container back to the original mform field\n * it replaced, so the warning can be anchored in the right place: an Atto\n * content div or a TinyMCE iframe both replace an original textarea, while\n * a plain autoSearch-attached textarea is already that field itself.\n *\n * @param {HTMLElement} container The element WEBSPELLCHECKER attached an instance to.\n * @returns {HTMLElement|null}\n */\nconst findOriginalField = (container) => {\n if (!container) {\n return null;\n }\n\n if (container.tagName === 'IFRAME' && window.tinymce && typeof window.tinymce.get === 'function') {\n const editor = window.tinymce.get().find((candidate) => candidate && candidate.iframeElement === container);\n if (editor && typeof editor.getElement === 'function') {\n return editor.getElement();\n }\n return null;\n }\n\n // Atto hides the original textarea and inserts its editable wrapper as a\n // sibling within the same mform field wrapper; a plain textarea is its\n // own only match within that same wrapper.\n const wrapper = container.closest('.felement, .fitem');\n return wrapper ? wrapper.querySelector('textarea') : null;\n};\n\n/**\n * Show a warning, inside the open modal if there is one, else page-wide.\n *\n * @param {string} message Warning text to display.\n */\nexport const showWarning = (message) => {\n if (!message) {\n return;\n }\n\n const modalBody = findOpenModalBody();\n if (!modalBody) {\n Notification.addNotification({message, type: 'warning'}).then(() => {\n document.querySelector('#user-notifications')?.scrollIntoView({behavior: 'smooth', block: 'center'});\n return;\n });\n return;\n }\n\n const alert = document.createElement('div');\n alert.className = 'alert alert-warning';\n alert.setAttribute('role', 'alert');\n alert.textContent = message;\n modalBody.prepend(alert);\n alert.scrollIntoView({behavior: 'smooth', block: 'nearest'});\n};\n\n/**\n * Insert or update the informational warning note beside a field, without\n * touching the field's own validity state.\n *\n * @param {HTMLElement} field The mform field to anchor the note to.\n * @param {string} message Warning text to display.\n */\nconst showFieldWarning = (field, message) => {\n const wrapper = field.closest('.felement') || field.parentElement;\n if (!wrapper) {\n showWarning(message);\n return;\n }\n\n let note = wrapper.querySelector(`.${FIELD_WARNING_CLASS}`);\n if (!note) {\n note = document.createElement('div');\n note.className = FIELD_WARNING_CLASS;\n note.setAttribute('role', 'status');\n wrapper.appendChild(note);\n }\n note.textContent = message;\n};\n\n/**\n * Show a warning next to the specific editor field that triggered it, as an\n * informational note rather than a form validation error - it must never\n * block form submission. Falls back to showWarning() if the container\n * cannot be resolved back to a real mform field.\n *\n * @param {HTMLElement} container The element passed to WEBSPELLCHECKER.init().\n * @param {string} message Warning text to display.\n */\nexport const notifyField = (container, message) => {\n if (!message) {\n return;\n }\n\n const field = findOriginalField(container);\n if (!field) {\n showWarning(message);\n return;\n }\n\n showFieldWarning(field, message);\n};\n\n/**\n * Show a warning against every currently attached WProofreader instance on\n * the page. Used for warnings that stem from shared configuration (e.g. an\n * unsupported language), which affects every attached instance equally\n * rather than one specific field.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyAllAttachedFields = (message) => {\n if (!message) {\n return;\n }\n\n document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container) => notifyField(container, message));\n};\n"],"names":["ATTACHED_INSTANCE_SELECTOR","INSTANCE_ATTR","INSTANCE_ATTR_VALUE","ATTACHED_ATTR","showWarning","message","modalBody","modal","document","querySelector","findOpenModalBody","addNotification","type","then","scrollIntoView","behavior","block","alert","createElement","className","setAttribute","textContent","prepend","notifyField","container","field","tagName","window","tinymce","get","editor","find","candidate","iframeElement","getElement","wrapper","closest","findOriginalField","parentElement","note","appendChild","showFieldWarning","querySelectorAll","forEach"],"mappings":";;;;;;;;;;;;;;;;;;;;;yNA6CMA,sCAAiCC,sCAAkBC,+CAA2BC,8BA4CvEC,YAAeC,cACnBA,qBAICC,UA/CgB,YAChBC,MAAQC,SAASC,cARC,sBASnBF,MAGEA,MAAME,cAAc,gBAAkBF,MAFlC,MA4COG,OACbJ,4CACYK,gBAAgB,CAACN,QAAAA,QAASO,KAAM,YAAYC,MAAK,6DAC1DL,SAASC,cAAc,+EAAwBK,eAAe,CAACC,SAAU,SAAUC,MAAO,oBAM5FC,MAAQT,SAASU,cAAc,OACrCD,MAAME,UAAY,sBAClBF,MAAMG,aAAa,OAAQ,SAC3BH,MAAMI,YAAchB,QACpBC,UAAUgB,QAAQL,OAClBA,MAAMH,eAAe,CAACC,SAAU,SAAUC,MAAO,oDAoCxCO,YAAc,CAACC,UAAWnB,eAC9BA,qBAICoB,MArFiBD,CAAAA,gBAClBA,iBACM,QAGe,WAAtBA,UAAUE,SAAwBC,OAAOC,SAAyC,mBAAvBD,OAAOC,QAAQC,IAAoB,OACxFC,OAASH,OAAOC,QAAQC,MAAME,MAAMC,WAAcA,WAAaA,UAAUC,gBAAkBT,mBAC7FM,QAAuC,mBAAtBA,OAAOI,WACjBJ,OAAOI,aAEX,WAMLC,QAAUX,UAAUY,QAAQ,4BAC3BD,QAAUA,QAAQ1B,cAAc,YAAc,MAoEvC4B,CAAkBb,WAC3BC,MAhCgB,EAACA,MAAOpB,iBACvB8B,QAAUV,MAAMW,QAAQ,cAAgBX,MAAMa,kBAC/CH,oBACD/B,YAAYC,aAIZkC,KAAOJ,QAAQ1B,yBApFK,sBAqFnB8B,OACDA,KAAO/B,SAASU,cAAc,OAC9BqB,KAAKpB,UAvFe,oBAwFpBoB,KAAKnB,aAAa,OAAQ,UAC1Be,QAAQK,YAAYD,OAExBA,KAAKlB,YAAchB,SAuBnBoC,CAAiBhB,MAAOpB,SAJpBD,YAAYC,4EAeoBA,UAC/BA,SAILG,SAASkC,iBAAiB1C,4BAA4B2C,SAASnB,WAAcD,YAAYC,UAAWnB"} \ No newline at end of file +{"version":3,"file":"notify.min.js","sources":["../src/notify.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Shared warning display for WProofreader runtime notices.\n *\n * Warnings are always shown next to the specific editor field they concern,\n * styled as an informational (orange, bold) note in the same spot as\n * Moodle's own per-field validation error - but not using core_form/events'\n * notifyFieldValidationFailure() itself: that sets .is-invalid/.has-danger,\n * which mform's own client-side validation checks on submit and blocks the\n * form on. This is an FYI about proofreading, not a reason to stop the user\n * submitting their content. There is no page-level or modal-level fallback:\n * a warning that cannot be tied to a specific field is dropped rather than\n * shown anywhere else.\n *\n * @module local_wproofreader/notify\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {INSTANCE_ATTR, INSTANCE_ATTR_VALUE, ATTACHED_ATTR} from 'local_wproofreader/constants';\n\nconst FIELD_WARNING_CLASS = 'wsc-field-warning';\n// Plain textareas only carry the bundle's own INSTANCE_ATTR; Atto/TinyMCE\n// only carry our own ATTACHED_ATTR (see constants.js for why neither alone\n// covers all three editor types).\nconst ATTACHED_INSTANCE_SELECTOR = `[${INSTANCE_ATTR}=\"${INSTANCE_ATTR_VALUE}\"], [${ATTACHED_ATTR}]`;\n\n/**\n * Find the tinymce.Editor instance (new or legacy) whose iframe is, or\n * contains, the given container - either the outer iframe itself, or its\n * contentDocument.body, which is what a WProofreader instance's own\n * getContainerNode() reports once it finishes attaching inside an iframe\n * (it reassigns its tracked container from the outer iframe we passed in to\n * the inner editable body).\n *\n * @param {HTMLElement} container\n * @returns {Object|null} tinymce.Editor instance, or null if none matches.\n */\nconst findTinymceEditorFor = (container) => {\n if (!window.tinymce || typeof window.tinymce.get !== 'function') {\n return null;\n }\n\n const editors = window.tinymce.get() || [];\n return (Array.isArray(editors) ? editors : []).find((candidate) => {\n if (!candidate) {\n return false;\n }\n const iframe = candidate.iframeElement || document.getElementById(`${candidate.id}_ifr`);\n return iframe === container || (iframe && iframe.contentDocument && iframe.contentDocument.body === container);\n }) || null;\n};\n\n/**\n * Resolve a WProofreader attach container back to the original mform field\n * it replaced, so the warning can be anchored in the right place: an Atto\n * content div or a TinyMCE iframe both replace an original textarea, while\n * a plain autoSearch-attached textarea is already that field itself.\n *\n * @param {HTMLElement} container The element WEBSPELLCHECKER attached an instance to.\n * @returns {HTMLElement|null}\n */\nconst findOriginalField = (container) => {\n if (!container) {\n return null;\n }\n\n if (container.tagName === 'IFRAME' || container.tagName === 'BODY') {\n const editor = findTinymceEditorFor(container);\n return editor && typeof editor.getElement === 'function' ? editor.getElement() : null;\n }\n\n // Atto hides the original textarea and inserts its editable wrapper as a\n // sibling within the same mform field wrapper; a plain textarea is its\n // own only match within that same wrapper.\n const wrapper = container.closest('.felement, .fitem');\n return wrapper ? wrapper.querySelector('textarea') : null;\n};\n\n/**\n * Insert or update the informational warning note beside a field, without\n * touching the field's own validity state.\n *\n * @param {HTMLElement} field The mform field to anchor the note to.\n * @param {string} message Warning text to display.\n */\nconst showFieldWarning = (field, message) => {\n const wrapper = field.closest('.felement') || field.parentElement;\n if (!wrapper) {\n return;\n }\n\n let note = wrapper.querySelector(`.${FIELD_WARNING_CLASS}`);\n if (!note) {\n note = document.createElement('div');\n note.className = FIELD_WARNING_CLASS;\n note.setAttribute('role', 'status');\n wrapper.appendChild(note);\n }\n note.textContent = message;\n};\n\n/**\n * Show a warning next to the specific editor field that triggered it, as an\n * informational note rather than a form validation error - it must never\n * block form submission. Dropped silently if the container cannot be\n * resolved back to a real mform field.\n *\n * @param {HTMLElement} container The element passed to WEBSPELLCHECKER.init().\n * @param {string} message Warning text to display.\n */\nexport const notifyField = (container, message) => {\n if (!message) {\n return;\n }\n\n const field = findOriginalField(container);\n if (!field) {\n return;\n }\n\n showFieldWarning(field, message);\n};\n\n/**\n * Show a warning against every currently attached WProofreader instance on\n * the page. Used for warnings that stem from shared configuration (e.g. an\n * unsupported language), which affects every attached instance equally\n * rather than one specific field.\n *\n * @param {string} message Warning text to display.\n */\nexport const notifyAllAttachedFields = (message) => {\n if (!message) {\n return;\n }\n\n document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container) => notifyField(container, message));\n};\n\n/**\n * Show a warning beside every container an editor environment finds, now and\n * for any that appear later (e.g. the calendar's AJAX-loaded \"New event\"\n * dialog, or an mform \"Show more\" section revealed after the initial scan).\n * Used when the WProofreader bundle failed to load entirely, so there is no\n * instance to attach and no ATTACHED_ATTR marker will ever be set the normal\n * way - this sets it itself, to avoid re-notifying the same container.\n *\n * @param {Function} findContainers Returns the current list of containers an environment recognizes.\n * @param {string} message Warning text to display.\n */\nexport const observeUnavailable = (findContainers, message) => {\n if (!message) {\n return;\n }\n\n const scan = () => {\n findContainers().forEach((container) => {\n if (!container.hasAttribute(ATTACHED_ATTR)) {\n container.setAttribute(ATTACHED_ATTR, '1');\n notifyField(container, message);\n }\n });\n };\n\n scan();\n\n if (typeof MutationObserver !== 'undefined') {\n new MutationObserver(scan).observe(document.body, {childList: true, subtree: true});\n }\n};\n"],"names":["ATTACHED_INSTANCE_SELECTOR","INSTANCE_ATTR","INSTANCE_ATTR_VALUE","ATTACHED_ATTR","findOriginalField","container","tagName","editor","window","tinymce","get","editors","Array","isArray","find","candidate","iframe","iframeElement","document","getElementById","id","contentDocument","body","findTinymceEditorFor","getElement","wrapper","closest","querySelector","notifyField","message","field","parentElement","note","createElement","className","setAttribute","appendChild","textContent","showFieldWarning","querySelectorAll","forEach","findContainers","scan","hasAttribute","MutationObserver","observe","childList","subtree"],"mappings":";;;;;;;;;;;;;;;;;;MAuCMA,sCAAiCC,sCAAkBC,+CAA2BC,8BAqC9EC,kBAAqBC,gBAClBA,iBACM,QAGe,WAAtBA,UAAUC,SAA8C,SAAtBD,UAAUC,QAAoB,OAC1DC,OA9BgBF,CAAAA,gBACrBG,OAAOC,SAAyC,mBAAvBD,OAAOC,QAAQC,WAClC,WAGLC,QAAUH,OAAOC,QAAQC,OAAS,UAChCE,MAAMC,QAAQF,SAAWA,QAAU,IAAIG,MAAMC,gBAC5CA,iBACM,QAELC,OAASD,UAAUE,eAAiBC,SAASC,yBAAkBJ,UAAUK,mBACxEJ,SAAWX,WAAcW,QAAUA,OAAOK,iBAAmBL,OAAOK,gBAAgBC,OAASjB,cAClG,MAkBakB,CAAqBlB,kBAC7BE,QAAuC,mBAAtBA,OAAOiB,WAA4BjB,OAAOiB,aAAe,WAM/EC,QAAUpB,UAAUqB,QAAQ,4BAC3BD,QAAUA,QAAQE,cAAc,YAAc,MAmC5CC,YAAc,CAACvB,UAAWwB,eAC9BA,qBAICC,MAAQ1B,kBAAkBC,WAC3ByB,OA/BgB,EAACA,MAAOD,iBACvBJ,QAAUK,MAAMJ,QAAQ,cAAgBI,MAAMC,kBAC/CN,mBAIDO,KAAOP,QAAQE,yBAvEK,sBAwEnBK,OACDA,KAAOd,SAASe,cAAc,OAC9BD,KAAKE,UA1Ee,oBA2EpBF,KAAKG,aAAa,OAAQ,UAC1BV,QAAQW,YAAYJ,OAExBA,KAAKK,YAAcR,SAsBnBS,CAAiBR,MAAOD,4EAWYA,UAC/BA,SAILX,SAASqB,iBAAiBvC,4BAA4BwC,SAASnC,WAAcuB,YAAYvB,UAAWwB,wCActE,CAACY,eAAgBZ,eAC1CA,qBAICa,KAAO,KACTD,iBAAiBD,SAASnC,YACjBA,UAAUsC,aAAaxC,4BACxBE,UAAU8B,aAAahC,yBAAe,KACtCyB,YAAYvB,UAAWwB,cAKnCa,OAEgC,oBAArBE,sBACHA,iBAAiBF,MAAMG,QAAQ3B,SAASI,KAAM,CAACwB,WAAW,EAAMC,SAAS"} \ No newline at end of file diff --git a/amd/build/proofreader_config.min.js b/amd/build/proofreader_config.min.js index 8d3b2d0..0eb21dc 100644 --- a/amd/build/proofreader_config.min.js +++ b/amd/build/proofreader_config.min.js @@ -1,4 +1,4 @@ -define("local_wproofreader/proofreader_config",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.apply=void 0; +define("local_wproofreader/proofreader_config",["exports","local_wproofreader/notify"],(function(_exports,_notify){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.apply=void 0; /** * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup. * @@ -6,6 +6,6 @@ define("local_wproofreader/proofreader_config",["exports"],(function(_exports){O * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const toBoolean=(value,fallback)=>!0===value||"true"===value||!1!==value&&"false"!==value&&fallback,toArray=value=>Array.isArray(value)?value:[];_exports.apply=config=>{const isBadgeEnabled=toBoolean(config.enableBadgeButton,!0),defaultBadgeActions=isBadgeEnabled?["addWord","ignoreAll","settings","toggle","proofreadDialog"]:["addWord","ignoreAll","settings","proofreadDialog"],badgeActions=toArray(config.actionItems).length?config.actionItems:defaultBadgeActions;window.WEBSPELLCHECKER_CONFIG={autoSearch:toBoolean(config.autoSearch,!0),appType:config.appType,serviceProtocol:config.serviceProtocol||"https",serviceHost:config.serviceHost||"svc.webspellchecker.net",servicePath:config.servicePath||"api",servicePort:config.servicePort||"443",enableGrammar:toBoolean(config.enableGrammar,!1),aiWritingAssistant:toBoolean(config.aiWritingAssistant,!1),settingsSections:toArray(config.settingsSections),serviceId:config.serviceId,lang:config.lang,enableBadgeButton:isBadgeEnabled,actionItems:badgeActions,disableAutoSearchIn:[...toArray(config.disableAutoSearchIn),"textarea"],disableOptionsStorage:toArray(config.disableOptionsStorage),disableDictionariesPreferences:toBoolean(config.disableDictionariesPreferences,!1),autocomplete:toBoolean(config.autocomplete,!1),autocorrect:toBoolean(config.autocorrect,!1),spellingSuggestions:toBoolean(config.spellingSuggestions,!0),grammarSuggestions:toBoolean(config.grammarSuggestions,!1),styleGuideSuggestions:toBoolean(config.styleGuideSuggestions,!0),ignoreAllCapsWords:toBoolean(config.ignoreAllCapsWords,!0),ignoreDomainNames:toBoolean(config.ignoreDomainNames,!0),ignoreWordsWithMixedCases:toBoolean(config.ignoreWordsWithMixedCases,!0),ignoreWordsWithNumbers:toBoolean(config.ignoreWordsWithNumbers,!0),globalBadge:toBoolean(config.globalBadge,!1),compactBadge:toBoolean(config.compactBadge,!0),allSuggestionsMode:toBoolean(config.allSuggestionsMode,!0),onLoad:function(){const instance=this;try{this.subscribe("replaceProblem",(()=>{try{instance.getContainerNode().dispatchEvent(new Event("input",{bubbles:!0}))}catch(e){}}))}catch(e){}}},Array.isArray(config.generalOptions)&&config.generalOptions.length&&(window.WEBSPELLCHECKER_CONFIG.generalOptions=config.generalOptions)}})); +const STATUS_MESSAGE_KEYS={400:"runtimeErrorBadRequestMessage",403:"runtimeErrorForbiddenMessage",404:"runtimeErrorNotFoundMessage",409:"runtimeErrorConflictMessage",500:"runtimeErrorServerMessage",503:"runtimeServiceUnavailableMessage"},toBoolean=(value,fallback)=>!0===value||"true"===value||!1!==value&&"false"!==value&&fallback,toArray=value=>Array.isArray(value)?value:[];_exports.apply=config=>{const isBadgeEnabled=toBoolean(config.enableBadgeButton,!0),defaultBadgeActions=isBadgeEnabled?["addWord","ignoreAll","settings","toggle","proofreadDialog"]:["addWord","ignoreAll","settings","proofreadDialog"],badgeActions=toArray(config.actionItems).length?config.actionItems:defaultBadgeActions;window.WEBSPELLCHECKER_CONFIG={autoSearch:toBoolean(config.autoSearch,!0),appType:config.appType,serviceProtocol:config.serviceProtocol||"https",serviceHost:config.serviceHost||"svc.webspellchecker.net",servicePath:config.servicePath||"api",servicePort:config.servicePort||"443",enableGrammar:toBoolean(config.enableGrammar,!1),aiWritingAssistant:toBoolean(config.aiWritingAssistant,!1),settingsSections:toArray(config.settingsSections),serviceId:config.serviceId,lang:config.lang,enableBadgeButton:isBadgeEnabled,actionItems:badgeActions,disableAutoSearchIn:[...toArray(config.disableAutoSearchIn),"textarea"],disableOptionsStorage:toArray(config.disableOptionsStorage),disableDictionariesPreferences:toBoolean(config.disableDictionariesPreferences,!1),autocomplete:toBoolean(config.autocomplete,!1),autocorrect:toBoolean(config.autocorrect,!1),spellingSuggestions:toBoolean(config.spellingSuggestions,!0),grammarSuggestions:toBoolean(config.grammarSuggestions,!1),styleGuideSuggestions:toBoolean(config.styleGuideSuggestions,!0),ignoreAllCapsWords:toBoolean(config.ignoreAllCapsWords,!0),ignoreDomainNames:toBoolean(config.ignoreDomainNames,!0),ignoreWordsWithMixedCases:toBoolean(config.ignoreWordsWithMixedCases,!0),ignoreWordsWithNumbers:toBoolean(config.ignoreWordsWithNumbers,!0),globalBadge:toBoolean(config.globalBadge,!1),compactBadge:toBoolean(config.compactBadge,!0),allSuggestionsMode:toBoolean(config.allSuggestionsMode,!0),onLoad:function(){const instance=this;try{this.subscribe("replaceProblem",(()=>{try{instance.getContainerNode().dispatchEvent(new Event("input",{bubbles:!0}))}catch(e){}}))}catch(e){}},onErrorRequest:(error,instance)=>{const key=STATUS_MESSAGE_KEYS[error&&error.status],message=key&&config[key];if(message)try{(0,_notify.notifyField)(instance.getContainerNode(),message)}catch(e){}}},Array.isArray(config.generalOptions)&&config.generalOptions.length&&(window.WEBSPELLCHECKER_CONFIG.generalOptions=config.generalOptions)}})); //# sourceMappingURL=proofreader_config.min.js.map \ No newline at end of file diff --git a/amd/build/proofreader_config.min.js.map b/amd/build/proofreader_config.min.js.map index a43b31d..9013e96 100644 --- a/amd/build/proofreader_config.min.js.map +++ b/amd/build/proofreader_config.min.js.map @@ -1 +1 @@ -{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n // Plain textareas are attached explicitly by\n // local_wproofreader/environment_textarea instead of the bundle's\n // own autoSearch, which only scans once at load and misses fields\n // revealed later (e.g. an mform \"Show more\" advanced section).\n disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'],\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, false),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","generalOptions"],"mappings":";;;;;;;;MAuBMA,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aAKbgB,oBAAqB,IAAIvB,QAAQG,OAAOoB,qBAAsB,YAC9DC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,OAMb7C,MAAMC,QAAQC,OAAO4C,iBAAmB5C,OAAO4C,eAAetC,SAC9DC,OAAOC,uBAAuBoC,eAAiB5C,OAAO4C"} \ No newline at end of file +{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\n\n// Maps the HTTP status codes documented at\n// https://docs.wproofreader.com/api-reference/http-response-status-codes to\n// the config message key carrying the corresponding warning text. 503 reuses\n// the existing service-unavailable message - same condition, whether reported\n// as a transport failure or a real HTTP response.\nconst STATUS_MESSAGE_KEYS = {\n 400: 'runtimeErrorBadRequestMessage',\n 403: 'runtimeErrorForbiddenMessage',\n 404: 'runtimeErrorNotFoundMessage',\n 409: 'runtimeErrorConflictMessage',\n 500: 'runtimeErrorServerMessage',\n 503: 'runtimeServiceUnavailableMessage',\n};\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n // Plain textareas are attached explicitly by\n // local_wproofreader/environment_textarea instead of the bundle's\n // own autoSearch, which only scans once at load and misses fields\n // revealed later (e.g. an mform \"Show more\" advanced section).\n disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'],\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, false),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n onErrorRequest: (error, instance) => {\n const key = STATUS_MESSAGE_KEYS[error && error.status];\n const message = key && config[key];\n if (!message) {\n return;\n }\n\n try {\n notifyField(instance.getContainerNode(), message);\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["STATUS_MESSAGE_KEYS","toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","onErrorRequest","error","key","status","message","generalOptions"],"mappings":";;;;;;;;MA8BMA,oBAAsB,KACnB,oCACA,mCACA,kCACA,kCACA,gCACA,oCAGHC,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aAKbgB,oBAAqB,IAAIvB,QAAQG,OAAOoB,qBAAsB,YAC9DC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,MAIbC,eAAgB,CAACC,MAAOT,kBACdU,IAAMrD,oBAAoBoD,OAASA,MAAME,QACzCC,QAAUF,KAAO9C,OAAO8C,QACzBE,oCAKWZ,SAASG,mBAAoBS,SAC3C,MAAOL,OAMb7C,MAAMC,QAAQC,OAAOiD,iBAAmBjD,OAAOiD,eAAe3C,SAC9DC,OAAOC,uBAAuByC,eAAiBjD,OAAOiD"} \ No newline at end of file diff --git a/amd/src/environment_atto.js b/amd/src/environment_atto.js index 17a5ec0..ce8b568 100644 --- a/amd/src/environment_atto.js +++ b/amd/src/environment_atto.js @@ -21,7 +21,7 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import {notifyField} from 'local_wproofreader/notify'; +import {notifyField, observeUnavailable} from 'local_wproofreader/notify'; import {ATTACHED_ATTR} from 'local_wproofreader/constants'; const SELECTORS = [ @@ -112,3 +112,13 @@ export const init = (config) => { startObserver(); scanAndInit(); }; + +/** + * Show a warning next to every Atto editor on the page, now and for any that + * appear later, without attaching an instance. Used when the WProofreader + * bundle itself failed to load, so no instance can ever attach to report + * per-field problems itself. + * + * @param {string} message Warning text to display. + */ +export const notifyUnavailable = (message) => observeUnavailable(findEditors, message); diff --git a/amd/src/environment_textarea.js b/amd/src/environment_textarea.js index 4bdc54a..222a2a5 100644 --- a/amd/src/environment_textarea.js +++ b/amd/src/environment_textarea.js @@ -26,7 +26,7 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import {notifyField} from 'local_wproofreader/notify'; +import {notifyField, observeUnavailable} from 'local_wproofreader/notify'; import {ATTACHED_ATTR} from 'local_wproofreader/constants'; const SKIP_CLASS = 'wsc-skip-autosearch'; @@ -52,8 +52,13 @@ const tagSkippableTextareas = () => { const isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); +const isManagedByTinymce = (textarea) => !!textarea.id + && !!window.tinymce + && typeof window.tinymce.get === 'function' + && !!window.tinymce.get(textarea.id); + const findTextareas = () => Array.from(document.querySelectorAll('textarea')) - .filter((textarea) => !textarea.classList.contains(SKIP_CLASS)); + .filter((textarea) => !textarea.classList.contains(SKIP_CLASS) && !isManagedByTinymce(textarea)); const createInstance = (textarea) => { if (!textarea || textarea.hasAttribute(ATTACHED_ATTR) || !isVisible(textarea) || !window.WEBSPELLCHECKER) { @@ -125,3 +130,16 @@ export const init = (config) => { startObserver(); scanAndInit(); }; + +/** + * Show a warning next to every plain textarea on the page, now and for any + * that appear later, without attaching an instance. Used when the + * WProofreader bundle itself failed to load, so no instance can ever attach + * to report per-field problems itself. + * + * @param {string} message Warning text to display. + */ +export const notifyUnavailable = (message) => { + tagSkippableTextareas(); + observeUnavailable(findTextareas, message); +}; diff --git a/amd/src/environment_tinymce.js b/amd/src/environment_tinymce.js index a37bac0..0c63513 100644 --- a/amd/src/environment_tinymce.js +++ b/amd/src/environment_tinymce.js @@ -21,7 +21,7 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import {notifyField} from 'local_wproofreader/notify'; +import {notifyField, observeUnavailable} from 'local_wproofreader/notify'; import {ATTACHED_ATTR} from 'local_wproofreader/constants'; const SELECTOR = 'iframe.tox-edit-area__iframe'; @@ -137,3 +137,13 @@ export const init = (config) => { startObserver(); scanAndInit(); }; + +/** + * Show a warning next to every TinyMCE 6 editor on the page, now and for any + * that appear later, without attaching an instance. Used when the + * WProofreader bundle itself failed to load, so no instance can ever attach + * to report per-field problems itself. + * + * @param {string} message Warning text to display. + */ +export const notifyUnavailable = (message) => observeUnavailable(findIframes, message); diff --git a/amd/src/environment_tinymce_legacy.js b/amd/src/environment_tinymce_legacy.js new file mode 100644 index 0000000..00dd8f1 --- /dev/null +++ b/amd/src/environment_tinymce_legacy.js @@ -0,0 +1,159 @@ +// This file is part of Moodle - https://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 . + +/** + * Legacy TinyMCE editor integration (editor_tinymce, TinyMCE 3.5.x, Moodle's + * "TinyMCE HTML editor (legacy)"). Distinct from environment_tinymce.js, + * which targets the newer TinyMCE 6 "tiny" editor plugin - the two ship + * different iframe markup and editor APIs, and both publish themselves as + * window.tinymce, so editor instances are told apart by feature (an + * .iframeElement property only the newer editor exposes), not by a global + * version flag. + * + * @module local_wproofreader/environment_tinymce_legacy + * @copyright 2026 WebSpellChecker + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {notifyField, observeUnavailable} from 'local_wproofreader/notify'; +import {ATTACHED_ATTR} from 'local_wproofreader/constants'; + +let observer = null; +let hookInstalled = false; +let attachErrorMessage = null; + +const isMarked = (iframe) => iframe.hasAttribute(ATTACHED_ATTR); +const mark = (iframe) => iframe.setAttribute(ATTACHED_ATTR, '1'); +const unmark = (iframe) => iframe.removeAttribute(ATTACHED_ATTR); + +const isLegacyEditor = (editor) => !!editor + && !editor.iframeElement + && editor.onInit && typeof editor.onInit.add === 'function'; + +const findLegacyEditors = () => { + if (!window.tinymce || typeof window.tinymce.get !== 'function') { + return []; + } + const all = window.tinymce.get() || []; + return (Array.isArray(all) ? all : []).filter(isLegacyEditor); +}; + +const initInstance = (iframe) => { + const doc = iframe.contentDocument; + if (!doc || !doc.body) { + unmark(iframe); + return; + } + + try { + window.WEBSPELLCHECKER.init(Object.assign({}, window.WEBSPELLCHECKER_CONFIG, { + container: iframe, + })); + } catch (e) { + unmark(iframe); + if (window.console && window.console.warn) { + window.console.warn('WProofreader: failed to attach to legacy TinyMCE editor', e); + } + notifyField(iframe, attachErrorMessage); + } +}; + +/** + * Decide when to attach to a single legacy editor instance: now, or once it + * finishes initializing. + * + * @param {Object} editor A tinymce.Editor instance (legacy API). + */ +const attach = (editor) => { + const iframe = document.getElementById(`${editor.id}_ifr`); + if (!iframe || isMarked(iframe) || !window.WEBSPELLCHECKER || !window.WEBSPELLCHECKER_CONFIG) { + return; + } + + // Mark up front so repeated observer fires do not register duplicate hooks; + // initInstance unmarks again if the editor turns out not to be ready. + mark(iframe); + + if (!editor.initialized) { + editor.onInit.add(() => initInstance(iframe)); + return; + } + + initInstance(iframe); +}; + +const scanAndInit = () => { + findLegacyEditors().forEach(attach); +}; + +const startObserver = () => { + if (observer || typeof MutationObserver === 'undefined') { + return; + } + + observer = new MutationObserver(() => { + scanAndInit(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); +}; + +const hookBundleReady = () => { + if (hookInstalled) { + return; + } + hookInstalled = true; + + const previous = window.webspellcheckerAlreadyLoaded; + window.webspellcheckerAlreadyLoaded = function() { + if (typeof previous === 'function') { + try { + previous.apply(this, arguments); + } catch (e) { + // Preserve original callback contract on failure. + } + } + scanAndInit(); + }; +}; + +/** + * Initialize the legacy TinyMCE environment. + * + * @param {Object} config Page configuration. + */ +export const init = (config) => { + attachErrorMessage = config && config.editorAttachErrorMessage || null; + hookBundleReady(); + startObserver(); + scanAndInit(); +}; + +const findLegacyIframes = () => findLegacyEditors() + .map((editor) => document.getElementById(`${editor.id}_ifr`)) + .filter(Boolean); + +/** + * Show a warning next to every legacy TinyMCE editor on the page, now and + * for any that appear later, without attaching an instance. Used when the + * WProofreader bundle itself failed to load, so no instance can ever attach + * to report per-field problems itself. + * + * @param {string} message Warning text to display. + */ +export const notifyUnavailable = (message) => observeUnavailable(findLegacyIframes, message); diff --git a/amd/src/init.js b/amd/src/init.js index 48a0503..437a1f2 100644 --- a/amd/src/init.js +++ b/amd/src/init.js @@ -24,10 +24,10 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import {showWarning} from 'local_wproofreader/notify'; import * as Config from 'local_wproofreader/proofreader_config'; import * as AttoEnv from 'local_wproofreader/environment_atto'; import * as TinyMceEnv from 'local_wproofreader/environment_tinymce'; +import * as TinyMceLegacyEnv from 'local_wproofreader/environment_tinymce_legacy'; import * as TextareaEnv from 'local_wproofreader/environment_textarea'; import {loadBundle} from 'local_wproofreader/bundle_loader'; import {install as installBundleWarnings} from 'local_wproofreader/bundle_warnings'; @@ -47,7 +47,10 @@ export const init = async() => { await loadBundle(config.bundleUrl); if (!window.WEBSPELLCHECKER) { - showWarning(config.bundleLoadErrorMessage); + TextareaEnv.notifyUnavailable(config.bundleLoadErrorMessage); + AttoEnv.notifyUnavailable(config.bundleLoadErrorMessage); + TinyMceEnv.notifyUnavailable(config.bundleLoadErrorMessage); + TinyMceLegacyEnv.notifyUnavailable(config.bundleLoadErrorMessage); return; } @@ -56,4 +59,5 @@ export const init = async() => { TextareaEnv.init(config); AttoEnv.init(config); TinyMceEnv.init(config); + TinyMceLegacyEnv.init(config); }; diff --git a/amd/src/notify.js b/amd/src/notify.js index e47ab6f..d22fe08 100644 --- a/amd/src/notify.js +++ b/amd/src/notify.js @@ -16,41 +16,53 @@ /** * Shared warning display for WProofreader runtime notices. * - * core/notification.addNotification() always renders into the page-level - * #user-notifications region. When the editor triggering a warning lives - * inside an open Moodle modal (e.g. the calendar's "New event" dialog), that - * region sits behind the modal's backdrop and the notification is never - * seen, so render inside the open modal's own body in that case instead. - * - * Where the warning can be tied to a specific editor field, show it next to - * that field directly, styled as an informational (orange, bold) note in the - * same spot as Moodle's own per-field validation error - but not using - * core_form/events' notifyFieldValidationFailure() itself: that sets - * .is-invalid/.has-danger, which mform's own client-side validation checks - * on submit and blocks the form on. This is an FYI about proofreading, not a - * reason to stop the user submitting their content. + * Warnings are always shown next to the specific editor field they concern, + * styled as an informational (orange, bold) note in the same spot as + * Moodle's own per-field validation error - but not using core_form/events' + * notifyFieldValidationFailure() itself: that sets .is-invalid/.has-danger, + * which mform's own client-side validation checks on submit and blocks the + * form on. This is an FYI about proofreading, not a reason to stop the user + * submitting their content. There is no page-level or modal-level fallback: + * a warning that cannot be tied to a specific field is dropped rather than + * shown anywhere else. * * @module local_wproofreader/notify * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import Notification from 'core/notification'; import {INSTANCE_ATTR, INSTANCE_ATTR_VALUE, ATTACHED_ATTR} from 'local_wproofreader/constants'; -const OPEN_MODAL_SELECTOR = '.modal.show'; const FIELD_WARNING_CLASS = 'wsc-field-warning'; // Plain textareas only carry the bundle's own INSTANCE_ATTR; Atto/TinyMCE // only carry our own ATTACHED_ATTR (see constants.js for why neither alone // covers all three editor types). const ATTACHED_INSTANCE_SELECTOR = `[${INSTANCE_ATTR}="${INSTANCE_ATTR_VALUE}"], [${ATTACHED_ATTR}]`; -const findOpenModalBody = () => { - const modal = document.querySelector(OPEN_MODAL_SELECTOR); - if (!modal) { +/** + * Find the tinymce.Editor instance (new or legacy) whose iframe is, or + * contains, the given container - either the outer iframe itself, or its + * contentDocument.body, which is what a WProofreader instance's own + * getContainerNode() reports once it finishes attaching inside an iframe + * (it reassigns its tracked container from the outer iframe we passed in to + * the inner editable body). + * + * @param {HTMLElement} container + * @returns {Object|null} tinymce.Editor instance, or null if none matches. + */ +const findTinymceEditorFor = (container) => { + if (!window.tinymce || typeof window.tinymce.get !== 'function') { return null; } - return modal.querySelector('.modal-body') || modal; + + const editors = window.tinymce.get() || []; + return (Array.isArray(editors) ? editors : []).find((candidate) => { + if (!candidate) { + return false; + } + const iframe = candidate.iframeElement || document.getElementById(`${candidate.id}_ifr`); + return iframe === container || (iframe && iframe.contentDocument && iframe.contentDocument.body === container); + }) || null; }; /** @@ -67,12 +79,9 @@ const findOriginalField = (container) => { return null; } - if (container.tagName === 'IFRAME' && window.tinymce && typeof window.tinymce.get === 'function') { - const editor = window.tinymce.get().find((candidate) => candidate && candidate.iframeElement === container); - if (editor && typeof editor.getElement === 'function') { - return editor.getElement(); - } - return null; + if (container.tagName === 'IFRAME' || container.tagName === 'BODY') { + const editor = findTinymceEditorFor(container); + return editor && typeof editor.getElement === 'function' ? editor.getElement() : null; } // Atto hides the original textarea and inserts its editable wrapper as a @@ -82,33 +91,6 @@ const findOriginalField = (container) => { return wrapper ? wrapper.querySelector('textarea') : null; }; -/** - * Show a warning, inside the open modal if there is one, else page-wide. - * - * @param {string} message Warning text to display. - */ -export const showWarning = (message) => { - if (!message) { - return; - } - - const modalBody = findOpenModalBody(); - if (!modalBody) { - Notification.addNotification({message, type: 'warning'}).then(() => { - document.querySelector('#user-notifications')?.scrollIntoView({behavior: 'smooth', block: 'center'}); - return; - }); - return; - } - - const alert = document.createElement('div'); - alert.className = 'alert alert-warning'; - alert.setAttribute('role', 'alert'); - alert.textContent = message; - modalBody.prepend(alert); - alert.scrollIntoView({behavior: 'smooth', block: 'nearest'}); -}; - /** * Insert or update the informational warning note beside a field, without * touching the field's own validity state. @@ -119,7 +101,6 @@ export const showWarning = (message) => { const showFieldWarning = (field, message) => { const wrapper = field.closest('.felement') || field.parentElement; if (!wrapper) { - showWarning(message); return; } @@ -136,8 +117,8 @@ const showFieldWarning = (field, message) => { /** * Show a warning next to the specific editor field that triggered it, as an * informational note rather than a form validation error - it must never - * block form submission. Falls back to showWarning() if the container - * cannot be resolved back to a real mform field. + * block form submission. Dropped silently if the container cannot be + * resolved back to a real mform field. * * @param {HTMLElement} container The element passed to WEBSPELLCHECKER.init(). * @param {string} message Warning text to display. @@ -149,7 +130,6 @@ export const notifyField = (container, message) => { const field = findOriginalField(container); if (!field) { - showWarning(message); return; } @@ -171,3 +151,35 @@ export const notifyAllAttachedFields = (message) => { document.querySelectorAll(ATTACHED_INSTANCE_SELECTOR).forEach((container) => notifyField(container, message)); }; + +/** + * Show a warning beside every container an editor environment finds, now and + * for any that appear later (e.g. the calendar's AJAX-loaded "New event" + * dialog, or an mform "Show more" section revealed after the initial scan). + * Used when the WProofreader bundle failed to load entirely, so there is no + * instance to attach and no ATTACHED_ATTR marker will ever be set the normal + * way - this sets it itself, to avoid re-notifying the same container. + * + * @param {Function} findContainers Returns the current list of containers an environment recognizes. + * @param {string} message Warning text to display. + */ +export const observeUnavailable = (findContainers, message) => { + if (!message) { + return; + } + + const scan = () => { + findContainers().forEach((container) => { + if (!container.hasAttribute(ATTACHED_ATTR)) { + container.setAttribute(ATTACHED_ATTR, '1'); + notifyField(container, message); + } + }); + }; + + scan(); + + if (typeof MutationObserver !== 'undefined') { + new MutationObserver(scan).observe(document.body, {childList: true, subtree: true}); + } +}; diff --git a/amd/src/proofreader_config.js b/amd/src/proofreader_config.js index 7bdda0a..5eea4f0 100644 --- a/amd/src/proofreader_config.js +++ b/amd/src/proofreader_config.js @@ -21,6 +21,22 @@ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +import {notifyField} from 'local_wproofreader/notify'; + +// Maps the HTTP status codes documented at +// https://docs.wproofreader.com/api-reference/http-response-status-codes to +// the config message key carrying the corresponding warning text. 503 reuses +// the existing service-unavailable message - same condition, whether reported +// as a transport failure or a real HTTP response. +const STATUS_MESSAGE_KEYS = { + 400: 'runtimeErrorBadRequestMessage', + 403: 'runtimeErrorForbiddenMessage', + 404: 'runtimeErrorNotFoundMessage', + 409: 'runtimeErrorConflictMessage', + 500: 'runtimeErrorServerMessage', + 503: 'runtimeServiceUnavailableMessage', +}; + const toBoolean = (value, fallback) => { if (value === true || value === 'true') { return true; @@ -95,6 +111,19 @@ export const apply = (config) => { // Older bundles may not expose subscribe; ignore. } }, + onErrorRequest: (error, instance) => { + const key = STATUS_MESSAGE_KEYS[error && error.status]; + const message = key && config[key]; + if (!message) { + return; + } + + try { + notifyField(instance.getContainerNode(), message); + } catch (e) { + // The container may have been detached by the host editor; safe to ignore. + } + }, }; if (Array.isArray(config.generalOptions) && config.generalOptions.length) { diff --git a/classes/external/save_languages.php b/classes/external/save_languages.php index fff6317..e856420 100644 --- a/classes/external/save_languages.php +++ b/classes/external/save_languages.php @@ -16,30 +16,30 @@ namespace local_wproofreader\external; -use core_external\external_api; -use core_external\external_function_parameters; -use core_external\external_single_structure; -use core_external\external_value; - /** * External function that stores the live language list from the WebSpellChecker service. * * The settings page calls this over AJAX after asking the WProofreader bundle * for its supported languages. Only site administrators can invoke it. * + * Extends the global-namespace external API classes (not core_external\...) + * since Moodle keeps those as class_alias() back-compat shims on every + * version that also has the newer namespace, while older versions (pre-4.2) + * only ever had the global names. + * * @package local_wproofreader * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class save_languages extends external_api { +class save_languages extends \external_api { /** * Parameter definition for save_languages. * - * @return external_function_parameters + * @return \external_function_parameters */ - public static function execute_parameters(): external_function_parameters { - return new external_function_parameters([ - 'payload' => new external_value(PARAM_RAW, 'JSON-encoded language list from the WebSpellChecker service.'), + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'payload' => new \external_value(PARAM_RAW, 'JSON-encoded language list from the WebSpellChecker service.'), ]); } @@ -73,12 +73,12 @@ public static function execute(string $payload): array { /** * Return value definition. * - * @return external_single_structure + * @return \external_single_structure */ - public static function execute_returns(): external_single_structure { - return new external_single_structure([ - 'stored' => new external_value(PARAM_BOOL, 'Whether a language list was successfully stored.'), - 'count' => new external_value(PARAM_INT, 'Number of languages stored.'), + public static function execute_returns(): \external_single_structure { + return new \external_single_structure([ + 'stored' => new \external_value(PARAM_BOOL, 'Whether a language list was successfully stored.'), + 'count' => new \external_value(PARAM_INT, 'Number of languages stored.'), ]); } diff --git a/classes/local/config_builder.php b/classes/local/config_builder.php index c917cb4..55aee5c 100644 --- a/classes/local/config_builder.php +++ b/classes/local/config_builder.php @@ -93,6 +93,11 @@ public static function build(): array { 'editorAttachErrorMessage' => get_string('editor_attach_error', 'local_wproofreader'), 'runtimeServiceUnavailableMessage' => get_string('runtime_service_unavailable', 'local_wproofreader'), 'runtimeLanguageUnsupportedMessage' => get_string('runtime_language_unsupported', 'local_wproofreader'), + 'runtimeErrorBadRequestMessage' => get_string('runtime_error_bad_request', 'local_wproofreader'), + 'runtimeErrorForbiddenMessage' => get_string('runtime_error_forbidden', 'local_wproofreader'), + 'runtimeErrorNotFoundMessage' => get_string('runtime_error_not_found', 'local_wproofreader'), + 'runtimeErrorConflictMessage' => get_string('runtime_error_conflict', 'local_wproofreader'), + 'runtimeErrorServerMessage' => get_string('runtime_error_server', 'local_wproofreader'), ]; if ($isfree) { diff --git a/lang/en/local_wproofreader.php b/lang/en/local_wproofreader.php index f6914b4..6717d5b 100644 --- a/lang/en/local_wproofreader.php +++ b/lang/en/local_wproofreader.php @@ -69,6 +69,11 @@ $string['privacy:metadata:wproofreader_service:language'] = 'The language code selected for proofreading.'; $string['privacy:metadata:wproofreader_service:useragent'] = 'Browser user agent string sent with each request to the service.'; $string['privacy:metadata:wproofreader_service:userip'] = 'The IP address from which the browser contacts the WebSpellChecker service.'; +$string['runtime_error_bad_request'] = '⚠️ WProofreader sent an invalid request to the service. Please report this issue to the site administrator.'; +$string['runtime_error_conflict'] = '⚠️ WProofreader encountered a conflicting request. Please report this issue to the site administrator.'; +$string['runtime_error_forbidden'] = '⚠️ WProofreader access was denied by the service. Check your license key and domain settings, or contact the site administrator.'; +$string['runtime_error_not_found'] = '⚠️ WProofreader requested a resource that was not found on the service. Please report this issue to the site administrator.'; +$string['runtime_error_server'] = '⚠️ WProofreader service encountered an internal error. Try again in a moment.'; $string['runtime_language_unsupported'] = '⚠️ WProofreader: selected language is not supported. Please report this issue to the site administrator.'; $string['runtime_service_unavailable'] = '⚠️ WProofreader service is temporarily unavailable. Try again in a moment.'; $string['service_id'] = 'License key'; @@ -77,7 +82,7 @@ $string['settings_contexts'] = 'Where to enable WProofreader'; $string['settings_contexts_desc'] = 'Choose which Moodle areas should use real-time spell, grammar, and style checking.'; $string['settings_editors'] = 'Editor support'; -$string['settings_editors_desc'] = 'WProofreader works with the Atto editor (Moodle 4.5 only), TinyMCE 6, and plain HTML textareas. No extra configuration is needed.'; +$string['settings_editors_desc'] = 'WProofreader works with the Atto editor (Moodle 4.1 through 4.5 LTS, removed from core from 5.0 onward), TinyMCE 6, the legacy TinyMCE editor (Moodle 4.1 only, removed from core in later versions), and plain HTML textareas. No extra configuration is needed.'; $string['settings_features'] = 'Proofreading features'; $string['settings_features_desc'] = 'Choose which checks WProofreader runs by default. Users can override these per editor from the badge menu, unless the feature is restricted by your license tier.'; $string['settings_general'] = 'General settings'; diff --git a/version.php b/version.php index 5836033..f94a421 100644 --- a/version.php +++ b/version.php @@ -27,6 +27,6 @@ $plugin->component = 'local_wproofreader'; $plugin->version = 2026081100; $plugin->release = '1.0.10'; -$plugin->requires = 2024100700; -$plugin->supported = [45, 52]; +$plugin->requires = 2022112800; +$plugin->supported = [41, 52]; $plugin->maturity = MATURITY_STABLE; From 018b198aacc1a0aa991e6a890a7b62344345c709 Mon Sep 17 00:00:00 2001 From: Artem Okladov Date: Thu, 20 Aug 2026 16:33:03 +0300 Subject: [PATCH 3/4] fix: Save Languages Issues in Versions > 4.1 --- classes/external/save_languages.php | 46 +++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/classes/external/save_languages.php b/classes/external/save_languages.php index e856420..efacf6d 100644 --- a/classes/external/save_languages.php +++ b/classes/external/save_languages.php @@ -16,30 +16,46 @@ namespace local_wproofreader\external; +// Resolve the external API classes to whichever form is actually loadable on +// this Moodle version. On 4.2+, the global-namespace names (external_api, +// etc.) only exist via a class_alias() in lib/externallib.php - and that +// alias is only registered once that file has been require()'d, which is not +// guaranteed by the time a webservice call dispatches to this class (that +// path throws "Class external_api not found" on 4.3/4.4/4.5/5.2, confirmed +// 2026-08-20). core_external\external_api itself is always autoloadable +// there, so prefer it and only fall back to the bare global names on +// pre-4.2 versions (4.1), which never had the core_external\ namespace. +if (class_exists('core_external\\external_api')) { + class_alias('core_external\\external_api', __NAMESPACE__ . '\\compat_external_api'); + class_alias('core_external\\external_function_parameters', __NAMESPACE__ . '\\compat_external_function_parameters'); + class_alias('core_external\\external_value', __NAMESPACE__ . '\\compat_external_value'); + class_alias('core_external\\external_single_structure', __NAMESPACE__ . '\\compat_external_single_structure'); +} else { + class_alias('external_api', __NAMESPACE__ . '\\compat_external_api'); + class_alias('external_function_parameters', __NAMESPACE__ . '\\compat_external_function_parameters'); + class_alias('external_value', __NAMESPACE__ . '\\compat_external_value'); + class_alias('external_single_structure', __NAMESPACE__ . '\\compat_external_single_structure'); +} + /** * External function that stores the live language list from the WebSpellChecker service. * * The settings page calls this over AJAX after asking the WProofreader bundle * for its supported languages. Only site administrators can invoke it. * - * Extends the global-namespace external API classes (not core_external\...) - * since Moodle keeps those as class_alias() back-compat shims on every - * version that also has the newer namespace, while older versions (pre-4.2) - * only ever had the global names. - * * @package local_wproofreader * @copyright 2026 WebSpellChecker * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class save_languages extends \external_api { +class save_languages extends compat_external_api { /** * Parameter definition for save_languages. * - * @return \external_function_parameters + * @return compat_external_function_parameters */ - public static function execute_parameters(): \external_function_parameters { - return new \external_function_parameters([ - 'payload' => new \external_value(PARAM_RAW, 'JSON-encoded language list from the WebSpellChecker service.'), + public static function execute_parameters(): compat_external_function_parameters { + return new compat_external_function_parameters([ + 'payload' => new compat_external_value(PARAM_RAW, 'JSON-encoded language list from the WebSpellChecker service.'), ]); } @@ -73,12 +89,12 @@ public static function execute(string $payload): array { /** * Return value definition. * - * @return \external_single_structure + * @return compat_external_single_structure */ - public static function execute_returns(): \external_single_structure { - return new \external_single_structure([ - 'stored' => new \external_value(PARAM_BOOL, 'Whether a language list was successfully stored.'), - 'count' => new \external_value(PARAM_INT, 'Number of languages stored.'), + public static function execute_returns(): compat_external_single_structure { + return new compat_external_single_structure([ + 'stored' => new compat_external_value(PARAM_BOOL, 'Whether a language list was successfully stored.'), + 'count' => new compat_external_value(PARAM_INT, 'Number of languages stored.'), ]); } From 446043b286120b22d17fa1663a2431d00a80f5fd Mon Sep 17 00:00:00 2001 From: Artem Okladov Date: Thu, 20 Aug 2026 16:57:35 +0300 Subject: [PATCH 4/4] fix: Linting & Versioning --- CHANGELOG.md | 7 +++++++ amd/build/proofreader_config.min.js.map | 2 +- amd/src/proofreader_config.js | 12 ++++++------ version.php | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e23af..0a89f22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the WProofreader plugin for Moodle are documented here. +## 2.0.0 (2026-08-20) + +* Widened official Moodle support to 4.1 through the 5.2 branch (previously 4.5 LTS through 5.2). +* Added support for the legacy TinyMCE editor (Moodle 4.1's "TinyMCE HTML editor (legacy)"). +* Removed the global badge option. +* Surfaced the WProofreader bundle's own runtime warnings (unsupported language, service unavailable) and the WebSpellChecker service's HTTP error responses (400/403/404/409/500/503) as a non-blocking, per-editor note instead of leaving them silent or shown only in the browser console. + ## 1.0.10 (2026-08-11) * Added warning messages for an exceeded usage limit and an invalid license key on the settings page. diff --git a/amd/build/proofreader_config.min.js.map b/amd/build/proofreader_config.min.js.map index 9013e96..b2037c8 100644 --- a/amd/build/proofreader_config.min.js.map +++ b/amd/build/proofreader_config.min.js.map @@ -1 +1 @@ -{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\n\n// Maps the HTTP status codes documented at\n// https://docs.wproofreader.com/api-reference/http-response-status-codes to\n// the config message key carrying the corresponding warning text. 503 reuses\n// the existing service-unavailable message - same condition, whether reported\n// as a transport failure or a real HTTP response.\nconst STATUS_MESSAGE_KEYS = {\n 400: 'runtimeErrorBadRequestMessage',\n 403: 'runtimeErrorForbiddenMessage',\n 404: 'runtimeErrorNotFoundMessage',\n 409: 'runtimeErrorConflictMessage',\n 500: 'runtimeErrorServerMessage',\n 503: 'runtimeServiceUnavailableMessage',\n};\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n // Plain textareas are attached explicitly by\n // local_wproofreader/environment_textarea instead of the bundle's\n // own autoSearch, which only scans once at load and misses fields\n // revealed later (e.g. an mform \"Show more\" advanced section).\n disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'],\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, false),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n onErrorRequest: (error, instance) => {\n const key = STATUS_MESSAGE_KEYS[error && error.status];\n const message = key && config[key];\n if (!message) {\n return;\n }\n\n try {\n notifyField(instance.getContainerNode(), message);\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["STATUS_MESSAGE_KEYS","toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","onErrorRequest","error","key","status","message","generalOptions"],"mappings":";;;;;;;;MA8BMA,oBAAsB,KACnB,oCACA,mCACA,kCACA,kCACA,gCACA,oCAGHC,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aAKbgB,oBAAqB,IAAIvB,QAAQG,OAAOoB,qBAAsB,YAC9DC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,MAIbC,eAAgB,CAACC,MAAOT,kBACdU,IAAMrD,oBAAoBoD,OAASA,MAAME,QACzCC,QAAUF,KAAO9C,OAAO8C,QACzBE,oCAKWZ,SAASG,mBAAoBS,SAC3C,MAAOL,OAMb7C,MAAMC,QAAQC,OAAOiD,iBAAmBjD,OAAOiD,eAAe3C,SAC9DC,OAAOC,uBAAuByC,eAAiBjD,OAAOiD"} \ No newline at end of file +{"version":3,"file":"proofreader_config.min.js","sources":["../src/proofreader_config.js"],"sourcesContent":["// This file is part of Moodle - https://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 * Builds the global WEBSPELLCHECKER_CONFIG object the bundle reads at startup.\n *\n * @module local_wproofreader/proofreader_config\n * @copyright 2026 WebSpellChecker\n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {notifyField} from 'local_wproofreader/notify';\n\n// Maps the HTTP status codes documented at\n// https://docs.wproofreader.com/api-reference/http-response-status-codes to\n// the config message key carrying the corresponding warning text. 503 reuses\n// the existing service-unavailable message - same condition, whether reported\n// as a transport failure or a real HTTP response.\nconst STATUS_MESSAGE_KEYS = {\n '400': 'runtimeErrorBadRequestMessage',\n '403': 'runtimeErrorForbiddenMessage',\n '404': 'runtimeErrorNotFoundMessage',\n '409': 'runtimeErrorConflictMessage',\n '500': 'runtimeErrorServerMessage',\n '503': 'runtimeServiceUnavailableMessage',\n};\n\nconst toBoolean = (value, fallback) => {\n if (value === true || value === 'true') {\n return true;\n }\n if (value === false || value === 'false') {\n return false;\n }\n return fallback;\n};\n\nconst toArray = (value) => Array.isArray(value) ? value : [];\n\n/**\n * Write the WebSpellChecker config to window.WEBSPELLCHECKER_CONFIG.\n *\n * @param {Object} config Server-supplied config payload.\n */\nexport const apply = (config) => {\n const isBadgeEnabled = toBoolean(config.enableBadgeButton, true);\n const defaultBadgeActions = isBadgeEnabled\n ? ['addWord', 'ignoreAll', 'settings', 'toggle', 'proofreadDialog']\n : ['addWord', 'ignoreAll', 'settings', 'proofreadDialog'];\n const badgeActions = toArray(config.actionItems).length\n ? config.actionItems\n : defaultBadgeActions;\n\n window.WEBSPELLCHECKER_CONFIG = {\n autoSearch: toBoolean(config.autoSearch, true),\n appType: config.appType,\n serviceProtocol: config.serviceProtocol || 'https',\n serviceHost: config.serviceHost || 'svc.webspellchecker.net',\n servicePath: config.servicePath || 'api',\n servicePort: config.servicePort || '443',\n enableGrammar: toBoolean(config.enableGrammar, false),\n aiWritingAssistant: toBoolean(config.aiWritingAssistant, false),\n settingsSections: toArray(config.settingsSections),\n serviceId: config.serviceId,\n lang: config.lang,\n enableBadgeButton: isBadgeEnabled,\n actionItems: badgeActions,\n // Plain textareas are attached explicitly by\n // local_wproofreader/environment_textarea instead of the bundle's\n // own autoSearch, which only scans once at load and misses fields\n // revealed later (e.g. an mform \"Show more\" advanced section).\n disableAutoSearchIn: [...toArray(config.disableAutoSearchIn), 'textarea'],\n disableOptionsStorage: toArray(config.disableOptionsStorage),\n disableDictionariesPreferences: toBoolean(config.disableDictionariesPreferences, false),\n autocomplete: toBoolean(config.autocomplete, false),\n autocorrect: toBoolean(config.autocorrect, false),\n spellingSuggestions: toBoolean(config.spellingSuggestions, true),\n grammarSuggestions: toBoolean(config.grammarSuggestions, false),\n styleGuideSuggestions: toBoolean(config.styleGuideSuggestions, true),\n ignoreAllCapsWords: toBoolean(config.ignoreAllCapsWords, true),\n ignoreDomainNames: toBoolean(config.ignoreDomainNames, true),\n ignoreWordsWithMixedCases: toBoolean(config.ignoreWordsWithMixedCases, true),\n ignoreWordsWithNumbers: toBoolean(config.ignoreWordsWithNumbers, true),\n globalBadge: toBoolean(config.globalBadge, false),\n compactBadge: toBoolean(config.compactBadge, true),\n allSuggestionsMode: toBoolean(config.allSuggestionsMode, true),\n onLoad: function() {\n const instance = this;\n try {\n this.subscribe('replaceProblem', () => {\n try {\n const element = instance.getContainerNode();\n element.dispatchEvent(new Event('input', {bubbles: true}));\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n });\n } catch (e) {\n // Older bundles may not expose subscribe; ignore.\n }\n },\n onErrorRequest: (error, instance) => {\n const key = STATUS_MESSAGE_KEYS[error && error.status];\n const message = key && config[key];\n if (!message) {\n return;\n }\n\n try {\n notifyField(instance.getContainerNode(), message);\n } catch (e) {\n // The container may have been detached by the host editor; safe to ignore.\n }\n },\n };\n\n if (Array.isArray(config.generalOptions) && config.generalOptions.length) {\n window.WEBSPELLCHECKER_CONFIG.generalOptions = config.generalOptions;\n }\n};\n"],"names":["STATUS_MESSAGE_KEYS","toBoolean","value","fallback","toArray","Array","isArray","config","isBadgeEnabled","enableBadgeButton","defaultBadgeActions","badgeActions","actionItems","length","window","WEBSPELLCHECKER_CONFIG","autoSearch","appType","serviceProtocol","serviceHost","servicePath","servicePort","enableGrammar","aiWritingAssistant","settingsSections","serviceId","lang","disableAutoSearchIn","disableOptionsStorage","disableDictionariesPreferences","autocomplete","autocorrect","spellingSuggestions","grammarSuggestions","styleGuideSuggestions","ignoreAllCapsWords","ignoreDomainNames","ignoreWordsWithMixedCases","ignoreWordsWithNumbers","globalBadge","compactBadge","allSuggestionsMode","onLoad","instance","this","subscribe","getContainerNode","dispatchEvent","Event","bubbles","e","onErrorRequest","error","key","status","message","generalOptions"],"mappings":";;;;;;;;MA8BMA,oBAAsB,KACjB,oCACA,mCACA,kCACA,kCACA,gCACA,oCAGLC,UAAY,CAACC,MAAOC,YACR,IAAVD,OAA4B,SAAVA,QAGR,IAAVA,OAA6B,UAAVA,OAGhBC,SAGLC,QAAWF,OAAUG,MAAMC,QAAQJ,OAASA,MAAQ,kBAOpCK,eACZC,eAAiBP,UAAUM,OAAOE,mBAAmB,GACrDC,oBAAsBF,eACtB,CAAC,UAAW,YAAa,WAAY,SAAU,mBAC/C,CAAC,UAAW,YAAa,WAAY,mBACrCG,aAAeP,QAAQG,OAAOK,aAAaC,OAC3CN,OAAOK,YACPF,oBAENI,OAAOC,uBAAyB,CAC5BC,WAAYf,UAAUM,OAAOS,YAAY,GACzCC,QAASV,OAAOU,QAChBC,gBAAiBX,OAAOW,iBAAmB,QAC3CC,YAAaZ,OAAOY,aAAe,0BACnCC,YAAab,OAAOa,aAAe,MACnCC,YAAad,OAAOc,aAAe,MACnCC,cAAerB,UAAUM,OAAOe,eAAe,GAC/CC,mBAAoBtB,UAAUM,OAAOgB,oBAAoB,GACzDC,iBAAkBpB,QAAQG,OAAOiB,kBACjCC,UAAWlB,OAAOkB,UAClBC,KAAMnB,OAAOmB,KACbjB,kBAAmBD,eACnBI,YAAaD,aAKbgB,oBAAqB,IAAIvB,QAAQG,OAAOoB,qBAAsB,YAC9DC,sBAAuBxB,QAAQG,OAAOqB,uBACtCC,+BAAgC5B,UAAUM,OAAOsB,gCAAgC,GACjFC,aAAc7B,UAAUM,OAAOuB,cAAc,GAC7CC,YAAa9B,UAAUM,OAAOwB,aAAa,GAC3CC,oBAAqB/B,UAAUM,OAAOyB,qBAAqB,GAC3DC,mBAAoBhC,UAAUM,OAAO0B,oBAAoB,GACzDC,sBAAuBjC,UAAUM,OAAO2B,uBAAuB,GAC/DC,mBAAoBlC,UAAUM,OAAO4B,oBAAoB,GACzDC,kBAAmBnC,UAAUM,OAAO6B,mBAAmB,GACvDC,0BAA2BpC,UAAUM,OAAO8B,2BAA2B,GACvEC,uBAAwBrC,UAAUM,OAAO+B,wBAAwB,GACjEC,YAAatC,UAAUM,OAAOgC,aAAa,GAC3CC,aAAcvC,UAAUM,OAAOiC,cAAc,GAC7CC,mBAAoBxC,UAAUM,OAAOkC,oBAAoB,GACzDC,OAAQ,iBACEC,SAAWC,cAERC,UAAU,kBAAkB,SAETF,SAASG,mBACjBC,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,KACrD,MAAOC,QAIf,MAAOA,MAIbC,eAAgB,CAACC,MAAOT,kBACdU,IAAMrD,oBAAoBoD,OAASA,MAAME,QACzCC,QAAUF,KAAO9C,OAAO8C,QACzBE,oCAKWZ,SAASG,mBAAoBS,SAC3C,MAAOL,OAMb7C,MAAMC,QAAQC,OAAOiD,iBAAmBjD,OAAOiD,eAAe3C,SAC9DC,OAAOC,uBAAuByC,eAAiBjD,OAAOiD"} \ No newline at end of file diff --git a/amd/src/proofreader_config.js b/amd/src/proofreader_config.js index 5eea4f0..49844c5 100644 --- a/amd/src/proofreader_config.js +++ b/amd/src/proofreader_config.js @@ -29,12 +29,12 @@ import {notifyField} from 'local_wproofreader/notify'; // the existing service-unavailable message - same condition, whether reported // as a transport failure or a real HTTP response. const STATUS_MESSAGE_KEYS = { - 400: 'runtimeErrorBadRequestMessage', - 403: 'runtimeErrorForbiddenMessage', - 404: 'runtimeErrorNotFoundMessage', - 409: 'runtimeErrorConflictMessage', - 500: 'runtimeErrorServerMessage', - 503: 'runtimeServiceUnavailableMessage', + '400': 'runtimeErrorBadRequestMessage', + '403': 'runtimeErrorForbiddenMessage', + '404': 'runtimeErrorNotFoundMessage', + '409': 'runtimeErrorConflictMessage', + '500': 'runtimeErrorServerMessage', + '503': 'runtimeServiceUnavailableMessage', }; const toBoolean = (value, fallback) => { diff --git a/version.php b/version.php index f94a421..2ca36e1 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'local_wproofreader'; -$plugin->version = 2026081100; -$plugin->release = '1.0.10'; +$plugin->version = 2026082000; +$plugin->release = '2.0.0'; $plugin->requires = 2022112800; $plugin->supported = [41, 52]; $plugin->maturity = MATURITY_STABLE;