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
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@coremedia/ckeditor5-dom-converter": "26.0.1-rc.0",
"@coremedia/ckeditor5-font-mapper": "26.0.1-rc.0",
"@coremedia/ckeditor5-link-common": "26.0.1-rc.0",
"@coremedia/ckeditor5-text-direction": "workspace:*",
"@coremedia/service-agent": "^2.1.2",
"ckeditor5": "46.1.1",
"xml-formatter": "^3.6.2"
Expand Down
81 changes: 47 additions & 34 deletions app/src/editors/richtext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,47 @@

// ImageBlockEditing: See ckeditor/ckeditor5#12027.

import { DialogVisibility } from "@coremedia/ckeditor5-dialog-visibility";
import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist";
import { ContentClipboard, PasteContentPlugin } from "@coremedia/ckeditor5-coremedia-content-clipboard";
import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing";
import { ContentImagePlugin } from "@coremedia/ckeditor5-coremedia-images";
import {
ContentLinks,
COREMEDIA_CONTEXT_KEY,
COREMEDIA_LINK_CONFIG_KEY,
LinkTarget,
} from "@coremedia/ckeditor5-coremedia-link";
import { ContentClipboard, PasteContentPlugin } from "@coremedia/ckeditor5-coremedia-content-clipboard";
import { ContentImagePlugin } from "@coremedia/ckeditor5-coremedia-images";
import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-mapper";
import type {
LatestCoreMediaRichTextConfig,
V10CoreMediaRichTextConfig,
} from "@coremedia/ckeditor5-coremedia-richtext";
import {
COREMEDIA_MOCK_CONTENT_PLUGIN,
MockInputExamplePlugin,
MockStudioIntegration,
} from "@coremedia/ckeditor5-coremedia-studio-integration-mock";
replaceByElementAndClassBackAndForth,
replaceElementByElementAndClass,
stripFixedAttributes,
} from "@coremedia/ckeditor5-coremedia-richtext";
import "ckeditor5/ckeditor5.css";
import {
COREMEDIA_RICHTEXT_CONFIG_KEY,
COREMEDIA_RICHTEXT_SUPPORT_CONFIG_KEY,
CoreMediaStudioEssentials,
Strictness,
} from "@coremedia/ckeditor5-coremedia-studio-essentials";
import type { PluginConstructor } from "ckeditor5";
import {
COREMEDIA_MOCK_CONTENT_PLUGIN,
MockInputExamplePlugin,
MockStudioIntegration,
} from "@coremedia/ckeditor5-coremedia-studio-integration-mock";
import { DataFacade } from "@coremedia/ckeditor5-data-facade";
import type { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support";
import { DialogVisibility } from "@coremedia/ckeditor5-dialog-visibility";
import type { RuleConfig } from "@coremedia/ckeditor5-dom-converter";
import { FontMapper as CoreMediaFontMapper } from "@coremedia/ckeditor5-font-mapper";
import type { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common";
import { LinkAttributes } from "@coremedia/ckeditor5-link-common";
import type { PluginConstructor, TextPartLanguageOption } from "ckeditor5";
import {
TextPartLanguage,
Alignment,
Autoformat,
AutoLink,
Expand Down Expand Up @@ -63,25 +81,9 @@ import {
TableToolbar,
Underline,
} from "ckeditor5";
import type { RuleConfig } from "@coremedia/ckeditor5-dom-converter";
import type {
LatestCoreMediaRichTextConfig,
V10CoreMediaRichTextConfig,
} from "@coremedia/ckeditor5-coremedia-richtext";
import {
replaceByElementAndClassBackAndForth,
replaceElementByElementAndClass,
stripFixedAttributes,
} from "@coremedia/ckeditor5-coremedia-richtext";
import "ckeditor5/ckeditor5.css";
import type { FilterRuleSetConfiguration } from "@coremedia/ckeditor5-dataprocessor-support";
import type { LinkAttributesConfig } from "@coremedia/ckeditor5-link-common";
import { LinkAttributes } from "@coremedia/ckeditor5-link-common";
import { Differencing } from "@coremedia/ckeditor5-coremedia-differencing";
import { Blocklist } from "@coremedia/ckeditor5-coremedia-blocklist";
import { DataFacade } from "@coremedia/ckeditor5-data-facade";
import type { CKEditorInstanceFactory } from "../CKEditorInstanceFactory";
import { TextDirection } from "@coremedia/ckeditor5-text-direction";
import type { ApplicationState } from "../ApplicationState";
import type { CKEditorInstanceFactory } from "../CKEditorInstanceFactory";
import { getHashParam } from "../HashParams";
import { initInputExampleContent } from "../inputExampleContents";
import { updatePreview } from "../preview";
Expand Down Expand Up @@ -177,9 +179,15 @@ const getRichTextConfig = (
return {
strictness: Strictness.STRICT,
compatibility: "latest",
rules: richTextRuleConfigurations,
rules: [...richTextRuleConfigurations],
};
};
const textPartLanguage: TextPartLanguageOption[] = [
{ title: "Arabic", languageCode: "ar" },
{ title: "English", languageCode: "en" },
{ title: "Español", languageCode: "es" },
{ title: "Français", languageCode: "fr" },
];
export const createRichTextEditor: CKEditorInstanceFactory = async (
sourceElement: HTMLElement,
state: ApplicationState,
Expand All @@ -193,6 +201,13 @@ export const createRichTextEditor: CKEditorInstanceFactory = async (
return ClassicEditor.create(sourceElement, {
licenseKey,
placeholder: "Type your text here...",
language: {
// Language switch only applies to editor instance.
ui: uiLanguage,
// Won't change the language of content.
content: "en",
textPartLanguage,
},
plugins: [
...imagePlugins,
Alignment,
Expand Down Expand Up @@ -231,6 +246,8 @@ export const createRichTextEditor: CKEditorInstanceFactory = async (
Superscript,
Table,
TableToolbar,
TextDirection,
TextPartLanguage,
Underline,
CoreMediaFontMapper,
MockInputExamplePlugin,
Expand All @@ -255,6 +272,8 @@ export const createRichTextEditor: CKEditorInstanceFactory = async (
"|",
"link",
"|",
"textPartLanguage",
"textDirection",
"alignment",
"blockQuote",
"codeBlock",
Expand Down Expand Up @@ -412,12 +431,6 @@ export const createRichTextEditor: CKEditorInstanceFactory = async (
table: {
contentToolbar: ["tableColumn", "tableRow", "mergeTableCells"],
},
language: {
// Language switch only applies to editor instance.
ui: uiLanguage,
// Won't change the language of content.
content: "en",
},
autosave: {
waitingTime: 1000, // in ms
},
Expand Down
23 changes: 23 additions & 0 deletions packages/ckeditor5-text-direction/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { defineConfig } from "eslint/config";
import tsParser from "@typescript-eslint/parser";
import { configs as workspaceConfig } from "../../eslint.config.mjs";

export default defineConfig([
...workspaceConfig,
{
languageOptions: {
parser: tsParser,
},
},
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
project: "./tsconfig.json",
},
},
},
]);
65 changes: 65 additions & 0 deletions packages/ckeditor5-text-direction/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@coremedia/ckeditor5-text-direction",
"version": "26.0.1-rc.0",
"type": "module",
"description": "Plugin to handle text direction in CKEditor.",
"author": {
"name": "CoreMedia GmbH",
"email": "info@coremedia.com",
"url": "https://coremedia.com/",
"avatar": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2OTguNjY3IiBoZWlnaHQ9IjY5OC42NjciIHZlcnNpb249IjEuMCIgdmlld0JveD0iMCAwIDUyNCA1MjQiPjxwYXRoIGQ9Ik0yODIuNS42QzI0NSA0LjUgMjE1LjEgMTIuOSAxODUgMjcuOSAxMDYuNCA2Ny4yIDUzLjUgMTQyLjEgNDIuNCAyMjkuNWMtMi4xIDE3LjItMi4yIDQ4IDAgNjQuMUM0OCAzMzYuMiA2MS4zIDM3Mi4yIDg0IDQwNi4zYzYxLjkgOTMuMSAxNzUuNCAxMzYuNCAyODUgMTA4LjcgMTkuMi00LjggMzMuNy0xMC4zIDUzLjItMjAuMSAyMS0xMC41IDQyLjUtMjUuMyA1OC4zLTQwLjJsOC03LjYtMjEuNS0yMS42Yy0yMy42LTIzLjgtMjcuMS0yNi43LTM4LjYtMzIuNC0yMi40LTExLjItNDQuNy0xMS42LTcxLjctMS41LTE4LjYgNy0zMC40IDkuNC00OC43IDEwLjEtMTMgLjQtMTcuNS4yLTI3LjgtMS42LTMwLjUtNS4yLTU1LTE3LjgtNzYuOC0zOS41LTMzLjYtMzMuNi00Ny44LTgwLjQtMzguNC0xMjYuNCAxMC4yLTUwLjUgNDYuMi05MC41IDk1LjItMTA2LjEgMzEuNy0xMC4xIDY0LjItOC44IDk2LjYgMy45IDEyLjggNC45IDIyLjcgNyAzNC4xIDcgMTYuNCAwIDMxLjItNC4yIDQ1LjEtMTIuNyA1LjUtMy40IDEzLjYtMTAuNyAzMC40LTI3LjNsMjIuOC0yMi42LTkuMy04LjRjLTM5LjgtMzUuNC04NS42LTU3LTEzOC40LTY1LjEtMTIuNS0yLTQ4LjctMy4zLTU5LTIuM3oiLz48cGF0aCBkPSJNMjk2IDIxOC4xYy0yOC4zIDQuOC00NC4zIDM1LjQtMzIuMSA2MS40IDcuOSAxNyAyNy4yIDI3LjQgNDUuNCAyNC41IDI4LjktNC42IDQ1LjEtMzUuMiAzMi43LTYxLjctOC0xNy4yLTI3LjMtMjcuMy00Ni0yNC4yeiIvPjwvc3ZnPg=="
},
"keywords": [
"coremedia"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/src/index.js",
"import": "./dist/src/index.js"
}
},
"publishConfig": {
"directory": "dist",
"linkDirectory": false,
"exports": {
".": {
"types": "./src/index.d.ts",
"default": "./src/index.js"
}
}
},
"files": [
"/src"
],
"license": "Apache-2.0",
"devDependencies": {
"@coremedia-internal/studio-client.test-runner-helper": "0.2.3-alpha.050b44a",
"@types/node": "^22.0.0",
"ckeditor5": "46.1.1",
"copyfiles": "^2.4.1",
"expect": "^30.2.0",
"global-jsdom": "^27.0.0",
"jsdom": "^27.0.0",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",
"typescript": "5.4.5"
},
"peerDependencies": {
"ckeditor5": "46.1.1"
},
"dependencies": {
"@coremedia/ckeditor5-logging": "26.0.1-rc.0"
},
"scripts": {
"clean": "pnpm clean:src && pnpm clean:dist",
"clean:src": "rimraf --glob \"src/**/*.@(js|js.map|d.ts|d.ts.map)\"",
"clean:dist": "rimraf ./dist",
"build": "tsc --project tsconfig.release.json && copyfiles -u 1 theme/* theme/**/* dist/theme\"",
"postbuild": "node prepare-package.cjs && copyfiles ./README.md dist",
"npm-check-updates": "npm-check-updates --upgrade",
"lint": "eslint \"**/*.{ts,tsx}\"",
"test": "test-runner-react"
}
}
21 changes: 21 additions & 0 deletions packages/ckeditor5-text-direction/prepare-package.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#! /usr/bin/env node
"use strict";

const fs = require("fs");
const path = require("path");

const distPath = path.resolve("dist");
if (!fs.existsSync(distPath)) {
fs.mkdirSync(distPath);
}

const packageJson = JSON.parse(fs.readFileSync(path.resolve("package.json"), "utf-8"));
if (typeof packageJson.publishConfig === "object") {
delete packageJson.publishConfig.directory;
delete packageJson.publishConfig.linkDirectory;
Object.assign(packageJson, packageJson.publishConfig);
delete packageJson.publishConfig;
delete packageJson.scripts;
delete packageJson.devDependencies;
}
fs.writeFileSync(path.resolve("dist/package.json"), JSON.stringify(packageJson, null, 2) + "\n");
22 changes: 22 additions & 0 deletions packages/ckeditor5-text-direction/src/TextDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Plugin } from "ckeditor5";
import { TextDirectionEditing } from "./TextDirectionEditing";
import { TextDirectionUI } from "./TextDirectionUI";

/**
* The text direction plugin.
*/
export class TextDirection extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [TextDirectionEditing, TextDirectionUI];
}

/**
* @inheritDoc
*/
static get pluginName() {
return "TextDirection";
}
}
97 changes: 97 additions & 0 deletions packages/ckeditor5-text-direction/src/TextDirectionCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { ModelElement, ModelWriter } from "ckeditor5";
import { Command, first } from "ckeditor5";
import { isDefault, textDirectionAttributeName } from "./utils";
import type { TextDirectionOption } from "./utils";

/**
* The text direction command plugin.
*/
export class TextDirectionCommand extends Command {
/**
* @inheritDoc
*/
override refresh() {
const editor = this.editor;
const locale = editor.locale;
const firstBlock = first(this.editor.model.document.selection.getSelectedBlocks());

// As first check whether to enable or disable the command as the value will always be false if the command cannot be enabled.
this.isEnabled = !!firstBlock && this._canHaveTextDirection(firstBlock);

/**
* A value of the current block's text direction.
*
* @observable
* @readonly
* @member {String} #value
*/
if (this.isEnabled && firstBlock?.hasAttribute(textDirectionAttributeName)) {
this.value = firstBlock.getAttribute(textDirectionAttributeName);
} else {
this.value = locale.contentLanguageDirection === "rtl" ? "rtl" : "ltr";
}
}

/**
* Executes the command. Applies the dir `value` to the selected blocks.
* If no `value` is passed, the `value` is the default one or it is equal to the currently selected block's dir attribute,
* the command will remove the attribute from the selected blocks.
*
* @param {Object} [options] Options for the executed command.
* @param {String} [options.value] The value to apply.
* @fires execute
*/
override execute(options: { value?: TextDirectionOption }) {
const editor = this.editor;
const locale = editor.locale;
const model = editor.model;
const doc = model.document;

const value = options.value;

model.change((writer) => {
// Get only those blocks from selected that can have dir set
const blocks = Array.from(doc.selection.getSelectedBlocks()).filter((block) => this._canHaveTextDirection(block));
const currentTextDirection = blocks[0].getAttribute("dir");

// Remove TextDirection attribute if current dir is:
// - default (should not be stored in model as it will bloat model data)
// - equal to currently set
// - or no value is passed - denotes default TextDirection.
const removeTextDirection = !value || isDefault(value, locale) || currentTextDirection === value;

if (removeTextDirection) {
removeTextDirectionFromSelection(blocks, writer);
} else {
setTextDirectionOnSelection(blocks, writer, value);
}
});
}

/**
* Checks whether a block can have dir set.
*
* @private
* @param block The block to be checked.
* @returns {Boolean}
*/
_canHaveTextDirection(block: ModelElement) {
return this.editor.model.schema.checkAttribute(block, textDirectionAttributeName);
}
}

// Removes the dir attribute from blocks.
// @private
function removeTextDirectionFromSelection(blocks: ModelElement[], writer: ModelWriter) {
for (const block of blocks) {
writer.removeAttribute(textDirectionAttributeName, block);
}
}

// Sets the dir attribute on blocks.
// @private
function setTextDirectionOnSelection(blocks: ModelElement[], writer: ModelWriter, dirValue: TextDirectionOption) {
for (const block of blocks) {
writer.setAttribute(textDirectionAttributeName, dirValue, block);
}
}
Loading