This sample shows the following:
How to add a custom page to the File Menu.
How to define a custom component and use it in the File Menu.
const resources = {
tips: 'Please click the "File" button in the upper-left corner to access the File menu, then select the "Template" tab to view custom pages.',
loading: 'Loading...'
}
window.onload = async function () {
//change the file menu panel command to add the custom template execute logic.
let fileMenuPanelCommand = GC.Spread.Sheets.Designer.getCommand(GC.Spread.Sheets.Designer.CommandNames.FileMenuPanel);
let oldExecuteFn = fileMenuPanelCommand.execute;
let oldStatusFn = fileMenuPanelCommand.getState;
fileMenuPanelCommand.execute = async function (context, propertyName, newValue) {
oldExecuteFn.apply(this, arguments);
if (propertyName === "customTemplates") {
console.log(newValue);
showLoadingOverlay();
let json = await loadTemplateData(newValue);
let spread = context.getWorkbook();
context.setData("FileMenu_show", false);
await spread.fromJSON(json);
hideLoadingOverlay();
context.getWorkbook().focus(true);
}
}
fileMenuPanelCommand.getState = function () {
let result = oldStatusFn.apply(this, arguments);
result.customTemplates = custom_templates;
return result;
}
let config = GC.Spread.Sheets.Designer.DefaultConfig;
config.commandMap = {
fileMenuPanel: fileMenuPanelCommand
}
let designer = new GC.Spread.Sheets.Designer.Designer("dss", config);
let spread = designer.getWorkbook();
spread.getActiveSheet().setValue(1, 1, resources.tips);
}
function loadTemplateData(templateID) {
let dataPath = custom_templates.find(t => t.id === templateID).dataPath;
return fetch(dataPath).then(function (response) {
return response.text();
}).then((str) => getDataFromJS(str));
}
function showLoadingOverlay() {
let overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.id = 'loading-overlay';
overlay.innerHTML = `
<div class="loading-spinner"></div>
<div class="loading-text">${resources.loading}</div>
`;
document.body.appendChild(overlay);
}
function hideLoadingOverlay() {
let overlay = document.getElementById('loading-overlay');
if (overlay) {
document.body.removeChild(overlay);
}
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta name="spreadjs culture"/>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-designer/styles/gc.spread.sheets.designer.light.min.css">
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-shapes/dist/gc.spread.sheets.shapes.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-charts/dist/gc.spread.sheets.charts.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-print/dist/gc.spread.sheets.print.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-barcode/dist/gc.spread.sheets.barcode.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-pdf/dist/gc.spread.sheets.pdf.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-pivot-addon/dist/gc.spread.pivot.pivottables.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-slicers/dist/gc.spread.sheets.slicers.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-tablesheet/dist/gc.spread.sheets.tablesheet.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-formula-panel/dist/gc.spread.sheets.formulapanel.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-io/dist/gc.spread.sheets.io.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-designer-resources-en/dist/gc.spread.sheets.designer.resource.en.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/en/purejs/node_modules/@mescius/spread-sheets-designer/dist/gc.spread.sheets.designer.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/designer/license.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link href="./styles.css" rel="stylesheet" type="text/css" />
<script src="./component.js"></script>
<script src="./template.js"></script>
<script src="./data/templateData.js"></script>
</head>
<body style="margin:0;padding:0">
<div id="dss" style="width:100vw;border:1px solid darkgray"></div>
</body>
</html>
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
#dss {
height: 100%;
}
.template-selector {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
min-width: 0;
overflow-x: hidden;
}
.templates-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 20px;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
}
@media (max-width: 600px) {
.templates-grid {
justify-content: center;
}
}
.template-card {
border: 1px solid #d1d1d1;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.2s ease;
overflow: hidden;
height: 180px;
width: 200px;
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
.template-card:hover {
border-color: #0078d4;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.template-preview {
flex: 1;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.template-image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-content {
width: 90%;
height: 90%;
border-radius: 2px;
position: relative;
background: white;
border: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
}
.simple-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.preview-icon {
font-size: 32px;
opacity: 0.6;
}
.template-info {
padding: 12px;
background: white;
border-top: 1px solid #e0e0e0;
}
.template-title {
font-size: 14px;
font-weight: 400;
margin: 0;
color: #333;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.templates-grid {
gap: 12px;
justify-content: center;
}
.template-card {
width: 160px;
height: 150px;
}
.template-title {
font-size: 12px;
}
.template-info {
padding: 8px;
}
}
@media (max-width: 480px) {
.template-selector {
padding: 10px;
}
.templates-grid {
gap: 8px;
justify-content: space-around;
}
.template-card {
width: 140px;
height: 130px;
}
.template-title {
font-size: 11px;
}
}
@media (min-width: 1400px) {
.template-card {
width: 220px;
height: 200px;
}
.preview-icon {
font-size: 40px;
}
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #0078d4;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
color: white;
margin-left: 16px;
font-size: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
function FileTemplateSelector() {
GC.Spread.Sheets.Designer.AtomicComponentBase.call(this, ...arguments);
this.selectedTemplate = null;
};
FileTemplateSelector.prototype = new GC.Spread.Sheets.Designer.AtomicComponentBase();
FileTemplateSelector.prototype.getTemplate = function () {
return `
<div class="template-selector">
<div class="templates-grid">
</div>
</div>`;
}
FileTemplateSelector.prototype._generateTemplateItem = function (template) {
let div = document.createElement("div");
div.setAttribute("key", template.id);
div.className = "template-card";
div.innerHTML = `
<div class="template-preview">
<div class="template-image-placeholder">
<div class="preview-content">
<div class="simple-preview" style="background: url(${template.image}) no-repeat center center; background-size: cover;">
<div class="preview-icon"></div>
</div>
</div>
</div>
</div>
<div class="template-info">
<h3 class="template-title">${template.title}</h3>
</div>
`;
return div;
}
FileTemplateSelector.prototype.handleTemplateClick = function (e) {
const templateCard = e.target.closest(".template-card");
if (templateCard) {
this.selectedTemplate = templateCard.getAttribute("key");
this.raiseValueChanged()
}
}
FileTemplateSelector.prototype.onMounted = function (host) {
host.querySelector(".templates-grid").addEventListener("click", this.handleTemplateClick.bind(this));
}
FileTemplateSelector.prototype.onRaiseValueChanged = function () {
this.onRaiseValueChanged();
}
FileTemplateSelector.prototype.onValueChanged = function (prevValue, nextValue, host) {
let templates = nextValue;
if (templates && Array.isArray(templates) && templates.length > 0) {
host.querySelector(".templates-grid").innerHTML = "";
templates.forEach(template => {
let templateItem = this._generateTemplateItem(template);
host.querySelector(".templates-grid").appendChild(templateItem);
})
}
}
FileTemplateSelector.prototype.updateValue = function (host) {
//return the select template;
return this.selectedTemplate;
}
FileTemplateSelector.prototype.onDestroy = function (host) {
host.querySelector(".templates-grid").removeEventListener("click", this.handleTemplateClick.bind(this));
}
// Register Custom Component to Designer
GC.Spread.Sheets.Designer.Designer.RegisterComponent("FileTemplateSelector", FileTemplateSelector);
var fileMenuTemplate = GC.Spread.Sheets.Designer.getTemplate(
GC.Spread.Sheets.Designer.TemplateNames.FileMenuPanelTemplate
);
addCustomItem(fileMenuTemplate);
GC.Spread.Sheets.Designer.registerTemplate(
GC.Spread.Sheets.Designer.TemplateNames.FileMenuPanelTemplate,
fileMenuTemplate
);
function addCustomItem(template) {
//add Template to the left menu list;
template.content[0].children[0].children[0].children[0].children[1].items.push({ text: "Template", value: "Template" });
//add Template page to the right container;
var listDisplayContainer = template.content[0].children[0].children[1];
listDisplayContainer.children.push({
type: "Container",
visibleWhen: "activeCategory_main=Template",
children: [
{
type: 'TextBlock',
margin: '50px 0 15px 50px',
style: 'font-size:36px;',
text: 'Templates'
},
{
type: "FileTemplateSelector",
bindingPath:"customTemplates"
}
],
attributes:[
{key: 'data-activeCategory_main', value: 'Template'}
]
});
}