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:
@@ -408,6 +408,23 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema.type === "date") {
|
||||
const input = document.createElement("input");
|
||||
input.className = "form-control";
|
||||
input.type = "date";
|
||||
input.value = toDateInputValue(parent[key]);
|
||||
input.addEventListener("input", function () {
|
||||
parent[key] = fromDateInputValue(input.value);
|
||||
});
|
||||
input.addEventListener("change", function () {
|
||||
parent[key] = fromDateInputValue(input.value);
|
||||
});
|
||||
col.appendChild(input);
|
||||
appendHelp(col, schema, parent[key]);
|
||||
container.appendChild(col);
|
||||
return;
|
||||
}
|
||||
|
||||
if (schema.type === "image") {
|
||||
const group = document.createElement("div");
|
||||
group.className = "input-group";
|
||||
@@ -805,6 +822,79 @@
|
||||
return "col-md-6";
|
||||
}
|
||||
|
||||
function toDateInputValue(value) {
|
||||
const parsed = parseDateValue(value);
|
||||
if (!parsed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const year = parsed.getUTCFullYear();
|
||||
const month = String(parsed.getUTCMonth() + 1).padStart(2, "0");
|
||||
const day = String(parsed.getUTCDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function fromDateInputValue(value) {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const [year, month, day] = normalized.split("-").map(Number);
|
||||
if (!year || !month || !day) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
return `Effective Date: ${new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(date)}`;
|
||||
}
|
||||
|
||||
function parseDateValue(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
||||
const isoDate = new Date(`${raw}T00:00:00Z`);
|
||||
return Number.isNaN(isoDate.getTime()) ? null : isoDate;
|
||||
}
|
||||
|
||||
const withoutPrefix = raw.replace(/^Effective Date:\s*/i, "").trim();
|
||||
const textMatch = withoutPrefix.match(
|
||||
/^([A-Za-z]+)\s+(\d{1,2}),\s*(\d{4})$/,
|
||||
);
|
||||
if (textMatch) {
|
||||
const monthIndex = [
|
||||
"january",
|
||||
"february",
|
||||
"march",
|
||||
"april",
|
||||
"may",
|
||||
"june",
|
||||
"july",
|
||||
"august",
|
||||
"september",
|
||||
"october",
|
||||
"november",
|
||||
"december",
|
||||
].indexOf(textMatch[1].toLowerCase());
|
||||
if (monthIndex !== -1) {
|
||||
return new Date(
|
||||
Date.UTC(Number(textMatch[3]), monthIndex, Number(textMatch[2])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fallbackDate = new Date(`${withoutPrefix} UTC`);
|
||||
return Number.isNaN(fallbackDate.getTime()) ? null : fallbackDate;
|
||||
}
|
||||
|
||||
function resolveImageUrl(path) {
|
||||
if (!path) return "";
|
||||
if (/^https?:\/\//i.test(path)) return path;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}
|
||||
|
||||
.policy-block-editor {
|
||||
--editor-fullscreen-top-offset: 88px;
|
||||
--editor-border: rgba(148, 163, 184, 0.22);
|
||||
--editor-muted: #64748b;
|
||||
--editor-bg: #f8fafc;
|
||||
@@ -16,13 +17,34 @@
|
||||
padding-bottom: 140px;
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen {
|
||||
position: fixed;
|
||||
inset: var(--editor-fullscreen-top-offset) 0 0 0;
|
||||
z-index: 1080;
|
||||
width: 100%;
|
||||
max-width: none !important;
|
||||
margin: 0;
|
||||
padding: 1rem 1.25rem 140px;
|
||||
background: #f8fafc;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
body.policy-editor-fullscreen {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.policy-block-editor .editor-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.18fr) minmax(320px, 0.82fr);
|
||||
grid-template-columns: minmax(0, 0.98fr) minmax(420px, 1.02fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .editor-shell {
|
||||
grid-template-columns: minmax(0, 0.88fr) minmax(520px, 1.12fr);
|
||||
min-height: calc(100vh - var(--editor-fullscreen-top-offset) - 77px);
|
||||
}
|
||||
|
||||
.policy-block-editor .editor-panel,
|
||||
.policy-block-editor .preview-panel {
|
||||
border: 1px solid var(--editor-border);
|
||||
@@ -65,10 +87,34 @@
|
||||
min-height: calc(100vh - 210px);
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .editor-panel {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-panel {
|
||||
min-height: calc(100vh - 210px);
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .preview-panel {
|
||||
position: static;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.policy-block-editor .visual-editor {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .visual-editor {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.policy-block-editor .panel-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -100,16 +146,32 @@
|
||||
}
|
||||
|
||||
.policy-block-editor .editor-toolbar-title {
|
||||
min-width: 220px;
|
||||
min-width: 0;
|
||||
flex: 1 1 320px;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .editor-toolbar-title h1,
|
||||
.policy-block-editor .editor-toolbar-title p {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .editor-scroll {
|
||||
max-height: calc(100vh - 340px);
|
||||
overflow: auto;
|
||||
padding-right: 0.35rem;
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .editor-scroll {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.policy-block-editor.is-editor-fullscreen .preview-frame {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.policy-block-editor .validation-banner {
|
||||
display: none;
|
||||
gap: 0.75rem;
|
||||
@@ -141,6 +203,11 @@
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||
}
|
||||
|
||||
.policy-block-editor .block-card.has-floating-ui {
|
||||
overflow: visible;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.policy-block-editor .block-card:hover,
|
||||
.policy-block-editor .block-card.is-selected {
|
||||
border-color: rgba(184, 183, 106, 0.62);
|
||||
@@ -259,6 +326,139 @@
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-trigger {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.72rem 0.85rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-trigger:focus,
|
||||
.policy-block-editor .icon-combobox.is-open .icon-combobox-trigger {
|
||||
border-color: rgba(184, 183, 106, 0.65);
|
||||
box-shadow: 0 0 0 3px rgba(184, 183, 106, 0.12);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-preview {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(184, 183, 106, 0.12);
|
||||
color: #0f172a;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-text strong,
|
||||
.policy-block-editor .icon-combobox-text span {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-caret {
|
||||
color: #64748b;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.12);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-panel.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-search {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-option {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 0.6rem 0.7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-option:hover,
|
||||
.policy-block-editor .icon-combobox-option.is-active {
|
||||
background: rgba(184, 183, 106, 0.12);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-option-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-option-main span {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-empty {
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.35rem 0.2rem;
|
||||
}
|
||||
|
||||
.policy-block-editor .icon-combobox-empty.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.policy-block-editor .list-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -295,21 +495,26 @@
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device {
|
||||
margin: 0 auto;
|
||||
.policy-block-editor .preview-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device {
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device[data-mode="desktop"] {
|
||||
width: 1120px;
|
||||
width: min(100%, 920px);
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device[data-mode="tablet"] {
|
||||
width: 820px;
|
||||
width: min(100%, 820px);
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device[data-mode="mobile"] {
|
||||
width: 390px;
|
||||
width: min(100%, 390px);
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-canvas {
|
||||
@@ -318,6 +523,8 @@
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
padding: 2rem;
|
||||
min-height: 480px;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device[data-mode="desktop"] .preview-canvas {
|
||||
@@ -326,7 +533,7 @@
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-device[data-mode="desktop"] #previewContent {
|
||||
max-width: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-block {
|
||||
@@ -335,6 +542,18 @@
|
||||
scroll-margin-top: 140px;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-canvas h2,
|
||||
.policy-block-editor .preview-canvas h3,
|
||||
.policy-block-editor .preview-canvas h4,
|
||||
.policy-block-editor .preview-canvas h5,
|
||||
.policy-block-editor .preview-canvas p,
|
||||
.policy-block-editor .preview-canvas li,
|
||||
.policy-block-editor .preview-canvas blockquote,
|
||||
.policy-block-editor .preview-canvas .text-uppercase {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .preview-block.is-highlighted {
|
||||
background: rgba(184, 183, 106, 0.12);
|
||||
outline: 1px solid rgba(184, 183, 106, 0.35);
|
||||
@@ -369,6 +588,20 @@
|
||||
padding: 1.1rem 1.2rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: #f8fafc;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .callout-block > div,
|
||||
.policy-block-editor .callout-block p,
|
||||
.policy-block-editor .callout-block li,
|
||||
.policy-block-editor .callout-block h3,
|
||||
.policy-block-editor .callout-block h4,
|
||||
.policy-block-editor .callout-block h5,
|
||||
.policy-block-editor .callout-block a,
|
||||
.policy-block-editor .callout-block code {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.policy-block-editor .callout-block[data-tone="warning"] {
|
||||
@@ -534,6 +767,10 @@
|
||||
<button type="button" class="btn btn-primary" id="addBlockButton">
|
||||
<i class="fas fa-plus me-2"></i>Add block
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="toggleFullscreenButton" aria-pressed="false">
|
||||
<i class="fas fa-expand me-2" data-role="fullscreen-icon"></i>
|
||||
<span data-role="fullscreen-label">Full screen edit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user