@cubud/wen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ import yaml from 'https://esm.sh/js-yaml@4.1.0';
2
+ import { escapeHtml } from './wen-utils.js';
3
+
4
+ export class WenYamlView {
5
+ constructor(node, getPos, editor) {
6
+ this.node = node;
7
+ this.getPos = getPos;
8
+ this.editor = editor;
9
+
10
+ this.dom = document.createElement('div');
11
+ this.dom.className = 'wen-yaml-block';
12
+ this.dom.contentEditable = 'false';
13
+
14
+ this.dom.innerHTML = `
15
+ <div class="wen-yaml-header">
16
+ <span class="wen-yaml-title">🗂️ Properties</span>
17
+ <button class="wen-yaml-toggle-btn" data-state="visual">View Raw</button>
18
+ </div>
19
+ <div class="wen-yaml-visual-container">
20
+ <div class="wen-yaml-view-visual active"></div>
21
+ <div class="wen-yaml-fade"></div>
22
+ <button class="wen-yaml-expand-btn">▼ Show More</button>
23
+ </div>
24
+ <div class="wen-yaml-view-raw"><textarea class="wen-yaml-raw-input"></textarea></div>
25
+ <div class="wen-yaml-error-state hidden"><span class="wen-yaml-error-msg"></span></div>
26
+ `;
27
+
28
+ const toggleBtn = this.dom.querySelector('.wen-yaml-toggle-btn');
29
+ const visualContainer = this.dom.querySelector('.wen-yaml-visual-container');
30
+ const visualView = this.dom.querySelector('.wen-yaml-view-visual');
31
+ const expandBtn = this.dom.querySelector('.wen-yaml-expand-btn');
32
+ const rawView = this.dom.querySelector('.wen-yaml-view-raw');
33
+ const rawInput = this.dom.querySelector('.wen-yaml-raw-input');
34
+ const errorState = this.dom.querySelector('.wen-yaml-error-state');
35
+ const errorMsg = this.dom.querySelector('.wen-yaml-error-msg');
36
+
37
+ rawInput.value = this.node.textContent;
38
+ let isUpdatingFromUI = false;
39
+
40
+ // --- EXPAND/COLLAPSE LOGIC ---
41
+ let isExpanded = false;
42
+ const checkHeight = () => {
43
+ if (visualView.scrollHeight > 220) {
44
+ visualContainer.classList.add('needs-collapse');
45
+ visualContainer.classList.toggle('is-collapsed', !isExpanded);
46
+ } else {
47
+ visualContainer.classList.remove('needs-collapse', 'is-collapsed');
48
+ }
49
+ };
50
+
51
+ expandBtn.addEventListener('click', () => {
52
+ isExpanded = !isExpanded;
53
+ visualContainer.classList.toggle('is-collapsed', !isExpanded);
54
+ expandBtn.innerText = isExpanded ? '▲ Show Less' : '▼ Show More';
55
+ });
56
+
57
+ // --- TIP TAP SYNC ---
58
+ const updateTipTapState = (newRawYaml) => {
59
+ if (newRawYaml === this.node.textContent) return;
60
+ if (typeof this.getPos === 'function') {
61
+ const tr = this.editor.state.tr;
62
+ const start = this.getPos() + 1;
63
+ const end = start + this.node.nodeSize - 2;
64
+ if (newRawYaml) tr.replaceWith(start, end, this.editor.schema.text(newRawYaml));
65
+ else tr.delete(start, end);
66
+ this.editor.view.dispatch(tr);
67
+ }
68
+ };
69
+
70
+ const castValue = (str) => {
71
+ if (str === 'true') return true;
72
+ if (str === 'false') return false;
73
+ if (str === 'null') return null;
74
+ if (!isNaN(str) && str.trim() !== '') return Number(str);
75
+ return str;
76
+ };
77
+
78
+ const scrapeNode = (container) => {
79
+ if (container.classList.contains('wen-type-object')) {
80
+ const obj = {};
81
+ container.querySelectorAll(':scope > .wen-row-list > .wen-row').forEach(row => {
82
+ // Strictly target the direct key input of THIS row
83
+ const k = row.querySelector(':scope > .wen-row-top > .wen-key-wrap > .wen-key').value.trim();
84
+
85
+ // Strictly check THIS row's top level or THIS row's complex wrapper
86
+ const valContainer = row.querySelector(':scope > .wen-row-top > .wen-val-wrap > div')
87
+ || row.querySelector(':scope > .wen-complex-wrap > div');
88
+
89
+ if (k && valContainer) obj[k] = scrapeNode(valContainer);
90
+ });
91
+ return obj;
92
+ }
93
+ if (container.classList.contains('wen-type-array')) {
94
+ const arr = [];
95
+ container.querySelectorAll(':scope > .wen-array-list > .wen-array-item').forEach(item => {
96
+ // Same strict scoping for array items
97
+ const valContainer = item.querySelector(':scope > .wen-row-top > .wen-val-wrap > div')
98
+ || item.querySelector(':scope > .wen-complex-wrap > div');
99
+
100
+ if (valContainer) arr.push(scrapeNode(valContainer));
101
+ });
102
+ return arr;
103
+ }
104
+ if (container.classList.contains('wen-type-primitive')) {
105
+ return castValue(container.querySelector('.wen-val').value);
106
+ }
107
+ return '';
108
+ };
109
+
110
+ const sync = () => {
111
+ const newData = scrapeNode(visualView.firstElementChild);
112
+ const newYaml = Object.keys(newData).length ? yaml.dump(newData) : '';
113
+ rawInput.value = newYaml;
114
+
115
+ isUpdatingFromUI = true;
116
+ updateTipTapState(newYaml);
117
+ isUpdatingFromUI = false;
118
+
119
+ checkHeight(); // Re-check height if they added rows
120
+ };
121
+
122
+ // --- UI BUILDER ---
123
+ const buildUI = (data) => {
124
+ const createPrimitive = (val) => {
125
+ const wrapper = document.createElement('div');
126
+ wrapper.className = 'wen-type-primitive';
127
+ wrapper.innerHTML = `
128
+ <textarea class="wen-val" rows="1" placeholder="Empty value...">${escapeHtml(val !== null && val !== undefined ? val : '')}</textarea>
129
+ <div class="wen-type-controls">
130
+ <button class="wen-make-obj" title="Add Section">📂</button>
131
+ <button class="wen-make-arr" title="Add List">📋</button>
132
+ </div>
133
+ `;
134
+ const textarea = wrapper.querySelector('.wen-val');
135
+ const resize = () => { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; };
136
+ textarea.addEventListener('input', () => { resize(); sync(); });
137
+ setTimeout(resize, 0);
138
+
139
+ // --- THE FIX: Move the new complex object to the correct line ---
140
+ const handleComplexSwap = (newElement) => {
141
+ // Find the parent row (either a standard row or a list item)
142
+ const row = wrapper.closest('.wen-row') || wrapper.closest('.wen-array-item');
143
+ if (row) {
144
+ row.classList.add('has-complex');
145
+ // Append it to the dedicated block-level container underneath
146
+ row.querySelector('.wen-complex-wrap').appendChild(newElement);
147
+ // Delete the primitive wrapper from the top line
148
+ wrapper.remove();
149
+ } else {
150
+ wrapper.replaceWith(newElement);
151
+ }
152
+ sync();
153
+ };
154
+
155
+ wrapper.querySelector('.wen-make-obj').addEventListener('click', () => handleComplexSwap(createObject({ "": "" })));
156
+ wrapper.querySelector('.wen-make-arr').addEventListener('click', () => handleComplexSwap(createArray([""])));
157
+
158
+ return wrapper;
159
+ };
160
+
161
+ const createArray = (arr) => {
162
+ const wrapper = document.createElement('div');
163
+ wrapper.className = 'wen-type-array';
164
+ const list = document.createElement('div');
165
+ list.className = 'wen-array-list';
166
+
167
+ const addItem = (val) => {
168
+ const isComplex = typeof val === 'object' && val !== null;
169
+ const item = document.createElement('div');
170
+ item.className = `wen-array-item ${isComplex ? 'has-complex' : ''}`;
171
+ item.innerHTML = `
172
+ <div class="wen-row-top">
173
+ <span class="wen-bullet">-</span>
174
+ <div class="wen-val-wrap"></div>
175
+ <button class="wen-btn-delete" title="Remove Item">×</button>
176
+ </div>
177
+ <div class="wen-complex-wrap"></div>
178
+ `;
179
+
180
+ const targetWrap = isComplex ? item.querySelector('.wen-complex-wrap') : item.querySelector('.wen-val-wrap');
181
+ targetWrap.appendChild(isComplex ? (Array.isArray(val) ? createArray(val) : createObject(val)) : createPrimitive(val));
182
+
183
+ item.querySelector('.wen-btn-delete').addEventListener('click', () => { item.remove(); sync(); });
184
+ list.appendChild(item);
185
+ };
186
+
187
+ arr.forEach(addItem);
188
+ const addBtn = document.createElement('button');
189
+ addBtn.className = 'wen-btn-add';
190
+ addBtn.innerText = '+ Add list item';
191
+ addBtn.addEventListener('click', () => { addItem(''); sync(); });
192
+
193
+ wrapper.appendChild(list);
194
+ wrapper.appendChild(addBtn);
195
+ return wrapper;
196
+ };
197
+
198
+ const createObject = (obj) => {
199
+ const wrapper = document.createElement('div');
200
+ wrapper.className = 'wen-type-object';
201
+ const rowList = document.createElement('div');
202
+ rowList.className = 'wen-row-list';
203
+
204
+ const addRow = (k, v) => {
205
+ const isComplex = typeof v === 'object' && v !== null;
206
+ const row = document.createElement('div');
207
+ row.className = `wen-row ${isComplex ? 'has-complex' : ''}`;
208
+ row.innerHTML = `
209
+ <div class="wen-row-top">
210
+ <div class="wen-key-wrap"><input type="text" class="wen-key" value="${escapeHtml(k)}" placeholder="Label..." /><span class="wen-colon">:</span></div>
211
+ <div class="wen-val-wrap"></div>
212
+ <button class="wen-btn-delete" title="Remove Property">×</button>
213
+ </div>
214
+ <div class="wen-complex-wrap"></div>
215
+ `;
216
+
217
+ const targetWrap = isComplex ? row.querySelector('.wen-complex-wrap') : row.querySelector('.wen-val-wrap');
218
+ targetWrap.appendChild(isComplex ? (Array.isArray(v) ? createArray(v) : createObject(v)) : createPrimitive(v));
219
+
220
+ row.querySelector('.wen-key').addEventListener('input', sync);
221
+ row.querySelector('.wen-btn-delete').addEventListener('click', () => { row.remove(); sync(); });
222
+ rowList.appendChild(row);
223
+ };
224
+
225
+ Object.keys(obj).forEach(k => addRow(k, obj[k]));
226
+ const addBtn = document.createElement('button');
227
+ addBtn.className = 'wen-btn-add';
228
+ addBtn.innerText = '+ Add property';
229
+ addBtn.addEventListener('click', () => { addRow('', ''); sync(); });
230
+
231
+ wrapper.appendChild(rowList);
232
+ wrapper.appendChild(addBtn);
233
+ return wrapper;
234
+ };
235
+
236
+ return createObject(data);
237
+ };
238
+
239
+ const renderVisualGrid = () => {
240
+ visualView.innerHTML = '';
241
+ let dataObj = {};
242
+ try {
243
+ dataObj = yaml.load(rawInput.value) || {};
244
+ errorState.classList.remove('active');
245
+ } catch (err) {
246
+ errorMsg.innerText = "YAML Error: " + err.message;
247
+ errorState.classList.add('active');
248
+ return;
249
+ }
250
+ if (typeof dataObj !== 'object' || dataObj === null || Array.isArray(dataObj)) dataObj = { "": dataObj };
251
+ visualView.appendChild(buildUI(dataObj));
252
+ setTimeout(checkHeight, 0); // Check height after render
253
+ };
254
+
255
+ toggleBtn.addEventListener('click', () => {
256
+ const isVisual = toggleBtn.getAttribute('data-state') === 'visual';
257
+ if (isVisual) {
258
+ visualContainer.style.display = 'none';
259
+ rawView.classList.add('active');
260
+ toggleBtn.setAttribute('data-state', 'raw');
261
+ toggleBtn.innerText = 'View UI';
262
+ } else {
263
+ renderVisualGrid();
264
+ if (!errorState.classList.contains('active')) {
265
+ rawView.classList.remove('active');
266
+ visualContainer.style.display = 'block';
267
+ toggleBtn.setAttribute('data-state', 'visual');
268
+ toggleBtn.innerText = 'View Raw';
269
+ }
270
+ }
271
+ });
272
+
273
+ rawInput.addEventListener('input', () => {
274
+ updateTipTapState(rawInput.value);
275
+ });
276
+
277
+ renderVisualGrid();
278
+
279
+ this.update = (updatedNode) => {
280
+ if (updatedNode.type.name !== 'codeBlock' || updatedNode.attrs.language !== 'yaml') return false;
281
+ if (this.node.textContent !== updatedNode.textContent) {
282
+ this.node = updatedNode;
283
+ rawInput.value = this.node.textContent;
284
+ if (!isUpdatingFromUI) {
285
+ renderVisualGrid();
286
+ }
287
+ }
288
+ return true;
289
+ };
290
+
291
+ this.stopEvent = () => true;
292
+ }
293
+ }