Expose typed content fields for first home blocks
This commit is contained in:
@@ -197,6 +197,37 @@ h2 {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 14px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.content-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.content-fields .wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.content-fields .empty-note {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -265,4 +296,8 @@ textarea:focus {
|
||||
.field-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const el = {
|
||||
template: document.querySelector("#block-template"),
|
||||
anchor: document.querySelector("#block-anchor"),
|
||||
enabled: document.querySelector("#block-enabled"),
|
||||
contentFields: document.querySelector("#content-fields"),
|
||||
json: document.querySelector("#block-json"),
|
||||
moveUp: document.querySelector("#move-up"),
|
||||
moveDown: document.querySelector("#move-down"),
|
||||
@@ -36,6 +37,25 @@ function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function getByPath(source, path) {
|
||||
return path.split(".").reduce((value, key) => value?.[key], source);
|
||||
}
|
||||
|
||||
function setByPath(source, path, nextValue) {
|
||||
const keys = path.split(".");
|
||||
const lastKey = keys.pop();
|
||||
let target = source;
|
||||
|
||||
for (const key of keys) {
|
||||
if (target[key] == null) {
|
||||
target[key] = /^\d+$/.test(keys[0]) ? [] : {};
|
||||
}
|
||||
target = target[key];
|
||||
}
|
||||
|
||||
target[lastKey] = nextValue;
|
||||
}
|
||||
|
||||
function activeRegion() {
|
||||
if (!state.selected) return null;
|
||||
return state.page.regions[state.selected.regionIndex];
|
||||
@@ -118,6 +138,7 @@ function renderEditor() {
|
||||
el.template.value = block.template || "";
|
||||
el.anchor.value = block.anchor || "";
|
||||
el.enabled.checked = block.enabled !== false;
|
||||
renderContentFields(block);
|
||||
el.json.value = JSON.stringify(block, null, 2);
|
||||
}
|
||||
|
||||
@@ -130,6 +151,7 @@ function syncBlockFromFields() {
|
||||
const block = activeBlock();
|
||||
if (!block) return;
|
||||
|
||||
syncContentFields();
|
||||
block.id = el.id.value.trim();
|
||||
block.adminLabel = el.label.value.trim();
|
||||
block.anchor = el.anchor.value.trim() || null;
|
||||
@@ -138,6 +160,54 @@ function syncBlockFromFields() {
|
||||
renderRegions();
|
||||
}
|
||||
|
||||
function renderContentFields(block) {
|
||||
el.contentFields.innerHTML = "";
|
||||
|
||||
if (!block.editableFields?.length) {
|
||||
const note = document.createElement("div");
|
||||
note.className = "empty-note";
|
||||
note.textContent = "Для этого блока typed-поля ещё не выделены. Пока доступно редактирование через JSON.";
|
||||
el.contentFields.append(note);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const editableField of block.editableFields) {
|
||||
const label = document.createElement("label");
|
||||
const value = getByPath(block, editableField.path) ?? "";
|
||||
const isLong = editableField.kind === "html" || editableField.kind === "textarea";
|
||||
const input = isLong ? document.createElement("textarea") : document.createElement("input");
|
||||
|
||||
label.className = isLong ? "wide" : "";
|
||||
label.textContent = editableField.label || editableField.path;
|
||||
input.dataset.path = editableField.path;
|
||||
input.value = value;
|
||||
|
||||
if (!isLong) {
|
||||
input.type = editableField.kind === "media" || editableField.kind === "url" ? "text" : "text";
|
||||
input.autocomplete = "off";
|
||||
}
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
const active = activeBlock();
|
||||
if (!active) return;
|
||||
setByPath(active, editableField.path, input.value);
|
||||
el.json.value = JSON.stringify(active, null, 2);
|
||||
});
|
||||
|
||||
label.append(input);
|
||||
el.contentFields.append(label);
|
||||
}
|
||||
}
|
||||
|
||||
function syncContentFields() {
|
||||
const block = activeBlock();
|
||||
if (!block) return;
|
||||
|
||||
for (const input of el.contentFields.querySelectorAll("[data-path]")) {
|
||||
setByPath(block, input.dataset.path, input.value);
|
||||
}
|
||||
}
|
||||
|
||||
function syncBlockFromJson() {
|
||||
const region = activeRegion();
|
||||
if (!region) return false;
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
<button id="delete-block" class="danger-btn" type="button">Удалить</button>
|
||||
</div>
|
||||
|
||||
<section class="content-panel">
|
||||
<div class="section-title">Контент блока</div>
|
||||
<div id="content-fields" class="content-fields"></div>
|
||||
</section>
|
||||
|
||||
<label class="json-field">
|
||||
JSON выбранного блока
|
||||
<textarea id="block-json" spellcheck="false"></textarea>
|
||||
|
||||
Reference in New Issue
Block a user