mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 407df1d423 | |||
| e2dcebc6ee | |||
| a905ce3b4c | |||
| c017bb3d09 | |||
|
|
67c92c05a3 | ||
| e94ca2dec1 |
@@ -1 +1 @@
|
||||
{".":"0.20.0"}
|
||||
{".":"0.20.2"}
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [0.20.2](https://github.com/chenasraf/nextcloud-forum/compare/v0.20.1...v0.20.2) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bbcode cursor positions after inserting ([e2dcebc](https://github.com/chenasraf/nextcloud-forum/commit/e2dcebc6ee6e4d017f7f26fc86e72e6734a1f757))
|
||||
|
||||
## [0.20.1](https://github.com/chenasraf/nextcloud-forum/compare/v0.20.0...v0.20.1) (2026-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* db seed migrations ([c017bb3](https://github.com/chenasraf/nextcloud-forum/commit/c017bb3d09a517c19e772420311c23a957f25cba))
|
||||
* **l10n:** Update translations from Transifex ([67c92c0](https://github.com/chenasraf/nextcloud-forum/commit/67c92c05a3e7f58bbc05265087b763368653f7d3))
|
||||
|
||||
## [0.20.0](https://github.com/chenasraf/nextcloud-forum/compare/v0.19.7...v0.20.0) (2026-01-07)
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ This app is in early stages of development. While functional, you may encounter
|
||||
|
||||
The forum integrates seamlessly with your Nextcloud instance, using your existing users and groups for authentication and access control.
|
||||
]]></description>
|
||||
<version>0.20.0</version>
|
||||
<version>0.20.2</version>
|
||||
<licence>agpl</licence>
|
||||
<author mail="contact@casraf.dev" homepage="https://casraf.dev">Chen Asraf</author>
|
||||
<namespace>Forum</namespace>
|
||||
|
||||
13
composer.lock
generated
13
composer.lock
generated
@@ -1035,12 +1035,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||
"reference": "5ba14c800ff89c74333c22d56ca1c1f35c424805"
|
||||
"reference": "ebc5572f219ad85f60f20fcff71b98b5055c4f8e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5ba14c800ff89c74333c22d56ca1c1f35c424805",
|
||||
"reference": "5ba14c800ff89c74333c22d56ca1c1f35c424805",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ebc5572f219ad85f60f20fcff71b98b5055c4f8e",
|
||||
"reference": "ebc5572f219ad85f60f20fcff71b98b5055c4f8e",
|
||||
"shasum": ""
|
||||
},
|
||||
"conflict": {
|
||||
@@ -1173,10 +1173,11 @@
|
||||
"contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5",
|
||||
"contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8",
|
||||
"contao/managed-edition": "<=1.5",
|
||||
"coreshop/core-shop": "<=4.1.7",
|
||||
"corveda/phpsandbox": "<1.3.5",
|
||||
"cosenary/instagram": "<=2.3",
|
||||
"couleurcitron/tarteaucitron-wp": "<0.3",
|
||||
"craftcms/cms": "<=4.16.5|>=5,<=5.8.6",
|
||||
"craftcms/cms": "<=4.16.16|>=5,<=5.8.20",
|
||||
"croogo/croogo": "<=4.0.7",
|
||||
"cuyz/valinor": "<0.12",
|
||||
"czim/file-handling": "<1.5|>=2,<2.3",
|
||||
@@ -1681,7 +1682,7 @@
|
||||
"rap2hpoutre/laravel-log-viewer": "<0.13",
|
||||
"react/http": ">=0.7,<1.9",
|
||||
"really-simple-plugins/complianz-gdpr": "<6.4.2",
|
||||
"redaxo/source": "<5.20.1",
|
||||
"redaxo/source": "<=5.20.1",
|
||||
"remdex/livehelperchat": "<4.29",
|
||||
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
|
||||
"reportico-web/reportico": "<=8.1",
|
||||
@@ -2034,7 +2035,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-02T22:05:49+00:00"
|
||||
"time": "2026-01-07T20:06:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
||||
@@ -2,11 +2,15 @@ OC.L10N.register(
|
||||
"forum",
|
||||
{
|
||||
"Admin" : "Administrador",
|
||||
"Administrator role with full permissions" : "Rol de administrador con permisos completos",
|
||||
"Moderator" : "Moderador",
|
||||
"Moderator role with elevated permissions" : "Rol de moderador con permisos elevados",
|
||||
"User" : "Usuario",
|
||||
"Default user role with basic permissions" : "Rol de usuario por defecto con permisos básicos",
|
||||
"Guest" : "Invitado",
|
||||
"Guest role for unauthenticated users with read-only access" : "Rol de invitado para usuarios sin autenticar con acceso de solo lectura",
|
||||
"General" : "General",
|
||||
"General discussion categories" : "Categorías de discusión general",
|
||||
"Support" : "Soporte",
|
||||
"Bold text" : "Texto en negrita",
|
||||
"Underlined text" : "Texto subrayado",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{ "translations": {
|
||||
"Admin" : "Administrador",
|
||||
"Administrator role with full permissions" : "Rol de administrador con permisos completos",
|
||||
"Moderator" : "Moderador",
|
||||
"Moderator role with elevated permissions" : "Rol de moderador con permisos elevados",
|
||||
"User" : "Usuario",
|
||||
"Default user role with basic permissions" : "Rol de usuario por defecto con permisos básicos",
|
||||
"Guest" : "Invitado",
|
||||
"Guest role for unauthenticated users with read-only access" : "Rol de invitado para usuarios sin autenticar con acceso de solo lectura",
|
||||
"General" : "General",
|
||||
"General discussion categories" : "Categorías de discusión general",
|
||||
"Support" : "Soporte",
|
||||
"Bold text" : "Texto en negrita",
|
||||
"Underlined text" : "Texto subrayado",
|
||||
|
||||
@@ -93,7 +93,7 @@ OC.L10N.register(
|
||||
"Pick file from Nextcloud" : "Seleccionar un ficheiro en Nextcloud",
|
||||
"Upload file to Nextcloud" : "Enviar un ficheiro a Nextcloud",
|
||||
"Uploading file …" : "Enviando o ficheiro…",
|
||||
"Upload failed" : "Produciuse algún fallo no envío",
|
||||
"Upload failed" : "Produciuse un fallo no envío",
|
||||
"Close" : "Pechar",
|
||||
"Pick a file to attach" : "Escolla un ficheiro para anexar",
|
||||
"Failed to upload file" : "Produciuse un fallo ao enviar o ficheiro",
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"Pick file from Nextcloud" : "Seleccionar un ficheiro en Nextcloud",
|
||||
"Upload file to Nextcloud" : "Enviar un ficheiro a Nextcloud",
|
||||
"Uploading file …" : "Enviando o ficheiro…",
|
||||
"Upload failed" : "Produciuse algún fallo no envío",
|
||||
"Upload failed" : "Produciuse un fallo no envío",
|
||||
"Close" : "Pechar",
|
||||
"Pick a file to attach" : "Escolla un ficheiro para anexar",
|
||||
"Failed to upload file" : "Produciuse un fallo ao enviar o ficheiro",
|
||||
|
||||
@@ -87,7 +87,8 @@ class RepairSeeds extends Command {
|
||||
}
|
||||
};
|
||||
|
||||
SeedHelper::seedAll($migrationOutput);
|
||||
// Pass throwOnError=true so users get proper error feedback
|
||||
SeedHelper::seedAll($migrationOutput, true);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>Forum data repair/seed completed successfully!</info>');
|
||||
|
||||
@@ -17,8 +17,10 @@ class SeedHelper {
|
||||
* Each function checks its own state and returns early if already seeded
|
||||
*
|
||||
* @param \OCP\Migration\IOutput|null $output Optional output for console messages
|
||||
* @param bool $throwOnError If true, throws exceptions on failure. If false (default), logs errors and continues.
|
||||
* Set to false when called from migrations to avoid PostgreSQL transaction abort issues.
|
||||
*/
|
||||
public static function seedAll($output = null): void {
|
||||
public static function seedAll($output = null, bool $throwOnError = false): void {
|
||||
$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
|
||||
$logger->info('Forum seeding: Starting data seed/repair');
|
||||
|
||||
@@ -26,24 +28,61 @@ class SeedHelper {
|
||||
$output->info('Forum: Starting data seed/repair...');
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
// Ensure forum_users table exists (handle rename from forum_user_stats if needed)
|
||||
self::ensureForumUsersTable($output);
|
||||
// This is critical and should fail early if it cannot be done
|
||||
try {
|
||||
self::ensureForumUsersTable($output);
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'ensureForumUsersTable: ' . $e->getMessage();
|
||||
$logger->error('Forum seeding: Failed to ensure forum_users table', ['exception' => $e->getMessage()]);
|
||||
if ($output) {
|
||||
$output->warning(' Failed to ensure forum_users table: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Each function checks its own state and returns early if already seeded
|
||||
// They run independently so one failure doesn't block others
|
||||
self::seedDefaultRoles($output);
|
||||
self::seedCategoryHeaders($output);
|
||||
self::seedDefaultCategories($output);
|
||||
self::seedCategoryPermissions($output);
|
||||
self::seedGuestRolePermissions($output);
|
||||
self::seedDefaultBBCodes($output);
|
||||
self::assignUserRoles($output);
|
||||
self::seedWelcomeThread($output);
|
||||
// They run independently so one failure does not block others
|
||||
// This is especially important for PostgreSQL where a failed query aborts the transaction
|
||||
$seedOperations = [
|
||||
'seedDefaultRoles' => fn () => self::seedDefaultRoles($output),
|
||||
'seedCategoryHeaders' => fn () => self::seedCategoryHeaders($output),
|
||||
'seedDefaultCategories' => fn () => self::seedDefaultCategories($output),
|
||||
'seedCategoryPermissions' => fn () => self::seedCategoryPermissions($output),
|
||||
'seedGuestRolePermissions' => fn () => self::seedGuestRolePermissions($output),
|
||||
'seedDefaultBBCodes' => fn () => self::seedDefaultBBCodes($output),
|
||||
'assignUserRoles' => fn () => self::assignUserRoles($output),
|
||||
'seedWelcomeThread' => fn () => self::seedWelcomeThread($output),
|
||||
];
|
||||
|
||||
$logger->info('Forum seeding: Completed data seed/repair');
|
||||
foreach ($seedOperations as $name => $operation) {
|
||||
try {
|
||||
$operation();
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "$name: " . $e->getMessage();
|
||||
$logger->error("Forum seeding: $name failed", ['exception' => $e->getMessage()]);
|
||||
// Continue with other operations - don't let one failure block others
|
||||
}
|
||||
}
|
||||
|
||||
if ($output) {
|
||||
$output->info('Forum: Data seed/repair completed');
|
||||
if (!empty($errors)) {
|
||||
$errorSummary = 'Some seeding operations failed: ' . implode('; ', $errors);
|
||||
$logger->warning('Forum seeding: Completed with errors', ['errors' => $errors]);
|
||||
|
||||
if ($output) {
|
||||
$output->warning('Forum: Data seed/repair completed with errors. Run "occ forum:repair-seeds" to retry failed operations.');
|
||||
}
|
||||
|
||||
if ($throwOnError) {
|
||||
throw new \RuntimeException($errorSummary);
|
||||
}
|
||||
} else {
|
||||
$logger->info('Forum seeding: Completed data seed/repair successfully');
|
||||
|
||||
if ($output) {
|
||||
$output->info('Forum: Data seed/repair completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,15 @@ class Version15Date20260103000000 extends SimpleMigrationStep {
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
|
||||
// Re-run seeding to ensure all required data exists
|
||||
SeedHelper::seedAll($output);
|
||||
// Pass throwOnError=false to avoid PostgreSQL transaction abort issues
|
||||
// If seeding fails, users can run "occ forum:repair-seeds" to retry
|
||||
try {
|
||||
SeedHelper::seedAll($output, false);
|
||||
} catch (\Exception $e) {
|
||||
// This should not happen with throwOnError=false, but handle it gracefully
|
||||
$this->logger->error('Forum migration: Seeding failed unexpectedly', ['exception' => $e->getMessage()]);
|
||||
$output->warning('Forum: Seeding failed. Run "occ forum:repair-seeds" after enabling the app to complete setup.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,16 +43,16 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^9.39.2",
|
||||
"happy-dom": "^20.0.11",
|
||||
"happy-dom": "^20.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-vue": "^1.1.6",
|
||||
"rollup-plugin-visualizer": "^6.0.5",
|
||||
"sass": "^1.97.1",
|
||||
"sass-embedded": "^1.97.1",
|
||||
"sass": "^1.97.2",
|
||||
"sass-embedded": "^1.97.2",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "^8.51.0",
|
||||
"typescript-eslint": "^8.52.0",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-checker": "^0.12.0",
|
||||
"vitest": "^4.0.16",
|
||||
|
||||
769
pnpm-lock.yaml
generated
769
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -54,12 +54,12 @@ describe('bbcode utilities', () => {
|
||||
expect(result.cursorPosition).toBe(18)
|
||||
})
|
||||
|
||||
it('inserts template at cursor when no selection', () => {
|
||||
it('inserts template at cursor when no selection, cursor between tags', () => {
|
||||
const selection: TextSelection = { text: 'Hello world', start: 6, end: 6 }
|
||||
const result = applyBBCodeTemplate(selection, { template: '[b]{text}[/b]' })
|
||||
|
||||
expect(result.text).toBe('Hello [b][/b]world')
|
||||
expect(result.cursorPosition).toBe(13) // cursor after [/b]
|
||||
expect(result.cursorPosition).toBe(9) // cursor between tags (after [b]) for immediate typing
|
||||
})
|
||||
|
||||
it('uses fallback text when no selection', () => {
|
||||
@@ -130,6 +130,83 @@ describe('bbcode utilities', () => {
|
||||
|
||||
expect(result.text).toBe('[size=]text[/size]')
|
||||
})
|
||||
|
||||
describe('cursor positioning', () => {
|
||||
it('places cursor between tags when no selection (simple tag)', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, { template: '[b]{text}[/b]' })
|
||||
|
||||
expect(result.text).toBe('[b][/b]')
|
||||
expect(result.cursorPosition).toBe(3) // right after [b]
|
||||
})
|
||||
|
||||
it('places cursor between tags when no selection (tag with value)', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, {
|
||||
template: '[url={value}]{text}[/url]',
|
||||
value: 'http://example.com',
|
||||
})
|
||||
|
||||
expect(result.text).toBe('[url=http://example.com][/url]')
|
||||
expect(result.cursorPosition).toBe(24) // right after the ]
|
||||
})
|
||||
|
||||
it('places cursor between tags when no selection (color tag)', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, {
|
||||
template: '[color={value}]{text}[/color]',
|
||||
value: '#ff0000',
|
||||
})
|
||||
|
||||
expect(result.text).toBe('[color=#ff0000][/color]')
|
||||
expect(result.cursorPosition).toBe(15) // right after [color=#ff0000]
|
||||
})
|
||||
|
||||
it('places cursor after closing tag when selection exists', () => {
|
||||
const selection: TextSelection = { text: 'hello', start: 0, end: 5 }
|
||||
const result = applyBBCodeTemplate(selection, { template: '[b]{text}[/b]' })
|
||||
|
||||
expect(result.text).toBe('[b]hello[/b]')
|
||||
expect(result.cursorPosition).toBe(12) // after [/b]
|
||||
})
|
||||
|
||||
it('places cursor after closing tag when fallback text is used', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, {
|
||||
template: '[b]{text}[/b]',
|
||||
fallbackText: 'bold',
|
||||
})
|
||||
|
||||
expect(result.text).toBe('[b]bold[/b]')
|
||||
expect(result.cursorPosition).toBe(11) // after [/b]
|
||||
})
|
||||
|
||||
it('places cursor between tags for quote tag without selection', () => {
|
||||
const selection: TextSelection = { text: 'Some text', start: 9, end: 9 }
|
||||
const result = applyBBCodeTemplate(selection, { template: '[quote]{text}[/quote]' })
|
||||
|
||||
expect(result.text).toBe('Some text[quote][/quote]')
|
||||
expect(result.cursorPosition).toBe(16) // right after [quote]
|
||||
})
|
||||
|
||||
it('places cursor between tags for code tag without selection', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, { template: '[code]{text}[/code]' })
|
||||
|
||||
expect(result.text).toBe('[code][/code]')
|
||||
expect(result.cursorPosition).toBe(6) // right after [code]
|
||||
})
|
||||
|
||||
it('handles template with newlines - cursor between tags', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = applyBBCodeTemplate(selection, {
|
||||
template: '[list]\n[*]{text}\n[/list]',
|
||||
})
|
||||
|
||||
expect(result.text).toBe('[list]\n[*]\n[/list]')
|
||||
expect(result.cursorPosition).toBe(10) // right after [*]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('insertTextAtSelection', () => {
|
||||
@@ -183,12 +260,12 @@ describe('bbcode utilities', () => {
|
||||
expect(result.cursorPosition).toBe(18)
|
||||
})
|
||||
|
||||
it('inserts empty tags when no selection', () => {
|
||||
it('inserts empty tags when no selection, cursor between tags', () => {
|
||||
const selection: TextSelection = { text: 'Hello', start: 5, end: 5 }
|
||||
const result = wrapSelection(selection, '[i]', '[/i]')
|
||||
|
||||
expect(result.text).toBe('Hello[i][/i]')
|
||||
expect(result.cursorPosition).toBe(12)
|
||||
expect(result.cursorPosition).toBe(8) // cursor between tags (after [i]) for immediate typing
|
||||
})
|
||||
|
||||
it('uses fallback text when no selection', () => {
|
||||
@@ -419,12 +496,12 @@ describe('bbcode utilities', () => {
|
||||
expect(result.text).toBe('Use [code][b][/code] for bold')
|
||||
})
|
||||
|
||||
it('handles empty string', () => {
|
||||
it('handles empty string, cursor between tags', () => {
|
||||
const selection: TextSelection = { text: '', start: 0, end: 0 }
|
||||
const result = wrapSelection(selection, '[b]', '[/b]')
|
||||
|
||||
expect(result.text).toBe('[b][/b]')
|
||||
expect(result.cursorPosition).toBe(7)
|
||||
expect(result.cursorPosition).toBe(3) // cursor between tags (after [b]) for immediate typing
|
||||
})
|
||||
|
||||
it('handles very long text', () => {
|
||||
|
||||
@@ -74,6 +74,11 @@ export function getSelectedText(selection: TextSelection): string {
|
||||
* 2. Replaces the selected text with the BBCode-wrapped version
|
||||
* 3. Returns the new text and cursor position
|
||||
*
|
||||
* Cursor positioning:
|
||||
* - With selected text or fallback text: cursor is placed after the closing tag
|
||||
* - Without any content: cursor is placed between the opening and closing tags
|
||||
* so the user can immediately start typing
|
||||
*
|
||||
* Template placeholders:
|
||||
* - {text}: Replaced with selected text (or fallbackText if nothing selected)
|
||||
* - {value}: Replaced with the provided value (for tags like [url=...], [color=...])
|
||||
@@ -83,7 +88,7 @@ export function getSelectedText(selection: TextSelection): string {
|
||||
* @returns The insertion result with new text and cursor position
|
||||
*
|
||||
* @example
|
||||
* // Simple wrap with [b] tags
|
||||
* // Simple wrap with [b] tags - cursor after closing tag
|
||||
* applyBBCodeTemplate(
|
||||
* { text: 'Hello world', start: 6, end: 11 },
|
||||
* { template: '[b]{text}[/b]' }
|
||||
@@ -91,6 +96,14 @@ export function getSelectedText(selection: TextSelection): string {
|
||||
* // Returns: { text: 'Hello [b]world[/b]', cursorPosition: 18 }
|
||||
*
|
||||
* @example
|
||||
* // No selection - cursor between tags for immediate typing
|
||||
* applyBBCodeTemplate(
|
||||
* { text: 'Hello ', start: 6, end: 6 },
|
||||
* { template: '[b]{text}[/b]' }
|
||||
* )
|
||||
* // Returns: { text: 'Hello [b][/b]', cursorPosition: 9 }
|
||||
*
|
||||
* @example
|
||||
* // URL with value
|
||||
* applyBBCodeTemplate(
|
||||
* { text: 'Check this', start: 6, end: 10 },
|
||||
@@ -115,7 +128,27 @@ export function applyBBCodeTemplate(
|
||||
.replace('{text}', contentText)
|
||||
|
||||
const newText = beforeText + insertText + afterText
|
||||
const cursorPosition = beforeText.length + insertText.length
|
||||
|
||||
// Calculate cursor position:
|
||||
// - If there's content (selected text or fallback), place cursor after the closing tag
|
||||
// - If no content, place cursor between tags (at the {text} placeholder position)
|
||||
// so user can immediately start typing
|
||||
let cursorPosition: number
|
||||
if (contentText) {
|
||||
// Cursor after the entire inserted text (after closing tag)
|
||||
cursorPosition = beforeText.length + insertText.length
|
||||
} else {
|
||||
// No content - find where {text} was in the template and place cursor there
|
||||
const templateWithValue = template.template.replace('{value}', template.value || '')
|
||||
const textPlaceholderIndex = templateWithValue.indexOf('{text}')
|
||||
if (textPlaceholderIndex !== -1) {
|
||||
// Place cursor where {text} placeholder was (between tags)
|
||||
cursorPosition = beforeText.length + textPlaceholderIndex
|
||||
} else {
|
||||
// Fallback: place cursor at end of inserted text
|
||||
cursorPosition = beforeText.length + insertText.length
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: newText,
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.20.0
|
||||
0.20.2
|
||||
|
||||
Reference in New Issue
Block a user