开发者

SharePoint: commonModalDialogClose does not close cross-domain dialog

I have a page hosted in 'virtualcasa1' domain opening a modal dialog:

var options = {
    title: "Repro",
    width: 400,
    height: 600,
    url: http://domain2:999/sites/blank/_layouts/XDomainTest/XDomainTestTarget.aspx //[1]
    //url: http://virtualcasa1/sites/blank/_layouts/XDomainTest/XDomainTestTarget.aspx [2]
};
SP.UI.ModalDialog.showModalDialog(options);

And I have this code to close it:

alert(document.domain);
SP.UI.ModalDialog.co开发者_运维技巧mmonModalDialogClose(SP.UI.DialogResult.cancel, 'Cancelled clicked'); 

If both are in the same domain (case [2] above), the dialog closes well, no issues.

But - if target page hosted in the dialog (case [1] above), dialog does NOT close :-(

document.domain above shows the correct domain where page exists.

I suspect I'm facing a cross-domain issue here (duh), but how to fix it? Or am I wrong and issue is not XDomain-related?

Thanks much!


HTML5's postMessage is your answer.

https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage

Your parent window that initiates the dialog must have the following javascript:

function listener(event) {
  //alert(event.data);
  if (event.data == 'Cancel') {
    SP.UI.ModalDialog.commonModalDialogClose(SP.UI.DialogResult.cancel, 'Cancel clicked');
  }
  else {
    SP.UI.ModalDialog.commonModalDialogClose(SP.UI.DialogResult.OK, event.data);
  }
}

if (window.addEventListener) {
  addEventListener("message", listener, false)
} else {
  attachEvent("onmessage", listener)
}

Javascript for OK and Cancel buttons in your popup:

<input type="button" value="OK" onclick="parent.postMessage('Message to be displayed by the parent', '*');" class="ms-ButtonHeightWidth" />
<input type="button" value="Cancel" onclick="parent.postMessage('Cancel', '*');" class="ms-ButtonHeightWidth" />


Ajay's answer from the 1st of August 2014 is good, but it needs a bit more explanation. The reason for the failure to close the dialog is simple. Cross site scripting security features of modern browsers disallow a few things, one of which is the use of window.frameElement from within the framed window. This is a read-only property on the window object and it becomes set to null (or with IE, it actually throws an exception when you try to access it). The ordinary Cancel event handlers in the modal dialog conclude with a call to window.frameElement.cancelPopup(). This will fail of course. The ordinary Save handler where the Save worked on the server side results in SharePoint sending back a single line as the replacement document, which is a scriptlet to call window.frameElement.commitPopup(). This also will not work, and it's a real pain to overcome because the page has been reloaded and there is no script available to handle anything. XSS won't give us access to the framed DOM from the calling page.

In order to make a cross domain hosted form work seamlessly, you need to add script to both the page that opens the dialog and the framed page. In the page that opens the dialog, you set the message listener as suggested by Ajay. In the framed form page, you need something like below:

(function() {
    $(document).ready(function() {
        var frameElement = null;
        // Try/catch to overcome IE Access Denied exception on window.frameElement
        try {
            frameElement = window.frameElement;
        } catch (Exception) {}

        // Determine that the page is hosted in a dialog from a different domain
        if (window.parent && !frameElement) {
            // Set the correct height for #s4-workspace
            var frameHeight = $(window).height();
            var ribbonHeight = $('#s4-ribbonrow').height();
            $('#s4-workspace').height(frameHeight - ribbonHeight);

            // Finds the Save and Cancel buttons and hijacks the onclick
            function applyClickHandlers(theDocument) {
                $(theDocument).find('input[value="Cancel"]').removeAttr('onclick').on('click', doTheClose);
                $(theDocument).find('a[id="Ribbon.ListForm.Edit.Commit.Cancel-Large"]').removeAttr('onclick').removeAttr('href').on('click', doTheClose);
                $(theDocument).find('input[value="Save"]').removeAttr('onclick').on('click', doTheCommit);
                $(theDocument).find('a[id="Ribbon.ListForm.Edit.Commit.Publish-Large"]').removeAttr('onclick').removeAttr('href').on('click', doTheCommit);
            }

            // Function to perform onclick for Cancel
            function doTheClose(evt) {
                evt.preventDefault();
                parent.postMessage('Cancel', '*');
            }

            // Function to perform onclick for Save
            function doTheCommit(evt) {
                evt.preventDefault();

                if (!PreSaveItem()) return false;
                var targetName = $('input[value="Save"]').attr('name');
                var oldOnSubmit = WebForm_OnSubmit;
                WebForm_OnSubmit = function() {
                    var retVal = oldOnSubmit.call(this);
                    if (retVal) {
                        var theForm = $('#aspnetForm');
                        // not sure whether following line is needed,
                        // but doesn't hurt
                        $('#__EVENTTARGET').val(targetName);
                        var formData = new FormData(theForm[0]);
                        $.ajax(
                        {
                            url: theForm.attr('action'),
                            data: formData,
                            cache: false,
                            contentType: false,
                            processData: false,
                            method: 'POST',
                            type: 'POST', // For jQuery < 1.9
                            success: function(data, status, transport) {
                                console.log(arguments);
                                // hijack the response if it's just script to
                                // commit the popup (which will break)
                                if (data.startsWith('<script') &&
                                    data.indexOf('.commitPopup()') > -1)
                                {
                                    parent.postMessage('OK', '*');
                                    return;
                                }

                                // popup not being committed, so actually
                                // submit the form and replace the page.
                                theForm.submit();
                            }
                        }).fail(function() {
                            console.log('Ajax post failed.');
                            console.log(arguments);
                        });
                    }

                    return false;
                }
                WebForm_DoPostBackWithOptions(
                    new WebForm_PostBackOptions(targetName,
                                                "",
                                                true,
                                                "",
                                                "",
                                                false,
                                                true)
                );
                WebForm_OnSubmit = oldOnSubmit;
            }

            applyClickHandlers(document);
        }
    });
})();

This solution makes use of the jQuery library, which our organization uses extensively. It is our preferred framework (chosen by me). I'm sure someone very clever could rewrite this without that dependency, but this is a good starting point. I hope someone finds it useful, as it represents a good two days work. Some things to note:

SharePoint does a postback on all sorts of events on the page, including putting the page into edit mode. Because of this, it makes more sense to trap the specific button clicks, both on the form and in the ribbon, rather than wholesale redefinition of, for example, the global WebForm_OnSubmit function. We briefly override that on a Save click and then set it back.

On any Save click event, we defeat the normal posting of the form and replace that with an identical POST request using AJAX. This allows us to discard the returned scriptlet when the form was successfully posted. When the form submission was not successful, perhaps because of blank required values, we just post the form properly to allow the page to be updated. This is fine, since the form will not have been processed. An earlier version of this solution took the resulting HTML document and replaced all of the page contents, but Internet Explorer doesn't like this.

The FormData api allows us to post the form as multipart-mime. This api has at least basic support in all modern browsers, and there are workarounds for older ones.

Another thing that seems to fail in the cross domain hosted dialog is the scrolling of the content window. For whatever reason, the height is not set correctly on the div with id s4-workspace, so we also set that in the solution.

EDIT: Almost forgot. You may also need to add this control to your framed ASPX page, which can be done with SharePoint Designer:

<WebPartPages:AllowFraming runat="server"/>


I have exactly the same issue - a dialog opening a view page for an item works fine when opened from a site collection on the same web app/domain, but the Close button fails to work when opening the same item from a site collection hosted in a separate web application. I'm assuming it is a cross-domain thing so I've altered the solution to accomodate this restriction, however, I'm not 100% happy about it as it does make the overall solution a little awkward to use from a user-perspective. I've put the issue to one side for now due to project timescales, but I'm still curious as to why. The only things I can think of is the whole cross-domain thing causing it and that maybe it is there by design to prevent XSS security holes.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜