@kerebron/editor-kits 0.4.3 → 0.4.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/esm/editor-kits/src/StarterKit.d.ts +1 -4
- package/esm/editor-kits/src/StarterKit.d.ts.map +1 -1
- package/esm/extension-lsp/src/LspStatus.d.ts +34 -0
- package/esm/extension-lsp/src/LspStatus.d.ts.map +1 -0
- package/esm/extension-lsp/src/LspStatus.js +127 -0
- package/esm/extension-menu/src/CustomMenuPlugin.d.ts +7 -3
- package/esm/extension-menu/src/CustomMenuPlugin.d.ts.map +1 -1
- package/esm/extension-menu/src/CustomMenuPlugin.js +60 -4
- package/esm/extension-menu/src/ExtensionCustomMenu.d.ts +5 -2
- package/esm/extension-menu/src/ExtensionCustomMenu.d.ts.map +1 -1
- package/esm/extension-menu/src/ExtensionCustomMenu.js +35 -1
- package/esm/extension-yjs/src/CollaborationStatus.d.ts +59 -0
- package/esm/extension-yjs/src/CollaborationStatus.d.ts.map +1 -0
- package/esm/extension-yjs/src/CollaborationStatus.js +286 -0
- package/package.json +1 -1
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import "../../_dnt.polyfills.js";
|
|
2
2
|
import { Extension } from '../../editor/src/mod.js';
|
|
3
|
-
import { ExtensionBasicEditor } from '../../extension-basic-editor/src/ExtensionBasicEditor.js';
|
|
4
|
-
import { ExtensionMarkdown } from '../../extension-markdown/src/ExtensionMarkdown.js';
|
|
5
|
-
import { ExtensionCustomMenu } from '../../extension-menu/src/mod.js';
|
|
6
3
|
/**
|
|
7
4
|
* StarterKit is a collection of commonly used extensions that provide
|
|
8
5
|
* basic functionality like paragraphs, headings, bold, italic, and more.
|
|
@@ -26,7 +23,7 @@ import { ExtensionCustomMenu } from '../../extension-menu/src/mod.js';
|
|
|
26
23
|
*/
|
|
27
24
|
export declare class StarterKit extends Extension {
|
|
28
25
|
name: string;
|
|
29
|
-
requires:
|
|
26
|
+
requires: any[];
|
|
30
27
|
}
|
|
31
28
|
declare const _default: StarterKit;
|
|
32
29
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StarterKit.d.ts","sourceRoot":"","sources":["../../../src/editor-kits/src/StarterKit.ts"],"names":[],"mappings":"AAAA,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"StarterKit.d.ts","sourceRoot":"","sources":["../../../src/editor-kits/src/StarterKit.ts"],"names":[],"mappings":"AAAA,OAAO,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAKpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,UAAW,SAAQ,SAAS;IAC9B,IAAI,SAAiB;IAC9B,QAAQ,QAIN;CACH;;AAGD,wBAAgC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EditorState } from 'prosemirror-state';
|
|
2
|
+
import { EditorView } from 'prosemirror-view';
|
|
3
|
+
import type { MenuElement } from '../../extension-menu/src/mod.js';
|
|
4
|
+
import type { ExtensionLsp } from './ExtensionLsp.js';
|
|
5
|
+
export type LspConnectionStatus = 'connected' | 'connecting' | 'disconnected';
|
|
6
|
+
export interface LspStatusOptions {
|
|
7
|
+
/** The LSP extension instance to monitor */
|
|
8
|
+
lspExtension: ExtensionLsp;
|
|
9
|
+
/** Optional label to show (e.g., 'LSP' or language name) */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A menu element that displays LSP connection status.
|
|
14
|
+
* Shows a status indicator dot with an optional label.
|
|
15
|
+
*/
|
|
16
|
+
export declare class LspStatusElement implements MenuElement {
|
|
17
|
+
private lspExtension;
|
|
18
|
+
private label;
|
|
19
|
+
private dom;
|
|
20
|
+
private statusDot;
|
|
21
|
+
private labelEl;
|
|
22
|
+
private status;
|
|
23
|
+
private statusCheckInterval;
|
|
24
|
+
constructor(options: LspStatusOptions);
|
|
25
|
+
render(_view: EditorView): {
|
|
26
|
+
dom: HTMLElement;
|
|
27
|
+
update: (state: EditorState) => boolean;
|
|
28
|
+
};
|
|
29
|
+
private getClient;
|
|
30
|
+
private updateStatusFromClient;
|
|
31
|
+
private setStatus;
|
|
32
|
+
destroy(): void;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=LspStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LspStatus.d.ts","sourceRoot":"","sources":["../../../src/extension-lsp/src/LspStatus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,YAAY,GAAG,cAAc,CAAC;AAE9E,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,YAAY,EAAE,YAAY,CAAC;IAC3B,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,WAAW;IAClD,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,mBAAmB,CAA+C;gBAE9D,OAAO,EAAE,gBAAgB;IAKrC,MAAM,CACJ,KAAK,EAAE,UAAU,GAChB;QAAE,GAAG,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAA;KAAE;IAyChE,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,SAAS;IAoBjB,OAAO;CAMR"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const CSS_PREFIX = 'kb-lsp-status';
|
|
2
|
+
/**
|
|
3
|
+
* A menu element that displays LSP connection status.
|
|
4
|
+
* Shows a status indicator dot with an optional label.
|
|
5
|
+
*/
|
|
6
|
+
export class LspStatusElement {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
Object.defineProperty(this, "lspExtension", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "label", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "dom", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: null
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "statusDot", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: null
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "labelEl", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: null
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "status", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: 'disconnected'
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "statusCheckInterval", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: null
|
|
49
|
+
});
|
|
50
|
+
this.lspExtension = options.lspExtension;
|
|
51
|
+
this.label = options.label ?? 'LSP';
|
|
52
|
+
}
|
|
53
|
+
render(_view) {
|
|
54
|
+
// Create main container
|
|
55
|
+
this.dom = document.createElement('div');
|
|
56
|
+
this.dom.className = CSS_PREFIX;
|
|
57
|
+
this.dom.setAttribute('role', 'status');
|
|
58
|
+
this.dom.setAttribute('aria-live', 'polite');
|
|
59
|
+
// Create status indicator
|
|
60
|
+
const indicator = document.createElement('div');
|
|
61
|
+
indicator.className = `${CSS_PREFIX}__indicator`;
|
|
62
|
+
// Status dot
|
|
63
|
+
this.statusDot = document.createElement('span');
|
|
64
|
+
this.statusDot.className =
|
|
65
|
+
`${CSS_PREFIX}__dot ${CSS_PREFIX}__dot--disconnected`;
|
|
66
|
+
indicator.appendChild(this.statusDot);
|
|
67
|
+
// Label
|
|
68
|
+
this.labelEl = document.createElement('span');
|
|
69
|
+
this.labelEl.className = `${CSS_PREFIX}__label`;
|
|
70
|
+
this.labelEl.textContent = this.label;
|
|
71
|
+
indicator.appendChild(this.labelEl);
|
|
72
|
+
this.dom.appendChild(indicator);
|
|
73
|
+
// Set initial status based on client state
|
|
74
|
+
this.updateStatusFromClient();
|
|
75
|
+
// Poll for status changes since the client is created lazily
|
|
76
|
+
this.statusCheckInterval = setInterval(() => {
|
|
77
|
+
this.updateStatusFromClient();
|
|
78
|
+
}, 1000);
|
|
79
|
+
const update = (_state) => {
|
|
80
|
+
// Always visible
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
return { dom: this.dom, update };
|
|
84
|
+
}
|
|
85
|
+
getClient() {
|
|
86
|
+
const mainLang = this.lspExtension.mainLang || 'markdown';
|
|
87
|
+
return this.lspExtension.getClient(mainLang);
|
|
88
|
+
}
|
|
89
|
+
updateStatusFromClient() {
|
|
90
|
+
const client = this.getClient();
|
|
91
|
+
// Check if the client has server capabilities (meaning it's connected and initialized)
|
|
92
|
+
if (client?.serverCapabilities) {
|
|
93
|
+
this.setStatus('connected');
|
|
94
|
+
}
|
|
95
|
+
else if (client?.active) {
|
|
96
|
+
this.setStatus('connecting');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.setStatus('disconnected');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
setStatus(status) {
|
|
103
|
+
if (this.status === status)
|
|
104
|
+
return; // No change
|
|
105
|
+
this.status = status;
|
|
106
|
+
if (this.statusDot) {
|
|
107
|
+
this.statusDot.className =
|
|
108
|
+
`${CSS_PREFIX}__dot ${CSS_PREFIX}__dot--${status}`;
|
|
109
|
+
}
|
|
110
|
+
if (this.dom) {
|
|
111
|
+
this.dom.setAttribute('data-status', status);
|
|
112
|
+
const statusLabels = {
|
|
113
|
+
connected: 'LSP Connected',
|
|
114
|
+
connecting: 'LSP Connecting...',
|
|
115
|
+
disconnected: 'LSP Disconnected',
|
|
116
|
+
};
|
|
117
|
+
this.dom.title = statusLabels[status];
|
|
118
|
+
this.dom.setAttribute('aria-label', statusLabels[status]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
destroy() {
|
|
122
|
+
if (this.statusCheckInterval) {
|
|
123
|
+
clearInterval(this.statusCheckInterval);
|
|
124
|
+
this.statusCheckInterval = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EditorState, Plugin } from 'prosemirror-state';
|
|
2
2
|
import { EditorView } from 'prosemirror-view';
|
|
3
3
|
import type { CoreEditor } from '../../editor/src/mod.js';
|
|
4
|
-
import { CustomMenuOptions } from './ExtensionCustomMenu.js';
|
|
5
4
|
import type { MenuElement } from './menu.js';
|
|
6
5
|
interface ToolItem {
|
|
7
6
|
id: string;
|
|
@@ -19,6 +18,7 @@ export declare class CustomMenuView {
|
|
|
19
18
|
pinnedDropdownMenu: HTMLElement | null;
|
|
20
19
|
modal: HTMLElement | null;
|
|
21
20
|
tools: ToolItem[];
|
|
21
|
+
trailingElements: MenuElement[];
|
|
22
22
|
root: Document | ShadowRoot;
|
|
23
23
|
resizeHandle: HTMLElement;
|
|
24
24
|
editorContainer: HTMLElement;
|
|
@@ -26,7 +26,7 @@ export declare class CustomMenuView {
|
|
|
26
26
|
private closePinnedDropdownHandler;
|
|
27
27
|
private submenuStack;
|
|
28
28
|
private pinnedDropdownStack;
|
|
29
|
-
constructor(editorView: EditorView, editor: CoreEditor, content: readonly (readonly MenuElement[])[]);
|
|
29
|
+
constructor(editorView: EditorView, editor: CoreEditor, content: readonly (readonly MenuElement[])[], trailingElements?: readonly MenuElement[]);
|
|
30
30
|
private initializeTools;
|
|
31
31
|
/**
|
|
32
32
|
* Generate a stable ID from a label by converting it to a kebab-case slug.
|
|
@@ -54,8 +54,12 @@ export declare class CustomMenuView {
|
|
|
54
54
|
update(view: EditorView, prevState: EditorState): void;
|
|
55
55
|
destroy(): void;
|
|
56
56
|
}
|
|
57
|
+
export interface CustomMenuPluginOptions {
|
|
58
|
+
content: readonly (readonly MenuElement[])[];
|
|
59
|
+
trailingElements?: readonly MenuElement[];
|
|
60
|
+
}
|
|
57
61
|
export declare class CustomMenuPlugin extends Plugin {
|
|
58
|
-
constructor(editor: CoreEditor, options:
|
|
62
|
+
constructor(editor: CoreEditor, options: CustomMenuPluginOptions);
|
|
59
63
|
}
|
|
60
64
|
export {};
|
|
61
65
|
//# sourceMappingURL=CustomMenuPlugin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CustomMenuPlugin.d.ts","sourceRoot":"","sources":["../../../src/extension-menu/src/CustomMenuPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"CustomMenuPlugin.d.ts","sourceRoot":"","sources":["../../../src/extension-menu/src/CustomMenuPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAI1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuB7C,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,cAAc;IAmBvB,QAAQ,CAAC,UAAU,EAAE,UAAU;IAC/B,QAAQ,CAAC,MAAM,EAAE,UAAU;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE;IApBvD,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,WAAW,CAAC;IACrB,YAAY,EAAE,WAAW,CAAC;IAC1B,kBAAkB,EAAE,WAAW,GAAG,IAAI,CAAQ;IAC9C,KAAK,EAAE,WAAW,GAAG,IAAI,CAAQ;IACjC,KAAK,EAAE,QAAQ,EAAE,CAAM;IACvB,gBAAgB,EAAE,WAAW,EAAE,CAAM;IACrC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,YAAY,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,WAAW,CAAC;IAC7B,OAAO,CAAC,oBAAoB,CAA0C;IACtE,OAAO,CAAC,0BAA0B,CAA0C;IAC5E,OAAO,CAAC,YAAY,CAAmD;IACvE,OAAO,CAAC,mBAAmB,CAEpB;gBAGI,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,SAAS,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,EACrD,gBAAgB,CAAC,EAAE,SAAS,WAAW,EAAE;IAmE3C,OAAO,CAAC,eAAe;IAgCvB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyBxB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,eAAe;IAsCvB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,WAAW;IAqCnB,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,kBAAkB;IA0C1B,OAAO,CAAC,oBAAoB;IAsB5B,OAAO,CAAC,yBAAyB;IAuCjC,OAAO,CAAC,oBAAoB;IA8L5B,OAAO,CAAC,kBAAkB;IA6R1B,OAAO,CAAC,MAAM;IAuMd,OAAO,CAAC,eAAe;IAyHvB,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,WAAW;IAgDnB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW;IAQ/C,OAAO;CA8BR;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,SAAS,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,CAAC;IAC7C,gBAAgB,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CAC3C;AAED,qBAAa,gBAAiB,SAAQ,MAAM;gBAC9B,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,uBAAuB;CAYjE"}
|
|
@@ -20,7 +20,7 @@ function debug(...args) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
export class CustomMenuView {
|
|
23
|
-
constructor(editorView, editor, content) {
|
|
23
|
+
constructor(editorView, editor, content, trailingElements) {
|
|
24
24
|
Object.defineProperty(this, "editorView", {
|
|
25
25
|
enumerable: true,
|
|
26
26
|
configurable: true,
|
|
@@ -75,6 +75,12 @@ export class CustomMenuView {
|
|
|
75
75
|
writable: true,
|
|
76
76
|
value: []
|
|
77
77
|
});
|
|
78
|
+
Object.defineProperty(this, "trailingElements", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
value: []
|
|
83
|
+
});
|
|
78
84
|
Object.defineProperty(this, "root", {
|
|
79
85
|
enumerable: true,
|
|
80
86
|
configurable: true,
|
|
@@ -120,6 +126,8 @@ export class CustomMenuView {
|
|
|
120
126
|
debug('CustomMenuView constructor called');
|
|
121
127
|
debug('Content groups:', content.length);
|
|
122
128
|
this.root = editorView.root;
|
|
129
|
+
// Store trailing elements
|
|
130
|
+
this.trailingElements = trailingElements ? [...trailingElements] : [];
|
|
123
131
|
// Create wrapper
|
|
124
132
|
this.wrapper = document.createElement('div');
|
|
125
133
|
this.wrapper.classList.add(CSS_PREFIX + '__wrapper');
|
|
@@ -434,6 +442,37 @@ export class CustomMenuView {
|
|
|
434
442
|
// Create dropdown menu
|
|
435
443
|
this.pinnedDropdownMenu = document.createElement('div');
|
|
436
444
|
this.pinnedDropdownMenu.classList.add(CSS_PREFIX + '__pinned-dropdown');
|
|
445
|
+
// Inherit theme CSS variables from editor container
|
|
446
|
+
// This ensures dropdowns match the editor's theme even when appended to body
|
|
447
|
+
const editorContainer = this.editorView.dom.closest('.kb-component') ||
|
|
448
|
+
this.editorView.dom.parentElement;
|
|
449
|
+
if (editorContainer) {
|
|
450
|
+
const computedStyle = getComputedStyle(editorContainer);
|
|
451
|
+
const themeVars = [
|
|
452
|
+
// New unified naming
|
|
453
|
+
'--kb-menu-dropdown-bg',
|
|
454
|
+
'--kb-menu-dropdown-border',
|
|
455
|
+
'--kb-menu-dropdown-text',
|
|
456
|
+
// Legacy naming (for backwards compatibility)
|
|
457
|
+
'--kb-menu-dropdown-dark-bg',
|
|
458
|
+
'--kb-menu-dropdown-dark-border',
|
|
459
|
+
'--kb-menu-dropdown-dark-text',
|
|
460
|
+
// General theme colors
|
|
461
|
+
'--kb-color-surface',
|
|
462
|
+
'--kb-color-surface-elevated',
|
|
463
|
+
'--kb-color-surface-hover',
|
|
464
|
+
'--kb-color-border',
|
|
465
|
+
'--kb-color-border-strong',
|
|
466
|
+
'--kb-color-text',
|
|
467
|
+
'--kb-color-text-muted',
|
|
468
|
+
];
|
|
469
|
+
themeVars.forEach((varName) => {
|
|
470
|
+
const value = computedStyle.getPropertyValue(varName).trim();
|
|
471
|
+
if (value) {
|
|
472
|
+
this.pinnedDropdownMenu.style.setProperty(varName, value);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
437
476
|
// Position it below the trigger element
|
|
438
477
|
const rect = triggerElement.getBoundingClientRect();
|
|
439
478
|
this.pinnedDropdownMenu.style.position = 'absolute';
|
|
@@ -933,10 +972,27 @@ export class CustomMenuView {
|
|
|
933
972
|
});
|
|
934
973
|
this.toolbar.appendChild(overflowToggle);
|
|
935
974
|
}
|
|
975
|
+
// Render trailing elements (always visible, not pinnable)
|
|
976
|
+
if (this.trailingElements.length > 0) {
|
|
977
|
+
// Add separator before trailing elements
|
|
978
|
+
const separator = document.createElement('div');
|
|
979
|
+
separator.classList.add(CSS_PREFIX + '__separator');
|
|
980
|
+
separator.classList.add(CSS_PREFIX + '__separator--trailing');
|
|
981
|
+
this.toolbar.appendChild(separator);
|
|
982
|
+
// Create a trailing group container
|
|
983
|
+
const trailingGroup = document.createElement('div');
|
|
984
|
+
trailingGroup.classList.add(CSS_PREFIX + '__trailing-group');
|
|
985
|
+
this.trailingElements.forEach((element) => {
|
|
986
|
+
const { dom, update } = element.render(this.editorView);
|
|
987
|
+
const wrapper = document.createElement('span');
|
|
988
|
+
wrapper.classList.add(CSS_PREFIX + '__trailing-item');
|
|
989
|
+
wrapper.appendChild(dom);
|
|
990
|
+
trailingGroup.appendChild(wrapper);
|
|
991
|
+
});
|
|
992
|
+
this.toolbar.appendChild(trailingGroup);
|
|
993
|
+
}
|
|
936
994
|
// Render overflow menu content
|
|
937
995
|
this.renderOverflowMenu();
|
|
938
|
-
console.log('[kb-custom-menu] render() complete. Toolbar children:', this.toolbar.children.length);
|
|
939
|
-
console.log('[kb-custom-menu] Toolbar HTML:', this.toolbar.innerHTML.substring(0, 200));
|
|
940
996
|
}
|
|
941
997
|
openManageModal() {
|
|
942
998
|
// Close overflow menu
|
|
@@ -1123,7 +1179,7 @@ export class CustomMenuPlugin extends Plugin {
|
|
|
1123
1179
|
constructor(editor, options) {
|
|
1124
1180
|
super({
|
|
1125
1181
|
view(editorView) {
|
|
1126
|
-
return new CustomMenuView(editorView, editor, options.content);
|
|
1182
|
+
return new CustomMenuView(editorView, editor, options.content, options.trailingElements);
|
|
1127
1183
|
},
|
|
1128
1184
|
});
|
|
1129
1185
|
}
|
|
@@ -2,9 +2,12 @@ import { Plugin } from 'prosemirror-state';
|
|
|
2
2
|
import { Extension } from '../../editor/src/mod.js';
|
|
3
3
|
import { type MenuElement } from './menu.js';
|
|
4
4
|
export interface CustomMenuOptions {
|
|
5
|
-
content
|
|
5
|
+
content?: readonly (readonly MenuElement[])[];
|
|
6
|
+
trailingElements?: readonly MenuElement[];
|
|
7
|
+
autoAddCollaborationStatus?: boolean;
|
|
8
|
+
autoAddLspStatus?: boolean;
|
|
6
9
|
}
|
|
7
|
-
export declare class ExtensionCustomMenu extends Extension {
|
|
10
|
+
export declare class ExtensionCustomMenu extends Extension<CustomMenuOptions> {
|
|
8
11
|
name: string;
|
|
9
12
|
getProseMirrorPlugins(): Plugin[];
|
|
10
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExtensionCustomMenu.d.ts","sourceRoot":"","sources":["../../../src/extension-menu/src/ExtensionCustomMenu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"ExtensionCustomMenu.d.ts","sourceRoot":"","sources":["../../../src/extension-menu/src/ExtensionCustomMenu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAM7C,MAAM,WAAW,iBAAiB;IAEhC,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,CAAC;IAG9C,gBAAgB,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAG1C,0BAA0B,CAAC,EAAE,OAAO,CAAC;IAGrC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAGD,qBAAa,mBAAoB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACnE,IAAI,SAAgB;IAEX,qBAAqB,IAAI,MAAM,EAAE;CA2C3C"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Extension } from '../../editor/src/mod.js';
|
|
2
2
|
import { buildMenu } from './buildMenu.js';
|
|
3
3
|
import { CustomMenuPlugin } from './CustomMenuPlugin.js';
|
|
4
|
+
import { CollaborationStatusElement } from '../../extension-yjs/src/CollaborationStatus.js';
|
|
5
|
+
import { LspStatusElement } from '../../extension-lsp/src/LspStatus.js';
|
|
4
6
|
/// Extension for a customizable menu with pinned items
|
|
5
7
|
export class ExtensionCustomMenu extends Extension {
|
|
6
8
|
constructor() {
|
|
@@ -13,10 +15,42 @@ export class ExtensionCustomMenu extends Extension {
|
|
|
13
15
|
});
|
|
14
16
|
}
|
|
15
17
|
getProseMirrorPlugins() {
|
|
16
|
-
const content =
|
|
18
|
+
const content = this.config?.content ??
|
|
19
|
+
buildMenu(this.editor, this.editor.schema);
|
|
20
|
+
const trailingElements = this.config?.trailingElements
|
|
21
|
+
? [...this.config.trailingElements]
|
|
22
|
+
: [];
|
|
23
|
+
// Auto-add collaboration status if YJS extension is present
|
|
24
|
+
const autoAdd = this.config?.autoAddCollaborationStatus ?? true;
|
|
25
|
+
if (autoAdd) {
|
|
26
|
+
const yjsExtension = this.editor.getExtension('yjs');
|
|
27
|
+
if (yjsExtension) {
|
|
28
|
+
const yjsConfig = yjsExtension.config;
|
|
29
|
+
if (yjsConfig?.provider?.awareness) {
|
|
30
|
+
const collabStatus = new CollaborationStatusElement({
|
|
31
|
+
awareness: yjsConfig.provider.awareness,
|
|
32
|
+
provider: yjsConfig.provider,
|
|
33
|
+
});
|
|
34
|
+
trailingElements.push(collabStatus);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Auto-add LSP status if LSP extension is present
|
|
39
|
+
const autoAddLsp = this.config?.autoAddLspStatus ?? true;
|
|
40
|
+
if (autoAddLsp) {
|
|
41
|
+
const lspExtension = this.editor.getExtension('lsp');
|
|
42
|
+
if (lspExtension) {
|
|
43
|
+
const lspStatus = new LspStatusElement({
|
|
44
|
+
lspExtension: lspExtension,
|
|
45
|
+
label: 'LSP',
|
|
46
|
+
});
|
|
47
|
+
trailingElements.push(lspStatus);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
17
50
|
return [
|
|
18
51
|
new CustomMenuPlugin(this.editor, {
|
|
19
52
|
content,
|
|
53
|
+
trailingElements,
|
|
20
54
|
}),
|
|
21
55
|
];
|
|
22
56
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EditorState, Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
import { EditorView } from 'prosemirror-view';
|
|
3
|
+
import type * as awarenessProtocol from 'y-protocols/awareness';
|
|
4
|
+
import type { MenuElement } from '../../extension-menu/src/mod.js';
|
|
5
|
+
export interface CollaborationUser {
|
|
6
|
+
clientId: number;
|
|
7
|
+
name: string;
|
|
8
|
+
color: string;
|
|
9
|
+
colorLight?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CollaborationStatusOptions {
|
|
12
|
+
/** The Yjs awareness instance */
|
|
13
|
+
awareness: awarenessProtocol.Awareness;
|
|
14
|
+
/** Called when connection status changes */
|
|
15
|
+
onStatusChange?: (status: 'connected' | 'connecting' | 'disconnected') => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Plugin key for accessing collaboration status state
|
|
19
|
+
*/
|
|
20
|
+
export declare const collaborationStatusPluginKey: PluginKey<CollaborationStatusState>;
|
|
21
|
+
export interface CollaborationStatusState {
|
|
22
|
+
status: 'connected' | 'connecting' | 'disconnected';
|
|
23
|
+
users: CollaborationUser[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a ProseMirror plugin that tracks collaboration status and user presence.
|
|
27
|
+
* This plugin maintains state that can be accessed by other components.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collaborationStatusPlugin(awareness: awarenessProtocol.Awareness, options?: {
|
|
30
|
+
onStatusChange?: (status: 'connected' | 'connecting' | 'disconnected') => void;
|
|
31
|
+
}): Plugin;
|
|
32
|
+
/**
|
|
33
|
+
* A menu element that displays collaboration status, user count, and a dropdown of users.
|
|
34
|
+
*/
|
|
35
|
+
export declare class CollaborationStatusElement implements MenuElement {
|
|
36
|
+
private awareness;
|
|
37
|
+
private provider;
|
|
38
|
+
private dom;
|
|
39
|
+
private statusDot;
|
|
40
|
+
private userCount;
|
|
41
|
+
private dropdown;
|
|
42
|
+
private userList;
|
|
43
|
+
private isOpen;
|
|
44
|
+
private status;
|
|
45
|
+
constructor(options: {
|
|
46
|
+
awareness: awarenessProtocol.Awareness;
|
|
47
|
+
provider: any;
|
|
48
|
+
});
|
|
49
|
+
render(view: EditorView): {
|
|
50
|
+
dom: HTMLElement;
|
|
51
|
+
update: (state: EditorState) => boolean;
|
|
52
|
+
};
|
|
53
|
+
private setStatus;
|
|
54
|
+
private updateUserList;
|
|
55
|
+
private toggleDropdown;
|
|
56
|
+
private openDropdown;
|
|
57
|
+
private closeDropdown;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=CollaborationStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollaborationStatus.d.ts","sourceRoot":"","sources":["../../../src/extension-yjs/src/CollaborationStatus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAInE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA0B;IACzC,iCAAiC;IACjC,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;IACvC,4CAA4C;IAC5C,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,cAAc,KAChD,IAAI,CAAC;CACX;AAED;;GAEG;AACH,eAAO,MAAM,4BAA4B,qCAEhB,CAAC;AAE1B,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,cAAc,CAAC;IACpD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,iBAAiB,CAAC,SAAS,EACtC,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,CACf,MAAM,EAAE,WAAW,GAAG,YAAY,GAAG,cAAc,KAChD,IAAI,CAAC;CACN,GACL,MAAM,CAsCR;AAiBD;;GAEG;AACH,qBAAa,0BAA2B,YAAW,WAAW;IAC5D,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAA6D;gBAGzE,OAAO,EAAE;QAAE,SAAS,EAAE,iBAAiB,CAAC,SAAS,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE;IAMpE,MAAM,CACJ,IAAI,EAAE,UAAU,GACf;QAAE,GAAG,EAAE,WAAW,CAAC;QAAC,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAA;KAAE;IA+FhE,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,cAAc;IAuDtB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;CAOtB"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
2
|
+
const CSS_PREFIX = 'kb-collab-status';
|
|
3
|
+
/**
|
|
4
|
+
* Plugin key for accessing collaboration status state
|
|
5
|
+
*/
|
|
6
|
+
export const collaborationStatusPluginKey = new PluginKey('collaboration-status');
|
|
7
|
+
/**
|
|
8
|
+
* Creates a ProseMirror plugin that tracks collaboration status and user presence.
|
|
9
|
+
* This plugin maintains state that can be accessed by other components.
|
|
10
|
+
*/
|
|
11
|
+
export function collaborationStatusPlugin(awareness, options = {}) {
|
|
12
|
+
return new Plugin({
|
|
13
|
+
key: collaborationStatusPluginKey,
|
|
14
|
+
state: {
|
|
15
|
+
init() {
|
|
16
|
+
return {
|
|
17
|
+
status: 'connecting',
|
|
18
|
+
users: getUsers(awareness),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
apply(tr, value) {
|
|
22
|
+
const meta = tr.getMeta(collaborationStatusPluginKey);
|
|
23
|
+
if (meta) {
|
|
24
|
+
return { ...value, ...meta };
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
view(editorView) {
|
|
30
|
+
const updateUsers = () => {
|
|
31
|
+
const users = getUsers(awareness);
|
|
32
|
+
editorView.dispatch(editorView.state.tr.setMeta(collaborationStatusPluginKey, { users }));
|
|
33
|
+
};
|
|
34
|
+
awareness.on('change', updateUsers);
|
|
35
|
+
return {
|
|
36
|
+
destroy() {
|
|
37
|
+
awareness.off('change', updateUsers);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function getUsers(awareness) {
|
|
44
|
+
const users = [];
|
|
45
|
+
awareness.getStates().forEach((state, clientId) => {
|
|
46
|
+
if (state.user) {
|
|
47
|
+
users.push({
|
|
48
|
+
clientId,
|
|
49
|
+
name: state.user.name || `User ${clientId}`,
|
|
50
|
+
color: state.user.color || '#888888',
|
|
51
|
+
colorLight: state.user.colorLight,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return users;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A menu element that displays collaboration status, user count, and a dropdown of users.
|
|
59
|
+
*/
|
|
60
|
+
export class CollaborationStatusElement {
|
|
61
|
+
constructor(options) {
|
|
62
|
+
Object.defineProperty(this, "awareness", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: void 0
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(this, "provider", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
writable: true,
|
|
72
|
+
value: void 0
|
|
73
|
+
});
|
|
74
|
+
Object.defineProperty(this, "dom", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
value: null
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(this, "statusDot", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
value: null
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(this, "userCount", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: null
|
|
91
|
+
});
|
|
92
|
+
Object.defineProperty(this, "dropdown", {
|
|
93
|
+
enumerable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
value: null
|
|
97
|
+
});
|
|
98
|
+
Object.defineProperty(this, "userList", {
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true,
|
|
101
|
+
writable: true,
|
|
102
|
+
value: null
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(this, "isOpen", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: false
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "status", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: 'connecting'
|
|
115
|
+
});
|
|
116
|
+
this.awareness = options.awareness;
|
|
117
|
+
this.provider = options.provider;
|
|
118
|
+
}
|
|
119
|
+
render(view) {
|
|
120
|
+
// Create main container
|
|
121
|
+
this.dom = document.createElement('div');
|
|
122
|
+
this.dom.className = `${CSS_PREFIX}`;
|
|
123
|
+
this.dom.setAttribute('role', 'button');
|
|
124
|
+
this.dom.setAttribute('aria-haspopup', 'true');
|
|
125
|
+
this.dom.setAttribute('aria-expanded', 'false');
|
|
126
|
+
// Create status indicator button
|
|
127
|
+
const button = document.createElement('button');
|
|
128
|
+
button.type = 'button';
|
|
129
|
+
button.className = `${CSS_PREFIX}__button`;
|
|
130
|
+
button.title = 'Collaboration status';
|
|
131
|
+
button.setAttribute('aria-label', 'Collaboration status');
|
|
132
|
+
// Status dot
|
|
133
|
+
this.statusDot = document.createElement('span');
|
|
134
|
+
this.statusDot.className =
|
|
135
|
+
`${CSS_PREFIX}__dot ${CSS_PREFIX}__dot--connecting`;
|
|
136
|
+
button.appendChild(this.statusDot);
|
|
137
|
+
// User count badge
|
|
138
|
+
this.userCount = document.createElement('span');
|
|
139
|
+
this.userCount.className = `${CSS_PREFIX}__count`;
|
|
140
|
+
this.userCount.textContent = '0';
|
|
141
|
+
button.appendChild(this.userCount);
|
|
142
|
+
// Dropdown chevron
|
|
143
|
+
const chevron = document.createElement('span');
|
|
144
|
+
chevron.className = `${CSS_PREFIX}__chevron`;
|
|
145
|
+
chevron.innerHTML =
|
|
146
|
+
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>`;
|
|
147
|
+
button.appendChild(chevron);
|
|
148
|
+
this.dom.appendChild(button);
|
|
149
|
+
// Create dropdown
|
|
150
|
+
this.dropdown = document.createElement('div');
|
|
151
|
+
this.dropdown.className = `${CSS_PREFIX}__dropdown`;
|
|
152
|
+
this.dropdown.style.display = 'none';
|
|
153
|
+
// Dropdown header
|
|
154
|
+
const header = document.createElement('div');
|
|
155
|
+
header.className = `${CSS_PREFIX}__header`;
|
|
156
|
+
header.textContent = 'Collaborators';
|
|
157
|
+
this.dropdown.appendChild(header);
|
|
158
|
+
// User list
|
|
159
|
+
this.userList = document.createElement('div');
|
|
160
|
+
this.userList.className = `${CSS_PREFIX}__users`;
|
|
161
|
+
this.dropdown.appendChild(this.userList);
|
|
162
|
+
this.dom.appendChild(this.dropdown);
|
|
163
|
+
// Event handlers
|
|
164
|
+
button.addEventListener('click', (e) => {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
this.toggleDropdown();
|
|
168
|
+
});
|
|
169
|
+
// Close on outside click
|
|
170
|
+
const closeHandler = (e) => {
|
|
171
|
+
if (this.isOpen && this.dom && !this.dom.contains(e.target)) {
|
|
172
|
+
this.closeDropdown();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
document.addEventListener('click', closeHandler);
|
|
176
|
+
// Listen for awareness changes
|
|
177
|
+
const awarenessChangeHandler = () => {
|
|
178
|
+
this.updateUserList();
|
|
179
|
+
};
|
|
180
|
+
this.awareness.on('change', awarenessChangeHandler);
|
|
181
|
+
// Listen for provider status changes
|
|
182
|
+
if (this.provider && typeof this.provider.on === 'function') {
|
|
183
|
+
this.provider.on('status', (event) => {
|
|
184
|
+
this.setStatus(event.status);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Initial update
|
|
188
|
+
this.updateUserList();
|
|
189
|
+
const update = (_state) => {
|
|
190
|
+
// Always visible
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
return { dom: this.dom, update };
|
|
194
|
+
}
|
|
195
|
+
setStatus(status) {
|
|
196
|
+
this.status = status;
|
|
197
|
+
if (this.statusDot) {
|
|
198
|
+
this.statusDot.className =
|
|
199
|
+
`${CSS_PREFIX}__dot ${CSS_PREFIX}__dot--${status}`;
|
|
200
|
+
}
|
|
201
|
+
if (this.dom) {
|
|
202
|
+
this.dom.setAttribute('data-status', status);
|
|
203
|
+
const button = this.dom.querySelector('button');
|
|
204
|
+
if (button) {
|
|
205
|
+
const statusLabels = {
|
|
206
|
+
connected: 'Connected',
|
|
207
|
+
connecting: 'Connecting...',
|
|
208
|
+
disconnected: 'Disconnected',
|
|
209
|
+
};
|
|
210
|
+
button.title = `${statusLabels[status]} - Click to see collaborators`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
updateUserList() {
|
|
215
|
+
if (!this.userList || !this.userCount)
|
|
216
|
+
return;
|
|
217
|
+
const users = getUsers(this.awareness);
|
|
218
|
+
const currentClientId = this.awareness.clientID;
|
|
219
|
+
// Update count (including self)
|
|
220
|
+
this.userCount.textContent = String(users.length);
|
|
221
|
+
// Clear and rebuild user list
|
|
222
|
+
this.userList.innerHTML = '';
|
|
223
|
+
if (users.length === 0) {
|
|
224
|
+
const emptyMessage = document.createElement('div');
|
|
225
|
+
emptyMessage.className = `${CSS_PREFIX}__empty`;
|
|
226
|
+
emptyMessage.textContent = 'No users connected';
|
|
227
|
+
this.userList.appendChild(emptyMessage);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Sort users: current user first, then alphabetically
|
|
231
|
+
const sortedUsers = [...users].sort((a, b) => {
|
|
232
|
+
if (a.clientId === currentClientId)
|
|
233
|
+
return -1;
|
|
234
|
+
if (b.clientId === currentClientId)
|
|
235
|
+
return 1;
|
|
236
|
+
return a.name.localeCompare(b.name);
|
|
237
|
+
});
|
|
238
|
+
sortedUsers.forEach((user) => {
|
|
239
|
+
const userItem = document.createElement('div');
|
|
240
|
+
userItem.className = `${CSS_PREFIX}__user`;
|
|
241
|
+
// Color indicator
|
|
242
|
+
const colorDot = document.createElement('span');
|
|
243
|
+
colorDot.className = `${CSS_PREFIX}__user-color`;
|
|
244
|
+
colorDot.style.backgroundColor = user.color;
|
|
245
|
+
userItem.appendChild(colorDot);
|
|
246
|
+
// User name
|
|
247
|
+
const userName = document.createElement('span');
|
|
248
|
+
userName.className = `${CSS_PREFIX}__user-name`;
|
|
249
|
+
userName.textContent = user.name;
|
|
250
|
+
userItem.appendChild(userName);
|
|
251
|
+
// "You" badge for current user
|
|
252
|
+
if (user.clientId === currentClientId) {
|
|
253
|
+
const youBadge = document.createElement('span');
|
|
254
|
+
youBadge.className = `${CSS_PREFIX}__you-badge`;
|
|
255
|
+
youBadge.textContent = '(you)';
|
|
256
|
+
userItem.appendChild(youBadge);
|
|
257
|
+
}
|
|
258
|
+
this.userList.appendChild(userItem);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
toggleDropdown() {
|
|
262
|
+
if (this.isOpen) {
|
|
263
|
+
this.closeDropdown();
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
this.openDropdown();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
openDropdown() {
|
|
270
|
+
if (!this.dropdown || !this.dom)
|
|
271
|
+
return;
|
|
272
|
+
this.isOpen = true;
|
|
273
|
+
this.dropdown.style.display = 'block';
|
|
274
|
+
this.dom.setAttribute('aria-expanded', 'true');
|
|
275
|
+
this.dom.classList.add(`${CSS_PREFIX}--open`);
|
|
276
|
+
this.updateUserList(); // Refresh on open
|
|
277
|
+
}
|
|
278
|
+
closeDropdown() {
|
|
279
|
+
if (!this.dropdown || !this.dom)
|
|
280
|
+
return;
|
|
281
|
+
this.isOpen = false;
|
|
282
|
+
this.dropdown.style.display = 'none';
|
|
283
|
+
this.dom.setAttribute('aria-expanded', 'false');
|
|
284
|
+
this.dom.classList.remove(`${CSS_PREFIX}--open`);
|
|
285
|
+
}
|
|
286
|
+
}
|