Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ core repository, overriding anything in there.

### Type checking

The source is being converted to TypeScript. Files ending in `.ts` are checked in strict mode; files still ending in
`.js` are not checked. Webpack and jest strip types without checking them, so run the checker separately:
The source and tests are TypeScript, checked in strict mode. Webpack and jest strip types without checking them, so run
the checker separately:

```
npm run typecheck
Expand Down
44 changes: 0 additions & 44 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,50 +44,6 @@ export default [
{
ignores: ["tmp/", "coverage/", "node_modules/"],
},
{
files: ["src/**/*.js"],
languageOptions: {
globals: {
...globals.browser,
"jQuery": true,
"I18n": true,
"_": true
}
},
rules: {
"no-cond-assign": "off",
"no-unused-vars": [
"error",
{
// "args": "all",
"argsIgnorePattern": "^_",
// "caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
// "destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
// "ignoreRestSiblings": true
}
],
"no-undef": "error"
}
},
{
files: ["jest.setup.js", "tests/**/*.js"],
plugins: {
jest: jestPlugin
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.jest
}
},
rules: {
"no-unused-vars": "error",
"no-undef": "error"
}
},
{
files: ["jest.config.js", 'babel.config.js', 'webpack.config.js'],
languageOptions: {
Expand Down
28 changes: 10 additions & 18 deletions src/helpers/button-disabler.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
// The slice of an editor these helpers touch. It is declared structurally because
// `toolbar` only exists on some UI views, `_items` is private in CKEditor's
// ViewCollection, and `__currentlyDisabled` is bookkeeping this file adds.
import type { Editor } from '@ckeditor/ckeditor5-core';

interface ToolbarItem {
isEnabled?: boolean;
}

interface EditorWithToolbar {
// `__currentlyDisabled` is bookkeeping these helpers keep on the editor.
type EditorWithDisabledItems = Editor & {
__currentlyDisabled?: ToolbarItem[];
ui: {
view: {
toolbar?: {
items: {
_items: ToolbarItem[];
};
};
};
};
}
};

export function getToolbarItems(editor: EditorWithToolbar) {
export function getToolbarItems(editor: EditorWithDisabledItems) {
editor.__currentlyDisabled = editor.__currentlyDisabled || [];

if (!editor.ui.view.toolbar) {
return [];
}

return editor.ui.view.toolbar.items._items;
// `_items` is private in CKEditor's ViewCollection.
return (editor.ui.view.toolbar.items as unknown as { _items: ToolbarItem[] })._items;
}

export function disableItems(editor: EditorWithToolbar, except?: ToolbarItem) {
export function disableItems(editor: EditorWithDisabledItems, except?: ToolbarItem) {
getToolbarItems(editor).forEach((item) => {
if (item === except || !Object.prototype.hasOwnProperty.call(item, 'isEnabled')) {
return;
Expand All @@ -42,7 +34,7 @@ export function disableItems(editor: EditorWithToolbar, except?: ToolbarItem) {
});
}

export function enableItems(editor: EditorWithToolbar) {
export function enableItems(editor: EditorWithDisabledItems) {
getToolbarItems(editor).forEach((item) => {
if (Object.prototype.hasOwnProperty.call(item, 'isEnabled')
&& editor.__currentlyDisabled!.indexOf(item) < 0) {
Expand Down
20 changes: 13 additions & 7 deletions src/op-ckeditor-config.js → src/op-ckeditor-config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { Editor, EditorConfig } from "@ckeditor/ckeditor5-core";
import type { MentionFeed } from "@ckeditor/ckeditor5-mention";
import {userMentions} from "./mentions/user-mentions";
import {workPackageMentions} from "./mentions/work-package-mentions";
import {customItemRenderer, emojiItemRenderer} from './mentions/mentions-item-renderer';
import {emojiMentions} from "./mentions/emoji-mentions";
import {saveInLocalStorage} from "./plugins/op-content-revisions/storage";

export const defaultConfig = {
// CKEditor allows feed items to be plain strings. Our feeds only yield objects,
// which is what the item renderers take.
type ItemRenderer = MentionFeed["itemRenderer"];

export const defaultConfig: EditorConfig = {
autosave: {
waitingTime: 5000,
save(editor) {
save(editor: Editor) {
return saveInLocalStorage(editor);
},
},
Expand Down Expand Up @@ -94,31 +100,31 @@ export const defaultConfig = {
{
marker: '@',
feed: userMentions,
itemRenderer: customItemRenderer,
itemRenderer: customItemRenderer as ItemRenderer,
minimumCharacters: 0
},
{
marker: '###',
feed: workPackageMentions('###'),
itemRenderer: customItemRenderer,
itemRenderer: customItemRenderer as ItemRenderer,
minimumCharacters: 1
},
{
marker: '##',
feed: workPackageMentions('##'),
itemRenderer: customItemRenderer,
itemRenderer: customItemRenderer as ItemRenderer,
minimumCharacters: 1
},
{
marker: '#',
feed: workPackageMentions('#'),
itemRenderer: customItemRenderer,
itemRenderer: customItemRenderer as ItemRenderer,
minimumCharacters: 1
},
{
marker: ':',
feed: emojiMentions,
itemRenderer: emojiItemRenderer,
itemRenderer: emojiItemRenderer as ItemRenderer,
minimumCharacters: 1
}
]
Expand Down
14 changes: 10 additions & 4 deletions src/op-ckeditor.js → src/op-ckeditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import {builtinPlugins} from './op-plugins';
import {defaultConfig} from "./op-ckeditor-config";
import {configurationCustomizer} from './op-config-customizer';

export class ConstrainedEditor extends DecoupledEditor {}
export class FullEditor extends DecoupledEditor {}
type CreateCustomized = ReturnType<typeof configurationCustomizer>;

export class ConstrainedEditor extends DecoupledEditor {
declare static createCustomized: CreateCustomized;
}
export class FullEditor extends DecoupledEditor {
declare static createCustomized: CreateCustomized;
}

// Export the two common interfaces
window.OPConstrainedEditor = ConstrainedEditor;
Expand All @@ -17,7 +23,7 @@ window.OPEditorWatchdog = EditorWatchdog;
FullEditor.createCustomized = configurationCustomizer(FullEditor);
FullEditor.builtinPlugins = builtinPlugins;
FullEditor.defaultConfig = Object.assign({}, defaultConfig);
FullEditor.defaultConfig.toolbar = {
FullEditor.defaultConfig!.toolbar = {
items: [
'heading',
'|',
Expand Down Expand Up @@ -51,7 +57,7 @@ FullEditor.defaultConfig.toolbar = {
ConstrainedEditor.createCustomized = configurationCustomizer(ConstrainedEditor);
ConstrainedEditor.builtinPlugins = builtinPlugins;
ConstrainedEditor.defaultConfig = Object.assign({}, defaultConfig);
ConstrainedEditor.defaultConfig.toolbar = {
ConstrainedEditor.defaultConfig!.toolbar = {
items: [
'bold',
'italic',
Expand Down
14 changes: 9 additions & 5 deletions src/op-config-customizer.js → src/op-config-customizer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { EditorConfig } from '@ckeditor/ckeditor5-core';
import type { DecoupledEditor } from '@ckeditor/ckeditor5-editor-decoupled';
import {opImageUploadPlugins, opMacroPlugins} from './op-plugins';

export function configurationCustomizer(editorClass) {
return (wrapper, configuration) => {
const context = configuration.openProject.context;
export function configurationCustomizer(editorClass: typeof DecoupledEditor) {
return (wrapper: HTMLElement | string, configuration: EditorConfig) => {
// TODO(OP-18993): throws when core passes no openProject configuration.
const context = configuration.openProject!.context;

// We're going to remove some plugins from the default configuration
// when we detect they are unsupported in the current context
Expand All @@ -16,13 +19,14 @@ export function configurationCustomizer(editorClass) {

// Disable macros entirely
if (context.macros === false) {
configuration.openProject.disableAllMacros = true;
configuration.openProject!.disableAllMacros = true;
configuration.removePlugins.push(...opMacroPlugins.map(el => el.pluginName))
}

// Enable selective macros
if (Array.isArray(context.macros)) {
const disabledMacros = opMacroPlugins.filter(plugin => context.macros.indexOf(plugin.pluginName) === -1);
// The Array.isArray() check above does not carry into the callback.
const disabledMacros = opMacroPlugins.filter(plugin => (context.macros as string[]).indexOf(plugin.pluginName) === -1);
configuration.removePlugins.push(...disabledMacros);
}

Expand Down
File renamed without changes.
52 changes: 52 additions & 0 deletions src/op-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@
export interface OpResource {
_type: string;
id?: string | number | null;
href?: string | null;
canAddAttachments?: boolean;
$embedded?: {
workPackage?: OpResource;
};
// Present on resources that can hold attachments.
lookupDownloadLocationByName?(name: string): string | null | undefined;
}

/** An attachment as returned by core after an upload. */
export interface OpAttachment {
_links: {
staticDownloadLocation: {
href: string;
};
};
}

/** The editing context: which resource and field, and what is enabled. */
Expand Down Expand Up @@ -48,6 +60,32 @@ export interface OpServices {
i18n: {
t(key: string, options?: Record<string, unknown>): string;
};
// Opens core's modals for configuring a macro and resolves with the result.
// Declared as this repository calls them. Core's own declarations are looser
// in places: configureChildPages() takes includeParent as a string and
// resolves with `object`, and editCodeBlock() requires both arguments.
macros: {
editCodeBlock(content?: string, languageClass?: string): Promise<{ content: string; languageClass: string }>;
configureWorkPackageButton(type?: string, classes?: string): Promise<{ type: string; classes: string }>;
configureChildPages(page: string, includeParent?: boolean): Promise<{ page: string; includeParent: boolean }>;
};
// Opens core's query configuration modal for an embedded table.
externalQueryConfiguration: {
show(options: { currentQuery: unknown; callback: (newQuery: unknown) => void }): void;
};
turboRequests: {
request(url: string, options?: { method?: string }): Promise<unknown>;
};
notifications: {
addError(message: string): void;
};
timezone: {
formattedRelativeDateTime(datetimeString: string): string;
};
Comment on lines +82 to +84
attachmentsResourceService: {
// Returns an RxJS observable; only toPromise() is used here.
attachFiles(resource: OpResource, files: File[]): { toPromise(): Promise<OpAttachment[]> };
};
}

/** Helper functions of core that plugins call into. */
Expand Down Expand Up @@ -85,12 +123,26 @@ export interface OpMentionFeedItem extends OpMention {
name: string;
}

export interface OpWidgetToolbarConfig {
toolbar: string[];
}

// The import makes this a module augmentation rather than a new declaration.
import type {} from '@ckeditor/ckeditor5-core';

declare module '@ckeditor/ckeditor5-core' {
interface EditorConfig {
openProject?: OpConfig;
disabledMentions?: string[];
// Key under which the content revisions plugin stores drafts.
opContentRevisionKey?: string;
// Overrides the default revisions key.
storageKey?: string;
// Toolbars of our widgets; see createEditToolbar().
OPMacroEmbeddedTable?: OpWidgetToolbarConfig;
OPMacroWpButton?: OpWidgetToolbarConfig;
OPWikiIncludePage?: OpWidgetToolbarConfig;
OPCodeBlock?: OpWidgetToolbarConfig;
OPChildPages?: OpWidgetToolbarConfig;
}
}
13 changes: 0 additions & 13 deletions src/plugins/code-block/click-observer.js

This file was deleted.

22 changes: 22 additions & 0 deletions src/plugins/code-block/click-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DomEventObserver } from '@ckeditor/ckeditor5-engine';
import type { BubblingEvent, EditingView, ViewDocumentDomEventData } from '@ckeditor/ckeditor5-engine';

// The event this observer fires on the view document.
export type ViewDocumentDoubleClickEvent = BubblingEvent<{
name: 'dblclick';
args: [data: ViewDocumentDomEventData<MouseEvent>];
}>;

export default class DoubleClickObserver extends DomEventObserver<'dblclick'> {
declare domEventType: 'dblclick';

constructor( view: EditingView ) {
super( view );

this.domEventType = 'dblclick';
}

onDomEvent( domEvent: MouseEvent ) {
this.fire( domEvent.type, domEvent );
}
}
Loading
Loading