Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a5de0d
client: Basic integration of classic editor w/ no attribute editor
eliandoran Nov 8, 2024
01c53b6
client: Use same config as bubble editor for classic
eliandoran Nov 8, 2024
4473443
client: Remove block toolbar in classic mode
eliandoran Nov 8, 2024
821af8d
client: Integrate block toolbar into classic options
eliandoran Nov 9, 2024
918f425
client: Group options for classic editor
eliandoran Nov 9, 2024
48bc920
client: Create empty toolbar ribbon
eliandoran Nov 9, 2024
dd6e762
client: Activate ribbon toolbar by default
eliandoran Nov 9, 2024
4f39188
client: Use decoupled CKEditor
eliandoran Nov 9, 2024
787aa6f
client: Remove background for decoupled editor
eliandoran Nov 9, 2024
b88f0e0
client: Hide ribbon for non text or read-only notes
eliandoran Nov 9, 2024
85ee7de
client: Improve loading feel for classic toolbar
eliandoran Nov 9, 2024
6a11f9c
client: Add some JSDoc
eliandoran Nov 9, 2024
5771060
client: Reorganize classic toolbar
eliandoran Nov 9, 2024
06262ad
client: Use translation for classic toolbar title
eliandoran Nov 9, 2024
3972bb2
client: Use build of CKEditor containing both types
eliandoran Nov 9, 2024
745c984
client: Use better method to expose CK watchdog
eliandoran Nov 9, 2024
d2008e7
client: Use different method to highlight disabled buttons
eliandoran Nov 9, 2024
7a70fc1
server: Set up editor type option
eliandoran Nov 9, 2024
89420ea
client: Set up ui for selecting editor UI
eliandoran Nov 9, 2024
c421e75
client: Respect editor type choice
eliandoran Nov 9, 2024
6e0a10c
client: Hide ribbon tab when classic editor is off
eliandoran Nov 9, 2024
70a98a3
client: Use refactored version of CKEditor
eliandoran Nov 9, 2024
f88d322
client: Repair attribute editor
eliandoran Nov 9, 2024
8c69d47
client,server: Implement shortcut for toggle classic editor toolbar
eliandoran Nov 9, 2024
7c342ae
client: Use translations for editor settings
eliandoran Nov 9, 2024
15b4eac
client: Change design of editor settings slightly
eliandoran Nov 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions libraries/ckeditor/ckeditor.d.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 1 addition & 1 deletion libraries/ckeditor/ckeditor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion libraries/ckeditor/ckeditor.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/public/app/layouts/desktop_layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
Expand Down
6 changes: 1 addition & 5 deletions src/public/app/widgets/attribute_widgets/attribute_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down
12 changes: 11 additions & 1 deletion src/public/app/widgets/containers/ribbon_container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}
Expand Down
74 changes: 74 additions & 0 deletions src/public/app/widgets/ribbon_widgets/classic_editor_toolbar.js
Original file line number Diff line number Diff line change
@@ -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;
}

}
2 changes: 2 additions & 0 deletions src/public/app/widgets/type_widgets/content_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -68,6 +69,7 @@ const CONTENT_WIDGETS = {
],
_optionsShortcuts: [ KeyboardShortcutsOptions ],
_optionsTextNotes: [
EditorOptions,
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,
Expand Down
20 changes: 17 additions & 3 deletions src/public/app/widgets/type_widgets/editable_text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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"; }

Expand All @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions src/public/app/widgets/type_widgets/options/text_notes/editor.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
13 changes: 13 additions & 0 deletions src/public/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
3 changes: 2 additions & 1 deletion src/routes/api/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
'promotedAttributesOpenInRibbon',
'editedNotesOpenInRibbon',
'locale',
'firstDayOfWeek'
'firstDayOfWeek',
'textNoteEditorType'
]);

function getOptions() {
Expand Down
6 changes: 6 additions & 0 deletions src/services/keyboard_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
5 changes: 4 additions & 1 deletion src/services/options_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
];

/**
Expand Down
3 changes: 2 additions & 1 deletion translations/en/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down