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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
node_modules
yarn.lock
yarn-error.log
.DS_Store
tsconfig.tsbuildinfo
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
14.19.1
v24.14.0
64 changes: 64 additions & 0 deletions Configuration/Settings.CkStyles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# An example configuration:
#TechDivision:
# CkStyles:
# InlineStyles:
# presets:
# 'fontColor':
# # preset and option labels can be translated
# # example for translation:
# # label: i18n(My.Package:CkStyles:InlineStyles.fontColor.label)
# # This would look for the translation in "My.Package/Resources/Private/Translations/<locale>/CkStyles.xlf" with the key "InlineStyles.fontColor.label"
# # Make sure the translation file is autoloaded via settings yaml:
# # Neos:
# # Neos:
# # userInterface:
# # translation:
# # autoInclude:
# # My.Package:
# # - 'CkStyles'
# label: 'Font color'
# options:
# 'primary':
# label: 'Red'
# # presets and options can have icons
# # The icon has to be svg markup. For example, you can use free icons from fontawesome (https://fontawesome.com/search?ic=free-collection) and use the svg markup.
# # example for icon:
# # icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path class="my-class-red icon__shadow" d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>'
# # The icon will be displayed in the button element of the dropdown/item.
# # In the example above the icon has some custom styling: It is styled with the same class as the option, so it will be displayed in the same color as the option. And some shadow for better visibility.
# icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path class="my-class-red icon__shadow" d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>'
# # presets and options can hide the label and only show the icon by setting showLabel to false. By default, the label is shown.
# showLabel: false
# cssClass: 'my-class-red'
# 'secondary':
# label: 'Green'
# cssClass: 'my-class-green'
# '':
# label: 'unset color'
# cssClass: null
# 'fontSize':
# label: 'Font size'
# options:
# 'small':
# label: 'Small'
# cssClass: 'my-class-size-small'
# 'big':
# label: 'Large'
# cssClass: 'my-class-size-large'
# '':
# label: 'unset size'
# cssClass: null
# BlockStyles:
# presets:
# 'indent':
# label: 'Indentation'
# options:
# 'primary':
# label: '2 rem'
# cssClass: 'indent-2'
# 'secondary':
# label: '4 rem'
# cssClass: 'indent-4'
# '':
# label: 'remove indent'
# cssClass: null
44 changes: 0 additions & 44 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
@@ -1,47 +1,3 @@
# An example configuration:
#TechDivision:
# CkStyles:
# InlineStyles:
# presets:
# 'fontColor':
# label: 'Font color'
# options:
# 'primary':
# label: 'Red'
# cssClass: 'my-class-red'
# 'secondary':
# label: 'Green'
# cssClass: 'my-class-green'
# '':
# label: 'unset color'
# cssClass: null
# 'fontSize':
# label: 'Font size'
# options:
# 'small':
# label: 'Small'
# cssClass: 'my-class-size-small'
# 'big':
# label: 'Large'
# cssClass: 'my-class-size-large'
# '':
# label: 'unset size'
# cssClass: null
# BlockStyles:
# presets:
# 'indent':
# label: 'Indentation'
# options:
# 'primary':
# label: '2 rem'
# cssClass: 'indent-2'
# 'secondary':
# label: '4 rem'
# cssClass: 'indent-4'
# '':
# label: 'remove indent'
# cssClass: null

Neos:
Neos:
Ui:
Expand Down
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ It is also possible to set a different attribute (for usage with placeholders fo

**Demo:**

![Applying inline style](Documentation/assets/InlineStyleDemo.gif "Inline style")

<video src="https://github.com/user-attachments/assets/907773f1-19e5-4c89-a565-ea26f2d6c3c1" alt="Demo Video"></video>

**Example output:**

Expand Down Expand Up @@ -50,14 +49,38 @@ TechDivision:
InlineStyles:
presets:
'fontColor':
# preset and option labels can be translated
# example for translation:
# label: i18n(My.Package:CkStyles:InlineStyles.fontColor.label)
# This would look for the translation in "My.Package/Resources/Private/Translations/<locale>/CkStyles.xlf" with the key "InlineStyles.fontColor.label"
# Make sure the translation file is autoloaded via settings yaml:
# Neos:
# Neos:
# userInterface:
# translation:
# autoInclude:
# My.Package:
# - 'CkStyles'
label: 'Font color'
options:
'primary':
label: 'Red'
# presets and options can have icons
# The icon has to be svg markup. For example, you can use free icons from fontawesome (https://fontawesome.com/search?ic=free-collection) and use the svg markup.
# example for icon:
# icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path class="my-class-red icon__shadow" d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>'
# The icon will be displayed in the button element of the dropdown/item.
# In the example above the icon has some custom styling: It is styled with the same class as the option, so it will be displayed in the same color as the option. And some shadow for better visibility.
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path class="my-class-red icon__shadow" d="M160 96L480 96C515.3 96 544 124.7 544 160L544 480C544 515.3 515.3 544 480 544L160 544C124.7 544 96 515.3 96 480L96 160C96 124.7 124.7 96 160 96z"/></svg>'
# presets and options can hide the label and only show the icon by setting showLabel to false. By default, the label is shown.
showLabel: false
cssClass: 'my-class-red'
'secondary':
label: 'Green'
cssClass: 'my-class-green'
'':
label: 'unset color'
cssClass: null
'fontSize':
label: 'Font size'
options:
Expand Down Expand Up @@ -94,7 +117,7 @@ TechDivision:
attributeValue: 'my-custom-attribute-value'
```

Example: [Configuration/Settings.yaml](Configuration/Settings.yaml)
Example: [Configuration/Settings.CkStyles.yaml](Configuration/Settings.CkStyles.yaml)


**What values are allowed for `cssClass` and/or `attributeValue`?**
Expand Down
12 changes: 12 additions & 0 deletions Resources/Private/JavaScript/CkStyles/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*.{ts,js}]
# Non-configurable Prettier behaviors
charset = utf-8
insert_final_newline = true

# Configurable Prettier behaviors
end_of_line = lf
indent_style = space
indent_size = 4
max_line_length = 120
1 change: 0 additions & 1 deletion Resources/Private/JavaScript/CkStyles/.nvmrc

This file was deleted.

2 changes: 2 additions & 0 deletions Resources/Private/JavaScript/CkStyles/.prettierrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# WHY empty?: We use prettier's defaults and applicable settings from .editorconfig.
# WHY YAML format?: To be able to use comments like these.
32 changes: 32 additions & 0 deletions Resources/Private/JavaScript/CkStyles/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const esbuild = require("esbuild");
const extensibilityMap = require("@neos-project/neos-ui-extensibility/extensibilityMap.json");
const isWatch = process.argv.includes("--watch");

/** @type {import("esbuild").BuildOptions} */
const options = {
logLevel: "info",
bundle: true,
minify: !isWatch,
sourcemap: "linked",
legalComments: "linked",
target: "es2020",
entryPoints: {
Plugin: "./src/index.ts",
},
outdir: "../../../Public/JavaScript/CkStyles/",
alias: {
"@ckeditor/ckeditor5-ui/theme/components/form/form.css": "./empty.css",
"@ckeditor/ckeditor5-ui/theme/components/responsive-form/responsiveform.css": "./empty.css",
...extensibilityMap,
},
loader: {
".svg": "text",
".css": "empty",
},
};

if (isWatch) {
esbuild.context(options).then((ctx) => ctx.watch())
} else {
esbuild.build(options);
}
Empty file.
9 changes: 9 additions & 0 deletions Resources/Private/JavaScript/CkStyles/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module "@neos-project/neos-ui-extensibility";
declare module "@neos-project/neos-ui-i18n" {
export function translate(
fullyQualifiedTranslationAddressAsString: string,
fallback: string | [string, string] = "",
parameters: Parameters = [],
quantity: number = 0,
): string;
}
19 changes: 13 additions & 6 deletions Resources/Private/JavaScript/CkStyles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
"license": "GNU GPLv3",
"private": true,
"scripts": {
"build": "neos-react-scripts build",
"watch": "neos-react-scripts watch"
"build": "tsc --noEmit && node build.js",
"watch": "concurrently -n 'TSC,ESBuild' 'tsc --noEmit --watch --incremental --preserveWatchOutput' 'node build.js --watch'",
"prettier:write": "prettier --write './**/*.{ts,js}'"
},
"devDependencies": {
"@neos-project/neos-ui-extensibility-webpack-adapter": "^8.3.0"
"dependencies": {
"@ckeditor/ckeditor5-core": "47.2.0",
"@ckeditor/ckeditor5-ui": "47.2.0",
"@ckeditor/ckeditor5-utils": "47.2.0",
"@neos-project/neos-ui-extensibility": "^9.1.4"
},
"neos": {
"buildTargetDirectory": "../../../Public/JavaScript/CkStyles"
"devDependencies": {
"esbuild": "^0.27.3",
"typescript": "^5.9.3",
"concurrently": "^9.2.1",
"prettier": "^3.8.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
// Originally taken from https://raw.githubusercontent.com/ckeditor/ckeditor5/master/packages/ckeditor5-basic-styles/src/attributecommand.js and adjusted
import {Command} from 'ckeditor5-exports';
import { Command, type Editor } from "@ckeditor/ckeditor5-core";

/**
* Set a key-value block style; e.g. "fontColor=red".
*/

export default class BlockStyleCommand extends Command {
declare public value: string[];

/**
* The attribute that will be set by the command.
* @observable
* @readonly
*/
public readonly attributeKey: string;

/**
* @param {module:core/editor/editor~Editor} editor
* @param {String} attributeKey Attribute that will be set by the command.
*/
constructor(editor, attributeKey) {
constructor(editor: Editor, attributeKey: string) {
super(editor);

/**
* The attribute that will be set by the command.
*
* @readonly
* @member {String}
*/
this.attributeKey = attributeKey;

/**
* Flag indicating whether the command is active. The command is active when the
* {@link module:engine/model/selection~Selection#hasAttribute selection has the attribute} which means that:
*
* @observable
* @readonly
* @member {Boolean} #value
*/
this.value = [];
}

/**
* Updates the command's {@link #value} and {@link #isEnabled}.
*/
refresh() {
public override refresh(): void {
const model = this.editor.model;
const doc = model.document;
const blocksToChange = Array.from(doc.selection.getSelectedBlocks());
const schema = model.schema;
const blocks = Array.from(doc.selection.getSelectedBlocks());

// reset value and enabled state
this.isEnabled = false;
const valuesInCurrentSelection: string[] = [];

this.value = this._getValueFromBlockNode();
for (const block of blocksToChange) {
if (model.schema.checkAttribute(block, this.attributeKey)) {
for (const block of blocks) {
if (schema.checkAttribute(block, this.attributeKey)) {
// if at least one block allows the attribute, the command should be enabled
this.isEnabled = true;

// if the block has the attribute and its value is one of the allowed values, add it to the command value
const value = block.getAttribute(this.attributeKey);
if (typeof value === "string" && !valuesInCurrentSelection.includes(value) && value !== "") {
valuesInCurrentSelection.push(value);
}
}
}

this.value = valuesInCurrentSelection;
}

/**
Expand All @@ -55,13 +62,18 @@ export default class BlockStyleCommand extends Command {
* @param {Object} [options] Command options.
* @param {String} [options.value] The value to be set; if null or not existing, the attribute will be removed.
*/
execute(options = {}) {
public override execute(options: { value?: any } = {}) {
const model = this.editor.model;
const doc = model.document;
const selection = doc.selection;
const value = options.value;

const blocksToChange = Array.from(selection.getSelectedBlocks());
model.change(writer => {

// toggle the value: if all selected blocks already have the attribute with the same value, remove it; otherwise, set it for all selected blocks
const allBlocksHaveValue = blocksToChange.every(block => block.getAttribute(this.attributeKey) === options.value);
const value = allBlocksHaveValue ? undefined : options.value;

model.change((writer) => {
for (const block of blocksToChange) {
if (value) {
writer.setAttribute(this.attributeKey, value, block);
Expand All @@ -71,25 +83,4 @@ export default class BlockStyleCommand extends Command {
}
});
}

/**
* Checks the attribute value of the parent block node(s)
*
* @private
* @returns {String} The attribute value.
*/
_getValueFromBlockNode() {
const model = this.editor.model;
const schema = model.schema;
const selection = model.document.selection;
const blocks = Array.from(selection.getSelectedBlocks());

for (const block of blocks) {
if (schema.checkAttribute(block, this.attributeKey)) {
return block.getAttribute(this.attributeKey);
}
}

return undefined;
}
}
Loading