mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-10-02 02:41:34 +08:00
Merge pull request #571 from TriliumNext/feature/classic_editor
Classic editor for text notes (with fixed toolbar)
This commit is contained in:
commit
81310d33b0
49
libraries/ckeditor/ckeditor.d.ts
vendored
Normal file
49
libraries/ckeditor/ckeditor.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import { DecoupledEditor as DecoupledEditorBase } from '@ckeditor/ckeditor5-editor-decoupled';
|
||||
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
||||
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
||||
import { FontSize, FontFamily, FontColor, FontBackgroundColor } from '@ckeditor/ckeditor5-font';
|
||||
import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
|
||||
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
||||
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
|
||||
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
||||
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
|
||||
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
|
||||
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
|
||||
import { Heading } from '@ckeditor/ckeditor5-heading';
|
||||
import { Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
|
||||
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
|
||||
import { Link } from '@ckeditor/ckeditor5-link';
|
||||
import { List, ListProperties } from '@ckeditor/ckeditor5-list';
|
||||
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
||||
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
||||
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
||||
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
||||
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
||||
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
|
||||
export default class DecoupledEditor extends DecoupledEditorBase {
|
||||
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof Alignment | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof CKFinderUploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof Strikethrough | typeof Underline | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof ListProperties | typeof Indent | typeof IndentBlock | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
|
||||
static defaultConfig: {
|
||||
toolbar: {
|
||||
items: string[];
|
||||
};
|
||||
image: {
|
||||
resizeUnit: "px";
|
||||
toolbar: string[];
|
||||
};
|
||||
table: {
|
||||
contentToolbar: string[];
|
||||
};
|
||||
list: {
|
||||
properties: {
|
||||
styles: boolean;
|
||||
startIndex: boolean;
|
||||
reversed: boolean;
|
||||
};
|
||||
};
|
||||
language: string;
|
||||
};
|
||||
}
|
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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())
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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,17 @@ 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) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
const $classicToolbarWidget = $parentSplit.find("> .ribbon-container .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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1508,5 +1508,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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'promotedAttributesOpenInRibbon',
|
||||
'editedNotesOpenInRibbon',
|
||||
'locale',
|
||||
'firstDayOfWeek'
|
||||
'firstDayOfWeek',
|
||||
'textNoteEditorType'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
@ -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: [],
|
||||
|
@ -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 }
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,8 @@
|
||||
"copy-without-formatting": "Copy selected text without formatting",
|
||||
"force-save-revision": "Force creating / saving new note revision of the active note",
|
||||
"show-help": "Shows built-in Help / cheatsheet",
|
||||
"toggle-book-properties": "Toggle Book Properties"
|
||||
"toggle-book-properties": "Toggle Book Properties",
|
||||
"toggle-classic-editor-toolbar": "Toggle the Formatting tab for the classic editor"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
|
Loading…
x
Reference in New Issue
Block a user