@peter.prib/renderengine 0.2.3 → 0.2.5
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.
- package/REFERENCE.md +2231 -2205
- package/dist/renderengine-workspace.umd.js +7965 -7306
- package/dist/renderengine.umd.js +9674 -9211
- package/package.json +1 -1
- package/src/charting/DiagramEditor.js +3330 -3330
- package/src/charting/DiagramTypeRegistry.js +361 -326
- package/src/charting/FormDialog.js +1 -1
- package/src/charting/ModelComparisonDialog.js +122 -0
- package/src/charting/Palette.js +119 -119
- package/src/charting/TemplateCatalog.js +61 -59
- package/src/charting/diagramModel/ComparedKind.js +67 -0
- package/src/charting/diagramModel/DiagramView.js +11 -0
- package/src/charting/diagramModel/MergeModelCommand.js +37 -0
- package/src/charting/diagramModel/ModelComparison.js +100 -0
- package/src/charting/diagramModel/ViewSetModel.js +260 -243
- package/src/charting/diagramModel/ViewsCompared.js +39 -0
- package/src/charting/diagramModel/index.js +10 -6
- package/src/charting/diagrams/ArchimateModel.js +1170 -1155
- package/src/charting/diagrams/ArchimateViewsCompared.js +70 -0
- package/src/charting/diagrams/ConceptsCompared.js +76 -0
- package/src/charting/modelTree/ModelTreePanel.js +886 -914
- package/src/charting/modelTree/viewFolders.js +31 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +345 -345
- package/src/workspace/parts/DiagramEditorPart.js +135 -135
- package/src/workspace/parts/EditorPart.js +26 -7
- package/src/workspace/parts/ModelsPart.js +58 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { MergeModelCommand } from './MergeModelCommand.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ModelComparison — how one model differs from another, kind by kind (the
|
|
5
|
+
* model's `comparedKinds()`: views, and a notation's elements and
|
|
6
|
+
* relationships), matched by id: what only the other has ('added'), what both
|
|
7
|
+
* have but not alike ('changed', with the fields that differ), and what only
|
|
8
|
+
* this one has ('removed'). It can merge (`mergeCommand`): bring what only the
|
|
9
|
+
* other has, and the other's version of what changed, into this model, with
|
|
10
|
+
* whatever those need that this model lacks.
|
|
11
|
+
*/
|
|
12
|
+
export class ModelComparison {
|
|
13
|
+
/**
|
|
14
|
+
* @param {ViewSetModel} mine - The model compared, and merged into
|
|
15
|
+
* @param {ViewSetModel} theirs - The model it is compared with
|
|
16
|
+
*/
|
|
17
|
+
constructor(mine, theirs) {
|
|
18
|
+
this.mine = mine;
|
|
19
|
+
this.theirs = theirs;
|
|
20
|
+
/** The kinds compared, in the order things are brought (what is needed first). */
|
|
21
|
+
this.kinds = mine.comparedKinds();
|
|
22
|
+
this._differences = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every difference: `{key, kind, id, name, change, fields?}`; `change` is
|
|
27
|
+
* 'added' (only theirs), 'changed' (both; `fields` say which differ) or
|
|
28
|
+
* 'removed' (only mine). Kind by kind, in each model's order.
|
|
29
|
+
*/
|
|
30
|
+
differences() {
|
|
31
|
+
if (this._differences) return this._differences;
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const kind of this.kinds) {
|
|
34
|
+
const mine = new Map(kind.items(this.mine).map(item => [kind.idOf(item), item]));
|
|
35
|
+
const theirs = new Map(kind.items(this.theirs).map(item => [kind.idOf(item), item]));
|
|
36
|
+
const entry = (id, item, change, extra = {}) => ({ key: `${kind.key}:${id}`, kind, id, name: kind.nameOf(item), change, ...extra });
|
|
37
|
+
for (const [id, item] of theirs) {
|
|
38
|
+
if (!mine.has(id)) { out.push(entry(id, item, 'added')); continue; }
|
|
39
|
+
const fields = kind.changedFields(mine.get(id), item);
|
|
40
|
+
if (fields.length) out.push(entry(id, item, 'changed', { fields }));
|
|
41
|
+
}
|
|
42
|
+
for (const [id, item] of mine) if (!theirs.has(id)) out.push(entry(id, item, 'removed'));
|
|
43
|
+
}
|
|
44
|
+
return (this._differences = out);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Whether the two models are alike in everything compared. */
|
|
48
|
+
get same() {
|
|
49
|
+
return !this.differences().length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How many differences of each change there are, kind by kind: [{kind, added, changed, removed}]. */
|
|
53
|
+
summary() {
|
|
54
|
+
return this.kinds.map(kind => {
|
|
55
|
+
const of = this.differences().filter(d => d.kind === kind);
|
|
56
|
+
const count = (change) => of.filter(d => d.change === change).length;
|
|
57
|
+
return { kind, added: count('added'), changed: count('changed'), removed: count('removed') };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The differences a merge can bring: what only theirs has, and what changed. */
|
|
62
|
+
mergeable() {
|
|
63
|
+
return this.differences().filter(d => d.change !== 'removed');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bring some differences (by key; default every mergeable one) into this
|
|
68
|
+
* model, with what they need that it lacks (a view's elements, a
|
|
69
|
+
* relationship's ends): a MergeModelCommand, not yet run, or null when
|
|
70
|
+
* there is nothing to bring.
|
|
71
|
+
* @param {Iterable<string>} [keys]
|
|
72
|
+
* @returns {MergeModelCommand|null}
|
|
73
|
+
*/
|
|
74
|
+
mergeCommand(keys = this.mergeable().map(d => d.key)) {
|
|
75
|
+
const byKey = new Map(this.mergeable().map(d => [d.key, d]));
|
|
76
|
+
const kindOf = new Map(this.kinds.map(kind => [kind.key, kind]));
|
|
77
|
+
const chosen = new Set();
|
|
78
|
+
const take = (kind, id) => {
|
|
79
|
+
const key = `${kind.key}:${id}`;
|
|
80
|
+
if (chosen.has(key)) return;
|
|
81
|
+
// Something needed is brought when only theirs has it; what both have stays as chosen.
|
|
82
|
+
if (!byKey.has(key)) return;
|
|
83
|
+
chosen.add(key);
|
|
84
|
+
const item = kind.get(this.theirs, id);
|
|
85
|
+
for (const [needKind, needId] of kind.requires(item, this.theirs)) {
|
|
86
|
+
const need = kindOf.get(needKind);
|
|
87
|
+
if (need && !need.get(this.mine, needId)) take(need, needId);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const d = byKey.get(key);
|
|
92
|
+
if (d) take(d.kind, d.id);
|
|
93
|
+
}
|
|
94
|
+
if (!chosen.size) return null;
|
|
95
|
+
const steps = this.kinds.flatMap(kind => this.mergeable()
|
|
96
|
+
.filter(d => d.kind === kind && chosen.has(d.key))
|
|
97
|
+
.map(d => ({ kind, item: kind.get(this.theirs, d.id) })));
|
|
98
|
+
return new MergeModelCommand(this.mine, this.theirs, steps);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,243 +1,260 @@
|
|
|
1
|
-
import { ViewSetChecker } from './ViewSetChecker.js';
|
|
2
|
-
import { ModelReport } from './ModelReport.js';
|
|
3
|
-
import { ModelSearch } from './ModelSearch.js';
|
|
4
|
-
import { ModelHistory } from './ModelHistory.js';
|
|
5
|
-
import { AddViewCommand } from './AddViewCommand.js';
|
|
6
|
-
import { RemoveViewCommand } from './RemoveViewCommand.js';
|
|
7
|
-
import { RenameViewCommand } from './RenameViewCommand.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* @param {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
history
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
view
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.history.execute(new
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this._views.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
1
|
+
import { ViewSetChecker } from './ViewSetChecker.js';
|
|
2
|
+
import { ModelReport } from './ModelReport.js';
|
|
3
|
+
import { ModelSearch } from './ModelSearch.js';
|
|
4
|
+
import { ModelHistory } from './ModelHistory.js';
|
|
5
|
+
import { AddViewCommand } from './AddViewCommand.js';
|
|
6
|
+
import { RemoveViewCommand } from './RemoveViewCommand.js';
|
|
7
|
+
import { RenameViewCommand } from './RenameViewCommand.js';
|
|
8
|
+
import { ModelComparison } from './ModelComparison.js';
|
|
9
|
+
import { ViewsCompared } from './ViewsCompared.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ViewSetModel — a model edited as several views (diagrams), with one current.
|
|
13
|
+
* Owns what every such model does with its views: create, duplicate, rename,
|
|
14
|
+
* delete, switch, and telling listeners after each change, so a UI only has to
|
|
15
|
+
* re-render on notification. It has one undo history (`history`, a
|
|
16
|
+
* ModelHistory, as Archi has one command stack per model) that every view of
|
|
17
|
+
* it shares: adding, renaming and deleting views are undoable steps on it too.
|
|
18
|
+
*
|
|
19
|
+
* Subclasses make their own kind of view through `_newView(id, title)`; a view
|
|
20
|
+
* has an `id`, a `title`, and `copy(id, title)` for duplication. DiagramModel
|
|
21
|
+
* (the template editor) and ArchimateModel extend it.
|
|
22
|
+
*/
|
|
23
|
+
export class ViewSetModel {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} [options]
|
|
26
|
+
* @param {string} [options.idPrefix] - Prefix for generated view ids
|
|
27
|
+
* @param {string} [options.viewNoun] - What a view is called in default titles ('Diagram', 'View')
|
|
28
|
+
*/
|
|
29
|
+
constructor({ idPrefix = 'view', viewNoun = 'View' } = {}) {
|
|
30
|
+
if (new.target === ViewSetModel) throw new TypeError('ViewSetModel is abstract — subclass it');
|
|
31
|
+
/** @type {Map<string, Object>} in tab order */
|
|
32
|
+
this._views = new Map();
|
|
33
|
+
this.currentViewId = null;
|
|
34
|
+
this.idPrefix = idPrefix;
|
|
35
|
+
this.viewNoun = viewNoun;
|
|
36
|
+
this._nextId = 1;
|
|
37
|
+
this._listeners = [];
|
|
38
|
+
/** Bumped by every change to the set of views (new, duplicated, renamed, deleted). */
|
|
39
|
+
this._structure = 0;
|
|
40
|
+
this._saved = null;
|
|
41
|
+
this._editListeners = [];
|
|
42
|
+
this._watched = new WeakSet();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The model's one undo history, which every view of it shares (ModelHistory). */
|
|
46
|
+
get history() {
|
|
47
|
+
return (this._history ??= new ModelHistory(this, this.historyOptions ?? {}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Make a view one of this model's: its edits go on the model's history. @protected */
|
|
51
|
+
_adopt(view) {
|
|
52
|
+
view.history = this.history;
|
|
53
|
+
return view;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===== Saved or not =====
|
|
57
|
+
|
|
58
|
+
/** Remember this state as the saved one (after opening or saving a file). */
|
|
59
|
+
markSaved() {
|
|
60
|
+
this._saved = { structure: this._structure, token: this.history.stateToken };
|
|
61
|
+
this._emitEdit();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether anything changed since markSaved: an edit on the model's history
|
|
66
|
+
* (any view's, or to the views themselves) not undone back again.
|
|
67
|
+
*/
|
|
68
|
+
isDirty() {
|
|
69
|
+
const saved = this._saved ?? { structure: 0, token: 0 };
|
|
70
|
+
return this._structure !== saved.structure || this.history.stateToken !== saved.token;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Call `fn(model)` after every edit (any view's history, or the set of views). Returns an unsubscribe function. */
|
|
74
|
+
onEdit(fn) {
|
|
75
|
+
this._editListeners.push(fn);
|
|
76
|
+
this._watchHistories();
|
|
77
|
+
return () => { this._editListeners = this._editListeners.filter(l => l !== fn); };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Listen to the model's history (and any view's own, for one not adopted). @private */
|
|
81
|
+
_watchHistories() {
|
|
82
|
+
for (const history of [this.history, ...this.views().map(v => v.history)]) {
|
|
83
|
+
if (!history?.onChange || this._watched.has(history)) continue;
|
|
84
|
+
this._watched.add(history);
|
|
85
|
+
history.onChange(() => this._emitEdit());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @private */
|
|
90
|
+
_emitEdit() {
|
|
91
|
+
for (const fn of this._editListeners) fn(this);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A new, empty view of this model's kind. @protected */
|
|
95
|
+
_newView(id, title) {
|
|
96
|
+
throw new Error(`${this.constructor.name} must implement _newView()`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Subscribe to changes; returns an unsubscribe function. */
|
|
100
|
+
onChange(fn) {
|
|
101
|
+
this._listeners.push(fn);
|
|
102
|
+
return () => { this._listeners = this._listeners.filter(l => l !== fn); };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** All views, in order. */
|
|
106
|
+
views() {
|
|
107
|
+
return [...this._views.values()];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get viewCount() {
|
|
111
|
+
return this._views.size;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get currentView() {
|
|
115
|
+
return this._views.get(this.currentViewId) ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getView(id) {
|
|
119
|
+
return this._views.get(id) ?? null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Title offered for the next new view. */
|
|
123
|
+
nextViewTitle() {
|
|
124
|
+
return `${this.viewNoun} ${this._views.size + 1}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Add an empty view and make it current. */
|
|
128
|
+
createView(title = this.nextViewTitle()) {
|
|
129
|
+
return this._add(this._newView(this._newId(), title));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Add an independent copy of a view and make it current. */
|
|
133
|
+
duplicateView(id) {
|
|
134
|
+
const original = this._views.get(id);
|
|
135
|
+
if (!original) return null;
|
|
136
|
+
return this._add(original.copy(this._newId(), `${original.title} (copy)`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Give a view a new title (not an empty one). One undo step. */
|
|
140
|
+
renameView(id, title) {
|
|
141
|
+
const view = this._views.get(id);
|
|
142
|
+
if (!view || !title || title === view.title) return false;
|
|
143
|
+
this.history.execute(new RenameViewCommand(this, view, title));
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A view's title set, as RenameViewCommand and replace do it (not undoable by itself). @protected */
|
|
148
|
+
_retitle(id, title) {
|
|
149
|
+
const view = this._views.get(id);
|
|
150
|
+
if (!view) return;
|
|
151
|
+
view.title = title;
|
|
152
|
+
this._notify();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The checks this model is held to (ViewSetChecker; a notation's model returns its own). */
|
|
156
|
+
checker() {
|
|
157
|
+
return new ViewSetChecker(this);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Find and replace over the model's texts (ModelSearch; a notation's model returns its own). */
|
|
161
|
+
search() {
|
|
162
|
+
return new ModelSearch(this);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The model's report (ModelReport; a notation's model returns its own), for reportHtml. */
|
|
166
|
+
report(options) {
|
|
167
|
+
return new ModelReport(this, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What two such models are compared on and merged by (ComparedKind), what is needed first: their views here. */
|
|
171
|
+
comparedKinds() {
|
|
172
|
+
return [new ViewsCompared()];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** How this model differs from another (ModelComparison), which can merge the other's into it. */
|
|
176
|
+
compareWith(other) {
|
|
177
|
+
return new ModelComparison(this, other);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A copy of another model's view, as one of this model's, under the same id (a merge brings it). @protected */
|
|
181
|
+
_viewFrom(view) {
|
|
182
|
+
return this._adopt(view.copy(view.id, view.title));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Whether a view may be deleted: the last one stays. */
|
|
186
|
+
canDeleteView(id) {
|
|
187
|
+
return this._views.has(id) && this._views.size > 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Remove a view; the last one cannot be removed. The neighbour becomes current. One undo step. */
|
|
191
|
+
deleteView(id) {
|
|
192
|
+
if (!this.canDeleteView(id)) return false;
|
|
193
|
+
this.history.execute(new RemoveViewCommand(this, this._views.get(id)));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
switchTo(id) {
|
|
198
|
+
if (!this._views.has(id) || id === this.currentViewId) return false;
|
|
199
|
+
this.currentViewId = id;
|
|
200
|
+
this._notify();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Add a view (last), shown unless `current` is false: one undo step on the
|
|
206
|
+
* model's history (AddViewCommand).
|
|
207
|
+
* @protected
|
|
208
|
+
*/
|
|
209
|
+
_add(view, { current = true } = {}) {
|
|
210
|
+
this.history.execute(new AddViewCommand(this, this._adopt(view), { current }));
|
|
211
|
+
return view;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Put a view in the model at `index` (default last), current if asked; as
|
|
216
|
+
* AddViewCommand and RemoveViewCommand's undo do it.
|
|
217
|
+
* @protected
|
|
218
|
+
*/
|
|
219
|
+
_insertView(view, { index = this._views.size, current = true } = {}) {
|
|
220
|
+
const entries = [...this._views].filter(([id]) => id !== view.id);
|
|
221
|
+
entries.splice(Math.min(index, entries.length), 0, [view.id, view]);
|
|
222
|
+
this._views = new Map(entries);
|
|
223
|
+
if (current || !this.currentViewId) this.currentViewId = view.id;
|
|
224
|
+
this._notify();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Take a view out of the model; the one in front becomes `next` (if given
|
|
229
|
+
* and still there) or, when it was this one, its neighbour.
|
|
230
|
+
* @returns {{index: number, current: boolean}} where it was, and whether it was in front
|
|
231
|
+
* @protected
|
|
232
|
+
*/
|
|
233
|
+
_removeView(id, { next = null } = {}) {
|
|
234
|
+
const ids = [...this._views.keys()];
|
|
235
|
+
const index = ids.indexOf(id);
|
|
236
|
+
const current = this.currentViewId === id;
|
|
237
|
+
this._views.delete(id);
|
|
238
|
+
if (next && this._views.has(next)) this.currentViewId = next;
|
|
239
|
+
else if (current) this.currentViewId = ids[index + 1] ?? ids[index - 1] ?? null;
|
|
240
|
+
this._notify();
|
|
241
|
+
return { index, current };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_newId() {
|
|
245
|
+
let id;
|
|
246
|
+
do { id = `${this.idPrefix}-${this._nextId++}`; } while (this._views.has(id));
|
|
247
|
+
return id;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_notify() {
|
|
251
|
+
for (const fn of this._listeners) fn(this);
|
|
252
|
+
this._watchHistories();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** A change to the set of views: the model is no longer as saved. @protected */
|
|
256
|
+
_changedStructure() {
|
|
257
|
+
this._structure++;
|
|
258
|
+
this._emitEdit();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ComparedKind } from './ComparedKind.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ViewsCompared — the views of two models (any ViewSetModel): a view's
|
|
5
|
+
* content is its JSON (`toJSON`); bringing one copies it into the model
|
|
6
|
+
* (`ViewSetModel#_viewFrom`) under its own id, in place of the view with that
|
|
7
|
+
* id, else last.
|
|
8
|
+
*/
|
|
9
|
+
export class ViewsCompared extends ComparedKind {
|
|
10
|
+
get key() { return 'view'; }
|
|
11
|
+
get label() { return 'Views'; }
|
|
12
|
+
|
|
13
|
+
items(model) {
|
|
14
|
+
return model.views();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(model, id) {
|
|
18
|
+
return model.getView(id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
content(view) {
|
|
22
|
+
return view.toJSON();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
bring(model, view, from) {
|
|
26
|
+
const twin = model._viewFrom(view);
|
|
27
|
+
const old = model.getView(twin.id);
|
|
28
|
+
if (!old) {
|
|
29
|
+
model._insertView(twin, { current: false });
|
|
30
|
+
return () => model._removeView(twin.id);
|
|
31
|
+
}
|
|
32
|
+
const { index, current } = model._removeView(old.id);
|
|
33
|
+
model._insertView(twin, { index, current });
|
|
34
|
+
return () => {
|
|
35
|
+
model._removeView(twin.id);
|
|
36
|
+
model._insertView(old, { index, current });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|