diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..50d7077
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+/.vscode export-ignore
+/.gitignore export-ignore
+/.gitattributes export-ignore
+EM.code-workspace export-ignore
+package-lock.json export-ignore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+
diff --git a/ExternalModule.php b/ExternalModule.php
deleted file mode 100644
index a95406a..0000000
--- a/ExternalModule.php
+++ /dev/null
@@ -1,149 +0,0 @@
-getSystemSetting("su_only") && !SUPER_USER) return null;
- return true;
- }
-
- function redcap_data_entry_form_top($project_id, $record = null, $instrument, $event_id, $group_id = null, $repeat_instance = 1) {
- $this->injectJS("data_entry", $instrument);
- }
-
- function redcap_survey_page_top($project_id, $record = null, $instrument, $event_id, $group_id = null, $survey_hash, $response_id = null, $repeat_instance = 1) {
- $this->injectJS("survey", $instrument);
- }
-
- function redcap_project_home_page ($project_id) {
- $this->injectJS("php", null);
- }
-
- function redcap_every_page_top ($project_id) {
- if (PAGE === "DataEntry/record_status_dashboard.php") {
- $this->injectJS("rsd", null);
- }
- else if (PAGE === "DataEntry/record_home.php" && !isset($_GET["id"])) {
- $this->injectJS("aer", null);
- }
- else if (PAGE === "DataEntry/record_home.php" && isset($_GET["id"])) {
- $this->injectJS("rhp", null);
- }
- else if (strpos(PAGE, "ProjectDashController:view") !== false && isset($_GET["dash_id"])) {
- $this->injectJS("db", null);
- }
- else if (strpos(PAGE, "surveys/index.php") !== false && isset($_GET["__dashboard"])) {
- $this->injectJS("dbp", null);
- }
- else if (strpos(PAGE, "DataExport/index.php") !== false && isset($_GET["report_id"])) {
- $this->injectJS("report", null);
- }
- // All project pages.
- if ($project_id !== null) {
- $this->injectJS("all", null);
- }
- }
-
- /**
- * Inject JS code.
- *
- * @param string $type
- * Accepted types: 'data_entry' or 'survey'.
- * @param string $instrument
- * The instrument name.
- */
- function injectJS($type, $instrument) {
- $settings = $this->getFormattedSettings(PROJECT_ID);
-
- if (empty($settings["js"])) {
- return;
- }
-
- foreach ($settings["js"] as $row) {
- if (empty($row["js_enabled"])) continue;
- $inject = strpos($row["js_type"], $type) !== false;
- if (strpos("survey,data_entry", $row["js_type"]) !== false) {
- // Check instrument.
- $inject = $inject && (!array_filter($row["js_instruments"]) || in_array($instrument, $row["js_instruments"], true));
- }
- if ($inject) {
- echo "";
- }
- }
- }
-
-
- /**
- * The code for getFormattedSettings and _getFormattedSettings
- * originates from https://github.com/ctsit/redcap_css_injector
- * as of March 27, 2019.
- */
-
- /**
- * Formats settings into a hierarchical key-value pair array.
- *
- * @param int $project_id
- * Enter a project ID to get project settings.
- * Leave blank to get system settings.
- *
- * @return array
- * The formatted settings.
- */
- function getFormattedSettings($project_id = null) {
- $settings = $this->getConfig();
-
- if ($project_id) {
- $settings = $settings['project-settings'];
- $values = ExternalModules::getProjectSettingsAsArray($this->PREFIX, $project_id);
- }
- else {
- $settings = $settings['system-settings'];
- $values = ExternalModules::getSystemSettingsAsArray($this->PREFIX);
- }
-
- return $this->_getFormattedSettings($settings, $values);
- }
-
- /**
- * Auxiliary function for getFormattedSettings().
- */
- protected function _getFormattedSettings($settings, $values, $inherited_deltas = []) {
- $formatted = [];
-
- foreach ($settings as $setting) {
- $key = $setting['key'];
- $value = $values[$key]['value'];
- if ($value == null) continue;
-
- foreach ($inherited_deltas as $delta) {
- $value = $value[$delta];
- }
-
- if ($setting['type'] == 'sub_settings') {
- $deltas = array_keys($value);
- $value = [];
-
- foreach ($deltas as $delta) {
- $sub_deltas = array_merge($inherited_deltas, [$delta]);
- $value[$delta] = $this->_getFormattedSettings($setting['sub_settings'], $values, $sub_deltas);
- }
-
- if (empty($setting['repeatable'])) {
- $value = $value[0];
- }
- }
-
- $formatted[$key] = $value;
- }
-
- return $formatted;
- }
-}
diff --git a/JSInjectorExternalModule.php b/JSInjectorExternalModule.php
new file mode 100644
index 0000000..8e5daa6
--- /dev/null
+++ b/JSInjectorExternalModule.php
@@ -0,0 +1,453 @@
+fw = $this->framework;
+ }
+
+ #region Hooks
+
+ // Workaround for the redcap_module_system_change_version hook not working
+ function redcap_module_link_check_display($project_id, $link) {
+ if ($this->getSystemSetting("v1-upgrade") != "done") {
+ $this->convert_v1_settings();
+ }
+ return null;
+ }
+
+ // Perform settings upgrade to v2 model
+ function redcap_module_system_change_version($version, $old_version) {
+ $major = explode(".", trim($old_version, "v"))[0] * 1;
+ if ($major == 1) {
+ $this->convert_v1_settings();
+ }
+ }
+
+ // Insert JSMO name into config dialog
+ function redcap_module_configuration_settings($project_id, $settings) {
+ $key = $project_id == null ? "sys-jsmo-info" : "proj-jsmo-info";
+ $jsmo = $this->getJavascriptModuleObjectName();
+ foreach ($settings as &$setting) {
+ foreach ($setting["sub_settings"] as &$sub_setting) {
+ if ($sub_setting["key"] === $key) {
+ $sub_setting["name"] = str_replace("#JSMO#", $jsmo, $sub_setting["name"]);
+ }
+ }
+ }
+ return $settings;
+ }
+
+ // Set visibility of Configure button in projects
+ function redcap_module_configure_button_display() {
+ if ($this->getSystemSetting("su_only") && !SUPER_USER) return null;
+ return true;
+ }
+
+ // Determine context and inject
+ function redcap_every_page_top ($project_id) {
+ $page = defined("PAGE") ? PAGE : "";
+ $instrument = null;
+ $context = [
+ "cc" => false,
+ "todo" => false,
+ "langs" => false,
+ "browseprojects" => false,
+ "browseusers" => false,
+ "edituser" => false,
+ "emailusers" => false,
+ "login" => false,
+ "php" => false,
+ "rsd" => false,
+ "aer" => false,
+ "rhp" => false,
+ "data_entry" => false,
+ "survey" => false,
+ "report" => false,
+ "db" => false,
+ "dbp" => false,
+ ];
+
+ // System context
+ if ($project_id == null) {
+ if (!defined("USERID")) {
+ $context["login"] = true;
+ }
+ else {
+ if ($page === "ToDoList/index.php") {
+ $context["todo"] = true;
+ $context["cc"] = true;
+ }
+ else if ($page === "LanguageUpdater/index.php") {
+ $context["langs"] = true;
+ $context["cc"] = true;
+ }
+ else if ($page === "ControlCenter/view_projects.php") {
+ $context["browseprojects"] = true;
+ $context["cc"] = true;
+ }
+ else if ($page === "ControlCenter/view_users.php") {
+ $context["browseusers"] = true;
+ $context["cc"] = true;
+ }
+ else if ($page === "ControlCenter/create_user.php") {
+ $context["edituser"] = true;
+ $context["cc"] = true;
+ }
+ else if ($page === "ControlCenter/email_users.php") {
+ $context["emailusers"] = true;
+ $context["cc"] = true;
+ }
+ else if (starts_with($page, "ControlCenter/")) {
+ $context["cc"] = true;
+ }
+ else if ($page === "FhirStatsController:index") {
+ $context["cc"] = true;
+ }
+ else if ($page === "MultiLanguageController:systemConfig") {
+ $context["cc"] = true;
+ }
+ else if (self::IsSystemExternalModulesManager($page)) {
+ $context["cc"] = true;
+ }
+ }
+ }
+ // Project context
+ else {
+ $Proj = new \Project();
+ if ($page === "surveys/index.php") {
+ if (isset($_GET["page"])) {
+ $context["survey"] = true;
+ $instrument = isset($Proj->forms[$_GET["page"]]) ? $_GET["page"] : null;
+ }
+ else if (isset($_GET["__dashboard"])) {
+ $context["dbp"] = true;
+ }
+ }
+ else if (!defined("USERID")) {
+ $context["login"] = true;
+ }
+ else {
+ if ($page === "DataEntry/index.php") {
+ $context["data_entry"] = true;
+ $instrument = isset($Proj->forms[$_GET["page"]]) ? $_GET["page"] : null;
+ }
+ else if ($page == "index.php") {
+ $context["php"] = true;
+ }
+ else if ($page == "DataEntry/record_status_dashboard.php") {
+ $context["rsd"] = true;
+ }
+ else if ($page == "DataEntry/record_home.php") {
+ if (isset($_GET["id"])) {
+ $context["rhp"] = true;
+ }
+ else {
+ $context["aer"] = true;
+ }
+ }
+ else if ($page == "ProjectDashController:view") {
+ $context["db"] = true;
+ }
+ else if ($page == "DataExport/index.php" && isset($_GET["report_id"]) && !isset($_GET["addedit"])) {
+ $context["report"] = true;
+ }
+ }
+ }
+ // Inject
+ $this->inject_js($project_id, $context, $instrument);
+ }
+
+ #endregion
+
+ /**
+ * Inject JS code based on context.
+ *
+ * @param string|null $project_id
+ * @param array $context
+ * @param string $instrument
+ */
+ function inject_js($project_id, $context, $instrument) {
+
+ // Get "work schedule" ;)
+ $snippets = $this->parse_settings($project_id, array_keys($context));
+
+ // Determine if this is a "named" context
+ $named_context = array_reduce($context, function($carry, $item) {
+ return $carry || $item;
+ }, false);
+ // Then, get the context name
+ $context_names = array_keys(array_filter($context, function($v) {
+ return $v;
+ }));
+
+ $inject_jsmo = false;
+ $debug_jsmo = false;
+
+ // Verify snippets
+ $snippets_to_inject = [];
+ foreach ($snippets as $snippet) {
+ $inject = false;
+ $reject = false;
+
+ // Project context? Check project list and enabled state, but not for "login"
+ if ($project_id != null && !in_array("login", $context_names, true)) {
+ if (!$snippet["proj-enabled"]) {
+ $reject = true;
+ }
+ else if ($snippet["proj-limit"] == "include") {
+ if (!in_array($project_id, $snippet["proj-list"], true)) {
+ $reject = true;
+ }
+ }
+ else if ($snippet["proj-limit"] == "exclude") {
+ if (in_array($project_id, $snippet["proj-list"], true)) {
+ $reject = true;
+ }
+ }
+ }
+ else {
+ if (!$snippet["sys-enabled"]) {
+ $reject = true;
+ }
+ }
+ // Rejected? Continue to next item
+ if ($reject) continue;
+
+ // Page type / context
+ if ($named_context) {
+ // Check if there is a named match
+ foreach ($context_names as $this_ctx) {
+ // Additionally evaluate form for data entry and survey pages
+ $instrument_check = true;
+ if ($snippet["type"] == "proj" && in_array($this_ctx, ["data_entry","survey"], true)) {
+ if (count($snippet["form-list"])) {
+ $instrument_check = in_array($instrument, $snippet["form-list"], true);
+ }
+ }
+ $inject = $inject || ($snippet["ctx"][$this_ctx] && $instrument_check);
+ }
+ }
+ else {
+ // Check if the snippet should always be injected
+ $inject = $snippet["ctx"][$project_id == null ? "sysall" : "projall"];
+ }
+
+ if ($inject) {
+ $snippets_to_inject[] = $snippet;
+ $inject_jsmo = $inject_jsmo || $snippet["jsmo"];
+ $debug_jsmo = $debug_jsmo || ($snippet["jsmo"] && $snippet["debug"]);
+ }
+ }
+
+ // JSMO
+ if ($inject_jsmo) {
+ $this->initializeJavascriptModuleObject();
+ if ($debug_jsmo) {
+ $jsmo_name = $this->fw->getJavascriptModuleObjectName();
+ print "\n";
+ }
+ }
+
+ // Inject snippets (after JSMO)
+ foreach ($snippets_to_inject as $snippet) {
+ // Add debug info
+ $info = $snippet["debug"] ? " data-from=\"REDCap JavaScript Injector {$this->VERSION}\"" : "";
+ $context = $snippet["debug"] ? " data-context=\"{$snippet["type"]}\"" : "";
+ $name = $snippet["debug"] ? (" data-name=\"" . js_escape($snippet["name"]) . "\"") : "";
+ // Actual injection
+ print "\n";
+ // More debug info, after the fact
+ if ($snippet["debug"]) {
+ print "\n";
+ }
+ }
+ }
+
+ #region Settings Parser
+
+ /**
+ * Parses project and system settings into a useable format.
+ * @param string|null $project_id
+ * @return array
+ */
+ function parse_settings($project_id = null, $contexts) {
+ // Make a list of all snippets
+ $snippets = [];
+ // Load settings
+ $ss = $this->getSystemSettings();
+ $ps = $project_id == null ? [ "proj-snippet" => []] : $this->getProjectSettings($project_id);
+ // Parse system settings
+ foreach ($ss["sys-snippet"]["system_value"] as $i => $_) {
+ $snippet = [
+ "type" => "sys"
+ ];
+ $snippet["name"] = $ss["sys-name"]["system_value"][$i];
+ if (empty($snippet["name"])) {
+ $snippet["name"] = "";
+ }
+ $snippet["jsmo"] = $ss["sys-jsmo"]["system_value"][$i] == true;
+ $snippet["debug"] = $ss["sys-debug"]["system_value"][$i] == true;
+ $snippet["code"] = $ss["sys-code"]["system_value"][$i] ?? "";
+ $snippet["sys-enabled"] = $ss["sys-enabled"]["system_value"][$i] == true;
+ $snippet["proj-enabled"] = $ss["sys-proj-enabled"]["system_value"][$i] == true;
+ $snippet["proj-limit"] = "all";
+ $snippet["proj-list"] = [];
+ $snippet["form-list"] = [];
+ if ($snippet["proj-enabled"]) {
+ $limit = $ss["sys-proj-limit"]["system_value"][$i];
+ $snippet["proj-limit"] = in_array($limit, ["include","exclude"], true) ? $limit : "all";
+ $snippet["proj-list"] = array_unique(explode(",", trim($ss["sys-proj-list"]["system_value"][$i] ?? "")));
+ }
+ $snippet["ctx"]["projall"] = $ss["sys-proj-context_all"]["system_value"][$i] == true;
+ $snippet["ctx"]["sysall"] = $ss["sys-context_all"]["system_value"][$i] == true;
+ foreach ($contexts as $this_context) {
+ $snippet["ctx"][$this_context] = false;
+ }
+ foreach ($ss as $this_key => $_) {
+ $this_context = array_pop(explode("_", $this_key, 2));
+ if (in_array($this_context, $contexts, true)) {
+ $val = $ss[$this_key]["system_value"][$i];
+ if ($val == "include") {
+ $snippet["ctx"][$this_context] = true;
+ }
+ else if ($val <> "exclude") {
+ $snippet["ctx"][$this_context] = contains($this_key, "proj-context") ? $snippet["ctx"]["projall"] : $snippet["ctx"]["sysall"];
+ }
+ }
+ }
+ $snippets[] = $snippet;
+ }
+ // Parse project settings
+ foreach ($ps["proj-snippet"] as $i => $_) {
+ $snippet = [
+ "type" => "proj"
+ ];
+ $snippet["name"] = $ps["proj-name"][$i];
+ if (empty($snippet["name"])) {
+ $snippet["name"] = "";
+ }
+ $snippet["sys-enabled"] = false;
+ $snippet["proj-enabled"] = $ps["proj-enabled"][$i] == true;
+ $snippet["jsmo"] = $ps["proj-jsmo"][$i] == true;
+ $snippet["debug"] = $ps["proj-debug"][$i] == true;
+ $snippet["code"] = $ps["proj-code"][$i];
+ $snippet["proj-limit"] = "include";
+ $snippet["proj-list"] = [$project_id];
+ $snippet["form-list"] = $ps["proj-instruments"][$i];
+ $snippet["ctx"]["projall"] = $ps["proj-context_all"][$i] == true;
+ $snippet["ctx"]["sysall"] = false;
+ foreach ($contexts as $this_context) {
+ $snippet["ctx"][$this_context] = false;
+ }
+ foreach ($ps as $this_key => $this_val) {
+ if (starts_with($this_key, "proj-context")) {
+ $this_context = array_pop(explode("_", $this_key, 2));
+ if (in_array($this_context, $contexts, true)) {
+ if ($this_val[$i] == "include") {
+ $snippet["ctx"][$this_context] = true;
+ }
+ else if ($this_val[$i] <> "exclude") {
+ $snippet["ctx"][$this_context] = $snippet["ctx"]["projall"];
+ }
+ }
+ }
+ }
+ $snippets[] = $snippet;
+ }
+ return $snippets;
+ }
+
+ #endregion
+
+ #region Legacy (v1) Settings Conversion
+
+ /**
+ * Converts legacy v1 project settings to the new v2 model
+ * @return void
+ */
+ private function convert_v1_settings() {
+ $projects = $this->getProjectsWithModuleEnabled(true);
+ // Process each project where the module is enabled
+ foreach ($projects as $pid) {
+ $old = $this->getProjectSettings($pid);
+ if (is_array($old["js"]) && count($old["js"])) {
+ // Prepare new settings format
+ $new = [
+ "enabled" => true,
+ "reserved-hide-from-non-admins-in-project-list" => $old["reserved-hide-from-non-admins-in-project-list"],
+ // Removes legacy settings
+ "js" => null,
+ "js_enabled" => null,
+ "js_type" => null,
+ "js_instruments" => null,
+ "js_code" => null
+ ];
+ foreach ($old["js"] as $i => $_) {
+ $new["proj-snippet"][$i] = true;
+ $new["proj-enabled"][$i] = $old["js_enabled"][$i];
+ $new["proj-jsmo"][$i] = false;
+ $new["proj-debug"][$i] = false;
+ // Set default contexts
+ $new["proj-context_all"][$i] = false;
+ $new["proj-context_php"][$i] = null;
+ $new["proj-context_rsd"][$i] = null;
+ $new["proj-context_aer"][$i] = null;
+ $new["proj-context_rhp"][$i] = null;
+ $new["proj-context_data_entry"][$i] = null;
+ $new["proj-context_survey"][$i] = null;
+ $new["proj-context_report"][$i] = null;
+ $new["proj-context_db"][$i] = null;
+ $new["proj-context_dbp"][$i] = null;
+ // Convert legacy type to contexts
+ switch ($old["js_type"][$i]) {
+ case "all":
+ $new["proj-context_all"][$i] = true;
+ break;
+ case "survey,data_entry":
+ $new["proj-context_data_entry"][$i] = "include";
+ $new["proj-context_survey"][$i] = "include";
+ break;
+ default:
+ $new["proj-context_" . $old["js_type"][$i]][$i] = "include";
+ break;
+ }
+ $new["proj-instruments"][$i] = $old["js_instruments"][$i];
+ $new["proj-code"][$i] = $old["js_code"][$i];
+ }
+ // Store converted settings
+ $this->setProjectSettings($new, $pid);
+ }
+ }
+ // Workaround for version-change hook not working
+ $this->setSystemSetting("v1-upgrade", "done");
+ }
+
+ #endregion
+
+ #region Helpers
+
+ public static function IsSystemExternalModulesManager($page) {
+ return (strpos($page, "manager/control_center.php") !== false);
+ }
+
+ public static function IsProjectExternalModulesManager($page) {
+ return (strpos($page, "manager/project.php") !== false);
+ }
+
+ #endregion
+
+}
diff --git a/README.md b/README.md
index 2ef8467..a4a0151 100644
--- a/README.md
+++ b/README.md
@@ -4,40 +4,82 @@ A REDCap External Module that allows injection of JavaScript on pages.
## Requirements
-- REDCap 8.1.0 or newer (tested with REDCap 8.11.7).
+- REDCap 12.0.0 or newer (tested with REDCap 13.2.4).
## Installation
-- Clone this repo into `/modules/redcap_js_injector_v`, or
+- Clone this repo into `/modules/redcap_javascript_injector_v`, or
- Obtain this module from the Consortium [REDCap Repo](https://redcap.vanderbilt.edu/consortium/modules/index.php) via the Control Center.
- Go to _Control Center > Technical / Developer Tools > External Modules_ and enable REDCap JavaScript Injector.
-## Configuration
+### Upgrading from version 1.x
+
+Configuration data from version 1 of this module will be automatically converted to the new configuration model used by version 2.
+
+**Warning**: Once upgraded, there is no way going back to the previous configuration! Thus, it is strongly advised to make a backup of the module settings in all projects using _JavaScript Injector_ before upgrading.
+
+## Project Configuration
In a project, go to _Applications > External Modules_ and click the _Configure_ button for the REDCap JS Injector module.
-In the configuration dialog, you can define JavaScript snippets for your project that are injected in different contexts. Each context is defined by a page type (_Project Home Page_, _Record Status Dashboard_, _Add / Edit Records_, _Record Home Page_, and _Surveys_, _Data Entry Pages_, _Both, Surveys and Data Entry Pages_, _Project Dashboards_, _Reports_, or _All project pages_. For data entry and survey pages, the context can be further limited by specifying one or more instruments.
+In the configuration dialog, you can define JavaScript snippets for your project that are injected in _All project pages_ or limited to a choice of different project pages, including
+- _Project Home Page_,
+- _Record Status Dashboard_,
+- _Add / Edit Records_,
+- _Record Home Page_,
+- _Data Entry Pages_,
+- _Surveys_,
+- _Reports_, and
+- _Project Dashboards_.
-The configuration options include a checkbox to enable/disable each of the JavaScript snippets. Make sure to enable the ones you want to be injected.
+For data entry and survey pages, injection can be further limited by specifying one or more instruments.
-If more than one snippet is injected into the same page, the injection occurs in the order the snippets are defined in the configuration dialog.
+If more than one snippet is injected on the same page, the injections occur in the order the snippets are defined in the configuration dialog.
-_Note:_ Due to a limitation in the EM configuration dialog, branching logic does not work for nested elements, and thus the instrument selection box cannot be hidden when not applicable (in case of _Project Home Page_, _Record Status Dashboard_, _Add / Edit Records_, and _Record Home Page_).
+## System Configuration
-In the system configuration, admins can set this module's project configurations to be accessible only to super users.
+Admins can set this module's project configurations to be accessible only to super users with the **Allow only super-users to configure this module in projects** option.
-## Acknowledgments
+**New since version 2:** Admins can define _global_ JavaScript injections and optionally limit their scope to certain pages.
+
+> **NOTE:** Note that for any injections targeted at project contexts, these will only work if the module is enabled in the projects. To achieve this without exposing the module's project configuration too widely, it is recommended to turn on **Enable module on all projects by default** and **Hide this module from non-admins in the list of enabled modules on each project** (and then enable visibility in the projects where the module should be exposed).
+
+Snippets can be turned on/off for system/projects contexts with the **Enabled in system/project contexts** options. For both, system and project pages, the pages where injection occurs can then be set to include all such context pages or to be limited to a few selected pages.
+
+In addition to the projects pages listed above, the following non-project pages can be targeted:
+- _Control Center_ (this will include any of the other listed options),
+- _To-Do List_,
+- _Language File Creator/Updater_,
+- _Browse Projects_,
+- _Create/Edit Single User_,
+- _Email Users_, and the
+- _Login Page_.
-This external module is basically just a modification of the [REDCap CSS Injector](https://github.com/ctsit/redcap_css_injector) module.
-## Testing
+If more than one snippet is injected on the same page, the injections occur in the order the snippets are defined in the configuration dialog.
+
+## JavascriptModuleObject and Dynamic Pages
+
+When injecting into dynamic pages, the effect of a JavaScript snippet may have to be re-applied after a re-render of a page (without reloading). This is often the case on survey pages and data entry forms with Multi-Language Management (MLM) enabled, as switching between languages redraws parts of the screen. To detect such changes, the EM Framework's _JavascriptModuleObject_ (JSMO) provides a convenient mechanism (`JSMO.afterRender()`), where a callback function can be regisered that will then be executed each time after REDCap has redrawn (parts of) the page. While `afterRender` will be mostly triggered by MLM, this mechanism is not dependent on MLM being active, and thus can be used on any page, with MLM turned on or off. Access to the JSMO in custom JavaScript snippets can now be obtained by enabling the **Add the JavascriptModuleObject** option (the concrete name of the JSMO is shown under this option). This option should be enabled for each JavaScript snippet that uses the JSMO (the module and _EM Framework_ will ensure that the JSMO is injected only once).
+
+Thus, to have JavaScript snippet do something each time after a re-render of the page, inject code such as this:
+
+```JS
+ExternalModules.DE.RUB.JSInjectorExternalModule.afterRender(function() {
+ console.log('Rendered'); // Replace with your code
+});
+```
+
+
+## Acknowledgments
-Instructions for testing the module can be found [here](?prefix=redcap_javascript_injector&page=tests/JavaScriptInjectorManualTest.md).
+The original version of this external module was basically just a modified version of the [REDCap CSS Injector](https://github.com/ctsit/redcap_css_injector) module. Version 2 is a major overhaul.
## Changelog
Version | Description
------- | ------------------
+2.0.0 | Major new feature: System and pan-project injections.
Redesigned page limitation setup (old settings will be migrated automatically).
Debug logging.
1.1.4 | Added additional injection options.
1.1.3 | Fix an issue where a (silent, but logged) exception would be thrown.
1.1.2 | Add system setting for limiting project configuration access to super users only.
diff --git a/config.json b/config.json
index f5af307..d9dbd91 100644
--- a/config.json
+++ b/config.json
@@ -1,13 +1,17 @@
{
"name": "REDCap JavaScript Injector",
- "namespace": "RUB\\JSInjector\\ExternalModule",
+ "namespace": "DE\\RUB\\JSInjectorExternalModule",
"description": "Allow project admins to inject JavaScript code into surveys and data entry pages. See full documentation here.",
"permissions": [
"redcap_data_entry_form_top",
"redcap_survey_page_top",
"redcap_project_home_page",
- "redcap_every_page_top"
+ "redcap_every_page_top",
+ "redcap_module_system_change_version",
+ "redcap_module_link_check_display"
],
+ "framework-version": 9,
+ "enable-every-page-hooks-on-system-pages": true,
"authors": [
{
"name": "Günther Rezniczek",
@@ -15,97 +19,684 @@
"institution": "Ruhr-Universität Bochum"
}
],
+ "links": {
+ "control-center": [
+ {
+ "name": "Version Upgrade Link",
+ "key": "version-upgrade",
+ "url": "javascript: //"
+ }
+ ]
+ },
"system-settings": [
{
- "name": "Allow only super-users to configure this module in projects",
+ "name": "Allow only super-users to configure this module in projects",
"key": "su_only",
"type": "checkbox"
+ },
+ {
+ "name": "Note: When system-defined JavaScript injections are targeted at project contexts, they will only work in projects where this module is enabled. To achieve global injections without exposing this module's project configuration too widely, it is recommended to turn on Enable module on all projects by default and Hide this module from non-admins in the list of enabled modules on each project, and then enable visibility in only the projects where the module should be exposed for project-specific purposes.",
+ "key": "sys-enable_injections",
+ "type": "descriptive"
+ },
+ {
+ "name": "JavaScript Snippet",
+ "key": "sys-snippet",
+ "type": "sub_settings",
+ "repeatable": true,
+ "sub_settings": [
+ {
+ "name": "Name (optional)",
+ "key": "sys-name",
+ "type": "textbox"
+ },
+ {
+ "name": "Enabled in system contexts",
+ "key": "sys-enabled",
+ "type": "checkbox"
+ },
+ {
+ "name": "Enabled in project contexts",
+ "key": "sys-proj-enabled",
+ "type": "checkbox"
+ },
+ {
+ "name": "Add the JavascriptModuleObject",
+ "key": "sys-jsmo",
+ "type": "checkbox"
+ },
+ {
+ "name": "The JavascriptModuleObject (JSMO) is accesible at #JSMO#.",
+ "key": "sys-jsmo-info",
+ "type": "descriptive"
+ },
+ {
+ "name": "Enable debug logging",
+ "key": "sys-debug",
+ "type": "checkbox"
+ },
+ {
+ "name": "Debug logging outputs information useful for troubleshooting to the console",
+ "key": "sys-debug-info",
+ "type": "descriptive"
+ },
+ {
+ "name": "JavaScript Code (without <script> tags)",
+ "key": "sys-code",
+ "type": "textarea"
+ },
+ {
+ "name": "Apply and/or limit to these system pages:",
+ "key": "sys-pages",
+ "type": "descriptive"
+ },
+ {
+ "name": "All system pages",
+ "key": "sys-context_all",
+ "type": "checkbox"
+ },
+ {
+ "name": "Control Center (includes all sub-pages)",
+ "key": "sys-context_cc",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "To-Do List",
+ "key": "sys-context_todo",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Language File Creator/Updater",
+ "key": "sys-context_lang",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Browse Projects",
+ "key": "sys-context_browseprojects",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Browse Users",
+ "key": "sys-context_browseusers",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Create/Edit Single User",
+ "key": "sys-context_edituser",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Email Users",
+ "key": "sys-context_emailusers",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Login Page",
+ "key": "sys-context_login",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Appy and/or limit to these projects and project pages:",
+ "key": "sys-proj-pages",
+ "type": "descriptive"
+ },
+ {
+ "name": "Limit to only some projects",
+ "key": "sys-proj-limit",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Inject in all projects"
+ },
+ {
+ "value": "include",
+ "name": "Inject only in the projects listed below"
+ },
+ {
+ "value": "exclude",
+ "name": "Inject in projects except those listed below"
+ }
+ ]
+ },
+ {
+ "name": "List of projects to limit injection to or to exclude from injection:
Provide a comma-separated list of project ids",
+ "key": "sys-proj-list",
+ "type": "textarea"
+ },
+ {
+ "name": "All project pages",
+ "key": "sys-proj-context_all",
+ "type": "checkbox"
+ },
+ {
+ "name": "Project Home Page",
+ "key": "sys-proj-context_php",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Record Status Dashboard",
+ "key": "sys-proj-context_rsd",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Add / Edit Records",
+ "key": "sys-proj-context_aer",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Record Home Page",
+ "key": "sys-proj-context_rhp",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Data Entry Pages",
+ "key": "sys-proj-context_data_entry",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Surveys",
+ "key": "sys-proj-context_survey",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Reports",
+ "key": "sys-proj-context_report",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Project Dashboards (authenticated)",
+ "key": "sys-proj-context_db",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Project Dashboards (public)",
+ "key": "sys-proj-context_dbp",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ }
+ ]
}
],
"project-settings": [
{
- "name": "JS Code",
- "key": "js",
+ "name": "JavaScript Snippet",
+ "key": "proj-snippet",
"type": "sub_settings",
"repeatable": true,
"sub_settings": [
{
- "name": "Enabled",
- "key": "js_enabled",
+ "name": "Name (optional)",
+ "key": "proj-name",
+ "type": "textbox"
+ },
+ {
+ "name": "Enabled",
+ "key": "proj-enabled",
+ "type": "checkbox"
+ },
+ {
+ "name": "Add the JavascriptModuleObject",
+ "key": "proj-jsmo",
+ "type": "checkbox"
+ },
+ {
+ "name": "The JavascriptModuleObject (JSMO) is accesible at #JSMO#.",
+ "key": "proj-jsmo-info",
+ "type": "descriptive"
+ },
+ {
+ "name": "Enable debug logging",
+ "key": "proj-debug",
+ "type": "checkbox"
+ },
+ {
+ "name": "Debug logging outputs information useful for troubleshooting to the console",
+ "key": "proj-debug-info",
+ "type": "descriptive"
+ },
+ {
+ "name": "JavaScript Code (without <script> tags)",
+ "key": "proj-code",
+ "type": "textarea"
+ },
+ {
+ "name": "Apply and/or limit to these project pages:",
+ "key": "proj-limit",
+ "type": "descriptive"
+ },
+ {
+ "name": "All project pages",
+ "key": "proj-context_all",
"type": "checkbox"
},
{
- "name": "Apply to",
- "key": "js_type",
- "type": "radio",
- "required": true,
+ "name": "Project Home Page",
+ "key": "proj-context_php",
+ "type": "dropdown",
"choices": [
{
- "value": "php",
- "name": "Project Home Page"
+ "value": "",
+ "name": "Not set (on if all is on)"
},
{
- "value": "rsd",
- "name": "Record Status Dashboard"
+ "value": "include",
+ "name": "Always on"
},
{
- "value": "aer",
- "name": "Add / Edit Records"
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Record Status Dashboard",
+ "key": "proj-context_rsd",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
},
{
- "value": "rhp",
- "name": "Record Home Page"
+ "value": "include",
+ "name": "Always on"
},
{
- "value": "survey",
- "name": "Surveys"
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Add / Edit Records",
+ "key": "proj-context_aer",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
},
{
- "value": "data_entry",
- "name": "Data Entry Pages"
+ "value": "include",
+ "name": "Always on"
},
{
- "value": "survey,data_entry",
- "name": "Both, Surveys and Data Entry Pages"
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Record Home Page",
+ "key": "proj-context_rhp",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
},
{
- "value": "report",
- "name": "Reports"
+ "value": "include",
+ "name": "Always on"
},
{
- "value": "db",
- "name": "Project Dashboards (authenticated)"
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Data Entry Pages",
+ "key": "proj-context_data_entry",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
},
{
- "value": "dbp",
- "name": "Project Dashboards (public)"
+ "value": "include",
+ "name": "Always on"
},
{
- "value": "all",
- "name": "All project pages"
+ "value": "exclude",
+ "name": "Always off"
}
]
},
{
- "name": "Apply to the following instruments (leave blank to apply to all instruments)",
- "key": "js_instruments",
+ "name": "Surveys",
+ "key": "proj-context_survey",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Reports",
+ "key": "proj-context_report",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Project Dashboards (authenticated)",
+ "key": "proj-context_db",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Project Dashboards (public)",
+ "key": "proj-context_dbp",
+ "type": "dropdown",
+ "choices": [
+ {
+ "value": "",
+ "name": "Not set (on if all is on)"
+ },
+ {
+ "value": "include",
+ "name": "Always on"
+ },
+ {
+ "value": "exclude",
+ "name": "Always off"
+ }
+ ]
+ },
+ {
+ "name": "Limit injection to certain instruments on Data Entry and Survey pages:",
+ "key": "proj-instruments-explanation",
+ "type": "descriptive"
+ },
+ {
+ "name": "Inject on this instrument",
+ "key": "proj-instruments",
"type": "form-list",
"repeatable": true,
"select2": true
- },
- {
- "name": "JavaScript Snippet (without <script> tags)",
- "key": "js_code",
- "required": true,
- "type": "textarea"
}
]
}
],
"compatibility": {
- "php-version-min": "5.4.0",
+ "php-version-min": "7.4.0",
"php-version-max": "",
- "redcap-version-min": "8.1.0",
+ "redcap-version-min": "12.2.10",
"redcap-version-max": ""
}
}
\ No newline at end of file
diff --git a/tests/JavaScriptInjectorManualTest.md b/tests/JavaScriptInjectorManualTest.md
deleted file mode 100644
index a1856b3..0000000
--- a/tests/JavaScriptInjectorManualTest.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# JavaScript Injector - Manual Testing Procedure
-
-Version 1 - 2020-04-12
-
-## Prerequisites
-
-- A project with **two** instruments, both enabled as surveys.
-- JavaScript Injector is enabled for this project.
-- No other external modules should be enabled, except those with which this module's interaction should be tested.
-
-## Test Procedure
-
-1. Using an admin account, configure the module:
- - Create JS Code snippets for each for the pages listed under _Apply to_.
- - Turn on _Enabled_ for each.
- - The snippet should read `console.log('Page type')`, where _Page type_ is the same as the active choice in _Apply to_.
- - For the _Surveys_ injection, limit to the **first** survey instrument.
- - For the _Data Entry Pages_ injection, limit to the second survey instrument.
-1. Press F12 to open the browser tools and switch to the console.
-1. Click the _Project Home_ link on the main menu and verify the following:
- - The _All project pages_ alert is displayed.
- - The _Project Home Page_ alert is displayed.
-1. Click the _Record Status Dashboard_ link on the main menu and verify the following:
- - The _Record Status Dashboard_ alert is displayed.
- - The _All project pages_ alert is displayed.
-1. Click the _Add new record_ button on the _Record Status Dashboard_ and verify the following:
- - The _Record Home Page_ alert is displayed.
- - The _All project pages_ alert is displayed.
-1. Open the first instrument (by clicking the gray icon) and verify the following:
- - The _All project pages_ alert is displayed.
- - The _Both, Surveys and Data Entry Pages_ alert is displayed.
-1. Click the _Save & Exit Form_ button and verify the following:
- - The _Record Home Page_ alert is displayed.
- - The _All project pages_ alert is displayed.
-1. Open the second instrument (by clicking the gray icon) and verify the following:
- - The _All project pages_ alert is displayed.
- - The _Data Entry Pages_ alert is displayed.
- - The _Both, Surveys and Data Entry Pages_ alert is displayed.
-1. Using the _Survey options_ button, open the instrument in survey mode, press F12, and verify the following:
- - The _All project pages_ alert is displayed.
- - The _Both, Surveys and Data Entry Pages_ alert is displayed.
-1. Submit the survey and verify the following:
- - The _All project pages_ alert is displayed.
-1. Close the survey.
-1. Click on _Leave without saving changes_ and verify the following:
- - The _Record Home Page_ alert is displayed.
- - The _All project pages_ alert is displayed.
-1. Click the _Add / Edit Records_ link on the main menu and verify the following:
- - The _Add / Edit Records_ alert is displayed.
- - The _All project pages_ alert is displayed.
-1. Click the _Survey Distribution Tools_ link on the main menu and verify the following:
- - The _All project pages_ alert is displayed.
-1. Click the _Open public survey_ button, press F12, and verify the following:
- - The _All project pages_ alert is displayed.
- - The _Surveys_ alert is displayed.
- - The _Both, Surveys and Data Entry Pages_ alert is displayed.
-
-Done.
-
-## Reporting Errors
-
-Before reporting errors:
-- Make sure there is no interference with any other external module by turning off all others and repeating the tests.
-- Check if you are using the latest version of the module. If not, see if updating fixes the issue.
-
-To report an issue:
-- Please report errors by opening an issue on [GitHub](https://github.com/grezniczek/redcap_javascript_injector/issues) or on the community site (please tag @gunther.rezniczek).
-- Include essential details about your REDCap installation such as **version** and platform (operating system, PHP version).
-- If the problem occurs only in conjunction with another external module, please provide its details (you may also want to report the issue to that module's authors).