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:
Tống Thành Đạt
2026-04-22 12:02:00 +07:00
parent cdaddddfeb
commit a44d005c13
7 changed files with 620 additions and 25 deletions
@@ -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;
+245 -8
View File
@@ -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>