forked from UKSOURCE/cms.lams
feat(cms): enhance policy editor with fullscreen mode and date picking
Improve the policy content management experience by adding a dedicated fullscreen editing mode and a structured date picker for effective dates. - Implement fullscreen toggle in `policies-block-editor.js` with corresponding CSS for an expanded workspace - Add `date` field type to `sharedFields.js` and integrate it into `policiesConfig.js` - Implement date parsing and formatting utilities in `editor-script.ejs` to handle "Effective Date" strings - Update `policiesController.js` to use `updateOne` for more reliable document persistence and state reloading - Add support for `ICON_OPTIONS` in the policy editor configuration - Refine editor UI layout and overflow handling for better stability with long content strings
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
const previewContent = document.getElementById("previewContent");
|
||||
const blockTypeSelect = document.getElementById("blockTypeSelect");
|
||||
const addBlockButton = document.getElementById("addBlockButton");
|
||||
const toggleFullscreenButton = document.getElementById("toggleFullscreenButton");
|
||||
const validationBanner = document.getElementById("validationBanner");
|
||||
const validationSummary = document.getElementById("validationSummary");
|
||||
const previewDevice = document.getElementById("previewDevice");
|
||||
@@ -43,6 +44,7 @@
|
||||
toolbarTarget: null,
|
||||
sortable: null,
|
||||
previewTargetTimer: null,
|
||||
isFullscreen: false,
|
||||
};
|
||||
|
||||
const stylePalette = [
|
||||
@@ -50,6 +52,7 @@
|
||||
{ value: "warning", label: "Warning" },
|
||||
{ value: "success", label: "Success" },
|
||||
];
|
||||
const iconOptions = Array.isArray(config.iconOptions) ? config.iconOptions : [];
|
||||
|
||||
bindStaticEvents();
|
||||
render();
|
||||
@@ -80,6 +83,12 @@
|
||||
insertBlock(blockTypeSelect.value || "paragraph");
|
||||
});
|
||||
|
||||
if (toggleFullscreenButton) {
|
||||
toggleFullscreenButton.addEventListener("click", function () {
|
||||
setEditorFullscreen(!ui.isFullscreen);
|
||||
});
|
||||
}
|
||||
|
||||
form.querySelectorAll("[data-intent]").forEach((button) => {
|
||||
button.addEventListener("click", function () {
|
||||
intentInput.value = this.dataset.intent || "save";
|
||||
@@ -101,9 +110,31 @@
|
||||
});
|
||||
|
||||
document.addEventListener("selectionchange", handleSelectionChange);
|
||||
root.addEventListener(
|
||||
"wheel",
|
||||
function (event) {
|
||||
if (!ui.isFullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interactiveTarget = event.target.closest(
|
||||
".icon-combobox-panel, .icon-combobox-options",
|
||||
);
|
||||
if (interactiveTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.scrollTop += event.deltaY;
|
||||
event.preventDefault();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
document.addEventListener("click", handleGlobalClick);
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
if (ui.isFullscreen) {
|
||||
setEditorFullscreen(false);
|
||||
}
|
||||
hideFloatingToolbar();
|
||||
hideLinkPopover();
|
||||
hideSlashMenu();
|
||||
@@ -164,11 +195,39 @@
|
||||
hideLinkPopover();
|
||||
hideSlashMenu();
|
||||
updatePreviewModeButtons();
|
||||
updateFullscreenToggle();
|
||||
renderBlocks();
|
||||
renderPreview();
|
||||
renderValidation(validateState());
|
||||
}
|
||||
|
||||
function setEditorFullscreen(nextValue) {
|
||||
ui.isFullscreen = Boolean(nextValue);
|
||||
root.classList.toggle("is-editor-fullscreen", ui.isFullscreen);
|
||||
document.body.classList.toggle("policy-editor-fullscreen", ui.isFullscreen);
|
||||
updateFullscreenToggle();
|
||||
}
|
||||
|
||||
function updateFullscreenToggle() {
|
||||
if (!toggleFullscreenButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const label = toggleFullscreenButton.querySelector('[data-role="fullscreen-label"]');
|
||||
const icon = toggleFullscreenButton.querySelector('[data-role="fullscreen-icon"]');
|
||||
toggleFullscreenButton.setAttribute("aria-pressed", ui.isFullscreen ? "true" : "false");
|
||||
toggleFullscreenButton.classList.toggle("btn-outline-secondary", !ui.isFullscreen);
|
||||
toggleFullscreenButton.classList.toggle("btn-outline-primary", ui.isFullscreen);
|
||||
|
||||
if (label) {
|
||||
label.textContent = ui.isFullscreen ? "Exit full screen" : "Full screen edit";
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
icon.className = `fas ${ui.isFullscreen ? "fa-compress" : "fa-expand"} me-2`;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewModeButtons() {
|
||||
root.querySelectorAll("[data-preview-mode]").forEach((button) => {
|
||||
button.classList.toggle("is-active", button.dataset.previewMode === ui.previewMode);
|
||||
@@ -490,7 +549,25 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Icon</label>
|
||||
<input type="text" class="form-control" data-field="icon" value="${escapeAttribute(block.icon || "")}" placeholder="fa-circle-info" />
|
||||
<div class="icon-combobox" data-role="icon-combobox">
|
||||
<input type="hidden" data-field="icon" value="${escapeAttribute(block.icon || "")}" />
|
||||
<button type="button" class="icon-combobox-trigger">
|
||||
<span class="icon-combobox-value">
|
||||
<span class="icon-combobox-preview">
|
||||
${block.icon ? `<i class="fa-solid ${escapeAttribute(block.icon)}"></i>` : '<i class="fas fa-icons"></i>'}
|
||||
</span>
|
||||
<span class="icon-combobox-text">
|
||||
<strong>${escapeHtml(block.icon || "Select icon")}</strong>
|
||||
</span>
|
||||
</span>
|
||||
<i class="fas fa-chevron-down icon-combobox-caret"></i>
|
||||
</button>
|
||||
<div class="icon-combobox-panel is-hidden">
|
||||
<input type="text" class="form-control icon-combobox-search" placeholder="Search icon name" value="${escapeAttribute(block.icon || "")}" />
|
||||
<div class="icon-combobox-options"></div>
|
||||
<div class="icon-combobox-empty is-hidden">No matching icons. Press Enter to use custom value.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label fw-semibold">Title</label>
|
||||
@@ -506,12 +583,16 @@
|
||||
block.html = value;
|
||||
});
|
||||
|
||||
body.querySelectorAll("[data-field]").forEach((input) => {
|
||||
input.addEventListener("input", function () {
|
||||
initializeIconCombobox(body.querySelector('[data-role="icon-combobox"]'), block);
|
||||
|
||||
body.querySelectorAll("input[data-field], select[data-field], textarea[data-field]").forEach((input) => {
|
||||
const syncField = function () {
|
||||
block[this.dataset.field] = this.value;
|
||||
renderPreview();
|
||||
renderValidation(validateState());
|
||||
});
|
||||
};
|
||||
input.addEventListener("input", syncField);
|
||||
input.addEventListener("change", syncField);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -580,6 +661,113 @@
|
||||
return editor;
|
||||
}
|
||||
|
||||
function initializeIconCombobox(wrapper, block) {
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hiddenInput = wrapper.querySelector('input[data-field="icon"]');
|
||||
const trigger = wrapper.querySelector(".icon-combobox-trigger");
|
||||
const panel = wrapper.querySelector(".icon-combobox-panel");
|
||||
const searchInput = wrapper.querySelector(".icon-combobox-search");
|
||||
const optionsContainer = wrapper.querySelector(".icon-combobox-options");
|
||||
const emptyState = wrapper.querySelector(".icon-combobox-empty");
|
||||
const preview = wrapper.querySelector(".icon-combobox-preview");
|
||||
const title = wrapper.querySelector(".icon-combobox-text strong");
|
||||
const card = wrapper.closest(".block-card");
|
||||
|
||||
if (!hiddenInput || !trigger || !panel || !searchInput || !optionsContainer || !emptyState || !preview || !title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setOpen = function (isOpen) {
|
||||
wrapper.classList.toggle("is-open", isOpen);
|
||||
panel.classList.toggle("is-hidden", !isOpen);
|
||||
if (card) {
|
||||
card.classList.toggle("has-floating-ui", isOpen);
|
||||
}
|
||||
if (isOpen) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
};
|
||||
|
||||
const syncIconValue = function (value) {
|
||||
const nextValue = String(value || "").trim();
|
||||
hiddenInput.value = nextValue;
|
||||
searchInput.value = nextValue;
|
||||
block.icon = nextValue;
|
||||
preview.innerHTML = nextValue
|
||||
? `<i class="fa-solid ${escapeAttribute(nextValue)}"></i>`
|
||||
: '<i class="fas fa-icons"></i>';
|
||||
title.textContent = nextValue || "Select icon";
|
||||
renderPreview();
|
||||
renderValidation(validateState());
|
||||
};
|
||||
|
||||
const renderOptions = function (query) {
|
||||
const normalizedQuery = String(query || "").trim().toLowerCase();
|
||||
const filteredOptions = iconOptions.filter((option) =>
|
||||
option.toLowerCase().includes(normalizedQuery),
|
||||
);
|
||||
|
||||
optionsContainer.innerHTML = "";
|
||||
emptyState.classList.toggle("is-hidden", filteredOptions.length > 0);
|
||||
|
||||
filteredOptions.forEach((option) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "icon-combobox-option";
|
||||
button.classList.toggle("is-active", option === hiddenInput.value);
|
||||
button.innerHTML = `
|
||||
<span class="icon-combobox-option-main">
|
||||
<i class="fa-solid ${escapeAttribute(option)}"></i>
|
||||
<span>${escapeHtml(option)}</span>
|
||||
</span>
|
||||
${option === hiddenInput.value ? '<i class="fas fa-check small"></i>' : ""}
|
||||
`;
|
||||
button.addEventListener("click", function () {
|
||||
syncIconValue(option);
|
||||
renderOptions(option);
|
||||
setOpen(false);
|
||||
});
|
||||
optionsContainer.appendChild(button);
|
||||
});
|
||||
};
|
||||
|
||||
trigger.addEventListener("click", function () {
|
||||
const nextIsOpen = panel.classList.contains("is-hidden");
|
||||
setOpen(nextIsOpen);
|
||||
if (nextIsOpen) {
|
||||
renderOptions(searchInput.value || hiddenInput.value);
|
||||
}
|
||||
});
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
renderOptions(searchInput.value);
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
syncIconValue(searchInput.value);
|
||||
renderOptions(searchInput.value);
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.addEventListener("focusout", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!wrapper.contains(document.activeElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
syncIconValue(block.icon || "");
|
||||
renderOptions(block.icon || "");
|
||||
}
|
||||
|
||||
function bindEditable(element, onChange, options) {
|
||||
element.addEventListener("input", function () {
|
||||
normalizeEditorHtml(element);
|
||||
|
||||
Reference in New Issue
Block a user