聚合物纸张对话框:如何准备切换注入的对话框?

时间:2016-01-09 02:22:10

标签: polymer

在我的应用程序中,为了响应用户输入,我将包含可滚动区域(paper-dialog-scrollable)的纸质对话框注入DOM作为正文的最后一个子项。我在它被调用时注入它,因为出于几个不同的原因,我发现在页面中包含对话框是不切实际的,以防用户决定激活它。我可以解释这些原因,但我认为这不会有效。

我按这样注入对话框:

var fragment = "<paper-dialog id='mydialog' ...><paper-dialog-scrollable ...>...";
$('body').append(fragment);
var dialog = $('#mydialog').get(0);

我发现的第一件事是,如果我尝试通过调用dialog.toggle()立即激活对话框,则对话框确实会显示在Chrome上,但在Firefox上,我在控制台中出现错误:

TypeError: dialog.toggle is not a function

我认为这种差异与需要在Firefox上进行更多填充而不是Chrome相关。我尝试的下一件事是用这段代码激活对话框:

Polymer.Base.async(function(){ dialog.toggle(); }, 1);

通过此更改,当我尝试调用它时,toggle()方法就在那里,并出现对话框。

在Chrome上测试时,我遇到的下一个问题是,如果paper-dialog包含可滚动部分(paper-dialog-scrollable),如果我“太快”激活对话框,则可滚动部分的高度为零注射后。这是因为“可滚动”div上的“适合”类,它是paper-dialog-scrollable元素的唯一子元素。我通过手动删除Chrome开发者工具中的“适合”类来验证这一点。看到对话框然后正确显示。

paper-dialog-scrollable的代码中,我发现了这个:

attached: function()
{
    this.classList.add('no-padding');
    // Set itself to the overlay sizing target
    this.dialogElement.sizingTarget = this.scrollTarget;
    // If the host is sized, fit the scrollable area to the container.
    // Otherwise let it be its natural size.
    requestAnimationFrame(function() {
        if (this.offsetHeight > 0) {
            // this happens when I toggle "too quickly"
            this.$.scrollable.classList.add('fit');
        }
        this._scroll();
    }.bind(this));
}

如果我在切换对话框之前等待更长时间:

Polymer.Base.async(function(){ dialog.toggle(); }, 100);

..然后“适合”类缺席&amp;正确显示对话框的可滚动部分。然而,这不是解决方案,因为可能需要等待更长时间(或不长),具体取决于机器的速度,当前负载等。我需要对话框可以可靠地工作而不必等待在切换之前不必要的时间它。是否有一些我能听到的事件会在切换对话框时安全触发?另外,有没有人知道应用“适合”类的paper-dialog-scrollable代码?也许有一些方法可以防止这个类名首先被应用(除了让用户等待的时间超过真正必要的时间)?

2 个答案:

答案 0 :(得分:1)

关于&#34; WebComponentsReady&#34;事件

Ryan White 建议的WebComponentsReady事件声音就像它可以提供帮助一样,但是在最初加载的页面上的所有导入都已加载并且所有自定义元素都加载后,它会在页面上触发一次在页面上已升级。在我的测试中,我发现它之后不再会发射。由于Web Components polyfill(例如Firefox上需要)加载导入并执行元素升级异步,因此在使用任何组件或自定义元素之前等待WebComponentsReady是必要的存在于初始页面中。如果初始页面确实导入了Web组件但不包含自定义元素,则WebComponentsReady事件仍然表示已完成导入的加载并且可以使用它们。

在我的情况下,最初加载的页面不会导入任何Web组件,也不包含自定义元素。我不想加载可能需要的所有组件,或者实例化用户可能要求的所有对话框。相反,我想加载Web组件并根据需要创建自定义元素。在接下来的部分中,我将分享我在动态注入Web组件(以及使用它们的自定义元素)方面所学到的知识。

注入Web组件并等待加载

这非常简单。

var util = {};

///////////////////////////////////////////////////////////////////////////////////////////////////
// util.listenOnce(elem, type, listener, useCapture)
//     Return a promise for an event of type <type> raised on <elem>.
///////////////////////////////////////////////////////////////////////////////////////////////////

util.listenOnce = function(elem, type, useCapture)
{
    var deferred = $.Deferred();
    var promise = deferred.promise();
    var callback = function()
    {
        deferred.resolve.apply(deferred, arguments);
        elem.removeEventListener(type, callback, useCapture);
    };
    elem.addEventListener(type, callback, useCapture);
    return promise;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// util.urlNormalize(url)
//    If <url> is a site-local URL, return a full URL version of it, otherwise return <url> as-is.
///////////////////////////////////////////////////////////////////////////////////////////////////

util.urlNormalize = function(url)
{
    // already a full URL -> return as-is
    if ((url.indexOf('http:') == 0) || (url.indexOf('https:') == 0)) return url;

    var path;
    if (url[0] == '/')
    {
        path = url;
    }
    else
    {
        path = window.location.pathname;
        if (path.charAt(path.length - 1) != '/') path += '/';
        path += url;
    }
    return window.location.protocol + '//' + window.location.hostname + path;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// util.addImport(url)
//     Add an HTML import to the DOM, returning a promise.
//     It's OK to call this multiple times with the same url.
///////////////////////////////////////////////////////////////////////////////////////////////////

{
    var completeUrls = [];

    util.addImport = function(url)
    {
        // already loaded this import?
        if (completeUrls.indexOf(url) >= 0)
        {
            return $.Deferred().resolve().promise();
        }

        // find the link element for this import
        var urlFull = util.urlNormalize(url);
        var links = $('head > link');
        var link;
        links.each(function(){
            if ((this.rel == 'import') && (this.href == urlFull))
            {
                link = this;
                return false;
            }
        });

        // create the <link> element if necessary, and watch for the 'load' event
        var loaded;
        if (link)
        {
            loaded = util.listenOnce(link, 'load');
        }
        else
        {
            // create a <link> element
            link = document.createElement('link');
            link.rel = 'import';
            link.href = url;

            // on load, update completeUrls
            loaded = util.listenOnce(link, 'load');
            loaded.then(function() { completeUrls.push(url); });

            // append the <link> element to the head
            var head = document.getElementsByTagName('head')[0];
            head.appendChild(link);
        }
        return loaded;
    };
}

由于util.addImport()会返回一个承诺,因此很容易等待加载多个导入:

///////////////////////////////////////////////////////////////////////////////////////////////////
// util.addImports(urls)
//     Add multiple HTML imports to the DOM, returning a promise.
///////////////////////////////////////////////////////////////////////////////////////////////////

util.addImports =
function(urls)
{
    var promises = urls.map(function(url){
        return util.addImport(url);
    });
    return $.when.apply($, promises);
};

E.g。

util.addImports(['//this/component.html', '//that/component.html']).then(function(){
    // go ahead and do stuff that requires the loaded components
});

注入自定义元素并等待与之交互

如果已加载所有必需的组件导入,并按照我的方式注入元素..

var fragment = "<paper-dialog id='mydialog' ...><paper-dialog-scrollable ...>...";
$('body').append(fragment);
var dialog = $('#mydialog').get(0);

..然后在Chrome本身支持网络组件的浏览器上,dialog升级为paper-dialog将同步进行,我们立即与paper-dialog进行互动。但是,在Firefox等需要填充的浏览器上,升级是异步的,立即尝试调用dialog.toggle()将失败:

TypeError: dialog.toggle is not a function

正如我发现的那样,如果我只是让polyfill有机会工作,那么我就有一个升级的元素与之交互:

Polymer.Base.async(function() { dialog.toggle(); }, 1);

在与元素交互之前简单地等待升级似乎在大多数情况下都可以。但是,在paper-dialog-scrollable的情况下,它已升级的事实意味着可以继续切换其父对话框。其原因实际上是我在问题中包含的paper-dialog-scrollable.attached()代码。

又来了:

attached: function()
{
    this.classList.add('no-padding');
    // Set itself to the overlay sizing target
    this.dialogElement.sizingTarget = this.scrollTarget;
    // If the host is sized, fit the scrollable area to the container.
    // Otherwise let it be its natural size.
    requestAnimationFrame(function() {
        if (this.offsetHeight > 0) {
            this.$.scrollable.classList.add('fit');
        }
        this._scroll();
    }.bind(this));
}

当我在升级后过早地尝试toggle()paper-dialog时,&#34;适合&#34; class将应用于div#scrollable容器,这会导致可滚动区域折叠。正如我们在paper-dialog-scrollable.attached()中看到的那样,它立即测试this.offsetHeight > 0,但它实际上使用requestAnimationFrame()等待直到下一次重绘之前执行此测试。当我在升级后仅约1ms调用dialog.toggle()时,这会导致对话框可见,因此可滚动区域的内容具有非零高度。但是,当我在这样切换之前等待100ms:

Polymer.Base.async(function() { dialog.toggle(); }, 100);

..然后requestAnimationFrame()安装的paper-dialog-scrollable.attached()回调有机会运行,由于此时对话框仍未激活,因此发现paper-dialog-scrollable元素不在大小,因此不适用&#34; fit&#34; class(允许可滚动区域为&#34;为其自然大小&#34;)。

当然,我想在之后尽快切换我的对话框决定不适用&#34; fit&#34; class,我可以通过为attached()对象安装自己的paper-dialog-scrollable处理程序来实现。我的attached()处理程序调用普通paper-dialog-scrollable.attached()处理程序,然后执行:

requestAnimationFrame(function() { dialog.toggle(); });

由于requestAnimationFrame()回调是在下一次重绘之前按顺序执行的,因此toggle()之后的对话框可以安全地执行。Polymer.Base.create()为了实现这个解决方案,我使用paper-dialog-scrollable.attached()而不是为整个对话框构建标记片段字符串。让jQuery注入它。使用Polymer.Base.create()会立即为您提供一个升级的元素,这很不错。我还认为该函数比以前操作文本blob的版本更好阅读和维护。

然而,似乎还有另一种解决方案不需要我自己的Polymer.Base.async(function(){ requestAnimationFrame(function(){ dialog.toggle(); }); }, 1); 处理程序:

util.dialog = function(options)
{
    // provide default options
    var defaults =
    {
        imports: [],
        id: 'cms-dialog',
        classes: '',
        title: '',
        content: '',
        scrollable: false,
        dismissButtonLabel: 'Cancel',
        dismissButtonFn: null,
        confirmButtonLabel: 'OK',
        confirmButtonFn: null
    };
    options = $.extend({}, defaults, options);
    options.classes += ' cms-dialog';

    // make a list of required components
    var imports = options.imports;
    var polymerRoot = '//cdn.rawgit.com/download/polymer-cdn/1.2.3/lib/';
    imports.push(polymerRoot + 'neon-animation/animations/scale-up-animation.html');
    imports.push(polymerRoot + 'neon-animation/animations/fade-out-animation.html');
    imports.push(polymerRoot + 'paper-dialog/paper-dialog.html');
    imports.push(polymerRoot + 'paper-dialog-scrollable/paper-dialog-scrollable.html');
    imports.push(polymerRoot + 'paper-button/paper-button.html');

    // load required imports, then create the dialog
    util.addImports(imports).then(function(){
        // nuke any existing dialog
        $('.cms-dialog').remove();

        // create paper-dialog
        var dialogProps = {
            id: options.id,
            modal: true,
            className: options.classes,
            entryAnimation: 'scale-up-animation',
            exitAnimation: 'fade-out-animation'
        };
        var dialog = Polymer.Base.create('paper-dialog', dialogProps);

        // add title
        if (options.title)
        {
            $(dialog).append("<h2 class='title'>" + options.title + '</h2>');
        }

        // add content
        var content;
        if (options.scrollable)
        {
            var scrollableProps = {
                className: 'content'
            };
            content = Polymer.Base.create('paper-dialog-scrollable', scrollableProps);
            content.dialogElement = dialog;
            $(content.scrollTarget).append(options.content);
        }
        else
        {
            content = $("<div class='content'>" + options.content + "</div>").get(0);
        }
        $(dialog).append(content);

        // add buttons
        var dismissButton = '';
        if (options.dismissButtonLabel)
        {
            dismissButton =
                "<paper-button id='dialog-dismiss-button' class='cms-button' dialog-dismiss>" +
                    options.dismissButtonLabel +
                "</paper-button>";
        }
        var confirmButton = '';
        if (options.confirmButtonLabel)
        {
            confirmButton =
                "<paper-button id='dialog-confirm-button' class='cms-button' dialog-confirm>" +
                    options.confirmButtonLabel +
                "</paper-button>";
        }
        $(dialog).append(
            "<div class='buttons'>" +
                dismissButton +
                confirmButton +
            "</div>");

        // activate the dialog
        var toggle = function(){
            // install on-click event handlers
            if (options.dismissButtonFn)
            {
                $('#dialog-dismiss-button').on('click', options.dismissButtonFn);
            }
            if (options.confirmButtonFn)
            {
                $('#dialog-confirm-button').on('click', options.confirmButtonFn);
            }

            // run on-ready callback (if given)
            if (options.onReady) options.onReady();

            // bring up the dialog
            dialog.toggle();
        };

        // toggle when it's safe
        var attachedTarget = options.scrollable ? content : dialog;
        var attachedOrig = attachedTarget.attached;
        attachedTarget.attached = function() {
            if (attachedOrig) attachedOrig.apply(attachedTarget, arguments);
            requestAnimationFrame(toggle);
        };

        // toggle when it's safe (this also appears to work)
        //Polymer.Base.async(function() { requestAnimationFrame(toggle); }, 1);

        // add the dialog to the document
        document.body.appendChild(dialog);
    });
};

也许这种解决方案更好,因为它更通用,但我对它始终有效的信心不足。

新对话框创建代码

WebComponentsReady

结束思路

尽管Polymer.Base.create()事件不是我的答案,但我仍然发现Ryan White的演示很有用,因为它包含使用attached()和覆盖{{1}的示例处理程序,帮助我深入研究问题以找到解决方案。

尽管修复很好,但我觉得这里有一个缺少的事件,当一个注入的自定义元素及其子元素已经升级并稳定下来时,它会发出信号,因此它和#39;可以安全地进行互动。

答案 1 :(得分:0)

在附加可滚动纸张对话框之前等待WebComponentsReady事件。

在chrome中有本机html导入和自定义元素,因此您不需要等待此事件,因为该过程是同步的,但在firefox中html导入不是本机的,而是使用polyfill添加。

window.addEventListener('WebComponentsReady', function(e) {
  var dialogScrollable = Polymer.Base.create('paper-dialog-scrollable');
  var dialog = Polymer.Base.create('paper-dialog', { id: 'mydialog', });
  dialog.appendChild(dialogScrollable);
  document.body.appendChild(dialog);
});

或者更类似于您提供的示例

window.addEventListener('WebComponentsReady', function(e) {
  var fragment = "<paper-dialog id='mydialog' ...><paper-dialog-scrollable ...>...";
  $('body').append(fragment);
  var dialog = $('#mydialog').get(0);
});

这是指向jsbin demo的链接,显示WebComponentsReady事件已触发,并且对话框已切换。 这里也显示了dialogScrollable的问题,但这应该是关于堆栈溢出的一个单独的问题,因为它与“我怎么知道注入的对话框何时可以切换?”无直接关系。