The .zddc lint used a hand-kept TOP_KEYS/ROLE_KEYS/ACL_KEYS/CONVERT_KEYS list
with bespoke type tags — a parallel grammar that drifted from the Go structs
(we'd already had to patch in party_source/history/etc.). Replace it with one
schema-driven validator over the same JSON Schema that now drives completion +
hover, so lint/completion/hover/form all share ONE grammar.
- Bake zddc.schema.json into the bundle at build time (window.__ZDDC_SCHEMA__),
the exact file the server serves at /.api/zddc-schema. Synchronous + works
offline (file://), where that endpoint is unreachable — drops the runtime
fetch entirely.
- validateZddc() now walks the parsed doc against the schema: type, enum,
pattern, properties, additionalProperties (false|schema), patternProperties,
items, and the recursive $ref:"#" (paths:). Same {keyPath,severity,message}
shape, so findLine + the CM lint helper are unchanged.
- Delete TOP_KEYS/ALLOWED_TOOLS/ROLE_KEYS/ACL_KEYS/CONVERT_KEYS + walkObject/
checkValue/addTypeErr. preview-yaml completion now reads getZddcSchema too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
557 lines
25 KiB
JavaScript
557 lines
25 KiB
JavaScript
// preview-yaml.js — YAML editor plugin for the browse preview pane.
|
|
//
|
|
// Routes any .yaml / .yml file, plus the .zddc cascade files
|
|
// (`.zddc` and `*.zddc.yaml`), through a CodeMirror 5 editor with
|
|
// syntax highlighting and live linting. js-yaml.loadAll feeds parse
|
|
// errors into CM's lint gutter; for .zddc files an additional
|
|
// schema-aware pass flags unknown keys, bad enum values, and wrong
|
|
// types.
|
|
//
|
|
// Layout (single column):
|
|
// ┌─────────────────────────────────────────────────────────────┐
|
|
// │ name | dirty | status | source | [Save] │
|
|
// ├─────────────────────────────────────────────────────────────┤
|
|
// │ CodeMirror editor (line numbers + lint gutter) │
|
|
// └─────────────────────────────────────────────────────────────┘
|
|
//
|
|
// Save (Ctrl+S) writes back via PUT (server mode) or
|
|
// FileSystemWritableFileStream (FS-API). Zip members and
|
|
// virtual nodes are read-only — Save stays disabled.
|
|
(function () {
|
|
'use strict';
|
|
|
|
if (!window.app || !window.app.modules) return;
|
|
|
|
var util = window.app.modules.util;
|
|
var escapeHtml = util.escapeHtml;
|
|
|
|
// ── Filename routing ────────────────────────────────────────────────────
|
|
|
|
// True for .zddc cascade files — `.zddc` (literal name, no ext)
|
|
// and `<anything>.zddc.yaml` (e.g. `defaults.zddc.yaml`). These
|
|
// get the schema-aware lint layer.
|
|
function isZddcFile(name) {
|
|
if (!name) return false;
|
|
if (name === '.zddc') return true;
|
|
return /\.zddc\.ya?ml$/i.test(name);
|
|
}
|
|
|
|
function isYamlFile(node) {
|
|
if (!node || !node.name) return false;
|
|
if (isZddcFile(node.name)) return true;
|
|
var ext = (node.ext || '').toLowerCase();
|
|
return ext === 'yaml' || ext === 'yml';
|
|
}
|
|
|
|
// ── Save (mirrors preview-markdown.js) ─────────────────────────────────
|
|
|
|
function saveContent(node, content, opts) {
|
|
// Via the shared saveFile so local (FS-Access) saves escalate to
|
|
// readwrite the same as the markdown editor — previously this path
|
|
// skipped ensureWritable and failed on read-only-picked folders.
|
|
return util.saveFile(node, content, 'application/x-yaml; charset=utf-8', opts);
|
|
}
|
|
|
|
var isZipMemberNode = util.isZipMemberNode;
|
|
var isEditableZipMember = util.isEditableZipMember;
|
|
|
|
function canSave(node) {
|
|
// A .zddc.zip bundle member is saveable iff editable (elevated admin);
|
|
// the server's ServeZipWrite is the real gate. Other zip members are
|
|
// read-only.
|
|
if (isZipMemberNode(node)) return isEditableZipMember(node);
|
|
// Virtual .zddc placeholders are designed to be saved — a PUT
|
|
// materializes the file from the synthetic body and the next
|
|
// listing serves a real entry. Every other virtual node (per-
|
|
// user home, canonical-folder virtuals) is just a tree
|
|
// affordance, not a writable file.
|
|
if (node.virtual && node.name !== '.zddc') return false;
|
|
// Server-computed authority gate. The virtual .zddc entry
|
|
// requires the admin verb 'a' (matches fileapi.go's
|
|
// ActionAdmin gate at the .zddc URL); regular YAML files
|
|
// require write 'w'. cap.has falls back to node.writable for
|
|
// 'w' when verbs is absent (offline FS-API listings).
|
|
if (node.url && window.app.state.source === 'server' && window.zddc.cap) {
|
|
var needed = node.name === '.zddc' ? 'a' : 'w';
|
|
if (!window.zddc.cap.has(node, needed)) return false;
|
|
}
|
|
if (node.handle && typeof node.handle.createWritable === 'function') return true;
|
|
if (node.url && window.app.state.source === 'server') return true;
|
|
return false;
|
|
}
|
|
|
|
var hashContent = util.hashContent;
|
|
|
|
// ── .zddc schema ────────────────────────────────────────────────────────
|
|
//
|
|
// Mirrors the Go-side decoder in zddc/internal/zddc/*. Allowed
|
|
// tool names are the embedded set (always available) plus the
|
|
// composable ones served when declared in apps:. Unknown keys at
|
|
// any level surface as warnings — typos like `defaul_tool` are
|
|
// common and the cascade silently ignores them.
|
|
|
|
// The valid keys, types, enums and nesting are NOT hand-listed here any
|
|
// more — they come from the baked .zddc JSON Schema (window.__ZDDC_SCHEMA__,
|
|
// the same grammar the server serves at /.api/zddc-schema and that drives
|
|
// completion + hover). One source, no drift. See validateZddcSchema below.
|
|
|
|
function typeOf(v) {
|
|
if (v === null || v === undefined) return 'null';
|
|
if (Array.isArray(v)) return 'array';
|
|
return typeof v; // 'string' | 'number' | 'boolean' | 'object'
|
|
}
|
|
|
|
// The .zddc JSON Schema, baked into the bundle at build time
|
|
// (window.__ZDDC_SCHEMA__ — the same file the server serves at
|
|
// /.api/zddc-schema). Single source for lint, completion and hover; works
|
|
// offline. Synchronous, so the lint helper can use it directly.
|
|
function getZddcSchema() {
|
|
return (window.__ZDDC_SCHEMA__ && window.__ZDDC_SCHEMA__.properties)
|
|
? window.__ZDDC_SCHEMA__ : {};
|
|
}
|
|
|
|
// Validate a parsed .zddc document against the JSON Schema, producing
|
|
// { keyPath, severity, message } issues (mapped to source lines by
|
|
// findLine). Covers the draft-2020-12 subset .zddc uses: type, enum,
|
|
// properties, additionalProperties (false | schema), patternProperties,
|
|
// items, pattern, and the recursive $ref:"#" (paths:).
|
|
function validateZddc(doc) {
|
|
var schema = getZddcSchema();
|
|
var issues = [];
|
|
if (!schema || !schema.properties) return issues; // schema unavailable
|
|
if (typeOf(doc) === 'null') return issues;
|
|
if (typeOf(doc) !== 'object') {
|
|
issues.push({ keyPath: [], severity: 'error',
|
|
message: 'Root must be a map (got ' + typeOf(doc) + ').' });
|
|
return issues;
|
|
}
|
|
function deref(n) { return (n && n.$ref === '#') ? schema : n; }
|
|
function typeOk(t, want) {
|
|
if (Array.isArray(want)) {
|
|
for (var i = 0; i < want.length; i++) if (typeOk(t, want[i])) return true;
|
|
return false;
|
|
}
|
|
if (want === 'integer' || want === 'number') return t === 'number';
|
|
return t === want;
|
|
}
|
|
function walk(value, sch, path) {
|
|
sch = deref(sch);
|
|
if (!sch) return;
|
|
var t = typeOf(value);
|
|
if (t === 'null') return; // empty value mid-edit — don't flag
|
|
if (sch.type && !typeOk(t, sch.type)) {
|
|
issues.push({ keyPath: path, severity: 'error',
|
|
message: 'Expected ' + (Array.isArray(sch.type) ? sch.type.join('/') : sch.type)
|
|
+ ', got ' + t + '.' });
|
|
return;
|
|
}
|
|
if (sch.enum && sch.enum.map(String).indexOf(String(value)) === -1) {
|
|
issues.push({ keyPath: path, severity: 'warning',
|
|
message: 'Unknown value "' + value + '". Allowed: ' + sch.enum.join(', ') + '.' });
|
|
}
|
|
if (sch.pattern && t === 'string' && !new RegExp(sch.pattern).test(value)) {
|
|
issues.push({ keyPath: path, severity: 'error',
|
|
message: 'Value "' + value + '" must match ' + sch.pattern + '.' });
|
|
}
|
|
if (t === 'object') {
|
|
var props = sch.properties || {};
|
|
for (var k in value) {
|
|
if (!Object.prototype.hasOwnProperty.call(value, k)) continue;
|
|
var kp = path.concat([k]);
|
|
if (props[k]) { walk(value[k], props[k], kp); continue; }
|
|
var ap = sch.additionalProperties;
|
|
if (ap && typeof ap === 'object') { walk(value[k], ap, kp); continue; }
|
|
if (sch.patternProperties) {
|
|
var matched = null;
|
|
for (var p in sch.patternProperties) {
|
|
if (Object.prototype.hasOwnProperty.call(sch.patternProperties, p)
|
|
&& new RegExp(p).test(k)) { matched = sch.patternProperties[p]; break; }
|
|
}
|
|
if (matched) { walk(value[k], matched, kp); continue; }
|
|
}
|
|
if (ap === false) {
|
|
issues.push({ keyPath: kp, severity: 'warning',
|
|
message: 'Unknown key "' + k + '" — not in the .zddc schema; it will be ignored.' });
|
|
}
|
|
}
|
|
} else if (t === 'array' && sch.items) {
|
|
for (var i = 0; i < value.length; i++) {
|
|
walk(value[i], sch.items, path.concat([String(i)]));
|
|
}
|
|
}
|
|
}
|
|
walk(doc, schema, []);
|
|
return issues;
|
|
}
|
|
|
|
// Locate the source line for a key path. .zddc files are
|
|
// block-style YAML in practice (no flow style, no anchors), so a
|
|
// simple indent-aware scan works: for each segment, find a line
|
|
// matching "<indent><key>:" whose indent is deeper than the
|
|
// previously-matched line. Falls back to line 0 if no match.
|
|
function findLine(source, keyPath) {
|
|
if (!keyPath || keyPath.length === 0) return 0;
|
|
var lines = source.split('\n');
|
|
var prevIndent = -1;
|
|
var prevLine = 0;
|
|
for (var i = 0; i < keyPath.length; i++) {
|
|
var key = keyPath[i];
|
|
var found = -1;
|
|
// Escape regex metachars in the key.
|
|
var keyRe = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
var re = new RegExp('^(\\s*)"?' + keyRe + '"?\\s*:');
|
|
for (var j = prevLine; j < lines.length; j++) {
|
|
var m = lines[j].match(re);
|
|
if (m && m[1].length > prevIndent) {
|
|
found = j;
|
|
prevIndent = m[1].length;
|
|
prevLine = j + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (found === -1) return prevLine > 0 ? prevLine - 1 : 0;
|
|
}
|
|
return prevLine > 0 ? prevLine - 1 : 0;
|
|
}
|
|
|
|
// ── CodeMirror lint helper ──────────────────────────────────────────────
|
|
|
|
function registerLinter(CM) {
|
|
// The lint helper signature: function(text, options, editor) → annotations[]
|
|
// Each annotation: { from, to, message, severity }.
|
|
CM.registerHelper('lint', 'yaml', function (text, _opts, editor) {
|
|
var out = [];
|
|
if (!window.jsyaml) return out;
|
|
var parsed;
|
|
try {
|
|
// loadAll handles multi-doc YAML; we only validate the
|
|
// first doc against the schema (the .zddc cascade reads
|
|
// only the first document).
|
|
var docs = [];
|
|
window.jsyaml.loadAll(text, function (d) { docs.push(d); });
|
|
parsed = docs[0];
|
|
} catch (e) {
|
|
var mark = e.mark;
|
|
var pos = mark ? CM.Pos(mark.line, mark.column) : CM.Pos(0, 0);
|
|
out.push({ from: pos, to: pos, severity: 'error',
|
|
message: e.message || String(e) });
|
|
return out;
|
|
}
|
|
// Schema layer — only for .zddc cascade files.
|
|
var node = editor._zddcNode;
|
|
if (node && isZddcFile(node.name)) {
|
|
var issues = validateZddc(parsed);
|
|
for (var i = 0; i < issues.length; i++) {
|
|
var ln = findLine(text, issues[i].keyPath);
|
|
out.push({
|
|
from: CM.Pos(ln, 0),
|
|
to: CM.Pos(ln, (editor.getLine(ln) || '').length),
|
|
severity: issues[i].severity,
|
|
message: issues[i].message
|
|
});
|
|
}
|
|
}
|
|
return out;
|
|
});
|
|
}
|
|
|
|
// ── Mount ───────────────────────────────────────────────────────────────
|
|
|
|
var currentEditor = null;
|
|
var currentDirty = false;
|
|
var currentNodeRef = null;
|
|
// Server version token for the loaded file — sent as If-Match on save
|
|
// and refreshed from each successful PUT's response ETag.
|
|
var currentEtag = null;
|
|
var currentLastModified = null;
|
|
|
|
function dispose() {
|
|
// CM doesn't have an explicit destroy(); GC handles it once
|
|
// the host element is removed. Clear our reference so a stale
|
|
// editor doesn't keep handlers alive.
|
|
currentEditor = null;
|
|
currentDirty = false;
|
|
currentNodeRef = null;
|
|
currentEtag = null;
|
|
currentLastModified = null;
|
|
}
|
|
|
|
function isDirty() {
|
|
return currentDirty;
|
|
}
|
|
|
|
function currentNode() {
|
|
return currentNodeRef;
|
|
}
|
|
|
|
async function render(node, container, ctx) {
|
|
if (typeof window.CodeMirror === 'undefined') {
|
|
container.innerHTML =
|
|
'<div class="preview-empty" style="color:var(--danger)">'
|
|
+ 'CodeMirror isn\'t bundled in this build.</div>';
|
|
return;
|
|
}
|
|
dispose();
|
|
|
|
var text, loadedEtag = null, loadedLastModified = null;
|
|
try {
|
|
if (ctx.getContentWithVersion) {
|
|
var loaded = await ctx.getContentWithVersion(node);
|
|
text = new TextDecoder('utf-8', { fatal: false }).decode(loaded.buf);
|
|
loadedEtag = loaded.etag;
|
|
loadedLastModified = loaded.lastModified;
|
|
} else {
|
|
var buf = await ctx.getArrayBuffer(node);
|
|
text = new TextDecoder('utf-8', { fatal: false }).decode(buf);
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML =
|
|
'<div class="preview-empty" style="color:var(--danger)">'
|
|
+ 'Could not read ' + escapeHtml(node.name) + ': '
|
|
+ escapeHtml(e.message || String(e)) + '</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
var shell = document.createElement('div');
|
|
shell.className = 'yaml-shell';
|
|
container.appendChild(shell);
|
|
|
|
// Info header — same look as the markdown plugin's info-header
|
|
// so the two editors feel like one family.
|
|
var infohdr = document.createElement('div');
|
|
infohdr.className = 'md-shell__infohdr yaml-shell__infohdr';
|
|
|
|
var titleEl = document.createElement('span');
|
|
titleEl.className = 'md-shell__title';
|
|
titleEl.textContent = node.name;
|
|
titleEl.title = node.name;
|
|
|
|
var schemaTag = document.createElement('span');
|
|
schemaTag.className = 'md-shell__source yaml-shell__schema';
|
|
if (isZddcFile(node.name)) {
|
|
schemaTag.textContent = '.zddc schema ↗';
|
|
schemaTag.title = 'Linted against the .zddc cascade schema '
|
|
+ '(unknown keys, bad enums, and wrong types are flagged). '
|
|
+ 'Click to view the full JSON Schema.';
|
|
// Clickable → opens the canonical machine grammar the lint mirrors.
|
|
schemaTag.classList.add('yaml-shell__schema--link');
|
|
schemaTag.setAttribute('role', 'link');
|
|
schemaTag.setAttribute('tabindex', '0');
|
|
var openSchema = function () {
|
|
window.open('/.api/zddc-schema', '_blank', 'noopener');
|
|
};
|
|
schemaTag.addEventListener('click', openSchema);
|
|
schemaTag.addEventListener('keydown', function (ev) {
|
|
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openSchema(); }
|
|
});
|
|
} else {
|
|
schemaTag.textContent = 'YAML';
|
|
}
|
|
|
|
var dirtyEl = document.createElement('span');
|
|
dirtyEl.className = 'md-shell__dirty';
|
|
|
|
var statusEl = document.createElement('span');
|
|
statusEl.className = 'md-shell__status';
|
|
|
|
var sourceEl = document.createElement('span');
|
|
sourceEl.className = 'md-shell__source';
|
|
if (isZipMemberNode(node)) sourceEl.textContent = isEditableZipMember(node) ? 'config bundle' : 'read-only (zip)';
|
|
else if (node.handle) sourceEl.textContent = 'local';
|
|
else if (node.url) sourceEl.textContent = 'server';
|
|
|
|
var saveBtn = document.createElement('button');
|
|
saveBtn.className = 'btn btn-sm btn-primary md-shell__save';
|
|
saveBtn.type = 'button';
|
|
saveBtn.textContent = 'Save';
|
|
saveBtn.disabled = true;
|
|
|
|
infohdr.appendChild(titleEl);
|
|
infohdr.appendChild(schemaTag);
|
|
infohdr.appendChild(dirtyEl);
|
|
infohdr.appendChild(statusEl);
|
|
infohdr.appendChild(sourceEl);
|
|
infohdr.appendChild(saveBtn);
|
|
shell.appendChild(infohdr);
|
|
|
|
var editorHost = document.createElement('div');
|
|
editorHost.className = 'yaml-shell__editor';
|
|
shell.appendChild(editorHost);
|
|
|
|
// Register the lint helper once per page lifetime.
|
|
if (!window.CodeMirror.__zddcYamlLinterReady) {
|
|
registerLinter(window.CodeMirror);
|
|
window.CodeMirror.__zddcYamlLinterReady = true;
|
|
}
|
|
|
|
var writable = canSave(node);
|
|
var editor = window.CodeMirror(editorHost, {
|
|
value: text,
|
|
mode: 'yaml',
|
|
lineNumbers: true,
|
|
tabSize: 2,
|
|
indentUnit: 2,
|
|
indentWithTabs: false,
|
|
lineWrapping: false,
|
|
gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers'],
|
|
lint: { hasGutters: true },
|
|
// autofocus:false keeps the keyboard caret in the browse
|
|
// tree pane so arrow-key nav can continue through yaml /
|
|
// .zddc files without diverting into the editor. User
|
|
// clicks (or tabs) into the editor when they want to type.
|
|
autofocus: false,
|
|
// Read-only uses readOnly:true (NOT "nocursor"): the editor
|
|
// stays focusable so the user can click in, select text, and
|
|
// copy — they just can't edit. "nocursor" removes the textarea
|
|
// from focus, which also kills click-drag selection (the whole
|
|
// reason a viewer would otherwise force admin mode just to copy
|
|
// a .zddc snippet). autofocus:false keeps arrow-key tree nav
|
|
// intact until the user deliberately clicks into the editor.
|
|
readOnly: !writable,
|
|
});
|
|
// Stash the node on the editor so the lint helper can decide
|
|
// whether to apply the .zddc schema layer.
|
|
editor._zddcNode = node;
|
|
// Force an initial lint pass now that _zddcNode is set.
|
|
editor.performLint();
|
|
// Schema completion + hover docs for .zddc files (the machine grammar
|
|
// drives keys, enum/boolean values, and nested paths via $ref:"#").
|
|
// Plain .yaml gets no schema (lint + highlighting only).
|
|
var yc = window.app.modules.yamlComplete;
|
|
if (yc && isZddcFile(node.name)) {
|
|
yc.attach(editor, yc.schemaProvider(getZddcSchema), { readOnly: !writable });
|
|
}
|
|
currentEditor = editor;
|
|
currentNodeRef = node;
|
|
currentDirty = false;
|
|
currentEtag = loadedEtag;
|
|
currentLastModified = loadedLastModified;
|
|
|
|
if (!writable) {
|
|
saveBtn.disabled = true;
|
|
saveBtn.title = 'Save not available — read-only source.';
|
|
// Read-only banner above the editor explains why.
|
|
var roBanner = document.createElement('div');
|
|
roBanner.className = 'yaml-readonly-banner';
|
|
roBanner.innerHTML = '<span aria-hidden="true">🔒</span>'
|
|
+ ' Read-only — you don\'t have write access to this file.';
|
|
editorHost.insertBefore(roBanner, editorHost.firstChild);
|
|
}
|
|
|
|
var initialHash = await hashContent(text);
|
|
|
|
function markDirty(isDirty) {
|
|
if (currentEditor !== editor) return; // editor replaced
|
|
currentDirty = isDirty;
|
|
saveBtn.disabled = !isDirty || !canSave(node);
|
|
dirtyEl.textContent = isDirty ? '● modified' : '';
|
|
}
|
|
|
|
editor.on('change', async function () {
|
|
if (currentEditor !== editor) return; // switched away
|
|
var h = await hashContent(editor.getValue());
|
|
if (currentEditor !== editor) return; // replaced during await
|
|
markDirty(h !== initialHash);
|
|
});
|
|
|
|
// Adopt the new server ETag + refresh the dirty baseline after a
|
|
// successful write so save→edit→save doesn't false-conflict.
|
|
async function markSaved(content, res) {
|
|
if (currentEditor !== editor) return;
|
|
if (res && res.etag) currentEtag = res.etag;
|
|
initialHash = await hashContent(content);
|
|
if (currentEditor !== editor) return;
|
|
markDirty(false);
|
|
statusEl.textContent = 'Saved ' + new Date().toLocaleTimeString();
|
|
if (window.zddc && window.zddc.toast) {
|
|
window.zddc.toast('Saved ' + node.name, 'success');
|
|
}
|
|
}
|
|
|
|
// 412 → file changed on the server since load. Open the shared
|
|
// conflict dialog instead of clobbering.
|
|
async function resolveConflict(content) {
|
|
var conflict = window.app.modules.conflict;
|
|
var prev = window.app.modules.preview;
|
|
if (!conflict || !prev) return;
|
|
await conflict.open({
|
|
filename: node.name,
|
|
mineText: content,
|
|
fetchTheirs: function () {
|
|
return prev.getContentWithVersion(node).then(function (r) {
|
|
return new TextDecoder('utf-8', { fatal: false }).decode(r.buf);
|
|
});
|
|
},
|
|
onOverwrite: function () {
|
|
return prev.getContentWithVersion(node).then(function (cur) {
|
|
return saveContent(node, content, { etag: cur.etag, lastModified: cur.lastModified });
|
|
}).then(function (res) { return markSaved(content, res); });
|
|
},
|
|
onReload: function () {
|
|
markDirty(false);
|
|
currentDirty = false;
|
|
return prev.showFilePreview(node);
|
|
},
|
|
onSaveCopy: function () {
|
|
return util.saveCopy(node, content, 'application/x-yaml; charset=utf-8')
|
|
.then(function (name) {
|
|
if (window.zddc && window.zddc.toast) {
|
|
window.zddc.toast('Saved your version as ' + name, 'success');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (currentEditor === editor) statusEl.textContent = '';
|
|
}
|
|
|
|
async function save() {
|
|
if (saveBtn.disabled) return;
|
|
// Re-check authority at click time, not via the mount-time
|
|
// `writable` capture — the listing may have re-evaluated
|
|
// (e.g. user toggled admin mode without a hard reload).
|
|
if (!canSave(node)) return;
|
|
var content = editor.getValue();
|
|
try {
|
|
statusEl.textContent = 'Saving…';
|
|
var res = await saveContent(node, content, {
|
|
etag: currentEtag, lastModified: currentLastModified
|
|
});
|
|
await markSaved(content, res);
|
|
} catch (e) {
|
|
if (e && e.status === 412) {
|
|
if (currentEditor !== editor) return;
|
|
statusEl.textContent = 'Conflict — resolving…';
|
|
await resolveConflict(content);
|
|
return;
|
|
}
|
|
statusEl.textContent = 'Save failed: ' + (e.message || e);
|
|
if (window.zddc && window.zddc.toast) {
|
|
window.zddc.toast('Save failed: ' + (e.message || e), 'error');
|
|
}
|
|
}
|
|
}
|
|
saveBtn.addEventListener('click', save);
|
|
editor.setOption('extraKeys', {
|
|
'Ctrl-S': save,
|
|
'Cmd-S': save
|
|
});
|
|
|
|
// CM defers layout until its host has a size — refresh after
|
|
// mount so the gutters and viewport sync to the grid cell.
|
|
setTimeout(function () { try { editor.refresh(); } catch (_e) {} }, 0);
|
|
}
|
|
|
|
function handles(node) {
|
|
if (!node || node.isDir || node.isZip) return false;
|
|
return isYamlFile(node);
|
|
}
|
|
|
|
window.app.modules.yamledit = {
|
|
handles: handles,
|
|
render: render,
|
|
dispose: dispose,
|
|
isDirty: isDirty,
|
|
currentNode: currentNode
|
|
};
|
|
})();
|