Merge pull request #654 from TriliumNext/feature/adjustable_launcher_bar_position

Add support for horizontal launcher bar
This commit is contained in:
Elian Doran 2024-11-23 14:40:30 +02:00 committed by GitHub
commit 68fd954a67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 418 additions and 183 deletions

View File

@ -88,8 +88,11 @@ export default class Component {
if (fun) { if (fun) {
return this.callMethod(fun, data); return this.callMethod(fun, data);
} else {
if (!this.parent) {
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
} }
else {
return this.parent.triggerCommand(name, data); return this.parent.triggerCommand(name, data);
} }
} }

View File

@ -83,6 +83,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js"; import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) { constructor(customWidgets) {
@ -92,24 +93,31 @@ export default class DesktopLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
appContext.noteTreeWidget = new NoteTreeWidget(); appContext.noteTreeWidget = new NoteTreeWidget();
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext)
.optChild(launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget().class("full-width"))
.child(new TitleBarButtonsWidget())
.css('height', '40px')
.css('background-color', 'var(--launcher-pane-background-color)')
.setParent(appContext) .setParent(appContext)
.child(new FlexContainer("column")
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
.child(new LeftPaneToggleWidget())
) )
.optChild(launcherPaneIsHorizontal, launcherPane)
.child(new FlexContainer('row')
.css("flex-grow", "1")
.optChild(!launcherPaneIsHorizontal, launcherPane)
.child(new LeftPaneContainer() .child(new LeftPaneContainer()
.child(new QuickSearchWidget()) .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.child(appContext.noteTreeWidget) .child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane')) .child(...this.customWidgets.get('left-pane'))
) )
.child(new FlexContainer('column') .child(new FlexContainer('column')
.id('rest-pane') .id('rest-pane')
.css("flex-grow", "1") .css("flex-grow", "1")
.child(new FlexContainer('row') .optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget()) .child(new TabRowWidget())
.child(new TitleBarButtonsWidget()) .child(new TitleBarButtonsWidget())
.css('height', '40px') .css('height', '40px')
@ -201,6 +209,7 @@ export default class DesktopLayout {
) )
) )
) )
)
.child(new BulkActionsDialog()) .child(new BulkActionsDialog())
.child(new AboutDialog()) .child(new AboutDialog())
.child(new HelpDialog()) .child(new HelpDialog())
@ -225,4 +234,27 @@ export default class DesktopLayout {
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()); .child(new PromptDialog());
} }
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LeftPaneToggleWidget(true))
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true))
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")
.class("vertical")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false))
.child(new LeftPaneToggleWidget(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
} }

View File

@ -24,6 +24,7 @@ import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js"; import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@ -112,15 +113,12 @@ span.fancytree-expander {
export default class MobileLayout { export default class MobileLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext) .setParent(appContext)
.cssBlock(MOBILE_CSS) .cssBlock(MOBILE_CSS)
.child(new FlexContainer("column") .child(this.#buildLauncherPane(launcherPaneIsHorizontal))
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
)
.child(new FlexContainer("row") .child(new FlexContainer("row")
.filling() .filling()
.child(new ScreenContainer("tree", 'column') .child(new ScreenContainer("tree", 'column')
@ -140,12 +138,14 @@ export default class MobileLayout {
.child(new FlexContainer('row').contentSized() .child(new FlexContainer('row').contentSized()
.css('font-size', 'larger') .css('font-size', 'larger')
.css('align-items', 'center') .css('align-items', 'center')
.child(new MobileDetailMenuWidget().contentSized()) .optChild(!launcherPaneIsHorizontal, new MobileDetailMenuWidget(false).contentSized())
.child(new NoteTitleWidget() .child(new NoteTitleWidget()
.contentSized() .contentSized()
.css("position: relative;") .css("position: relative;")
.css("top: 5px;") .css("top: 5px;")
.optCss(launcherPaneIsHorizontal, "padding-left", "0.5em")
) )
.optChild(launcherPaneIsHorizontal, new MobileDetailMenuWidget(true).contentSized())
.child(new CloseDetailButtonWidget().contentSized())) .child(new CloseDetailButtonWidget().contentSized()))
.child(new SharedInfoWidget()) .child(new SharedInfoWidget())
.child(new FloatingButtons() .child(new FloatingButtons()
@ -174,4 +174,25 @@ export default class MobileLayout {
.child(new ConfirmDialog()) .child(new ConfirmDialog())
); );
} }
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true));
} else {
launcherPane = new FlexContainer("column")
.class("vertical")
.css("width", "53px")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
} }

View File

@ -40,6 +40,21 @@ class BasicWidget extends Component {
return this; return this;
} }
/**
* Conditionally adds the given components as children to this component.
*
* @param {boolean} condition whether to add the components.
* @param {...any} components the components to be added as children to this component provided the condition is truthy.
* @returns self for chaining.
*/
optChild(condition, ...components) {
if (condition) {
return this.child(...components);
} else {
return this;
}
}
id(id) { id(id) {
this.attrs.id = id; this.attrs.id = id;
return this; return this;
@ -50,11 +65,34 @@ class BasicWidget extends Component {
return this; return this;
} }
/**
* Sets the CSS attribute of the given name to the given value.
*
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
css(name, value) { css(name, value) {
this.attrs.style += `${name}: ${value};`; this.attrs.style += `${name}: ${value};`;
return this; return this;
} }
/**
* Sets the CSS attribute of the given name to the given value, but only if the condition provided is truthy.
*
* @param {boolean} condition `true` in order to apply the CSS, `false` to ignore it.
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
optCss(condition, name, value) {
if (condition) {
return this.css(name, value);
}
return this;
}
contentSized() { contentSized() {
this.css("contain", "none"); this.css("contain", "none");

View File

@ -23,7 +23,11 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.tooltip = new bootstrap.Tooltip(this.$widget, { this.tooltip = new bootstrap.Tooltip(this.$widget, {
html: true, title: () => this.getTitle(), trigger: 'hover' html: true,
title: () => this.getTitle(),
trigger: 'hover',
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
}) })
if (this.settings.onContextMenu) { if (this.settings.onContextMenu) {
@ -36,8 +40,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
}); });
} }
this.$widget.attr("data-placement", this.settings.titlePlacement);
super.doRender(); super.doRender();
} }

View File

@ -5,7 +5,7 @@ import UpdateAvailableWidget from "./update_available.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
const TPL = ` const TPL = `
<div class="dropdown global-menu dropend"> <div class="dropdown global-menu">
<style> <style>
.global-menu { .global-menu {
width: 53px; width: 53px;
@ -107,22 +107,6 @@ const TPL = `
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" <button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action global-menu-button"> aria-expanded="false" class="icon-action global-menu-button">
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>
<div class="global-menu-button-update-available"></div> <div class="global-menu-button-update-available"></div>
</button> </button>
@ -235,7 +219,7 @@ const TPL = `
${t('global_menu.options')} ${t('global_menu.options')}
</li> </li>
<div class="dropdown-divider"></div> <div class="dropdown-divider desktop-only"></div>
<li class="dropdown-item show-help-button" data-trigger-command="showHelp"> <li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-help-circle"></span> <span class="bx bx-help-circle"></span>
@ -265,18 +249,46 @@ const TPL = `
`; `;
export default class GlobalMenuWidget extends BasicWidget { export default class GlobalMenuWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.updateAvailableWidget = new UpdateAvailableWidget(); this.updateAvailableWidget = new UpdateAvailableWidget();
this.isHorizontalLayout = isHorizontalLayout;
} }
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); if (!this.isHorizontalLayout) {
this.$widget.addClass("dropend");
}
const $globalMenuButton = this.$widget.find(".global-menu-button")
if (!this.isHorizontalLayout) {
$globalMenuButton.prepend($(`\
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>`));
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" }); this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
} else {
$globalMenuButton.toggleClass("bx bx-menu");
}
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"), {
alignment: "bottom"
});
this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog")); this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
@ -316,10 +328,14 @@ export default class GlobalMenuWidget extends BasicWidget {
this.$zoomState = this.$widget.find(".zoom-state"); this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => { this.$widget.on('show.bs.dropdown', () => {
this.updateZoomState(); this.updateZoomState();
if (this.tooltip) {
this.tooltip.hide(); this.tooltip.hide();
this.tooltip.disable(); this.tooltip.disable();
}
}); });
if (this.tooltip) {
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable()); this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
}
this.$widget.find(".zoom-buttons").on("click", this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change // delay to wait for the actual zoom change

View File

@ -4,7 +4,7 @@ import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget { export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.class("launcher-button"); this.class("launcher-button");
@ -20,6 +20,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
this.settings.command = () => options.is('leftPaneVisible') this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane" ? "hideLeftPane"
: "showLeftPane"; : "showLeftPane";
if (isHorizontalLayout) {
this.settings.titlePlacement = "bottom";
}
} }
refreshIcon() { refreshIcon() {

View File

@ -2,13 +2,7 @@ import BasicWidget from "../basic_widget.js";
const TPL = ` const TPL = `
<div class="dropdown right-dropdown-widget dropend"> <div class="dropdown right-dropdown-widget dropend">
<style> <button type="button" data-bs-toggle="dropdown"
.right-dropdown-widget {
height: 53px;
}
</style>
<button type="button" data-bs-toggle="dropdown" data-placement="right"
aria-haspopup="true" aria-expanded="false" aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button> class="bx right-dropdown-button launcher-button"></button>
@ -25,6 +19,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.iconClass = iconClass; this.iconClass = iconClass;
this.title = title; this.title = title;
this.dropdownTpl = dropdownTpl; this.dropdownTpl = dropdownTpl;
this.settings = {
titlePlacement: "right"
};
} }
doRender() { doRender() {
@ -33,7 +31,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")); this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title); this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new bootstrap.Tooltip(this.$tooltip); this.tooltip = new bootstrap.Tooltip(this.$tooltip, {
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
});
this.$widget.find(".right-dropdown-button") this.$widget.find(".right-dropdown-button")
.addClass(this.iconClass) .addClass(this.iconClass)

View File

@ -10,12 +10,14 @@ import CommandButtonWidget from "../buttons/command_button.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js"; import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
export default class LauncherWidget extends BasicWidget { export default class LauncherWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.innerWidget = null; this.innerWidget = null;
this.isHorizontalLayout = isHorizontalLayout;
} }
isEnabled() { isEnabled() {
@ -63,6 +65,9 @@ export default class LauncherWidget extends BasicWidget {
} }
this.child(this.innerWidget); this.child(this.innerWidget);
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true; return true;
} }
@ -86,28 +91,30 @@ export default class LauncherWidget extends BasicWidget {
initBuiltinWidget(note) { initBuiltinWidget(note) {
const builtinWidget = note.getLabelValue("builtinWidget"); const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
if (builtinWidget === 'calendar') { case "calendar":
return new CalendarWidget(note.title, note.getIcon()); return new CalendarWidget(note.title, note.getIcon());
} else if (builtinWidget === 'spacer') { case "spacer":
// || has to be inside since 0 is a valid value // || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100"); const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor); return new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') { case "bookmarks":
return new BookmarkButtons(); return new BookmarkButtons();
} else if (builtinWidget === 'protectedSession') { case "protectedSession":
return new ProtectedSessionStatusWidget(); return new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') { case "syncStatus":
return new SyncStatusWidget(); return new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') { case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory"); return new HistoryNavigationButton(note, "backInNoteHistory");
} else if (builtinWidget === 'forwardInHistoryButton') { case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory"); return new HistoryNavigationButton(note, "forwardInNoteHistory");
} else if (builtinWidget === 'todayInJournal') { case "todayInJournal":
return new TodayLauncher(note); return new TodayLauncher(note);
} else { case "quickSearch":
return new QuickSearchLauncherWidget(this.isHorizontalLayout);
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
} }
} }

View File

@ -4,12 +4,13 @@ import appContext from "../../components/app_context.js";
import LauncherWidget from "./launcher.js"; import LauncherWidget from "./launcher.js";
export default class LauncherContainer extends FlexContainer { export default class LauncherContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('column'); super(isHorizontalLayout ? "row" : "column");
this.id('launcher-container'); this.id('launcher-container');
this.css('height', '100%'); this.css(isHorizontalLayout ? "width" : 'height', '100%');
this.filling(); this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load(); this.load();
} }
@ -29,7 +30,7 @@ export default class LauncherContainer extends FlexContainer {
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) { for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try { try {
const launcherWidget = new LauncherWidget(); const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote); const success = await launcherWidget.initLauncher(launcherNote);
if (success) { if (success) {

View File

@ -1,8 +1,8 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer { export default class RootContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('row'); super(isHorizontalLayout ? "column" : "row");
this.id('root-widget'); this.id('root-widget');
this.css('height', '100%'); this.css('height', '100%');

View File

@ -6,12 +6,20 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
const TPL = `<button type="button" class="action-button bx bx-menu" style="padding-top: 10px;"></button>`; const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
class MobileDetailMenuWidget extends BasicWidget { class MobileDetailMenuWidget extends BasicWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
this.$widget.on("click", async e => { this.$widget.on("click", async e => {
const note = appContext.tabManager.getActiveContextNote(); const note = appContext.tabManager.getActiveContextNote();

View File

@ -0,0 +1,33 @@
import utils from "../services/utils.js";
import QuickSearchWidget from "./quick_search.js";
/**
* Similar to the {@link QuickSearchWidget} but meant to be included inside the launcher bar.
*
* <p>
* Adds specific tweaks such as:
*
* - Hiding the widget on mobile.
*/
export default class QuickSearchLauncherWidget extends QuickSearchWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
if (!this.isHorizontalLayout) {
// The quick search widget is added somewhere else on the vertical layout.
return false;
}
if (utils.isMobile()) {
// The widget takes too much spaces to be included in the mobile layout.
return false;
}
return super.isEnabled();
}
}

View File

@ -56,6 +56,10 @@ const TAB_ROW_TPL = `
overflow: hidden; overflow: hidden;
} }
.tab-row-widget.full-width {
background: var(--launcher-pane-background-color);
}
.tab-row-widget * { .tab-row-widget * {
box-sizing: inherit; box-sizing: inherit;
font: inherit; font: inherit;

View File

@ -5,6 +5,26 @@ import { t } from "../../../../services/i18n.js";
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4>${t("theme.layout")}</h4>
<div class="form-group row">
<div>
<label>
<input type="radio" name="layout-orientation" value="vertical" />
<strong>${t("theme.layout-vertical-title")}</strong>
- ${t("theme.layout-vertical-description")}
</label>
</div>
<div>
<label>
<input type="radio" name="layout-orientation" value="horizontal" />
<strong>${t("theme.layout-horizontal-title")}</strong>
- ${t("theme.layout-horizontal-description")}
</label>
</div>
</div>
<h4>${t("theme.title")}</h4> <h4>${t("theme.title")}</h4>
<div class="form-group row"> <div class="form-group row">
@ -27,6 +47,11 @@ export default class ThemeOptions extends OptionsWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select"); this.$themeSelect = this.$widget.find(".theme-select");
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts"); this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
this.$layoutOrientation = this.$widget.find(`input[name="layout-orientation"]`).on("change", async () => {
const newLayoutOrientation = this.$widget.find(`input[name="layout-orientation"]:checked`).val();
await this.updateOption("layoutOrientation", newLayoutOrientation);
utils.reloadFrontendApp("layout orientation change");
});
this.$themeSelect.on('change', async () => { this.$themeSelect.on('change', async () => {
const newTheme = this.$themeSelect.val(); const newTheme = this.$themeSelect.val();
@ -57,5 +82,8 @@ export default class ThemeOptions extends OptionsWidget {
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts); this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`)
.prop("checked", "true");
} }
} }

View File

@ -40,6 +40,10 @@ body {
font-size: var(--main-font-size); font-size: var(--main-font-size);
} }
body.mobile .desktop-only {
display: none !important;
}
a { a {
text-decoration: none; text-decoration: none;
} }
@ -1022,6 +1026,18 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
overflow: auto; overflow: auto;
} }
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
left: calc(-100% + 10px);
}
#launcher-pane.horizontal .right-dropdown-widget {
width: 53px;
}
#launcher-pane.vertical .right-dropdown-widget {
height: 53px;
}
/* rotate caret on hover */ /* rotate caret on hover */
.dropdown-menu > li > a:hover:after { .dropdown-menu > li > a:hover:after {
text-decoration: underline; text-decoration: underline;
@ -1124,8 +1140,20 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
border: none; border: none;
color: var(--launcher-pane-text-color); color: var(--launcher-pane-text-color);
background-color: var(--launcher-pane-background-color); background-color: var(--launcher-pane-background-color);
height: 53px; }
#launcher-pane.vertical .launcher-button {
width: 100%; width: 100%;
height: 53px;
}
#launcher-pane.horizontal .launcher-button {
width: 53px;
height: 100%;
}
#launcher-pane.horizontal .quick-search {
width: 350px;
} }
#launcher-pane .icon-action:hover { #launcher-pane .icon-action:hover {

View File

@ -1061,7 +1061,12 @@
"theme_label": "Theme", "theme_label": "Theme",
"override_theme_fonts_label": "Override theme fonts", "override_theme_fonts_label": "Override theme fonts",
"light_theme": "Light", "light_theme": "Light",
"dark_theme": "Dark" "dark_theme": "Dark",
"layout": "Layout",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"layout-vertical-description": "launcher bar is on the left (default)",
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
}, },
"zoom_factor": { "zoom_factor": {
"title": "Zoom Factor (desktop build only)", "title": "Zoom Factor (desktop build only)",

View File

@ -66,7 +66,8 @@ const ALLOWED_OPTIONS = new Set([
'editedNotesOpenInRibbon', 'editedNotesOpenInRibbon',
'locale', 'locale',
'firstDayOfWeek', 'firstDayOfWeek',
'textNoteEditorType' 'textNoteEditorType',
'layoutOrientation'
]); ]);
function getOptions() { function getOptions() {

View File

@ -34,7 +34,7 @@ interface Item {
baseSize?: string; baseSize?: string;
growthFactor?: string; growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap"; targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar"; builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
command?: keyof typeof Command; command?: keyof typeof Command;
} }
@ -240,6 +240,7 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
{ id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' }, { id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
{ id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' }, { id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
{ id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" }, { id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
{ id: '_lbQuickSearch', title: "Quick Search", type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' }, { id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
{ id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }, { id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
{ id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' } { id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }

View File

@ -134,7 +134,9 @@ const defaultOptions: DefaultOption[] = [
{ name: "codeBlockWordWrap", value: "false", isSynced: true }, { name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration // Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true } { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "layoutOrientation", value: "vertical", isSynced: false }
]; ];
/** /**