mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-08 15:33:59 +02:00
vim mode for editors
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Generated
+10
@@ -14,6 +14,7 @@
|
||||
"devalue": "^5.3.2",
|
||||
"license-checker": "^25.0.1",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"monaco-vim": "^0.4.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"npm-check-updates": "^17.1.3",
|
||||
"papaparse": "^5.5.3",
|
||||
@@ -2461,6 +2462,15 @@
|
||||
"@types/trusted-types": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-vim": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.2.tgz",
|
||||
"integrity": "sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"monaco-editor": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"devalue": "^5.3.2",
|
||||
"license-checker": "^25.0.1",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"monaco-vim": "^0.4.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"npm-check-updates": "^17.1.3",
|
||||
"papaparse": "^5.5.3",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
|
||||
import { BiMap } from '$lib/utils/maps';
|
||||
import { previewQR as generateQR } from '$lib/utils/qrPreview';
|
||||
import { vimModeEnabled } from '$lib/store/vimMode.js';
|
||||
/** @type {'domain'|'page'|'email'} */
|
||||
|
||||
export let contentType;
|
||||
@@ -12,6 +13,8 @@
|
||||
export let baseURL = 'example.test';
|
||||
export let domainMap = new BiMap({});
|
||||
export let selectedDomain = '';
|
||||
export let externalVimMode = null; // allow external control of vim mode
|
||||
let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled;
|
||||
let editor = null;
|
||||
let previewFrame = null;
|
||||
let previewRenderDelayID = null;
|
||||
@@ -24,6 +27,7 @@
|
||||
let externalFrameRef = null;
|
||||
let fileInputRef;
|
||||
let shadowContainer = null;
|
||||
let vimStatusBar = null;
|
||||
|
||||
const apiTemplates = [
|
||||
{ label: 'Custom Field 1', text: '{{.CustomField1}}' },
|
||||
@@ -123,6 +127,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
// enable vim mode if preference is enabled
|
||||
if (localVimMode) {
|
||||
initializeVimMode();
|
||||
}
|
||||
|
||||
editor.getModel().onDidChangeContent((e) => {
|
||||
if (previewRenderDelayID) {
|
||||
clearTimeout(previewRenderDelayID);
|
||||
@@ -136,6 +145,10 @@
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
if (vimModeInstance && vimModeInstance.dispose) {
|
||||
vimModeInstance.dispose();
|
||||
vimModeInstance = null;
|
||||
}
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
monaco.editor.getModels().forEach((model) => model.dispose());
|
||||
@@ -143,6 +156,52 @@
|
||||
};
|
||||
});
|
||||
|
||||
// track vim mode state to prevent duplicate initialization
|
||||
let vimModeInstance = null;
|
||||
|
||||
const initializeVimMode = () => {
|
||||
if (localVimMode && editor && !vimModeInstance) {
|
||||
import('monaco-vim')
|
||||
.then((vimModule) => {
|
||||
const statusNode = vimStatusBar;
|
||||
vimModeInstance = vimModule.initVimMode(editor, statusNode);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('vim mode not available', e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const destroyVimMode = () => {
|
||||
if (vimModeInstance) {
|
||||
// use official monaco-vim dispose method
|
||||
vimModeInstance.dispose();
|
||||
|
||||
// clear vim status bar
|
||||
if (vimStatusBar) {
|
||||
vimStatusBar.textContent = '';
|
||||
}
|
||||
|
||||
vimModeInstance = null;
|
||||
}
|
||||
};
|
||||
|
||||
// sync with external vim mode control
|
||||
$: if (externalVimMode !== null) {
|
||||
localVimMode = externalVimMode;
|
||||
} else {
|
||||
localVimMode = $vimModeEnabled;
|
||||
}
|
||||
|
||||
// Watch for vim mode changes after initial load
|
||||
$: if (editor && typeof localVimMode === 'boolean') {
|
||||
if (localVimMode && !vimModeInstance) {
|
||||
initializeVimMode();
|
||||
} else if (!localVimMode && vimModeInstance) {
|
||||
destroyVimMode();
|
||||
}
|
||||
}
|
||||
|
||||
const selectPreviewDomain = () => {
|
||||
baseURL = selectedDomain ? selectedDomain : baseURL;
|
||||
updatePreview();
|
||||
@@ -564,6 +623,30 @@
|
||||
<span>Preview</span>
|
||||
</button>
|
||||
|
||||
<!-- vim mode toggle button -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
vimModeEnabled.update((v) => !v);
|
||||
}}
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
|
||||
class:font-bold={localVimMode}
|
||||
class:bg-cta-blue={localVimMode}
|
||||
class:dark:bg-indigo-600={localVimMode}
|
||||
class:text-white={localVimMode}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="transition-colors duration-200"
|
||||
>
|
||||
<path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z" />
|
||||
</svg>
|
||||
<span>Vim</span>
|
||||
</button>
|
||||
|
||||
<!-- template selector -->
|
||||
<select
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 bg-white dark:bg-gray-700 text-black dark:text-gray-200 cursor-pointer transition-colors duration-200"
|
||||
@@ -647,7 +730,18 @@
|
||||
class:h-55vh={isDetailsVisible}
|
||||
class:h-67vh={!isDetailsVisible}
|
||||
>
|
||||
<div id="monaco-editor" class="h-full" />
|
||||
<div
|
||||
id="monaco-editor"
|
||||
class="h-full"
|
||||
style={localVimMode ? 'height: calc(100% - 25px)' : ''}
|
||||
/>
|
||||
{#if localVimMode}
|
||||
<div
|
||||
bind:this={vimStatusBar}
|
||||
class="px-2 py-1 bg-gray-100 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 text-xs font-mono text-gray-700 dark:text-gray-300"
|
||||
style="height: 25px;"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="bg-cta-blue dark:bg-indigo-600 cursor-move w-1 transition-colors duration-200"
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
import * as monaco from 'monaco-editor';
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
import { vimModeEnabled } from '$lib/store/vimMode.js';
|
||||
|
||||
export let value = '';
|
||||
export let height = 'medium';
|
||||
export let language = 'json';
|
||||
export let placeholder = '';
|
||||
export let showVimToggle = true;
|
||||
export let externalVimMode = null; // allow external control of vim mode
|
||||
let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled;
|
||||
|
||||
let editor = null;
|
||||
let editorContainer = null;
|
||||
let isDark = false;
|
||||
let vimStatusBar = null;
|
||||
let vimModeInstance = null;
|
||||
|
||||
const heightClasses = {
|
||||
small: 'h-64',
|
||||
@@ -88,12 +94,23 @@
|
||||
wordBasedSuggestions: 'off'
|
||||
});
|
||||
|
||||
// enable vim mode if preference is enabled
|
||||
if (localVimMode) {
|
||||
initializeVimMode();
|
||||
}
|
||||
|
||||
// Update value when editor content changes
|
||||
editor.getModel().onDidChangeContent(() => {
|
||||
value = editor.getValue();
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
return () => {
|
||||
cleanup();
|
||||
if (vimModeInstance && vimModeInstance.dispose) {
|
||||
vimModeInstance.dispose();
|
||||
vimModeInstance = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Watch for external value changes
|
||||
@@ -101,6 +118,49 @@
|
||||
editor.setValue(value || '');
|
||||
}
|
||||
|
||||
const initializeVimMode = () => {
|
||||
if (localVimMode && editor && !vimModeInstance) {
|
||||
import('monaco-vim')
|
||||
.then((vimModule) => {
|
||||
const statusNode = vimStatusBar;
|
||||
vimModeInstance = vimModule.initVimMode(editor, statusNode);
|
||||
})
|
||||
.catch(() => {
|
||||
console.warn('vim mode not available - monaco-vim package not installed');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const destroyVimMode = () => {
|
||||
if (vimModeInstance) {
|
||||
// use official monaco-vim dispose method
|
||||
vimModeInstance.dispose();
|
||||
|
||||
// clear vim status bar
|
||||
if (vimStatusBar) {
|
||||
vimStatusBar.textContent = '';
|
||||
}
|
||||
|
||||
vimModeInstance = null;
|
||||
}
|
||||
};
|
||||
|
||||
// sync with external vim mode control
|
||||
$: if (externalVimMode !== null) {
|
||||
localVimMode = externalVimMode;
|
||||
} else {
|
||||
localVimMode = $vimModeEnabled;
|
||||
}
|
||||
|
||||
// Watch for vim mode changes
|
||||
$: if (editor && typeof localVimMode === 'boolean') {
|
||||
if (localVimMode && !vimModeInstance) {
|
||||
initializeVimMode();
|
||||
} else if (!localVimMode && vimModeInstance) {
|
||||
destroyVimMode();
|
||||
}
|
||||
}
|
||||
|
||||
let showExample = false;
|
||||
|
||||
const loadExample = () => {
|
||||
@@ -114,12 +174,46 @@
|
||||
|
||||
<div class="w-full">
|
||||
<div class="bg-white dark:bg-gray-800 transition-colors duration-200 rounded-md">
|
||||
{#if showVimToggle}
|
||||
<div
|
||||
class="flex justify-between items-center p-2 border-b border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
vimModeEnabled.update((v) => !v);
|
||||
}}
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
|
||||
class:font-bold={localVimMode}
|
||||
class:bg-cta-blue={localVimMode}
|
||||
class:dark:bg-indigo-600={localVimMode}
|
||||
class:text-white={localVimMode}
|
||||
>
|
||||
<span>Vim</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={editorContainer}
|
||||
class="border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-900 rounded-md {heightClasses[
|
||||
height
|
||||
]} w-full transition-colors duration-200"
|
||||
class="border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-900 w-full transition-colors duration-200"
|
||||
class:rounded-b-md={showVimToggle}
|
||||
class:rounded-md={!showVimToggle}
|
||||
class:h-64={height === 'small' && !localVimMode}
|
||||
class:h-80={height === 'medium' && !localVimMode}
|
||||
class:h-96={height === 'large' && !localVimMode}
|
||||
style={localVimMode
|
||||
? `height: ${height === 'small' ? '224px' : height === 'medium' ? '294px' : '359px'}`
|
||||
: ''}
|
||||
></div>
|
||||
{#if localVimMode}
|
||||
<div
|
||||
bind:this={vimStatusBar}
|
||||
class="px-2 py-1 bg-gray-100 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 text-xs font-mono text-gray-700 dark:text-gray-300 rounded-b-md"
|
||||
style="height: 25px;"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if placeholder}
|
||||
<div class="mt-2">
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import { vimModeEnabled, toggleVimMode } from '$lib/store/vimMode.js';
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={toggleVimMode}
|
||||
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
|
||||
class:font-bold={$vimModeEnabled}
|
||||
class:bg-cta-blue={$vimModeEnabled}
|
||||
class:dark:bg-indigo-600={$vimModeEnabled}
|
||||
class:text-white={$vimModeEnabled}
|
||||
title={$vimModeEnabled ? 'Disable vim mode' : 'Enable vim mode'}
|
||||
>
|
||||
<span>Vim</span>
|
||||
</button>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { getCookie, setCookie } from '$lib/utils/cookies.js';
|
||||
|
||||
// get initial vim mode preference from cookie
|
||||
const getInitialVimMode = () => {
|
||||
if (typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const vimMode = getCookie('vim_mode_enabled');
|
||||
return vimMode === 'true';
|
||||
};
|
||||
|
||||
// create writable store for vim mode state
|
||||
export const vimModeEnabled = writable(getInitialVimMode());
|
||||
|
||||
// subscribe to changes and save to cookie
|
||||
vimModeEnabled.subscribe((enabled) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
setCookie('vim_mode_enabled', enabled.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// helper function to toggle vim mode
|
||||
export const toggleVimMode = () => {
|
||||
vimModeEnabled.update(enabled => !enabled);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* simple cookie utility for storing user preferences
|
||||
*/
|
||||
|
||||
/**
|
||||
* get a cookie value by name
|
||||
* @param {string} name - cookie name
|
||||
* @returns {string|null} cookie value or null if not found
|
||||
*/
|
||||
export function getCookie(name) {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
|
||||
if (parts.length === 2) {
|
||||
return parts.pop().split(';').shift();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* set a cookie value
|
||||
* @param {string} name - cookie name
|
||||
* @param {string} value - cookie value
|
||||
* @param {number} days - expiration in days (default: 365)
|
||||
*/
|
||||
export function setCookie(name, value, days = 365) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const expires = new Date();
|
||||
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
|
||||
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||
}
|
||||
|
||||
/**
|
||||
* get vim mode preference from cookie
|
||||
* @returns {boolean} vim mode enabled state
|
||||
*/
|
||||
export function getVimModePreference() {
|
||||
const vimMode = getCookie('vim_mode_enabled');
|
||||
return vimMode === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* save vim mode preference to cookie
|
||||
* @param {boolean} enabled - vim mode enabled state
|
||||
*/
|
||||
export function setVimModePreference(enabled) {
|
||||
setCookie('vim_mode_enabled', enabled.toString());
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte';
|
||||
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
|
||||
import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte';
|
||||
import VimToggle from '$lib/components/editor/VimToggle.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -426,16 +427,20 @@ X-Custom-Header: Hello Friend"
|
||||
>Request Headers</TextareaField
|
||||
>
|
||||
<div class="flex flex-col py-2 w-full">
|
||||
<div class="flex items-center">
|
||||
<p class="font-bold text-slate-600 dark:text-gray-300 py-2">Request Body</p>
|
||||
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md">
|
||||
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<p class="font-bold text-slate-600 dark:text-gray-300 py-2">Request Body</p>
|
||||
<div class="bg-gray-100 dark:bg-gray-700 ml-2 px-2 rounded-md">
|
||||
<p class="text-slate-600 dark:text-gray-300 text-xs">optional</p>
|
||||
</div>
|
||||
</div>
|
||||
<VimToggle />
|
||||
</div>
|
||||
<SimpleCodeEditor
|
||||
bind:value={formValues.requestBody}
|
||||
height="medium"
|
||||
language="json"
|
||||
showVimToggle={false}
|
||||
placeholder={`{
|
||||
"to": "{{.Name}}",
|
||||
"from": "{{.From}}",
|
||||
|
||||
Reference in New Issue
Block a user