在 SpreadJS 中实现工作表导航:从数据获取到交互设计

前言

Microsoft Excel工作表导航功能能够提取工作簿内每个工作表中的表格(Table)、数据透视表(PivotTable)、图表(Chart)、图片、形状(Shape)等元素,并将其摘要信息以树的形式罗列显示,当点击某元素信息时,工作簿跳转至该元素所在工作表,并聚焦在该元素上,如图1所示。

img

(图1. Excel 工作表导航)

那么SpreadJS是否支持自定义开发实现该功能呢?撰写本文的目的正是为了介绍在SpreadJS中如何利用现有功能或产品设计自定义开发实现该功能,因此答案显然是“支持”。接下来,我们一起走进工作表导航自定义实现方案。

我们来分析下工作表导航功能的要素,参考Microsoft Excel官网对该功能的介绍,包含以下需求项:

  1. 罗列工作簿内每个工作表中的元素。
  2. 点击某元素可以跳转至相关工作表并聚焦该元素。
  3. 导航面板支持模糊搜索匹配工作表元素。

首先,需要确定SpreadJS是否能够获取工作表中的上述元素信息。SpreadJS对上述元素均抽象了独立的管理器,比如TableManager管理Table实例,PivotTableManager管理PivotTable实例,ChartCollection管理Chart实例、ShapeCollection管理Shape实例,如图2所示。由于17.0.0版本优化了图表Chart实现,图表不再是基于DOM元素的浮动对象,而是作为一种形状存在,所以图表Chart实例也可由ShapeCollection管理。以上,便可确定,通过各类型元素的管理器便可获取工作表中对应类型的各元素。

img

(图2. 工作表元素管理)

其次,各类型元素的管理器可以以元素名称为检索依据快速获取元素实例。以Table为例,TableManager:findByName()根据Table名称获取Table实例,Table实例中包含了Table所在单元格区域,Worksheet:setSelection()便可选中区域,聚焦该区域。PivotTable同理,PivotTable整体布局在工作表中,占据一片连续的单元格区域,通过PivotTable:getRange()即可得到数据透视表所在单元格区域。形状Shape实体悬浮在工作表图层上一层,并不占据单元格,便无法通过Worksheet:setSelection()聚焦。然而,ShapeBase:isSelected()可聚焦选中调用者形状实例

至此,SpreadJS便提供了完整的工作表元素管理能力,凭借此能力可以增删和访问元素。如何以树的形式布局这些元素,呈现一个清晰直观的树结构,以供用户快速了解工作表以及工作簿元素组成全貌呢?此外,如何同时提供交互能力呢?

我们来看一下Microsoft Excel中工作表导航的布局结构,如图3所示,工作表视图区域右侧显示一设置面板,面板内自上而下分为两部分,包括搜索输入栏和工作表元素信息列表树。

img

(图3. 工作表导航面板)

我们知道SpreadJS中的各个设置面板均为IDialogTemplate实例,且SpreadJS公开了IDialogTemplate结构标准以支持自定义扩展。在此,我们可以参考AtmoicComponentBase标准自定义原子组件,这便是工作表元素列表树的布局容器,在容器内自上而下依次创建DOM元素封装每一级树节点,以形成工作表树元素列表树。树包括两级节点,父节点显示工作表名称,代表一个工作表实例;相应地,子节点即工作表内的诸多元素名称。在定义工作表元素布局的同时,定义搜索框的模糊搜索逻辑,展开父节点以显示符合搜索关键字的匹配项,当键入或删除新字符后导致当前匹配项中部分项不再匹配时,隐藏相关项并折叠无匹配项的父节点。

工作表导航面板既已实现,再考虑该面板显示与否的控制要求。Microsoft Excel的工作表导航功能位于工具栏–>视图–>显示–>导航,参考此路径,在工具栏–>视图选项卡中新增自定义功能按钮,设置图标以诉诸该图标为工作表导航功能按钮,如图4所示。

img

(图4. 工作表导航按钮)

对该自定义功能按钮定义相应的命令,在该按钮被点击时触发,通过切换工作表导航面板对应的命令的visibleContext属性值控制面板可见性。

至此,便利用SpreadJS的元素管理能力自定义实现了与Microsoft Excel相似的工作表导航功能,如图5所示。附上demo,以供参考。

img

(图5. SpreadJS 工作表导航)

工作表导航demo(Html)

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>工作表导航</title>
    <meta name="spreadjs culture" content="zh-cn"/>
    <link href="http://cdn.grapecity.com/spreadjs/hosted/css/gc.spread.sheets.excel2013white.18.1.0.css"
          rel="stylesheet" type="text/css"/>
    <link href="http://cdn.grapecity.com/spreadjs/hosted/css/gc.spread.sheets.designer.18.1.0.min.css" rel="stylesheet"
          type="text/css"/>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/gc.spread.sheets.all.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.shapes.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.charts.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.slicers.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.print.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.barcode.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.pdf.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.pivot.pivottables.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.tablesheet.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.ganttesheet.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.report.reportsheet.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.formulapanel.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/plugins/gc.spread.sheets.io.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/interop/gc.spread.excelio.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/resources/zh/gc.spread.sheets.resources.zh.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/gc.spread.sheets.designer.resource.cn.18.1.0.min.js"></script>
    <script src="http://cdn.grapecity.com/spreadjs/hosted/scripts/gc.spread.sheets.designer.all.18.1.0.min.js"></script>

    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">

    <style>
        .sample-tutorial {
            position: relative;
            height: 100%;
            overflow: hidden;
        }
        body {
            position: absolute;
            margin: 0;
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
        }

        .gc-across-sheet-ftb {
            z-index: -1 !important;
        }

        .gc-across-sheet-func-popup {
            z-index: -1 !important;
        }

        .sheet-name-container {
            width: 80%;
            height: 50px;
            display: block;
            margin-top: 5px;
            background-color: white;
        }

                .tree-container {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            padding: 16px;
            margin-top: 24px;
        }
        
        .tree-node {
            margin-bottom: 8px;
            border-radius: 4px;
            overflow: hidden;
        }
        
        .node-header {
            display: flex;
            align-items: center;
            padding: 10px 12px;
            background-color: #f1f5f9;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        
        .node-header:hover {
            background-color: #e2e8f0;
        }
        
        .toggle-btn {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            background-color: #dbeafe;
            color: #3b82f6;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 12px;
            transition: transform 0.3s;
            border: none;
            cursor: pointer;
        }
        
        .node-header.expanded .toggle-btn {
            transform: rotate(45deg);
        }
        
        .node-name {
            font-weight: 500;
        }
        
        .node-children {
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease-out;
            padding-left: 36px;
            position: relative;
        }
        
        .node-children::before {
            content: '';
            position: absolute;
            left: 12px;
            top: 0;
            bottom: 0;
            width: 2px;
            background-color: #e2e8f0;
        }
        
        .child-item {
            padding: 8px 12px;
            margin: 4px 0;
            background-color: #f8fafc;
            border-radius: 4px;
            transition: background-color 0.2s;
        }
        
        .child-item:hover {
            background-color: #f1f5f9;
        }
        
        .child-item.selected {
            background-color: #e0f2fe;
            border-left: 3px solid #3b82f6;
        }
        
        .empty-message {
            padding: 8px 12px;
            color: #64748b;
            font-style: italic;
        }
    </style>

</head>

<body>
    <div class="sample-tutorial">
        <div class="demo-options">
            <div class="option-row">
                <div id="formulaBar" contenteditable="true" spellcheck="false"
                    style="font-family: Calibri;border: 1px solid #808080;width:100%;box-sizing: border-box;"></div>
            </div>
        </div>
        <div id="ss" style="width:100%; height: calc(100% - 20px)"></div>
    </div>
</body>

<script>

window.onload = function () {
    var designer = new GC.Spread.Sheets.Designer.Designer(document.getElementById('ss'));
    var spread = designer.getWorkbook();
    spread.setSheetCount(4);

    initSpread(spread);

    initNavigator(designer);
};

function initSpread(spread) {
    var sheet1 = spread.getSheet(0);
    sheet1.setArray(0, 0, [
        ['名称', '数值'], 
        ['A', 1], 
        ['B', 2], 
        ['C', 3], 
        ['D', 4]
    ]);
    //  数据透视表
    var pivotTable = sheet1.pivotTables.add('pivottable1', '=Sheet1!A1:B3', 5, 0, GC.Spread.Pivot.PivotTableLayoutType.outline, GC.Spread.Pivot.PivotTableThemes.medium2);
    pivotTable.add('名称', '名称', GC.Spread.Pivot.PivotTableFieldType.rowField);
    pivotTable.add('数值', '数值', GC.Spread.Pivot.PivotTableFieldType.valueField, GC.Pivot.SubtotalType.sum);

    var sheet2 = spread.getSheet(1);
    sheet2.setArray(0, 0, [
        ['属性1', '属性2', '属性3', '属性4'], 
        [1, 2, 3, 4], 
        [2, 3, 4, 5]
    ]);
    //  表格Table
    sheet2.tables.add('table', 0, 0, 3, 4);

    sheet2.setArray(7, 0, [
        ['属性1', '属性2', '属性3', '属性4'], 
        [1, 2, 3, 4], 
        [2, 3, 4, 5]
    ]);
    //  表格Table
    sheet2.tables.add('table2', 7, 0, 4, 4);

    var sheet3 = spread.getSheet(2);
    sheet3.setArray(0, 0, [
        [1, 2, 3, 4], 
        [1, 2, 3, 4]
    ]);
    //  区域快照
    var cameraShape = sheet3.shapes.addCameraShape('CameraShape', 'Sheet3!A1:D2', 240, 200);
    //  图片
    var imgShape = sheet3.shapes.addPictureShape('pic1', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAWtJREFUSEvt1r1KHWEQxvGfH50Saz+iuYWINyGx00sIplAD6QzpYikWohC0tLcSEu9BorcgMailiqQy6Bl4V45HX9w9RzzNvu3OzDPPf2f23R5dOj1d0lULvxr5GnWB+gO2MNoh+7+Yx6/WOjnUJxjrULRIj1rjZYVvU+AQrtpsYADXKfeRwZzjS7xpYDrFZ+xWFJ/BJibwD9HEg5MTHsE65lL0Hhbx55kGYiYibzbF/cQCjssKNw9ZdP4udf4da7hpKdSLjw1nq4nUGZaxk2s05/gHvuIiYQrBQN6PwzSpv1PRybQBU/iPyP2WZmMQK/hS1nEM13mj4HBTwntsoxAIEhG31CjehyN8wkFTTryatzy+E3KOi6lufd6KNDRieHKvIFcneztlE5Kb2PGN5CQcx64+dV5cuOx21cL3pKoOV1nERVxl1F27JKbTznZ6Q4WBuBb3y35AqiKtHF//+lRG1m5CjbpdcpXz7gDWz0QfNVlZrQAAAABJRU5ErkJggg==', 100, 100, 200, 200);
    //  普通形状
    var baseShape = sheet3.shapes.add("heartShape", GC.Spread.Sheets.Shapes.AutoShapeType.heart, 100, 50, 100, 150);

    var sheet4 = spread.getSheet(3);
    sheet4.setArray(0, 0, [
        ['名称', '数值'], 
        ['A', 1], 
        ['B', 2], 
        ['C', 3], 
        ['D', 4]
    ]);
    //  图表
    var chart = sheet4.charts.add('名称-数值-柱状图', GC.Spread.Sheets.Charts.ChartType.columnClustered, 0, 100, 400, 300, 'A1:B5');
}

/**
 * 初始化sheet导航功能及其相关元素(工具栏功能按钮、导航面板)
*/
function initNavigator(designer) {
    var config = GC.Spread.Sheets.Designer.DefaultConfig;

    GC.Spread.Sheets.Designer.Designer.RegisterComponent('MyComponent', MyTemplateComponent);

    //  定义命令控制sheet导航面板显示与否
    config.commandMap = {
        SheetNaviCmd: {
            title: 'SheetNavi', 
            text: 'sheet导航', 
            iconClass: "ribbon-button-zoom", 
            bigButton: 'false', 
            commandName: 'SheetNavi', 
            execute: async (context, propertyName) => {
                if (context.getData('CData')) {
                    context.setData('CData', false);
                } else {
                    context.setData('CData', true);
                }
            }
        }
    }

    //  在工具栏“视图”选项卡添加sheet导航功能按钮
    config.ribbon[5].buttonGroups.unshift({
        'label': '自定义sheet导航', 
        'thumbnailClass': 'ribbon-thumbnail-chart-alt-text',
        'commandGroup': {
            'children': [
                {
                    'direction': 'vertical', 
                    'commands': [
                        'SheetNavi'  //  与命令的commandName相同
                    ]
                }
            ]
        }
    });

    //  定义sheet导航面板
    var searchTemplate = {
        templateName: 'searchTemplate', 
        content: [
            {
                type: 'TextBlock', 
                style: 'margin:10px;font-size:20px;font-weight:lighter;color:#08892c', 
                text: '导航'
            }, 
            {
                type: 'Container', 
                children: [
                    {
                        type: 'Container', 
                        margin: '10px 5px', 
                        children: [
                            {
                                type: 'MyComponent',  //  自定义Template控件,提供工作表导航信息布局能力
                                bindingPath: 'myComponentProcess'
                            }
                        ]
                    }
                ]
            }
        ]
    };

    GC.Spread.Sheets.Designer.registerTemplate('searchTemplate', searchTemplate);


    //  导航面板命令
    var sidePanelAuditCommands = {
        searchPanelCommand: {
            comamndName: 'searchPanelCommand', 
            enableContext: 'AllowEditObject', 
            visibleContext: 'CData'  //  导航面包显示控制属性
        }
    };

    //  Template配置
    var searchPanelConfig = {
        position: 'right', 
        width: '315px', 
        command: 'searchPanelCommand', 
        uiTemplate: 'searchTemplate', 
        showCloseButton: true
    };

    Object.assign(config.commandMap, sidePanelAuditCommands);
    config.sidePanels.push(searchPanelConfig);

    designer.setConfig(config);
}

function MyTemplateComponent() {
    console.log('MyTemplateComponent, constructor');
    GC.Spread.Sheets.Designer.AtomicComponentBase.call(this, ...arguments);
    this.inputContent = 0;
}

MyTemplateComponent.prototype = new GC.Spread.Sheets.Designer.AtomicComponentBase();
MyTemplateComponent.prototype.getTemplate = function(template) {
    console.log('MyTemplateComponent, getTemplate: ', template);
    var innerHTML = '<div id=\"sheetContainer\" class=\"sheet-info-container\"></div>';
    return innerHTML;
}

MyTemplateComponent.prototype.onMounted = function(host, options) {
    console.log('MyTemplateComponent, onMounted, host: ', host, '\noptions: ', options);
    var hostDesigner = GC.Spread.Sheets.Designer.findControl('ss');
    var hostSpread = hostDesigner.getWorkbook();
    console.log('sheet count: ', hostSpread.getSheetCount());

    var sheetNameContainer = document.createElement('div');
    sheetNameContainer.className = 'tree-container';

    // 创建搜索输入框
    var searchInput = document.createElement('input');
    searchInput.type = 'text';
    searchInput.placeholder = '搜索表格、图表等...';
    searchInput.style.cssText = `
        display: block;
        margin-bottom: 16px;
        width: 100%;
        padding: 8px;
        border: 1px solid #ddd;
        border-radius: 4px;
        box-sizing: border-box;
    `;
    sheetNameContainer.appendChild(searchInput);

    // 生成树形结构
    for (var i = 0; i < hostSpread.getSheetCount(); i++) {
        // 创建节点容器
        var treeNode = document.createElement('div');
        treeNode.className = 'tree-node';
        treeNode.dataset.sheetIndex = i;
        
        // 创建节点头部
        var nodeHeader = document.createElement('div');
        nodeHeader.className = 'node-header';

        // 创建展开/折叠按钮
        var toggleBtn = document.createElement('button');
        toggleBtn.className = 'toggle-btn';
        toggleBtn.innerHTML = '<i class="fa fa-plus text-xs"></i>';

        // 创建节点名称
        var nodeName = document.createElement('span');
        nodeName.className = 'node-name';
        nodeName.innerText = hostSpread.getSheet(i).name();

        // 组装节点头部
        nodeHeader.appendChild(toggleBtn);
        nodeHeader.appendChild(nodeName);

        // 为节点头部添加点击事件(切换sheet)
        nodeHeader.addEventListener('click', function(e) {
            if (e.target.closest('.toggle-btn')) return; // 跳过按钮点击
            e.stopPropagation();
            const sheetName = this.querySelector('.node-name').textContent;
            hostSpread.setActiveSheet(sheetName);
        });

        // 为按钮添加点击事件(展开/折叠)
        toggleBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            const node = this.closest('.tree-node');
            const children = node.querySelector('.node-children');
            const icon = this.querySelector('i');
            
            if (node.classList.contains('expanded')) {
                // 折叠节点
                children.style.maxHeight = '0px';
                node.classList.remove('expanded');
                icon.classList.replace('fa-minus', 'fa-plus');
            } else {
                // 展开节点
                const height = children.scrollHeight;
                children.style.maxHeight = height + 'px';
                node.classList.add('expanded');
                icon.classList.replace('fa-plus', 'fa-minus');
            }
        });

        // 创建子节点容器
        var childrenNode = document.createElement('div');
        childrenNode.className = 'node-children';

        // 创建子节点内容
        var childrenContent = document.createElement('div');
        initTreeNode(hostSpread, i, childrenContent);

        // 组装子节点
        childrenNode.appendChild(childrenContent);

        // 组装完整节点
        treeNode.appendChild(nodeHeader);
        treeNode.appendChild(childrenNode);

        // 添加到容器
        sheetNameContainer.appendChild(treeNode);
    }

    var container = document.getElementById('sheetContainer');
    if (container.childElementCount === 1) {
        container.appendChild(document.createElement('br'));
    }
    container.appendChild(sheetNameContainer);

    // 添加搜索功能
    searchInput.addEventListener('input', function(e) {
        const searchText = e.target.value.toLowerCase();
        const treeNodes = sheetNameContainer.querySelectorAll('.tree-node');
        
        treeNodes.forEach(node => {
            const nodeName = node.querySelector('.node-name').innerText.toLowerCase();
            const nodeChildren = node.querySelector('.node-children');
            const childItems = nodeChildren.querySelectorAll('.child-item');
            
            // 检查当前节点名称是否匹配
            const nodeMatch = nodeName.includes(searchText);
            
            // 检查子项是否匹配
            let childMatch = false;
            childItems.forEach(item => {
                const itemText = item.innerText.toLowerCase();
                const match = itemText.includes(searchText);
                item.style.display = match ? '' : 'none';
                if (match) childMatch = true;
            });
            
            // 根据匹配结果显示或隐藏节点
            if (nodeMatch || childMatch) {
                node.style.display = '';
                
                // 如果有子项匹配,自动展开节点
                if (childMatch && searchText) {
                    node.classList.add('expanded');
                    const toggleBtn = node.querySelector('.toggle-btn');
                    const icon = toggleBtn.querySelector('i');
                    icon.classList.replace('fa-plus', 'fa-minus');
                    nodeChildren.style.maxHeight = nodeChildren.scrollHeight + 'px';
                } else if (!searchText) {
                    // 搜索框为空时,恢复初始折叠状态
                    node.classList.remove('expanded');
                    const toggleBtn = node.querySelector('.toggle-btn');
                    const icon = toggleBtn.querySelector('i');
                    icon.classList.replace('fa-minus', 'fa-plus');
                    nodeChildren.style.maxHeight = '0px';
                }
            } else {
                node.style.display = 'none';
            }
        });
    });
}

// 辅助函数:创建树节点内容
function initTreeNode(hostSpread, sheetIndex, container) {
    var tempSheet = hostSpread.getSheet(sheetIndex);
    
    // 添加表格
    tempSheet.tables.all().forEach(table => {
        var childItem = document.createElement('div');
        childItem.className = 'child-item';
        childItem.innerText = table.name();
        childItem.dataset.type = 'table';
        childItem.addEventListener('click', function() {
            const nodeName = this.closest('.tree-node').querySelector('.node-name').textContent;
            hostSpread.setActiveSheet(nodeName);
            changeSheet(hostSpread.getActiveSheet(), 'table', table.name());
        });
        container.appendChild(childItem);
    });

    // 添加数据透视表
    tempSheet.pivotTables.all().forEach(pivotTable => {
        var childItem = document.createElement('div');
        childItem.className = 'child-item';
        childItem.innerText = pivotTable.name();
        childItem.dataset.type = 'pivotTable';
        childItem.addEventListener('click', function() {
            const nodeName = this.closest('.tree-node').querySelector('.node-name').textContent;
            hostSpread.setActiveSheet(nodeName);
            changeSheet(hostSpread.getActiveSheet(), 'pivotTable', pivotTable.name());
        });
        container.appendChild(childItem);
    });

    // 添加形状
    tempSheet.shapes.all().forEach(shape => {
        var childItem = document.createElement('div');
        childItem.className = 'child-item';
        childItem.innerText = shape.name();
        childItem.dataset.type = 'shape';
        childItem.addEventListener('click', function() {
            const nodeName = this.closest('.tree-node').querySelector('.node-name').textContent;
            hostSpread.setActiveSheet(nodeName);
            changeSheet(hostSpread.getActiveSheet(), 'shape', shape.name());
        });
        container.appendChild(childItem);
    });
}


/**
 * 创建树最底层节点,即叶子节点,表示sheet中的图表、形状、Table、数据透视表等
*/
function initTreeNode(hostSpread, sheetIndex, childrenContent) {
    var tempSheet = hostSpread.getSheet(sheetIndex);
    var tableInfos = getSheetTableInfo(tempSheet);
    if (tableInfos && tableInfos.length > 0) {
        tableInfos.forEach(tableInfo => {
            childrenContent.appendChild(createChildItem(tableInfo, hostSpread));
        });
    }

    var pivotTablesInfo = getSheetPivotTablesInfo(tempSheet);
    if (pivotTablesInfo && pivotTablesInfo.length > 0) {
        pivotTablesInfo.forEach(pivotTable => {
            childrenContent.appendChild(createChildItem(pivotTable, hostSpread));
        });
    }

    var shapesInfo = getSheetShapesInfo(tempSheet);
    console.log('shapesInfo: ', shapesInfo);
    if (shapesInfo && shapesInfo.length > 0) {
        shapesInfo.forEach(shape => {
            childrenContent.appendChild(createChildItem(shape, hostSpread));
        });
    }
}

/**
 * 获取Table摘要
*/
function getSheetTableInfo(sheet) {
    var tablesInfo = [];
    sheet.tables.all().forEach(table => {
        tablesInfo.push({
            type: 'table',
            name: table.name(), 
            range: table.range()
        });
    })

    return tablesInfo;
}

/**
 * 获取PivotTable摘要
*/
function getSheetPivotTablesInfo(sheet) {
    var pivotTablesInfo = [];
    sheet.pivotTables.all().forEach(pivotTable => {
        pivotTablesInfo.push({
            type: 'pivotTable',
            name: pivotTable.name(), 
            range: pivotTable.getRange()?.content
        });
    });

    return pivotTablesInfo;
}

/**
 * 获取Shape摘要(形状、图表、区域快照、图片都属于Shape)
*/
function getSheetShapesInfo(sheet) {
    var shapesInfo = [];
    sheet.shapes.all().forEach(shape => {
        shapesInfo.push({
            type: 'shape', 
            name: shape.name()
        });
    });

    return shapesInfo;
}

/**
 * 创建childItem
*/
function createChildItem(info, hostSpread) {
    var childItem = document.createElement('div');
    childItem.className = 'child-item';
    childItem.innerText = info.name;
    childItem.dataset.type = info.type;
    childItem.addEventListener('click', function() {
        console.log('child item clicked');
        //  获取子元素对应的sheet名称
        var nearestNodeChildren = this.closest('.node-children');
        var nearestNodeHeader = nearestNodeChildren.previousElementSibling;
        var nodeNameElement = nearestNodeHeader.querySelector('.node-name');
        var nodeName = nodeNameElement.textContent;
        console.log('node name---: ', nodeName);

        //  跳转sheet后选中相应的元素
        hostSpread.setActiveSheet(nodeName);
        var curSheet = hostSpread.getActiveSheet();
        changeSheet(curSheet, this.dataset.type, this.innerText);
    });

    return childItem;
}

/**
 * 切换sheet,并选中sheet中的指定元素
*/
function changeSheet(sheet, elementType, elementName) {
    console.log('elementType: ', elementType, ', elementName: ', elementName);
    if (elementType === 'table') {
        selectTable(sheet, elementName);
    } else if (elementType === 'pivotTable') {
        selectPivotTable(sheet, elementName);
    } else if (elementType === 'chart') {
        selectChart(sheet, elementName);
    } else if (elementType === 'shape') {
        selectShape(sheet, elementName);
    }
}

/**
 * 选中Table
*/
function selectTable(sheet, tableName) {
    var table = sheet.tables.findByName(tableName);
    if (table) {
        var range = table.range();
        sheet.setSelection(range.row, range.col, range.rowCount, range.colCount);
    }
}

/**
 * 选中数据透视表
*/
function selectPivotTable(sheet, pivotTableName) {
    var range = sheet.pivotTables.get(pivotTableName)?.getRange().content;
    if (range) {
        sheet.setSelection(range.row, range.col, range.rowCount, range.colCount);
    }
}

/**
 * 选中形状
*/
function selectShape(sheet, shapeName) {
    sheet.shapes.all().forEach(shape => {
        if (shape.name() === shapeName) {
            shape.isSelected(true);
        } else {
            shape.isSelected(false);
        }
    });
}


   </script>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值