Merge branch 'develop' into develop

This commit is contained in:
Yiran Lu 2025-04-15 23:36:17 +02:00 committed by GitHub
commit e2bf203404
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 307 additions and 121 deletions

View File

@ -61,7 +61,7 @@
"hD3V4hiu2VW4",
"VN3xnce1vLkX"
],
"title": "v0.92.8-beta",
"title": "v0.93.0",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
@ -69,7 +69,7 @@
"mime": "text/html",
"attributes": [],
"format": "markdown",
"dataFileName": "v0.92.8-beta.md",
"dataFileName": "v0.93.0.md",
"attachments": []
},
{

View File

@ -1,4 +1,4 @@
# v0.92.8-beta
# v0.93.0
## 💡 Key highlights
* …
@ -14,6 +14,7 @@
* [Note background is gray in 0.92.7 (light theme)](https://github.com/TriliumNext/Notes/issues/1689)
* [config.Session.cookieMaxAge is ignored](https://github.com/TriliumNext/Notes/issues/1709) by @pano9000
* [Return correct HTTP status code on failed login attempts instead of 200](https://github.com/TriliumNext/Notes/issues/1707) by @pano9000
* [Calendar stops displaying notes after adding a Day Note](https://github.com/TriliumNext/Notes/issues/1705)
## ✨ Improvements
@ -32,6 +33,8 @@
* [Center Search results under quick search bar](https://github.com/TriliumNext/Notes/issues/1679)
* Native ARM builds for Windows are now back.
* Basic Touch Bar support for macOS.
* [Support Bearer Token](https://github.com/TriliumNext/Notes/issues/1701)
* The tab bar is now scrollable when there are many tabs by @SiriusXT
## 🌍 Internationalization

View File

@ -9636,6 +9636,13 @@
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
@ -9649,13 +9656,6 @@
"value": "default-note-title",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",
@ -10014,6 +10014,13 @@
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
@ -10027,13 +10034,6 @@
"value": "bx bx-list-plus",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "habiZ3HU8Kw8",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
@ -11066,32 +11066,32 @@
"mime": "text/markdown",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "script-api",
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"value": "Q2z6av6JZVWm",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "Q2z6av6JZVWm",
"value": "MEtfsqa5VwNi",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "MEtfsqa5VwNi",
"type": "label",
"name": "shareAlias",
"value": "script-api",
"isInheritable": false,
"position": 40
"position": 10
}
],
"format": "markdown",

View File

@ -9,16 +9,26 @@ As an alternative to calling the API directly, there are client libraries to sim
* [trilium-py](https://github.com/Nriver/trilium-py), you can use Python to communicate with Trilium.
## Obtaining a token
All operations with the REST API have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)).
## Authentication
All operations have to be authenticated using a token. You can get this token either from Options -> ETAPI or programmatically using the `/auth/login` REST call (see the [spec](https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml)):
### Via the `Authorization` header
```
GET https://myserver.com/etapi/app-info
Authorization: ETAPITOKEN
```
Alternatively, since 0.56 you can also use basic auth format:
where `ETAPITOKEN` is the token obtained in the previous step.
For compatibility with various tools, it's also possible to specify the value of the `Authorization` header in the format `Bearer ETAPITOKEN` (since 0.93.0).
### Basic authentication
Since v0.56 you can also use basic auth format:
```
GET https://myserver.com/etapi/app-info

View File

@ -8,12 +8,19 @@
<li><a href="https://github.com/Nriver/trilium-py">trilium-py</a>, you can
use Python to communicate with Trilium.</li>
</ul>
<h2>Obtaining a token</h2>
<p>All operations with the REST API have to be authenticated using a token.
You can get this token either from Options -&gt; ETAPI or programmatically
using the <code>/auth/login</code> REST call (see the <a href="https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml">spec</a>).</p>
<h2>Authentication</h2>
<p>All operations have to be authenticated using a token. You can get this
token either from Options -&gt; ETAPI or programmatically using the <code>/auth/login</code> REST
call (see the <a href="https://github.com/TriliumNext/Notes/blob/master/src/etapi/etapi.openapi.yaml">spec</a>):</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
<h3>Via the <code>Authorization</code> header</h3><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
Authorization: ETAPITOKEN</code></pre>
<p>Alternatively, since 0.56 you can also use basic auth format:</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
<p>where <code>ETAPITOKEN</code> is the token obtained in the previous step.</p>
<p>For compatibility with various tools, it's also possible to specify the
value of the <code>Authorization</code> header in the format <code>Bearer ETAPITOKEN</code> (since
0.93.0).</p>
<h3>Basic authentication</h3>
<p>Since v0.56 you can also use basic auth format:</p><pre><code class="language-text-x-trilium-auto">GET https://myserver.com/etapi/app-info
Authorization: Basic BATOKEN</code></pre>
<ul>
<li>Where <code>BATOKEN = BASE64(username + ':' + password)</code> - this is

View File

@ -36,8 +36,8 @@
<h3>Running the Docker Container</h3>
<h4>Local Access Only</h4>
<p>Run the container to make it accessible only from the localhost. This
setup is suitable for testing or when using a prox ay server like Nginx
or Apache.</p><pre><code class="language-text-x-trilium-auto">sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]</code></pre>
setup is suitable for testing or when using a proxy server like Nginx or
Apache.</p><pre><code class="language-text-x-trilium-auto">sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/notes:[VERSION]</code></pre>
<ol>
<li>Verify the container is running using <code>docker ps</code>.</li>
<li>Access Trilium via a web browser at <code>127.0.0.1:8080</code>.</li>

View File

@ -1,11 +1,10 @@
<p>For <a href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">script code notes</a>,
Trilium offers an API that gives them access to various features of the
application.</p>
<p>For <a href="#root/_help_CdNpE2pqjmI6">script code notes</a>, Trilium offers
an API that gives them access to various features of the application.</p>
<p>There are two APIs:</p>
<ul>
<li>One for the front-end scripts:&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_Q2z6av6JZVWm">Frontend API</a>
<li>One for the front-end scripts:&nbsp;<a class="reference-link" href="#root/_help_Q2z6av6JZVWm">Frontend API</a>
</li>
<li>One for the back-end scripts:&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_MEtfsqa5VwNi">Backend API</a>
<li>One for the back-end scripts:&nbsp;<a class="reference-link" href="#root/_help_MEtfsqa5VwNi">Backend API</a>
</li>
</ul>
<p>In both cases, the API resides in a global variable, <code>api</code>,

View File

@ -13,7 +13,7 @@ declare module "draggabilly" {
containment: HTMLElement
});
element: HTMLElement;
on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
on(event: "staticClick" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
dragEnd();
isDragging: boolean;
positionDrag: () => void;

View File

@ -11,10 +11,11 @@ import type NoteContext from "../components/note_context.js";
const isDesktop = utils.isDesktop();
const TAB_CONTAINER_MIN_WIDTH = 24;
const TAB_CONTAINER_MIN_WIDTH = 100;
const TAB_CONTAINER_MAX_WIDTH = 240;
const TAB_CONTAINER_LEFT_PADDING = 5;
const NEW_TAB_WIDTH = 32;
const SCROLL_BUTTON_WIDTH = 36;
const NEW_TAB_WIDTH = 36;
const MIN_FILLER_WIDTH = isDesktop ? 50 : 15;
const MARGIN_WIDTH = 5;
@ -32,6 +33,8 @@ const TAB_TPL = `
</div>
</div>`;
const CONTAINER_ANCHOR_TPL = `<div class="tab-row-container-anchor"></div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="${t("tab_row.add_new_tab")}">+</div>`;
const FILLER_TPL = `<div class="tab-row-filler"></div>`;
@ -39,11 +42,11 @@ const TAB_ROW_TPL = `
<div class="tab-row-widget">
<style>
.tab-row-widget {
display:flex;
box-sizing: border-box;
position: relative;
width: 100%;
background: var(--main-background-color);
overflow: hidden;
user-select: none;
}
@ -59,7 +62,6 @@ const TAB_ROW_TPL = `
.tab-row-widget .tab-row-widget-container {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
}
@ -74,15 +76,12 @@ const TAB_ROW_TPL = `
}
.note-new-tab {
position: absolute;
left: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 ${NEW_TAB_WIDTH}px;
height: ${NEW_TAB_WIDTH}px;
padding: 1px;
border: 0;
margin: 0;
z-index: 1;
text-align: center;
font-size: 24px;
cursor: pointer;
box-sizing: border-box;
@ -96,11 +95,22 @@ const TAB_ROW_TPL = `
.tab-row-filler {
box-sizing: border-box;
-webkit-app-region: drag;
position: absolute;
left: 0;
height: 100%;
min-width: ${MIN_FILLER_WIDTH}px;
flex-grow: 1;
}
.tab-row-container-anchor{
position: absolute;
left: 0;
width: 0px;
height: 36px;
border: 0;
margin: 0;
z-index: 1;
cursor: pointer;
box-sizing: border-box;
}
body.mobile .tab-row-filler {
display: none;
}
@ -184,6 +194,38 @@ const TAB_ROW_TPL = `
text-align: center;
}
.tab-scroll-button-left, .tab-scroll-button-right {
display: none;
flex: 0 0 ${SCROLL_BUTTON_WIDTH}px;
height: ${SCROLL_BUTTON_WIDTH}px;
padding: 1px 1px 1px 1px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.tab-scroll-button-left {
color: var(--active-tab-text-color);
box-shadow: inset -1px 0 0 0 var(--main-border-color);
}
.tab-scroll-button-right {
color: var(--active-tab-text-color);
box-shadow: inset 1px 0 0 0 var(--main-border-color);
}
.tab-scroll-button-left.disabled,
.tab-scroll-button-right.disabled {
color: var(--inactive-tab-text-color);
box-shadow: none;
pointer-events: none;
}
.tab-scroll-button-left:hover,
.tab-scroll-button-right:hover {
background-color: var(--tab-background-color, var(--inactive-tab-hover-background-color));
}
.tab-row-widget .note-tab:hover .note-tab-wrapper {
background-color: var(--tab-background-color, var(--inactive-tab-hover-background-color));
}
@ -231,9 +273,29 @@ const TAB_ROW_TPL = `
.tab-row-widget:not(.tab-row-widget-is-sorting) .note-tab.note-tab-was-just-dragged {
transition: transform 120ms ease-in-out;
}
.tab-row-widget-wrapper {
display: flex;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.tab-row-widget-scrolling-container {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none; /* Firefox */
}
/* Chrome/Safari */
.tab-row-widget-scrolling-container::-webkit-scrollbar {
display: none;
}
</style>
<div class="tab-row-widget-container"></div>
<div class="tab-scroll-button-left bx bx-chevron-left"></div>
<div class="tab-row-widget-scrolling-container">
<div class="tab-row-widget-container"></div>
</div>
<div class="tab-scroll-button-right bx bx-chevron-right"></div>
</div>`;
export default class TabRowWidget extends BasicWidget {
@ -244,11 +306,24 @@ export default class TabRowWidget extends BasicWidget {
private draggabillyDragging?: Draggabilly | null;
private $style!: JQuery<HTMLElement>;
private $tabScrollingContainer!: JQuery<HTMLElement>;
private $tabContainer!: JQuery<HTMLElement>;
private $scrollButtonLeft!: JQuery<HTMLElement>;
private $scrollButtonRight!: JQuery<HTMLElement>;
private $containerAnchor!: JQuery<HTMLElement>;
private $filler!: JQuery<HTMLElement>;
private $newTab!: JQuery<HTMLElement>;
private updateScrollTimeout: ReturnType<typeof setTimeout> | undefined;
private newTabOuterWidth: number = 0;
private scrollButtonsOuterWidth: number = 0;
doRender() {
this.$widget = $(TAB_ROW_TPL);
this.$tabScrollingContainer = this.$widget.children(".tab-row-widget-scrolling-container");
this.$tabContainer = this.$widget.find(".tab-row-widget-container");
this.$scrollButtonLeft = this.$widget.children(".tab-scroll-button-left");
this.$scrollButtonRight = this.$widget.children(".tab-scroll-button-right");
const documentStyle = window.getComputedStyle(document.documentElement);
this.showNoteIcons = documentStyle.getPropertyValue("--tab-note-icons") === "true";
@ -257,11 +332,13 @@ export default class TabRowWidget extends BasicWidget {
this.setupStyle();
this.setupEvents();
this.setupContainerAnchor();
this.setupDraggabilly();
this.setupNewButton();
this.setupFiller();
this.layoutTabs();
this.setVisibility();
this.setupScrollEvents();
this.$widget.on("contextmenu", ".note-tab", (e) => {
e.preventDefault();
@ -300,6 +377,60 @@ export default class TabRowWidget extends BasicWidget {
this.$widget.append(this.$style);
}
scrollTabContainer(direction: number, behavior: ScrollBehavior = "smooth") {
const currentScrollLeft = this.$tabScrollingContainer[0]?.scrollLeft;
this.$tabScrollingContainer[0].scrollTo({
left: currentScrollLeft + direction,
behavior
});
};
setupScrollEvents() {
let isScrolling = false;
this.$tabScrollingContainer[0].addEventListener('wheel', (event) => {
if (!isScrolling) {
isScrolling = true;
requestAnimationFrame(() => {
this.scrollTabContainer(event.deltaY * 1.5, 'instant');
isScrolling = false;
});
}
});
this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200));
this.$scrollButtonRight[0].addEventListener('click', () => this.scrollTabContainer(200));
this.$tabScrollingContainer[0].addEventListener('scroll', () => {
clearTimeout(this.updateScrollTimeout);
this.updateScrollTimeout = setTimeout(() => {
this.updateScrollButtonState();
}, 100);
});
}
updateScrollButtonState() {
const scrollLeft = this.$tabScrollingContainer[0].scrollLeft;
const scrollWidth = this.$tabScrollingContainer[0].scrollWidth;
const clientWidth = this.$tabScrollingContainer[0].clientWidth;
// Detect whether the scrollbar is at the far left or far right.
this.$scrollButtonLeft.toggleClass("disabled", Math.abs(scrollLeft) <= 1);
this.$scrollButtonRight.toggleClass("disabled", Math.abs(scrollLeft + clientWidth - scrollWidth) <= 1);
}
setScrollButtonVisibility(show: boolean = true) {
if (show) {
this.$scrollButtonLeft.css("display", "flex");
this.$scrollButtonRight.css("display", "flex");
clearTimeout(this.updateScrollTimeout);
this.updateScrollTimeout = setTimeout(() => {
this.updateScrollButtonState();
}, 200);
} else {
this.$scrollButtonLeft.css("display", "none");
this.$scrollButtonRight.css("display", "none");
}
}
setupEvents() {
new ResizeObserver((_) => {
this.cleanUpPreviouslyDraggedTabs();
@ -317,14 +448,32 @@ export default class TabRowWidget extends BasicWidget {
return Array.prototype.slice.call(this.$widget.find(".note-tab"));
}
get $tabContainer() {
return this.$widget.find(".tab-row-widget-container");
updateOuterWidth() {
if (this.newTabOuterWidth == 0) {
this.newTabOuterWidth = this.$newTab?.outerWidth(true) ?? 0;
}
if (this.scrollButtonsOuterWidth == 0) {
this.scrollButtonsOuterWidth = (this.$scrollButtonLeft?.outerWidth(true) ?? 0) + (this.$scrollButtonRight?.outerWidth(true) ?? 0);
}
}
get tabWidths() {
const numberOfTabs = this.tabEls.length;
const tabsContainerWidth = this.$tabContainer[0].clientWidth - NEW_TAB_WIDTH - MIN_FILLER_WIDTH;
const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH;
// this.$newTab may include margin, and using NEW_TAB_WIDTH could cause tabsContainerWidth to be slightly larger,
// resulting in misaligned scrollbars/buttons. Therefore, use outerwidth.
this.updateOuterWidth();
let tabsContainerWidth = Math.floor(this.$widget.width() ?? 0);
tabsContainerWidth -= this.newTabOuterWidth + MIN_FILLER_WIDTH;
// Check whether the scroll buttons need to be displayed.
if ((TAB_CONTAINER_MIN_WIDTH + MARGIN_WIDTH) * numberOfTabs > tabsContainerWidth) {
tabsContainerWidth -= this.scrollButtonsOuterWidth;
this.setScrollButtonVisibility(true);
} else {
this.setScrollButtonVisibility(false);
}
const marginWidth = (numberOfTabs - 1) * MARGIN_WIDTH + TAB_CONTAINER_LEFT_PADDING;
const targetWidth = (tabsContainerWidth - marginWidth) / numberOfTabs;
const clampedTargetWidth = Math.max(TAB_CONTAINER_MIN_WIDTH, Math.min(TAB_CONTAINER_MAX_WIDTH, targetWidth));
const flooredClampedTargetWidth = Math.floor(clampedTargetWidth);
@ -344,10 +493,6 @@ export default class TabRowWidget extends BasicWidget {
}
}
if (this.$filler) {
this.$filler.css("width", `${extraWidthRemaining + MIN_FILLER_WIDTH}px`);
}
return widths;
}
@ -362,10 +507,9 @@ export default class TabRowWidget extends BasicWidget {
position -= MARGIN_WIDTH; // the last margin should not be applied
const newTabPosition = position;
const fillerPosition = position + 32;
const anchorPosition = position;
return { tabPositions, newTabPosition, fillerPosition };
return { tabPositions, anchorPosition };
}
layoutTabs() {
@ -386,15 +530,14 @@ export default class TabRowWidget extends BasicWidget {
let styleHTML = "";
const { tabPositions, newTabPosition, fillerPosition } = this.getTabPositions();
const { tabPositions, anchorPosition } = this.getTabPositions();
tabPositions.forEach((position, i) => {
styleHTML += `.note-tab:nth-child(${i + 1}) { transform: translate3d(${position}px, 0, 0)} `;
});
styleHTML += `.note-new-tab { transform: translate3d(${newTabPosition}px, 0, 0) } `;
styleHTML += `.tab-row-filler { transform: translate3d(${fillerPosition}px, 0, 0) } `;
styleHTML += `.tab-row-container-anchor { transform: translate3d(${anchorPosition}px, 0, 0) } `;
styleHTML += `.tab-row-widget-container {width: ${anchorPosition}px}`;
this.$style.html(styleHTML);
}
@ -406,8 +549,7 @@ export default class TabRowWidget extends BasicWidget {
$tab.addClass("note-tab-was-just-added");
setTimeout(() => $tab.removeClass("note-tab-was-just-added"), 500);
this.$newTab.before($tab);
this.$containerAnchor.before($tab);
this.setVisibility();
this.setTabCloseEvent($tab);
this.updateTitle($tab, t("tab_row.new_tab"));
@ -507,6 +649,7 @@ export default class TabRowWidget extends BasicWidget {
setupDraggabilly() {
const tabEls = this.tabEls;
const { tabPositions } = this.getTabPositions();
let initialScrollLeft = 0;
if (this.isDragging && this.draggabillyDragging) {
this.isDragging = false;
@ -533,7 +676,7 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillies.push(draggabilly);
draggabilly.on("pointerDown", () => {
draggabilly.on("staticClick", () => {
appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id"));
});
@ -542,11 +685,20 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillyDragging = draggabilly;
tabEl.classList.add("note-tab-is-dragging");
this.$widget.addClass("tab-row-widget-is-sorting");
initialScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
draggabilly.positionDrag = () => { };
});
draggabilly.on("dragEnd", () => {
this.isDragging = false;
const finalTranslateX = parseFloat(tabEl.style.left);
const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
const scrollDelta = currentScrollLeft - initialScrollLeft;
const translateX = parseFloat(tabEl.style.left) + scrollDelta;
const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth;
const minTranslateX = 0;
const finalTranslateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX));
tabEl.style.transform = `translate3d(0, 0, 0)`;
// Animate dragged tab back into its place
@ -570,12 +722,31 @@ export default class TabRowWidget extends BasicWidget {
});
});
draggabilly.on("dragMove", (event: unknown, pointer: unknown, moveVector: MoveVector) => {
draggabilly.on("dragMove", (event: unknown, pointer: PointerEvent, moveVector: MoveVector) => {
// The current index be computed within the event since it can change during the dragMove
const tabEls = this.tabEls;
const currentIndex = tabEls.indexOf(tabEl);
const currentTabPositionX = originalTabPositionX + moveVector.x;
const scorllContainerBounds = this.$tabScrollingContainer[0]?.getBoundingClientRect();
const pointerX = pointer.pageX;
const scrollSpeed = 100; // The increment of each scroll.
// Check if the mouse is near the edge of the container and trigger scrolling.
if (pointerX < scorllContainerBounds.left) {
this.scrollTabContainer(- scrollSpeed);
} else if (pointerX > scorllContainerBounds.right) {
this.scrollTabContainer(scrollSpeed);
}
const currentScrollLeft = this.$tabScrollingContainer?.scrollLeft() ?? 0;
const scrollDelta = currentScrollLeft - initialScrollLeft;
let translateX = moveVector.x + scrollDelta;
// Limit the `translateX` so that `tabEl` cannot exceed the left and right boundaries of the container.
const maxTranslateX = this.$tabContainer[0]?.offsetWidth - tabEl.offsetWidth - originalTabPositionX;
const minTranslateX = - originalTabPositionX;
translateX = Math.min(maxTranslateX, Math.max(minTranslateX, translateX));
tabEl.style.transform = `translate3d(${translateX}px, 0, 0)`;
const currentTabPositionX = originalTabPositionX + translateX;
const destinationIndexTarget = this.closest(currentTabPositionX, tabPositions);
const destinationIndex = Math.max(0, Math.min(tabEls.length, destinationIndexTarget));
@ -594,8 +765,7 @@ export default class TabRowWidget extends BasicWidget {
if (destinationIndex < originIndex) {
tabEl.parentNode?.insertBefore(tabEl, this.tabEls[destinationIndex]);
} else {
const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0];
const beforeEl = this.tabEls[destinationIndex + 1] || this.$containerAnchor[0];
tabEl.parentNode?.insertBefore(tabEl, beforeEl);
}
this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() });
@ -604,14 +774,19 @@ export default class TabRowWidget extends BasicWidget {
setupNewButton() {
this.$newTab = $(NEW_TAB_BUTTON_TPL);
this.$tabContainer.append(this.$newTab);
this.$widget.append(this.$newTab);
}
setupFiller() {
this.$filler = $(FILLER_TPL);
this.$tabContainer.append(this.$filler);
this.$widget.append(this.$filler);
}
setupContainerAnchor() {
this.$containerAnchor = $(CONTAINER_ANCHOR_TPL);
this.$tabContainer.append(this.$containerAnchor);
}
closest(value: number, array: number[]) {
@ -660,7 +835,9 @@ export default class TabRowWidget extends BasicWidget {
updateTabById(ntxId: string | null) {
const $tab = this.getTabById(ntxId);
$tab[0].scrollIntoView({
behavior: 'smooth'
});
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
this.updateTab($tab, noteContext);

View File

@ -869,46 +869,23 @@ body.mobile .fancytree-node > span {
position: relative;
}
/* #region Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */
body.layout-horizontal .tab-row-widget,
body.layout-horizontal .tab-row-widget-container,
body.layout-horizontal .tab-row-container .note-tab[active] {
overflow: visible !important;
}
body.layout-horizontal .tab-row-container .note-tab[active]:before {
content: "";
position: absolute;
bottom: 0;
left: -100vw;
top: var(--tab-height);
right: calc(100% - 1px);
height: 1px;
/* Apply a border to the tab bar that avoids the current tab but also allows a transparent active tab. */
body.layout-horizontal .tab-row-container {
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
body.layout-horizontal .tab-row-container .note-tab[active]:after {
content: "";
position: absolute;
bottom: 0;
left: 100%;
top: var(--tab-height);
right: 0;
width: 100vw;
height: 1px;
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
}
/* #endregion */
body.layout-vertical.electron.platform-darwin .tab-row-container {
border-bottom: 1px solid var(--subtle-border-color);
}
.tab-row-widget-container {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
height: var(--tab-height) !important;
}
.tab-row-widget > * {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
}
body.layout-horizontal .tab-row-container {
padding-top: calc((var(--tab-bar-height) - var(--tab-height)));
}
@ -923,11 +900,12 @@ body.layout-vertical #left-pane .quick-search {
/* Limit the drag area for the previous elements to include just to the element itself
and not its descendants also */
body.layout-horizontal .tab-row-container > *,
body.layout-vertical .tab-row-widget > *,
body.layout-vertical .tab-row-widget > *:not(.tab-row-filler),
body.layout-vertical #left-pane .quick-search > * {
-webkit-app-region: no-drag;
}
body.layout-horizontal .tab-row-widget,
body.layout-horizontal .tab-row-widget-container {
margin-top: 0;
position: relative;
@ -991,7 +969,7 @@ body.layout-horizontal .tab-row-widget .note-tab .note-tab-wrapper {
text-overflow: ellipsis;
}
body.layout-vertical .tab-row-widget-is-sorting .note-tab[active] .note-tab-wrapper {
body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .note-tab-wrapper {
transform: scale(0.85);
box-shadow: var(--active-tab-dragging-shadow) !important;
}

View File

@ -112,12 +112,21 @@ function upsert<T extends {}>(tableName: string, primaryKey: string, rec: T) {
execute(query, rec);
}
function stmt(sql: string) {
if (!(sql in statementCache)) {
statementCache[sql] = dbConnection.prepare(sql);
/**
* For the given SQL query, returns a prepared statement. For the same query (string comparison), the same statement is returned.
*
* @param sql the SQL query for which to return a prepared statement.
* @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query.
* @returns the corresponding {@link Statement}.
*/
function stmt(sql: string, isRaw?: boolean) {
const key = (isRaw ? "raw/" + sql : sql);
if (!(key in statementCache)) {
statementCache[key] = dbConnection.prepare(sql);
}
return statementCache[sql];
return statementCache[key];
}
function getRow<T>(query: string, params: Params = []): T {
@ -172,7 +181,7 @@ function getRows<T>(query: string, params: Params = []): T[] {
}
function getRawRows<T extends {} | unknown[]>(query: string, params: Params = []): T[] {
return (wrap(query, (s) => s.raw().all(params)) as T[]) || [];
return (wrap(query, (s) => s.raw().all(params), true) as T[]) || [];
}
function iterateRows<T>(query: string, params: Params = []): IterableIterator<T> {
@ -234,7 +243,10 @@ function executeScript(query: string): DatabaseType {
return dbConnection.exec(query);
}
function wrap(query: string, func: (statement: Statement) => unknown): unknown {
/**
* @param isRaw indicates whether `.raw()` is going to be called on the prepared statement in order to return the raw rows (e.g. via {@link getRawRows()}). The reason is that the raw state is preserved in the saved statement and would break non-raw calls for the same query.
*/
function wrap(query: string, func: (statement: Statement) => unknown, isRaw?: boolean): unknown {
const startTimestamp = Date.now();
let result;
@ -243,7 +255,7 @@ function wrap(query: string, func: (statement: Statement) => unknown): unknown {
}
try {
result = func(stmt(query));
result = func(stmt(query, isRaw));
} catch (e: any) {
if (e.message.includes("The database connection is not open")) {
// this often happens on killing the app which puts these alerts in front of user