JSON canvas initial
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
// edit.js — editing a node's content.
|
||||
//
|
||||
// Text nodes get the vendored CodeMirror editor in place, positioned over the
|
||||
// node in *screen* coordinates rather than mounted inside the zoomed world.
|
||||
// CodeMirror measures its own layout in device pixels, so a CSS-scaled parent
|
||||
// wrecks caret placement and selection maths; a fixed-position panel sidesteps
|
||||
// that entirely and stays sharp at every zoom level.
|
||||
//
|
||||
// The other node types have nothing to type into, so they reuse the existing
|
||||
// tree picker and modal instead of an editor.
|
||||
window.Canvas = window.Canvas || {};
|
||||
|
||||
Canvas.Edit = (function () {
|
||||
var Doc = Canvas.Doc;
|
||||
var View = Canvas.View;
|
||||
|
||||
var MIN_WIDTH = 360;
|
||||
var MIN_HEIGHT = 200;
|
||||
|
||||
var panel = null;
|
||||
var view = null;
|
||||
var currentId = null;
|
||||
var original = '';
|
||||
|
||||
function destroy() {
|
||||
if (view) { view.destroy(); view = null; }
|
||||
if (panel) { panel.remove(); panel = null; }
|
||||
currentId = null;
|
||||
original = '';
|
||||
document.removeEventListener('pointerdown', onOutside, true);
|
||||
}
|
||||
|
||||
function commit() {
|
||||
if (!panel || !view) return;
|
||||
var text = view.state.doc.toString();
|
||||
var id = currentId;
|
||||
destroy();
|
||||
var n = Doc.node(id);
|
||||
if (!n || text === original) return;
|
||||
Doc.begin('edit text');
|
||||
n.text = text;
|
||||
Doc.commit();
|
||||
}
|
||||
|
||||
function onOutside(e) {
|
||||
if (!panel || panel.contains(e.target)) return;
|
||||
commit();
|
||||
}
|
||||
|
||||
// screenRect places the editor over the node it edits, clamped to a usable
|
||||
// minimum and kept inside the viewport — a 60x60 node still needs a panel
|
||||
// big enough to type in.
|
||||
function screenRect(n) {
|
||||
var s = View.scale();
|
||||
var tl = View.toScreen(n.x, n.y);
|
||||
var w = Math.max(MIN_WIDTH, n.width * s);
|
||||
var h = Math.max(MIN_HEIGHT, n.height * s);
|
||||
w = Math.min(w, window.innerWidth - 16);
|
||||
h = Math.min(h, window.innerHeight - 16);
|
||||
var left = Math.max(8, Math.min(tl.x, window.innerWidth - w - 8));
|
||||
var top = Math.max(8, Math.min(tl.y, window.innerHeight - h - 8));
|
||||
return { left: left, top: top, width: w, height: h };
|
||||
}
|
||||
|
||||
function openText(n) {
|
||||
var rect = screenRect(n);
|
||||
panel = document.createElement('div');
|
||||
panel.className = 'canvas-editor panel panel-floating';
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = rect.top + 'px';
|
||||
panel.style.width = rect.width + 'px';
|
||||
panel.style.height = rect.height + 'px';
|
||||
|
||||
var hint = document.createElement('div');
|
||||
hint.className = 'panel-header';
|
||||
hint.textContent = 'Ctrl+Enter or click away to apply · Escape to cancel';
|
||||
panel.appendChild(hint);
|
||||
|
||||
var mount = document.createElement('div');
|
||||
mount.className = 'canvas-editor-mount';
|
||||
panel.appendChild(mount);
|
||||
document.body.appendChild(panel);
|
||||
|
||||
currentId = n.id;
|
||||
original = n.text || '';
|
||||
|
||||
view = new CM.EditorView({
|
||||
state: CM.EditorState.create({
|
||||
doc: original,
|
||||
selection: { anchor: original.length },
|
||||
extensions: [
|
||||
CM.history(),
|
||||
CM.drawSelection(),
|
||||
CM.indentOnInput(),
|
||||
CM.EditorView.lineWrapping,
|
||||
CM.EditorView.contentAttributes.of({ spellcheck: 'true', autocapitalize: 'sentences' }),
|
||||
CM.markdown({ base: CM.markdownLanguage }),
|
||||
CM.syntaxHighlighting(CM.highlightStyle),
|
||||
CM.closeBrackets(),
|
||||
CM.autocompletion({ override: [WikiComplete.source] }),
|
||||
CM.theme,
|
||||
// Mod-Enter goes first (nothing else claims it); Escape goes
|
||||
// last so an open completion popup still gets first refusal
|
||||
// on it and closes itself instead of discarding the edit.
|
||||
CM.keymap.of([{ key: 'Mod-Enter', run: function () { commit(); return true; } }]
|
||||
.concat(
|
||||
CM.closeBracketsKeymap,
|
||||
CM.completionKeymap,
|
||||
CM.markdownKeymap,
|
||||
CM.defaultKeymap,
|
||||
CM.historyKeymap,
|
||||
[CM.indentWithTab],
|
||||
[{ key: 'Escape', run: function () { destroy(); return true; } }]
|
||||
))
|
||||
]
|
||||
}),
|
||||
parent: mount
|
||||
});
|
||||
view.focus();
|
||||
// Capture phase: a click on a toolbar button should apply the edit
|
||||
// before that button's own handler reads the document.
|
||||
document.addEventListener('pointerdown', onOutside, true);
|
||||
}
|
||||
|
||||
function openFile(n) {
|
||||
var start = (n.file || '/').replace(/\/+$/, '') || '/';
|
||||
openTreePicker({
|
||||
title: 'File node — pick a target',
|
||||
mode: 'any',
|
||||
initialPath: start,
|
||||
preselect: start,
|
||||
confirmLabel: 'USE',
|
||||
onSelect: function (p) {
|
||||
if (!p || p === n.file) return;
|
||||
Doc.begin('retarget file node');
|
||||
n.file = p;
|
||||
n.subpath = '';
|
||||
Doc.commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function promptValue(title, label, value, onValue) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'input';
|
||||
input.placeholder = label;
|
||||
input.value = value || '';
|
||||
openModal({
|
||||
title: title,
|
||||
body: input,
|
||||
confirm: {
|
||||
label: 'OK',
|
||||
onConfirm: function () {
|
||||
closeModal();
|
||||
onValue(input.value.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openLink(n) {
|
||||
promptValue('Link node', 'https://example.com', n.url, function (v) {
|
||||
if (!v || v === n.url) return;
|
||||
Doc.begin('edit link');
|
||||
n.url = v;
|
||||
Doc.commit();
|
||||
});
|
||||
}
|
||||
|
||||
function openGroup(n) {
|
||||
promptValue('Group label', 'Label', n.label, function (v) {
|
||||
if (v === (n.label || '')) return;
|
||||
Doc.begin('edit group label');
|
||||
if (v) n.label = v; else delete n.label;
|
||||
Doc.commit();
|
||||
});
|
||||
}
|
||||
|
||||
// close applies whatever is currently open, if anything.
|
||||
function close() {
|
||||
if (panel) commit();
|
||||
}
|
||||
|
||||
return {
|
||||
open: function (id) {
|
||||
var n = Doc.node(id);
|
||||
if (!n) return;
|
||||
close();
|
||||
switch (n.type) {
|
||||
case 'text': openText(n); break;
|
||||
case 'file': openFile(n); break;
|
||||
case 'link': openLink(n); break;
|
||||
case 'group': openGroup(n); break;
|
||||
}
|
||||
},
|
||||
|
||||
// Called from the gesture layer on any pointerdown on the surface, so
|
||||
// starting a drag never silently drops an edit.
|
||||
close: close,
|
||||
|
||||
isOpen: function () { return panel !== null; }
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user