Last active
April 24, 2021 23:42
-
-
Save josephxbrick/9ebf2af1bf4132611574db4c90a8659c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* createTOC */ | |
// This script updates a table of contents for your document. It assumes your document is using | |
// sections, as each section will be an entry in the TOC | |
// Please view the "sample" page for this script in the file | |
// This script also adds section numbers to section and page headers and names the docPage's frame | |
// Along the way it sorts the docPages and page-numbers them, using other scripts in this file. | |
// See the other scripts for code comments on them | |
// We will use an array of this type to collect information about the document as we go. | |
interface DocPageInfo { | |
docPage:FrameNode; | |
pageNumber:number; | |
sectionNumber:number; | |
pageInSectionNumber:number; | |
pageTitle:string; | |
pageType: "page"|"section"|""; | |
} | |
const createTOC = (docInfo:DocPageInfo[]) =>{ | |
let tocItemsCreated:number = 0; | |
const tocSections = docInfo.filter(node => node.pageType === 'section'); | |
if (tocSections.length === 0){ | |
return; | |
} | |
// find TOC frame | |
let tocFrame:FrameNode|null = null; | |
for (let i = 0; i < docInfo.length; i++){ | |
tocFrame = docInfo[i].docPage.findChild(node => node.type === 'FRAME' && node.name === '#tocFrame') as FrameNode|null; | |
if (tocFrame !== null){ | |
break | |
} | |
} | |
if (tocFrame === null){ | |
return; | |
} | |
const tocItems = tocFrame.findChildren(node => node.type === 'INSTANCE' && node.name === '#tocItem'); | |
let length:number = tocItems.length; | |
if (length === 0){ | |
return 0; | |
} | |
// remove all but one item | |
for (let i = 0; i < length - 1 ;i++){ | |
const itemToDelete = tocItems.pop() as InstanceNode; | |
itemToDelete.remove(); | |
} | |
const cloner = tocItems[0] as InstanceNode; | |
for (let i = 0;i < tocSections.length - 1; i++){ | |
const clone = cloner.clone() | |
tocFrame.appendChild(clone); | |
tocItems.push(clone); | |
} | |
for (let i = 0; i < tocItems.length; i++){ | |
const tocItem = tocItems[i] as InstanceNode; | |
setTextOfContainer(tocItem, '#tocPageNum', tocSections[i].pageNumber.toString()); | |
setTextOfContainer(tocItem, '#tocPageName', `${tocSections[i].sectionNumber}. ${tocSections[i].pageTitle}`); | |
} | |
figma.notify(`${tocItems.length} TOC items created; ${docInfo.length} pages processed.`,{timeout: 4000}); | |
return tocItems; | |
} | |
interface Options { | |
hSpacing: number; // horizontal spacing between frames | |
vSpacing: number; // vertical spacing between frames | |
vSpacingWithinSection:number; // vertical spacing between frames when a section wraps | |
} | |
const sortedDocPages = (options:Options) => { | |
// insure that spacing values are 0 or greater | |
options.hSpacing = Math.max(options.hSpacing, 0); | |
options.vSpacing = Math.max(options.vSpacing, 0); | |
// inner function adjusts the in-between spacing and aligns the tops of frames of the given row. | |
// It returns the y value of the bottom of the row (i.e., the bottom of its tallest frame) | |
const tidyUpRow = (rowFrames:FrameNode[], rowX:number, rowY:number, rowSpacing:number) => { | |
let maxFrameHeight = 0; | |
for (const frame of rowFrames) { | |
maxFrameHeight = Math.max(frame.height, maxFrameHeight); | |
frame.x = rowX; | |
frame.y = rowY; | |
// update rowX to the desired x value of next frame in the row | |
rowX += frame.width + rowSpacing; | |
} | |
return rowY + maxFrameHeight; | |
} | |
let sortedFrames:FrameNode[] = []; | |
let rowX = 0; | |
let rowY = 0; | |
let isFirstRow = true; | |
let spacing = 0; | |
// get all top-level frames on the page | |
let availableFrames = figma.currentPage.findChildren(n => n.type === 'FRAME') as FrameNode[]; | |
while(availableFrames.length > 0){ | |
// find y of topmost frame in availableFrames, as well as the height of the shortest frame. | |
// We will use these to find frames in the top row. | |
let minY = Number.MAX_SAFE_INTEGER; | |
let minHeight = Number.MAX_SAFE_INTEGER; | |
for (const frame of availableFrames){ | |
minY = Math.min(minY, frame.y) | |
minHeight = Math.min(minHeight, frame.height); | |
} | |
// find frames in top row: assume a frame is in the top row when its distance from minY | |
// is less than the height of the shortest frame. | |
const topRow:FrameNode[] = []; | |
const otherRows:FrameNode[] = []; | |
for (const frame of availableFrames){ | |
if (Math.abs(minY - frame.y) < minHeight){ | |
topRow.push(frame) | |
} else { | |
otherRows.push(frame) | |
} | |
} | |
// we have the top row of the remaining rows! | |
topRow.sort((a,b) => a.x - b.x); // sort frames in array by left-to-right positon | |
// check if this is the first row so we can initialize rowX and rowY | |
if (isFirstRow){ | |
rowX = topRow[0].x; | |
rowY = topRow[0].y; | |
isFirstRow = false; | |
} | |
if (topRow[0].findChild(n => n.type === "INSTANCE" && n.name === '#pageInSectionHeader')){ | |
spacing = options.vSpacingWithinSection; | |
} else { | |
spacing = options.vSpacing; | |
} | |
// clean up the row layout, get y value for top of next row | |
rowY = tidyUpRow(topRow, rowX, rowY + spacing - options.vSpacing, options.hSpacing) + options.vSpacing; | |
sortedFrames = [...sortedFrames, ...topRow] // append frames in row to sorted frames | |
availableFrames = [...otherRows] // update to contain the frames not in any previous row | |
} | |
// sort the frames in the layer list such that they read top-to-bottom (i.e., reverse z-order) | |
for (let i = sortedFrames.length - 1; i >= 0; i--) { | |
const docPage = sortedFrames[i]; | |
figma.currentPage.appendChild(docPage); | |
} | |
// return array of sorted frames | |
return sortedFrames; | |
} | |
// adds page numbers to pages | |
const numberPages = (sortedDocPages:FrameNode[]) => { | |
const footerName:string = '#footer'; | |
const textNodeName:string = '#pageNumber'; | |
const docPageInfo:DocPageInfo[] = []; | |
let curPageNum:number = 0; | |
for (const docPage of sortedDocPages) { | |
curPageNum++; | |
docPageInfo.push({ | |
docPage: docPage, | |
pageNumber: curPageNum, | |
} as DocPageInfo) | |
const footer = docPage.findChild(n => n.type === 'INSTANCE' && n.name === footerName) as InstanceNode|null; | |
if (footer !== null) { | |
setTextOfContainer(footer, textNodeName, curPageNum.toString()); | |
} | |
} | |
return docPageInfo; | |
} | |
// Here's the numberSections code. Its parameter is the return value from numberPages() | |
const numberSections = (docPageInfo:DocPageInfo[]) => { | |
let sectionNumber = 0; | |
let pageInSectionNumber = 0; | |
let pageTitle:string; | |
let pageType: "page"|"section"|"titlePage"|""; | |
for (let i=0; i < docPageInfo.length; i++){ | |
const pageInfo = docPageInfo[i]; | |
pageTitle = ''; | |
pageType = ''; | |
// see if there is an instance of the component #sectionHeader on this page. | |
// if so, update its section number | |
let header = pageInfo.docPage.findChild(n => n.type === 'INSTANCE' && n.name === '#sectionHeader') as InstanceNode|null; | |
if (header !== null) { | |
sectionNumber++; | |
pageInSectionNumber = 1; | |
setTextOfContainer(header, '#sectionNumber', sectionNumber.toString()); | |
pageTitle = getTextOfContainer(header, '#headerTitle') | |
pageInfo.docPage.name = `${sectionNumber} – ${pageTitle}` | |
pageType = "section"; | |
} | |
if (pageInSectionNumber > 0 && pageType == ''){ | |
// see if there is instance of the component #sectionHeader on this pageInfo. | |
// if so update its section number | |
header = pageInfo.docPage.findChild(n => n.type === 'INSTANCE' && n.name === '#pageInSectionHeader') as InstanceNode|null; | |
if (header !== null) { | |
setTextOfContainer(header, '#sectionNumber', `${sectionNumber}.${pageInSectionNumber}`) | |
pageTitle = getTextOfContainer(header, '#headerTitle') | |
pageType = "page"; | |
pageInfo.docPage.name = `${sectionNumber}.${pageInSectionNumber} – ${pageTitle}` | |
pageInSectionNumber++; | |
} | |
} | |
// merge new values into array element's object | |
docPageInfo[i] = {...docPageInfo[i],...{ | |
pageType:pageType, | |
sectionNumber: sectionNumber, | |
pageInSectionNumber: pageInSectionNumber, | |
pageTitle:pageTitle, | |
} | |
} | |
} | |
return docPageInfo; | |
} | |
// Utility function: sets text of a textnode (of a given name) within a Figma node | |
// that can contain children (frame, Instance, group, etc.). | |
// Returns the textNode, or null if no such node exists within the instance | |
const setTextOfContainer = (container:ChildrenMixin, textNodeName: string, value: string) => { | |
const textNode = container.findOne(n => n.type === 'TEXT' && n.name === textNodeName) as TextNode|null; | |
if (textNode !== null) { | |
textNode.characters = value; | |
} | |
return textNode; | |
} | |
// Same as setTextOfContainer above, except that it gets the value | |
const getTextOfContainer = (container:ChildrenMixin, textNodeName: string) => { | |
const textNode = container.findOne(n => n.type === 'TEXT' && n.name === textNodeName) as TextNode|null; | |
if (textNode !== null) { | |
return textNode.characters; | |
} | |
return ''; | |
} | |
// sample code | |
// get sorted doc pages | |
// NOTE: Change the spacing values in this row to adjust frame spacing | |
const pages = sortedDocPages({hSpacing: 100, vSpacing: 500, vSpacingWithinSection: 150 }); | |
// number them and receive an array of type docPageInfo[] | |
const numberedPages = numberPages(pages); | |
// number sections and receive an array of type docPageInfo[] | |
const sectionPages = numberSections(numberedPages); | |
// create table of contents | |
createTOC(sectionPages); | |
console.log(sectionPages); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment