65.9K
CodeProject 正在变化。 阅读更多。
Home

MyFamily.Show:一个简单的JQuery Mobile家族树:第2部分

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (1投票)

2015年3月16日

CPOL

14分钟阅读

viewsIcon

18629

downloadIcon

130

演示了表单背景图、滑动面板、自定义过渡效果、d3动画树、SVG导出为图像、类似iOS的启动台、扁平数据到树的转换、关注点分离和pagecontainer。

引言

本文是我上一篇文章的延续,上一篇文章介绍了如何创建一个家族树应用程序,在此处有详细介绍。我们之前的家族树应用程序只能添加、更新、删除和查看家庭成员。我们想要更多。我们希望在应用程序启动台中有一个背景图像,我们希望以树状图的形式显示家族树,我们希望能够将家族树导出为图像。如果有一个左侧面板来方便应用程序导航,那也会很棒。启动台上的基于列表视图的导航在小设备上显示效果不佳,能否将其更改为更流畅的用户界面?在这个版本中,我们将尝试进行这些添加,从而整体上增强家族树应用程序。然而,这些增强功能可以应用于任何可以创建的JQuery Mobile应用程序。

我先用屏幕截图来介绍一下新增强的功能。我开始喜欢使用ripple模拟器来做这个。

下载 MyFamily.Show.zip

图1:带有背景图像和类似iOS的导航按钮的启动台。

我为每个锚点添加了一些CSS,以实现类似iOS的导航启动台。虽然按钮不像我以前基于列表视图的启动台那样能随屏幕缩放,但我发现这些按钮在所有我测试过的设备上都组织得很好,而且赏心悦目。这里的背景图像是通过样式添加到页面定义的。这张图片在方向改变时会自行调整大小,这是一个不错的发现。

图2:滑动面板

对于“添加和编辑家庭成员”屏幕,我们在表单的左侧添加了一个滑动面板,面板内包含带有按钮的列表视图。这些被证明易于使用,并提高了应用程序整体的导航性。让我解释一下这些列表视图按钮:

1. 新建 - 进入添加新成员的页面

2. 关系 > 父亲 - 显示一个d3树状图,包含父亲和子女的关系

3. 关系 > 母亲 - 显示一个d3树状图,包含母亲和子女的关系

4. 报告 - 进入报告屏幕。这是一个可导出的表格。我在这篇文章中开始讨论表格及其导出。此应用程序与该文章一样,也是一个“一次编写,随处运行”的应用程序。

滑动面板可以位于页面的左侧或右侧。它还具有显示方式的属性,如“显示”、“推送”和“覆盖”。有关面板的更多信息,请访问JQuery Mobile网站的此链接

图3:自定义过渡效果

应用程序中添加了一个名为animation.css的新CSS文件。通过它,可以为过渡效果添加自己的自定义动画。这与JQuery Mobile提供的动画在此处是分开的。新的MyFamily.Show使用了customFade过渡效果。我在我的app.js文件中声明了一个名为pgtransition的全局变量,如下所示:

var pgtransition = 'customFade';

过渡效果的枚举包括 customRotate, customBounce, customFade, customFlip和 customCrazy。我从这里获取了动画。

注意:如果您更改了过渡效果,也请更新index.html文件。继续,将'customFade'替换为'customCrazy',是不是很棒?

图4:d3动画树

经过多次尝试和错误,这个终于奏效了。我的数据是扁平的,因此需要一些技巧才能将其转换成树形结构,以便最终与此配合使用,并实现导出为图像的功能。此树是可缩放、可点击、可折叠、可拖动的等等。显示时,树始终居于视图区域的中心,这意味着#节点将位于屏幕中央,您需要移动树形图才能像上面那样查看它。虽然不是一个拥挤的区域,但如果它能显示在屏幕的左上角就好了。设备的视区宽度和高度也是其他决定因素。

图5:导出为图像

使d3树可导出的诀窍是确保您的CSS定义包含在每个节点或链接的JavaScript定义中,而不仅仅是在页面的<style></style>中。我喜欢导出的png是透明的,但是它只导出可见的视口,因此对于许多树来说,需要放大屏幕。

点击导出将激活树的导出,该导出将自动下载。

我打开导出的图像并将其粘贴到MS Word中(上图)。

背景

在我深入本文的核心之前,让我先回顾一下……

1. 我已经在这里讨论了为JQuery Mobile创建CRUD应用程序。MyFamily.Show仍然将数据存储在LocalStorage中。

2. 我还在这里讨论了报告和将表格导出到Excel。这涉及到从LocalStorage读取数据并创建一个带有可切换列的表格来显示数据。

3. 我在这里开始了MyFamily.Show的开发,这是对该文章的改版。它演示了将表单输入拆分成家族树输入的各个部分。为此,我们使用了一个带有“个人信息”、“联系方式”、“关系”和“死亡详情”的标题导航栏。我们演示了如何通过单击第一个导航按钮来使其在屏幕打开时处于活动状态。我们还使用单个页面,为不同的信息部分使用一个div,而不是像有时那样使用不同的HTML文件。

我们还演示了如何在ListView项目上显示更多信息,例如计数气泡、描述等。包括按全名对本地存储中的JSON数据进行排序。这包括显示如何使用关系从存储的信息加载下拉列表,即从当前存储的家庭成员列表中加载父亲和母亲。注意我将对此进行调整,以在母亲中添加女性,在父亲中添加男性。

那篇文章还演示了一个日期选择器小部件,该小部件具有用于日期选择的滑动日期。

这个版本发生了很多事情。例如,我们通过将index.html文件中的JavaScript代码移到一个单独的文件中来分离关注点。这使得代码更容易维护,而不会影响视图。我们还添加了一个用于页面过渡的全局变量,以便于代码维护。我们还修复了之前代码中发现的一些错误。我们还以最小的级别运行了代码JSLint,并在必要时实施其建议。这与NotePad++插件配合得很好。Fold也有助于缩进代码。

使用代码

1. 创建带有背景图像的新启动台

HTML定义

<div data-theme="a" style="background-image:url('b1.jpg');background-position:center center;background-repeat:no-repeat;background-size:cover;background-size: 100% 100%;" id="pgMenu" data-role="page">
        
        <header id="pgMenuHdr" data-role="header" data-position="fixed">
            <h1>MyFamily.Show</h1>
        </header>
        <div id="pgMenucontent" data-role="content">
            <div style="width:130px;height:96px;margin:15px 4px 10px 4px;float:left;padding-bottom:35px;text-align:center;">
                <a id="sbFamilyMember" data-transition="customFade" href="#pgFamilyMember"><img height="96" width="96" title="Family Members" src="apps80.png" alt="Maintain Family Members"></img>
                <strong>Family Members</strong></a>
            </div>
            <div style="width:130px;height:96px;margin:15px 4px 10px 4px;float:left;padding-bottom:35px;text-align:center;">
                <a id="sbReports" data-transition="customFade" href="#pgReports"><img height="96" width="96" title="Reports" src="apps80.png" alt="Access Reports"></img>
                <strong>Reports</strong></a>
            </div>
        </div>
        
        <footer id="pgMenuFtr" data-role="footer" data-position="fixed">
            <h1>Powered by JQM.Show © Anele Mbanga 2015</h1>
        </footer>
</div>

启动台现在有一个背景图像。任何您想要有背景图像的页面都可以遵循相同的定义来获得它。这对于不同页面有不同的图像非常有用,因为可以通过CSS全局方式做到这一点。

<div data-theme="a" style="background-image:url('b1.jpg');background-position:center center;background-repeat:no-repeat;background-size:cover;background-size: 100% 100%;" id="pgMenu" data-role="page">

这段HTML定义将背景图像渲染到启动台。文件名是b1.jpg。它居中显示,不重复,是封面图像,尺寸为100%。但是,在打开启动台时,有一个一秒或更短的延迟,因为图像会在方向改变或重新打开页面时自行调整大小。

2. 定义启动台的类似iOS的按钮

<div style="width:130px;height:96px;margin:15px 4px 10px 4px;float:left;padding-bottom:35px;text-align:center;">
                <a id="sbFamilyMember" data-transition="customFade" href="#pgFamilyMember"><img height="96" width="96" title="Family Members" src="apps80.png" alt="Maintain Family Members"></img>
                <strong>Family Members</strong></a>
            </div>

从页面上的代码可以看出,显示在启动台上的每个按钮都定义为一个锚点,其中一个图像位于div中。每个div的高度和宽度设置为96,文本居中。像往常一样,我们在锚点和锚点定义中定义每个过渡效果,以便使其可点击。这里没有JavaScript链接到点击事件,因为每个按钮都有一个href。我从gizmo freeware reviews那里得到了这个想法。

style="text-decoration: none; color: #FFFFFF;"

将移除锚点的下划线,并将链接显示为白色,请参见下面的更新后的启动台。

图:更新后的启动台

将此与图1中的链接进行比较。

3. 创建侧边面板

为了改进导航,在家庭成员列表、添加和更新屏幕上添加了侧边面板。建议在页面上的面板在头部定义之前添加。

<div data-position="left" data-display="reveal" data-position-fixed="true" id="pgFamilyMemberPnl" data-role="panel">
                    <ul data-role="listview" id="pgFamilyMemberPnlLV">
                        <li ><a data-transition="customFade" href="#pgAddFamilyMember">New</a></li>
                        <li ><a data-transition="customFade" href="#pgFamilyMemberMindFather">Relationships > Father</a></li>
                        <li ><a data-transition="customFade" href="#pgFamilyMemberMindMother">Relationships > Mother</a></li>
                        <li ><a data-transition="customFade" href="#pgRptFamilyMember">Report</a></li>
                        <li ><a data-transition="customFade" href="#pgMenu">Back</a></li>
                    </ul>
</div>

我们定义的面板是“显示”面板,这意味着它在选中菜单按钮之前是隐藏的。然后,菜单按钮在此情况下充当切换按钮,用于打开和关闭面板以显示带有按钮的列表视图。在每个面板内,都有一个列表视图,其中包含用于添加新成员、查看关系、访问报告和返回的按钮。报告选项是受控的,因为可以从任何屏幕访问报告,因此要从报告返回到上一个屏幕,必须编写一些代码,如下所示:

$('#pgRptFamilyMemberBack').on('click', function (e) {
                e.preventDefault();
                e.stopImmediatePropagation();
                var pgFrom = $('#pgRptFamilyMemberBack').data('from');
                switch (pgFrom) {
                    case "pgAddFamilyMember":
                    $.mobile.changePage('#pgFamilyMember', {transition: pgtransition});
                    break;
                    case "pgEditFamilyMember":
                    $.mobile.changePage('#pgFamilyMember', {transition: pgtransition});
                    break;
                    case "pgFamilyMember":
                    $.mobile.changePage('#pgFamilyMember', {transition: pgtransition});
                    break;
                    default:
                    // go back to the listing screen
                    $.mobile.changePage('#pgReports', {transition: pgtransition});
                }
});

我需要告诉报告屏幕返回到它起源的屏幕,因此,当每个页面显示时,我都会将源存储在报告屏幕的后退按钮的数据属性中。

4. 使用d3图表框架创建家族树

在我们的HTML定义中,我们添加了d3引用和CSS脚本。d3.min.js脚本可以从上面提供的d3网站链接下载。d3tree.js是我从该网站获取的一个示例,用于使用扁平数据生成d3树。他们提供了许多示例来从数据生成任何类型的图表。

<script src="d3.min.js" type="text/javascript"></script>
<script src="d3tree.js" type="text/javascript"></script>

我们还添加的CSS

.node { cursor: pointer; }
.overlay{ background-color:#EEE; }
.node circle { fill: #fff; stroke: steelblue; stroke-width: 1.5px; }
.node text { font-size:10px; font-family:sans-serif; }
.link { fill: none; stroke: #ccc; stroke-width: 1.5px; }
.templink { fill: none; stroke: red; stroke-width: 3px; }
.ghostCircle.show { display:block; }
.ghostCircle, .activeDrag .ghostCircle { display: none; }

为了导出,每个不同的元素都必须更新,以便将这些样式包含在它们在JavaScript中的实际定义中。

请参阅下面的app.FamilyMemberMindMapFather函数,了解信息如何从我们的后端读取并转换为父亲信息的树形结构:

HTML定义 - 父亲和母亲关系

显示每个父亲和母亲的家族树的页面除了“返回”和“导出”按钮以及一个容纳d3树元素的div之外,没有其他基本内容。这是父亲关系屏幕。母亲的屏幕几乎相同。我决定分开显示父母双方的关系,因为我想展示关系和父母的来源。例如,女性以她们的娘家姓名录入。因此,父亲的关系树将显示任何有父亲或没有父亲的人,无论他们是女性还是男性。女儿的孩子不会显示,尽管女儿与父亲相连或未给出父亲。母亲的关系也是如此。下图描绘了这一点。

<div data-theme="a" id="pgFamilyMemberMindFather" data-role="page">                
                <header id="pgFamilyMemberMindFatherHdr" data-role="header" data-position="fixed">
                    <h1>MyFamily.Show > Relationships > Father</h1>
                    <a data-role="button" id="pgFamilyMemberMindFatherBackFather" href="#pgFamilyMember" data-icon="arrow-l" data-transition="customFade" class="ui-btn-left">Back</a>
                    <a data-role="button" id="FamilyMemberMindExportFather" data-icon="camera" class="ui-btn-right">Export</a>
                </header>
                <div id="FamilyMemberMindFather">
                </div>            

</div>

图:父亲

如果看本文开头图5,Bulelwa Mbanga(我姐姐)有三个孩子,但在这里孩子没有显示,但她与她的父亲Mbulelo Mbanga相连。顺便说一句,Mbulelo是我父亲,愿他安息。

生成此树和其他树是通过d3tree.js文件中的代码完成的,当关系屏幕显示时。但是,我们需要从本地存储中读取和生成树的扁平记录。这是这样做的。

我们获取所有家庭成员的信息,并为每个成员获取父亲和该家庭成员的全名。然后,我们将父节点链接到子节点,然后使用data.reduce生成一个父子关系树,以便d3绘制。为此,我们需要有一个null父节点,因此我们在脚本中也定义了它。

app.FamilyMemberMindMapFather = function () {
            //create an array for flat records table with parent and child name
            var data = [], rec, n;
            // get Family Member records.
            var FamilyMemberObj = app.getFamilyMember();
            for (n in FamilyMemberObj) {
                //get the record parent and child relationship
                var FamilyMemberRec = FamilyMemberObj[n];
                var parentn = FamilyMemberRec.Father;
                var childn = FamilyMemberRec.FullName;
                if (parentn == 'null') {
                    parentn = '#';
                };
                // the child relationship used parent name and child name
                rec = {};
                rec.name = childn;
                rec.parent = parentn;
                data.push(rec);
            };
            //add the parent node, make it # for sorting purposes
            rec = {};
            rec.name = '#';
            rec.parent = null;
            data.push(rec);
            //create a name based map for the nodes
            var dataMap = data.reduce(function(map, node) {
                map[node.name] = node;
                return map;
            }, {});
            //iteratively add each child to its parent
            var treeData = [];
            data.forEach(function(node) {
                // add to parent
                var parent = dataMap[node.parent];
                if (parent) {
                    // create child array if it doesn't exist
                    (parent.children || (parent.children = []))
                    // add node to child array
                    .push(node);
                    } else {
                    // parent is null or missing
                    treeData.push(node);
                }
            });
            //draw the d3 tree
            DrawTree(treeData, '#FamilyMemberMindFather');
        };

根据treeData变量,树需要有一个null父节点才能绘制。DrawTree函数是组成d3tree.js的代码。要理解该文件的内容,需要对d3函数有很好的理解。这是我探索的示例之一,我大部分文件内容来自d3网站,并对其进行了调整,使其在以下方面能够工作:

$(divID).html = '';
d3.select('svg').remove();

上面的几行代码清除了d3地图的div容器以及d3 svg。如果没有第二行,即使我刷新或通过单击返回并再次打开页面,树也会在现有地图之后重新绘制。

var viewerWidth = (window.innerWidth > 0) ? window.innerWidth : screen.width;
var viewerHeight = (window.innerHeight > 0) ? window.innerHeight : screen.height;

这是为了确定设备的视区。虽然可以使宽度适应100%,但高度一直很棘手。

我还对visit函数添加了[0],因为我们是从已转换为树的扁平记录中获取数据的,请参见上面的MindMapFather。

visit(treeData[0], function(d) {...

最后……

root = treeData[0];

在d3tree.js文件的第527行。

您会注意到,当您运行该应用程序时,树会在家庭关系页面加载后显示。我在这里对pagecontainer进行了调整。它取代了JQuery Mobile的下一个版本(1.5及以上版本)中将不再可用的一些方法,例如pageshow等。

$(document).on('pagecontainershow', function (e, ui) {
                var pageId = $(':mobile-pagecontainer').pagecontainer('getActivePage').attr('id');
                switch (pageId) {
                    case 'pgFamilyMemberMindFather':
                    app.FamilyMemberMindMapFather();
                    break;
                    case 'pgFamilyMemberMindMother':
                    app.FamilyMemberMindMapMother();
                    break;
                }
            });

在这里,通过getActivePage获取正在显示的页面的id,然后根据选定的页面执行树的构建。

5. 导出家族树

将家族树导出为图像并不像我希望的那样容易。尽管我搜索了很多来使其工作,但它就是不听使唤,但最终,它奏效了,通过遵循我找到的一个示例。我还必须调整d3tree.js文件中的CSS属性,以确保它完美工作。

// export button click on records mindmap page
            $('#FamilyMemberMindExportFather').on('click', function (e) {
                e.preventDefault();
                e.stopImmediatePropagation();
                d3.selectAll('svg').attr('version', '1.1');
                d3.selectAll('svg').attr('xmlns', 'http://www.w3.org/2000/svg');
                var html = d3.select('svg').node().parentNode.innerHTML;
                var imgsrc = 'data:image/svg+xml;base64,' + btoa(html);
                var img = '<img src=' + imgsrc + '>';
                //create a canvas to store the image
                var canvas = document.createElement('canvas');
                canvas.width = (window.innerWidth > 0) ? window.innerWidth : screen.width;
                canvas.height = (window.innerHeight > 0) ? window.innerHeight : screen.height;
                var context = canvas.getContext('2d');
                var image = new Image;
                image.src = imgsrc;
                image.onload = function() {
                    context.drawImage(image, 0, 0);
                    var canvasdata = canvas.toDataURL('image/png');
                    var pngimg = '<img src=' + canvasdata + '>';
                    var a = document.createElement('a');
                    a.download = 'Father.png';
                    a.href = canvasdata;
                    a.click();
                };
            });

当用户点击“母亲”或“父亲”关系屏幕上的导出按钮时,上面的代码会为相应的按钮执行。我遵循了互联网上的建议来使其工作,但不幸的是,它不适用于将部分代码放入锚点的href中,但我猜想可以将它放入一个js文件中,然后调用一个单一的方法。

这会读取d3 svg的HTML,然后运行btoa将其转换为base64对象。然后从该对象创建一个图像。然后创建一个canvas,并将图像加载到其中。我们确保canvas的宽度和高度能够与设备屏幕平衡,以确保所有树的属性都已导出。图像被加载到canvas中,并在文档中添加一个链接来下载创建的canvas数据,最后通过click方法执行自动下载。

图像的名称被设置为我们感兴趣的关系名称。这样就得到了图6中所示的透明图像,该图像被粘贴到MS Word中。

关注点

关于家庭的第一个文章只是提供了捕获家庭成员的框架,触及了应用程序的表面。通过这次更新,您可以为启动台添加背景图像,能够以树状图的形式查看父子关系,并且还能够将家族树导出为图像。此外,还添加了一个侧边面板来改进屏幕导航。

对我来说,d3的发现是一件惊奇的事情。我将进一步探索如何使用它。我喜欢发现orgchart来处理组织结构图,并将进一步探索它。这对我正在使用的RAD工具JQM.Show帮助很大,我计划使用这个框架开发一个庞大的Web应用程序,涵盖我目前为止所写的所有内容。不过,我仍然需要进行一些数据验证,因为数据完整性将是未来至关重要的。

信息在网上分散的方式仍然是一个挑战,因为大多数事情都需要大量的研究才能找到它们,但找到它们并使其正常工作证明是一段值得的旅程。

对于pagecontainer以及JQuery Mobile在这方面的向后兼容性不足,存在很多不便。我想随着时间的推移,我们会看到会发生什么。

事实证明,实现面板以方便导航是一项有价值的工作。不过,我在主题方面仍有一些挑战。自定义动画是一个不错的发现,customCrazy非常棒,但我觉得它对眼睛来说很有挑战性。我选择了新的淡入淡出效果。

我想知道这一切在Angular中会如何工作……#自言自语。

© . All rights reserved.