Skip to content

Commit

Permalink
Merge branch '3.11' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonkelly committed Jan 23, 2025
2 parents 128127c + 9cdc0e5 commit c37f060
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 19 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# Release Notes for CKEditor for Craft CMS

## Unreleased
## 3.10.0 - 2024-10-19

- Image toolbars now include an “Edit Image” button. ([#253](https://github.com/craftcms/ckeditor/issues/253))
- The `ckeditor/convert/redactor` command now ensures that it’s being run interactively.
- CKEditor container divs now have `data-config` attributes, set to the CKEditor config’s handle. ([#284](https://github.com/craftcms/ckeditor/issues/284))
- Fixed a bug where page breaks were being lost.
- Fixed a bug where menus within overflown toolbar items weren’t fully visible. ([#286](https://github.com/craftcms/ckeditor/issues/286))

## 3.9.0 - 2024-08-15

- CKEditor configs created via the `ckeditor/convert` command now allow modifying HTML attributes, classes, and styles within the source view, if the Redactor config included the `html` button. ([#264](https://github.com/craftcms/ckeditor/pull/264), [#263](https://github.com/craftcms/ckeditor/issues/263))
- Added `craft\ckeditor\events\ModifyConfigEvent::$toolbar`. ([#233](https://github.com/craftcms/ckeditor/pull/233))
- Fixed a bug where code blocks created by a Redactor field only had `<pre>` tags with no `<code>` tags inside them. ([#258](https://github.com/craftcms/ckeditor/issues/258))
- Fixed a bug where dropdown menus didn’t have a maximum height. ([#268](https://github.com/craftcms/ckeditor/issues/268))
- Fixed a bug where word counts weren’t handling unicode characters correctly. ([#275](https://github.com/craftcms/ckeditor/issues/275))

## 3.8.3 - 2024-03-28

Expand Down
68 changes: 61 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,65 @@ composer require craftcms/ckeditor

CKEditor configs are managed globally from **Settings****CKEditor**.

Configurations define the available toolbar buttons, as well as any custom [config options](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html) and CSS styles that should be regisered with the field.
Configurations define the available toolbar buttons, as well as any custom [config options](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html) and CSS styles that should be registered with the field.

New configs can also be created inline from CKEditor field settings.

![A “Create a new field” page within the Craft CMS control panel, with “CKEditor” as the chosen field type. A slideout is open with CKEditor config settings.](field-settings.png)

Once you have selected which toolbar buttons should be available in fields using a given configuration, additional settings may be applied via **Config options**. Options can be defined as static JSON, or a dynamically-evaluated JavaScript snippet; the latter is used as the body of an [immediately-invoked function expression](https://developer.mozilla.org/en-US/docs/Glossary/IIFE), and does not receive any arguments.

> [!NOTE]
> Available options can be found in the [CKEditor's documentation](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html). Craft will auto-complete config properties for most bundled CKEditor extensions.
### Examples

#### Table Features

Suppose we wanted to give editors more control over the layout and appearance of in-line tables. Whenever you add the “Insert table” button to an editor, inline controls are exposed for _Table Row_, _Table Column_, and _Merge_. These can be supplemented with _Table Properties_, _Table Cell Properties_, and _Table Caption_ buttons by adding them in the field’s **Config options** section:

```json
{
"table": {
"contentToolbar": [
"tableRow",
"tableColumn",
"mergeTableCells",
"toggleTableCaption",
"tableProperties",
"tableCellProperties"
]
}
}
```

Some of these additional buttons can be customized further. For example, to modify the colors available for a cell’s background (within the “[Table Cell Properties](https://ckeditor.com/docs/ckeditor5/latest/api/module_table_tableconfig-TableConfig.html#member-tableCellProperties)” balloon), you would provide an array compatible with the [`TableColorConfig` schema](https://ckeditor.com/docs/ckeditor5/latest/api/module_table_tableconfig-TableColorConfig.html) under `table.tableCellProperties.backgroundColors`.

#### External Links

Multiple configuration concerns can coexist in one **Config options** object! You might have a `table` key at the top level to customize table controls (as we've done above), as well as a `link` key that introduces “external” link support:

```json
{
"table": { /* ... */ },
"link": {
"decorators": {
"openInNewTab": {
"mode": "manual",
"label": "Open in new tab?",
"attributes": {
"target": "_blank",
"rel": "noopener noreferrer"
}
}
}
}
}
```

> [!TIP]
> An automatic version of this feature is available natively, via the [`link.addTargetToExternalLinks`](https://ckeditor.com/docs/ckeditor5/latest/api/module_link_linkconfig-LinkConfig.html#member-addTargetToExternalLinks) option.
### Registering Custom Styles

CKEditor’s [Styles](https://ckeditor.com/docs/ckeditor5/latest/features/style.html) plugin makes it easy to apply custom styles to your content via CSS classes.
Expand Down Expand Up @@ -85,11 +138,9 @@ You can then register custom CSS styles that should be applied within the editor

### HTML Purifier Configs

CKEditor fields use [HTML Purifier](http://htmlpurifier.org) to ensure that no malicious code makes it into its field values, to prevent XSS attacks
and other vulnerabilities.
CKEditor fields use [HTML Purifier](http://htmlpurifier.org) to ensure that no malicious code makes it into its field values, to prevent XSS attacks and other vulnerabilities.

You can create custom HTML Purifier configs that will be available to your CKEditor fields. They should be created as JSON files in
your `config/htmlpurifier/` folder.
You can create custom HTML Purifier configs that will be available to your CKEditor fields. They should be created as JSON files in your `config/htmlpurifier/` folder.

Use this as a starting point, which is the default config that CKEditor fields use if no custom HTML Purifier config is selected:

Expand Down Expand Up @@ -131,12 +182,14 @@ See CKEditor’s [media embed documentation](https://ckeditor.com/docs/ckeditor5

## Converting Redactor Fields

You can used the `ckeditor/convert` command to convert any existing Redactor fields over to CKEditor. For each unique Redactor config, a new CKEditor config will be created.
You can use the `ckeditor/convert` command to convert any existing Redactor fields over to CKEditor. For each unique Redactor config, a new CKEditor config will be created and associated with the appropriate field(s).

```sh
php craft ckeditor/convert
```

The command will make changes to your project config. You should commit them, and run `craft up` on other environments for the changes to take effect.

## Adding CKEditor Plugins

Craft CMS plugins can register additional CKEditor plugins to extend its functionality.
Expand All @@ -146,7 +199,8 @@ The first step is to create a [DLL-compatible](https://ckeditor.com/docs/ckedito
- If you’re including one of CKEditor’s [first-party packages](https://github.com/ckeditor/ckeditor5/tree/master/packages), it will already include a `build` directory with a DLL-compatible package inside it.
- If you’re creating a custom CKEditor plugin, use [CKEditor’s package generator](https://ckeditor.com/docs/ckeditor5/latest/framework/plugins/package-generator/using-package-generator.html) to scaffold it, and run its [`dll:build` command](https://ckeditor.com/docs/ckeditor5/latest/framework/plugins/package-generator/javascript-package.html#dllbuild) to create a DLL-compatible package.

> :bulb: Check out CKEditor’s [Implementing an inline widget](https://ckeditor.com/docs/ckeditor5/latest/framework/tutorials/implementing-an-inline-widget.html) tutorial for an in-depth look at how to create a custom CKEditor plugin.
> [!TIP]
> Check out CKEditor’s [Implementing an inline widget](https://ckeditor.com/docs/ckeditor5/latest/framework/tutorials/implementing-an-inline-widget.html) tutorial for an in-depth look at how to create a custom CKEditor plugin.
Once the CKEditor package is in place in your Craft plugin, create an [asset bundle](https://craftcms.com/docs/4.x/extend/asset-bundles.html) which extends [`BaseCkeditorPackageAsset`](src/web/assets/BaseCkeditorPackageAsset.php). The asset bundle defines the package’s build directory, filename, a list of CKEditor plugin names provided by the package, and any toolbar items that should be made available via the plugin.

Expand Down
84 changes: 78 additions & 6 deletions src/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function(ElementInterface $element) {
$value = strip_tags((string)$element->getFieldValue($this->handle));
if (
// regex copied from the WordCount plugin, for consistency
preg_match_all('/(?:[\p{L}\p{N}]+\S?)+/', $value, $matches) &&
preg_match_all('/(?:[\p{L}\p{N}]+\S?)+/u', $value, $matches) &&
count($matches[0]) > $this->wordLimit
) {
$element->addError(
Expand Down Expand Up @@ -342,6 +342,8 @@ protected function inputHtml(mixed $value, ElementInterface $element = null): st
...(!empty($transforms) ? ['transformImage', '|'] : []),
'toggleImageCaption',
'imageTextAlternative',
'|',
'imageEditor',
],
],
'assetSources' => $this->_assetSources(),
Expand Down Expand Up @@ -477,6 +479,9 @@ protected function inputHtml(mixed $value, ElementInterface $element = null): st
'class' => array_filter([
$this->showWordCount ? 'ck-with-show-word-count' : null,
]),
'data' => [
'config' => $this->ckeConfig,
],
]);
}

Expand Down Expand Up @@ -536,6 +541,9 @@ protected function prepValueForInput($value, ?ElementInterface $element): string
// Redactor to CKEditor syntax for <figure>
// (https://github.com/craftcms/ckeditor/issues/96)
$value = $this->_normalizeFigures($value);
// Redactor to CKEditor syntax for <pre>
// (https://github.com/craftcms/ckeditor/issues/258)
$value = $this->_normalizePreTags($value);
}

return parent::prepValueForInput($value, $element);
Expand All @@ -550,13 +558,44 @@ public function serializeValue(mixed $value, ?ElementInterface $element = null):
$value = $value->getRawContent();
}

if ($value !== null) {
// Redactor to CKEditor syntax for <figure>
// (https://github.com/craftcms/ckeditor/issues/96)
$value = $this->_normalizeFigures($value);
if (!$value) {
return null;
}

// Redactor to CKEditor syntax for <figure>
// (https://github.com/craftcms/ckeditor/issues/96)
$value = $this->_normalizeFigures($value);
// Redactor to CKEditor syntax for <pre>
// (https://github.com/craftcms/ckeditor/issues/258)
$value = $this->_normalizePreTags($value);

// Protect page breaks
$this->escapePageBreaks($value);
$value = parent::serializeValue($value, $element);
return str_replace(
'{PAGEBREAK_MARKER}',
'<div class="page-break" style="page-break-after:always;"><span style="display:none;">&nbsp;</span></div>',
$value,
);
}

private function escapePageBreaks(string &$html): void
{
$offset = 0;
$r = '';

while (($pos = stripos($html, '<div class="page-break"', $offset)) !== false) {
$endPos = strpos($html, '</div>', $pos + 23);
if ($endPos === false) {
break;
}
$r .= substr($html, $offset, $pos - $offset) . '{PAGEBREAK_MARKER}';
$offset = $endPos + 6;
}

return parent::serializeValue($value, $element);
if ($offset !== 0) {
$html = $r . substr($html, $offset);
}
}

/**
Expand Down Expand Up @@ -618,6 +657,39 @@ function(array $match) use ($previewsInData) {
return $value;
}

/**
* Normalizes <pre> tags, ensuring they have a <code> tag inside them.
* If there's no <code> tag in there, ensure it's added with class="language-plaintext".
*
* @param string $value
* @return string
*/
private function _normalizePreTags(string $value): string
{
$offset = 0;
while (preg_match('/<pre\b[^>]*>\s*(.*?)<\/pre>/is', $value, $match, PREG_OFFSET_CAPTURE, $offset)) {
/** @var int $startPos */
$startPos = $match[1][1];
$endPos = $startPos + strlen($match[1][0]);
$preContent = $match[1][0];

// if there's already a <code tag inside, leave it alone and carry on
if (str_starts_with($preContent, '<code')) {
$offset = $startPos + strlen($preContent);
continue;
}

$preContent = Html::tag('code', $preContent, [
'class' => 'language-plaintext',
]);

$value = substr($value, 0, $startPos) . $preContent . substr($value, $endPos);
$offset = $startPos + strlen($preContent);
}

return $value;
}

/**
* Returns the field’s CKEditor config.
*
Expand Down
27 changes: 26 additions & 1 deletion src/console/controllers/ConvertController.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ class ConvertController extends Controller
*/
public function actionRedactor(): int
{
if (!$this->interactive) {
$this->stderr("This command must be run interactively.\n");
return ExitCode::UNSPECIFIED_ERROR;
}

$this->projectConfig = Craft::$app->getProjectConfig();

// Find Redactor fields
Expand Down Expand Up @@ -207,7 +212,7 @@ public function actionRedactor(): int
throw new Exception('`manualConfig` contains invalid JSON.');
}
$configName = $field['name'] ?? (!empty($field['handle']) ? Inflector::camel2words($field['handle']) : 'Untitled');
$ckeConfig = $this->generateCkeConfig($configName, $redactorConfig, $ckeConfigs, $fieldSettingsByConfig);
$ckeConfig = $this->generateCkeConfig($configName, $redactorConfig, $ckeConfigs, $fieldSettingsByConfig, $field);
$this->stdout(PHP_EOL);
} else {
$basename = ($field['settings']['redactorConfig'] ?? $field['settings']['configFile'] ?? null) ?: 'Default.json';
Expand Down Expand Up @@ -251,6 +256,9 @@ public function actionRedactor(): int
}

$this->stdout("\n ✓ Finished converting Redactor fields.\n", Console::FG_GREEN, Console::BOLD);
$this->stdout("\nCommit your project config changes,
and run `craft up` on other environments
for the changes to take effect.\n", Console::FG_GREEN);

return ExitCode::OK;
}
Expand Down Expand Up @@ -347,6 +355,7 @@ private function generateCkeConfig(
array $redactorConfig,
array &$ckeConfigs,
array &$fieldSettingsByConfig,
?array $redactorField = null,
): string {
// Make sure the name is unique
$baseConfigName = $configName;
Expand Down Expand Up @@ -669,6 +678,22 @@ private function generateCkeConfig(
}
}

// if we added sourceEditing button, then to align with what Redactor allowed,
// we need add this predefined htmlSupport.allow config
if ($ckeConfig->hasButton('sourceEditing')) {
$htmlSupport = [
'attributes' => true,
'classes' => true,
'styles' => true,
];

if ($redactorField !== null && $redactorField['settings']['removeInlineStyles']) {
unset($htmlSupport['styles']);
}

$ckeConfig->options['htmlSupport']['allow'][] = $htmlSupport;
}

// redactor-link-styles
if (!empty($fullRedactorConfig['linkClasses'])) {
foreach ($fullRedactorConfig['linkClasses'] as $linkClass) {
Expand Down
38 changes: 38 additions & 0 deletions src/controllers/CkeditorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace craft\ckeditor\controllers;

use Craft;
use craft\elements\Asset;
use craft\web\Controller;
use yii\web\NotFoundHttpException;
Expand Down Expand Up @@ -50,4 +51,41 @@ public function actionImageUrl(): Response
'height' => $asset->getHeight($transform),
]);
}

/**
* Returns image permissions.
*
* @return Response
* @throws NotFoundHttpException
* @throws \yii\base\InvalidConfigException
* @throws \yii\web\BadRequestHttpException
*/
public function actionImagePermissions(): Response
{
$assetId = $this->request->getRequiredBodyParam('assetId');

$asset = Asset::find()
->id($assetId)
->kind('image')
->one();

if (!$asset) {
throw new NotFoundHttpException('Image not found');
}

$userSession = Craft::$app->getUser();
$volume = $asset->getVolume();

$previewable = Craft::$app->getAssets()->getAssetPreviewHandler($asset) !== null;
$editable = (
$asset->getSupportsImageEditor() &&
$userSession->checkPermission("editImages:$volume->uid") &&
($userSession->getId() == $asset->uploaderId || $userSession->checkPermission("editPeerImages:$volume->uid"))
);

return $this->asJson([
'previewable' => $previewable,
'editable' => $editable,
]);
}
}
2 changes: 1 addition & 1 deletion src/web/assets/ckeditor/dist/ckeditor5-craftcms.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/ckeditor/dist/ckeditor5-craftcms.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit c37f060

Please sign in to comment.