Merge remote-tracking branch 'origin/develop' into renovate/vitest-monorepo

This commit is contained in:
Elian Doran 2025-06-20 15:35:27 +03:00
commit c61713333d
No known key found for this signature in database
86 changed files with 1325 additions and 1791 deletions

View File

@ -12,7 +12,7 @@ on:
paths: paths:
- .github/actions/build-electron/* - .github/actions/build-electron/*
- .github/workflows/nightly.yml - .github/workflows/nightly.yml
- forge.config.cjs - forge.config.ts
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

View File

@ -35,10 +35,10 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.53.0", "@playwright/test": "1.53.1",
"@stylistic/eslint-plugin": "4.4.1", "@stylistic/eslint-plugin": "4.4.1",
"@types/express": "5.0.3", "@types/express": "5.0.3",
"@types/node": "22.15.31", "@types/node": "22.15.32",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"eslint": "9.29.0", "eslint": "9.29.0",

4
apps/client/.env Normal file
View File

@ -0,0 +1,4 @@
# The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project.
# Expires on: 2025-09-13
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w

View File

@ -1,6 +0,0 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false
# The development license key for premium CKEditor features.
# Note: This key is for development purposes only and should not be used in production.
# Expires on: 2025-09-13
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6IjRmMjdkYmYxLTcwOTEtNDYwZi04ZDZmLTc0NzBiZjQwNjg2MCIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwibGljZW5zZVR5cGUiOiJkZXZlbG9wbWVudCIsImZlYXR1cmVzIjpbIkRSVVAiLCJDTVQiLCJETyIsIkZQIiwiU0MiLCJUT0MiLCJUUEwiLCJQT0UiLCJDQyIsIk1GIiwiU0VFIiwiRUNIIiwiRUlTIl0sInZjIjoiMjMxYzMwNTEifQ.9Ct5lIKbioC3dM8EFatDTmimEIVOdItE3Uh_ICHlS_A_8ueqIfkZpsN3L4_EqprvteNki9yqbuZVGpZTaQ51xg

View File

@ -1,6 +1 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false VITE_CKEDITOR_ENABLE_INSPECTOR=false
# The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project.
# Expires on: 2025-09-13
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w

View File

@ -27,7 +27,7 @@
"@triliumnext/highlightjs": "workspace:*", "@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*", "@triliumnext/share-theme": "workspace:*",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
"bootstrap": "5.3.6", "bootstrap": "5.3.7",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
@ -48,7 +48,7 @@
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "15.0.12", "marked": "15.0.12",
"mermaid": "11.6.0", "mermaid": "11.6.0",
"mind-elixir": "4.6.0", "mind-elixir": "4.6.1",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"preact": "10.26.9", "preact": "10.26.9",
@ -75,6 +75,9 @@
"dependsOn": [ "dependsOn": [
"^build" "^build"
] ]
},
"circular-deps": {
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false"
} }
} }
} }

View File

@ -1,424 +0,0 @@
/*
* Remove template code below
*/
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
}
svg {
display: block;
vertical-align: middle;
}
svg {
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {
}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {
}
.button-pill {
padding: 1.5rem 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-console-jetbrains {
margin-top: 2rem;
}
#nx-console-jetbrains:hover {
background-color: rgba(255, 49, 140, 1);
}
#nx-console-jetbrains svg {
color: rgba(255, 49, 140, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@ -1,21 +0,0 @@
import { AppElement } from './app.element';
describe('AppElement', () => {
let app: AppElement;
beforeEach(() => {
app = new AppElement();
});
it('should create successfully', () => {
expect(app).toBeTruthy();
});
it('should have a greeting', () => {
app.connectedCallback();
expect(app.querySelector('h1').innerHTML).toContain(
'Welcome @triliumnext/client'
);
});
});

View File

@ -1,409 +0,0 @@
import './app.element.css';
export class AppElement extends HTMLElement {
public static observedAttributes = [
];
connectedCallback() {
const title = '@triliumnext/client';
this.innerHTML = `
<div class="wrapper">
<div class="container">
<!-- WELCOME -->
<div id="welcome">
<h1>
<span> Hello there, </span>
Welcome ${title} 👋
</h1>
</div>
<!-- HERO -->
<div id="hero" class="rounded">
<div class="text-container">
<h2>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>You&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;s next? </a>
</div>
<div class="logo-container">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
/>
</svg>
</div>
</div>
<!-- MIDDLE CONTENT -->
<div id="middle-content">
<div id="learning-materials" class="rounded shadow">
<h2>Learning materials</h2>
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<span>
Documentation
<span> Everything is in there </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
<span>
Blog
<span> Changelog, features & events </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>YouTube</title>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg>
<span>
YouTube channel
<span> Nx Show, talks & tutorials </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<span>
Interactive tutorials
<span> Create an app, step-by-step </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
/>
</svg>
<span>
Video courses
<span> Nx custom courses </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<div id="other-links">
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Visual Studio Code</title>
<path
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
/>
</svg>
<span>
Install Nx Console for VSCode
<span>The official VSCode extension for Nx.</span>
</span>
</a>
<a
id="nx-console-jetbrains"
class="button-pill rounded shadow"
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
target="_blank"
rel="noreferrer"
>
<svg
height="48"
width="48"
viewBox="20 20 60 60"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m22.5 22.5h60v60h-60z" />
<g fill="#fff">
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
</g>
</svg>
<span>
Install Nx Console for JetBrains
<span>
Available for WebStorm, Intellij IDEA Ultimate and more!
</span>
</span>
</a>
<div id="nx-cloud" class="rounded shadow">
<div>
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
</svg>
<h2>
Nx Cloud
<span>
Enable faster CI & better DX
</span>
</h2>
</div>
<p>
You can activate distributed tasks executions and caching by
running:
</p>
<pre>nx connect</pre>
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
</div>
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
<span>
Nx is open source
<span> Love Nx? Give us a star! </span>
</span>
</a>
</div>
</div>
<!-- COMMANDS -->
<div id="commands" class="rounded shadow">
<h2>Next steps</h2>
<p>Here are some things you can do with Nx:</p>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Add UI library
</summary>
<pre><span># Generate UI lib</span>
nx g @nx/angular:lib ui
<span># Add a component</span>
nx g @nx/angular:component ui/src/lib/button</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View interactive project graph
</summary>
<pre>nx graph</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Run affected commands
</summary>
<pre><span># see what&apos;s been affected by changes</span>
nx affected:graph
<span># run tests for current changes</span>
nx affected:test
<span># run e2e tests for current changes</span>
nx affected:e2e</pre>
</details>
</div>
<p id="love">
Carefully crafted with
<svg
fill="currentColor"
stroke="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</p>
</div>
</div>
`;
}
}
customElements.define('triliumnext-root', AppElement);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Client</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<triliumnext-root></triliumnext-root>
</body>
</html>

View File

@ -1 +0,0 @@
import './app/app.element';

View File

@ -1 +0,0 @@
/* You can add global styles to this file, and also import other style files */

View File

@ -1,5 +1,4 @@
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import bundleService from "../services/bundle.js";
import RootCommandExecutor from "./root_command_executor.js"; import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
import options from "../services/options.js"; import options from "../services/options.js";
@ -470,6 +469,7 @@ export class AppContext extends Component {
this.tabManager.loadTabs(); this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000); setTimeout(() => bundleService.executeStartupBundles(), 2000);
} }

View File

@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror"; import type CodeMirror from "@triliumnext/codemirror";
import { closeActiveDialog } from "../services/dialog.js";
export interface SetNoteOpts { export interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
utils.closeActiveDialog(); closeActiveDialog();
this.notePath = resolvedNotePath; this.notePath = resolvedNotePath;
this.viewScope = opts.viewScope; this.viewScope = opts.viewScope;

View File

@ -1,7 +1,6 @@
import server from "../services/server.js"; import server from "../services/server.js";
import noteAttributeCache from "../services/note_attribute_cache.js"; import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js"; import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js"; import type { Froca } from "../services/froca-interface.js";
@ -410,8 +409,8 @@ class FNote {
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
notePath: path, notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => froca.notes[noteId].isArchived), isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived),
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden") isHidden: path.includes("_hidden")
})); }));
@ -982,7 +981,7 @@ class FNote {
continue; continue;
} }
const parentNote = froca.notes[parentNoteId]; const parentNote = this.froca.notes[parentNoteId];
if (!parentNote || parentNote.type === "search") { if (!parentNote || parentNote.type === "search") {
continue; continue;

View File

@ -1,6 +1,6 @@
import ScriptContext from "./script_context.js"; import ScriptContext from "./script_context.js";
import server from "./server.js"; import server from "./server.js";
import toastService from "./toast.js"; import toastService, { showError } from "./toast.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
} catch (e: any) { } catch (e: any) {
const note = await froca.getNote(bundle.noteId); const note = await froca.getNote(bundle.noteId);
toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`); const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
showError(message);
logError(message);
} }
} }

View File

@ -4,7 +4,7 @@ import froca from "./froca.js";
import linkService from "./link.js"; import linkService from "./link.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toast from "./toast.js"; import { throwError } from "./ws.js";
let clipboardBranchIds: string[] = []; let clipboardBranchIds: string[] = [];
let clipboardMode: string | null = null; let clipboardMode: string | null = null;
@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }
@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }

View File

@ -1,6 +1,41 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
export function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
async function info(message: string) { async function info(message: string) {
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));

View File

@ -0,0 +1,29 @@
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
export function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
export function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}

View File

@ -1,6 +1,7 @@
import { LOCALES } from "@triliumnext/commons"; import { LOCALES } from "@triliumnext/commons";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import { describe, expect, it } from "vitest";
describe("i18n", () => { describe("i18n", () => {
it("translations are valid JSON", () => { it("translations are valid JSON", () => {

View File

@ -1,5 +1,5 @@
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toastService from "./toast.js"; import toastService, { showError } from "./toast.js";
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try { try {
@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
if (success) { if (success) {
toastService.showMessage(t("image.copied-to-clipboard")); toastService.showMessage(t("image.copied-to-clipboard"));
} else { } else {
toastService.showAndLogError(t("image.cannot-copy")); const message = t("image.cannot-copy");
showError(message);
logError(message);
} }
} finally { } finally {
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();

View File

@ -289,13 +289,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
} }
if (suggestion.action === "create-note") { if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
if (!success) { if (!success) {
return; return;
} }
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle, title: suggestion.noteTitle,
activate: false, activate: false,
type: noteType, type: noteType,

View File

@ -116,7 +116,7 @@ async function chooseNoteType() {
} }
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const { success, noteType, templateNoteId } = await chooseNoteType(); const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
if (!success) { if (!success) {
return; return;
@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
options.type = noteType; options.type = noteType;
options.templateNoteId = templateNoteId; options.templateNoteId = templateNoteId;
return await createNote(parentNotePath, options); return await createNote(notePath || parentNotePath, options);
} }
/* If the first element is heading, parse it out and use it as a new heading. */ /* If the first element is heading, parse it out and use it as a new heading. */

View File

@ -1,4 +1,4 @@
import FrontendScriptApi, { type Entity } from "./frontend_script_api.js"; import type { Entity } from "./frontend_script_api.js";
import utils from "./utils.js"; import utils from "./utils.js";
import froca from "./froca.js"; import froca from "./froca.js";
@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn
throw new Error(`Could not find start note ${startNoteId}.`); throw new Error(`Could not find start note ${startNoteId}.`);
} }
const FrontendScriptApi = (await import("./frontend_script_api.js")).default;
return { return {
modules: modules, modules: modules,
notes: utils.toObject(allNotes, (note) => [note.noteId, note]), notes: utils.toObject(allNotes, (note) => [note.noteId, note]),

View File

@ -1,5 +1,6 @@
import utils, { isShare } from "./utils.js"; import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js"; import ValidationError from "./validation_error.js";
import { throwError } from "./ws.js";
type Headers = Record<string, string | null | undefined>; type Headers = Record<string, string | null | undefined>;
@ -276,7 +277,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
} else { } else {
const title = `${statusCode} ${method} ${url}`; const title = `${statusCode} ${method} ${url}`;
toastService.showErrorTitleAndMessage(title, messageStr); toastService.showErrorTitleAndMessage(title, messageStr);
toastService.throwError(`${title} - ${message}`); throwError(`${title} - ${message}`);
} }
} }

View File

@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) {
}); });
} }
function showAndLogError(message: string, delay = 10000) { export function showError(message: string, delay = 10000) {
showError(message, delay);
ws.logError(message);
}
function showError(message: string, delay = 10000) {
console.log(utils.now(), "error: ", message); console.log(utils.now(), "error: ", message);
toast({ toast({
@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000)
}); });
} }
function throwError(message: string) {
ws.logError(message);
throw new Error(message);
}
export default { export default {
showMessage, showMessage,
showError, showError,
showErrorTitleAndMessage, showErrorTitleAndMessage,
showAndLogError,
throwError,
showPersistent, showPersistent,
closePersistent closePersistent
}; };

View File

@ -1,5 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Modal } from "bootstrap";
import type { ViewScope } from "./link.js"; import type { ViewScope } from "./link.js";
const SVG_MIME = "image/svg+xml"; const SVG_MIME = "image/svg+xml";
@ -275,69 +274,6 @@ function getMimeTypeClass(mime: string) {
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
} }
function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
function isHtmlEmpty(html: string) { function isHtmlEmpty(html: string) {
if (!html) { if (!html) {
return true; return true;
@ -823,10 +759,6 @@ export default {
setCookie, setCookie,
getNoteTypeClass, getNoteTypeClass,
getMimeTypeClass, getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
isHtmlEmpty, isHtmlEmpty,
clearBrowserCache, clearBrowserCache,
copySelectionToClipboard, copySelectionToClipboard,

View File

@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastPingTs: number; let lastPingTs: number;
let frontendUpdateDataQueue: EntityChange[] = []; let frontendUpdateDataQueue: EntityChange[] = [];
function logError(message: string) { export function logError(message: string) {
console.error(utils.now(), message); // needs to be separate from .trace() console.error(utils.now(), message); // needs to be separate from .trace()
if (ws && ws.readyState === 1) { if (ws && ws.readyState === 1) {
@ -301,6 +301,12 @@ setTimeout(() => {
setInterval(sendPing, 1000); setInterval(sendPing, 1000);
}, 0); }, 0);
export function throwError(message: string) {
logError(message);
throw new Error(message);
}
export default { export default {
logError, logError,
subscribeToMessages, subscribeToMessages,

View File

@ -440,10 +440,11 @@ body #context-menu-container .dropdown-item > span {
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
margin: 4px; margin: 4px;
font-size: var(--monospace-font-size);
} }
body .cm-editor { .cm-scroller {
font-size: var(--monospace-font-size); font-family: var(--monospace-font-family) !important;
} }
body .cm-editor .cm-gutters { body .cm-editor .cm-gutters {

View File

@ -233,6 +233,8 @@
"move_success_message": "Selected notes have been moved into " "move_success_message": "Selected notes have been moved into "
}, },
"note_type_chooser": { "note_type_chooser": {
"change_path_prompt": "Change where to create the new note:",
"search_placeholder": "search path by name (default if empty)",
"modal_title": "Choose note type", "modal_title": "Choose note type",
"close": "Close", "close": "Close",
"modal_body": "Choose note type / template of the new note:", "modal_body": "Choose note type / template of the new note:",

View File

@ -11,6 +11,7 @@ import utils from "../../services/utils.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js"; import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="attr-detail tn-tool-dialog"> <div class="attr-detail tn-tool-dialog">
@ -483,7 +484,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
return; return;
} }
utils.saveFocusedElement(); saveFocusedElement();
this.attrType = this.getAttrType(attribute); this.attrType = this.getAttrType(attribute);
@ -605,7 +606,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.hide(); this.hide();
utils.focusSavedElement(); focusSavedElement();
} }
async cancelAndClose() { async cancelAndClose() {
@ -613,7 +614,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.hide(); this.hide();
utils.focusSavedElement(); focusSavedElement();
} }
userEditedAttribute() { userEditedAttribute() {

View File

@ -4,6 +4,7 @@ import BasicWidget from "../basic_widget.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { openDialog } from "../../services/dialog.js";
interface AppInfo { interface AppInfo {
appVersion: string; appVersion: string;
@ -111,6 +112,6 @@ export default class AboutDialog extends BasicWidget {
async openAboutDialogEvent() { async openAboutDialogEvent() {
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
} }

View File

@ -1,11 +1,11 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import type { Suggestion } from "../../services/note_autocomplete.js"; import type { Suggestion } from "../../services/note_autocomplete.js";
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js"; import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -111,7 +111,7 @@ export default class AddLinkDialog extends BasicWidget {
this.updateTitleSettingsVisibility(); this.updateTitleSettingsVisibility();
await utils.openDialog(this.$widget); await openDialog(this.$widget);
this.$autoComplete.val(""); this.$autoComplete.val("");
this.$linkTitle.val(""); this.$linkTitle.val("");

View File

@ -2,11 +2,11 @@ import treeService from "../../services/tree.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
@ -93,7 +93,7 @@ export default class BranchPrefixDialog extends BasicWidget {
} }
await this.refresh(notePath); await this.refresh(notePath);
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
async savePrefix() { async savePrefix() {

View File

@ -1,11 +1,11 @@
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import bulkActionService from "../../services/bulk_action.js"; import bulkActionService from "../../services/bulk_action.js";
import utils from "../../services/utils.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
@ -104,7 +104,7 @@ export default class BulkActionsDialog extends BasicWidget {
}); });
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000); toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
utils.closeActiveDialog(); closeActiveDialog();
}); });
} }
@ -170,6 +170,6 @@ export default class BulkActionsDialog extends BasicWidget {
this.$includeDescendants.prop("checked", false); this.$includeDescendants.prop("checked", false);
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
} }

View File

@ -1,5 +1,4 @@
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
@ -8,6 +7,7 @@ import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
@ -94,7 +94,7 @@ export default class CloneToDialog extends BasicWidget {
} }
} }
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus"); this.$noteAutoComplete.val("").trigger("focus");
this.$noteList.empty(); this.$noteList.empty();

View File

@ -1,10 +1,10 @@
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { FAttributeRow } from "../../entities/fattribute.js"; import type { FAttributeRow } from "../../entities/fattribute.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
// TODO: Use common with server. // TODO: Use common with server.
interface Response { interface Response {
@ -119,13 +119,13 @@ export default class DeleteNotesDialog extends BasicWidget {
this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus")); this.$widget.on("shown.bs.modal", () => this.$okButton.trigger("focus"));
this.$cancelButton.on("click", () => { this.$cancelButton.on("click", () => {
utils.closeActiveDialog(); closeActiveDialog();
this.resolve({ proceed: false }); this.resolve({ proceed: false });
}); });
this.$okButton.on("click", () => { this.$okButton.on("click", () => {
utils.closeActiveDialog(); closeActiveDialog();
this.resolve({ this.resolve({
proceed: true, proceed: true,
@ -179,7 +179,7 @@ export default class DeleteNotesDialog extends BasicWidget {
await this.renderDeletePreview(); await this.renderDeletePreview();
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones); this.$deleteAllClones.prop("checked", !!forceDeleteAllClones).prop("disabled", !!forceDeleteAllClones);

View File

@ -8,6 +8,7 @@ import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="export-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -214,7 +215,7 @@ export default class ExportDialog extends BasicWidget {
this.$widget.find(".opml-v2").prop("checked", true); // setting default this.$widget.find(".opml-v2").prop("checked", true); // setting default
utils.openDialog(this.$widget); openDialog(this.$widget);
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);

View File

@ -1,6 +1,6 @@
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog"> <div class="help-dialog modal use-tn-links" tabindex="-1" role="dialog">
@ -155,6 +155,6 @@ export default class HelpDialog extends BasicWidget {
} }
showCheatsheetEvent() { showCheatsheetEvent() {
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
} }

View File

@ -1,4 +1,4 @@
import utils, { escapeQuotes } from "../../services/utils.js"; import { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import importService, { type UploadFilesOptions } from "../../services/import.js"; import importService, { type UploadFilesOptions } from "../../services/import.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
@ -6,6 +6,7 @@ import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { Modal, Tooltip } from "bootstrap"; import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -155,7 +156,7 @@ export default class ImportDialog extends BasicWidget {
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId)); this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
async importIntoNote(parentNoteId: string) { async importIntoNote(parentNoteId: string) {

View File

@ -1,12 +1,12 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import type EditableTextTypeWidget from "../type_widgets/editable_text.js"; import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -83,7 +83,7 @@ export default class IncludeNoteDialog extends BasicWidget {
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) { async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
this.textTypeWidget = textTypeWidget; this.textTypeWidget = textTypeWidget;
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
} }

View File

@ -1,9 +1,9 @@
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import type { ConfirmDialogCallback } from "./confirm.js"; import type { ConfirmDialogCallback } from "./confirm.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;"> <div class="info-dialog modal mx-auto" tabindex="-1" role="dialog" style="z-index: 2000;">
@ -72,7 +72,7 @@ export default class InfoDialog extends BasicWidget {
} }
utils.openDialog(this.$widget); openDialog(this.$widget);
this.resolve = callback; this.resolve = callback;
} }

View File

@ -5,6 +5,7 @@ import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
@ -54,7 +55,7 @@ export default class JumpToNoteDialog extends BasicWidget {
} }
async jumpToNoteEvent() { async jumpToNoteEvent() {
const dialogPromise = utils.openDialog(this.$widget); const dialogPromise = openDialog(this.$widget);
if (utils.isMobile()) { if (utils.isMobile()) {
dialogPromise.then(($dialog) => { dialogPromise.then(($dialog) => {
const el = $dialog.find(">.modal-dialog")[0]; const el = $dialog.find(">.modal-dialog")[0];

View File

@ -6,6 +6,7 @@ import BasicWidget from "../basic_widget.js";
import shortcutService from "../../services/shortcuts.js"; import shortcutService from "../../services/shortcuts.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="markdown-import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -89,7 +90,7 @@ export default class MarkdownImportDialog extends BasicWidget {
this.convertMarkdownToHtml(text); this.convertMarkdownToHtml(text);
} else { } else {
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
} }

View File

@ -1,5 +1,4 @@
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import branchService from "../../services/branches.js"; import branchService from "../../services/branches.js";
@ -7,6 +6,7 @@ import treeService from "../../services/tree.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -83,7 +83,7 @@ export default class MoveToDialog extends BasicWidget {
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) { async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
this.movedBranchIds = branchIds; this.movedBranchIds = branchIds;
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus"); this.$noteAutoComplete.val("").trigger("focus");

View File

@ -2,6 +2,7 @@ import type { CommandNames } from "../../components/app_context.js";
import type { MenuCommandItem } from "../../menus/context_menu.js"; import type { MenuCommandItem } from "../../menus/context_menu.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import noteTypesService from "../../services/note_types.js"; import noteTypesService from "../../services/note_types.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Dropdown, Modal } from "bootstrap"; import { Dropdown, Modal } from "bootstrap";
@ -13,6 +14,11 @@ const TPL = /*html*/`
z-index: 1100 !important; z-index: 1100 !important;
} }
.note-type-chooser-dialog .input-group {
margin-top: 15px;
margin-bottom: 15px;
}
.note-type-chooser-dialog .note-type-dropdown { .note-type-chooser-dialog .note-type-dropdown {
position: relative; position: relative;
font-size: large; font-size: large;
@ -30,6 +36,12 @@ const TPL = /*html*/`
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("note_type_chooser.close")}"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
${t("note_type_chooser.change_path_prompt")}
<div class="input-group">
<input class="choose-note-path form-control" placeholder="${t("note_type_chooser.search_placeholder")}">
</div>
${t("note_type_chooser.modal_body")} ${t("note_type_chooser.modal_body")}
<div class="dropdown" style="display: flex;"> <div class="dropdown" style="display: flex;">
@ -48,6 +60,7 @@ export interface ChooseNoteTypeResponse {
success: boolean; success: boolean;
noteType?: string; noteType?: string;
templateNoteId?: string; templateNoteId?: string;
notePath?: string;
} }
type Callback = (data: ChooseNoteTypeResponse) => void; type Callback = (data: ChooseNoteTypeResponse) => void;
@ -57,6 +70,7 @@ export default class NoteTypeChooserDialog extends BasicWidget {
private dropdown!: Dropdown; private dropdown!: Dropdown;
private modal!: Modal; private modal!: Modal;
private $noteTypeDropdown!: JQuery<HTMLElement>; private $noteTypeDropdown!: JQuery<HTMLElement>;
private $autoComplete!: JQuery<HTMLElement>;
private $originalFocused: JQuery<HTMLElement> | null; private $originalFocused: JQuery<HTMLElement> | null;
private $originalDialog: JQuery<HTMLElement> | null; private $originalDialog: JQuery<HTMLElement> | null;
@ -71,7 +85,8 @@ export default class NoteTypeChooserDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget[0]); this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$autoComplete = this.$widget.find(".choose-note-path");
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown"); this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]); this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger")[0]);
@ -116,9 +131,20 @@ export default class NoteTypeChooserDialog extends BasicWidget {
}); });
} }
async refresh() {
noteAutocompleteService
.initNoteAutocomplete(this.$autoComplete, {
allowCreatingNotes: false,
hideGoToSelectedNoteButton: true,
allowJumpToSearchNotes: false,
})
}
async chooseNoteTypeEvent({ callback }: { callback: Callback }) { async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
this.$originalFocused = $(":focus"); this.$originalFocused = $(":focus");
await this.refresh();
const noteTypes = await noteTypesService.getNoteTypeItems(); const noteTypes = await noteTypesService.getNoteTypeItems();
this.$noteTypeDropdown.empty(); this.$noteTypeDropdown.empty();
@ -153,12 +179,14 @@ export default class NoteTypeChooserDialog extends BasicWidget {
const $item = $(e.target).closest(".dropdown-item"); const $item = $(e.target).closest(".dropdown-item");
const noteType = $item.attr("data-note-type"); const noteType = $item.attr("data-note-type");
const templateNoteId = $item.attr("data-template-note-id"); const templateNoteId = $item.attr("data-template-note-id");
const notePath = this.$autoComplete.getSelectedNotePath() || undefined;
if (this.resolve) { if (this.resolve) {
this.resolve({ this.resolve({
success: true, success: true,
noteType, noteType,
templateNoteId templateNoteId,
notePath
}); });
} }
this.resolve = null; this.resolve = null;

View File

@ -1,5 +1,5 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -37,6 +37,6 @@ export default class PasswordNoteSetDialog extends BasicWidget {
} }
showPasswordNotSetEvent() { showPasswordNotSetEvent() {
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
} }

View File

@ -1,5 +1,5 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -110,6 +110,6 @@ export default class PromptDialog extends BasicWidget {
this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer)); this.$dialogBody.empty().append($("<div>").addClass("form-group").append(this.$question).append(this.$answer));
utils.openDialog(this.$widget, false); openDialog(this.$widget, false);
} }
} }

View File

@ -1,6 +1,6 @@
import { openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -49,7 +49,7 @@ export default class ProtectedSessionPasswordDialog extends BasicWidget {
} }
showProtectedSessionPasswordDialogEvent() { showProtectedSessionPasswordDialogEvent() {
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$passwordInput.trigger("focus"); this.$passwordInput.trigger("focus");
} }

View File

@ -2,13 +2,12 @@ import { formatDateTime } from "../../utils/formatters.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import appContext, { type EventData } from "../../components/app_context.js"; import appContext, { type EventData } from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import dialogService from "../../services/dialog.js"; import dialogService, { openDialog } from "../../services/dialog.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import hoistedNoteService from "../../services/hoisted_note.js"; import hoistedNoteService from "../../services/hoisted_note.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
import ws from "../../services/ws.js"; import ws from "../../services/ws.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
@ -62,7 +61,7 @@ export default class RecentChangesDialog extends BasicWidget {
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
async refresh() { async refresh() {

View File

@ -6,7 +6,7 @@ import appContext from "../../components/app_context.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import protectedSessionHolder from "../../services/protected_session_holder.js"; import protectedSessionHolder from "../../services/protected_session_holder.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import dialogService from "../../services/dialog.js"; import dialogService, { openDialog } from "../../services/dialog.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js"; import type { NoteType } from "../../entities/fnote.js";
@ -182,7 +182,7 @@ export default class RevisionsDialog extends BasicWidget {
return; return;
} }
utils.openDialog(this.$widget); openDialog(this.$widget);
await this.loadRevisions(noteId); await this.loadRevisions(noteId);
} }

View File

@ -1,7 +1,7 @@
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { closeActiveDialog, openDialog } from "../../services/dialog.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog"> const TPL = /*html*/`<div class="sort-child-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -97,14 +97,14 @@ export default class SortChildNotesDialog extends BasicWidget {
await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale }); await server.put(`notes/${this.parentNoteId}/sort-children`, { sortBy, sortDirection, foldersFirst, sortNatural, sortLocale });
utils.closeActiveDialog(); closeActiveDialog();
}); });
} }
async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) { async sortChildNotesEvent({ node }: EventData<"sortChildNotes">) {
this.parentNoteId = node.data.noteId; this.parentNoteId = node.data.noteId;
utils.openDialog(this.$widget); openDialog(this.$widget);
this.$form.find("input:first").focus(); this.$form.find("input:first").focus();
} }

View File

@ -1,11 +1,12 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import utils, { escapeQuotes } from "../../services/utils.js"; import { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import importService from "../../services/import.js"; import importService from "../../services/import.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal, Tooltip } from "bootstrap"; import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import { openDialog } from "../../services/dialog.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="upload-attachments-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -98,7 +99,7 @@ export default class UploadAttachmentsDialog extends BasicWidget {
this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId)); this.$noteTitle.text(await treeService.getNoteTitle(this.parentNoteId));
utils.openDialog(this.$widget); openDialog(this.$widget);
} }
async uploadAttachments(parentNoteId: string) { async uploadAttachments(parentNoteId: string) {

View File

@ -52,9 +52,9 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
toastService.showMessage(t("code_buttons.opening_api_docs_message")); toastService.showMessage(t("code_buttons.opening_api_docs_message"));
if (this.note?.mime.endsWith("frontend")) { if (this.note?.mime.endsWith("frontend")) {
window.open("https://zadam.github.io/trilium/frontend_api/FrontendScriptApi.html", "_blank"); window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html", "_blank");
} else { } else {
window.open("https://zadam.github.io/trilium/backend_api/BackendScriptApi.html", "_blank"); window.open("https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html", "_blank");
} }
}); });

View File

@ -56,6 +56,8 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
return byNoteType[note.type]; return byNoteType[note.type];
} else if (note?.hasLabel("calendarRoot")) { } else if (note?.hasLabel("calendarRoot")) {
return "l0tKav7yLHGF"; return "l0tKav7yLHGF";
} else if (note?.hasLabel("textSnippet")) {
return "pwc194wlRzcH";
} else if (note && note.type === "book") { } else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""] return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
} }

View File

@ -4,18 +4,64 @@ import { buildExtraCommands, type EditorConfig } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js"; import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js"; import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js"; import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import utils from "../../../services/utils.js";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url"; import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
import { copyTextWithToast } from "../../../services/clipboard_ext.js"; import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import getTemplates from "./snippets.js"; import getTemplates from "./snippets.js";
import { PREMIUM_PLUGINS } from "../../../../../../packages/ckeditor5/src/plugins.js";
import { t } from "../../../services/i18n.js";
import { getMermaidConfig } from "../../../services/mermaid.js";
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
import mimeTypesService from "../../../services/mime_types.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { buildToolbarConfig } from "./toolbar.js";
const TEXT_FORMATTING_GROUP = { export const OPEN_SOURCE_LICENSE_KEY = "GPL";
label: "Text formatting",
icon: "text"
};
export async function buildConfig(): Promise<EditorConfig> { export interface BuildEditorOptions {
return { forceGplLicense: boolean;
isClassicEditor: boolean;
contentLanguage: string | null;
}
export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfig> {
const licenseKey = (opts.forceGplLicense ? OPEN_SOURCE_LICENSE_KEY : getLicenseKey());
const hasPremiumLicense = (licenseKey !== OPEN_SOURCE_LICENSE_KEY);
const config: EditorConfig = {
licenseKey,
placeholder: t("editable_text.placeholder"),
mention: {
feeds: [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const itemElement = document.createElement("button");
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
}
],
},
codeBlock: {
languages: buildListOfLanguages()
},
math: {
engine: "katex",
outputType: "span", // or script
lazyLoad: async () => {
(window as any).katex = (await import("../../../services/math.js")).default
},
forceOutputType: false, // forces output to use outputType
enablePreview: true // Enable preview view
},
mermaid: {
lazyLoad: async () => (await import("mermaid")).default, // FIXME
config: getMermaidConfig()
},
image: { image: {
styles: { styles: {
options: [ options: [
@ -130,149 +176,59 @@ export async function buildConfig(): Promise<EditorConfig> {
template: { template: {
definitions: await getTemplates() definitions: await getTemplates()
}, },
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags"))
},
// This value must be kept in sync with the language defined in webpack.config.js. // This value must be kept in sync with the language defined in webpack.config.js.
language: "en" language: "en"
}; };
}
export function buildToolbarConfig(isClassicToolbar: boolean) { // Set up content language.
if (utils.isMobile()) { const { contentLanguage } = opts;
return buildMobileToolbar(); if (contentLanguage) {
} else if (isClassicToolbar) { config.language = {
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"; ui: (typeof config.language === "string" ? config.language : "en"),
return buildClassicToolbar(multilineToolbar); content: contentLanguage
} else {
return buildFloatingToolbar();
}
}
export function buildMobileToolbar() {
const classicConfig = buildClassicToolbar(false);
const items: string[] = [];
for (const item of classicConfig.toolbar.items) {
if (typeof item === "object" && "items" in item) {
for (const subitem of item.items) {
items.push(subitem);
}
} else {
items.push(item);
} }
} }
// Enable premium plugins.
if (hasPremiumLicense) {
config.extraPlugins = [
...PREMIUM_PLUGINS
];
}
return { return {
...classicConfig, ...config,
toolbar: { ...buildToolbarConfig(opts.isClassicEditor)
...classicConfig.toolbar,
items
}
}; };
} }
export function buildClassicToolbar(multilineToolbar: boolean) { function buildListOfLanguages() {
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars. const userLanguages = mimeTypesService
return { .getMimeTypes()
toolbar: { .filter((mt) => mt.enabled)
items: [ .map((mt) => ({
"heading", language: normalizeMimeTypeForCKEditor(mt.mime),
"fontSize", label: mt.title
"|", }));
"bold",
"italic",
{
...TEXT_FORMATTING_GROUP,
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
},
"|",
"fontColor",
"fontBackgroundColor",
"removeFormat",
"|",
"bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"admonition",
"insertTable",
"|",
"code",
"codeBlock",
"|",
"footnote",
{
label: "Insert",
icon: "plus",
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
},
"|",
"alignment",
"outdent",
"indent",
"|",
"insertTemplate",
"markdownImport",
"cuttonote",
"findAndReplace"
],
shouldNotGroupWhenFull: multilineToolbar
}
};
}
export function buildFloatingToolbar() { return [
return { {
toolbar: { language: mimeTypesService.MIME_TYPE_AUTO,
items: [ label: t("editable-text.auto-detect-language")
"fontSize",
"bold",
"italic",
"underline",
{
...TEXT_FORMATTING_GROUP,
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
},
"|",
"fontColor",
"fontBackgroundColor",
"|",
"code",
"link",
"bookmark",
"removeFormat",
"internallink",
"cuttonote"
]
}, },
...userLanguages
blockToolbar: [ ];
"heading", }
"|",
"bulletedList", function getLicenseKey() {
"numberedList", const premiumLicenseKey = import.meta.env.VITE_CKEDITOR_KEY;
"todoList", if (!premiumLicenseKey) {
"|", logError("CKEditor license key is not set, premium features will not be available.");
"blockQuote", return OPEN_SOURCE_LICENSE_KEY;
"admonition", }
"codeBlock",
"insertTable", return premiumLicenseKey;
"footnote",
{
label: "Insert",
icon: "plus",
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
},
"|",
"alignment",
"outdent",
"indent",
"|",
"insertTemplate",
"imageUpload",
"markdownImport",
"specialCharacters",
"emoji",
"findAndReplace"
]
};
} }

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildClassicToolbar, buildFloatingToolbar } from "./config.js"; import { buildClassicToolbar, buildFloatingToolbar } from "./toolbar.js";
type ToolbarConfig = string | "|" | { items: ToolbarConfig[] }; type ToolbarConfig = string | "|" | { items: ToolbarConfig[] };

View File

@ -0,0 +1,149 @@
import utils from "../../../services/utils.js";
import options from "../../../services/options.js";
const TEXT_FORMATTING_GROUP = {
label: "Text formatting",
icon: "text"
};
export function buildToolbarConfig(isClassicToolbar: boolean) {
if (utils.isMobile()) {
return buildMobileToolbar();
} else if (isClassicToolbar) {
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
return buildClassicToolbar(multilineToolbar);
} else {
return buildFloatingToolbar();
}
}
export function buildMobileToolbar() {
const classicConfig = buildClassicToolbar(false);
const items: string[] = [];
for (const item of classicConfig.toolbar.items) {
if (typeof item === "object" && "items" in item) {
for (const subitem of item.items) {
items.push(subitem);
}
} else {
items.push(item);
}
}
return {
...classicConfig,
toolbar: {
...classicConfig.toolbar,
items
}
};
}
export function buildClassicToolbar(multilineToolbar: boolean) {
// For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars.
return {
toolbar: {
items: [
"heading",
"fontSize",
"|",
"bold",
"italic",
{
...TEXT_FORMATTING_GROUP,
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
},
"|",
"fontColor",
"fontBackgroundColor",
"removeFormat",
"|",
"bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"admonition",
"insertTable",
"|",
"code",
"codeBlock",
"|",
"footnote",
{
label: "Insert",
icon: "plus",
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
},
"|",
"alignment",
"outdent",
"indent",
"|",
"insertTemplate",
"markdownImport",
"cuttonote",
"findAndReplace"
],
shouldNotGroupWhenFull: multilineToolbar
}
};
}
export function buildFloatingToolbar() {
return {
toolbar: {
items: [
"fontSize",
"bold",
"italic",
"underline",
{
...TEXT_FORMATTING_GROUP,
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
},
"|",
"fontColor",
"fontBackgroundColor",
"|",
"code",
"link",
"bookmark",
"removeFormat",
"internallink",
"cuttonote"
]
},
blockToolbar: [
"heading",
"|",
"bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"admonition",
"codeBlock",
"insertTable",
"footnote",
{
label: "Insert",
icon: "plus",
items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"]
},
"|",
"alignment",
"outdent",
"indent",
"|",
"insertTemplate",
"imageUpload",
"markdownImport",
"specialCharacters",
"emoji",
"findAndReplace"
]
};
}

View File

@ -1,6 +1,3 @@
import { t } from "../../services/i18n.js";
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
import mimeTypesService from "../../services/mime_types.js";
import utils, { hasTouchBar } from "../../services/utils.js"; import utils, { hasTouchBar } from "../../services/utils.js";
import keyboardActionService from "../../services/keyboard_actions.js"; import keyboardActionService from "../../services/keyboard_actions.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
@ -12,29 +9,12 @@ import dialogService from "../../services/dialog.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import toast from "../../services/toast.js"; import toast from "../../services/toast.js";
import { buildSelectedBackgroundColor } from "../../components/touch_bar.js"; import { buildSelectedBackgroundColor } from "../../components/touch_bar.js";
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js"; import { buildConfig, BuildEditorOptions, OPEN_SOURCE_LICENSE_KEY } from "./ckeditor/config.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import { getMermaidConfig } from "../../services/mermaid.js"; import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
import "@triliumnext/ckeditor5/index.css"; import "@triliumnext/ckeditor5/index.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { updateTemplateCache } from "./ckeditor/snippets.js"; import { updateTemplateCache } from "./ckeditor/snippets.js";
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const itemElement = document.createElement("button");
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
}
];
const TPL = /*html*/` const TPL = /*html*/`
<div class="note-detail-editable-text note-detail-printable"> <div class="note-detail-editable-text note-detail-printable">
<style> <style>
@ -97,24 +77,6 @@ const TPL = /*html*/`
</div> </div>
`; `;
function buildListOfLanguages() {
const userLanguages = mimeTypesService
.getMimeTypes()
.filter((mt) => mt.enabled)
.map((mt) => ({
language: normalizeMimeTypeForCKEditor(mt.mime),
label: mt.title
}));
return [
{
language: mimeTypesService.MIME_TYPE_AUTO,
label: t("editable-text.auto-detect-language")
},
...userLanguages
];
}
/** /**
* The editor can operate into two distinct modes: * The editor can operate into two distinct modes:
* *
@ -147,7 +109,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async initEditor() { async initEditor() {
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic"; const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
// CKEditor since version 12 needs the element to be visible before initialization. At the same time, // CKEditor since version 12 needs the element to be visible before initialization. At the same time,
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate // we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
@ -192,34 +153,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.setCreator(async (_, editorConfig) => { this.watchdog.setCreator(async (_, editorConfig) => {
logInfo("Creating new CKEditor"); logInfo("Creating new CKEditor");
const finalConfig = {
...editorConfig,
...(await buildConfig()),
...buildToolbarConfig(isClassicEditor),
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
styles: true,
classes: true,
attributes: true
},
licenseKey: getLicenseKey()
};
const contentLanguage = this.note?.getLabelValue("language"); const contentLanguage = this.note?.getLabelValue("language");
if (contentLanguage) { this.contentLanguage = contentLanguage ?? null;
// TODO: Wrong type?
//@ts-ignore
finalConfig.language = {
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
content: contentLanguage
}
this.contentLanguage = contentLanguage;
} else {
this.contentLanguage = null;
}
//@ts-ignore const opts: BuildEditorOptions = {
const editor = await editorClass.create(this.$editor[0], finalConfig); contentLanguage: this.contentLanguage,
forceGplLicense: false,
isClassicEditor
};
const editor = await buildEditor(this.$editor[0], isClassicEditor, opts);
const notificationsPlugin = editor.plugins.get("Notification"); const notificationsPlugin = editor.plugins.get("Notification");
notificationsPlugin.on("show:warning", (evt, data) => { notificationsPlugin.on("show:warning", (evt, data) => {
@ -296,28 +238,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
async createEditor() { async createEditor() {
await this.watchdog.create(this.$editor[0], { await this.watchdog.create(this.$editor[0]);
placeholder: t("editable_text.placeholder"),
mention: {
feeds: mentionSetup,
},
codeBlock: {
languages: buildListOfLanguages()
},
math: {
engine: "katex",
outputType: "span", // or script
lazyLoad: async () => {
(window as any).katex = (await import("../../services/math.js")).default
},
forceOutputType: false, // forces output to use outputType
enablePreview: true // Enable preview view
},
mermaid: {
lazyLoad: async () => (await import("mermaid")).default, // FIXME
config: getMermaidConfig()
}
});
} }
async doRefresh(note: FNote) { async doRefresh(note: FNote) {
@ -656,12 +577,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
function getLicenseKey() { async function buildEditor(element: HTMLElement, isClassicEditor: boolean, opts: BuildEditorOptions) {
const premiumLicenseKey = import.meta.env.VITE_CKEDITOR_KEY; const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
if (!premiumLicenseKey) { let config = await buildConfig(opts);
logError("CKEditor license key is not set, premium features will not be available."); let editor = await editorClass.create(element, config);
return "GPL";
} if (editor.isReadOnly) {
editor.destroy();
opts.forceGplLicense = true;
config = await buildConfig(opts);
editor = await editorClass.create(element, config);
}
return editor;
return premiumLicenseKey;
} }

View File

@ -3,7 +3,6 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/vitest", "outDir": "./out-tsc/vitest",
"types": [ "types": [
"vitest/globals",
"vitest/importMeta", "vitest/importMeta",
"vite/client", "vite/client",
"node", "node",

View File

@ -97,6 +97,12 @@ export default defineConfig(() => ({
} }
} }
}, },
test: {
environment: "happy-dom",
setupFiles: [
"./src/test/setup.ts"
]
},
optimizeDeps: { optimizeDeps: {
exclude: [ exclude: [
"@triliumnext/highlightjs" "@triliumnext/highlightjs"

View File

@ -1,11 +1,11 @@
const path = require("path"); import path from "path";
const fs = require("fs-extra"); import fs from "fs-extra";
const { LOCALES } = require("@triliumnext/commons"); import { LOCALES } from "@triliumnext/commons";
import { PRODUCT_NAME } from "../src/app-info.js";
const ELECTRON_FORGE_DIR = __dirname; const ELECTRON_FORGE_DIR = __dirname;
const EXECUTABLE_NAME = "trilium"; // keep in sync with server's package.json -> packagerConfig.executableName const EXECUTABLE_NAME = "trilium"; // keep in sync with server's package.json -> packagerConfig.executableName
const PRODUCT_NAME = "TriliumNext Notes";
const APP_ICON_PATH = path.join(ELECTRON_FORGE_DIR, "app-icon"); const APP_ICON_PATH = path.join(ELECTRON_FORGE_DIR, "app-icon");
const extraResourcesForPlatform = getExtraResourcesForPlatform(); const extraResourcesForPlatform = getExtraResourcesForPlatform();
@ -147,13 +147,13 @@ module.exports = {
const isMac = (process.platform === "darwin"); const isMac = (process.platform === "darwin");
let localesToKeep = LOCALES let localesToKeep = LOCALES
.filter(locale => !locale.contentOnly) .filter(locale => !locale.contentOnly)
.map(locale => locale.electronLocale); .map(locale => locale.electronLocale) as string[];
if (!isMac) { if (!isMac) {
localesToKeep = localesToKeep.map(locale => locale.replace("_", "-")) localesToKeep = localesToKeep.map(locale => locale.replace("_", "-"))
} }
const keptLocales = new Set(); const keptLocales = new Set();
const removedLocales = []; const removedLocales: string[] = [];
const extension = (isMac ? ".lproj" : ".pak"); const extension = (isMac ? ".lproj" : ".pak");
for (const outputPath of packageResult.outputPaths) { for (const outputPath of packageResult.outputPaths) {
@ -169,39 +169,39 @@ module.exports = {
console.log(`No locales directory found in '${localeDir}'.`); console.log(`No locales directory found in '${localeDir}'.`);
process.exit(2); process.exit(2);
} }
const files = fs.readdirSync(localeDir); const files = fs.readdirSync(localeDir);
for (const file of files) { for (const file of files) {
if (!file.endsWith(extension)) { if (!file.endsWith(extension)) {
continue; continue;
} }
let localeName = path.basename(file, extension); let localeName = path.basename(file, extension);
if (localeName === "en-US" && !isMac) { if (localeName === "en-US" && !isMac) {
// If the locale is "en-US" on Windows, we treat it as "en". // If the locale is "en-US" on Windows, we treat it as "en".
// This is because the Windows version of Electron uses "en-US.pak" instead of "en.pak". // This is because the Windows version of Electron uses "en-US.pak" instead of "en.pak".
localeName = "en"; localeName = "en";
} }
if (localesToKeep.includes(localeName)) { if (localesToKeep.includes(localeName)) {
keptLocales.add(localeName); keptLocales.add(localeName);
continue; continue;
} }
const filePath = path.join(localeDir, file); const filePath = path.join(localeDir, file);
if (isMac) { if (isMac) {
fs.rm(filePath, { recursive: true }); fs.rm(filePath, { recursive: true });
} else { } else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
removedLocales.push(file); removedLocales.push(file);
} }
} }
} }
console.log(`Removed unused locale files: ${removedLocales.join(", ")}`); console.log(`Removed unused locale files: ${removedLocales.join(", ")}`);
// Ensure all locales that should be kept are actually present. // Ensure all locales that should be kept are actually present.
for (const locale of localesToKeep) { for (const locale of localesToKeep) {
@ -229,7 +229,7 @@ module.exports = {
if (TRILIUM_ARTIFACT_NAME_HINT) { if (TRILIUM_ARTIFACT_NAME_HINT) {
fileName = TRILIUM_ARTIFACT_NAME_HINT.replaceAll("/", "-") + extension; fileName = TRILIUM_ARTIFACT_NAME_HINT.replaceAll("/", "-") + extension;
} }
const outputPath = path.join(outputDir, fileName); const outputPath = path.join(outputDir, fileName);
console.log(`[Artifact] ${artifactPath} -> ${outputPath}`); console.log(`[Artifact] ${artifactPath} -> ${outputPath}`);
fs.copyFileSync(artifactPath, outputPath); fs.copyFileSync(artifactPath, outputPath);
@ -240,7 +240,7 @@ module.exports = {
}; };
function getExtraResourcesForPlatform() { function getExtraResourcesForPlatform() {
const resources = []; const resources: string[] = [];
const getScriptResources = () => { const getScriptResources = () => {
const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"]; const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];

View File

@ -17,7 +17,7 @@
"@types/electron-squirrel-startup": "1.0.2", "@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/server": "workspace:*", "@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"electron": "36.4.0", "electron": "36.5.0",
"@electron-forge/cli": "7.8.1", "@electron-forge/cli": "7.8.1",
"@electron-forge/maker-deb": "7.8.1", "@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-dmg": "7.8.1", "@electron-forge/maker-dmg": "7.8.1",
@ -29,7 +29,7 @@
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"
}, },
"config": { "config": {
"forge": "./electron-forge/forge.config.cjs" "forge": "./electron-forge/forge.config.ts"
}, },
"scripts": { "scripts": {
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js" "start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"

View File

@ -0,0 +1,4 @@
/**
* The Electron product name (can be used for the window WMClass or passed down to the Electron packager).
*/
export const PRODUCT_NAME = "TriliumNext Notes";

View File

@ -8,6 +8,7 @@ import options from "@triliumnext/server/src/services/options.js";
import electronDebug from "electron-debug"; import electronDebug from "electron-debug";
import electronDl from "electron-dl"; import electronDl from "electron-dl";
import { deferred } from "@triliumnext/server/src/services/utils.js"; import { deferred } from "@triliumnext/server/src/services/utils.js";
import { PRODUCT_NAME } from "./app-info";
async function main() { async function main() {
const serverInitializedPromise = deferred<void>(); const serverInitializedPromise = deferred<void>();
@ -28,6 +29,7 @@ async function main() {
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions. // Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
// See https://github.com/electron/electron/issues/46538 for more info. // See https://github.com/electron/electron/issues/46538 for more info.
if (process.platform === "linux") { if (process.platform === "linux") {
electron.app.setName(PRODUCT_NAME);
electron.app.commandLine.appendSwitch("gtk-version", "3"); electron.app.commandLine.appendSwitch("gtk-version", "3");
} }

View File

@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2020",
"outDir": "dist",
"types": [
"node",
"express"
],
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
},
"include": [
"**/*.ts",
"../server/src/*.d.ts"
],
"exclude": [
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
]
}

View File

@ -11,6 +11,9 @@
}, },
{ {
"path": "./tsconfig.app.json" "path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.forge.json"
} }
] ]
} }

View File

@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*", "@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"electron": "36.4.0", "electron": "36.5.0",
"fs-extra": "11.3.0" "fs-extra": "11.3.0"
}, },
"nx": { "nx": {

View File

@ -59,7 +59,7 @@
"debounce": "2.2.0", "debounce": "2.2.0",
"debug": "4.4.1", "debug": "4.4.1",
"ejs": "3.1.10", "ejs": "3.1.10",
"electron": "36.4.0", "electron": "36.5.0",
"electron-debug": "4.1.0", "electron-debug": "4.1.0",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "1.0.3", "escape-html": "1.0.3",

View File

@ -5,7 +5,7 @@ import cls from "../services/cls.js";
import sql from "../services/sql.js"; import sql from "../services/sql.js";
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import type { Request, Response, Router } from "express"; import type { Request, Response, Router } from "express";
import { safeExtractMessageAndStackFromError } from "../services/utils.js"; import { safeExtractMessageAndStackFromError, normalizeCustomHandlerPattern } from "../services/utils.js";
function handleRequest(req: Request, res: Response) { function handleRequest(req: Request, res: Response) {
@ -38,11 +38,19 @@ function handleRequest(req: Request, res: Response) {
continue; continue;
} }
const regex = new RegExp(`^${attr.value}$`); // Get normalized patterns to handle both trailing slash cases
let match; const patterns = normalizeCustomHandlerPattern(attr.value);
let match: RegExpMatchArray | null = null;
try { try {
match = path.match(regex); // Try each pattern until we find a match
for (const pattern of patterns) {
const regex = new RegExp(`^${pattern}$`);
match = path.match(regex);
if (match) {
break; // Found a match, exit pattern loop
}
}
} catch (e: unknown) { } catch (e: unknown) {
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`); log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`);

View File

@ -2,6 +2,7 @@
import optionService from "./options.js"; import optionService from "./options.js";
import config from "./config.js"; import config from "./config.js";
import { normalizeUrl } from "./utils.js";
/* /*
* Primary configuration for sync is in the options (document), but we allow to override * Primary configuration for sync is in the options (document), but we allow to override
@ -17,7 +18,10 @@ function get(name: keyof typeof config.Sync) {
export default { export default {
// env variable is the easiest way to guarantee we won't overwrite prod data during development // env variable is the easiest way to guarantee we won't overwrite prod data during development
// after copying prod document/data directory // after copying prod document/data directory
getSyncServerHost: () => get("syncServerHost"), getSyncServerHost: () => {
const host = get("syncServerHost");
return host ? normalizeUrl(host) : host;
},
isSyncSetup: () => { isSyncSetup: () => {
const syncServerHost = get("syncServerHost"); const syncServerHost = get("syncServerHost");

View File

@ -628,3 +628,56 @@ describe("#formatDownloadTitle", () => {
}); });
}); });
}); });
describe("#normalizeUrl", () => {
const testCases: TestCase<typeof utils.normalizeUrl>[] = [
[ "should remove trailing slash from simple URL", [ "https://example.com/" ], "https://example.com" ],
[ "should remove trailing slash from URL with path", [ "https://example.com/path/" ], "https://example.com/path" ],
[ "should preserve URL without trailing slash", [ "https://example.com" ], "https://example.com" ],
[ "should preserve URL without trailing slash with path", [ "https://example.com/path" ], "https://example.com/path" ],
[ "should preserve protocol-only URLs", [ "https://" ], "https://" ],
[ "should preserve protocol-only URLs", [ "http://" ], "http://" ],
[ "should fix double slashes in path", [ "https://example.com//api//test" ], "https://example.com/api/test" ],
[ "should handle multiple double slashes", [ "https://example.com///api///test" ], "https://example.com/api/test" ],
[ "should handle trailing slash with double slashes", [ "https://example.com//api//" ], "https://example.com/api" ],
[ "should preserve protocol double slash", [ "https://example.com/api" ], "https://example.com/api" ],
[ "should handle empty string", [ "" ], "" ],
[ "should handle whitespace-only string", [ " " ], "" ],
[ "should trim whitespace", [ " https://example.com/ " ], "https://example.com" ],
[ "should handle null as empty", [ null as any ], null ],
[ "should handle undefined as empty", [ undefined as any ], undefined ]
];
testCases.forEach((testCase) => {
const [ desc, fnParams, expected ] = testCase;
it(desc, () => {
const result = utils.normalizeUrl(...fnParams);
expect(result).toStrictEqual(expected);
});
});
});
describe("#normalizeCustomHandlerPattern", () => {
const testCases: TestCase<typeof utils.normalizeCustomHandlerPattern>[] = [
[ "should handle pattern without ending - add both versions", [ "foo" ], [ "foo", "foo/" ] ],
[ "should handle pattern with trailing slash - add both versions", [ "foo/" ], [ "foo", "foo/" ] ],
[ "should handle pattern ending with $ - add optional slash", [ "foo$" ], [ "foo/?$" ] ],
[ "should handle pattern with trailing slash and $ - add both versions", [ "foo/$" ], [ "foo$", "foo/$" ] ],
[ "should preserve existing optional slash pattern", [ "foo/?$" ], [ "foo/?$" ] ],
[ "should preserve existing optional slash pattern (alternative)", [ "foo/?)" ], [ "foo/?)" ] ],
[ "should handle regex pattern with special chars", [ "api/[a-z]+$" ], [ "api/[a-z]+/?$" ] ],
[ "should handle complex regex pattern", [ "user/([0-9]+)/profile$" ], [ "user/([0-9]+)/profile/?$" ] ],
[ "should handle empty string", [ "" ], [ "" ] ],
[ "should handle whitespace-only string", [ " " ], [ "" ] ],
[ "should handle null", [ null as any ], [ null ] ],
[ "should handle undefined", [ undefined as any ], [ undefined ] ]
];
testCases.forEach((testCase) => {
const [ desc, fnParams, expected ] = testCase;
it(desc, () => {
const result = utils.normalizeCustomHandlerPattern(...fnParams);
expect(result).toStrictEqual(expected);
});
});
});

View File

@ -375,6 +375,85 @@ export function safeExtractMessageAndStackFromError(err: unknown): [errMessage:
return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const; return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const;
} }
/**
* Normalizes URL by removing trailing slashes and fixing double slashes.
* Preserves the protocol (http://, https://) but removes trailing slashes from the rest.
*
* @param url The URL to normalize
* @returns The normalized URL without trailing slashes
*/
export function normalizeUrl(url: string | null | undefined): string | null | undefined {
if (!url || typeof url !== 'string') {
return url;
}
// Trim whitespace
url = url.trim();
if (!url) {
return url;
}
// Fix double slashes (except in protocol) first
url = url.replace(/([^:]\/)\/+/g, '$1');
// Remove trailing slash, but preserve protocol
if (url.endsWith('/') && !url.match(/^https?:\/\/$/)) {
url = url.slice(0, -1);
}
return url;
}
/**
* Normalizes a path pattern for custom request handlers.
* Ensures both trailing slash and non-trailing slash versions are handled.
*
* @param pattern The original pattern from customRequestHandler attribute
* @returns An array of patterns to match both with and without trailing slash
*/
export function normalizeCustomHandlerPattern(pattern: string | null | undefined): (string | null | undefined)[] {
if (!pattern || typeof pattern !== 'string') {
return [pattern];
}
pattern = pattern.trim();
if (!pattern) {
return [pattern];
}
// If pattern already ends with optional trailing slash, return as-is
if (pattern.endsWith('/?$') || pattern.endsWith('/?)')) {
return [pattern];
}
// If pattern ends with $, handle it specially
if (pattern.endsWith('$')) {
const basePattern = pattern.slice(0, -1);
// If already ends with slash, create both versions
if (basePattern.endsWith('/')) {
const withoutSlash = basePattern.slice(0, -1) + '$';
const withSlash = pattern;
return [withoutSlash, withSlash];
} else {
// Add optional trailing slash
const withSlash = basePattern + '/?$';
return [withSlash];
}
}
// For patterns without $, add both versions
if (pattern.endsWith('/')) {
const withoutSlash = pattern.slice(0, -1);
return [withoutSlash, pattern];
} else {
const withSlash = pattern + '/';
return [pattern, withSlash];
}
}
export default { export default {
compareVersions, compareVersions,
@ -400,6 +479,8 @@ export default {
md5, md5,
newEntityId, newEntityId,
normalize, normalize,
normalizeCustomHandlerPattern,
normalizeUrl,
quoteRegex, quoteRegex,
randomSecureToken, randomSecureToken,
randomString, randomString,

View File

@ -1,38 +1,11 @@
# sv # apps/website
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). Landing page for Trilium Notes powered by [Svelte](https://github.com/sveltejs/cli) and [Tailwind CSS](https://tailwindcss.com/).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing ## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: To run a dev server that will hot-reload changes: `pnpm nx run website:dev`
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building ## Building
To create a production version of your app: To create a production build: `pnpm nx run website:build`
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@ -9,4 +9,6 @@ The license key is stored in the application and it enables the use of the previ
## Can I opt out of these features? ## Can I opt out of these features?
At this moment there is no way to disable this features, apart from manually modifying the source code. If this is a problem, [let us know](../../Troubleshooting/Reporting%20issues.md). At this moment there is no way to disable these features, apart from manually modifying the source code. If this is a problem, [let us know](../../Troubleshooting/Reporting%20issues.md).
If you have the possibility of rebuilding the source code (e.g. if a package maintainer), then modify `VITE_CKEDITOR_KEY` in `apps/client/.env` to be `GPL`.

View File

@ -169,8 +169,6 @@
comment = meta.description; comment = meta.description;
desktopName = "TriliumNext Notes"; desktopName = "TriliumNext Notes";
categories = [ "Office" ]; categories = [ "Office" ];
# TODO: electron-forge build has this set to PRODUCT_NAME (forge.config.cjs)
# But the plain build doesn't set this (or the app icon).
startupWMClass = "TriliumNext Notes"; startupWMClass = "TriliumNext Notes";
}) })
]; ];

View File

@ -27,24 +27,25 @@
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@electron/rebuild": "4.0.1", "@electron/rebuild": "4.0.1",
"@nx/devkit": "21.2.0", "@nx/devkit": "21.2.1",
"@nx/esbuild": "21.2.0", "@nx/esbuild": "21.2.1",
"@nx/eslint": "21.2.0", "@nx/eslint": "21.2.1",
"@nx/eslint-plugin": "21.2.0", "@nx/eslint-plugin": "21.2.1",
"@nx/express": "21.2.0", "@nx/express": "21.2.1",
"@nx/js": "21.2.0", "@nx/js": "21.2.1",
"@nx/node": "21.2.0", "@nx/node": "21.2.1",
"@nx/playwright": "21.2.0", "@nx/playwright": "21.2.1",
"@nx/vite": "21.2.0", "@nx/vite": "21.2.1",
"@nx/web": "21.2.0", "@nx/web": "21.2.1",
"@playwright/test": "^1.36.0", "@playwright/test": "^1.36.0",
"@triliumnext/server": "workspace:*", "@triliumnext/server": "workspace:*",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "22.15.31", "@types/node": "22.15.32",
"@vitest/coverage-v8": "^3.0.5", "@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.0", "@vitest/ui": "^3.0.0",
"chalk": "5.4.1", "chalk": "5.4.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dpdm": "3.14.0",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^9.8.0", "eslint": "^9.8.0",
"eslint-config-prettier": "^10.0.0", "eslint-config-prettier": "^10.0.0",
@ -53,7 +54,7 @@
"jiti": "2.4.2", "jiti": "2.4.2",
"jsdom": "~26.1.0", "jsdom": "~26.1.0",
"jsonc-eslint-parser": "^2.1.0", "jsonc-eslint-parser": "^2.1.0",
"nx": "21.2.0", "nx": "21.2.1",
"react-refresh": "^0.17.0", "react-refresh": "^0.17.0",
"rollup-plugin-webpack-stats": "2.0.7", "rollup-plugin-webpack-stats": "2.0.7",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@ -11,31 +11,12 @@ export default defineConfig( {
svg() svg()
], ],
test: { test: {
browser: { environment: "happy-dom",
enabled: true,
name: 'chrome',
provider: 'webdriverio',
providerOptions: {},
headless: true,
ui: false
},
include: [ include: [
'tests/**/*.[jt]s' 'tests/**/*.[jt]s'
], ],
globals: true, globals: true,
watch: false, watch: false,
passWithNoTests: true, passWithNoTests: true
coverage: {
thresholds: {
lines: 100,
functions: 100,
branches: 100,
statements: 100
},
provider: 'istanbul',
include: [
'src'
]
}
} }
} ); } );

View File

@ -1,7 +1,8 @@
import "ckeditor5/ckeditor5.css"; import "ckeditor5/ckeditor5.css";
import "./theme/code_block_toolbar.css"; import "./theme/code_block_toolbar.css";
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS, PREMIUM_PLUGINS } from "./plugins";
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
import "./translation_overrides.js";
export { EditorWatchdog } from "ckeditor5"; export { EditorWatchdog } from "ckeditor5";
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, Node, Position, Element, WatchdogConfig } from "ckeditor5"; export type { EditorConfig, MentionFeed, MentionFeedObjectItem, Node, Position, Element, WatchdogConfig } from "ckeditor5";
export type { TemplateDefinition } from "ckeditor5-premium-features"; export type { TemplateDefinition } from "ckeditor5-premium-features";

View File

@ -1,4 +1,4 @@
import { Autoformat, AutoLink, BlockQuote, BlockToolbar, Bold, CKFinderUploadAdapter, Clipboard, Code, CodeBlock, Enter, FindAndReplace, Font, FontBackgroundColor, FontColor, GeneralHtmlSupport, Heading, HeadingButtonsUI, HorizontalLine, Image, ImageCaption, ImageInline, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Alignment, Indent, IndentBlock, Italic, Link, List, ListProperties, Mention, PageBreak, Paragraph, ParagraphButtonUI, PasteFromOffice, PictureEditing, RemoveFormat, SelectAll, ShiftEnter, SpecialCharacters, SpecialCharactersEssentials, Strikethrough, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableSelection, TableToolbar, TextPartLanguage, TextTransformation, TodoList, Typing, Underline, Undo, Bookmark, Emoji } from "ckeditor5"; import { Autoformat, AutoLink, BlockQuote, BlockToolbar, Bold, CKFinderUploadAdapter, Clipboard, Code, CodeBlock, Enter, FindAndReplace, Font, FontBackgroundColor, FontColor, GeneralHtmlSupport, Heading, HeadingButtonsUI, HorizontalLine, Image, ImageCaption, ImageInline, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Alignment, Indent, IndentBlock, Italic, Link, List, ListProperties, Mention, PageBreak, Paragraph, ParagraphButtonUI, PasteFromOffice, PictureEditing, RemoveFormat, SelectAll, ShiftEnter, SpecialCharacters, SpecialCharactersEssentials, Strikethrough, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableSelection, TableToolbar, TextPartLanguage, TextTransformation, TodoList, Typing, Underline, Undo, Bookmark, Emoji, Notification } from "ckeditor5";
import { SlashCommand, Template } from "ckeditor5-premium-features"; import { SlashCommand, Template } from "ckeditor5-premium-features";
import type { Plugin } from "ckeditor5"; import type { Plugin } from "ckeditor5";
import CutToNotePlugin from "./plugins/cuttonote.js"; import CutToNotePlugin from "./plugins/cuttonote.js";
@ -148,8 +148,7 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
Emoji, Emoji,
...TRILIUM_PLUGINS, ...TRILIUM_PLUGINS,
...EXTERNAL_PLUGINS, ...EXTERNAL_PLUGINS
...PREMIUM_PLUGINS
]; ];
/** /**
@ -157,5 +156,5 @@ export const COMMON_PLUGINS: typeof Plugin[] = [
*/ */
export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [ export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [
...COMMON_PLUGINS, ...COMMON_PLUGINS,
BlockToolbar BlockToolbar,
]; ];

View File

@ -0,0 +1,8 @@
window.CKEDITOR_TRANSLATIONS = {
en: {
dictionary: {
"Insert template": "Insert text snippet",
"Search template": "Search text snippet"
}
}
};

View File

@ -0,0 +1,37 @@
import { it } from "vitest";
import { describe } from "vitest";
import { ClassicEditor } from "../src/index.js";
import { type BalloonEditor, type ButtonView, type Editor } from "ckeditor5";
import { beforeEach } from "vitest";
import { expect } from "vitest";
describe("Text snippets", () => {
let editorElement: HTMLDivElement;
let editor: Editor;
beforeEach(async () => {
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
console.log("Trigger each");
editor = await ClassicEditor.create(editorElement, {
licenseKey: "GPL",
toolbar: {
items: [
"insertTemplate"
]
}
});
});
it("uses correct translations", () => {
const itemsWithButtonView = Array.from(editor.ui.view.toolbar?.items)
.filter(item => "buttonView" in item)
.map(item => (item.buttonView as ButtonView).label);
expect(itemsWithButtonView).not.toContain("Insert template");
expect(itemsWithButtonView).toContain("Insert text snippet");
});
});

View File

@ -23,9 +23,9 @@
"@codemirror/lang-css": "6.3.1", "@codemirror/lang-css": "6.3.1",
"@codemirror/lang-html": "6.4.9", "@codemirror/lang-html": "6.4.9",
"@codemirror/lang-javascript": "6.2.4", "@codemirror/lang-javascript": "6.2.4",
"@codemirror/lang-json": "6.0.1", "@codemirror/lang-json": "6.0.2",
"@codemirror/lang-markdown": "6.3.3", "@codemirror/lang-markdown": "6.3.3",
"@codemirror/lang-php": "6.0.1", "@codemirror/lang-php": "6.0.2",
"@codemirror/lang-vue": "0.1.3", "@codemirror/lang-vue": "0.1.3",
"@codemirror/lang-xml": "6.1.0", "@codemirror/lang-xml": "6.1.0",
"@codemirror/legacy-modes": "6.5.1", "@codemirror/legacy-modes": "6.5.1",

986
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff