vzcode 0.85.0 → 0.86.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.
- package/dist/assets/{index-BC0TPgeW.js → index-DzsReLik.js} +53 -54
- package/dist/assets/{worker-1Ck54dGO.js → worker-DHoGVnSk.js} +18 -18
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +91 -0
- package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +139 -0
- package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
- package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
- package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
- package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
- package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
- package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
- package/src/client/CodeEditor/getOrCreateEditor.ts +4 -3
- package/src/client/VZCodeContext.tsx +36 -0
- package/src/client/VZLeft.tsx +2 -0
- package/src/client/VZSettings.tsx +14 -17
- package/src/client/VZSidebar/CreateDirModal.tsx +99 -0
- package/src/client/VZSidebar/index.tsx +21 -1
- package/src/client/VZSidebar/useDragAndDrop.tsx +29 -18
- package/src/client/useFileCRUD.ts +18 -0
- package/src/server/index.js +8 -2
- package/src/client/CodeEditor/widgets.ts +0 -509
package/dist/index.html
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-DzsReLik.js"></script>
|
|
24
24
|
<link rel="stylesheet" crossorigin href="/assets/index-9yhZQKtK.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
2
|
+
export const boolTogglerRegex = /true|false/g;
|
|
3
|
+
export const boolToggler = (
|
|
4
|
+
onInteract?: () => void,
|
|
5
|
+
): InteractRule => ({
|
|
6
|
+
regexp: boolTogglerRegex,
|
|
7
|
+
cursor: 'pointer',
|
|
8
|
+
onClick: (text, setText) => {
|
|
9
|
+
setText(text === 'true' ? 'false' : 'true');
|
|
10
|
+
if (onInteract) onInteract();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
2
|
+
|
|
3
|
+
// Regular expression for hex colors.
|
|
4
|
+
export const colorPickerRegex = /#[0-9A-Fa-f]{6}/g;
|
|
5
|
+
|
|
6
|
+
// hex color picker
|
|
7
|
+
// Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
|
|
8
|
+
// Works without quotes to support CSS.
|
|
9
|
+
export const colorPicker = (
|
|
10
|
+
onInteract?: () => void,
|
|
11
|
+
): InteractRule => ({
|
|
12
|
+
regexp: colorPickerRegex,
|
|
13
|
+
cursor: 'pointer',
|
|
14
|
+
onClick(
|
|
15
|
+
text: string,
|
|
16
|
+
setText: (newText: string) => void,
|
|
17
|
+
) {
|
|
18
|
+
const startingColor: string = text;
|
|
19
|
+
|
|
20
|
+
const sel: HTMLInputElement =
|
|
21
|
+
document.createElement('input');
|
|
22
|
+
sel.type = 'color';
|
|
23
|
+
sel.value = startingColor.toLowerCase();
|
|
24
|
+
|
|
25
|
+
// `valueIsUpper` maintains the style of the user's code.
|
|
26
|
+
// It keeps the case of a-f the same case as the original.
|
|
27
|
+
const valueIsUpper: boolean =
|
|
28
|
+
startingColor.toUpperCase() === startingColor;
|
|
29
|
+
|
|
30
|
+
const updateHex = (e: Event) => {
|
|
31
|
+
const el: HTMLInputElement =
|
|
32
|
+
e.target as HTMLInputElement;
|
|
33
|
+
if (el.value) {
|
|
34
|
+
setText(
|
|
35
|
+
valueIsUpper ? el.value.toUpperCase() : el.value,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (onInteract) onInteract();
|
|
39
|
+
};
|
|
40
|
+
sel.addEventListener('input', updateHex);
|
|
41
|
+
sel.click();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// TODO get this working
|
|
46
|
+
// rgb color picker
|
|
47
|
+
// Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
|
|
48
|
+
//TODO: create color picker for hsl colors
|
|
49
|
+
// {
|
|
50
|
+
// regexp: /rgb\(.*\)/g,
|
|
51
|
+
// cursor: 'pointer',
|
|
52
|
+
// onClick: (text, setText, e) => {
|
|
53
|
+
// const res =
|
|
54
|
+
// /rgb\((?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)\)/.exec(
|
|
55
|
+
// text,
|
|
56
|
+
// );
|
|
57
|
+
// const r = Number(res?.groups?.r);
|
|
58
|
+
// const g = Number(res?.groups?.g);
|
|
59
|
+
// const b = Number(res?.groups?.b);
|
|
60
|
+
|
|
61
|
+
// //sel will open the color picker when sel.click is called.
|
|
62
|
+
// const sel = document.createElement('input');
|
|
63
|
+
// sel.type = 'color';
|
|
64
|
+
|
|
65
|
+
// if (!isNaN(r + g + b)) sel.value = rgb2Hex(r, g, b);
|
|
66
|
+
|
|
67
|
+
// const updateRGB = (e: Event) => {
|
|
68
|
+
// const el = e.target as HTMLInputElement;
|
|
69
|
+
// if (onInteract) onInteract();
|
|
70
|
+
|
|
71
|
+
// if (el.value) {
|
|
72
|
+
// const [r, g, b] = hex2RGB(el.value);
|
|
73
|
+
// setText(`rgb(${r}, ${g}, ${b})`);
|
|
74
|
+
// }
|
|
75
|
+
// sel.removeEventListener('change', updateRGB);
|
|
76
|
+
// };
|
|
77
|
+
|
|
78
|
+
// sel.addEventListener('change', updateRGB);
|
|
79
|
+
// sel.click();
|
|
80
|
+
// },
|
|
81
|
+
// },
|
|
82
|
+
|
|
83
|
+
// // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L108
|
|
84
|
+
// const hex2RGB = (hex: string): [number, number, number] => {
|
|
85
|
+
// const v = parseInt(hex.substring(1), 16);
|
|
86
|
+
// return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
|
|
87
|
+
// };
|
|
88
|
+
|
|
89
|
+
// // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L117
|
|
90
|
+
// const rgb2Hex = (r: number, g: number, b: number): string =>
|
|
91
|
+
// '#' + r.toString(16) + g.toString(16) + b.toString(16);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Extension, RangeSet } from '@codemirror/state';
|
|
2
|
+
import {
|
|
3
|
+
Decoration,
|
|
4
|
+
EditorView,
|
|
5
|
+
ViewPlugin,
|
|
6
|
+
WidgetType,
|
|
7
|
+
} from '@codemirror/view';
|
|
8
|
+
import { colorPickerRegex } from './colorPicker';
|
|
9
|
+
|
|
10
|
+
const colorCircleTheme = EditorView.baseTheme({
|
|
11
|
+
'.color-circle-parent': {
|
|
12
|
+
display: 'inline-block',
|
|
13
|
+
cursor: 'inherit',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
class ColorWidget extends WidgetType {
|
|
18
|
+
color: string;
|
|
19
|
+
constructor(color: string) {
|
|
20
|
+
super();
|
|
21
|
+
this.color = color;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
eq(widget: ColorWidget): boolean {
|
|
25
|
+
return widget.color === this.color;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toDOM(): HTMLElement {
|
|
29
|
+
const parent = document.createElement('div');
|
|
30
|
+
const size = 20;
|
|
31
|
+
const innerSize = 14;
|
|
32
|
+
|
|
33
|
+
parent.setAttribute(
|
|
34
|
+
'style',
|
|
35
|
+
`width:${size}px;height:${size}px`,
|
|
36
|
+
);
|
|
37
|
+
parent.className = 'color-circle-parent';
|
|
38
|
+
const svg = document.createElementNS(
|
|
39
|
+
'http://www.w3.org/2000/svg',
|
|
40
|
+
'svg',
|
|
41
|
+
);
|
|
42
|
+
const colorCircle = document.createElementNS(
|
|
43
|
+
'http://www.w3.org/2000/svg',
|
|
44
|
+
'circle',
|
|
45
|
+
);
|
|
46
|
+
colorCircle.setAttributeNS(
|
|
47
|
+
null,
|
|
48
|
+
'fill',
|
|
49
|
+
this.color.replace(/["']/g, ''),
|
|
50
|
+
);
|
|
51
|
+
colorCircle.setAttributeNS(null, 'stroke', 'white');
|
|
52
|
+
colorCircle.setAttributeNS(
|
|
53
|
+
null,
|
|
54
|
+
'stroke-width',
|
|
55
|
+
'0.75',
|
|
56
|
+
);
|
|
57
|
+
colorCircle.setAttributeNS(
|
|
58
|
+
null,
|
|
59
|
+
'r',
|
|
60
|
+
'' + innerSize / 2,
|
|
61
|
+
);
|
|
62
|
+
colorCircle.setAttributeNS(null, 'cx', '' + size / 2);
|
|
63
|
+
colorCircle.setAttributeNS(
|
|
64
|
+
null,
|
|
65
|
+
'cy',
|
|
66
|
+
'' + (size / 2 - 1),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
svg.setAttributeNS(null, 'width', '' + size);
|
|
70
|
+
svg.setAttributeNS(null, 'height', '' + size);
|
|
71
|
+
|
|
72
|
+
svg.appendChild(colorCircle);
|
|
73
|
+
|
|
74
|
+
parent.appendChild(svg);
|
|
75
|
+
return parent;
|
|
76
|
+
}
|
|
77
|
+
ignoreEvent() {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const colorsInTextPlugin: Extension = [
|
|
83
|
+
ViewPlugin.fromClass(
|
|
84
|
+
class {
|
|
85
|
+
decorations: any;
|
|
86
|
+
view: EditorView;
|
|
87
|
+
constructor(view: EditorView) {
|
|
88
|
+
this.decorations = RangeSet.of([]);
|
|
89
|
+
this.view = view;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
decorations: (v) => {
|
|
94
|
+
const colorInfos = [];
|
|
95
|
+
|
|
96
|
+
const lines = v.view.state.doc.iter();
|
|
97
|
+
let line = lines.next();
|
|
98
|
+
|
|
99
|
+
// Offset is the number of characters before the hex
|
|
100
|
+
// so the circle can be placed properly.
|
|
101
|
+
let offset = 0;
|
|
102
|
+
while (!line.done) {
|
|
103
|
+
if (line.value === '\n') {
|
|
104
|
+
offset++;
|
|
105
|
+
line = lines.next();
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const hexColorOccurences = line.value.matchAll(
|
|
109
|
+
colorPickerRegex,
|
|
110
|
+
);
|
|
111
|
+
let hexOccurance = hexColorOccurences.next();
|
|
112
|
+
while (!hexOccurance.done) {
|
|
113
|
+
const offsetColorInfo = hexOccurance.value;
|
|
114
|
+
offsetColorInfo.index += offset;
|
|
115
|
+
colorInfos.push(offsetColorInfo);
|
|
116
|
+
hexOccurance = hexColorOccurences.next();
|
|
117
|
+
}
|
|
118
|
+
offset += line.value.length;
|
|
119
|
+
line = lines.next();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Decoration.set(
|
|
123
|
+
colorInfos.map((colorInfo) => {
|
|
124
|
+
return {
|
|
125
|
+
// 7 is the length of the hex color string
|
|
126
|
+
from: colorInfo.index + 7,
|
|
127
|
+
to: colorInfo.index + 7,
|
|
128
|
+
value: Decoration.widget({
|
|
129
|
+
side: -1,
|
|
130
|
+
widget: new ColorWidget(colorInfo[0]),
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
),
|
|
138
|
+
colorCircleTheme,
|
|
139
|
+
];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// TODO - This file is not being used.
|
|
2
|
+
// It was an attempt to highlight the interactive widgets in the editor.
|
|
3
|
+
// It is a good idea to keep this file for future reference, as it's
|
|
4
|
+
// pretty close to what we need to implement. It suffers from the following
|
|
5
|
+
// issues:
|
|
6
|
+
// - Hardcoded regexes for interactive widgets - we can pull from the various modules
|
|
7
|
+
// - The highlight is not working properly - it's not un-highlighting
|
|
8
|
+
// when the Alt key is released. This is likely due to the fact that the
|
|
9
|
+
// plugin is not being updated when the key is released.
|
|
10
|
+
|
|
11
|
+
// export const highlightWidgets = ViewPlugin.fromClass(
|
|
12
|
+
// class {
|
|
13
|
+
// showHighlight: boolean;
|
|
14
|
+
// view: EditorView;
|
|
15
|
+
// constructor(view: EditorView) {
|
|
16
|
+
// this.showHighlight = false;
|
|
17
|
+
// this.view = view;
|
|
18
|
+
// }
|
|
19
|
+
// },
|
|
20
|
+
// {
|
|
21
|
+
// decorations: (v) => {
|
|
22
|
+
// if (!v.showHighlight) {
|
|
23
|
+
// return Decoration.none;
|
|
24
|
+
// }
|
|
25
|
+
|
|
26
|
+
// const interactOpportunities = [];
|
|
27
|
+
// const lines = v.view.state.doc.iter();
|
|
28
|
+
// let line = lines.next();
|
|
29
|
+
|
|
30
|
+
// //Offset is the number of characters before the regex so the highlighting can be placed properly.
|
|
31
|
+
// let offset = 0;
|
|
32
|
+
// while (!line.done) {
|
|
33
|
+
// if (line.value === '\n') {
|
|
34
|
+
// offset++;
|
|
35
|
+
// line = lines.next();
|
|
36
|
+
// continue;
|
|
37
|
+
// }
|
|
38
|
+
// const interactiveOccurances = line.value.matchAll(
|
|
39
|
+
// //The below line contains all of the Regexes for all of the interactive widgets.
|
|
40
|
+
// /\"\#([0-9]|[A-F]|[a-f]){6}\"|rotate\(-?\d*\.?\d*\)|https?:\/\/[^ "]+|vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)|true|false|(?<!\#)-?\b\d+\.?\d*\b|rgb\(.*\)/dg,
|
|
41
|
+
// );
|
|
42
|
+
// let interactOccurance =
|
|
43
|
+
// interactiveOccurances.next();
|
|
44
|
+
// while (!interactOccurance.done) {
|
|
45
|
+
// const offsetInteract = interactOccurance.value;
|
|
46
|
+
// offsetInteract.indices[0][0] += offset;
|
|
47
|
+
// offsetInteract.indices[0][1] += offset;
|
|
48
|
+
// interactOpportunities.push(offsetInteract);
|
|
49
|
+
// interactOccurance = interactiveOccurances.next();
|
|
50
|
+
// }
|
|
51
|
+
// offset += line.value.length;
|
|
52
|
+
// line = lines.next();
|
|
53
|
+
// }
|
|
54
|
+
// return Decoration.set(
|
|
55
|
+
// interactOpportunities.map((opportunity) => {
|
|
56
|
+
// return {
|
|
57
|
+
// from: opportunity.indices[0][0],
|
|
58
|
+
// to: opportunity.indices[0][1],
|
|
59
|
+
// value: Decoration.mark({
|
|
60
|
+
// class: 'cm-interact ',
|
|
61
|
+
// }),
|
|
62
|
+
// };
|
|
63
|
+
// }),
|
|
64
|
+
// );
|
|
65
|
+
// },
|
|
66
|
+
// eventHandlers: {
|
|
67
|
+
// keydown(event, view) {
|
|
68
|
+
// if (event.key == 'Alt') {
|
|
69
|
+
// this.showHighlight = true;
|
|
70
|
+
// }
|
|
71
|
+
// },
|
|
72
|
+
// keyup(event, view) {
|
|
73
|
+
// if (event.key == 'Alt') {
|
|
74
|
+
// this.showHighlight = false;
|
|
75
|
+
// }
|
|
76
|
+
// },
|
|
77
|
+
// },
|
|
78
|
+
// },
|
|
79
|
+
// );
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import interact, {
|
|
2
|
+
InteractRule,
|
|
3
|
+
} from '@replit/codemirror-interact';
|
|
4
|
+
import { urlClicker } from './urlClicker';
|
|
5
|
+
import { colorPicker } from './colorPicker';
|
|
6
|
+
import { numberDragger } from './numberDragger';
|
|
7
|
+
import { boolToggler } from './boolToggler';
|
|
8
|
+
import { vec2Slider } from './vec2Slider';
|
|
9
|
+
import { rotateWidget } from './rotateWidget';
|
|
10
|
+
import { markdownCheckboxToggler } from './markdownCheckboxToggler';
|
|
11
|
+
|
|
12
|
+
// Additional supporting CodeMirror plugins.
|
|
13
|
+
export { colorsInTextPlugin } from './colorsInTextPlugin';
|
|
14
|
+
export { rotationIndicator } from './rotateWidget';
|
|
15
|
+
|
|
16
|
+
// Interactive code widgets.
|
|
17
|
+
// * Number dragger
|
|
18
|
+
// * Boolean toggler
|
|
19
|
+
// * URL clicker
|
|
20
|
+
// * color picker
|
|
21
|
+
// Inspired by:
|
|
22
|
+
// https://github.com/replit/codemirror-interact/blob/master/dev/index.ts
|
|
23
|
+
// `onInteract` is called when the user interacts with a widget.
|
|
24
|
+
export const widgets = ({
|
|
25
|
+
onInteract,
|
|
26
|
+
customInteractRules,
|
|
27
|
+
}: {
|
|
28
|
+
onInteract?: () => void;
|
|
29
|
+
customInteractRules?: Array<InteractRule>;
|
|
30
|
+
}) => {
|
|
31
|
+
const rules: Array<InteractRule> = [
|
|
32
|
+
colorPicker(onInteract),
|
|
33
|
+
numberDragger(onInteract),
|
|
34
|
+
markdownCheckboxToggler(onInteract),
|
|
35
|
+
boolToggler(onInteract),
|
|
36
|
+
vec2Slider(onInteract),
|
|
37
|
+
rotateWidget(onInteract),
|
|
38
|
+
urlClicker,
|
|
39
|
+
];
|
|
40
|
+
if (customInteractRules) {
|
|
41
|
+
rules.push(...customInteractRules);
|
|
42
|
+
}
|
|
43
|
+
return interact({ rules });
|
|
44
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
2
|
+
|
|
3
|
+
export const markdownCheckboxToggler = (
|
|
4
|
+
onInteract?: () => void,
|
|
5
|
+
): InteractRule => ({
|
|
6
|
+
regexp: /\[[ x]\]/g, // Matches [ ], [x], [X]
|
|
7
|
+
cursor: 'pointer',
|
|
8
|
+
onClick: (text, setText) => {
|
|
9
|
+
setText(text === '[ ]' ? '[x]' : '[ ]');
|
|
10
|
+
if (onInteract) onInteract();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
2
|
+
|
|
3
|
+
const numberDraggerRegex = /(?<!\#)-?\b\d+\.?\d*\b/g;
|
|
4
|
+
|
|
5
|
+
// a rule for a number dragger
|
|
6
|
+
export const numberDragger = (
|
|
7
|
+
onInteract?: () => void,
|
|
8
|
+
): InteractRule => ({
|
|
9
|
+
// the regexp matching the value
|
|
10
|
+
regexp: numberDraggerRegex,
|
|
11
|
+
// set cursor to "ew-resize" on hover
|
|
12
|
+
cursor: 'ew-resize',
|
|
13
|
+
// change number value based on mouse X movement on drag
|
|
14
|
+
onDrag: (text, setText, e) => {
|
|
15
|
+
const newVal = Number(text) + e.movementX;
|
|
16
|
+
if (isNaN(newVal)) return;
|
|
17
|
+
setText(newVal.toString());
|
|
18
|
+
if (onInteract) onInteract();
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Decoration,
|
|
3
|
+
EditorView,
|
|
4
|
+
ViewPlugin,
|
|
5
|
+
WidgetType,
|
|
6
|
+
} from '@codemirror/view';
|
|
7
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
8
|
+
|
|
9
|
+
let rotationOrigin: { x: number; y: number } = null;
|
|
10
|
+
// Set rotation to the angle between the x-axis and a line
|
|
11
|
+
// from the word "rotate" to the mouse pointer (while dragging).
|
|
12
|
+
// The rotation is in range (-180,180]
|
|
13
|
+
export const rotateWidget = (
|
|
14
|
+
onInteract?: () => void,
|
|
15
|
+
): InteractRule => ({
|
|
16
|
+
regexp: /rotate\(-?\d*\.?\d*\)/g,
|
|
17
|
+
cursor: 'move',
|
|
18
|
+
onDragStart(text, setText, e) {
|
|
19
|
+
rotationOrigin = { x: e.clientX, y: e.clientY };
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
onDrag(text, setText, e) {
|
|
23
|
+
if (rotationOrigin == null) return;
|
|
24
|
+
const rotationDegree = Math.round(
|
|
25
|
+
(Math.atan2(
|
|
26
|
+
rotationOrigin.y - e.clientY,
|
|
27
|
+
e.clientX - rotationOrigin.x,
|
|
28
|
+
) *
|
|
29
|
+
180) /
|
|
30
|
+
Math.PI,
|
|
31
|
+
);
|
|
32
|
+
//Calculate the angle between the x axis and a line from where the user first clicks to the current location of the mouse.
|
|
33
|
+
setText(`rotate(${rotationDegree})`);
|
|
34
|
+
const updateDragEvent = new CustomEvent(
|
|
35
|
+
'updateRotateDrag',
|
|
36
|
+
{ detail: rotationDegree },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
document.dispatchEvent(updateDragEvent);
|
|
40
|
+
|
|
41
|
+
if (onInteract) onInteract();
|
|
42
|
+
},
|
|
43
|
+
onDragEnd() {
|
|
44
|
+
rotationOrigin = null;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const rotationIndicator = ViewPlugin.fromClass(
|
|
49
|
+
class {
|
|
50
|
+
view: EditorView;
|
|
51
|
+
textPosition?: number;
|
|
52
|
+
rotation: number;
|
|
53
|
+
constructor(view) {
|
|
54
|
+
this.view = view;
|
|
55
|
+
this.textPosition = null;
|
|
56
|
+
this.rotation = 0;
|
|
57
|
+
|
|
58
|
+
document.addEventListener(
|
|
59
|
+
'updateRotateDrag',
|
|
60
|
+
(e: CustomEvent) => {
|
|
61
|
+
this.textPosition = this.view.posAtCoords(
|
|
62
|
+
rotationOrigin,
|
|
63
|
+
false,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
this.rotation = e.detail;
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
decorations: (v) => {
|
|
73
|
+
if (rotationOrigin === null) {
|
|
74
|
+
return Decoration.none;
|
|
75
|
+
}
|
|
76
|
+
return Decoration.set([
|
|
77
|
+
{
|
|
78
|
+
from: v.textPosition,
|
|
79
|
+
to: v.textPosition,
|
|
80
|
+
value: Decoration.widget({
|
|
81
|
+
side: -1,
|
|
82
|
+
widget: new RotationCircle(v.rotation),
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
class RotationCircle extends WidgetType {
|
|
91
|
+
angle: number;
|
|
92
|
+
constructor(angle: number) {
|
|
93
|
+
super();
|
|
94
|
+
this.angle = angle;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toDOM(view: EditorView): HTMLElement {
|
|
98
|
+
const parent = document.createElement('div');
|
|
99
|
+
|
|
100
|
+
parent.setAttribute('style', 'width:20px;height:20px');
|
|
101
|
+
parent.className = 'color-circle-parent';
|
|
102
|
+
const svg = document.createElementNS(
|
|
103
|
+
'http://www.w3.org/2000/svg',
|
|
104
|
+
'svg',
|
|
105
|
+
);
|
|
106
|
+
const colorCircle = document.createElementNS(
|
|
107
|
+
'http://www.w3.org/2000/svg',
|
|
108
|
+
'circle',
|
|
109
|
+
);
|
|
110
|
+
colorCircle.setAttributeNS(null, 'fill', '#808080');
|
|
111
|
+
colorCircle.setAttributeNS(null, 'r', '5');
|
|
112
|
+
colorCircle.setAttributeNS(null, 'cx', '5');
|
|
113
|
+
colorCircle.setAttributeNS(null, 'cy', '5');
|
|
114
|
+
|
|
115
|
+
const indicatorLine = document.createElementNS(
|
|
116
|
+
'http://www.w3.org/2000/svg',
|
|
117
|
+
'line',
|
|
118
|
+
);
|
|
119
|
+
indicatorLine.setAttributeNS(null, 'x1', '10');
|
|
120
|
+
indicatorLine.setAttributeNS(null, 'x2', '5');
|
|
121
|
+
indicatorLine.setAttributeNS(null, 'y1', '5');
|
|
122
|
+
indicatorLine.setAttributeNS(null, 'y2', '5');
|
|
123
|
+
indicatorLine.setAttributeNS(null, 'stroke', 'black');
|
|
124
|
+
indicatorLine.setAttributeNS(
|
|
125
|
+
null,
|
|
126
|
+
'transform',
|
|
127
|
+
`rotate(${360 - this.angle},5,5)`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
svg.setAttributeNS(null, 'viewBox', '0 0 10 10');
|
|
131
|
+
svg.setAttributeNS(null, 'width', '20');
|
|
132
|
+
svg.setAttributeNS(null, 'height', '20');
|
|
133
|
+
|
|
134
|
+
svg.appendChild(colorCircle);
|
|
135
|
+
svg.appendChild(indicatorLine);
|
|
136
|
+
|
|
137
|
+
parent.appendChild(svg);
|
|
138
|
+
return parent;
|
|
139
|
+
}
|
|
140
|
+
ignoreEvent() {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { urlClickerRegex } from './urlClicker';
|
|
3
|
+
|
|
4
|
+
describe('URL regex', () => {
|
|
5
|
+
test.each([
|
|
6
|
+
{
|
|
7
|
+
name: 'standard URL',
|
|
8
|
+
input:
|
|
9
|
+
'Visit https://example.com for more information.',
|
|
10
|
+
expected: ['https://example.com'],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'URL with single quote at the end',
|
|
14
|
+
input: "Click here: https://example.com';",
|
|
15
|
+
expected: ['https://example.com'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'multiple URLs',
|
|
19
|
+
input:
|
|
20
|
+
'Check out https://site1.com and https://site2.com.',
|
|
21
|
+
expected: ['https://site1.com', 'https://site2.com'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'URL with query parameters',
|
|
25
|
+
input:
|
|
26
|
+
'Visit https://example.com?param1=value1¶m2=value2.',
|
|
27
|
+
expected: [
|
|
28
|
+
'https://example.com?param1=value1¶m2=value2',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'URL with parentheses',
|
|
33
|
+
input:
|
|
34
|
+
'See the map at https://example.com/maps?location=(123,456).',
|
|
35
|
+
expected: [
|
|
36
|
+
'https://example.com/maps?location=(123,456)',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'no URL present',
|
|
41
|
+
input: 'This text does not contain a URL.',
|
|
42
|
+
expected: [],
|
|
43
|
+
},
|
|
44
|
+
])('$name', ({ input, expected }) => {
|
|
45
|
+
const matches = input.match(urlClickerRegex) || [];
|
|
46
|
+
expect(matches).toEqual(expected);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
2
|
+
|
|
3
|
+
export const urlClickerRegex =
|
|
4
|
+
/https?:\/\/[^\s'")]*([^\s'",;.]|\([^\s'")]*\)|\b)/g;
|
|
5
|
+
|
|
6
|
+
export const urlClicker: InteractRule = {
|
|
7
|
+
regexp: urlClickerRegex,
|
|
8
|
+
cursor: 'pointer',
|
|
9
|
+
onClick: (text: string) => {
|
|
10
|
+
window.open(text);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// vec2 slider
|
|
2
|
+
// Inspired by: https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L61
|
|
3
|
+
|
|
4
|
+
import { InteractRule } from '@replit/codemirror-interact';
|
|
5
|
+
|
|
6
|
+
export const vec2Slider = (
|
|
7
|
+
onInteract?: () => void,
|
|
8
|
+
): InteractRule => ({
|
|
9
|
+
regexp:
|
|
10
|
+
/vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)/g,
|
|
11
|
+
cursor: 'move',
|
|
12
|
+
|
|
13
|
+
onDrag: (text, setText, e) => {
|
|
14
|
+
const res =
|
|
15
|
+
/vec2\((?<x>-?\b\d+\.?\d*\b)\s*(,\s*(?<y>-?\b\d+\.?\d*\b))?\)/.exec(
|
|
16
|
+
text,
|
|
17
|
+
);
|
|
18
|
+
const x = Number(res?.groups?.x);
|
|
19
|
+
let y = Number(res?.groups?.y);
|
|
20
|
+
if (isNaN(x)) return;
|
|
21
|
+
if (isNaN(y)) y = x;
|
|
22
|
+
setText(`vec2(${x + e.movementX}, ${y - e.movementY})`);
|
|
23
|
+
if (onInteract) onInteract();
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -26,10 +26,9 @@ import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
|
26
26
|
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
27
27
|
import {
|
|
28
28
|
colorsInTextPlugin,
|
|
29
|
-
highlightWidgets,
|
|
30
29
|
rotationIndicator,
|
|
31
30
|
widgets,
|
|
32
|
-
} from './
|
|
31
|
+
} from './InteractiveWidgets';
|
|
33
32
|
import {
|
|
34
33
|
EditorCache,
|
|
35
34
|
EditorCacheValue,
|
|
@@ -243,7 +242,9 @@ export const getOrCreateEditor = ({
|
|
|
243
242
|
widgets({ onInteract, customInteractRules }),
|
|
244
243
|
);
|
|
245
244
|
|
|
246
|
-
|
|
245
|
+
// TODO fix the bugginess in this one where
|
|
246
|
+
// the highlight persists after the mouse leaves.
|
|
247
|
+
// extensions.push(highlightWidgets);
|
|
247
248
|
|
|
248
249
|
extensions.push(rotationIndicator);
|
|
249
250
|
|