diff --git a/djangocms_text_ckeditor5/__init__.py b/djangocms_text_ckeditor5/__init__.py
index ad6de06..59d76c4 100644
--- a/djangocms_text_ckeditor5/__init__.py
+++ b/djangocms_text_ckeditor5/__init__.py
@@ -7,7 +7,12 @@
ckeditor5 = RTEConfig(
name="ckeditor5",
config="CKEDITOR5",
- js=("djangocms_text/bundles/bundle.ckeditor5.min.js",),
- css={"all": ("djangocms_text/css/cms.ckeditor5.css",)},
+ js=("djangocms_text_ckeditor5/bundles/bundle.ckeditor5.min.js",),
+ css={
+ "all": (
+ "djangocms_text_ckeditor5/css/cms.ckeditor5.css",
+ "djangocms_text_ckeditor5/css/cms.linkfield.css",
+ )
+ },
inline_editing=True,
)
diff --git a/djangocms_text_ckeditor5/static/djangocms_text/css/cms.ckeditor5.css b/djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/css/cms.ckeditor5.css
similarity index 100%
rename from djangocms_text_ckeditor5/static/djangocms_text/css/cms.ckeditor5.css
rename to djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/css/cms.ckeditor5.css
diff --git a/djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/css/cms.linkfield.css b/djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/css/cms.linkfield.css
new file mode 100644
index 0000000..d875a67
--- /dev/null
+++ b/djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/css/cms.linkfield.css
@@ -0,0 +1,62 @@
+.ck.ck-balloon-panel {
+ .cms-linkfield-wrapper {
+ font-size: 0.8rem;
+ position: relative;
+ input[type="text"] {
+ padding-inline-end: 3em;
+ background: var(--dca-white) url('data:image/svg+xml;utf8, ') no-repeat inline-end center;
+ background-size: auto 1em;
+ }
+ .cms-linkfield-selected {
+ font-weight: bold;
+ }
+ .cms-linkfield-dropdown:not(:empty), .cms-linkfield-dropdown:active {
+ /* Hide dropdown when empty */
+ visibility: visible;
+ }
+ .cms-linkfield-dropdown {
+ z-index: 1;
+ visibility: hidden;
+ position: absolute;
+ max-block-size: 400px;
+ overflow: auto;
+ inset-inline-start: 0;
+ inset-block-start: 100%;
+ border: 1px solid var(--dca-gray-lighter);
+ background: var(--dca-white);
+ inline-size: 120%;
+ resize: both;
+ border-end-start-radius: 4px;
+ border-end-end-radius: 4px;
+ box-shadow: 0 1.5px 1.5px rgba(var(--dca-shadow),.4);
+ .cms-linkfield-error {
+ color: red;
+ font-size: 0.8rem;
+ }
+ div {
+ padding: 0.3rem 6px;
+ white-space: nowrap;
+ font-weight: normal;
+ border-block-end: 1px solid var(--dca-gray-lighter);
+ &:last-child {
+ border-bottom: none;
+ }
+ &.cms-linkfield-parent {
+ font-weight: bold;
+ background: var(--dca-gray-lightest);
+ }
+ &.cms-linkfield-message {
+ font-style: italic;
+ color: var(--dca-gray);
+ }
+ &.cms-linkfield-option {
+ cursor: pointer;
+ }
+ &.cms-linkfield-option:hover {
+ background: var(--dca-primary);
+ color: var(--dca-white);
+ }
+ }
+ }
+ }
+}
diff --git a/private/js/ckeditor5_plugins/cms-link/index.js b/private/js/ckeditor5_plugins/cms-link/index.js
index 47fbdbe..cfe56e5 100644
--- a/private/js/ckeditor5_plugins/cms-link/index.js
+++ b/private/js/ckeditor5_plugins/cms-link/index.js
@@ -2,6 +2,7 @@
/* jshint esversion: 6 */
// index.js
-import CMSLinkPlugin from './src/cmsLinkPlugin';
+import CmsLink from './src/cmsLink';
+import LinkSuggestionsEditing from './src/linksuggestionediting';
-export default CMSLinkPlugin;
+export { CmsLink, LinkSuggestionsEditing };
diff --git a/private/js/ckeditor5_plugins/cms-link/src/autocomplete.js b/private/js/ckeditor5_plugins/cms-link/src/autocomplete.js
deleted file mode 100644
index a3a5007..0000000
--- a/private/js/ckeditor5_plugins/cms-link/src/autocomplete.js
+++ /dev/null
@@ -1,162 +0,0 @@
-const $ = jQuery;
-
-/**
- * Override jQuery UI _renderItem function to output HTML by default.
- *
- * This uses function() syntax as required by jQuery UI.
- *
- * @param {object} ul
- * The
element that the newly created element must be appended to.
- * @param {object} item
- * The list item to append.
- *
- * @return {object}
- * jQuery collection of the ul element.
- */
-function renderItem(ul, item) {
- const $line = $(' ');
- const $wrapper = $(
- '',
- );
- $wrapper.append(
- `
${item.label} `,
- );
- if (item.hasOwnProperty('description')) {
- $wrapper.append(
- `
${item.description} `,
- );
- }
- return $line.append($wrapper).appendTo(ul);
-}
-
-/**
- * Override jQuery UI _renderMenu function to handle groups.
- *
- * This uses function() syntax as required by jQuery UI.
- *
- * @param {object} ul
- * An empty
element to use as the widget's menu.
- * @param {array} items
- * An Array of items that match the user typed term.
- */
-function renderMenu(ul, items) {
- const groupedItems = {};
- items.forEach((item) => {
- const group = item.hasOwnProperty('group') ? item.group : '';
- if (!groupedItems.hasOwnProperty(group)) {
- groupedItems[group] = [];
- }
- groupedItems[group].push(item);
- });
-
- Object.keys(groupedItems).forEach((groupLabel) => {
- const groupItems = groupedItems[groupLabel];
- if (groupLabel.length) {
- ul.append(
- ``,
- );
- }
- groupItems.forEach((item) => {
- this.element.autocomplete('instance')._renderItemData(ul, item);
- });
- });
-}
-
-export default function initializeAutocomplete(element, settings) {
- const {
- autocompleteUrl,
- selectHandler,
- closeHandler,
- openHandler,
- queryParams,
- } = settings;
- const autocomplete = {
- cache: {},
- ajax: {
- dataType: 'json',
- jsonp: false,
- },
- };
-
- /**
- * JQuery UI autocomplete source callback.
- *
- * @param {object} request
- * The request object.
- * @param {function} response
- * The function to call with the response.
- */
- function sourceData(request, response) {
- const { cache } = autocomplete;
- const { term } = request;
-
- /**
- * Transforms the data object into an array and update autocomplete results.
- *
- * @param {object} data
- * The data sent back from the server.
- */
- function sourceCallbackHandler(data) {
- cache[term] = data.suggestions;
- response(data.suggestions);
- }
-
- // Get the desired term and construct the autocomplete URL for it.
-
- // Check if the term is already cached.
- if (cache.hasOwnProperty(term)) {
- response(cache[term]);
- } else {
- const data = queryParams;
- data.q = term;
- $.ajax(autocompleteUrl, {
- success: sourceCallbackHandler,
- data,
- ...autocomplete.ajax,
- });
- }
- }
-
- const options = {
- appendTo: element.closest('.ck-labeled-field-view'),
- source: sourceData,
- select: selectHandler,
- focus: () => false,
- search: () => !options.isComposing,
- close: closeHandler,
- open: openHandler,
- minLength: 1,
- isComposing: false,
- };
- const $auto = $(element).autocomplete(options);
-
- // Override a few things.
- const instance = $auto.data('ui-autocomplete');
- instance
- .widget()
- .menu(
- 'option',
- 'items',
- '> :not(.entity-link-suggestions-result-line--group)',
- );
- instance._renderMenu = renderMenu;
- instance._renderItem = renderItem;
-
- $auto
- .autocomplete('widget')
- .addClass('ck-reset_all-excluded entity-link-suggestions-ui-autocomplete');
-
- $auto.on('click', () => {
- $auto.autocomplete('search', $auto[0].value);
- });
-
- // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
- $auto.on('compositionstart.autocomplete', () => {
- options.isComposing = true;
- });
- $auto.on('compositionend.autocomplete', () => {
- options.isComposing = false;
- });
-
- return $auto;
-}
diff --git a/private/js/ckeditor5_plugins/cms-link/src/cms.linkfield.js b/private/js/ckeditor5_plugins/cms-link/src/cms.linkfield.js
new file mode 100644
index 0000000..d4fbc47
--- /dev/null
+++ b/private/js/ckeditor5_plugins/cms-link/src/cms.linkfield.js
@@ -0,0 +1,188 @@
+/* eslint-env es11 */
+/* jshint esversion: 11 */
+/* global document, window, console */
+
+
+class LinkField {
+ constructor(element, options) {
+ console.log(element, options);
+ this.options = options;
+ this.urlElement = element;
+ this.form = element.closest("form");
+ this.selectElement = this.form.querySelector(`input[name="${this.urlElement.id + '_select'}"]`);
+ if (this.selectElement) {
+ this.urlElement.setAttribute('type', 'hidden'); // Two input types?
+ this.selectElement.setAttribute('type', 'hidden'); // Make hidden and add common input
+ this.prepareField();
+ this.registerEvents();
+ }
+ }
+
+ prepareField() {
+ this.inputElement = document.createElement('input');
+ this.inputElement.setAttribute('type', 'text');
+ this.inputElement.setAttribute('autocomplete', 'off');
+ this.inputElement.setAttribute('spellcheck', 'false');
+ this.inputElement.setAttribute('autocorrect', 'off');
+ this.inputElement.setAttribute('autocapitalize', 'off');
+ this.inputElement.setAttribute('placeholder', this.urlElement.getAttribute('placeholder') ||'');
+ this.inputElement.className = this.urlElement.className;
+ this.inputElement.classList.add('cms-linkfield-input');
+ if (this.selectElement.value) {
+ this.handleChange({target: this.selectElement});
+ this.inputElement.classList.add('cms-linkfield-selected');
+ } else if (this.urlElement.value) {
+ this.inputElement.value = this.urlElement.value;
+ this.inputElement.classList.remove('cms-linkfield-selected');
+ }
+ if (this.selectElement.getAttribute('data-value')) {
+ this.inputElement.value = this.selectElement.getAttribute('data-value');
+ this.inputElement.classList.add('cms-linkfield-selected');
+ }
+ if (this.selectElement.getAttribute('data-href')) {
+ this.urlElement.value = this.selectElement.getAttribute('data-href');
+ this.inputElement.classList.add('cms-linkfield-selected');
+ }
+ this.wrapper = document.createElement('div');
+ this.wrapper.classList.add('cms-linkfield-wrapper');
+ this.urlElement.insertAdjacentElement('afterend', this.wrapper);
+ this.urlElement.setAttribute('type', 'hidden');
+ this.dropdown = document.createElement('div');
+ this.dropdown.classList.add('cms-linkfield-dropdown');
+ if (this.form.style.zIndex) {
+ this.dropdown.style.zIndex = this.form.style.zIndex + 1;
+ }
+ this.wrapper.appendChild(this.inputElement);
+ this.wrapper.appendChild(this.dropdown);
+ }
+
+ registerEvents() {
+ this.inputElement.addEventListener('input', this.handleInput.bind(this));
+ this.inputElement.addEventListener('focus', event => this.search());
+ // this.inputElement.addEventListener('blur', event => {
+ // setTimeout(() => { this.dropdown.style.visibility = 'hidden'; }, 200);
+ // });
+ this.urlElement.addEventListener('input', event => {
+ this.inputElement.value = event.target.value || '';
+ this.inputElement.classList.remove('cms-linkfield-selected');
+ // this.selectElement.value = '';
+ });
+ this.selectElement.addEventListener('input', event => this.handleChange(event));
+ this.intersection = new IntersectionObserver((entries, observer) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ this.updateSearch();
+ observer.disconnect();
+ }
+ });
+ });
+ }
+
+ handleInput(event) {
+ // User typed something into the field -> no predefined value selected
+ this.selectElement.value = '';
+ this.urlElement.value = this.inputElement.value;
+ this.inputElement.classList.remove('cms-linkfield-selected');
+ this.search();
+ }
+
+ showResults(response, page = 1) {
+ var currentSection; // Keep track of the current section so that paginated data can be added
+ if (page === 1) {
+ // First page clears the dropdown
+ this.dropdown.innerHTML = '';
+ currentSection = '';
+ } else {
+ // Remove the more link
+ const more = this.dropdown.querySelector('.cms-linkfield-more');
+ currentSection = more?.dataset.group;
+ more?.remove();
+ }
+ response.results.forEach(result => currentSection = this._addResult(result, currentSection));
+ if (response?.pagination?.more) {
+ const more = document.createElement('div');
+ more.classList.add('cms-linkfield-more');
+ more.setAttribute('data-page', page + 1);
+ more.setAttribute('data-group', currentSection);
+ more.textContent = '...';
+ this.dropdown.appendChild(more);
+ this.intersection.observe(more);
+ }
+ }
+
+ _addResult(result, currentSection = '') {
+ const item = document.createElement('div');
+ item.textContent = result.text;
+ if (result.id) {
+ item.classList.add('cms-linkfield-option');
+ item.setAttribute('data-value', result.id);
+ item.setAttribute('data-href', result.url);
+ item.setAttribute('data-text', result.verbose || result.text);
+ item.addEventListener('click', this.handleSelection.bind(this));
+ }
+ if (result.children && result.children.length > 0) {
+ item.classList.add('cms-linkfield-parent');
+ if (result.text !== currentSection) {
+ this.dropdown.appendChild(item);
+ currentSection = result.text;
+ }
+ result.children.forEach(child => {
+ this._addResult(child);
+ });
+ } else {
+ this.dropdown.appendChild(item);
+ }
+ return currentSection;
+ }
+
+ handleSelection(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.inputElement.value = event.target.getAttribute('data-text') || event.target.textContent;
+ this.inputElement.classList.add('cms-linkfield-selected');
+ this.urlElement.value = event.target.getAttribute('data-href');
+ this.selectElement.value = event.target.getAttribute('data-value');
+ this.inputElement.blur();
+ this.dropdown.innerHTML = ''; // CSS hides dropdown when empty
+ }
+
+ handleChange(event) {
+ if (this.selectElement.value) {
+ fetch(this.options.url + '?g=' + encodeURIComponent(this.selectElement.value))
+ .then(response => response.json())
+ .then(data => {
+ this.inputElement.value = data.text;
+ this.inputElement.classList.add('cms-linkfield-selected');
+ this.urlElement.value = data.url;
+ });
+ }
+ }
+
+ search(page = 1) {
+ const searchText = this.inputElement.value.toLowerCase();
+ this.fetchData(searchText, page).then(response => {
+ this.showResults(response, page);
+ }).catch (error => {
+ this.dropdown.innerHTML = `${error.message}
`;
+ });
+ }
+
+ updateSearch() {
+ const more = this.dropdown.querySelector('.cms-linkfield-more');
+ if (more) {
+ this.search(parseInt(more.getAttribute('data-page')));
+ }
+ }
+
+ fetchData(searchText, page ) {
+ if (this.options.url) {
+ return fetch(this.options.url + `?q=${encodeURIComponent(searchText)}${page > 1 ? '&page=' + page : ''}`)
+ .then(response => response.json());
+ }
+ return new Promise(resolve => {
+ resolve({results: []});
+ });
+ }
+}
+
+export default LinkField;
diff --git a/private/js/ckeditor5_plugins/cms-link/src/cmsLink.js b/private/js/ckeditor5_plugins/cms-link/src/cmsLink.js
index 8658af3..aec8770 100644
--- a/private/js/ckeditor5_plugins/cms-link/src/cmsLink.js
+++ b/private/js/ckeditor5_plugins/cms-link/src/cmsLink.js
@@ -1,12 +1,13 @@
-/* eslint-env es8 */
-/* jshint esversion: 8 */
+/* eslint-env es11 */
+/* jshint esversion: 11 */
import {Plugin} from 'ckeditor5/src/core';
import {SwitchButtonView, View, ViewCollection} from 'ckeditor5/src/ui';
import LinkSuggestionsEditing from './linksuggestionediting';
-import initializeAutocomplete from './autocomplete';
+import LinkField from "./cms.linkfield";
-export default class CmsLinkPlugin extends Plugin {
+
+export default class CmsLink extends Plugin {
/**
* @inheritdoc
*/
@@ -19,213 +20,34 @@ export default class CmsLinkPlugin extends Plugin {
// TRICKY: Work-around until the CKEditor team offers a better solution: force the ContextualBalloon to get instantiated early thanks to imageBlock not yet being optimized like https://github.com/ckeditor/ckeditor5/commit/c276c45a934e4ad7c2a8ccd0bd9a01f6442d4cd3#diff-1753317a1a0b947ca8b66581b533616a5309f6d4236a527b9d21ba03e13a78d8.
editor.plugins.get('LinkUI')._createViews();
- this._buttonViews = new ViewCollection();
-
this._enableLinkAutocomplete();
this._handleExtraFormFieldSubmit();
this._handleDataLoadingIntoExtraFormField();
- this._handleEntityLinkPreviews();
- }
-
- _handleEntityLinkPreviews() {
- const {editor} = this;
- const linkActionsView = editor.plugins.get('LinkUI').actionsView;
- const previewButton = linkActionsView.previewButtonView;
- previewButton.set('parentHref');
-
- previewButton
- .bind('parentHref')
- .to(linkActionsView, 'href', this, 'entityMetadata', (value) => value);
- previewButton.unbind('isEnabled');
- previewButton
- .bind('isEnabled')
- .to(
- linkActionsView,
- 'href',
- (href) => !!href && !href.startsWith('entity:'),
- );
- previewButton.unbind('label');
-
- const bind = previewButton.bindTemplate;
-
- previewButton.setTemplate({
- tag: 'a',
- attributes: {
- href: bind.to('parentHref', (hrefValue, another) => {
- const {selection} = this.editor.model.document;
- // If the active selection is image or media, the link metadata is
- // stored in the drupalLinkEntityMetadata property.
- if (
- selection.getSelectedElement() &&
- ['imageBlock', 'drupalMedia'].includes(
- selection.getSelectedElement().name,
- ) &&
- selection
- .getSelectedElement()
- .hasAttribute('drupalLinkEntityMetadata')
- ) {
- const entityMetadata = JSON.parse(
- selection
- .getSelectedElement()
- .getAttribute('drupalLinkEntityMetadata'),
- );
- if (entityMetadata.path) {
- return `${
- drupalSettings.path.baseUrl
- }${entityMetadata.path.replace('entity:', '')}`;
- }
- } else if (selection.hasAttribute('data-entity-metadata')) {
- // If the active selection is the link itself, the metadata is
- // available in its data-entity-metadata attribute.
- const entityMetadata = JSON.parse(
- selection.getAttribute('data-entity-metadata'),
- );
- if (entityMetadata.path) {
- return `${
- drupalSettings.path.baseUrl
- }${entityMetadata.path.replace('entity:', '')}`;
- }
- }
-
- // If path is not available via metadata, use the hrefValue directly.
- if (hrefValue && hrefValue.startsWith('entity:')) {
- return `${drupalSettings.path.baseUrl}/${hrefValue.replace(
- 'entity:',
- '',
- )}`;
- }
-
- return hrefValue;
- }),
- target: '_blank',
- class: ['ck', 'ck-link-actions__preview'],
- 'aria-labelledby': 'ck-aria-label-preview-button',
- },
- children: [
- {
- tag: 'span',
- attributes: {
- class: ['ck', 'ck-button'],
- id: 'ck-aria-label-preview-button',
- },
- children: [
- {
- text: bind.to('parentHref', (parentHref) => {
- const {selection} = this.editor.model.document;
-
- let entityMetadata = {};
- if (
- selection.getSelectedElement() &&
- ['imageBlock', 'drupalMedia'].includes(
- selection.getSelectedElement().name,
- ) &&
- selection
- .getSelectedElement()
- .hasAttribute('drupalLinkEntityMetadata')
- ) {
- entityMetadata = JSON.parse(
- selection
- .getSelectedElement()
- .getAttribute('drupalLinkEntityMetadata'),
- );
- } else if (selection.hasAttribute('data-entity-metadata')) {
- entityMetadata = JSON.parse(
- selection.getAttribute('data-entity-metadata'),
- );
- }
-
- if (
- entityMetadata.label &&
- (!parentHref ||
- parentHref.startsWith('entity:') ||
- entityMetadata.path.startsWith('entity:'))
- ) {
- const group = entityMetadata.group
- ? ` (${entityMetadata.group})`
- : '';
- const element = document.createElement('div');
- element.innerHTML = entityMetadata.label;
- return `${element.textContent}${group.replace(' - )', ')')}`;
- }
- return parentHref;
- }),
- },
- ],
- },
- ],
- });
- }
-
- _createExtraButtonView(modelName, options) {
- const {editor} = this;
- const linkFormView = editor.plugins.get('LinkUI').formView;
-
- const buttonView = new SwitchButtonView(editor.locale);
- buttonView.set({
- name: modelName,
- label: options.label,
- withText: true,
- });
- buttonView.on('execute', () => {
- this.set(modelName, !buttonView.isOn);
- });
- this.on(`change:${modelName}`, (evt, propertyName, newValue) => {
- buttonView.isOn = newValue === true;
- buttonView.isVisible = typeof newValue === 'boolean';
- });
-
- linkFormView.on('render', () => {
- linkFormView._focusables.add(buttonView, 1);
- linkFormView.focusTracker.add(buttonView.element);
- });
-
- this._buttonViews.add(buttonView);
- linkFormView[modelName] = buttonView;
}
_enableLinkAutocomplete() {
const {editor} = this;
- const hostEntityTypeId = editor.sourceElement.getAttribute(
- 'data-ckeditor5-host-entity-type',
- );
- const hostEntityLangcode = editor.sourceElement.getAttribute(
- 'data-ckeditor5-host-entity-langcode',
- );
const linkFormView = editor.plugins.get('LinkUI').formView;
const linkActionsView = editor.plugins.get('LinkUI').actionsView;
let wasAutocompleteAdded = false;
- linkFormView.extendTemplate({
- attributes: {
- class: ['ck-vertical-form', 'ck-link-form_layout-vertical'],
- },
- });
-
- const additionalButtonsView = new View();
- additionalButtonsView.setTemplate({
- tag: 'ul',
- children: this._buttonViews.map((buttonView) => ({
- tag: 'li',
- children: [buttonView],
- attributes: {
- class: ['ck', 'ck-list__item'],
- },
- })),
- attributes: {
- class: ['ck', 'ck-reset', 'ck-list'],
- },
- });
- linkFormView.children.add(additionalButtonsView, 1);
-
editor.plugins
.get('ContextualBalloon')
.on('set:visibleView', (evt, propertyName, newValue) => {
- if (newValue === linkActionsView && this.entityMetadata) {
- linkActionsView.set('metadata', this.entityMetadata);
- }
-
- if (newValue !== linkFormView || wasAutocompleteAdded) {
+ const selection = editor.model.document.selection;
+ const cmsHref = selection.getAttribute('cmsHref');
+
+ if (newValue === linkActionsView) {
+ // Add the link target name of a cms link into the action view
+ if(cmsHref && editor.config.get('url_endpoint')) {
+ fetch(editor.config.get('url_endpoint') + '?g=' + encodeURIComponent(cmsHref))
+ .then(response => response.json())
+ .then(data => {
+ const button = linkActionsView.previewButtonView.element;
+ button.firstElementChild.textContent = data.text;
+ });
+ }
return;
}
@@ -234,56 +56,22 @@ export default class CmsLinkPlugin extends Plugin {
*
* @type {boolean}
*/
- let selected;
-
- initializeAutocomplete(linkFormView.urlInputView.fieldView.element, {
- // @see \Drupal\ckeditor5\Plugin\CKEditor5Plugin\EntityLinkSuggestions::getDynamicPluginConfig()
- autocompleteUrl: this.editor.config.get('drupalEntityLinkSuggestions')
- .suggestionsUrl,
- queryParams: {
- hostEntityLangcode,
- hostEntityTypeId,
- },
- selectHandler: (event, {item}) => {
- if (!item.path && !item.href) {
- // eslint-disable-next-line no-throw-literal
- throw `Missing path or href param. ${JSON.stringify(item)}`;
- }
-
- if (item.entity_type_id || item.entity_uuid) {
- if (!item.entity_type_id || !item.entity_uuid) {
- // eslint-disable-next-line no-throw-literal
- throw `Missing entity type id and/or entity uuid. ${JSON.stringify(
- item,
- )}`;
- }
- this.set('entityType', item.entity_type_id);
- this.set('entityUuid', item.entity_uuid);
- this.set('entityMetadata', JSON.stringify(item));
- } else {
- this.set('entityType', null);
- this.set('entityUuid', null);
- this.set('entityMetadata', null);
- }
-
- event.target.value = item.path ?? item.href;
- selected = true;
- return false;
- },
- openHandler: () => {
- selected = false;
- },
- closeHandler: () => {
- if (!selected) {
- this.set('entityType', null);
- this.set('entityUuid', null);
- this.set('entityMetadata', null);
- }
- selected = false;
- },
+ if (wasAutocompleteAdded) {
+ return;
+ }
+ const hiddenInput = document.createElement('input');
+ hiddenInput.setAttribute('type', 'hidden');
+ hiddenInput.setAttribute('name', linkFormView.urlInputView.fieldView.element.id + '_select');
+ hiddenInput.value = cmsHref || '';
+ linkFormView.urlInputView.fieldView.element.parentNode.insertBefore(
+ hiddenInput,
+ linkFormView.urlInputView.fieldView.element
+ );
+ linkFormView.urlInputView.fieldView.element.parentNode.querySelector('label')?.remove();
+ new LinkField(linkFormView.urlInputView.fieldView.element, {
+ url: editor.config.get('url_endpoint') || ''
});
-
wasAutocompleteAdded = true;
});
}
@@ -297,13 +85,11 @@ export default class CmsLinkPlugin extends Plugin {
linkFormView,
'submit',
() => {
+
+ const id = linkFormView.urlInputView.fieldView.element.id + '_select';
+ const selectElement = linkFormView.urlInputView.fieldView.element.closest('form').querySelector(`input[name="${id}"]`);
const values = {
- 'data-entity-type': this.entityType,
- 'data-entity-uuid': this.entityUuid,
- 'data-entity-metadata': this.entityMetadata,
- 'data-link-entity-type': this.entityType,
- 'data-link-entity-uuid': this.entityUuid,
- 'data-link-entity-metadata': this.entityMetadata,
+ 'cmsHref': selectElement.value,
};
// Stop the execution of the link command caused by closing the form.
// Inject the extra attribute value. The highest priority listener here
@@ -335,9 +121,7 @@ export default class CmsLinkPlugin extends Plugin {
const {editor} = this;
const linkCommand = editor.commands.get('link');
- this.bind('entityType').to(linkCommand, 'data-entity-type');
- this.bind('entityUuid').to(linkCommand, 'data-entity-uuid');
- this.bind('entityMetadata').to(linkCommand, 'data-entity-metadata');
+ this.bind('cmsHref').to(linkCommand, 'cmsHref');
}
/**
diff --git a/private/js/ckeditor5_plugins/cms-link/src/linksuggestionediting.js b/private/js/ckeditor5_plugins/cms-link/src/linksuggestionediting.js
index 4d690f1..a014b53 100644
--- a/private/js/ckeditor5_plugins/cms-link/src/linksuggestionediting.js
+++ b/private/js/ckeditor5_plugins/cms-link/src/linksuggestionediting.js
@@ -1,253 +1,211 @@
-/* eslint-disable import/no-extraneous-dependencies */
-import { Plugin } from 'ckeditor5/src/core';
-import { findAttributeRange } from 'ckeditor5/src/typing';
-
-export default class DrupalEntityLinkSuggestionsEditing extends Plugin {
- init() {
- this.attrs = [
- 'data-entity-type',
- 'data-entity-uuid',
- 'data-entity-metadata',
- ];
- this.blockLinkAttrs = [
- 'data-link-entity-type',
- 'data-link-entity-uuid',
- 'data-link-entity-metadata',
- ];
- this.blockLinkAttrsToModel = {
- 'data-link-entity-type': 'drupalLinkEntityType',
- 'data-link-entity-uuid': 'drupalLinkEntityUuid',
- 'data-link-entity-metadata': 'drupalLinkEntityMetadata',
- };
- this._allowAndConvertExtraAttributes();
- this._removeExtraAttributesOnUnlinkCommandExecute();
- this._refreshExtraAttributeValues();
- this._addExtraAttributesOnLinkCommandExecute();
- }
-
- _allowAndConvertExtraAttributes() {
- const { editor } = this;
- editor.model.schema.extend('$text', { allowAttributes: this.attrs });
-
- this.attrs.forEach((attribute) => {
- editor.conversion.for('downcast').attributeToElement({
- model: attribute,
- view: (value, { writer }) => {
- const viewAttributes = {};
- viewAttributes[attribute] = value;
- const linkViewElement = writer.createAttributeElement(
- 'a',
- viewAttributes,
- { priority: 5 },
- );
-
- // Without it the isLinkElement() will not recognize the link and the UI will not show up
- // when the user clicks a link.
- writer.setCustomProperty('link', true, linkViewElement);
-
- return linkViewElement;
- },
- });
-
- editor.conversion.for('upcast').elementToAttribute({
- view: {
- name: 'a',
- attributes: {
- [attribute]: true,
- },
- },
- model: {
- key: attribute,
- value: (viewElement) => viewElement.getAttribute(attribute),
- },
- });
- });
- }
-
- _addExtraAttributesOnLinkCommandExecute() {
- const { editor } = this;
- const linkCommand = editor.commands.get('link');
- let linkCommandExecuting = false;
-
- linkCommand.on(
- 'execute',
- (evt, args) => {
- // Custom handling is only required if an extra attribute was passed into
- // editor.execute( 'link', ... ).
- if (args.length < 3) {
- return;
- }
- if (linkCommandExecuting) {
- linkCommandExecuting = false;
- return;
- }
-
- // If the additional attribute was passed, we stop the default execution
- // of the LinkCommand. We're going to create Model#change() block for undo
- // and execute the LinkCommand together with setting the extra attribute.
- evt.stop();
-
- // Prevent infinite recursion by keeping records of when link command is
- // being executed by this function.
- linkCommandExecuting = true;
- const extraAttributeValues = args[args.length - 1];
- const { model } = editor;
- const { selection } = model.document;
-
- // Wrapping the original command execution in a model.change() block to
- // ensure there is a single undo step when the extra attribute is added.
- model.change((writer) => {
- editor.execute('link', ...args);
-
- const firstPosition = selection.getFirstPosition();
- this.attrs.forEach((attribute) => {
- if (selection.isCollapsed) {
- const node = firstPosition.textNode || firstPosition.nodeBefore;
- if (extraAttributeValues[attribute]) {
- writer.setAttribute(
- attribute,
- extraAttributeValues[attribute],
- writer.createRangeOn(node),
+/* eslint-env es6 */
+/* jshint esversion: 6 */
+
+import {Plugin} from 'ckeditor5/src/core';
+import {findAttributeRange} from 'ckeditor5/src/typing';
+
+export default class LinkSuggestionsEditing extends Plugin {
+ init() {
+ const editor = this.editor;
+ const linkCommand = editor.commands.get('link');
+ const unlinkCommand = editor.commands.get('unlink');
+
+ this._allowAndConvertExtraAttributes();
+ this._removeExtraAttributesOnUnlinkCommandExecute();
+ this._refreshExtraAttributeValues();
+ this._addExtraAttributesOnLinkCommandExecute();
+
+ }
+
+ _allowAndConvertExtraAttributes() {
+ const {editor} = this;
+ editor.model.schema.extend('$text', {allowAttributes: 'cmsHref'});
+
+ editor.conversion.for('downcast').attributeToElement({
+ model: 'cmsHref',
+ view: (value, {writer}) => {
+ const viewAttributes = {};
+ viewAttributes['data-cms-href'] = value;
+ const linkViewElement = writer.createAttributeElement(
+ 'a',
+ viewAttributes,
+ {priority: 5},
);
- } else {
- writer.removeAttribute(attribute, writer.createRangeOn(node));
- }
-
- writer.removeSelectionAttribute(attribute);
- } else {
- const ranges = model.schema.getValidRanges(
- selection.getRanges(),
- attribute,
- );
-
- // eslint-disable-next-line no-restricted-syntax
- for (const range of ranges) {
- if (extraAttributeValues[attribute]) {
- writer.setAttribute(
- attribute,
- extraAttributeValues[attribute],
- range,
- );
- } else {
- writer.removeAttribute(attribute, range);
- }
- }
- }
- });
- if (
- selection.getSelectedElement() &&
- ['imageBlock', 'drupalMedia'].includes(
- selection.getSelectedElement().name,
- )
- ) {
- const selectedElement = selection.getSelectedElement();
-
- this.blockLinkAttrs.forEach((attribute) => {
- if (extraAttributeValues[attribute]) {
- writer.setAttribute(
- this.blockLinkAttrsToModel[attribute],
- extraAttributeValues[attribute],
- selectedElement,
- );
- } else {
- writer.removeAttribute(
- this.blockLinkAttrsToModel[attribute],
- selectedElement,
- );
- }
- });
- }
+
+ // Without it the isLinkElement() will not recognize the link and the UI will not show up
+ // when the user clicks a link.
+ writer.setCustomProperty('link', true, linkViewElement);
+
+ return linkViewElement;
+ },
});
- },
- { priority: 'high' },
- );
- }
-
- _removeExtraAttributesOnUnlinkCommandExecute() {
- const { editor } = this;
- const unlinkCommand = editor.commands.get('unlink');
- const { model } = editor;
- const { selection } = model.document;
-
- let isUnlinkingInProgress = false;
-
- // Make sure all changes are in a single undo step so cancel the original unlink first in the high priority.
- unlinkCommand.on(
- 'execute',
- (evt) => {
- if (isUnlinkingInProgress) {
- return;
- }
-
- evt.stop();
-
- // This single block wraps all changes that should be in a single undo step.
- model.change(() => {
- // Now, in this single "undo block" let the unlink command flow naturally.
- isUnlinkingInProgress = true;
-
- // Do the unlinking within a single undo step.
- editor.execute('unlink');
-
- // Let's make sure the next unlinking will also be handled.
- isUnlinkingInProgress = false;
-
- // The actual integration that removes the extra attribute.
- model.change((writer) => {
- // Get ranges to unlink.
- let ranges;
-
- this.attrs.forEach((attribute) => {
- if (selection.isCollapsed) {
- ranges = [
- findAttributeRange(
- selection.getFirstPosition(),
- attribute,
- selection.getAttribute(attribute),
- model,
- ),
- ];
- } else {
- ranges = model.schema.getValidRanges(
- selection.getRanges(),
- attribute,
- );
- }
-
- // Remove the extra attribute from specified ranges.
- // eslint-disable-next-line no-restricted-syntax
- for (const range of ranges) {
- writer.removeAttribute(attribute, range);
- }
- });
- });
+
+ editor.conversion.for('upcast').elementToAttribute({
+ view: {
+ name: 'a',
+ attributes: {
+ 'data-cms-href': true
+ },
+ },
+ model: {
+ key: 'cmsHref',
+ value: (viewElement) => viewElement.getAttribute('data-cms-href'),
+ },
+ });
+ }
+
+ _addExtraAttributesOnLinkCommandExecute() {
+ const {editor} = this;
+ const linkCommand = editor.commands.get('link');
+ let linkCommandExecuting = false;
+
+ linkCommand.on(
+ 'execute',
+ (evt, args) => {
+ // Custom handling is only required if an extra attribute was passed into
+ // editor.execute( 'link', ... ).
+ if (args.length < 3) {
+ return;
+ }
+ if (linkCommandExecuting) {
+ linkCommandExecuting = false;
+ return;
+ }
+
+ // If the additional attribute was passed, we stop the default execution
+ // of the LinkCommand. We're going to create Model#change() block for undo
+ // and execute the LinkCommand together with setting the extra attribute.
+ evt.stop();
+
+ // Prevent infinite recursion by keeping records of when link command is
+ // being executed by this function.
+ linkCommandExecuting = true;
+ const extraAttributeValues = args[args.length - 1];
+ const {model} = editor;
+ const {selection} = model.document;
+
+ // Wrapping the original command execution in a model.change() block to
+ // ensure there is a single undo step when the extra attribute is added.
+ model.change((writer) => {
+ editor.execute('link', ...args);
+
+ const firstPosition = selection.getFirstPosition();
+ if (selection.isCollapsed) {
+ const node = firstPosition.textNode || firstPosition.nodeBefore;
+ if (extraAttributeValues['cmsHref']) {
+ writer.setAttribute(
+ 'cmsHref',
+ extraAttributeValues['cmsHref'],
+ writer.createRangeOn(node),
+ );
+ } else {
+ writer.removeAttribute('cmsHref', writer.createRangeOn(node));
+ }
+
+ writer.removeSelectionAttribute('cmsHref');
+ } else {
+ const ranges = model.schema.getValidRanges(
+ selection.getRanges(),
+ 'cmsHref',
+ );
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const range of ranges) {
+ if (extraAttributeValues['cmsHref']) {
+ writer.setAttribute(
+ 'cmsHref',
+ extraAttributeValues['cmsHref'],
+ range,
+ );
+ } else {
+ writer.removeAttribute('cmsHref', range);
+ }
+ }
+ }
+ });
+ },
+ {priority: 'high'},
+ );
+ }
+
+ _removeExtraAttributesOnUnlinkCommandExecute() {
+ const {editor} = this;
+ const unlinkCommand = editor.commands.get('unlink');
+ const {model} = editor;
+ const {selection} = model.document;
+
+ let isUnlinkingInProgress = false;
+
+ // Make sure all changes are in a single undo step so cancel the original unlink first in the high priority.
+ unlinkCommand.on(
+ 'execute',
+ (evt) => {
+ if (isUnlinkingInProgress) {
+ return;
+ }
+
+ evt.stop();
+
+ // This single block wraps all changes that should be in a single undo step.
+ model.change(() => {
+ // Now, in this single "undo block" let the unlink command flow naturally.
+ isUnlinkingInProgress = true;
+
+ // Do the unlinking within a single undo step.
+ editor.execute('unlink');
+
+ // Let's make sure the next unlinking will also be handled.
+ isUnlinkingInProgress = false;
+
+ // The actual integration that removes the extra attribute.
+ model.change((writer) => {
+ // Get ranges to unlink.
+ let ranges;
+
+ if (selection.isCollapsed) {
+ ranges = [
+ findAttributeRange(
+ selection.getFirstPosition(),
+ 'cmsHref',
+ selection.getAttribute('cmsHref'),
+ model,
+ ),
+ ];
+ } else {
+ ranges = model.schema.getValidRanges(
+ selection.getRanges(),
+ 'cmsHref',
+ );
+ }
+
+ // Remove the extra attribute from specified ranges.
+ // eslint-disable-next-line no-restricted-syntax
+ for (const range of ranges) {
+ writer.removeAttribute('cmsHref', range);
+ }
+ });
+ });
+ },
+ {priority: 'high'},
+ );
+ }
+
+ _refreshExtraAttributeValues() {
+ const {editor} = this;
+ const linkCommand = editor.commands.get('link');
+ const {model} = editor;
+ const {selection} = model.document;
+
+ linkCommand.set('cmsHref', null);
+ model.document.on('change', () => {
+ linkCommand['cmsHref'] = selection.getAttribute('data-cms-href');
});
- },
- { priority: 'high' },
- );
- }
-
- _refreshExtraAttributeValues() {
- const { editor } = this;
- const attributes = this.attrs;
- const linkCommand = editor.commands.get('link');
- const { model } = editor;
- const { selection } = model.document;
-
- attributes.forEach((attribute) => {
- linkCommand.set(attribute, null);
- });
- model.document.on('change', () => {
- attributes.forEach((attribute) => {
- linkCommand[attribute] = selection.getAttribute(attribute);
- });
- });
- }
-
- /**
- * @inheritdoc
- */
- static get pluginName() {
- return 'DrupalEntityLinkSuggestionsEditing';
- }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ static get pluginName() {
+ return 'LinkSuggestionsEditing';
+ }
}
diff --git a/private/js/cms.ckeditor5.js b/private/js/cms.ckeditor5.js
index f6a332a..e9ca09a 100644
--- a/private/js/cms.ckeditor5.js
+++ b/private/js/cms.ckeditor5.js
@@ -50,7 +50,7 @@ import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalli
//import UserStyle from './ckeditor5-user-style/src/userstyle';
import CmsPlugin from './ckeditor5_plugins/cms.plugin';
-//import { CmsLink, LinkSuggestionsEditing } from "./ckeditor5_plugins/cms-link";
+import { CmsLink, LinkSuggestionsEditing } from "./ckeditor5_plugins/cms-link";
class ClassicEditor extends ClassicEditorBase {}
class BalloonEditor extends BalloonEditorBase {}
@@ -84,9 +84,9 @@ var builtinPlugins = [
// ImageToolbar,
// ImageUpload,
Indent,
- //CmsLink,
Link,
- //LinkSuggestionsEditing,
+ CmsLink,
+ LinkSuggestionsEditing,
List,
MediaEmbed,
Paragraph,
@@ -99,7 +99,7 @@ var builtinPlugins = [
TableToolbar,
TextTransformation,
// UserStyle,
- // CmsPlugin
+ CmsPlugin
];
ClassicEditor.builtinPlugins = builtinPlugins;
@@ -145,8 +145,7 @@ var defaultConfig = {
};
ClassicEditor.defaultConfig = Object.assign({}, defaultConfig);
-ClassicEditor.defaultConfig.toolbar.items.push('|', 'sourceEditing');
-// InlineEditor.defaultConfig = defaultConfig;
+ClassicEditor.defaultConfig.toolbar.items.push('|', 'SourceEditing');
BalloonEditor.defaultConfig = {
heading: defaultConfig.heading,
table: defaultConfig.table,
@@ -182,7 +181,7 @@ class CmsCKEditor5Plugin {
this._CSS = [];
this._pluginNames = {
Table: 'insertTable',
- Source: 'sourceEditing',
+ Source: 'SourceEditing',
HorizontalRule: 'horizontalLine',
JustifyLeft: 'Alignment',
Strike: 'Strikethrough',
@@ -291,7 +290,7 @@ class CmsCKEditor5Plugin {
addingToBlock = true;
item = '|';
}
- } else if (item === 'ShowBlocks' && inline || item === 'Source' && inline) {
+ } else if (inline && ['ShowBlocks', 'Source', 'sourceEditing'].includes(item)) {
// No source editing or show blocks in inline editor
continue;
} else if (this._pluginNames[item] !== undefined) {
diff --git a/webpack.config.js b/webpack.config.js
index 6f8ead5..40376b9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,7 +5,7 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const distribution = {
- ckeditor5: 'djangocms_text_ckeditor5/static/djangocms_text/',
+ ckeditor5: 'djangocms_text_ckeditor5/static/djangocms_text_ckeditor5/',
};
module.exports = {