diff --git a/ep.json b/ep.json index 0c4f9c3..5f77dc3 100644 --- a/ep.json +++ b/ep.json @@ -2,12 +2,15 @@ "parts": [ { "name": "ep_spellcheck", - "client_hooks": { - "postAceInit": "ep_spellcheck/static/js/spellcheck:postAceInit" - }, "hooks": { + "loadSettings": "ep_spellcheck/spellcheck", + "clientVars": "ep_spellcheck/spellcheck", "eejsBlock_mySettings": "ep_spellcheck/spellcheck", - "eejsBlock_dd_view" : "ep_spellcheck/spellcheck" + "eejsBlock_padSettings": "ep_spellcheck/spellcheck" + }, + "client_hooks": { + "postAceInit": "ep_spellcheck/static/js/spellcheck:postAceInit", + "handleClientMessage_CLIENT_MESSAGE": "ep_spellcheck/static/js/spellcheck:handleClientMessage_CLIENT_MESSAGE" } } ] diff --git a/package.json b/package.json index 109a0f2..f1decd0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "type": "individual", "url": "https://etherpad.org/" }, + "dependencies": { + "ep_plugin_helpers": "^0.5.2" + }, "devDependencies": { "eslint": "^8.57.1", "eslint-config-etherpad": "^4.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a1425a..85bb5b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + ep_plugin_helpers: + specifier: ^0.5.2 + version: 0.5.2 devDependencies: eslint: specifier: ^8.57.1 @@ -400,6 +404,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + ep_plugin_helpers@0.5.2: + resolution: {integrity: sha512-6M0gYqRTaF2SnTvV1OiqOZQ4B37ZHL0Zjoz3/eOIAXeCEBA4jId62uz9uboLs87Qcfae7eirDYWpdir5u5zkEQ==} + engines: {node: '>=18.0.0'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1673,6 +1681,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + ep_plugin_helpers@0.5.2: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 diff --git a/spellcheck.js b/spellcheck.js index 6155bbe..2a842ec 100644 --- a/spellcheck.js +++ b/spellcheck.js @@ -1,21 +1,29 @@ 'use strict'; -const eejs = require('ep_etherpad-lite/node/eejs/'); -const settings = require('ep_etherpad-lite/node/utils/Settings'); +const {padToggle} = require('ep_plugin_helpers/pad-toggle-server'); -exports.eejsBlock_mySettings = (hookName, args, cb) => { - let checkedState = 'checked'; - if (settings.ep_spellcheck) { - if (settings.ep_spellcheck.disabledByDefault === true) { - checkedState = ''; - } +// Parallel User Settings + Pad Wide Settings checkboxes for "Spell Check". +// Helper owns markup, storage, broadcast, enforce, and i18n wiring. +const spellcheckToggle = padToggle({ + pluginName: 'ep_spellcheck', + settingId: 'spellcheck', + l10nId: 'ep_spellcheck.spellcheck', + defaultLabel: 'Spell Check', + defaultEnabled: true, +}); + +// Older settings.json used `ep_spellcheck.disabledByDefault: true` to flip +// the checkbox off. Translate to the helper's `defaultEnabled` so existing +// installs keep their current behavior after the conversion. +exports.loadSettings = async (hookName, args) => { + const ps = args && args.settings && args.settings.ep_spellcheck; + if (ps && typeof ps.defaultEnabled !== 'boolean' && + typeof ps.disabledByDefault === 'boolean') { + ps.defaultEnabled = !ps.disabledByDefault; } - const ejsPath = 'ep_spellcheck/templates/spellcheck_entry.ejs'; - args.content += eejs.require(ejsPath, {checked: checkedState}); - return cb(); + return spellcheckToggle.loadSettings(hookName, args); }; -exports.eejsBlock_dd_view = (hookName, args, cb) => { - const li = "
  • Spell Check
  • "; - args.content += li; -}; +exports.clientVars = spellcheckToggle.clientVars; +exports.eejsBlock_mySettings = spellcheckToggle.eejsBlock_mySettings; +exports.eejsBlock_padSettings = spellcheckToggle.eejsBlock_padSettings; diff --git a/static/js/spellcheck.js b/static/js/spellcheck.js index f5cb365..c747178 100644 --- a/static/js/spellcheck.js +++ b/static/js/spellcheck.js @@ -1,51 +1,35 @@ 'use strict'; -const padcookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie; +// Sub-path import keeps the client bundle clean — the top-level +// `ep_plugin_helpers` index pulls in server-only modules (eejs, Settings). +const {padToggle} = require('ep_plugin_helpers/pad-toggle'); -const postAceInit = (hook, context) => { - const $outer = $('iframe[name="ace_outer"]').contents().find('iframe'); - const $inner = $outer.contents().find('#innerdocbody'); - // The `spellcheck` attribute is inherited from the nearest ancestor that - // sets it, so we only need to toggle it on #innerdocbody. The previous - // implementation walked every
    / descendant on every toggle, - // which is O(n) in line count and multiplied the browser's already-slow - // per-keystroke spellchecking work — observable as typing lag on pads - // with more than a few hundred lines (#26). Setting the attribute once - // on the contenteditable root is equivalent for the browser's checker. - const spellcheck = { - enable: () => { $inner.attr('spellcheck', 'true'); }, - disable: () => { $inner.attr('spellcheck', 'false'); }, - }; - /* init */ - if (padcookie.getPref('spellcheck') === false) { - $('#options-spellcheck').val(); - $('#options-spellcheck').attr('checked', 'unchecked'); - $('#options-spellcheck').attr('checked', false); - } else { - $('#options-spellcheck').attr('checked', 'checked'); - } +// Same config as the server-side instance — must agree on pluginName, +// settingId, l10nId, and defaultLabel so checkbox ids and clientVars line up. +const spellcheckToggle = padToggle({ + pluginName: 'ep_spellcheck', + settingId: 'spellcheck', + l10nId: 'ep_spellcheck.spellcheck', + defaultLabel: 'Spell Check', + defaultEnabled: true, +}); + +// Re-export so the helper sees pad-wide broadcasts and refreshes our state +// when another user toggles the pad-wide checkbox. +exports.handleClientMessage_CLIENT_MESSAGE = spellcheckToggle.handleClientMessage_CLIENT_MESSAGE; - if ($('#options-spellcheck').is(':checked')) { - spellcheck.enable(); - } else { - spellcheck.disable(); - } +exports.postAceInit = () => { + // The `spellcheck` attribute is inherited from the nearest ancestor that + // sets it, so we only need to toggle it on #innerdocbody. Walking every + //
    / descendant (the previous behavior) is O(n) in line count + // and multiplied the browser's already-slow per-keystroke spellchecking + // work — observable as typing lag on pads with > a few hundred lines (#26). + const $inner = $('iframe[name="ace_outer"]').contents().find('iframe') + .contents().find('#innerdocbody'); - /* on click */ - $('#options-spellcheck').on('click', () => { - if ($('#options-spellcheck').is(':checked')) { - padcookie.setPref('spellcheck', true); - spellcheck.enable(); - } else { - padcookie.setPref('spellcheck', false); - spellcheck.disable(); - } - // The previous code force-reloaded the page on Chrome via - // `window.browser.chrome`. That check was always a no-op on Chrome - // (Chrome has no `window.browser` object — it's the Firefox - // WebExtension API) and would have thrown a TypeError on Firefox. - // Toggling the spellcheck attribute takes effect in every current - // browser without a reload, so just drop the reload. + spellcheckToggle.init({ + onChange: (enabled) => { + $inner.attr('spellcheck', enabled ? 'true' : 'false'); + }, }); }; -exports.postAceInit = postAceInit; diff --git a/templates/spellcheck_entry.ejs b/templates/spellcheck_entry.ejs deleted file mode 100644 index 4db9191..0000000 --- a/templates/spellcheck_entry.ejs +++ /dev/null @@ -1,4 +0,0 @@ -

    - > - -