Script Library - Quality Document Manager
Overview
The QDM script library contains all interactive behaviors for the document management interface. These scripts orchestrate the user experience by reacting to actions and facilitating data entry.
Interactive features offered
-
Contextual input assistance
- Automatic field completion based on context (document type, level, dates)
- Dynamic showing/hiding of sections based on user choices
- Automatic calculations (revision dates, references)
-
Real-time validation
- Immediate control of required fields during input
- Consistency verification (e.g.: end date after start date)
- Instant alert in case of error with explanatory message
-
Field dependency management
- If "Training required" = Yes → automatic creation of training session
- If type = "Procedure" → automatic application of "Confidential" level
- Document type change → form adaptation
-
Productivity improvement
- No page reload between actions
- Immediate feedback on actions (confirmation messages, errors)
- Intelligent pre-filling based on history and business rules
Business scenario examples
- Procedure creation: User selects "Procedure" → form automatically adapts, dates are calculated, reference is generated
- Document validation: User clicks "Validate" → system instantly checks that all required fields are filled and consistent before sending to server
- Training addition: User checks "Training required" → system immediately creates link to new training session
Script structure
File locations
Scripts are organized by module and functionality:
2.Advanced/Web/Custom/
├── QDM/
│ ├── Document/
│ │ └── Scripts/
│ │ ├── DocumentManager.js # Document management
│ │ └── ButtonManager.js # Button management
│ └── Binder/
│ └── ViewExtendActions.js # Binder actions
├── EXTEND/
│ ├── OfficeModule/Office/
│ │ └── Scripts/
│ │ ├── office.engine.js # Office engine
│ │ ├── office.js / office.min.js # Complete Office module
│ │ ├── jquery.fileupload*.js # File upload
│ │ └── jquery.ajaxQueue.js # AJAX queue
│ ├── Survey/
│ │ └── Scripts/
│ │ └── [Survey scripts]
│ ├── DateTools.js # Date utilities
│ ├── ViewActions_custom.js # Custom view actions
│ ├── DocLinkCopy/
│ │ └── DocLinkCopy.js # Document link copy
│ ├── ExportPJs/
│ │ └── ExportPJs.js # Attachment export
│ ├── Help/
│ │ └── OnlineHelp.js # Online help
│ └── ApsPhotos/
│ └── ApsPhotos.js # Photo management
└── BI/
└── Scripts/
└── DeleteExtension.js # BI - Extension deletion
Main QDM scripts
DocumentManager.js
Path: Custom/QDM/Document/Scripts/DocumentManager.js
Functional role: This script orchestrates the entire user experience when creating or modifying a quality document. It automates repetitive tasks and ensures consistency of entered data.
Implemented business functionalities
-
Dynamic form adaptation based on document type
- Need: A Procedure form doesn't have the same fields as an Instruction form
- Solution: Script automatically shows/hides relevant sections
-
Automatic application of business rules
- Need: Prevent user from having to memorize all rules (levels, durations, workflows)
- Solution: Script retrieves and automatically applies configured rules
-
Document-training link management
- Need: Ensure that a document requiring training has its training created and published
- Solution: Script automatically creates training and blocks validation if not published
-
Automatic calculation of lifecycle dates
- Need: Automatically calculate revision and validity dates according to business rules
- Solution: Script calculates and automatically fills these dates, user cannot modify them
Main object: form
The form object is a JavaScript structure that groups all functions necessary to manage the document form. Think of this object as a toolbox containing different instruments, each with a specific role.
var form = {
Loaded: function () { }, // Initialization on load
Init: function () { }, // Initial configuration
OnTypeChange: function () { }, // Type change event
OnSurveyChange: function () { }, // Survey event
OnFormationChange: function () { } // Training event
};
Loading cycle
How the form prepares at startup
When a document page opens in the browser, several steps automatically chain together. Here's the detailed flow:
// STEP 1: Wait for HTML page to be completely loaded
// Like waiting for all furniture to be delivered before arranging a room
APSAttachEventToElement(document, 'DOMContentLoaded', function () {
form.Loaded();
});
var form = {
// STEP 2: Function called as soon as the page is ready
Loaded: function () {
// Check we're in edit mode (not simple consultation)
if (IsEditMode) {
console.log("form.Loaded");
// Initialize all form parameters
this.Init();
// STEP 3: "Connect" event listeners
// Like installing motion detectors on doors
// When document type changes
APSAttachEventToElement(
APSGetFieldByName("ApsType"),
"change",
this.OnTypeChange
);
// When "Survey" option changes
APSAttachEventToRadio("Survey", "change", this.OnSurveyChange);
// When "Training" option changes
APSAttachEventToRadio("Formation", "change", this.OnFormationChange);
}
},
// STEP 4: Initial form configuration
Init: function () {
console.log("form.Init");
// Prepare certain fields as read-only (user cannot modify them)
// These dates are automatically calculated by the system
PrepareReadOnlyField("DateApp"); // Application date
PrepareReadOnlyField("DateRev"); // Revision date
PrepareReadOnlyField("DateValidity"); // Validity date
// Prepare configuration message display
PrepareMessageConfig();
// Trigger current document type processing
this.OnTypeChange();
// Schedule automatic training creation if necessary
// Wait 500ms to let APScore framework finish initialization
setTimeout(function () {
CreateTraining();
}, 500);
}
};
Loading cycle summary
The form loads, checks it's in edit mode, installs all its "event detectors", configures fields, and launches necessary automatic processing.
Document type change management
Business scenario
A quality manager starts creating a document. They select "Operating Procedure" type from the dropdown. The system must immediately adapt the form according to rules defined for this type.
Implemented functional behavior
OnTypeChange: function (s, e) {
console.log("form.OnTypeChange");
// Retrieve selected type
var type = APSGetFieldValueByName("ApsType");
// Query configuration repository for this type
var config = JSON.parse(
HandlerRequest("Custom/QDM/Document/DocumentManager",
"GetConfig",
"&type=" + encodeURI(type))
);
if (config.isError) {
console.log("Error : " + config.message);
return;
}
// APPLICATION OF CONFIGURED BUSINESS RULES
if (config.data.isExist) {
// Type with configuration: Automatic rule application
// Rule 1: Imposed classification level
APSSetFieldValueByName("Niveau", config.data.NIVEAU);
// Rule 2: Automatically calculated dates (non-modifiable)
SetFieldReadonly("DateApp"); // Application date = validation + X days
SetFieldReadonly("DateRev"); // Revision date = application + Y months
SetFieldReadonly("DateValidity"); // Validity date according to defined cycle
// User information
ShowMessageConfig(); // "Operating Procedure type rules are applied"
} else {
// Type without configuration: Manual entry
// Minimal default values
APSSetFieldValueByName("Niveau", 1);
APSSetFieldValueByName("DocumentParent", "");
// User must manually enter dates
SetFieldEditable("DateApp");
SetFieldEditable("DateRev");
SetFieldEditable("DateValidity");
HideMessageConfig();
}
}
Benefits for the user
- Time savings: No need to manually fill all fields according to complex rules
- Guaranteed compliance: Business rules are automatically applied, no error possible
- Clarity: User immediately sees which fields are automatic and which require input
Document training management
Training existence verification
function VerifyTrainingExistence() {
var trainingNeeded = APSGetFieldValueByName("Formation");
if (trainingNeeded == "Non") {
console.log("INFO : No training needed.");
return { success: true, training: null };
}
var ref = APSGetFieldValueByName("reference");
var training = JSON.parse(
HandlerRequest("Custom/QDM/Document/DocumentManager",
"GetTraining",
"&RefDoc=" + ref)
);
if (training.isError) {
console.log("Error : " + training.message);
return { success: false, training: null };
}
return { success: true, training };
}
Training control before validation
function ControlTraining() {
var { success, training } = VerifyTrainingExistence();
if (!training)
return success;
if (!training.data.isExist) {
console.log("INFO : Training is missing.");
return false;
}
if (!training.data.isPublished) {
console.log("INFO : Training is not published.");
APScore.Window.Alert(
LocalResourceManager.GetString("TrainingPublished")
.replace("${training.data.reference}", training.data.reference),
LocalResourceManager.GetString("TrainingPublishedTitle")
);
return false;
}
return true;
}
Automatic training creation
function CreateTraining() {
var { success, training } = VerifyTrainingExistence();
if (!training)
return;
if (training.data.isExist) {
console.log("INFO : Training already exists.");
return;
}
// Automatic creation
var result = JSON.parse(
HandlerRequest("Custom/QDM/Document/DocumentManager",
"CreateTraining",
"&IdDoc=" + IdDoc + "&RefDoc=" + ref)
);
if (result.isError) {
console.error("Training creation error:", result.message);
} else {
console.log("Training created automatically");
}
}
Training deletion
function DeleteTraining() {
var ref = APSGetFieldValueByName("reference");
var result = JSON.parse(
HandlerRequest("Custom/QDM/Document/DocumentManager",
"DeleteTraining",
"&RefDoc=" + ref)
);
if (result.isError) {
console.error("Training deletion error:", result.message);
} else {
console.log("Training deleted");
}
}
ButtonManager.js
Path: Custom/QDM/Document/Scripts/ButtonManager.js
Objective: Management of actions related to document form buttons.
Training session creation
function CreateTrainingSession() {
var url = BaseSite +
"/PageLoader.ashx?Create&IdForm=Training_session________________" +
"&IdInheritance=" + IdDoc + "&ext=1";
var scope = window;
APScore.Window.OpenModal(url, {
title: LocalResourceManager.GetString("ui_title_CreateTrainingSession"),
closable: true,
width: 1200,
height: 630,
modal: true,
scope: scope,
showBtnCancel: true,
handlers: {
load: function () {
var modal = this.context.popup;
this.Ok = function () {
modal.IsOk = true;
var btnValidate = this.GetDocumentWindow()
.document.querySelector('.btn-validate');
btnValidate.click();
}
this.Cancel = function () {
modal.hide();
}
this.ExtPopupHide = function () {
modal.hide();
};
this.initContext(scope);
}
}
});
_continue = false;
}
Usage
- Called from a form button
- Opens modal popup to create a session
- Handles validation and cancellation
- Refreshes page after creation
ViewExtendActions.js
Path: Custom/QDM/Binder/ViewExtendActions.js
Objective: Custom actions on views (document lists).
Typical actions
// Excel export of view
function ExportToExcel() {
var viewId = GetCurrentViewId();
var filters = GetCurrentFilters();
window.location.href = "/Custom/EXTEND/ViewLink/Actions.ashx" +
"?action=ExportToExcel" +
"&viewId=" + viewId +
"&filters=" + encodeURI(JSON.stringify(filters));
}
// Bulk validation
function BulkValidate() {
var selectedDocs = GetSelectedDocuments();
if (selectedDocs.length === 0) {
APScore.Window.Alert("No document selected", "Information");
return;
}
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/ViewLink/Actions",
"BulkValidate",
"&IdDocs=" + selectedDocs.join(","))
);
if (!result.isError) {
APScore.Window.Alert(
result.data.count + " document(s) validated",
"Success"
);
RefreshView();
} else {
APScore.Window.Alert(result.message, "Error");
}
}
EXTEND scripts
office.engine.js
Path: Custom/EXTEND/OfficeModule/Office/Scripts/office.engine.js
Objective: Office document editing engine in browser.
Office engine initialization
var OfficeEngine = {
Init: function(config) {
this.config = config;
this.documentId = config.documentId;
this.fileName = config.fileName;
this.mode = config.mode; // edit | view
this.LoadEditor();
},
LoadEditor: function() {
// Loading Office Online or OnlyOffice editor
var editorUrl = this.GetEditorUrl();
this.CreateIframe(editorUrl);
},
GetEditorUrl: function() {
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/OfficeModule/Office/Actions",
"OpenDocument",
"&IdDoc=" + this.documentId +
"&FileName=" + encodeURI(this.fileName))
);
if (result.isError) {
console.error("Editor loading error:", result.message);
return null;
}
return result.data.url;
},
CreateIframe: function(url) {
var iframe = document.createElement('iframe');
iframe.src = url;
iframe.width = "100%";
iframe.height = "100%";
iframe.frameBorder = "0";
document.getElementById('office-container').appendChild(iframe);
},
Save: function() {
// Trigger save in editor
this.SendMessageToEditor({ action: 'save' });
},
Close: function() {
// Close editor
this.SendMessageToEditor({ action: 'close' });
window.close();
}
};
File locking management
var FileLocking = {
CheckOut: function(documentId, fileName) {
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/OfficeModule/Office/Actions",
"CheckOut",
"&IdDoc=" + documentId +
"&FileName=" + encodeURI(fileName))
);
if (result.isError) {
APScore.Window.Alert(
"File is locked by: " + result.data.lockedBy,
"File locked"
);
return false;
}
return true;
},
CheckIn: function(documentId, fileName) {
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/OfficeModule/Office/Actions",
"CheckIn",
"&IdDoc=" + documentId +
"&FileName=" + encodeURI(fileName))
);
return !result.isError;
},
CancelCheckOut: function(documentId, fileName) {
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/OfficeModule/Office/Actions",
"CancelCheckOut",
"&IdDoc=" + documentId +
"&FileName=" + encodeURI(fileName))
);
return !result.isError;
}
};
DateTools.js
Path: Custom/EXTEND/DateTools.js
Objective: Date manipulation utilities.
Available functions
var DateTools = {
// Add N days to a date
AddDays: function(date, days) {
var result = new Date(date);
result.setDate(result.getDate() + days);
return result;
},
// Add N months to a date
AddMonths: function(date, months) {
var result = new Date(date);
result.setMonth(result.getMonth() + months);
return result;
},
// Calculate difference in days between two dates
DiffDays: function(date1, date2) {
var diff = Math.abs(date2.getTime() - date1.getTime());
return Math.ceil(diff / (1000 * 3600 * 24));
},
// Format date as DD/MM/YYYY
FormatDate: function(date) {
var day = ("0" + date.getDate()).slice(-2);
var month = ("0" + (date.getMonth() + 1)).slice(-2);
var year = date.getFullYear();
return day + "/" + month + "/" + year;
},
// Parse date in DD/MM/YYYY format
ParseDate: function(dateString) {
var parts = dateString.split('/');
return new Date(parts[2], parts[1] - 1, parts[0]);
},
// Check if date is in the past
IsPast: function(date) {
return date < new Date();
},
// Check if date is within N days
IsInDays: function(date, days) {
var target = this.AddDays(new Date(), days);
return date <= target;
}
};
Typical usage in QDM:
// Calculate expected revision date (+ 1 year)
var dateApp = APSGetFieldValueByName("DateApp");
var dateRev = DateTools.AddMonths(dateApp, 12);
APSSetFieldValueByName("DateRev", DateTools.FormatDate(dateRev));
// Check if revision is approaching (within 30 days)
var dateRev = DateTools.ParseDate(APSGetFieldValueByName("DateRev"));
if (DateTools.IsInDays(dateRev, 30)) {
console.log("Revision to plan within 30 days");
}
DocLinkCopy.js
Path: Custom/EXTEND/DocLinkCopy/DocLinkCopy.js
Objective: Copy document links to clipboard.
var DocLinkCopy = {
CopyLink: function(documentId, linkType) {
// linkType: 'direct' | 'reference' | 'title'
var link = this.GenerateLink(documentId, linkType);
this.CopyToClipboard(link);
APScore.Window.Notification("Link copied", "success");
},
GenerateLink: function(documentId, linkType) {
var baseUrl = window.location.origin;
switch(linkType) {
case 'direct':
return baseUrl + "/PageLoader.ashx?Edit&IdDoc=" + documentId;
case 'reference':
var ref = this.GetDocumentReference(documentId);
return "Reference: " + ref;
case 'title':
var title = this.GetDocumentTitle(documentId);
return title + " - " + baseUrl + "/PageLoader.ashx?Edit&IdDoc=" + documentId;
default:
return baseUrl + "/PageLoader.ashx?Edit&IdDoc=" + documentId;
}
},
CopyToClipboard: function(text) {
var tempInput = document.createElement("input");
tempInput.value = text;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
document.body.removeChild(tempInput);
},
GetDocumentReference: function(documentId) {
// Server call to retrieve reference
var result = JSON.parse(
HandlerRequest("Custom/QDM/Document/DocumentManager",
"GetDocumentInfo",
"&IdDoc=" + documentId)
);
return result.data.reference;
}
};
ExportPJs.js
Path: Custom/EXTEND/ExportPJs/ExportPJs.js
Objective: Export attachments as ZIP archive.
var ExportPJs = {
ExportAllAttachments: function(documentId) {
var url = "/Custom/EXTEND/ExportPJs/ZipHandler.ashx" +
"?action=ExportAttachments" +
"&IdDoc=" + documentId;
// ZIP file download
window.location.href = url;
},
ExportSelectedAttachments: function(documentId, attachmentIds) {
var url = "/Custom/EXTEND/ExportPJs/ZipHandler.ashx" +
"?action=ExportAttachments" +
"&IdDoc=" + documentId +
"&AttachmentIds=" + attachmentIds.join(",");
window.location.href = url;
},
ExportMultipleDocuments: function(documentIds) {
var url = "/Custom/EXTEND/ExportPJs/ZipHandler.ashx" +
"?action=ExportMultipleDocuments" +
"&IdDocs=" + documentIds.join(",");
window.location.href = url;
}
};
OnlineHelp.js
Path: Custom/EXTEND/Help/OnlineHelp.js
Objective: Contextual online help management.
var OnlineHelp = {
ShowHelp: function(context) {
// context: 'document', 'request', 'distribution', etc.
var helpUrl = this.GetHelpUrl(context);
APScore.Window.OpenModal(helpUrl, {
title: "Help - " + context,
width: 800,
height: 600,
closable: true
});
},
GetHelpUrl: function(context) {
var result = JSON.parse(
HandlerRequest("Custom/EXTEND/Help/OnlineHelp",
"GetHelpContent",
"&context=" + context)
);
if (!result.isError) {
return result.data.url;
}
return "/Help/default.html";
},
ShowTooltip: function(element, helpKey) {
var helpText = LocalResourceManager.GetString("help_" + helpKey);
APScore.Tooltip.Show(element, helpText);
}
};
Global utility functions
Field management
Field preparation
// Prepare read-only field with style
function PrepareReadOnlyField(fieldName) {
var field = APSGetFieldByName(fieldName);
if (field) {
field.readOnly = true;
field.style.backgroundColor = "#f5f5f5";
field.style.cursor = "not-allowed";
}
}
// Set field as read-only
function SetFieldReadonly(fieldName) {
var field = APSGetFieldByName(fieldName);
if (field) {
field.readOnly = true;
}
}
// Set field as editable
function SetFieldEditable(fieldName) {
var field = APSGetFieldByName(fieldName);
if (field) {
field.readOnly = false;
field.style.backgroundColor = "";
}
}
Message management
Configuration messages
// Prepare configuration message display
function PrepareMessageConfig() {
var messageDiv = document.getElementById("message-config");
if (!messageDiv) {
messageDiv = document.createElement("div");
messageDiv.id = "message-config";
messageDiv.style.display = "none";
messageDiv.className = "alert alert-info";
messageDiv.innerHTML = "This document type has specific configuration.";
var container = document.querySelector(".form-container");
container.insertBefore(messageDiv, container.firstChild);
}
}
// Show configuration message
function ShowMessageConfig() {
var messageDiv = document.getElementById("message-config");
if (messageDiv) {
messageDiv.style.display = "block";
}
}
// Hide configuration message
function HideMessageConfig() {
var messageDiv = document.getElementById("message-config");
if (messageDiv) {
messageDiv.style.display = "none";
}
}
AJAX requests
Secure request wrapper
// Wrapper function for HandlerRequest with error handling
function SafeHandlerRequest(handler, action, params) {
try {
var result = HandlerRequest(handler, action, params);
var jsonResult = JSON.parse(result);
if (jsonResult.isError) {
console.error("Handler error:", jsonResult.message);
APScore.Window.Alert(jsonResult.message, "Error");
return null;
}
return jsonResult;
} catch (ex) {
console.error("Exception in SafeHandlerRequest:", ex);
APScore.Window.Alert("An error occurred", "Error");
return null;
}
}
Patterns and best practices
Pattern: Form script initialization
Why use this pattern?
This code organization model is a best practice that allows proper structuring of a form script. Instead of having scattered code everywhere, we group all form-related functions in a single object, making the code more readable and maintainable.
Recommended structure
// ENTRY POINT: Wait for page to load before starting
APSAttachEventToElement(document, 'DOMContentLoaded', function () {
FormScript.Init();
});
// MAIN OBJECT: Groups all form logic
var FormScript = {
// === PHASE 1: INITIALIZATION ===
Init: function () {
// Check we're in edit mode
if (!IsEditMode) return;
console.log("FormScript initialized");
// Launch 3 initialization steps in order
this.LoadConfiguration(); // 1. Load config
this.AttachEvents(); // 2. Connect events
this.InitializeFields(); // 3. Prepare fields
},
// === PHASE 2: CONFIGURATION LOADING ===
LoadConfiguration: function () {
// Retrieve form configuration from server
var config = SafeHandlerRequest(
"Custom/Module/Handler",
"GetConfig",
"&formularId=" + IdFormular
);
if (config) {
// Store config for later use
this.config = config.data;
}
},
// === PHASE 3: EVENT BINDING ===
AttachEvents: function () {
// "Connect" functions to field changes
// When Field1 changes, call OnField1Change
APSAttachEventToElement(
APSGetFieldByName("Field1"),
"change",
this.OnField1Change.bind(this) // .bind(this) keeps object access
);
// When radio button changes
APSAttachEventToRadio(
"RadioField",
"change",
this.OnRadioChange.bind(this)
);
},
// === PHASE 4: FIELD INITIALIZATION ===
InitializeFields: function () {
// Set some fields as read-only
PrepareReadOnlyField("ReadOnlyField");
// Calculate default values
this.CalculateDefaultValues();
},
// === EVENT HANDLERS ===
// These functions are called when user interacts with form
OnField1Change: function (e) {
console.log("Field1 changed");
// Update other fields that depend on Field1
this.UpdateDependentFields();
},
OnRadioChange: function (e) {
console.log("Radio changed");
// Show or hide sections based on choice
this.ToggleFields();
},
// === BUSINESS METHODS ===
// These functions contain form-specific logic
CalculateDefaultValues: function () {
// Example: calculate end date = start date + 1 year
var dateStart = APSGetFieldValueByName("DateStart");
var dateEnd = DateTools.AddMonths(dateStart, 12);
APSSetFieldValueByName("DateEnd", DateTools.FormatDate(dateEnd));
},
UpdateDependentFields: function () {
// Example: if Field1 = "Urgent", show message
var value = APSGetFieldValueByName("Field1");
if (value === "Urgent") {
ShowMessageConfig();
}
},
ToggleFields: function () {
// Example: if radio = "Yes", show details section
var radioValue = APSGetFieldValueByName("RadioField");
if (radioValue === "Yes") {
document.getElementById("details-section").style.display = "block";
} else {
document.getElementById("details-section").style.display = "none";
}
}
};
Advantages of this organization
- Clarity: All form-related functions are grouped in the same place
- Logical order: Initialization always follows the same steps in the same order
- Maintainability: If we need to modify behavior, we know where to look
- Reusability: We can copy this structure to create a new form
Pattern: Pre-submission validation
Why validate before submitting?
Before saving or validating a document, it's essential to check that all data is correct. This prevents sending invalid data to the server and allows displaying clear error messages to the user immediately.
3-level validation strategy
// MAIN FUNCTION: Validation orchestration
// This function is called automatically when user clicks "Save"
function ValidateBeforeSubmit() {
console.log("ValidateBeforeSubmit");
// LEVEL 1: Check required fields are filled
// Most basic and fastest validation
if (!ValidateRequiredFields()) {
return false; // Stop everything if required field is empty
}
// LEVEL 2: Check data logical consistency
// For example: end date must be after start date
if (!ValidateDataCoherence()) {
return false; // Stop if data is inconsistent
}
// LEVEL 3: Server-side validation for complex rules
// For example: check similar document doesn't already exist
if (!ValidateServerSide()) {
return false; // Stop if server refuses validation
}
// If all validations pass, allow submission