Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## [Unreleased]

### Changed

- Removed jQuery dependency from the CKEditor build. All jQuery usage has been replaced with vanilla JavaScript equivalents:
- `jQuery.each()` replaced with native `Array.forEach()`
- `jQuery.getJSON()` replaced with `fetch()` API
- `jQuery.ajax()` replaced with `fetch()` API
- jQuery DOM manipulation replaced with native DOM APIs (`document.createElement()`, `element.parentElement`, `element.style`, etc.)

### Migration Notes

This library no longer uses jQuery internally. However, downstream consumers (such as OpenProject) should keep the jQuery global available for other parts of the application until they are ready to remove it. This change only affects the internal implementation of this library and should not require changes in consuming applications.

For more context, see the related OpenProject pull request: https://github.com/opf/openproject/pull/19429
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,13 @@ Now the webpack development mode is building the files and outputting them to `a



## Migration Notes

### jQuery Removal

As of version 11.2.0, this library no longer uses jQuery internally. All jQuery dependencies have been replaced with vanilla JavaScript equivalents using the Fetch API and native DOM manipulation.

**Important for downstream consumers (e.g., OpenProject):** While this library no longer uses jQuery internally, downstream applications should continue to expose the jQuery global if other parts of the application depend on it. Do not remove the jQuery global from the downstream application (OpenProject) yet until all components have been migrated.

For more details on the downstream migration, see: https://github.com/opf/openproject/pull/19429

4 changes: 2 additions & 2 deletions src/helpers/button-disabler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function getToolbarItems(editor) {
}

export function disableItems(editor, except) {
jQuery.each(getToolbarItems(editor), function (index, item) {
getToolbarItems(editor).forEach((item) => {
let toDisable = item;

if (item instanceof FileDialogButtonView) {
Expand All @@ -31,7 +31,7 @@ export function disableItems(editor, except) {
}

export function enableItems(editor) {
jQuery.each(getToolbarItems(editor), function (index, item) {
getToolbarItems(editor).forEach((item) => {
let toEnable = item;

if (item instanceof FileDialogButtonView) {
Expand Down
37 changes: 24 additions & 13 deletions src/mentions/user-mentions.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,29 @@ export function userMentions(queryText) {
const pluginContext = getPluginContext(editor);
const base = window.OpenProject.urlRoot;

return new Promise((resolve, _reject) => {
jQuery.getJSON(url, collection => {
resolve(_.uniqBy(collection._embedded.elements, (el) => el.id).map(mention => {
const type = mention._type.toLowerCase();
const text = `@${mention.name}`;
const id = `@${mention.id}`;
const idNumber = mention.id;
const typeSegment = pluginContext.services.apiV3Service[`${type}s`].segment;
const link = `${base}/${typeSegment}/${idNumber}`;
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(collection => {
resolve(_.uniqBy(collection._embedded.elements, (el) => el.id).map(mention => {
const type = mention._type.toLowerCase();
const text = `@${mention.name}`;
const id = `@${mention.id}`;
const idNumber = mention.id;
const typeSegment = pluginContext.services.apiV3Service[`${type}s`].segment;
const link = `${base}/${typeSegment}/${idNumber}`;

return {type, id, text, link, idNumber, name: mention.name};
}));
});
})
return {type, id, text, link, idNumber, name: mention.name};
}));
})
.catch(error => {
console.error('Error fetching user mentions:', error);
reject(error);
});
});
}
28 changes: 20 additions & 8 deletions src/mentions/work-package-mentions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@ export function workPackageMentions(prefix) {
return [];
}

return new Promise((resolve, _reject) => {
jQuery.getJSON(url, { q: query, scope: "all" }, collection => {
resolve(collection.map(wp => {
const id = `${prefix}${wp.id}`;
const idNumber = wp.id;
return new Promise((resolve, reject) => {
const params = new URLSearchParams({ q: query, scope: "all" });
fetch(`${url}?${params.toString()}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(collection => {
resolve(collection.map(wp => {
const id = `${prefix}${wp.id}`;
const idNumber = wp.id;

return { id, idNumber, type: "work_package", text: id, name: wp.to_s, link: base + wp.id };
}));
});
return { id, idNumber, type: "work_package", text: id, name: wp.to_s, link: base + wp.id };
}));
})
.catch(error => {
console.error('Error fetching work package mentions:', error);
reject(error);
});
});
};
}
57 changes: 39 additions & 18 deletions src/plugins/op-preview.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@ export default class OPPreviewPlugin extends Plugin {


let showPreview = function(preview) {
let $reference = jQuery(editor.ui.getEditableElement()).parent();
let $previewWrapper = jQuery('<div class="ck-editor__preview op-uc-container"></div>');
$reference.siblings('.ck-editor__preview').remove();
const editableElement = editor.ui.getEditableElement();
const reference = editableElement.parentElement;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference error if editableElement.parentElement is null. The code immediately accesses reference.parentElement on lines 39 and 46 without checking if reference (which is editableElement.parentElement) is null first.

Consider adding a null check:

const editableElement = editor.ui.getEditableElement();
const reference = editableElement.parentElement;
if (!reference || !reference.parentElement) {
  console.error('Cannot show preview: invalid editor structure');
  return;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@myabc myabc Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply suggestion, but use optional chaining

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0497dc1 - added optional chaining (?.) and early return guard with error logging

const previewWrapper = document.createElement('div');
previewWrapper.className = 'ck-editor__preview op-uc-container';

// Remove existing preview elements
const existingPreviews = reference.parentElement.querySelectorAll('.ck-editor__preview');
existingPreviews.forEach(el => el.remove());

const previewService = getOPService(editor, 'ckEditorPreview');
unregisterPreview = previewService.render($previewWrapper[0], preview);
unregisterPreview = previewService.render(previewWrapper, preview);

$reference.hide();
$reference.after($previewWrapper);
reference.style.display = 'none';
reference.parentElement.insertBefore(previewWrapper, reference.nextSibling);

disableItems(editor, view);
};
Expand All @@ -47,22 +52,38 @@ export default class OPPreviewPlugin extends Plugin {
let link = getOPPreviewContext(editor);
let url = getOPPath(editor).api.v3.previewMarkup(link);

jQuery
.ajax({
data: editor.getData(),
url: url,
response_type: 'text',
contentType: 'text/plain; charset=UTF-8',
method: 'POST',
}).done(showPreview);
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'text/plain; charset=UTF-8'
},
body: editor.getData()
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(showPreview)
.catch(error => {
console.error('Error fetching preview:', error);
});
};

let disablePreviewing = function() {
let $mainEditor = jQuery(editor.ui.getEditableElement()).parent();
const editableElement = editor.ui.getEditableElement();
const mainEditor = editableElement.parentElement;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference error if editableElement.parentElement is null. The code immediately accesses mainEditor.parentElement on line 83 without checking if mainEditor (which is editableElement.parentElement) is null first.

Consider adding a null check:

const editableElement = editor.ui.getEditableElement();
const mainEditor = editableElement.parentElement;
if (!mainEditor || !mainEditor.parentElement) {
  console.error('Cannot disable preview: invalid editor structure');
  return;
}
Suggested change
const mainEditor = editableElement.parentElement;
const mainEditor = editableElement && editableElement.parentElement;
if (!mainEditor || !mainEditor.parentElement) {
console.error('Cannot disable preview: invalid editor structure');
return;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@myabc myabc Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply suggestion, but use optional chaining

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0497dc1 - added optional chaining (?.) and early return guard with error logging as requested


unregisterPreview();
$mainEditor.siblings('.ck-editor__preview').remove();
$mainEditor.show();
if (unregisterPreview) {
unregisterPreview();
}

// Remove existing preview elements
const existingPreviews = mainEditor.parentElement.querySelectorAll('.ck-editor__preview');
existingPreviews.forEach(el => el.remove());

mainEditor.style.display = '';

enableItems(editor);
};
Expand Down
27 changes: 18 additions & 9 deletions src/plugins/op-source-code.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@ export default class OPSourceCodePlugin extends Plugin {


let showSource = function(_preview) {
let $reference = jQuery(editor.ui.getEditableElement()).parent();
let $sourceWrapper = jQuery('<div class="ck-editor__source"></div>');
$reference.siblings('.ck-editor__source').remove();

$reference.hide();
$reference.after($sourceWrapper);
const editableElement = editor.ui.getEditableElement();
const reference = editableElement.parentElement;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference error if editableElement.parentElement is null. The code immediately accesses reference.parentElement on lines 44 and 48 without checking if reference (which is editableElement.parentElement) is null first.

Consider adding a null check:

const editableElement = editor.ui.getEditableElement();
const reference = editableElement.parentElement;
if (!reference || !reference.parentElement) {
  console.error('Cannot show source: invalid editor structure');
  return;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@myabc myabc Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply suggestion, but use optional chaining

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0497dc1 - added optional chaining (?.) and early return guard with error logging

const sourceWrapper = document.createElement('div');
sourceWrapper.className = 'ck-editor__source';

// Remove existing source elements
const existingSources = reference.parentElement.querySelectorAll('.ck-editor__source');
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The selector logic has changed from the original jQuery behavior. $reference.siblings('.ck-editor__source') only selected sibling elements of reference, but reference.parentElement.querySelectorAll('.ck-editor__source') selects ALL descendants matching the selector within the parent element, not just siblings. This could remove unintended elements if there are nested structures.

Consider using a more precise selector or filtering to match only siblings:

const existingSources = Array.from(reference.parentElement.children)
  .filter(el => el.classList.contains('ck-editor__source'));
existingSources.forEach(el => el.remove());

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please fix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0497dc1 - replaced querySelectorAll with Array.from(parentElement.children).filter() to match jQuery's .siblings() behavior

existingSources.forEach(el => el.remove());

reference.style.display = 'none';
reference.parentElement.insertBefore(sourceWrapper, reference.nextSibling);

disableItems(editor, view);

Expand All @@ -56,12 +61,16 @@ export default class OPSourceCodePlugin extends Plugin {
};

let hideSource = function() {
let $mainEditor = jQuery(editor.ui.getEditableElement()).parent();
const editableElement = editor.ui.getEditableElement();
const mainEditor = editableElement.parentElement;
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null reference error if editableElement.parentElement is null. The code immediately accesses mainEditor.parentElement on line 70 without checking if mainEditor (which is editableElement.parentElement) is null first.

Consider adding a null check:

const editableElement = editor.ui.getEditableElement();
const mainEditor = editableElement.parentElement;
if (!mainEditor || !mainEditor.parentElement) {
  console.error('Cannot hide source: invalid editor structure');
  return;
}
Suggested change
const mainEditor = editableElement.parentElement;
const mainEditor = editableElement.parentElement;
if (!mainEditor || !mainEditor.parentElement) {
console.error('Cannot hide source: invalid editor structure');
return;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply suggestion, but use optional chaining

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0497dc1 - added optional chaining (?.) and early return guard with error logging


editor.fire('op:source-code-disabled');

$mainEditor.siblings('.ck-editor__source').remove();
$mainEditor.show();
// Remove existing source elements
const existingSources = mainEditor.parentElement.querySelectorAll('.ck-editor__source');
existingSources.forEach(el => el.remove());

mainEditor.style.display = '';

enableItems(editor);

Expand Down
Loading