Skip to content

Commit

Permalink
Merge branch 'xwiki:master' into XWIKI-21878
Browse files Browse the repository at this point in the history
  • Loading branch information
Sereza7 authored Mar 6, 2024
2 parents b95e57d + 0ad5064 commit 0bfc237
Show file tree
Hide file tree
Showing 58 changed files with 3,038 additions and 2,150 deletions.
2 changes: 1 addition & 1 deletion .mvn/extensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
<extension>
<groupId>com.gradle</groupId>
<artifactId>common-custom-user-data-maven-extension</artifactId>
<version>1.12.5</version>
<version>1.13</version>
</extension>
</extensions>
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<zookeeper.version>3.9.1</zookeeper.version>

<!-- Versions of other software we need in our functional tests -->
<testcontainers.version>1.19.6</testcontainers.version>
<testcontainers.version>1.19.7</testcontainers.version>
<!-- The LO version must point to the latest version from "Still" branch (LTS). When upgrading the version make
sure the new version is available from https://download.documentfoundation.org/libreoffice/stable/
Note: We don't need to exact version (e.g. 7.2.7.2) since the LTS is made available using a max of 2 dots
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
import org.openqa.selenium.support.FindBy;
import org.xwiki.test.ui.po.EditRightsPane;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Represents the actions possible on the Global Rights Administration Page.
*
Expand Down Expand Up @@ -68,18 +71,23 @@ public void unforceAuthenticatedView()

private void setAuthenticatedView(boolean enabled)
{
String desiredAltValue = enabled ? "yes" : "no";

if (!this.forceAuthenticatedViewLink.getAttribute("alt").equals(desiredAltValue)) {
String desiredCheckedValue = enabled ? "checked" : null;
String initialCheckedValue = this.forceAuthenticatedViewLink.getAttribute("checked");
if (initialCheckedValue == null || !initialCheckedValue.equals(desiredCheckedValue)) {
this.forceAuthenticatedViewLink.click();

// Wait for the setting to apply. Wait longer than usual in this case in an attempt to avoid some false
// positives in the tests.
int defaultTimeout = getDriver().getTimeout();
try {
getDriver().setTimeout(defaultTimeout * 2);
getDriver().waitUntilElementHasAttributeValue(
By.id(this.forceAuthenticatedViewLink.getAttribute("id")), "alt", desiredAltValue);
if (enabled) {
getDriver().waitUntilElementHasAttributeValue(
By.id(this.forceAuthenticatedViewLink.getAttribute("id")), "checked", "true");
} else {
getDriver().waitUntilCondition(driver ->
this.forceAuthenticatedViewLink.getAttribute("checked") == null);
}
} finally {
// Restore the utils timeout for other tests.
getDriver().setTimeout(defaultTimeout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<!-- This is needed for realtime editing. -->
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-realtime-wysiwyg-webjar</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
<!-- Marked as optional because the realtime plugin is disabled by default. -->
<optional>true</optional>
</dependency>
<!-- This is used by the resource suggest picker on the link modal. -->
<dependency>
<groupId>org.webjars.npm</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
define('xwiki-ckeditor-realtime-adapter', [
'xwiki-realtime-wysiwyg-editor'
], function (Editor) {
'use strict';

/**
* Implementation of the {@link Editor} "interface" for CKEditor.
*/
class CKEditorAdapter extends Editor {
/**
* @param {CKEDITOR.editor} ckeditor the CKEditor instance that is being synchronized in real-time
* @param {CKEDITOR} CKEDITOR the CKEditor API entry point
* @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html
* @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html
*/
constructor(ckeditor, CKEDITOR) {
super();

this._ckeditor = ckeditor;
this._CKEDITOR = CKEDITOR;

// Disable temporary attachment upload for now.
if (this._ckeditor.config['xwiki-upload']) {
this._ckeditor.config['xwiki-upload'].isTemporaryAttachmentSupported = false;
}

// Register code to be executed each time the editor content is reloaded.
// We use a very low priority because we want our listener to be executed after CKEditor's default listeners
// (e.g. after the CKEditor widgets are initialized).
const priority = 1000;
this._ckeditor.on('contentDom', this._onContentLoaded.bind(this), null, null, priority);
if (this._ckeditor.editable()) {
// Initial content load.
this._onContentLoaded();
}
}

/** @inheritdoc */
getFormFieldName() {
return this._ckeditor.name;
}

/** @inheritdoc */
getOutputHTML() {
return this._ckeditor.getData();
}

/** @inheritdoc */
getContentWrapper() {
return this._ckeditor.editable()?.$;
}

/** @inheritdoc */
getToolBar() {
return this._ckeditor.ui.space('top').$.querySelector('.cke_toolbox');
}

/** @inheritdoc */
contentUpdated(updatedNodes, propagate) {
try {
this._initializeWidgets(updatedNodes);
} catch (e) {
console.log("Failed to (re)initialize the widgets.", e);
}

// Notify the content change (e.g. to update the empty line placeholders) without triggering our own change
// handler (see #onChange()).
this._ckeditor.fire('change', {remote: !propagate});
}

/** @inheritdoc */
onChange(callback) {
this._ckeditor.on('change', (event) => {
if (!event.data?.remote) {
callback();
}
});
}

/** @inheritdoc */
getSelection() {
return this._ckeditor.getSelection()?.getNative();
}

/** @inheritdoc */
saveSelection() {
this._CKEDITOR.plugins.xwikiSelection.saveSelection(this._ckeditor);
}

/** @inheritdoc */
restoreSelection() {
this._CKEDITOR.plugins.xwikiSelection.restoreSelection(this._ckeditor);
}

/** @inheritdoc */
convertDataToHtml(data) {
return this._ckeditor.dataProcessor.toHtml(data);
}

/** @inheritdoc */
showNotification(message, type) {
this._ckeditor.showNotification(message, type);
}

/** @inheritdoc */
getCustomFilters() {
// Widget attributes that may have different values for each user (so they can't really be synchronized).
const ignoredWidgetAttributes = ['data-cke-widget-upcasted', 'data-cke-widget-id', 'data-cke-filter'];

// Reject the CKEditor drag and resize handlers (for widgets and images).
const ignoredWidgetHelpers = [
'cke_widget_drag_handler_container', 'cke_widget_drag_handler', 'cke_image_resizer'
];

return [
//
// Widget filter.
//
{
shouldSerializeNode: (node) => !(
// Reject the hidden (widget) selection and some widget helpers.
node.nodeType === Node.ELEMENT_NODE &&
(node.hasAttribute('data-cke-hidden-sel') ||
ignoredWidgetHelpers.some(className => node.classList.contains(className)))
),
filterHyperJSON: (hjson) => {
ignoredWidgetAttributes.forEach(attributeName => {
delete hjson[1][attributeName];
});
// Each user may have a different widget selected and/or focused, we don't want to synchronize that.
if (hjson[1].class) {
hjson[1].class = hjson[1].class.split(/\s+/).filter(className => ![
'cke_widget_selected', 'cke_widget_focused', 'cke_widget_editable_focused'
].includes(className)).join(' ');
}
return hjson;
}
},

//
// Filling character sequence filter.
// See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_dom_selection.html#property-FILLING_CHAR_SEQUENCE
// See https://bugs.webkit.org/show_bug.cgi?id=15256 (Impossible to place an editable selection inside empty
// elements)
//
{
// Both shouldSerializeNode and filterHyperJSON are currently called only for elements so in order to filter
// text nodes we need to filter the direct text child nodes of the element passed to filterHyperJSON.
filterHyperJSON: (hjson) => {
const oldChildNodes = hjson[2];
const newChildNodes = [];
oldChildNodes.forEach(childNode => {
if (typeof childNode === 'string') {
// Remove the filling character sequence from text nodes.
childNode = childNode.replace(CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, '');
// Ignore text nodes that are used only to allow the user to place the caret inside empty elements.
if (childNode !== '') {
newChildNodes.push(childNode);
}
} else {
newChildNodes.push(childNode);
}
});
hjson[2] = newChildNodes;
return hjson;
}
}
];
}

/** @inheritdoc */
onBeforeDestroy(callback) {
this._ckeditor.on('beforeDestroy', callback);
}

_initializeWidgets(updatedNodes) {
// Reset the focused and selected widgets, as well as the widget holding the focused editable because they may
// have been invalidated by the DOM changes.
this._ckeditor.widgets.focused = null;
this._ckeditor.widgets.selected = [];
this._ckeditor.widgets.widgetHoldingFocusedEditable = null;

// Find the widgets that need to be reinitialized because some of their content was updated.
const updatedWidgets = new Set();
updatedNodes.forEach(updatedNode => {
if (updatedNode.nodeType === Node.ATTRIBUTE_NODE) {
// For attribute nodes we consider the owner element was updated.
updatedNode = updatedNode.ownerElement;
} else if (updatedNode.nodeType !== Node.ELEMENT_NODE) {
// The updated node is a text or comment, most probably, so it doesn't affect the widget.
return;
}
const updatedWidget = this._ckeditor.widgets.getByElement(new this._CKEDITOR.dom.element(updatedNode));
if (updatedWidget) {
updatedWidgets.add(updatedWidget);
// We also have to reinitialize the nested widgets.
updatedWidget.wrapper.find('.cke_widget_wrapper').toArray().forEach(nestedWidgetWrapper => {
const nestedWidget = this._ckeditor.widgets.getByElement(nestedWidgetWrapper, true);
if (nestedWidget) {
updatedWidgets.add(nestedWidget);
}
});
}
});

// Delete the updated widgets so that we can reinitialize them.
updatedWidgets.forEach(widget => {
delete this._ckeditor.widgets.instances[widget.id];
});

// Remove the widgets whose element was removed from the DOM and add widgets to match the widget elements found in
// the DOM.
this._ckeditor.widgets.checkWidgets();

// Update the focused and selected widgets, as well as the widget holding the focused editable, after the
// selection is restored.
setTimeout(() => this._ckeditor.widgets.checkSelection(), 0);
}

_onContentLoaded() {
this._fixMagicLine();
}

_fixMagicLine() {
// Make sure the magic line is not synchronized between editors.
const magicLine = this._ckeditor._.magiclineBackdoor?.that?.line?.$;
if (magicLine) {
[magicLine, magicLine.parentElement].forEach(function (element) {
element.setAttribute('class', 'rt-non-realtime');
});
}
}
}

return CKEditorAdapter;
});
Loading

0 comments on commit 0bfc237

Please sign in to comment.