infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
375 lines (337 loc) • 12.9 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/
"use strict";
/******
* ToC *
*******/
fluid.registerNamespace("fluid.tableOfContents");
fluid.tableOfContents.headingTextToAnchorInfo = function (heading) {
var id = fluid.allocateSimpleId(heading);
var anchorInfo = {
id: id,
url: "#" + id
};
return anchorInfo;
};
fluid.tableOfContents.locateHeadings = function (that) {
var headings = that.locate("headings");
fluid.each(that.options.ignoreForToC, function (sel) {
headings = headings.not(sel).not(sel + " :header");
});
return headings;
};
fluid.tableOfContents.refreshView = function (that) {
var headings = that.locateHeadings();
that.anchorInfo = fluid.transform(headings, function (heading) {
return that.headingTextToAnchorInfo(heading);
});
var headingsModel = that.modelBuilder.assembleModel(headings, that.anchorInfo);
that.applier.change("", headingsModel);
that.events.onRefresh.fire();
};
fluid.defaults("fluid.tableOfContents", {
gradeNames: ["fluid.viewComponent"],
components: {
levels: {
type: "fluid.tableOfContents.levels",
// This is a createOnEvent markup since the parent acquires its model state from the DOM on startup
// and it is currently too irritating to express this via expanders until we implement proxies for FLUID-6372
createOnEvent: "onCreate",
container: "{tableOfContents}.dom.tocContainer",
options: {
model: {
headings: "{tableOfContents}.model"
},
events: {
afterRender: "{tableOfContents}.events.afterRender"
},
listeners: {
"{tableOfContents}.events.onRefresh": "{that}.refreshView"
},
strings: "{tableOfContents}.options.strings"
}
},
modelBuilder: {
type: "fluid.tableOfContents.modelBuilder"
}
},
model: [],
invokers: {
headingTextToAnchorInfo: "fluid.tableOfContents.headingTextToAnchorInfo",
locateHeadings: {
funcName: "fluid.tableOfContents.locateHeadings",
args: ["{that}"]
},
refreshView: {
funcName: "fluid.tableOfContents.refreshView",
args: ["{that}"]
},
// TODO: is it weird to have hide and show on a component?
hide: {
"this": "{that}.dom.tocContainer",
"method": "hide"
},
show: {
"this": "{that}.dom.tocContainer",
"method": "show"
}
},
strings: {
tocHeader: "Table of Contents"
},
selectors: {
headings: ":header:visible",
tocContainer: ".flc-toc-tocContainer"
},
ignoreForToC: {
tocContainer: "{that}.options.selectors.tocContainer"
},
events: {
onRefresh: null,
afterRender: null,
onReady: {
events: {
"onCreate": "onCreate",
"afterRender": "afterRender"
},
args: ["{that}"]
}
},
listeners: {
"onCreate.setLabel": {
"this": "{that}.dom.tocContainer",
"method": "attr",
"args": ["aria-label", "{that}.options.strings.tocHeader"],
"priority": "before:refreshView"
},
"onCreate.refreshView": {
func: "{that}.refreshView",
// New for FLUID-6148: Make sure we do not try to refresh view until after "levels" subcomponent is constructed
priority: "after:fluid-componentConstruction"
}
}
});
/*******************
* ToC ModelBuilder *
********************/
fluid.registerNamespace("fluid.tableOfContents.modelBuilder");
fluid.tableOfContents.modelBuilder.toModel = function (headingInfo, modelLevelFn) {
var headings = fluid.copy(headingInfo);
var buildModelLevel = function (headings, level) {
var modelLevel = [];
while (headings.length > 0) {
var heading = headings[0];
if (heading.level < level) {
break;
}
if (heading.level > level) {
var subHeadings = buildModelLevel(headings, level + 1);
if (modelLevel.length > 0) {
fluid.peek(modelLevel).headings = subHeadings;
} else {
modelLevel = modelLevelFn(modelLevel, subHeadings);
}
}
if (heading.level === level) {
modelLevel.push(heading);
headings.shift();
}
}
return modelLevel;
};
return buildModelLevel(headings, 1);
};
fluid.tableOfContents.modelBuilder.gradualModelLevelFn = function (modelLevel, subHeadings) {
// Clone the subHeadings because we don't want to modify the reference of the subHeadings.
// the reference will affect the equality condition in generateTree(), resulting an unwanted tree.
var subHeadingsClone = fluid.copy(subHeadings);
subHeadingsClone[0].level--;
return subHeadingsClone;
};
fluid.tableOfContents.modelBuilder.skippedModelLevelFn = function (modelLevel, subHeadings) {
modelLevel.push({headings: subHeadings});
return modelLevel;
};
fluid.tableOfContents.modelBuilder.convertToHeadingObjects = function (that, headings, anchorInfo) {
headings = $(headings);
return fluid.transform(headings, function (heading, index) {
return {
level: that.headingCalculator.getHeadingLevel(heading),
text: $(heading).text(),
url: anchorInfo[index].url
};
});
};
fluid.tableOfContents.modelBuilder.assembleModel = function (that, headings, anchorInfo) {
var headingInfo = that.convertToHeadingObjects(headings, anchorInfo);
return that.toModel(headingInfo);
};
fluid.defaults("fluid.tableOfContents.modelBuilder", {
gradeNames: ["fluid.component"],
components: {
headingCalculator: {
type: "fluid.tableOfContents.modelBuilder.headingCalculator"
}
},
invokers: {
toModel: {
funcName: "fluid.tableOfContents.modelBuilder.toModel",
args: ["{arguments}.0", "{modelBuilder}.modelLevelFn"]
},
modelLevelFn: "fluid.tableOfContents.modelBuilder.gradualModelLevelFn",
convertToHeadingObjects: "fluid.tableOfContents.modelBuilder.convertToHeadingObjects({that}, {arguments}.0, {arguments}.1)", // headings, anchorInfo
assembleModel: "fluid.tableOfContents.modelBuilder.assembleModel({that}, {arguments}.0, {arguments}.1)" // headings, anchorInfo
}
});
/*************************************
* ToC ModelBuilder headingCalculator *
**************************************/
fluid.registerNamespace("fluid.tableOfContents.modelBuilder.headingCalculator");
fluid.tableOfContents.modelBuilder.headingCalculator.getHeadingLevel = function (that, heading) {
return that.options.levels.indexOf(heading.tagName) + 1;
};
fluid.defaults("fluid.tableOfContents.modelBuilder.headingCalculator", {
gradeNames: ["fluid.component"],
invokers: {
getHeadingLevel: "fluid.tableOfContents.modelBuilder.headingCalculator.getHeadingLevel({that}, {arguments}.0)" // heading
},
levels: ["H1", "H2", "H3", "H4", "H5", "H6"]
});
/*************
* ToC Levels *
**************/
fluid.registerNamespace("fluid.tableOfContents.levels");
/**
* Create an object model based on the type and ID. The object should contain an
* ID that maps the selectors (ie. level1:), and the object should contain a children
* @param {String} type - Accepted values are: level, items
* @param {Integer} ID - The current level which is used here as the ID.
* @return {Object} - An object that models the level based on the type and ID.
*/
fluid.tableOfContents.levels.objModel = function (type, ID) {
var objModel = {
ID: type + ID + ":",
children: []
};
return objModel;
};
/*
* Configure item object when item object has no text, uri, level in it.
* defaults to add a decorator to hide the bullets.
*/
fluid.tableOfContents.levels.handleEmptyItemObj = function (itemObj) {
itemObj.decorators = [{
type: "addClass",
classes: "fl-tableOfContents-hide-bullet"
}];
};
/**
* @param {Object} headingsModel - that.model, the model with all the headings, it should be in the format of {headings: [...]}
* @param {Integer} currentLevel - the current level we want to generate the tree for. default to 1 if not defined.
* @return {Object} - A tree that looks like {children: [{ID: x, subTree:[...]}, ...]}
*/
fluid.tableOfContents.levels.generateTree = function (headingsModel, currentLevel) {
currentLevel = currentLevel || 0;
var levelObj = fluid.tableOfContents.levels.objModel("level", currentLevel);
// FLUID-4352, run generateTree if there are headings in the model.
if (headingsModel.headings.length === 0) {
return currentLevel ? [] : {children: []};
}
// base case: level is 0, returns {children:[generateTree(nextLevel)]}
// purpose is to wrap the first level with a children object.
if (currentLevel === 0) {
var tree = {
children: [
fluid.tableOfContents.levels.generateTree(headingsModel, currentLevel + 1)
]
};
return tree;
}
// Loop through the heading array, which can have multiple headings on the same level
$.each(headingsModel.headings, function (index, model) {
var itemObj = fluid.tableOfContents.levels.objModel("items", currentLevel);
var linkObj = {
ID: "link" + currentLevel,
target: model.url,
linktext: model.text
};
// If level is undefined, then add decorator to it, otherwise add the links to it.
if (!model.level) {
fluid.tableOfContents.levels.handleEmptyItemObj(itemObj);
} else {
itemObj.children.push(linkObj);
}
// If there are sub-headings, go into the next level recursively
if (model.headings) {
itemObj.children.push(fluid.tableOfContents.levels.generateTree(model, currentLevel + 1));
}
// At this point, the itemObj should be in a tree format with sub-headings children
levelObj.children.push(itemObj);
});
return levelObj;
};
/**
* @param {Object} that - The component itself.
* @return {Object} - Returned produceTree must be in {headings: [trees]}
*/
fluid.tableOfContents.levels.produceTree = function (that) {
var tree = fluid.tableOfContents.levels.generateTree(that.model);
// Add the header to the tree
tree.children.push({
ID: "tocHeader",
messagekey: "tocHeader"
});
return tree;
};
fluid.defaults("fluid.tableOfContents.levels", {
gradeNames: ["fluid.rendererComponent", "fluid.resourceLoader"],
produceTree: "fluid.tableOfContents.levels.produceTree",
strings: {
tocHeader: "Table of Contents"
},
selectors: {
tocHeader: ".flc-toc-header",
level1: ".flc-toc-levels-level1",
level2: ".flc-toc-levels-level2",
level3: ".flc-toc-levels-level3",
level4: ".flc-toc-levels-level4",
level5: ".flc-toc-levels-level5",
level6: ".flc-toc-levels-level6",
items1: ".flc-toc-levels-items1",
items2: ".flc-toc-levels-items2",
items3: ".flc-toc-levels-items3",
items4: ".flc-toc-levels-items4",
items5: ".flc-toc-levels-items5",
items6: ".flc-toc-levels-items6",
link1: ".flc-toc-levels-link1",
link2: ".flc-toc-levels-link2",
link3: ".flc-toc-levels-link3",
link4: ".flc-toc-levels-link4",
link5: ".flc-toc-levels-link5",
link6: ".flc-toc-levels-link6"
},
repeatingSelectors: ["level1", "level2", "level3", "level4", "level5", "level6", "items1", "items2", "items3", "items4", "items5", "items6"],
model: {
headings: [] // [text: heading, url: linkURL, headings: [ an array of subheadings in the same format]
},
resources: {
template: {
url: "../html/TableOfContents.html"
}
},
renderOnInit: true,
rendererFnOptions: {
noexpand: true
},
rendererOptions: {
debugMode: false
}
});