Move paragraph to another page (content restructure)

Hi,

Recently I encountered a seemingly easy problem that I can’t solve:
I have some really long pages in xwiki that I want restructure. My question is how do you move parts, say a paragraph or chapter with pictures to another/new page?

Just cut and paste will break the image links to attached images.

Any suggestions?

Thanks.

rbr

Not if you indicate the full image reference as in SomePage.SubPage@file.png.

Yes, but…

This is not a workflow, that I can/want to ask my users to follow.

What they do is type some text, paste the images save the doc. And then images use the “short” references.

When they restructure it it’s somewhat the same:
Copy/cut the source, open the target, paste the content, save the doc. Now the links are broken.

Even worse while editing in WYSIWYG editor the pictures even show, before they break in view mode.

So it starts to sound like a bug in the editor to me.

OK I tough your use case was actually the opposite: have some content in a different page with its attachments and include that document.

Indeed if you copy an image from the WYSIWYG, paste it in the WYSIWYG in another page you should definitely not end up with a broken image in the end. I also think this is a bug to fix (either by changing the reference of the image or by copying the attachment to).

I created a bug for this
https://jira.xwiki.org/browse/XWIKI-15350

Hope it gets fixed soon.

It can get fixed very soon if you can provide a Pull Request :wink: If you’re not a developer an option is to sponsor it, see http://dev.xwiki.org/xwiki/bin/view/Community/Contributing#HSponsoringissues

Thanks

I’m really trying to get into xwiki development, unfortunately my dev-box is just not ready yet.
See https://forum.xwiki.org/t/building-xwiki-10-4-x-branch-fails-on-windows/3106
:wink:

So I have looked a bit into this. Here is the setting:
First page http://192.168.2.86:8080/xwiki/bin/view/editorPasteTest/editorPasteSource/ having a heading 1 and an image as the paste source
Second page http://192.168.2.86:8080/xwiki/bin/view/editorPasteTest/ is the target to paste to and there I have javascript extension that subscribes to the ckeditor paste event and alerts the pasted data, see below.

I see the following get’s pasted:

<h1>This is a test</h1>
<p>
	<span tabindex="-1" contenteditable="false" data-cke-widget-wrapper="1" data-cke-filter="off" class="cke_widget_wrapper cke_widget_inline cke_widget_image cke_image_nocaption cke_widget_selected" data-cke-display-name="image" data-cke-widget-id="1" role="region" aria-label=" image widget">
		<img data-cke-saved-src="/xwiki/bin/download/editorPasteTest/editorPasteSource/WebHome/1531810060801-979.png" src="/xwiki/bin/download/editorPasteTest/editorPasteSource/WebHome/1531810060801-979.png" width="520" height="295" data-reference="false|-|attach|-|1531810060801-979.png" data-cke-widget-data="%7B%22hasCaption%22%3Afalse%2C%22src%22%3A%22%2Fxwiki%2Fbin%2Fdownload%2FeditorPasteTest%2FeditorPasteSource%2FWebHome%2F1531810060801-979.png%22%2C%22alt%22%3A%22%22%2C%22width%22%3A%22520%22%2C%22height%22%3A%22295%22%2C%22lock%22%3Atrue%2C%22align%22%3A%22none%22%2C%22resourceReference%22%3A%7B%22typed%22%3Afalse%2C%22type%22%3A%22attach%22%2C%22reference%22%3A%221531810060801-979.png%22%2C%22parameters%22%3A%7B%7D%7D%2C%22classes%22%3Anull%7D" data-cke-widget-upcasted="1" data-cke-widget-keep-attr="0" data-widget="image" class="cke_widget_element" alt="" />
		<span class="cke_reset cke_widget_drag_handler_container" style="background: url(&quot;http://192.168.2.86:8080/xwiki/webjars/wiki%3Axwiki/application-ckeditor-webjar/1.20/plugins/widget/images/handle.png&quot;) rgba(220, 220, 220, 0.5); top: -15px; left: 0px; display: block;">
			<img class="cke_reset cke_widget_drag_handler" data-cke-widget-drag-handler="1" src="data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==" width="15" title="Click and drag to move" height="15" role="presentation" draggable="true" />
		</span>
		<span class="cke_image_resizer" title="Click and drag to resize">​</span>
	</span>
</p>

So everything we need is already there in data-cke-saved-src and src but there is also data-reference. Is my assumption correct that “on save” or “on preview” (or similiar action) the src-attribute of the image gets overwritten by the values of data-reference? Where does this happen (which code file)? Is there any way to test if there is an attachment on the page that data-reference refers to else “do something” like update reference or copy/move attachment?

jsx code:

require(['deferred!ckeditor'], function(ckeditorPromise) {
    ckeditorPromise.done(function(ckeditor) {
        ckeditor.on('instanceCreated', function(event) {
            event.editor.on('paste', function(evt){
                alert(evt.data.dataValue);
            });
        });
    });
});

The src attribute doesn’t get overwritten. You can check by inspecting the HTTP request made when you Save & Continue (using the Network tab from the browser’s developer tools). Both the src attribute and the reference are sent to the server, where indeed, the XWiki Rendering gives precedence to the reference. The reason is because the HTML you edit with the CKEditor is saved as wiki syntax and the Rendering needs to be able to “recompute” the wiki syntax (or preserve it in case you edit the page without touching the image).

See https://github.com/xwiki-contrib/application-ckeditor/blob/master/plugins/src/main/resources/xwiki-image/plugin.js#L60 . toDataFormat is called to prepare the HTML for wiki conversion.

I don’t think this is the right approach, because it’s perfectly valid to “insert” an image that doesn’t exist. Maybe you’re going to attach the image later, maybe it was removed by a different user at the same time you were editing, etc.

A better approach might be to “filter” the HTML content that is copied (before it is copied) so that the data-reference attribute is removed, which would make the Rendering rely on the src attribute when you paste and save.

My ultimate goal would be to show a dialog containing all the pasted images allowing you to chose which action should be done image by image.
The actions would be something like:

  • Update reference
  • Copy image
  • Do nothing

I’m not sure if this a good idea, but I’d really love to see more support for content restructuring.
I will try to implement this but I guess it will be a looong way to go for me, because I’m really only scratching the surface with xwiki development.

For now I will try to filter the pasted html as you suggested. Many thanks for your help.

Unfortunately there is no “copy” or “beforeCopy” event in CK editor. I tried my luck with the paste event but whatever changes I try to apply to the evt.data.dataValue the xwiki plugin is still able to retrieve the original value of data-reference and therefore “breaking” the reference to the image.

Even if I overwrite the complete content of evt.data.dataValue with arbitrary html containing a relative reference to a image like “/xwiki/bin/download/editorPasteTest/editorPasteSource/WebHome/1531810060801-979.png?width=520&height=295” xwiki creates an url like “http://192.168.2.86:8080/xwiki/bin/download/editorPasteTest/WebHome/%2Fxwiki%2Fbin%2Fdownload%2FeditorPasteTest%2FeditorPasteSource%2FWebHome%2F1531810060801-979.png%3Fwidth%3D520%26height%3D295?width=520&height=295” which again fails to download.

So I’m pretty stuck now.

Can you think of any way to cut/copy from an xwiki editor, paste to another xwiki editor and have working image references?

Another option, that is not bullet proof though, is to write an event listener on the server side that:

  • catches the document save event
  • looks for Image blocks in the document content XDOM
    ** if the Image block point to an attachment that doesn’t exist and there is an attachment with the same name on the document being saved then update the attachment reference on the Image block

See https://extensions.xwiki.org/xwiki/bin/view/Extension/Observation+Module+Local .

Thanks again, @mflorea!

I will consider that, if all else fails.
Currently I still don’t want to give up on the client side solution.
From my understanding of the documentation of the paste event it is meant to be used for processing the data. So if I throw away attributes in the listener they should no longer be available OR considered. Correct?

What I observe is that even if I process data in this event xwiki seems to be able to work on the original pasted data or at least retrieve the data.

If I add breakpoints to my paste handler and the toDataFormat function I see that my paste handler gets called first. The call stack at first glance does not show any signs of xwiki code being called. Still in this line var a = CKEDITOR.tools.escapeComment(b.attributes["data-reference"])the attribute data-reference has a value although I remove it in the paste handler.
This holds true regardless if the priority of the pastehandler is 2, 10 (default AFAIK) or 100.

From my understanding there are two possibilities:

  • I’m doing something badly wrong (please find my current code below).
  • The paste event handler does not work as expected in xwiki and it’s usage should be discouraged. I could not find anything in the documentation about this, so it may need to be updated.

Current code of my paste handler:

require(['deferred!ckeditor'], function(ckeditorPromise) {
    ckeditorPromise.done(function(ckeditor) {
        ckeditor.on('instanceCreated', function(event) {
            event.editor.on('paste', function(evt){
                console.log(evt.data.dataValue);
                var fragment = CKEDITOR.htmlParser.fragment.fromHtml(evt.data.dataValue);
                count=0;
                fragment.forEach(function(node){
                  if(node.name!=null && node.name=="img"){
                    console.log(node);
                    console.log(node.attributes);
                    console.log("contains data-reference: " + node.attributes["data-reference"])
                    if(node.attributes["data-reference"]){
                      delete node.attributes["data-reference"];
                      count++;
                    }
                  }
                }, CKEDITOR.NODE_ELEMENT, true);
               if(count>0){
                 if(confirm("Update image references?")){
                   var writer = new CKEDITOR.htmlParser.basicWriter();
                   fragment.writeHtml( writer );
                   var writtenHTML=writer.getHtml();
                   console.log(writtenHTML);
                   evt.data.dataValue = writtenHTML;
                 }
               }
            },null,null, 100);
        });
    });
});

I guess I found the problem: It’s just not enough to delete the data-reference attribute, you also have to delete the data-cke-widget-data. I’m not that happy with the manipulation of the src attribute but currently it is the best I can think of.
@mflorea: What do you think is this reasonable approach or did I forget any edge cases?

So my current code looks like this:

require(['deferred!ckeditor'], function(ckeditorPromise) {
    ckeditorPromise.done(function(ckeditor) {
        ckeditor.on('instanceCreated', function(event) {
            event.editor.on('paste', function(evt){
                console.log(evt.data.dataValue);
                var fragment = CKEDITOR.htmlParser.fragment.fromHtml(evt.data.dataValue);
                count=0;
                fragment.forEach(function(node){
                  if(node.name!=null && node.name=="img"){
                    console.log(node);
                    console.log(node.attributes);
                    console.log("contains data-reference: " + node.attributes["data-reference"])
                    if(node.attributes["data-reference"]){
                      delete node.attributes["data-reference"];
                      delete node.attributes["data-cke-widget-data"];
                      src=node.attributes["src"]
                      node.attributes["src"]=window.location.origin + src
                      count++;
                    }
                  }
                }, CKEDITOR.NODE_ELEMENT, true);
               if(count>0){
                 if(confirm("Update image references?")){
                   var writer = new CKEDITOR.htmlParser.basicWriter();
                   fragment.writeHtml( writer );
                   var writtenHTML=writer.getHtml();
                   console.log(writtenHTML);
                   evt.data.dataValue = writtenHTML;
                 }
               }
            },null,null, 100);
        });
    });
});

What’s the value of data-cke-widget-data? I don’t recall. In any case, it looks like this attribute is set by CKEditor not by XWiki. You need to see if its value is required in some other place.

If I understand this correctly the data is from the custom xwiki-image-widget.
A sample looks like this:

data-cke-widget-data="{"hasCaption":false,"src":"/xwiki/bin/download/pasteTest/source/WebHome/1531915339545-818.png?width=288&height=288","alt":"1531915339545-818.png","width":"288","height":"288","lock":true,"align":"none","resourceReference":{"typed":false,"type":"attach","reference":"1531915339545-818.png","parameters":{}},"classes":null}"

Actually I’d argue that this data should be cleaned if it is from an external source which we can determine via evt.data.dataTransfer.getTransferType(), see here. Let’s assume you have an application where you copy data from that also uses ckeditor in a different configuration, then you should not “trust” and reuse the data from them.

I updated the script once more to include the test for the DataTransferType and to update links also:

require(['deferred!ckeditor'], function(ckeditorPromise) {
    ckeditorPromise.done(function(ckeditor) {
        ckeditor.on('instanceCreated', function(event) {
            event.editor.on('paste', function(evt){
                //Update references only if they are pasted from an external source
                console.log("DataTransferType: " + evt.data.dataTransfer.getTransferType())
                if(evt.data.dataTransfer.getTransferType()!==CKEDITOR.DATA_TRANSFER_EXTERNAL){
                    return;
                }
                var fragment = CKEDITOR.htmlParser.fragment.fromHtml(evt.data.dataValue);
                count=0;
                fragment.forEach(function(node){
                  if(node.name!=null && (node.name=="img" || node.name=="a")){
                    if(node.attributes["data-reference"] || node.attributes["data-cke-widget-data"]){
                      delete node.attributes["data-reference"];
                      delete node.attributes["data-cke-widget-data"];
                      src=node.attributes["src"]
                      node.attributes["src"]=window.location.origin + src
                      count++;
                    }
                  }
                }, CKEDITOR.NODE_ELEMENT, true);
               if(count>0){
                 if(confirm("Update image references?")){
                   var writer = new CKEDITOR.htmlParser.basicWriter();
                   fragment.writeHtml( writer );
                   var writtenHTML=writer.getHtml();
                   console.log(writtenHTML);
                   evt.data.dataValue = writtenHTML;
                 }
               }
            });
        });
    });
});

This works quite well for my workflow.
@mflorea, @vmassol: Do you think that this may be a (part of a) solution to fix https://jira.xwiki.org/browse/XWIKI-15350? Are you interested in having a working workflow of pasting editor to editor?
If so, I will investigate this further and try to make this solution more generic and include other things like the figure macro. But I may need some help from you here.
If not I will leave it as it is (for now).

I made another small update, fixing the call of getTransferType:

require(['deferred!ckeditor'], function(ckeditorPromise) {
    ckeditorPromise.done(function(ckeditor) {
        ckeditor.on('instanceCreated', function(event) {
            event.editor.on('paste', function(evt){
                //Update references only if they are pasted from an external source
                console.log("DataTransferType: " + evt.data.dataTransfer.getTransferType(event.editor))
                if(evt.data.dataTransfer.getTransferType(event.editor)!==CKEDITOR.DATA_TRANSFER_EXTERNAL){
                    return;
                }
                var fragment = CKEDITOR.htmlParser.fragment.fromHtml(evt.data.dataValue);
                count=0;
                fragment.forEach(function(node){
                  if(node.name!=null && (node.name=="img" || node.name=="a")){
                    if(node.attributes["data-reference"] || node.attributes["data-cke-widget-data"]){
                      delete node.attributes["data-reference"];
                      delete node.attributes["data-cke-widget-data"];
                      src=node.attributes["src"]
                      node.attributes["src"]=window.location.origin + src
                      count++;
                    }
                  }
                }, CKEDITOR.NODE_ELEMENT, true);
               if(count>0){
                 if(confirm("Update image references?")){
                   var writer = new CKEDITOR.htmlParser.basicWriter();
                   fragment.writeHtml( writer );
                   var writtenHTML=writer.getHtml();
                   console.log(writtenHTML);
                   evt.data.dataValue = writtenHTML;
                 }
               }
            });
        });
    });
});