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 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";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
constructor(customWidgets) {
|
constructor(customWidgets) {
|
||||||
@ -140,6 +141,7 @@ export default class DesktopLayout {
|
|||||||
// the order of the widgets matter. Some of these want to "activate" themselves
|
// 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".
|
// when visible. When this happens to multiple of them, the first one "wins".
|
||||||
// promoted attributes should always win.
|
// promoted attributes should always win.
|
||||||
|
.ribbon(new ClassicEditorToolbar())
|
||||||
.ribbon(new PromotedAttributesWidget())
|
.ribbon(new PromotedAttributesWidget())
|
||||||
.ribbon(new ScriptExecutorWidget())
|
.ribbon(new ScriptExecutorWidget())
|
||||||
.ribbon(new SearchDefinitionWidget())
|
.ribbon(new SearchDefinitionWidget())
|
||||||
|
@ -347,8 +347,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.$editor.on("click", e => this.handleEditorClick(e));
|
this.$editor.on("click", e => this.handleEditorClick(e));
|
||||||
|
|
||||||
/** @property {BalloonEditor} */
|
this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig);
|
||||||
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
|
|
||||||
this.textEditor.model.document.on('change:data', () => this.dataChanged());
|
this.textEditor.model.document.on('change:data', () => this.dataChanged());
|
||||||
this.textEditor.editing.view.document.on('enter', (event, data) => {
|
this.textEditor.editing.view.document.on('enter', (event, data) => {
|
||||||
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
|
// 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
|
// disable spellcheck for attribute editor
|
||||||
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
|
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() {
|
dataChanged() {
|
||||||
|
@ -216,7 +216,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
|||||||
this.$tabContainer.empty();
|
this.$tabContainer.empty();
|
||||||
|
|
||||||
for (const ribbonWidget of this.ribbonWidgets) {
|
for (const ribbonWidget of this.ribbonWidgets) {
|
||||||
const ret = ribbonWidget.getTitle(note);
|
const ret = await ribbonWidget.getTitle(note);
|
||||||
|
|
||||||
if (!ret.show) {
|
if (!ret.show) {
|
||||||
continue;
|
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() {
|
getActiveRibbonWidget() {
|
||||||
return this.ribbonWidgets.find(ch => ch.componentId === this.lastActiveComponentId)
|
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 RibbonOptions from "./options/appearance/ribbon.js";
|
||||||
import LocalizationOptions from "./options/appearance/i18n.js";
|
import LocalizationOptions from "./options/appearance/i18n.js";
|
||||||
import CodeBlockOptions from "./options/appearance/code_block.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">
|
const TPL = `<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -68,6 +69,7 @@ const CONTENT_WIDGETS = {
|
|||||||
],
|
],
|
||||||
_optionsShortcuts: [ KeyboardShortcutsOptions ],
|
_optionsShortcuts: [ KeyboardShortcutsOptions ],
|
||||||
_optionsTextNotes: [
|
_optionsTextNotes: [
|
||||||
|
EditorOptions,
|
||||||
HeadingStyleOptions,
|
HeadingStyleOptions,
|
||||||
TableOfContentsOptions,
|
TableOfContentsOptions,
|
||||||
HighlightsListOptions,
|
HighlightsListOptions,
|
||||||
|
@ -12,7 +12,6 @@ import appContext from "../../components/app_context.js";
|
|||||||
import dialogService from "../../services/dialog.js";
|
import dialogService from "../../services/dialog.js";
|
||||||
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import { isSyntaxHighlightEnabled } from "../../services/syntax_highlight.js";
|
|
||||||
|
|
||||||
const ENABLE_INSPECTOR = false;
|
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 {
|
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||||
static getType() { return "editableText"; }
|
static getType() { return "editableText"; }
|
||||||
|
|
||||||
@ -125,6 +130,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
|
|
||||||
async initEditor() {
|
async initEditor() {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||||
|
const isClassicEditor = (options.get("textNoteEditorType") === "ckeditor-classic")
|
||||||
|
const editorClass = (isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor);
|
||||||
|
|
||||||
const codeBlockLanguages = buildListOfLanguages();
|
const codeBlockLanguages = buildListOfLanguages();
|
||||||
|
|
||||||
@ -133,7 +140,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
// display of $widget in both branches.
|
// display of $widget in both branches.
|
||||||
this.$widget.show();
|
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).
|
// 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
|
// 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
|
// 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) => {
|
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
|
||||||
const editor = await BalloonEditor.create(elementOrData, editorConfig);
|
const editor = await editorClass.create(elementOrData, editorConfig);
|
||||||
|
|
||||||
await initSyntaxHighlighting(editor);
|
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());
|
editor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());
|
||||||
|
|
||||||
if (glob.isDev && ENABLE_INSPECTOR) {
|
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": {
|
"code_block": {
|
||||||
"word_wrapping": "Word wrapping"
|
"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',
|
'promotedAttributesOpenInRibbon',
|
||||||
'editedNotesOpenInRibbon',
|
'editedNotesOpenInRibbon',
|
||||||
'locale',
|
'locale',
|
||||||
'firstDayOfWeek'
|
'firstDayOfWeek',
|
||||||
|
'textNoteEditorType'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getOptions() {
|
function getOptions() {
|
||||||
|
@ -420,6 +420,12 @@ function getDefaultKeyboardActions() {
|
|||||||
separator: t("keyboard_actions.ribbon-tabs")
|
separator: t("keyboard_actions.ribbon-tabs")
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
actionName: "toggleRibbonTabClassicEditor",
|
||||||
|
defaultShortcuts: [],
|
||||||
|
description: t("keyboard_actions.toggle-classic-editor-toolbar"),
|
||||||
|
scope: "window"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
actionName: "toggleRibbonTabBasicProperties",
|
actionName: "toggleRibbonTabBasicProperties",
|
||||||
defaultShortcuts: [],
|
defaultShortcuts: [],
|
||||||
|
@ -131,7 +131,10 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
return "default:stackoverflow-dark";
|
return "default:stackoverflow-dark";
|
||||||
}
|
}
|
||||||
}, isSynced: false },
|
}, 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",
|
"copy-without-formatting": "Copy selected text without formatting",
|
||||||
"force-save-revision": "Force creating / saving new note revision of the active note",
|
"force-save-revision": "Force creating / saving new note revision of the active note",
|
||||||
"show-help": "Shows built-in Help / cheatsheet",
|
"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": {
|
"login": {
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user