Issue #3222797 by Wim Leers, nod_, bnjmnm, larowlan, mpp, Luke.Leber, lauriii, DanielVeza, Reinmar: Upgrade path from CKEditor 4's StylesCombo to CKEditor 5's Style
parent
a01e721e28
commit
f109ec0f08
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.ar=t.ar||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"أنماط الكتل","Multiple styles":"أنماط متعددة",Styles:"الأنماط","Text styles":"أنماط النصوص"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.bg=t.bg||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блокови стилове","Multiple styles":"Множество стилове",Styles:"Стилове","Text styles":"Текстови стилове"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const n=t.bn=t.bn||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"ব্লক স্টাইল","Multiple styles":"একাধিক স্টাইল",Styles:"স্টাইলস","Text styles":"টেস্কট স্টাইল"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.ca=s.ca||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estils de bloc","Multiple styles":"Estils múltiples",Styles:"Estils","Text styles":"Estils de text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.cs=t.cs||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Styly bloků","Multiple styles":"Více stylů",Styles:"Styly","Text styles":"Styly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const e=t.da=t.da||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Blokstile","Multiple styles":"Flere stile",Styles:"Stile","Text styles":"Tekststile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(e){const t=e.de=e.de||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block-Stile","Multiple styles":"Mehrere Stile",Styles:"Stile","Text styles":"Text-Stile"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.el=t.el||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Στυλ για μπλοκ","Multiple styles":"Πολλαπλά στυλ",Styles:"Στυλ","Text styles":"Στυλ για κείμενο"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s["en-au"]=s["en-au"]||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Block styles","Multiple styles":"Multiple styles",Styles:"Styles","Text styles":"Text styles"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.es=s.es||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.et=i.et||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Ploki stiilid","Multiple styles":"Mitu stiili",Styles:"Stiilid","Text styles":"Teksti stiilid"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const i=t.fi=t.fi||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Lohkotyylit","Multiple styles":"Useita tyylejä",Styles:"Tyylit","Text styles":"Tekstityylit"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const e=t.fr=t.fr||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Styles de bloc","Multiple styles":"Styles multiples",Styles:"Styles","Text styles":"Styles de texte"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.gl=s.gl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de bloque","Multiple styles":"Múltiples estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.he=t.he||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"סגנונות בלוקים","Multiple styles":"סגנונות מרובים",Styles:"סגנונות","Text styles":"עיצוב טקסט"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.hi=i.hi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"ब्लॉक स्टाइल्स","Multiple styles":"कई स्टाइल्स",Styles:"स्टाइल्स","Text styles":"टेक्स्ट स्टाइल्स"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.hr=i.hr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Tekstualni stilovi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.hu=s.hu||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokkstílusok","Multiple styles":"Többféle stílus",Styles:"Stílusok","Text styles":"Szövegstílusok"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(a){const t=a.id=a.id||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Banyak gaya",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.it=i.it||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Stili per blocchi","Multiple styles":"Stili multipli",Styles:"Stili","Text styles":"Stili per testi"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.ja=t.ja||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"ブロックスタイル","Multiple styles":"複数のスタイル",Styles:"スタイル","Text styles":"テキストスタイル"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.ko=t.ko||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"블록 스타일","Multiple styles":"다중 스타일",Styles:"스타일","Text styles":"텍스트 스타일"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.lt=i.lt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blokuoti stilius","Multiple styles":"Daug stilių",Styles:"Stiliai","Text styles":"Teksto stiliai"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.lv=i.lv||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Bloka stili","Multiple styles":"Vairāki stili",Styles:"Stili","Text styles":"Teksta stili"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const a=s.ms=s.ms||{};a.dictionary=Object.assign(a.dictionary||{},{"Block styles":"Gaya blok","Multiple styles":"Gaya berbilang",Styles:"Gaya","Text styles":"Gaya teks"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(e){const t=e.nl=e.nl||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Blok stijlen","Multiple styles":"Meerdere stijlen",Styles:"Stijlen","Text styles":"Tekst stijlen"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const i=t.no=t.no||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blokkstiler","Multiple styles":"Multiple stiler",Styles:"Stiler","Text styles":"Tekststiler"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const e=t.pl=t.pl||{};e.dictionary=Object.assign(e.dictionary||{},{"Block styles":"Style tekstu blokowego","Multiple styles":"Wiele stylów",Styles:"Style","Text styles":"Style tekstu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t["pt-br"]=t["pt-br"]||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Estilos de bloco","Multiple styles":"Múltiplos estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.pt=s.pt||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Estilos de blocos","Multiple styles":"Vários estilos",Styles:"Estilos","Text styles":"Estilos de texto"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const i=t.ro=t.ro||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Stiluri pentru blocuri","Multiple styles":"Stiluri multiple",Styles:"Stiluri","Text styles":"Stiluri pentru text"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.ru=t.ru||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Блочные стили","Multiple styles":"Несколько стилей",Styles:"Стили","Text styles":"Стиль текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const l=t.sk=t.sk||{};l.dictionary=Object.assign(l.dictionary||{},{"Block styles":"Štýly bloku","Multiple styles":"Viacero štýlov",Styles:"Štýly","Text styles":"Štýly textu"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const i=t["sr-latn"]=t["sr-latn"]||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilovi","Multiple styles":"Više stilova",Styles:"Stilovi","Text styles":"Stilovi teksta"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(s){const t=s.sr=s.sr||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Блок стилови","Multiple styles":"Више стилова",Styles:"Стилови","Text styles":"Стилови текста"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.sv=t.sv||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Blockstilar","Multiple styles":"Flera stilar",Styles:"Stilar","Text styles":"Texttyper"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.th=t.th||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"รูปแบบบล็อก","Multiple styles":"มีหลายรูปแบบ",Styles:"รูปแบบ","Text styles":"รูปแบบข้อความ"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const i=t.tr=t.tr||{};i.dictionary=Object.assign(i.dictionary||{},{"Block styles":"Blok stilleri","Multiple styles":"Birden fazla stil",Styles:"Stiller","Text styles":"Metin stilleri"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.uk=t.uk||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"Стилі блоку","Multiple styles":"Кілька стилів",Styles:"Стилі","Text styles":"Стилі тексту"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(i){const t=i.vi=i.vi||{};t.dictionary=Object.assign(t.dictionary||{},{"Block styles":"Kiểu của khối","Multiple styles":"Nhiều kiểu",Styles:"Kiểu","Text styles":"Kiểu văn bản"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const n=t["zh-cn"]=t["zh-cn"]||{};n.dictionary=Object.assign(n.dictionary||{},{"Block styles":"块级样式","Multiple styles":"多样式",Styles:"样式","Text styles":"文本样式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -0,0 +1 @@
|
|||
!function(t){const s=t.zh=t.zh||{};s.dictionary=Object.assign(s.dictionary||{},{"Block styles":"區塊樣式","Multiple styles":"多重樣式",Styles:"樣式","Text styles":"文字樣式"})}(window.CKEDITOR_TRANSLATIONS||(window.CKEDITOR_TRANSLATIONS={}));
|
|
@ -302,6 +302,19 @@ ckeditor5.codeBlock:
|
|||
- core/ckeditor5
|
||||
- core/ckeditor5.translations
|
||||
|
||||
ckeditor5.style:
|
||||
remote: https://github.com/ckeditor/ckeditor5
|
||||
version: "35.0.1"
|
||||
license:
|
||||
name: GNU-GPL-2.0-or-later
|
||||
url: https://github.com/ckeditor/ckeditor5/blob/v35.0.1/LICENSE.md
|
||||
gpl-compatible: true
|
||||
js:
|
||||
assets/vendor/ckeditor5/style/style.js: { minified: true }
|
||||
dependencies:
|
||||
- core/ckeditor5
|
||||
- core/ckeditor5.translations
|
||||
|
||||
ckeditor5.translations:
|
||||
# No sensible version can be specified, since the translations may change at
|
||||
# any time.
|
||||
|
|
|
@ -62,6 +62,27 @@ ckeditor5_heading:
|
|||
- <h5>
|
||||
- <h6>
|
||||
|
||||
ckeditor5_style:
|
||||
ckeditor5:
|
||||
plugins: [style.Style]
|
||||
drupal:
|
||||
label: Style
|
||||
library: core/ckeditor5.style
|
||||
admin_library: ckeditor5/admin.style
|
||||
class: Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
|
||||
toolbar_items:
|
||||
style:
|
||||
label: Style
|
||||
# This plugin is able to add any configured class on any tag that can be
|
||||
# created by some other CKEditor 5 plugin. Hence it indicates it allows all
|
||||
# classes on all tags. Its subset then restricts this to a concrete set of
|
||||
# tags, and a concrete set of classes.
|
||||
# @todo Update in https://www.drupal.org/project/drupal/issues/3280124
|
||||
# @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::getElementsSubset()
|
||||
# @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
|
||||
elements:
|
||||
- <$any-html5-element class>
|
||||
|
||||
ckeditor5_arbitraryHtmlSupport:
|
||||
ckeditor5:
|
||||
plugins: [htmlSupport.GeneralHtmlSupport]
|
||||
|
|
|
@ -177,6 +177,17 @@ admin.sourceEditing:
|
|||
theme:
|
||||
css/source-editing.admin.css: { }
|
||||
|
||||
admin.style:
|
||||
js:
|
||||
js/ckeditor5.style.admin.js: { }
|
||||
css:
|
||||
theme:
|
||||
css/style.admin.css: { }
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupal.vertical-tabs
|
||||
|
||||
admin.table:
|
||||
css:
|
||||
theme:
|
||||
|
|
|
@ -133,19 +133,6 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac
|
|||
if (isset($form['filters']['settings']['filter_html']['allowed_html'])) {
|
||||
$filter_allowed_html = &$form['filters']['settings']['filter_html']['allowed_html'];
|
||||
|
||||
if (isset($form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags'])) {
|
||||
$source_allowed_tags = &$form['editor']['settings']['subform']['plugins']['ckeditor5_sourceEditing']['allowed_tags'];
|
||||
// @todo if this triggers the callback via keyboard navigation such as
|
||||
// tab, focus should move to the next element, not to the rebuilt
|
||||
// "allowed tags" field
|
||||
// https://www.drupal.org/project/ckeditor5/issues/3231321.
|
||||
$source_allowed_tags['#ajax'] = [
|
||||
'callback' => '_update_ckeditor5_html_filter',
|
||||
'trigger_as' => ['name' => 'editor_configure'],
|
||||
'event' => 'change',
|
||||
];
|
||||
}
|
||||
|
||||
$filter_allowed_html['#value_callback'] = [CKEditor5::class, 'getGeneratedAllowedHtmlValue'];
|
||||
// Set readonly and add the form-disabled wrapper class as using #disabled
|
||||
// or the disabled attribute will prevent the new values from being
|
||||
|
@ -214,6 +201,7 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac
|
|||
'checkbox',
|
||||
'select',
|
||||
'radios',
|
||||
'textarea',
|
||||
];
|
||||
if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) {
|
||||
$plugins_config_form['#ajax'] = [
|
||||
|
@ -267,17 +255,22 @@ function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterfac
|
|||
function ckeditor5_filter_format_edit_form_submit(array $form, FormStateInterface $form_state) {
|
||||
$limit_allowed_html_tags = isset($form['filters']['settings']['filter_html']['allowed_html']);
|
||||
$manually_editable_tags = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_sourceEditing', 'allowed_tags']);
|
||||
if ($limit_allowed_html_tags && is_array($manually_editable_tags)) {
|
||||
// When "Manually editable tags" and "limit allowed HTML tags" are both
|
||||
// configured, the former informs the value of the latter. This dependent
|
||||
$styles = $form_state->getValue(['editor', 'settings', 'plugins', 'ckeditor5_style', 'styles']);
|
||||
if ($limit_allowed_html_tags && is_array($manually_editable_tags) || is_array($styles)) {
|
||||
// When "Manually editable tags", "Style" and "limit allowed HTML tags" are
|
||||
// all configured, the latter is dependent on the others. This dependent
|
||||
// value is typically updated via AJAX, but it's possible for "Manually
|
||||
// editable tags" to update without triggering the AJAX rebuild. That value
|
||||
// is recalculated here on save to ensure it happens even if the AJAX
|
||||
// rebuild doesn't happen.
|
||||
$manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags));
|
||||
$manually_editable_tags_restrictions = HTMLRestrictions::fromString(implode($manually_editable_tags ?? []));
|
||||
$styles_restrictions = HTMLRestrictions::fromString(implode($styles ? array_column($styles, 'element') : []));
|
||||
$format = $form_state->get('ckeditor5_validated_pair')->getFilterFormat();
|
||||
$allowed_html = HTMLRestrictions::fromTextFormat($format);
|
||||
$combined_tags_string = $manually_editable_tags_restrictions->merge($allowed_html)->toFilterHtmlAllowedTagsString();
|
||||
$combined_tags_string = $allowed_html
|
||||
->merge($manually_editable_tags_restrictions)
|
||||
->merge($styles_restrictions)
|
||||
->toFilterHtmlAllowedTagsString();
|
||||
$form_state->setValue(['filters', 'filter_html', 'settings', 'allowed_html'], $combined_tags_string);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ ckeditor5.plugin.ckeditor5_imageResize:
|
|||
constraints:
|
||||
NotNull: []
|
||||
|
||||
# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
|
||||
ckeditor5.plugin.ckeditor5_sourceEditing:
|
||||
type: mapping
|
||||
label: Source Editing
|
||||
|
@ -134,3 +135,35 @@ ckeditor5.plugin.media_media:
|
|||
label: 'Allow view mode override'
|
||||
constraints:
|
||||
NotNull: []
|
||||
|
||||
# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
|
||||
ckeditor5.plugin.ckeditor5_style:
|
||||
type: mapping
|
||||
label: Style
|
||||
mapping:
|
||||
styles:
|
||||
type: sequence
|
||||
label: 'Styles'
|
||||
constraints:
|
||||
NotBlank:
|
||||
message: "Enable at least one style, otherwise disable the Style plugin."
|
||||
UniqueLabelInList:
|
||||
labelKey: label
|
||||
sequence:
|
||||
type: mapping
|
||||
label: 'Style'
|
||||
mapping:
|
||||
label:
|
||||
type: label
|
||||
label: 'Style label'
|
||||
element:
|
||||
type: ckeditor5.element
|
||||
constraints:
|
||||
# Validate that this contains exactly 1 attribute (class) and >=1 class attr value.
|
||||
CKEditor5Element:
|
||||
requiredAttributes:
|
||||
-
|
||||
attributeName: class
|
||||
minAttributeValueCount: 1
|
||||
StyleSensibleElement: []
|
||||
label: 'Style tag + classes'
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
.ckeditor5-toolbar-button-style {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 110px;
|
||||
color: #000;
|
||||
}
|
||||
.ckeditor5-toolbar-button-style::before {
|
||||
margin-left: 10px;
|
||||
content: "Style";
|
||||
font-size: 14px;
|
||||
}
|
||||
[dir="rtl"] .ckeditor5-toolbar-button-style::before {
|
||||
margin-right: 10px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.ckeditor5-toolbar-button-style::after {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 10px;
|
||||
content: "";
|
||||
transform: rotate(135deg);
|
||||
border-width: 2px 2px 0 0;
|
||||
border-style: solid;
|
||||
}
|
||||
[dir="rtl"] .ckeditor5-toolbar-button-style::after {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
|
@ -1942,6 +1942,54 @@
|
|||
* @typedef {module:special-characters/ui/specialcharactersnavigationview} module:special-characters/ui/specialcharactersnavigationview~SpecialCharactersNavigationView
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/style.js
|
||||
*
|
||||
* @typedef {module:style/style} module:style/style~Style
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/stylecommand.js
|
||||
*
|
||||
* @typedef {module:style/stylecommand} module:style/stylecommand~StyleCommand
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/styleediting.js
|
||||
*
|
||||
* @typedef {module:style/styleediting} module:style/styleediting~StyleEditing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/styleui.js
|
||||
*
|
||||
* @typedef {module:style/styleui} module:style/styleui~StyleUI
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridbuttonview.js
|
||||
*
|
||||
* @typedef {module:style/ui/stylegridbuttonview} module:style/ui/stylegridbuttonview~StyleGridButtonView
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/ui/stylegridview.js
|
||||
*
|
||||
* @typedef {module:style/ui/stylegridview} module:style/ui/stylegridview~StyleGridView
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/ui/stylegroupview.js
|
||||
*
|
||||
* @typedef {module:style/ui/stylegroupview} module:style/ui/stylegroupview~StyleGroupView
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-style/src/ui/stylepanelview.js
|
||||
*
|
||||
* @typedef {module:style/ui/stylepanelview} module:style/ui/stylepanelview~StylePanelView
|
||||
*/
|
||||
|
||||
/**
|
||||
* Declared in file @ckeditor/ckeditor5-table/src/commands/insertcolumncommand.js
|
||||
*
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 5 Style admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
/**
|
||||
* Provides the summary for the "style" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behavior to the plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditor5StyleSettingsSummary = {
|
||||
attach() {
|
||||
$('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary(
|
||||
(context) => {
|
||||
const stylesElement = document.querySelector(
|
||||
'[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]',
|
||||
);
|
||||
const styleCount = stylesElement.value
|
||||
.split('\n')
|
||||
// Minimum length is 5: "p.z|Z" is the shortest possible style definition.
|
||||
.filter((line) => line.trim().length >= 5).length;
|
||||
|
||||
if (styleCount === 0) {
|
||||
return Drupal.t('No styles configured');
|
||||
}
|
||||
return Drupal.formatPlural(
|
||||
styleCount,
|
||||
'One style configured',
|
||||
'@count styles configured',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal);
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
Drupal.behaviors.ckeditor5StyleSettingsSummary = {
|
||||
attach() {
|
||||
$('[data-ckeditor5-plugin-id="ckeditor5_style"]').drupalSetSummary(context => {
|
||||
const stylesElement = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
const styleCount = stylesElement.value.split('\n').filter(line => line.trim().length >= 5).length;
|
||||
|
||||
if (styleCount === 0) {
|
||||
return Drupal.t('No styles configured');
|
||||
}
|
||||
|
||||
return Drupal.formatPlural(styleCount, 'One style configured', '@count styles configured');
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
})(jQuery, Drupal);
|
|
@ -9,6 +9,7 @@ use Drupal\Component\Utility\DiffArray;
|
|||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\filter\Plugin\Filter\FilterHtml;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
use Masterminds\HTML5\Elements;
|
||||
|
||||
/**
|
||||
* Represents a set of HTML restrictions.
|
||||
|
@ -66,6 +67,7 @@ final class HTMLRestrictions {
|
|||
* @var string[]
|
||||
*/
|
||||
private const WILDCARD_ELEMENT_METHODS = [
|
||||
'$any-html5-element' => 'getHtml5ElementList',
|
||||
'$text-container' => 'getTextContainerElementList',
|
||||
];
|
||||
|
||||
|
@ -1188,6 +1190,16 @@ final class HTMLRestrictions {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all known HTML5 elements.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of HTML5 element tags.
|
||||
*/
|
||||
private static function getHtml5ElementList(): array {
|
||||
return array_keys(Elements::$html5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the tags that match the provided wildcard.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Drupal\ckeditor5\Plugin\CKEditor4To5Upgrade;
|
|||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor4To5UpgradePluginInterface;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
||||
|
@ -58,10 +59,11 @@ use Drupal\filter\FilterFormatInterface;
|
|||
* "language",
|
||||
* },
|
||||
* cke5_plugin_elements_subset_configuration = {
|
||||
* "ckeditor5_heading",
|
||||
* "ckeditor5_alignment",
|
||||
* "ckeditor5_list",
|
||||
* "media_media",
|
||||
* "ckeditor5_heading",
|
||||
* "ckeditor5_alignment",
|
||||
* "ckeditor5_list",
|
||||
* "ckeditor5_style",
|
||||
* "media_media",
|
||||
* }
|
||||
* )
|
||||
*
|
||||
|
@ -163,8 +165,7 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface {
|
|||
|
||||
// @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo
|
||||
case 'Styles':
|
||||
// @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797
|
||||
return NULL;
|
||||
return ['style'];
|
||||
|
||||
// @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\specialCharacters
|
||||
case 'SpecialChar':
|
||||
|
@ -190,8 +191,17 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface {
|
|||
switch ($cke4_plugin_id) {
|
||||
// @see \Drupal\ckeditor\Plugin\CKEditorPlugin\StylesCombo
|
||||
case 'stylescombo':
|
||||
// @todo Change in https://www.drupal.org/project/ckeditor5/issues/3222797
|
||||
return NULL;
|
||||
if (!isset($cke4_plugin_settings['styles'])) {
|
||||
$styles = [];
|
||||
}
|
||||
else {
|
||||
[$styles] = Style::parseStylesFormValue($cke4_plugin_settings['styles']);
|
||||
}
|
||||
return [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => $styles,
|
||||
],
|
||||
];
|
||||
|
||||
// @see \Drupal\ckeditor\Plugin\CKEditorPlugin\Language
|
||||
case 'language':
|
||||
|
@ -284,6 +294,10 @@ class Core extends PluginBase implements CKEditor4To5UpgradePluginInterface {
|
|||
$configuration['allow_view_mode_override'] = !empty($restrictions['allowed']['drupal-media']['data-view-mode']);
|
||||
return $configuration;
|
||||
|
||||
case 'ckeditor5_style':
|
||||
// @see mapCKEditor4SettingsToCKEditor5Configuration()
|
||||
return NULL;
|
||||
|
||||
default:
|
||||
throw new \OutOfBoundsException();
|
||||
}
|
||||
|
|
|
@ -40,12 +40,12 @@ class SourceEditing extends CKEditor5PluginDefault implements CKEditor5PluginCon
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
// Match the config schema structure at ckeditor5.plugin.ckeditor5_heading.
|
||||
// Match the config schema structure at
|
||||
// ckeditor5.plugin.ckeditor5_sourceEditing.
|
||||
$form_value = $form_state->getValue('allowed_tags');
|
||||
if (!is_array($form_value)) {
|
||||
$config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray();
|
||||
$form_state->setValue('allowed_tags', $config_value);
|
||||
}
|
||||
assert(is_string($form_value));
|
||||
$config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray();
|
||||
$form_state->setValue('allowed_tags', $config_value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;
|
||||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
|
||||
/**
|
||||
* CKEditor 5 Style plugin configuration.
|
||||
*
|
||||
* @internal
|
||||
* Plugin classes are internal.
|
||||
*/
|
||||
class Style extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, CKEditor5PluginElementsSubsetInterface {
|
||||
|
||||
use CKEditor5PluginConfigurableTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
|
||||
$form['styles'] = [
|
||||
'#title' => $this->t('Styles'),
|
||||
'#type' => 'textarea',
|
||||
'#description' => $this->t('A list of classes that will be provided in the "Style" dropdown. Enter one or more classes on each line in the format: element.classA.classB|Label. Example: h1.title|Title. Advanced example: h1.fancy.title|Fancy title.<br />These styles should be available in your theme\'s CSS file.'),
|
||||
];
|
||||
if (!empty($this->configuration['styles'])) {
|
||||
$as_selectors = '';
|
||||
foreach ($this->configuration['styles'] as $style) {
|
||||
[$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element']));
|
||||
$as_selectors .= sprintf("%s.%s|%s\n", $tag, implode('.', $classes), $style['label']);
|
||||
}
|
||||
$form['styles']['#default_value'] = $as_selectors;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tag and classes for a parsed style element.
|
||||
*
|
||||
* @param \Drupal\ckeditor5\HTMLRestrictions $style_element
|
||||
* A parsed style element.
|
||||
*
|
||||
* @return array
|
||||
* An array containing two values:
|
||||
* - a HTML tag name
|
||||
* - a list of classes
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function getTagAndClasses(HTMLRestrictions $style_element): array {
|
||||
$tag = array_keys($style_element->getAllowedElements())[0];
|
||||
$classes = array_keys($style_element->getAllowedElements()[$tag]['class']);
|
||||
return [$tag, $classes];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
// Match the config schema structure at ckeditor5.plugin.ckeditor5_style.
|
||||
$form_value = $form_state->getValue('styles');
|
||||
[$styles, $unparseable_lines] = self::parseStylesFormValue($form_value);
|
||||
if (!empty($unparseable_lines)) {
|
||||
$line_numbers = array_keys($unparseable_lines);
|
||||
$form_state->setError($form['styles'], $this->formatPlural(
|
||||
count($unparseable_lines),
|
||||
'Line @line-number does not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.',
|
||||
'Lines @line-numbers do not contain a valid value. Enter a valid CSS selector containing one or more classes, followed by a pipe symbol and a label.',
|
||||
[
|
||||
'@line-number' => reset($line_numbers),
|
||||
'@line-numbers' => implode(', ', $line_numbers),
|
||||
]
|
||||
));
|
||||
}
|
||||
$form_state->setValue('styles', $styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the line-based (for form) style configuration.
|
||||
*
|
||||
* @param string $form_value
|
||||
* A string containing >=1 lines with on each line a CSS selector targeting
|
||||
* 1 tag with >=1 classes, a pipe symbol and a label. An example of a single
|
||||
* line: `p.foo.bar|Foo bar paragraph`.
|
||||
*
|
||||
* @return array
|
||||
* The parsed equivalent: a list of arrays with each containing:
|
||||
* - label: the label after the pipe symbol, with whitespace trimmed
|
||||
* - element: the CKEditor 5 element equivalent of the tag + classes
|
||||
*
|
||||
* @internal
|
||||
* This method is public only to allow the CKEditor 4 to 5 upgrade path to
|
||||
* reuse this logic. Mark this private in https://www.drupal.org/i/3239012.
|
||||
*
|
||||
* @see \Drupal\ckeditor5\Plugin\CKEditor4To5Upgrade\Core::mapCKEditor4SettingsToCKEditor5Configuration()
|
||||
*/
|
||||
public static function parseStylesFormValue(string $form_value): array {
|
||||
$unparseable_lines = [];
|
||||
|
||||
$lines = explode("\n", $form_value);
|
||||
$styles = [];
|
||||
foreach ($lines as $index => $line) {
|
||||
if (empty(trim($line))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the line.
|
||||
[$selector, $label] = array_map('trim', explode('|', $line));
|
||||
|
||||
// Validate the selector.
|
||||
$selector_matches = [];
|
||||
// @see https://www.w3.org/TR/CSS2/syndata.html#:~:text=In%20CSS%2C%20identifiers%20(including%20element,hyphen%20followed%20by%20a%20digit
|
||||
if (!preg_match('/^([a-z][0-9a-zA-Z\-]*)((\.[a-zA-Z0-9\x{00A0}-\x{FFFF}\-_]+)+)$/u', $selector, $selector_matches)) {
|
||||
$unparseable_lines[$index + 1] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse selector into tag + classes and normalize.
|
||||
$tag = $selector_matches[1];
|
||||
$classes = array_filter(explode('.', $selector_matches[2]));
|
||||
$normalized = HTMLRestrictions::fromString(sprintf('<%s class="%s">', $tag, implode(' ', $classes)));
|
||||
|
||||
$styles[] = [
|
||||
'label' => $label,
|
||||
'element' => $normalized->toCKEditor5ElementsArray()[0],
|
||||
];
|
||||
}
|
||||
return [$styles, $unparseable_lines];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->configuration['styles'] = $form_state->getValue('styles');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return [
|
||||
'styles' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getElementsSubset(): array {
|
||||
return array_column($this->configuration['styles'], 'element');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
|
||||
$definitions = [];
|
||||
foreach ($this->configuration['styles'] as $style) {
|
||||
[$tag, $classes] = self::getTagAndClasses(HTMLRestrictions::fromString($style['element']));
|
||||
// Transform configured styles to the configuration structure expected by
|
||||
// the CKEditor 5 Style plugin.
|
||||
$definitions[] = [
|
||||
'name' => $style['label'],
|
||||
'element' => $tag,
|
||||
'classes' => $classes,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'style' => [
|
||||
'definitions' => $definitions,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -331,10 +331,26 @@ class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5Pl
|
|||
$subset = $this->getPlugin($id, $editor)->getElementsSubset();
|
||||
$subset_restrictions = HTMLRestrictions::fromString(implode($subset));
|
||||
$defined_restrictions = HTMLRestrictions::fromString(implode($defined_elements));
|
||||
$subset_violations = $subset_restrictions->diff($defined_restrictions)->toCKEditor5ElementsArray();
|
||||
if (!empty($subset_violations)) {
|
||||
throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $subset_violations)));
|
||||
// Determine max supported elements by resolving wildcards in the
|
||||
// restrictions defined by the plugin.
|
||||
$max_supported = $defined_restrictions;
|
||||
if (!$defined_restrictions->getWildcardSubset()->allowsNothing()) {
|
||||
$concrete_tags_to_use_to_resolve_wildcards = $subset_restrictions->extractPlainTagsSubset();
|
||||
$max_supported = $max_supported->merge($concrete_tags_to_use_to_resolve_wildcards)
|
||||
->diff($concrete_tags_to_use_to_resolve_wildcards);
|
||||
}
|
||||
$not_in_max_supported = $subset_restrictions->diff($max_supported);
|
||||
if (!$not_in_max_supported->allowsNothing()) {
|
||||
// If the editor is still being configured, the configuration may
|
||||
// not yet be valid.
|
||||
if ($editor->isNew()) {
|
||||
$subset = [];
|
||||
}
|
||||
else {
|
||||
throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $not_in_max_supported->toCKEditor5ElementsArray())));
|
||||
}
|
||||
}
|
||||
|
||||
// Also detect what is technically a valid subset, but has lost the
|
||||
// ability to create tags that are still in the subset. This points to
|
||||
// a bug in the plugin's ::getElementsSubset() logic.
|
||||
|
@ -346,6 +362,7 @@ class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5Pl
|
|||
if (!$missing_creatable_for_subset->allowsNothing()) {
|
||||
throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("%s") but the following tags can no longer be created: "%s".', $id, implode($subset_restrictions->toCKEditor5ElementsArray()), implode($missing_creatable_for_subset->toCKEditor5ElementsArray())));
|
||||
}
|
||||
|
||||
$defined_elements = $subset;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -623,6 +623,7 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
$submitted_editor->setSettings($settings);
|
||||
$eventual_editor_and_format_for_plugin_settings_visibility = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor);
|
||||
$settings['plugins'] = [];
|
||||
$default_configurations = [];
|
||||
foreach ($this->ckeditor5PluginManager->getDefinitions() as $plugin_id => $definition) {
|
||||
if (!$definition->isConfigurable()) {
|
||||
continue;
|
||||
|
@ -636,6 +637,12 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
// @see editor_image_upload_settings_form()
|
||||
$default_configuration = $plugin->defaultConfiguration();
|
||||
$configuration_stored_out_of_band = empty($default_configuration);
|
||||
// If this plugin is configurable but has not yet had user interaction,
|
||||
// the default configuration will still be active and may trigger
|
||||
// validation errors. Do not trigger those validation errors until the
|
||||
// form is actually saved, to allow the user to first configure other
|
||||
// CKEditor 5 functionality.
|
||||
$default_configurations[$plugin_id] = $default_configuration;
|
||||
|
||||
if ($form_state->hasValue(['plugins', $plugin_id])) {
|
||||
$subform = $form['plugins'][$plugin_id];
|
||||
|
@ -672,6 +679,31 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
$eventual_editor_and_format = $this->getEventualEditorWithPrimedFilterFormat($form_state, $submitted_editor);
|
||||
$violations = CKEditor5::validatePair($eventual_editor_and_format, $eventual_editor_and_format->getFilterFormat());
|
||||
foreach ($violations as $violation) {
|
||||
$property_path_parts = explode('.', $violation->getPropertyPath());
|
||||
|
||||
// Special case: AJAX updates that do not submit the form (that cannot
|
||||
// result in configuration being saved).
|
||||
if ($form_state->getSubmitHandlers() === ['editor_form_filter_admin_format_editor_configure']) {
|
||||
// Ensure that plugins' validation constraints do not immediately
|
||||
// trigger a validation error: the user may choose to configure other
|
||||
// CKEditor 5 aspects first.
|
||||
if ($property_path_parts[0] === 'settings' && $property_path_parts[1] === 'plugins') {
|
||||
$plugin_id = $property_path_parts[2];
|
||||
// This CKEditor 5 plugin settings form was just added: the user has
|
||||
// not yet had a chance to configure it.
|
||||
if (!$form_state->hasValue(['plugins', $plugin_id])) {
|
||||
continue;
|
||||
}
|
||||
// This CKEditor 5 plugin settings form was added recently, the user
|
||||
// is triggering AJAX rebuilds of the configuration UI because they're
|
||||
// configuring other functionality first. Only require these to be
|
||||
// valid at form submission time.
|
||||
if ($form_state->getValue(['plugins', $plugin_id]) === $default_configurations[$plugin_id]) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$form_item_name = static::mapPairViolationPropertyPathsToFormNames($violation->getPropertyPath(), $form);
|
||||
// When adding a toolbar item, it is possible that not all conditions for
|
||||
// using it have been met yet. FormBuilder refuses to rebuild forms when a
|
||||
|
@ -816,6 +848,11 @@ class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
*/
|
||||
protected static function createEphemeralPairedEditor(EditorInterface $editor, FilterFormatInterface $filter_format): EditorInterface {
|
||||
$paired_editor = clone $editor;
|
||||
// If the editor is still being configured, the configuration may not yet be
|
||||
// valid. Explicitly mark the ephemeral paired editor as new to allow other
|
||||
// code to treat this accordingly.
|
||||
// @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getProvidedElements()
|
||||
$paired_editor->enforceIsNew(TRUE);
|
||||
$reflector = new \ReflectionObject($paired_editor);
|
||||
$property = $reflector->getProperty('filterFormat');
|
||||
$property->setAccessible(TRUE);
|
||||
|
|
|
@ -23,4 +23,25 @@ class CKEditor5ElementConstraint extends Constraint {
|
|||
*/
|
||||
public $message = 'The following tag is not valid HTML: %provided_element.';
|
||||
|
||||
/**
|
||||
* Violation message when a required attribute is missing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $missingRequiredAttributeMessage = 'The following tag is missing the required attribute <code>@required_attribute_name</code>: <code>@provided_element</code>.';
|
||||
|
||||
/**
|
||||
* Violation message when a required attribute does not allow enough values.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $requiredAttributeMinValuesMessage = 'The following tag does not have the minimum of @min_attribute_value_count allowed values for the required attribute <code>@required_attribute_name</code>: <code>@provided_element</code>.';
|
||||
|
||||
/**
|
||||
* Validation constraint option to impose attributes to be specified.
|
||||
*
|
||||
* @var null|array
|
||||
*/
|
||||
public $requiredAttributes = NULL;
|
||||
|
||||
}
|
||||
|
|
|
@ -32,6 +32,41 @@ class CKEditor5ElementConstraintValidator extends ConstraintValidator {
|
|||
->setParameter('%provided_element', $element)
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
// The optional "requiredAttributes" constraint property allows more
|
||||
// detailed validation.
|
||||
if (isset($constraint->requiredAttributes)) {
|
||||
$allowed_elements = $parsed->getAllowedElements();
|
||||
$tag = array_keys($allowed_elements)[0];
|
||||
$attribute_restrictions = $allowed_elements[$tag];
|
||||
assert(is_array($constraint->requiredAttributes));
|
||||
foreach ($constraint->requiredAttributes as $required_attribute) {
|
||||
// Validate attributeName.
|
||||
$required_attribute_name = $required_attribute['attributeName'];
|
||||
if (!is_array($attribute_restrictions) || !isset($attribute_restrictions[$required_attribute_name])) {
|
||||
$this->context->buildViolation($constraint->missingRequiredAttributeMessage)
|
||||
->setParameter('@provided_element', $element)
|
||||
->setParameter('@required_attribute_name', $required_attribute_name)
|
||||
->addViolation();
|
||||
continue;
|
||||
}
|
||||
|
||||
$attribute_values = $attribute_restrictions[$required_attribute_name];
|
||||
|
||||
// Validate minAttributeValueCount if specified.
|
||||
if (isset($required_attribute['minAttributeValueCount'])) {
|
||||
$min_attribute_value_count = $required_attribute['minAttributeValueCount'];
|
||||
if (!is_array($attribute_values) || count($attribute_values) < $min_attribute_value_count) {
|
||||
$this->context->buildViolation($constraint->requiredAttributeMinValuesMessage)
|
||||
->setParameter('@provided_element', $element)
|
||||
->setParameter('@required_attribute_name', $required_attribute_name)
|
||||
->setParameter('@min_attribute_value_count', $min_attribute_value_count)
|
||||
->addViolation();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
|||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
@ -199,32 +200,36 @@ class FundamentalCompatibilityConstraintValidator extends ConstraintValidator im
|
|||
foreach ($non_creatable_tags->toCKEditor5ElementsArray() as $non_creatable_tag) {
|
||||
// Find the plugin which has a non-creatable tag.
|
||||
$needle = HTMLRestrictions::fromString($non_creatable_tag);
|
||||
$matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle) {
|
||||
$matching_plugins = array_filter($enabled_definitions, function (CKEditor5PluginDefinition $d) use ($needle, $text_editor) {
|
||||
if (!$d->hasElements()) {
|
||||
return FALSE;
|
||||
}
|
||||
$haystack = HTMLRestrictions::fromString(implode($d->getElements()));
|
||||
return !$haystack->intersect($needle)->allowsNothing();
|
||||
$haystack = new HTMLRestrictions($this->pluginManager->getProvidedElements([$d->id()], $text_editor, FALSE, FALSE));
|
||||
return !$haystack->extractPlainTagsSubset()->intersect($needle)->allowsNothing();
|
||||
});
|
||||
assert(count($matching_plugins) === 1);
|
||||
$plugin_definition = reset($matching_plugins);
|
||||
assert($plugin_definition instanceof CKEditor5PluginDefinition);
|
||||
|
||||
// Compute which attributes it would be able to create on this tag.
|
||||
$matching_elements = array_filter($plugin_definition->getElements(), function (string $element) use ($needle) {
|
||||
$haystack = HTMLRestrictions::fromString($element);
|
||||
return !$haystack->intersect($needle)->allowsNothing();
|
||||
});
|
||||
$attributes_on_tag = HTMLRestrictions::fromString(implode($matching_elements));
|
||||
$provided_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements([$plugin_definition->id()], $text_editor, FALSE, FALSE));
|
||||
$attributes_on_tag = $provided_elements->intersect(
|
||||
new HTMLRestrictions(array_fill_keys(array_keys($needle->getAllowedElements()), TRUE))
|
||||
);
|
||||
|
||||
$violation = $this->context->buildViolation($constraint->nonCreatableTagMessage)
|
||||
->setParameter('@non_creatable_tag', $non_creatable_tag)
|
||||
->setParameter('%plugin', $plugin_definition->label())
|
||||
->setParameter('@attributes_on_tag', implode(', ', $attributes_on_tag->toCKEditor5ElementsArray()));
|
||||
|
||||
// If this plugin has a configurable subset, associate the violation
|
||||
// with the property path pointing to this plugin's settings form.
|
||||
if (is_a($plugin_definition->getClass(), CKEditor5PluginElementsSubsetInterface::class, TRUE)) {
|
||||
$violation->atPath(sprintf('settings.plugins.%s', $plugin_definition->id()));
|
||||
}
|
||||
// If this plugin is associated with a toolbar item, associate the
|
||||
// violation with the property path pointing to the active toolbar item.
|
||||
if ($plugin_definition->hasToolbarItems()) {
|
||||
elseif ($plugin_definition->hasToolbarItems()) {
|
||||
$toolbar_items = $plugin_definition->getToolbarItems();
|
||||
$active_toolbar_items = array_intersect(
|
||||
$text_editor->getSettings()['toolbar']['items'],
|
||||
|
|
|
@ -4,7 +4,11 @@ declare(strict_types = 1);
|
|||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
// cspell:ignore enableable
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
|
@ -40,4 +44,49 @@ trait PluginManagerDependentValidatorTrait {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all other enabled CKEditor 5 plugin definitions.
|
||||
*
|
||||
* @param \Drupal\editor\EditorInterface $text_editor
|
||||
* A Text Editor config entity configured to use CKEditor 5.
|
||||
* @param string $except
|
||||
* A CKEditor 5 plugin ID to exclude: all enabled plugins other than this
|
||||
* one are returned.
|
||||
*
|
||||
* @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[]
|
||||
* A list of CKEditor 5 plugin definitions keyed by plugin ID.
|
||||
*/
|
||||
private function getOtherEnabledPlugins(EditorInterface $text_editor, string $except): array {
|
||||
$enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor);
|
||||
unset($enabled_plugins[$except]);
|
||||
return $enabled_plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all disabled CKEditor 5 plugin definitions the user can enable.
|
||||
*
|
||||
* @param \Drupal\editor\EditorInterface $text_editor
|
||||
* A Text Editor config entity configured to use CKEditor 5.
|
||||
*
|
||||
* @return \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[]
|
||||
* A list of CKEditor 5 plugin definitions keyed by plugin ID.
|
||||
*/
|
||||
private function getEnableableDisabledPlugins(EditorInterface $text_editor) {
|
||||
$disabled_plugins = array_diff_key(
|
||||
$this->pluginManager->getDefinitions(),
|
||||
$this->pluginManager->getEnabledDefinitions($text_editor)
|
||||
);
|
||||
// Only consider plugins that can be explicitly enabled by the user: plugins
|
||||
// that have a toolbar item and do not have conditions. Those are the only
|
||||
// plugins that are truly available for the site builder to enable without
|
||||
// other consequences.
|
||||
// In the future, we may choose to expand this, but it will require complex
|
||||
// infrastructure to generate messages that explain which of the conditions
|
||||
// are already fulfilled and which are not.
|
||||
$enableable_disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) {
|
||||
return $definition->hasToolbarItems() && !$definition->hasConditions();
|
||||
});
|
||||
return $enableable_disabled_plugins;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\TypedData\Validation\ExecutionContext;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
|
||||
/**
|
||||
* A constraint may need preceding constraints to not have been violated.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait PrecedingConstraintAwareValidatorTrait {
|
||||
|
||||
/**
|
||||
* Checks whether any preceding constraints have been violated.
|
||||
*
|
||||
* @param \Symfony\Component\Validator\Constraint $current_constraint
|
||||
* The constraint currently being validated.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if any preceding constraints have been violated, FALSE otherwise.
|
||||
*/
|
||||
protected function hasViolationsForPrecedingConstraints(Constraint $current_constraint): bool {
|
||||
assert($this->context instanceof ExecutionContext);
|
||||
$earlier_constraints = iterator_to_array($this->getPrecedingConstraints($current_constraint));
|
||||
$earlier_violations = array_filter(
|
||||
iterator_to_array($this->context->getViolations()),
|
||||
function (ConstraintViolationInterface $violation) use ($earlier_constraints) {
|
||||
return in_array($violation->getConstraint(), $earlier_constraints);
|
||||
}
|
||||
);
|
||||
return !empty($earlier_violations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constraints preceding the given constraint in the current context.
|
||||
*
|
||||
* @param \Symfony\Component\Validator\Constraint $needle
|
||||
* The constraint to find the preceding constraints for.
|
||||
*
|
||||
* @return iterable
|
||||
* The preceding constraints.
|
||||
*/
|
||||
private function getPrecedingConstraints(Constraint $needle): iterable {
|
||||
assert($this->context instanceof ExecutionContext);
|
||||
$constraints = $this->context->getMetadata()->getConstraints();
|
||||
if (!in_array($needle, $constraints)) {
|
||||
throw new \OutOfBoundsException();
|
||||
}
|
||||
foreach ($constraints as $constraint) {
|
||||
if ($constraint != $needle) {
|
||||
yield $constraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,8 +4,9 @@ declare(strict_types = 1);
|
|||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
// cspell:ignore enableable
|
||||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
@ -38,25 +39,15 @@ class SourceEditingRedundantTagsConstraintValidator extends ConstraintValidator
|
|||
}
|
||||
|
||||
$text_editor = $this->createTextEditorObjectFromContext();
|
||||
$enabled_plugins = $this->pluginManager->getEnabledDefinitions($text_editor);
|
||||
$disabled_plugins = array_diff_key($this->pluginManager->getDefinitions(), $enabled_plugins);
|
||||
// Only consider plugins that can be explicitly enabled by the user: plugins
|
||||
// that have a toolbar item and do not have conditions. Those are the only
|
||||
// plugins that are truly available for the site builder to enable without
|
||||
// other consequences.
|
||||
// In the future, we may choose to expand this, but it will require complex
|
||||
// infrastructure to generate messages that explain which of the conditions
|
||||
// are already fulfilled and which are not.
|
||||
$disabled_plugins = array_filter($disabled_plugins, function (CKEditor5PluginDefinition $definition) {
|
||||
return $definition->hasToolbarItems() && !$definition->hasConditions();
|
||||
});
|
||||
unset($enabled_plugins['ckeditor5_sourceEditing']);
|
||||
|
||||
$other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_sourceEditing');
|
||||
$enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
|
||||
|
||||
// An array of tags enabled by every plugin other than Source Editing.
|
||||
$enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE));
|
||||
$disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE));
|
||||
$enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins), $text_editor, FALSE, TRUE));
|
||||
$disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins), $text_editor, FALSE, TRUE));
|
||||
$enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE));
|
||||
$disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE));
|
||||
$enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE, TRUE));
|
||||
$disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE, TRUE));
|
||||
|
||||
// The single element for which source editing is enabled, which we are
|
||||
// checking now.
|
||||
|
@ -85,7 +76,7 @@ class SourceEditingRedundantTagsConstraintValidator extends ConstraintValidator
|
|||
foreach ([$enabled_plugin_overlap, $disabled_plugin_overlap] as $overlap) {
|
||||
$checking_enabled = $overlap === $enabled_plugin_overlap;
|
||||
if (!$overlap->allowsNothing()) {
|
||||
$plugins_to_check_against = $checking_enabled ? $enabled_plugins : $disabled_plugins;
|
||||
$plugins_to_check_against = $checking_enabled ? $other_enabled_plugins : $enableable_disabled_plugins;
|
||||
$plain_tags_to_check_against = $checking_enabled ? $enabled_plugin_plain_tags : $disabled_plugin_plain_tags;
|
||||
$tags_plugin_report = $this->pluginsSupplyingTagsMessage($overlap, $plugins_to_check_against, $enabled_plugin_elements);
|
||||
$message = $checking_enabled ? $constraint->enabledPluginsMessage : $constraint->availablePluginsMessage;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
// cspell:ignore enableable
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Styles can only be specified for HTML5 tags and extra classes.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "StyleSensibleElement",
|
||||
* label = @Translation("Styles can only be specified for already supported tags.", context = "Validation"),
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class StyleSensibleElementConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* When a style is defined for a non-HTML5 tag.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $nonHtml5TagMessage = 'A style can only be specified for an HTML 5 tag. <code>@tag</code> is not an HTML5 tag.';
|
||||
|
||||
/**
|
||||
* When a Style is defined with classes supported by an enabled plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $conflictingEnabledPluginMessage = 'A style must only specify classes not supported by other plugins. The <code>@classes</code> classes on <code>@tag</code> are already supported by the enabled %plugin plugin.';
|
||||
|
||||
/**
|
||||
* When a Style is defined with classes supported by a disabled plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $conflictingDisabledPluginMessage = 'A style must only specify classes not supported by other plugins. The <code>@classes</code> classes on <code>@tag</code> are supported by the %plugin plugin. Remove this style and enable that plugin instead.';
|
||||
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
// cspell:ignore enableable
|
||||
|
||||
use Drupal\ckeditor5\HTMLRestrictions;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
/**
|
||||
* Styles can only be specified for HTML5 tags and extra classes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class StyleSensibleElementConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
use PrecedingConstraintAwareValidatorTrait;
|
||||
use PluginManagerDependentValidatorTrait;
|
||||
use TextEditorObjectDependentValidatorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
|
||||
* Thrown when the given constraint is not supported by this validator.
|
||||
*/
|
||||
public function validate($element, Constraint $constraint) {
|
||||
if (!$constraint instanceof StyleSensibleElementConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, StyleSensibleElementConstraint::class);
|
||||
}
|
||||
// The preceding constraints (in this case: CKEditor5Element) must be valid.
|
||||
if ($this->hasViolationsForPrecedingConstraints($constraint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$text_editor = $this->createTextEditorObjectFromContext();
|
||||
|
||||
// The single tag for which a style is specified, which we are checking now.
|
||||
$style_element = HTMLRestrictions::fromString($element);
|
||||
assert(count($style_element->getAllowedElements()) === 1);
|
||||
[$tag, $classes] = Style::getTagAndClasses($style_element);
|
||||
|
||||
// Ensure the tag is in the range supported by the Style plugin.
|
||||
$superset = HTMLRestrictions::fromString('<$any-html5-element class>');
|
||||
$supported_range = $superset->merge($style_element->extractPlainTagsSubset());
|
||||
if (!$style_element->diff($supported_range)->allowsNothing()) {
|
||||
$this->context->buildViolation($constraint->nonHtml5TagMessage)
|
||||
->setParameter('@tag', sprintf("<%s>", $tag))
|
||||
->addViolation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the list of tags enabled by every plugin other than Style.
|
||||
$other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_style');
|
||||
$enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
|
||||
|
||||
$other_enabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE));
|
||||
$disabled_plugin_elements = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE));
|
||||
|
||||
// Next, validate that the classes specified for this style are not
|
||||
// supported by an enabled plugin.
|
||||
if (self::intersectionWithClasses($style_element, $other_enabled_plugin_elements)) {
|
||||
$this->context->buildViolation($constraint->conflictingEnabledPluginMessage)
|
||||
->setParameter('@tag', sprintf("<%s>", $tag))
|
||||
->setParameter('@classes', implode(", ", $classes))
|
||||
->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
|
||||
->addViolation();
|
||||
}
|
||||
// Next, validate that the classes specified for this style are not
|
||||
// supported by a disabled plugin.
|
||||
elseif (self::intersectionWithClasses($style_element, $disabled_plugin_elements)) {
|
||||
$this->context->buildViolation($constraint->conflictingDisabledPluginMessage)
|
||||
->setParameter('@tag', sprintf("<%s>", $tag))
|
||||
->setParameter('@classes', implode(", ", $classes))
|
||||
->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is an intersection on allowed 'class' attribute values.
|
||||
*
|
||||
* @param \Drupal\ckeditor5\HTMLRestrictions $a
|
||||
* One set of HTML restrictions.
|
||||
* @param \Drupal\ckeditor5\HTMLRestrictions $b
|
||||
* Another set of HTML restrictions.
|
||||
*
|
||||
* @return bool
|
||||
* Whether there is an intersection.
|
||||
*/
|
||||
private static function intersectionWithClasses(HTMLRestrictions $a, HTMLRestrictions $b): bool {
|
||||
// Compute the intersection, but first resolve wildcards, by merging
|
||||
// tags of the other operand. Because only tags are merged, this cannot
|
||||
// introduce a 'class' attribute intersection.
|
||||
// For example: a plugin may support `<$text-container class="foo">`. On its
|
||||
// own that would not trigger an intersection, but when resolved into
|
||||
// concrete tags it could.
|
||||
$tags_from_a = array_diff(array_keys($a->getConcreteSubset()->getAllowedElements()), ['*']);
|
||||
$tags_from_b = array_diff(array_keys($b->getConcreteSubset()->getAllowedElements()), ['*']);
|
||||
$a = $a->merge(new HTMLRestrictions(array_fill_keys($tags_from_b, FALSE)));
|
||||
$b = $b->merge(new HTMLRestrictions(array_fill_keys($tags_from_a, FALSE)));
|
||||
$intersection = $a->intersect($b);
|
||||
|
||||
// Leverage the "GHS configuration" representation to easily find whether
|
||||
// there is an intersection for classes. Other implementations are possible.
|
||||
$intersection_as_ghs_config = $intersection->toGeneralHtmlSupportConfig();
|
||||
$ghs_config_classes = array_column($intersection_as_ghs_config, 'classes');
|
||||
return !empty($ghs_config_classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the plugin with elements that conflict with the style element.
|
||||
*
|
||||
* @param \Drupal\ckeditor5\HTMLRestrictions $needle
|
||||
* A style definition element: a single tag, plus the 'class' attribute,
|
||||
* plus >=1 allowed 'class' attribute values.
|
||||
*
|
||||
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
|
||||
* The label of the plugin that is conflicting with this style.
|
||||
*
|
||||
* @throws \OutOfBoundsException
|
||||
* When a $needle is provided which does not exist among the other plugins.
|
||||
*/
|
||||
private function findStyleConflictingPluginLabel(HTMLRestrictions $needle): TranslatableMarkup {
|
||||
foreach ($this->pluginManager->getDefinitions() as $id => $definition) {
|
||||
// We're looking to find the other plugin, not this one.
|
||||
if ($id === 'ckeditor5_style') {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert($definition instanceof CKEditor5PluginDefinition);
|
||||
if (!$definition->hasElements()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$haystack = HTMLRestrictions::fromString(implode($definition->getElements()));
|
||||
if ($id === 'ckeditor5_sourceEditing') {
|
||||
// The Source Editing plugin's allowed elements are based on stored
|
||||
// config. This differs from all other plugins, which establish allowed
|
||||
// elements as part of their definition. Because of this, the $haystack
|
||||
// is calculated differently for Source Editing.
|
||||
$text_editor = $this->createTextEditorObjectFromContext();
|
||||
$editor_plugins = $text_editor->getSettings()['plugins'];
|
||||
if (!empty($editor_plugins['ckeditor5_sourceEditing'])) {
|
||||
$source_tags = $editor_plugins['ckeditor5_sourceEditing']['allowed_tags'];
|
||||
$haystack = HTMLRestrictions::fromString(implode($source_tags));
|
||||
}
|
||||
}
|
||||
if (self::intersectionWithClasses($needle, $haystack)) {
|
||||
return $definition->label();
|
||||
}
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Uniquely labeled list item constraint.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "UniqueLabelInList",
|
||||
* label = @Translation("Unique label in list", context = "Validation"),
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UniqueLabelInListConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The default violation message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = 'The label %label is not unique.';
|
||||
|
||||
/**
|
||||
* The key of the label that this validation constraint should check.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
public $labelKey = NULL;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRequiredOptions() {
|
||||
return ['labelKey'];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
/**
|
||||
* Uniquely labeled list item constraint validator.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UniqueLabelInListConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
|
||||
* Thrown when the given constraint is not supported by this validator.
|
||||
*/
|
||||
public function validate($list, Constraint $constraint) {
|
||||
if (!$constraint instanceof UniqueLabelInListConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, UniqueLabelInListConstraint::class);
|
||||
}
|
||||
|
||||
$labels = array_column($list, $constraint->labelKey);
|
||||
$label_frequencies = array_count_values($labels);
|
||||
|
||||
foreach ($label_frequencies as $label => $frequency) {
|
||||
if ($frequency > 1) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('%label', $label)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -153,6 +153,15 @@ final class SmartDefaultSettings {
|
|||
[$upgraded_settings, $messages] = $this->createSettingsFromCKEditor4($old_editor->getSettings(), HTMLRestrictions::fromTextFormat($old_editor->getFilterFormat()));
|
||||
$editor->setSettings($upgraded_settings);
|
||||
$editor->setImageUploadSettings($old_editor->getImageUploadSettings());
|
||||
// *Before* determining which elements are still needed for this text
|
||||
// format, ensure that all already enabled plugins that are configurable
|
||||
// have valid settings.
|
||||
// For all already enabled plugins, find the ones that are configurable,
|
||||
// and add their default settings. For enabled plugins with element
|
||||
// subsets, compute the appropriate settings to achieve the subset that
|
||||
// matches the original text format restrictions.
|
||||
$this->addDefaultSettingsForEnabledConfigurablePlugins($editor);
|
||||
$this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format);
|
||||
}
|
||||
|
||||
// Add toolbar items based on HTML tags and attributes.
|
||||
|
@ -224,6 +233,9 @@ final class SmartDefaultSettings {
|
|||
// and add their default settings. For enabled plugins with element subsets,
|
||||
// compute the appropriate settings to achieve the subset that matches the
|
||||
// original text format restrictions.
|
||||
// Note: if switching from CKEditor 4, this will already have happened for
|
||||
// plugins that were already enabled in CKEditor 4. It's harmless to compute
|
||||
// this again.
|
||||
$this->addDefaultSettingsForEnabledConfigurablePlugins($editor);
|
||||
$this->computeSubsetSettingForEnabledPluginsWithSubsets($editor, $text_format);
|
||||
|
||||
|
@ -780,12 +792,13 @@ final class SmartDefaultSettings {
|
|||
* The text editor config entity to update.
|
||||
*
|
||||
* @return array|null
|
||||
* NULL when nothing happened, otherwise an array with three values:
|
||||
* NULL when nothing happened, otherwise an array with four values:
|
||||
* 1. a description (for use in a message) of which CKEditor 5 plugins were
|
||||
* enabled to match the HTML tags allowed by the text format.
|
||||
* 2. a description (for use in a message) of which CKEditor 5 plugins were
|
||||
* enabled to match the HTML attributes allowed by the text format.
|
||||
* 3. the unsupported elements, in an HTMLRestrictions value object
|
||||
* 3. the unsupported elements, in an HTMLRestrictions value object.
|
||||
* 4. the list of enabled plugin labels.
|
||||
*/
|
||||
private function addToolbarItemsToMatchHtmlElementsInFormat(FilterFormatInterface $format, EditorInterface $editor): ?array {
|
||||
$html_restrictions_needed_elements = $format->getHtmlRestrictions();
|
||||
|
@ -797,11 +810,9 @@ final class SmartDefaultSettings {
|
|||
$enabled_definitions = $this->pluginManager->getEnabledDefinitions($editor);
|
||||
$disabled_definitions = array_diff_key($all_definitions, $enabled_definitions);
|
||||
$enabled_plugins = array_keys($enabled_definitions);
|
||||
$provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins);
|
||||
$provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins, $editor);
|
||||
$provided = new HTMLRestrictions($provided_elements);
|
||||
$needed = HTMLRestrictions::fromTextFormat($format);
|
||||
$still_needed = $needed->diff($provided);
|
||||
|
||||
// Plugins only supporting <tag attr> cannot create the tag. For that, they
|
||||
// must support plain <tag> too. With this being the case, break down what
|
||||
// is needed based on what is currently provided.
|
||||
|
@ -812,11 +823,12 @@ final class SmartDefaultSettings {
|
|||
$provided_plain_tags = new HTMLRestrictions(
|
||||
$this->pluginManager->getProvidedElements($enabled_plugins, NULL, FALSE, TRUE)
|
||||
);
|
||||
|
||||
// Determine the still needed plain tags, the still needed attributes, and
|
||||
// the union of both.
|
||||
$still_needed_plain_tags = $needed->extractPlainTagsSubset()->diff($provided_plain_tags);
|
||||
$still_needed_attributes = $still_needed->diff($still_needed_plain_tags);
|
||||
// Merging $still_needed_plain_tags with $still_needed_attributes must
|
||||
// always equal $still_needed.
|
||||
assert($still_needed_plain_tags->merge($still_needed_attributes)->diff($still_needed)->allowsNothing());
|
||||
$still_needed_attributes = $needed->diff($provided)->diff($still_needed_plain_tags);
|
||||
$still_needed = $still_needed_plain_tags->merge($still_needed_attributes);
|
||||
|
||||
if (!$still_needed->allowsNothing()) {
|
||||
// Select plugins for supporting the still needed plain tags.
|
||||
|
@ -889,6 +901,7 @@ final class SmartDefaultSettings {
|
|||
NULL,
|
||||
NULL,
|
||||
$still_needed,
|
||||
NULL,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +7,5 @@ ckeditor5_plugin_elements_subset_sneakySuperset:
|
|||
elements:
|
||||
- <foo>
|
||||
- <bar>
|
||||
- <bar baz>
|
||||
- <$any-html5-element class>
|
||||
|
|
|
@ -48,7 +48,7 @@ class CKEditor5AllowedTagsTest extends CKEditor5TestBase {
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <a hreflang href> <blockquote cite> <ul type> <ol type start> <img src alt data-entity-type data-entity-uuid> <strong> <em> <code> <li>';
|
||||
protected $defaultElementsAfterUpdatingToCkeditor5 = '<br> <p> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <cite> <dl> <dt> <dd> <img src alt data-entity-type data-entity-uuid> <a hreflang href> <blockquote cite> <ul type> <ol type start> <strong> <em> <code> <li>';
|
||||
|
||||
/**
|
||||
* Test enabling CKEditor 5 in a way that triggers validation.
|
||||
|
|
|
@ -137,4 +137,14 @@ JS;
|
|||
$assert_session->assert((bool) preg_match($regex, $actual), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that no real-time validation errors are present.
|
||||
*
|
||||
* @throws \Behat\Mink\Exception\ElementNotFoundException
|
||||
*/
|
||||
protected function assertNoRealtimeValidationErrors(): void {
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertSame('', $assert_session->elementExists('css', '[data-drupal-selector="ckeditor5-realtime-validation-messages-container"]')->getHtml());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1624,24 +1624,4 @@ JS;
|
|||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects text inside an element.
|
||||
*
|
||||
* @param string $selector
|
||||
* A CSS selector for the element which contents should be selected.
|
||||
*/
|
||||
protected function selectTextInsideElement(string $selector): void {
|
||||
$javascript = <<<JS
|
||||
(function() {
|
||||
const el = document.querySelector("$selector");
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
})();
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,318 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\FunctionalJavascript;
|
||||
|
||||
// cspell:ignore sourceediting
|
||||
|
||||
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class StyleTest extends CKEditor5TestBase {
|
||||
|
||||
use CKEditor5TestTrait;
|
||||
|
||||
/**
|
||||
* @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style::buildConfigurationForm
|
||||
*/
|
||||
public function testStyleSettingsForm() {
|
||||
$this->drupalLogin($this->drupalCreateUser(['administer filters']));
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->createNewTextFormat($page, $assert_session);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The Style plugin settings form should not be present.
|
||||
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style"]');
|
||||
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-style'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-style', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// No validation error upon enabling the Style plugin.
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
$assert_session->pageTextContains('No styles configured');
|
||||
|
||||
// Still no validation error when configuring other functionality first.
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-undo', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$this->assertNoRealtimeValidationErrors();
|
||||
|
||||
// The Style plugin settings form should now be present and should have no
|
||||
// styles configured.
|
||||
$page->clickLink('Style');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
|
||||
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'p.foo.bar | Foobar paragraph';
|
||||
allowedTags.dispatchEvent(new Event('input'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// Immediately save the configuration. Intentionally do nothing that would
|
||||
// trigger an AJAX rebuild.
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextContains('Added text format');
|
||||
|
||||
// Verify that the configuration was saved.
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$page->clickLink('Style');
|
||||
$this->assertNotNull($styles_textarea = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]'));
|
||||
|
||||
$this->assertSame("p.foo.bar|Foobar paragraph\n", $styles_textarea->getValue());
|
||||
$assert_session->pageTextContains('One style configured');
|
||||
$allowed_html_field = $assert_session->fieldExists('filters[filter_html][settings][allowed_html]');
|
||||
$this->assertStringContainsString('<p class="foo bar">', $allowed_html_field->getValue());
|
||||
|
||||
// Attempt to use an unsupported HTML5 tag.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 's.redacted|Redacted';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// The CKEditor 5 module should refuse to specify styles on tags that cannot
|
||||
// (yet) be created.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\FundamentalCompatibilityConstraintValidator::checkAllHtmlTagsAreCreatable()
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
// The entire vertical tab for "Style" settings should be marked up as the
|
||||
// cause of the error, which means the "Styles" text area in there is marked
|
||||
// too.
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
|
||||
|
||||
// Attempt to save anyway: the warning should become an error.
|
||||
$page->pressButton('Save configuration');
|
||||
$assert_session->pageTextNotContains('Added text format');
|
||||
$assert_session->elementExists('css', '[aria-label="Error message"]:contains("The Style plugin needs another plugin to create <s>, for it to be able to create the following attributes: <s class="redacted">. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.")');
|
||||
|
||||
// Now, attempt to use a supported non-HTML5 tag.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'drupal-media.sensational|Sensational media';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
|
||||
// The CKEditor 5 module should refuse to allow styles on non-HTML5 tags.
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style can only be specified for an HTML 5 tag. <drupal-media> is not an HTML5 tag.")');
|
||||
// The vertical tab for "Style" settings should not be marked up as the cause
|
||||
// of the error, but only the "Styles" text area in the vertical tab.
|
||||
$assert_session->elementNotExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"][aria-invalid="true"]');
|
||||
$assert_session->elementExists('css', '.vertical-tabs__pane[data-ckeditor5-plugin-id="ckeditor5_style"] textarea[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"][aria-invalid="true"]');
|
||||
|
||||
// Test configuration overlaps across plugins.
|
||||
$this->drupalGet('admin/config/content/formats/manage/ckeditor5');
|
||||
$this->assertNotEmpty($assert_session->elementExists('css', '.ckeditor5-toolbar-item-sourceEditing'));
|
||||
$this->triggerKeyUp('.ckeditor5-toolbar-item-sourceEditing', 'ArrowDown');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// The Source Editing plugin settings form should now be present and should
|
||||
// have no allowed tags configured.
|
||||
$page->clickLink('Source editing');
|
||||
$this->assertNotNull($assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]'));
|
||||
|
||||
// Make `<aside class>` creatable.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-sourceediting-allowed-tags"]');
|
||||
allowedTags.value = '<aside class>';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Create a style with `aside` and a class name.
|
||||
$javascript = <<<JS
|
||||
const allowedTags = document.querySelector('[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-style-styles"]');
|
||||
allowedTags.value = 'aside.error|Aside';
|
||||
allowedTags.dispatchEvent(new Event('change'));
|
||||
JS;
|
||||
$this->getSession()->executeScript($javascript);
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// The CKEditor 5 module should refuse to create configuration overlaps
|
||||
// across plugins.
|
||||
// @see \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator::findStyleConflictingPluginLabel()
|
||||
$assert_session->waitForElement('css', '[role=alert][data-drupal-message-type="error"]:contains("A style must only specify classes not supported by other plugins.")');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Style functionality: setting a class, expected style choices.
|
||||
*/
|
||||
public function testStyleFunctionality() {
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => TRUE,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p class="highlighted interesting"> <br> <a href class="reliable"> <blockquote class="famous"> <h2 class="red-heading">',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor5',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'heading',
|
||||
'link',
|
||||
'blockQuote',
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Highlighted & interesting',
|
||||
'element' => '<p class="highlighted interesting">',
|
||||
],
|
||||
[
|
||||
'label' => 'Red heading',
|
||||
'element' => '<h2 class="red-heading">',
|
||||
],
|
||||
[
|
||||
'label' => 'Reliable source',
|
||||
'element' => '<a class="reliable">',
|
||||
],
|
||||
[
|
||||
'label' => 'Famous',
|
||||
'element' => '<blockquote class="famous">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
])->save();
|
||||
$this->assertSame([], array_map(
|
||||
function (ConstraintViolation $v) {
|
||||
return (string) $v->getMessage();
|
||||
},
|
||||
iterator_to_array(CKEditor5::validatePair(
|
||||
Editor::load('test_format'),
|
||||
FilterFormat::load('test_format')
|
||||
))
|
||||
));
|
||||
|
||||
// Create a sample entity to test CKEditor 5.
|
||||
$node = $this->createNode([
|
||||
'type' => 'page',
|
||||
'title' => 'A selection of the history of Drupal',
|
||||
'body' => [
|
||||
'value' => '<h2>Upgrades</h2><p class="history">Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p> — <a class="reliable" href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
// Observe.
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]));
|
||||
$this->drupalGet($node->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
|
||||
// Select the <h2>, assert that no style is active currently..
|
||||
$this->selectTextInsideElement('h2');
|
||||
$assert_session = $this->assertSession();
|
||||
$style_dropdown = $assert_session->elementExists('css', '.ck-style-dropdown');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
|
||||
// Click the dropdown, check the available styles.
|
||||
$style_dropdown->click();
|
||||
$buttons = $style_dropdown->findAll('css', '.ck-dropdown__panel button');
|
||||
$this->assertCount(4, $buttons);
|
||||
$this->assertSame('Highlighted & interesting', $buttons[0]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Red heading', $buttons[1]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Famous', $buttons[2]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('Reliable source', $buttons[3]->find('css', '.ck-button__label')->getText());
|
||||
$this->assertSame('true', $buttons[0]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[1]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
// @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
|
||||
// $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
|
||||
// Apply the "Red heading" style and verify it has the expected effect.
|
||||
$assert_session->elementExists('css', '.ck-editor__main h2:not(.red-heading)');
|
||||
$buttons[1]->click();
|
||||
$assert_session->elementExists('css', '.ck-editor__main h2.red-heading');
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertSame('Red heading', $style_dropdown->getText());
|
||||
|
||||
// Select the first paragraph and observe changes in:
|
||||
// - styles dropdown label
|
||||
// - button states
|
||||
$this->selectTextInsideElement('p');
|
||||
$this->assertSame('Styles', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[2]->getAttribute('aria-disabled'));
|
||||
// @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
|
||||
// $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
// Close the dropdown.
|
||||
$style_dropdown->click();
|
||||
|
||||
// Select the blockquote and observe changes in:
|
||||
// - styles dropdown label
|
||||
// - button states
|
||||
$this->selectTextInsideElement('blockquote');
|
||||
$this->assertSame('Famous', $style_dropdown->getText());
|
||||
$style_dropdown->click();
|
||||
$this->assertTrue($buttons[0]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[1]->hasClass('ck-off'));
|
||||
$this->assertTrue($buttons[2]->hasClass('ck-on'));
|
||||
$this->assertTrue($buttons[3]->hasClass('ck-off'));
|
||||
$this->assertFalse($buttons[0]->hasAttribute('aria-disabled'));
|
||||
$this->assertSame('true', $buttons[1]->getAttribute('aria-disabled'));
|
||||
$this->assertFalse($buttons[2]->hasAttribute('aria-disabled'));
|
||||
// @todo Uncomment this after https://github.com/ckeditor/ckeditor5/issues/11709 is fixed.
|
||||
// $this->assertSame('true', $buttons[3]->getAttribute('aria-disabled'));
|
||||
// Close the dropdown.
|
||||
$style_dropdown->click();
|
||||
|
||||
// The resulting markup should be identical to the starting markup, with two
|
||||
// changes:
|
||||
// 1. the `red-heading` class has been added to the `<h2>`
|
||||
// 2. the `history` class has been removed from the `<p>`, because CKEditor
|
||||
// 5 has not been configured for this: if a Style had configured for it,
|
||||
// it would have been retained.
|
||||
$this->assertSame('<h2 class="red-heading">Upgrades</h2><p>Drupal has historically been difficult to upgrade from one major version to the next.</p><p class="highlighted interesting">This changed with Drupal 8.</p><blockquote class="famous"><p>Updating from Drupal 8\'s latest version to Drupal 9.0.0 should be as easy as updating between minor versions of Drupal 8.</p></blockquote><p>— <a class="reliable" href="https://dri.es/making-drupal-upgrades-easy-forever">Dries</a></p>', $this->getEditorDataAsHtmlString());
|
||||
}
|
||||
|
||||
}
|
|
@ -975,26 +975,30 @@ PHP,
|
|||
}
|
||||
|
||||
/**
|
||||
* Tests detection of invalid CKEditor5PluginElementsSubsetInterface class.
|
||||
* Tests detection of invalid CKEditor5PluginElementsSubsetInterface classes.
|
||||
*
|
||||
* @dataProvider providerProvidedElementsInvalidElementSubset
|
||||
*/
|
||||
public function testProvidedElementsInvalidElementSubset(): void {
|
||||
public function testProvidedElementsInvalidElementSubset(array $configured_subset, string $expected_exception_message): void {
|
||||
$this->enableModules(['ckeditor5_plugin_elements_subset']);
|
||||
|
||||
// Configure the sneaky superset plugin to have a random tag as the subset.
|
||||
// Configure the sneaky superset plugin.
|
||||
$sneaky_plugin_id = 'ckeditor5_plugin_elements_subset_sneakySuperset';
|
||||
$random_tag_name = strtolower($this->randomMachineName());
|
||||
$random_tag = "<$random_tag_name>";
|
||||
$text_editor = Editor::create([
|
||||
'format' => 'dummy',
|
||||
'editor' => 'ckeditor5',
|
||||
'settings' => [
|
||||
'plugins' => [
|
||||
$sneaky_plugin_id => ['configured_subset' => [$random_tag]],
|
||||
$sneaky_plugin_id => ['configured_subset' => $configured_subset],
|
||||
],
|
||||
],
|
||||
'image_upload' => [],
|
||||
]);
|
||||
|
||||
// Invalid subsets are allowed on unsaved Text Editor config entities,
|
||||
// because they may have invalid configuration.
|
||||
$text_editor->enforceIsNew(FALSE);
|
||||
|
||||
// No exception when getting all provided elements.
|
||||
$this->assertGreaterThan(0, count($this->manager->getProvidedElements()));
|
||||
|
||||
|
@ -1005,10 +1009,35 @@ PHP,
|
|||
// editor config entity is passed: only then can a subset be generated based
|
||||
// on configuration.
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessage("The \"ckeditor5_plugin_elements_subset_sneakySuperset\" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: \"$random_tag\".");
|
||||
$this->expectExceptionMessage($expected_exception_message);
|
||||
$this->manager->getProvidedElements([$sneaky_plugin_id], $text_editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider.
|
||||
*
|
||||
* @return array
|
||||
* Test scenarios.
|
||||
*/
|
||||
public function providerProvidedElementsInvalidElementSubset(): array {
|
||||
$random_tag_name = strtolower($this->randomMachineName());
|
||||
$random_tag = "<$random_tag_name>";
|
||||
return [
|
||||
'superset: random tag not listed in the plugin definition' => [
|
||||
[$random_tag],
|
||||
"The \"ckeditor5_plugin_elements_subset_sneakySuperset\" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: \"$random_tag\".",
|
||||
],
|
||||
'subset that omits the essential creatable tag' => [
|
||||
['<bar baz>'],
|
||||
'The "ckeditor5_plugin_elements_subset_sneakySuperset" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("<bar baz>") but the following tags can no longer be created: "<bar>".',
|
||||
],
|
||||
'subset that tries to leverage the `<$any-html5-element>` wildcard tag but picks a concrete tag that the wildcard tag does not resolve into' => [
|
||||
['<drupal-media class="sensational">'],
|
||||
'The "ckeditor5_plugin_elements_subset_sneakySuperset" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "<drupal-media class="sensational">".',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the enabling of plugins.
|
||||
*/
|
||||
|
|
|
@ -64,6 +64,9 @@ class ConfigurablePluginTest extends KernelTestBase {
|
|||
'heading6',
|
||||
],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [],
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [],
|
||||
],
|
||||
|
|
|
@ -283,6 +283,9 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'items' => [
|
||||
'Language',
|
||||
'Styles',
|
||||
// Blockquote does not have settings. It's present only to
|
||||
// support an additional tag, to test realistic styles.
|
||||
'Blockquote',
|
||||
],
|
||||
],
|
||||
[
|
||||
|
@ -299,7 +302,7 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'language_list' => 'all',
|
||||
],
|
||||
'stylescombo' => [
|
||||
'styles' => "p.callout|Callout\r\nblockquote.interesting|Interesting quote",
|
||||
'styles' => "p.callout|Callout\r\nblockquote.interesting.highlighted|Interesting & highlighted quote\n\nblockquote.famous | Famous\n",
|
||||
],
|
||||
// Plugin setting without upgrade path.
|
||||
'llama_contextual_and_button' => [
|
||||
|
@ -309,6 +312,42 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
],
|
||||
])->save();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'cke4_stylescombo_span',
|
||||
'name' => 'A CKEditor 4 configured to have span styles',
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => 1,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <span class="llama">',
|
||||
] + $filter_plugin_manager->getDefinition('filter_html')['settings'],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'cke4_stylescombo_span',
|
||||
'editor' => 'ckeditor',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'rows' => [
|
||||
0 => [
|
||||
[
|
||||
'name' => 'Whatever',
|
||||
'items' => [
|
||||
'Styles',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'stylescombo' => [
|
||||
'styles' => "span.llama|Llama span",
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'cke4_contrib_plugins_now_in_core',
|
||||
'name' => 'All CKEditor 4 contrib plugins now in core',
|
||||
|
@ -527,6 +566,22 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
],
|
||||
],
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => TRUE,
|
||||
],
|
||||
'ckeditor5_list' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => TRUE,
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [
|
||||
'<cite>',
|
||||
|
@ -545,22 +600,6 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'<h6 id>',
|
||||
],
|
||||
],
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
'heading3',
|
||||
'heading4',
|
||||
'heading5',
|
||||
'heading6',
|
||||
],
|
||||
],
|
||||
'ckeditor5_imageResize' => [
|
||||
'allow_resize' => TRUE,
|
||||
],
|
||||
'ckeditor5_list' => [
|
||||
'reversed' => FALSE,
|
||||
'startIndex' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'expected_superset' => '',
|
||||
|
@ -650,12 +689,6 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'expected_ckeditor5_settings' => [
|
||||
'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => array_values(array_diff(
|
||||
$basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
['<h4 id>', '<h6 id>'],
|
||||
)),
|
||||
],
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading2',
|
||||
|
@ -665,6 +698,12 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
],
|
||||
'ckeditor5_imageResize' => ['allow_resize' => TRUE],
|
||||
'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => array_values(array_diff(
|
||||
$basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
['<h4 id>', '<h6 id>'],
|
||||
)),
|
||||
],
|
||||
],
|
||||
],
|
||||
'expected_superset' => $basic_html_test_case['expected_superset'],
|
||||
|
@ -689,9 +728,6 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'expected_ckeditor5_settings' => [
|
||||
'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
],
|
||||
'ckeditor5_heading' => [
|
||||
'enabled_headings' => [
|
||||
'heading1',
|
||||
|
@ -704,6 +740,9 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
],
|
||||
'ckeditor5_imageResize' => ['allow_resize' => TRUE],
|
||||
'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'expected_superset' => $basic_html_test_case['expected_superset'],
|
||||
|
@ -733,14 +772,14 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
),
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_imageResize' => ['allow_resize' => TRUE],
|
||||
'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => array_values(array_diff(
|
||||
$basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
['<h2 id>', '<h3 id>', '<h4 id>', '<h5 id>', '<h6 id>'],
|
||||
)),
|
||||
],
|
||||
'ckeditor5_imageResize' => ['allow_resize' => TRUE],
|
||||
'ckeditor5_list' => ['reversed' => FALSE, 'startIndex' => TRUE],
|
||||
],
|
||||
],
|
||||
'expected_superset' => $basic_html_test_case['expected_superset'],
|
||||
|
@ -801,13 +840,12 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
),
|
||||
],
|
||||
'plugins' => array_merge(
|
||||
array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins'], 0, 1),
|
||||
$basic_html_test_case['expected_ckeditor5_settings']['plugins'],
|
||||
[
|
||||
'ckeditor5_alignment' => [
|
||||
'enabled_alignments' => ['center', 'justify'],
|
||||
],
|
||||
],
|
||||
array_slice($basic_html_test_case['expected_ckeditor5_settings']['plugins'], 1),
|
||||
),
|
||||
],
|
||||
'expected_superset' => implode(' ', [
|
||||
|
@ -941,6 +979,9 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'expected_ckeditor5_settings' => [
|
||||
'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
|
||||
'plugins' => [
|
||||
'ckeditor5_heading' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_heading'],
|
||||
'ckeditor5_imageResize' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_imageResize'],
|
||||
'ckeditor5_list' => $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_list'],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => array_merge(
|
||||
$basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
|
||||
|
@ -1190,12 +1231,30 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
'toolbar' => [
|
||||
'items' => [
|
||||
'textPartLanguage',
|
||||
'style',
|
||||
'blockQuote',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_language' => [
|
||||
'language_list' => 'all',
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Callout',
|
||||
'element' => '<p class="callout">',
|
||||
],
|
||||
[
|
||||
'label' => 'Interesting & highlighted quote',
|
||||
'element' => '<blockquote class="interesting highlighted">',
|
||||
],
|
||||
[
|
||||
'label' => 'Famous',
|
||||
'element' => '<blockquote class="famous">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'expected_superset' => '',
|
||||
|
@ -1214,6 +1273,46 @@ class SmartDefaultSettingsTest extends KernelTestBase {
|
|||
],
|
||||
];
|
||||
|
||||
yield "cke4_stylescombo_span can be switched to CKEditor 5 without problems, only <span> in Source Editing" => [
|
||||
'format_id' => 'cke4_stylescombo_span',
|
||||
'filters_to_drop' => [],
|
||||
'expected_ckeditor5_settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Llama span',
|
||||
'element' => '<span class="llama">',
|
||||
],
|
||||
],
|
||||
],
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [
|
||||
'<span>',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'expected_superset' => '',
|
||||
'expected_fundamental_compatibility_violations' => [],
|
||||
'expected_db_logs' => [
|
||||
'status' => [
|
||||
"The following tags were permitted by the <em class=\"placeholder\">A CKEditor 4 configured to have span styles</em> text format's filter configuration, but no plugin was available that supports them. To ensure the tags remain supported by this text format, the following were added to the Source Editing plugin's <em>Manually editable HTML tags</em>: <span>. The text format must be saved to make these changes active.",
|
||||
],
|
||||
],
|
||||
'expected_messages' => [
|
||||
'status' => [
|
||||
'To maintain the capabilities of this text format, <a target="_blank" href="/admin/help/ckeditor5#migration-settings">the CKEditor 5 migration</a> did the following: Added these tags/attributes to the Source Editing Plugin\'s <a target="_blank" href="/admin/help/ckeditor5#source-editing">Manually editable HTML tags</a> setting: <span>. Additional details are available in your logs.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
yield "cke4_contrib_plugins_now_in_core can be switched to CKEditor 5 without problems" => [
|
||||
'format_id' => 'cke4_contrib_plugins_now_in_core',
|
||||
'filters_to_drop' => [],
|
||||
|
|
|
@ -63,6 +63,9 @@ class ValidatorsTest extends KernelTestBase {
|
|||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\ToolbarItemConstraintValidator
|
||||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\ToolbarItemDependencyConstraintValidator
|
||||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\EnabledConfigurablePluginsConstraintValidator
|
||||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\CKEditor5ElementConstraintValidator
|
||||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
|
||||
* @covers \Drupal\ckeditor5\Plugin\Validation\Constraint\UniqueLabelInListConstraintValidator
|
||||
* @dataProvider provider
|
||||
*
|
||||
* @param array $ckeditor5_settings
|
||||
|
@ -328,6 +331,232 @@ class ValidatorsTest extends KernelTestBase {
|
|||
],
|
||||
'violations' => [],
|
||||
];
|
||||
$data['INVALID: Style plugin with no styles'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles' => 'Enable at least one style, otherwise disable the Style plugin.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin configured to add class to GHS-supported non-HTML5 tag'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
'sourceEditing',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_sourceEditing' => [
|
||||
'allowed_tags' => [
|
||||
'<foo>',
|
||||
],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Barry foo',
|
||||
'element' => '<foo class="bar">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles.0.element' => 'A style can only be specified for an HTML 5 tag. <code><foo></code> is not an HTML5 tag.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin configured to add class to plugin-supported non-HTML5 tag'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Sensational media',
|
||||
'element' => '<drupal-media class="sensational">',
|
||||
],
|
||||
],
|
||||
],
|
||||
'media_media' => [
|
||||
'allow_view_mode_override' => FALSE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles.0.element' => 'A style can only be specified for an HTML 5 tag. <code><drupal-media></code> is not an HTML5 tag.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin configured to add class that is supported by a disabled plugin'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Justified paragraph',
|
||||
'element' => '<p class="text-align-justify">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles.0.element' => 'A style must only specify classes not supported by other plugins. The <code>text-align-justify</code> classes on <code><p></code> are supported by the <em class="placeholder">Alignment</em> plugin. Remove this style and enable that plugin instead.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin configured to add class that is supported by an enabled plugin if its configuration were different'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
'alignment',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_alignment' => [
|
||||
'enabled_alignments' => ['center'],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Justified paragraph',
|
||||
'element' => '<p class="text-align-justify">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [],
|
||||
];
|
||||
$data['INVALID: Style plugin configured to add class that is supported by an enabled plugin'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
'alignment',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_alignment' => [
|
||||
'enabled_alignments' => ['justify'],
|
||||
],
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Justified paragraph',
|
||||
'element' => '<p class="text-align-justify">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles.0.element' => 'A style must only specify classes not supported by other plugins. The <code>text-align-justify</code> classes on <code><p></code> are already supported by the enabled <em class="placeholder">Alignment</em> plugin.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin has multiple styles with same label'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'blockQuote',
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
0 => [
|
||||
'label' => 'Highlighted',
|
||||
'element' => '<p class="highlighted">',
|
||||
],
|
||||
1 => [
|
||||
'label' => 'Highlighted',
|
||||
'element' => '<blockquote class="highlighted">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles' => 'The label <em class="placeholder">Highlighted</em> is not unique.',
|
||||
],
|
||||
];
|
||||
$data['INVALID: Style plugin has styles with invalid elements'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'blockQuote',
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
0 => [
|
||||
'label' => 'missing class attribute',
|
||||
'element' => '<p>',
|
||||
],
|
||||
1 => [
|
||||
'label' => 'class attribute present but no allowed values listed',
|
||||
'element' => '<blockquote class="">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style.styles.0.element' => 'The following tag is missing the required attribute <code>class</code>: <code><p></code>.',
|
||||
'settings.plugins.ckeditor5_style.styles.1.element' => 'The following tag does not have the minimum of 1 allowed values for the required attribute <code>class</code>: <code><blockquote class=""></code>.',
|
||||
],
|
||||
];
|
||||
$data['VALID: Style plugin has multiple styles with different labels'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'blockQuote',
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Callout',
|
||||
'element' => '<p class="callout">',
|
||||
],
|
||||
[
|
||||
'label' => 'Interesting & highlighted quote',
|
||||
'element' => '<blockquote class="interesting highlighted">',
|
||||
],
|
||||
[
|
||||
'label' => 'Famous',
|
||||
'element' => '<blockquote class="famous">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [],
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
@ -749,7 +978,8 @@ class ValidatorsTest extends KernelTestBase {
|
|||
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.1' => 'The following tag(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these tags: <em class="placeholder">Table (<table>)</em>.',
|
||||
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.3' => 'The following attribute(s) are already supported by enabled plugins and should not be added to the Source Editing "Manually editable HTML tags" field: <em class="placeholder">Language (<span lang>)</em>.',
|
||||
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.5' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Code Block (<code class="language-*">)</em>.',
|
||||
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.6' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Alignment (<h2 class="text-align-center">)</em>.',
|
||||
// @todo "Style" should be removed from the suggestions in https://www.drupal.org/project/drupal/issues/3271179
|
||||
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.6' => 'The following attribute(s) are already supported by available plugins and should not be added to the Source Editing "Manually editable HTML tags" field. Instead, enable the following plugins to support these attributes: <em class="placeholder">Style (<h2 class="text-align-center">), Alignment (<h2 class="text-align-center">)</em>.',
|
||||
],
|
||||
];
|
||||
$data['INVALID some invalid Source Editable tags provided by plugin and another available in a not enabled plugin'] = [
|
||||
|
@ -985,7 +1215,44 @@ class ValidatorsTest extends KernelTestBase {
|
|||
'filters' => [],
|
||||
'violations' => [],
|
||||
];
|
||||
|
||||
$data['INVALID: Style plugin configured to add class to unsupported tag'] = [
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'items' => [
|
||||
'style',
|
||||
],
|
||||
],
|
||||
'plugins' => [
|
||||
'ckeditor5_style' => [
|
||||
'styles' => [
|
||||
[
|
||||
'label' => 'Highlighted',
|
||||
'element' => '<blockquote class="highlighted">',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
],
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'id' => 'filter_html',
|
||||
'provider' => 'filter',
|
||||
'status' => TRUE,
|
||||
'weight' => 0,
|
||||
'settings' => [
|
||||
'allowed_html' => '<p> <br> <blockquote class="highlighted">',
|
||||
'filter_html_help' => TRUE,
|
||||
'filter_html_nofollow' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
'violations' => [
|
||||
'settings.plugins.ckeditor5_style' => 'The <em class="placeholder">Style</em> plugin needs another plugin to create <code><blockquote></code>, for it to be able to create the following attributes: <code><blockquote class="highlighted"></code>. Enable a plugin that supports creating this tag. If none exists, you can configure the Source Editing plugin to support it.',
|
||||
],
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
|
|
@ -134,4 +134,24 @@ JS;
|
|||
return $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects text inside an element.
|
||||
*
|
||||
* @param string $selector
|
||||
* A CSS selector for the element which contents should be selected.
|
||||
*/
|
||||
protected function selectTextInsideElement(string $selector): void {
|
||||
$javascript = <<<JS
|
||||
(function() {
|
||||
const el = document.querySelector(".ck-editor__main $selector");
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
})();
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\ckeditor5\Unit;
|
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style
|
||||
* @group ckeditor5
|
||||
* @internal
|
||||
*/
|
||||
class StylePluginTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Provides a list of configs to test.
|
||||
*/
|
||||
public function providerGetDynamicPluginConfig(): array {
|
||||
return [
|
||||
'default configuration (empty)' => [
|
||||
[
|
||||
'styles' => [],
|
||||
],
|
||||
[
|
||||
'style' => [
|
||||
'definitions' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Simple' => [
|
||||
[
|
||||
'styles' => [
|
||||
['label' => 'fancy blockquote', 'element' => '<blockquote class="fancy">'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'style' => [
|
||||
'definitions' => [
|
||||
[
|
||||
'name' => 'fancy blockquote',
|
||||
'element' => 'blockquote',
|
||||
'classes' => ['fancy'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'Complex' => [
|
||||
[
|
||||
'styles' => [
|
||||
['label' => 'fancy highlighted blockquote', 'element' => '<blockquote class="fancy highlighted">'],
|
||||
['label' => 'important foobar', 'element' => '<foobar class="important">'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'style' => [
|
||||
'definitions' => [
|
||||
[
|
||||
'name' => 'fancy highlighted blockquote',
|
||||
'element' => 'blockquote',
|
||||
'classes' => ['fancy', 'highlighted'],
|
||||
],
|
||||
[
|
||||
'name' => 'important foobar',
|
||||
'element' => 'foobar',
|
||||
'classes' => ['important'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getDynamicPluginConfig
|
||||
* @dataProvider providerGetDynamicPluginConfig
|
||||
*/
|
||||
public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void {
|
||||
$plugin = new Style($configuration, 'ckeditor5_style', NULL);
|
||||
$dynamic_plugin_config = $plugin->getDynamicPluginConfig([], $this->prophesize(EditorInterface::class)->reveal());
|
||||
$this->assertSame($expected_dynamic_config, $dynamic_plugin_config);
|
||||
}
|
||||
|
||||
}
|
|
@ -59,6 +59,7 @@
|
|||
"@ckeditor/ckeditor5-remove-format": "35.0.x",
|
||||
"@ckeditor/ckeditor5-source-editing": "35.0.x",
|
||||
"@ckeditor/ckeditor5-special-characters": "35.0.x",
|
||||
"@ckeditor/ckeditor5-style": "35.0.x",
|
||||
"@ckeditor/ckeditor5-table": "35.0.x",
|
||||
"@drupal/once": "1.0.x",
|
||||
"@popperjs/core": "2.11.x",
|
||||
|
|
|
@ -1137,6 +1137,13 @@
|
|||
dependencies:
|
||||
ckeditor5 "^35.0.1"
|
||||
|
||||
"@ckeditor/ckeditor5-style@35.0.x":
|
||||
version "35.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-35.0.1.tgz#1d321d3bef67ba07f8a329d0061b4782ed1aa542"
|
||||
integrity sha512-Z/GyXt0J+0ua+X2eIRN2dBZU42z60wgS3hLcijIpj8rAz+SnlRQhFJ5hgdz188jwAksoxPR94Vgs4oJU40t8ww==
|
||||
dependencies:
|
||||
ckeditor5 "^35.0.1"
|
||||
|
||||
"@ckeditor/ckeditor5-table@35.0.x":
|
||||
version "35.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-35.0.1.tgz#7a1a3c339cd3f74cf7024f15f772bd2c2a9877b5"
|
||||
|
|
Loading…
Reference in New Issue