Merge branch 'develop' into siriusxt_patch

This commit is contained in:
SiriusXT
2024-11-11 18:26:07 +08:00
24 changed files with 1751 additions and 18 deletions

View File

@@ -82,6 +82,7 @@ import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -140,6 +141,7 @@ export default class DesktopLayout {
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new PromotedAttributesWidget())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())

View File

@@ -23,6 +23,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
const MOBILE_CSS = `
<style>
@@ -167,6 +168,7 @@ export default class MobileLayout {
.child(new NoteListWidget())
.child(new FilePropertiesWidget().css('font-size','smaller'))
)
.child(new ClassicEditorToolbar())
)
.child(new ProtectedSessionPasswordDialog())
.child(new ConfirmDialog())

View File

@@ -527,6 +527,58 @@ function downloadSvg(nameWithoutExtension, svgContent) {
document.body.removeChild(element);
}
/**
* Compares two semantic version strings.
* Returns:
* 1 if v1 is greater than v2
* 0 if v1 is equal to v2
* -1 if v1 is less than v2
*
* @param {string} v1 First version string
* @param {string} v2 Second version string
* @returns {number}
*/
function compareVersions(v1, v2) {
// Remove 'v' prefix and everything after dash if present
v1 = v1.replace(/^v/, '').split('-')[0];
v2 = v2.replace(/^v/, '').split('-')[0];
const v1parts = v1.split('.').map(Number);
const v2parts = v2.split('.').map(Number);
// Pad shorter version with zeros
while (v1parts.length < 3) v1parts.push(0);
while (v2parts.length < 3) v2parts.push(0);
// Compare major version
if (v1parts[0] !== v2parts[0]) {
return v1parts[0] > v2parts[0] ? 1 : -1;
}
// Compare minor version
if (v1parts[1] !== v2parts[1]) {
return v1parts[1] > v2parts[1] ? 1 : -1;
}
// Compare patch version
if (v1parts[2] !== v2parts[2]) {
return v1parts[2] > v2parts[2] ? 1 : -1;
}
return 0;
}
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
* @param {string} latestVersion
* @param {string} currentVersion
* @returns {boolean}
*/
function isUpdateAvailable(latestVersion, currentVersion) {
return compareVersions(latestVersion, currentVersion) > 0;
}
export default {
reloadFrontendApp,
parseDate,
@@ -567,5 +619,7 @@ export default {
areObjectsEqual,
copyHtmlToClipboard,
createImageSrcUrl,
downloadSvg
downloadSvg,
compareVersions,
isUpdateAvailable
};

View File

@@ -347,8 +347,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
this.$editor.on("click", e => this.handleEditorClick(e));
/** @property {BalloonEditor} */
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig);
this.textEditor.model.document.on('change:data', () => this.dataChanged());
this.textEditor.editing.view.document.on('enter', (event, data) => {
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
@@ -358,9 +357,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
// disable spellcheck for attribute editor
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
//await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector');
//CKEditorInspector.attach(this.textEditor);
}
dataChanged() {

View File

@@ -333,7 +333,8 @@ export default class GlobalMenuWidget extends BasicWidget {
const latestVersion = await this.fetchLatestVersion();
this.updateAvailableWidget.updateVersionStatus(latestVersion);
this.$updateToLatestVersionButton.toggle(latestVersion > glob.triliumVersion);
// Show "click to download" button in options menu if there's a new version available
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
}

View File

@@ -1,5 +1,6 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
const TPL = `
<div style="display: none;">
@@ -34,6 +35,6 @@ export default class UpdateAvailableWidget extends BasicWidget {
}
updateVersionStatus(latestVersion) {
this.$widget.toggle(latestVersion > glob.triliumVersion);
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
}
}

View File

@@ -216,7 +216,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
const ret = ribbonWidget.getTitle(note);
const ret = await ribbonWidget.getTitle(note);
if (!ret.show) {
continue;
@@ -351,6 +351,16 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
/**
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
*
* <p>
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
*/
readOnlyTemporarilyDisabledEvent() {
this.refresh();
}
getActiveRibbonWidget() {
return this.ribbonWidgets.find(ch => ch.componentId === this.lastActiveComponentId)
}

View File

@@ -0,0 +1,83 @@
import { t } from "../../services/i18n.js";
import options from "../../services/options.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `\
<div class="classic-toolbar-widget"></div>
<style>
.classic-toolbar-widget {
--ck-color-toolbar-background: transparent;
--ck-color-button-default-background: transparent;
--ck-color-button-default-disabled-background: transparent;
min-height: 39px;
}
.classic-toolbar-widget .ck.ck-toolbar {
border: none;
}
.classic-toolbar-widget .ck.ck-button.ck-disabled {
opacity: 0.3;
}
body.mobile .classic-toolbar-widget {
position: relative;
overflow-x: auto;
}
body.mobile .classic-toolbar-widget .ck.ck-toolbar {
position: absolute;
}
</style>
`;
/**
* Handles the editing toolbar when the CKEditor is in decoupled mode.
*
* <p>
* This toolbar is only enabled if the user has selected the classic CKEditor.
*
* <p>
* The ribbon item is active by default for text notes, as long as they are not in read-only mode.
*/
export default class ClassicEditorToolbar extends NoteContextAwareWidget {
get name() {
return "classicEditor";
}
get toggleCommand() {
return "toggleRibbonTabClassicEditor";
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
}
async getTitle() {
return {
show: await this.#shouldDisplay(),
activate: true,
title: t("classic_editor_toolbar.title"),
icon: "bx bx-edit-alt"
};
}
async #shouldDisplay() {
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
return false;
}
if (this.note.type !== "text") {
return false;
}
if (await this.noteContext.isReadOnly()) {
return false;
}
return true;
}
}

View File

@@ -35,6 +35,7 @@ import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_
import RibbonOptions from "./options/appearance/ribbon.js";
import LocalizationOptions from "./options/appearance/i18n.js";
import CodeBlockOptions from "./options/appearance/code_block.js";
import EditorOptions from "./options/text_notes/editor.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style>
@@ -68,6 +69,7 @@ const CONTENT_WIDGETS = {
],
_optionsShortcuts: [ KeyboardShortcutsOptions ],
_optionsTextNotes: [
EditorOptions,
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,

View File

@@ -12,7 +12,6 @@ import appContext from "../../components/app_context.js";
import dialogService from "../../services/dialog.js";
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
import options from "../../services/options.js";
import { isSyntaxHighlightEnabled } from "../../services/syntax_highlight.js";
const ENABLE_INSPECTOR = false;
@@ -107,6 +106,12 @@ function buildListOfLanguages() {
];
}
/**
* The editor can operate into two distinct modes:
*
* - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph).
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
static getType() { return "editableText"; }
@@ -125,6 +130,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const isClassicEditor = (options.get("textNoteEditorType") === "ckeditor-classic")
const editorClass = (isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor);
const codeBlockLanguages = buildListOfLanguages();
@@ -133,7 +140,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
// display of $widget in both branches.
this.$widget.show();
this.watchdog = new EditorWatchdog(BalloonEditor, {
this.watchdog = new CKEditor.EditorWatchdog(editorClass, {
// An average number of milliseconds between the last editor errors (defaults to 5000).
// When the period of time between errors is lower than that and the crashNumberLimit
// is also reached, the watchdog changes its state to crashedPermanently, and it stops
@@ -169,10 +176,23 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
});
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
const editor = await BalloonEditor.create(elementOrData, editorConfig);
const editor = await editorClass.create(elementOrData, editorConfig);
await initSyntaxHighlighting(editor);
if (isClassicEditor) {
let $classicToolbarWidget;
if (!utils.isMobile()) {
const $parentSplit = this.$widget.parents(".note-split.type-text");
$classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
} else {
$classicToolbarWidget = $("body").find(".classic-toolbar-widget");
}
$classicToolbarWidget.empty();
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
}
editor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());
if (glob.isDev && ENABLE_INSPECTOR) {

View File

@@ -0,0 +1,30 @@
import { t } from "../../../../services/i18n.js";
import utils from "../../../../services/utils.js";
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>${t("editing.editor_type.label")}</h4>
<select class="editor-type-select form-select">
<option value="ckeditor-balloon">${t("editing.editor_type.floating")}</option>
<option value="ckeditor-classic">${t("editing.editor_type.fixed")}</option>
</select>
</div>`;
export default class EditorOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$body = $("body");
this.$editorType = this.$widget.find(".editor-type-select");
this.$editorType.on('change', async () => {
const newEditorType = this.$editorType.val();
await this.updateOption('textNoteEditorType', newEditorType);
utils.reloadFrontendApp("editor type change");
});
}
async optionsLoaded(options) {
this.$editorType.val(options.textNoteEditorType);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1512,5 +1512,18 @@
},
"code_block": {
"word_wrapping": "Word wrapping"
},
"classic_editor_toolbar": {
"title": "Formatting"
},
"editor": {
"title": "Editor"
},
"editing": {
"editor_type": {
"label": "Formatting toolbar",
"floating": "Floating (editing tools appear near the cursor)",
"fixed": "Fixed (editing tools appear in the \"Formatting\" ribbon tab)"
}
}
}

View File

@@ -1508,5 +1508,18 @@
},
"code_block": {
"word_wrapping": "Încadrare text"
},
"classic_editor_toolbar": {
"title": "Formatare"
},
"editing": {
"editor_type": {
"fixed": "Editor cu bară fixă (uneltele de editare vor apărea în tab-ul „Formatare” din panglică)",
"floating": "Editor cu bară flotantă (uneltele de editare vor apărea lângă cursor)",
"label": "Bară de formatare"
}
},
"editor": {
"title": "Editor"
}
}

View File

@@ -65,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
'promotedAttributesOpenInRibbon',
'editedNotesOpenInRibbon',
'locale',
'firstDayOfWeek'
'firstDayOfWeek',
'textNoteEditorType'
]);
function getOptions() {
@@ -152,6 +153,10 @@ function getSupportedLocales() {
"id": "en",
"name": "English"
},
{
"id": "de",
"name": "Deutsch"
},
{
"id": "es",
"name": "Español"

View File

@@ -420,6 +420,12 @@ function getDefaultKeyboardActions() {
separator: t("keyboard_actions.ribbon-tabs")
},
{
actionName: "toggleRibbonTabClassicEditor",
defaultShortcuts: [],
description: t("keyboard_actions.toggle-classic-editor-toolbar"),
scope: "window"
},
{
actionName: "toggleRibbonTabBasicProperties",
defaultShortcuts: [],

View File

@@ -131,7 +131,10 @@ const defaultOptions: DefaultOption[] = [
return "default:stackoverflow-dark";
}
}, isSynced: false },
{ name: "codeBlockWordWrap", value: "false", isSynced: true }
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }
];
/**