diff --git a/demos/modules/demo_shape.mjs b/demos/modules/demo_shape.mjs index c921998c2..c5bb2d583 100644 --- a/demos/modules/demo_shape.mjs +++ b/demos/modules/demo_shape.mjs @@ -96,6 +96,7 @@ function genSlide01(pptx) { }); slide.addShape(pptx.shapes.RIGHT_TRIANGLE, { + sId: 20, x: 0.4, y: 4.3, w: 6.0, @@ -105,6 +106,7 @@ function genSlide01(pptx) { shapeName: "First Right Triangle", }); slide.addShape(pptx.shapes.RIGHT_TRIANGLE, { + sId: 21, x: 7.0, y: 4.3, w: 6.0, @@ -113,6 +115,22 @@ function genSlide01(pptx) { line: { color: pptx.colors.ACCENT1, width: 2 }, flipH: true, }); + + slide.addShape(pptx.shapes.LINE, { + x: 3108960, + y: 5303520, + w: 6035040, + h: 0, + line: { + width: 2, color: "000000", + sourceId: 20, + targetId: 21, + sourceAnchorPos: 5, // on rectangel pptx.anchor.TOP , pptx.anchor.BOTTOM , pptx.anchor.LEFT or pptx.anchor.RIGHT + targetAnchorPos: 5 // could be used instead + } + }); + + } /** @@ -251,6 +269,7 @@ function genSlide02(pptx) { slide.addText("RIGHT-TRIANGLE", { shape: pptx.shapes.RIGHT_TRIANGLE, align: "center", + sId: 20, x: 0.4, y: 4.3, w: 6, @@ -261,6 +280,7 @@ function genSlide02(pptx) { slide.addText("HYPERLINK-SHAPE", { shape: pptx.shapes.RIGHT_TRIANGLE, align: "center", + sId: 21, x: 7.0, y: 4.3, w: 6, @@ -270,4 +290,18 @@ function genSlide02(pptx) { flipH: true, hyperlink: { url: "https://github.com/gitbrent/pptxgenjs", tooltip: "Visit Homepage" }, }); + slide.addShape(pptx.shapes.LINE, { + x: 3108960, + y: 5303520, + w: 6035040, + h: 0, + line: { + width: 2, color: "000000", + sourceId: 20, + targetId: 21, + sourceAnchorPos: 5, // on rectangel pptx.anchor.TOP , pptx.anchor.BOTTOM , pptx.anchor.LEFT or pptx.anchor.RIGHT + targetAnchorPos: 5 // could be used instead + } + }); + slide.addText("Connected lines do not support text", {x:4.5,y:6}) } diff --git a/src/core-enums.ts b/src/core-enums.ts index 8a4950d3e..d31791d69 100644 --- a/src/core-enums.ts +++ b/src/core-enums.ts @@ -321,6 +321,13 @@ export enum AlignV { 'bottom' = 'bottom', } +export enum ANCHOR{ + "TOP" = 0, + "LEFT" = 1, + "BOTTOM" = 2, + "RIGHT" = 3 +} + export enum SHAPE_TYPE { ACTION_BUTTON_BACK_OR_PREVIOUS = 'actionButtonBackPrevious', ACTION_BUTTON_BEGINNING = 'actionButtonBeginning', diff --git a/src/core-interfaces.ts b/src/core-interfaces.ts index bf41a113f..c10baf1c5 100644 --- a/src/core-interfaces.ts +++ b/src/core-interfaces.ts @@ -16,6 +16,12 @@ import { CHART_NAME, PLACEHOLDER_TYPE, SHAPE_NAME, SLIDE_OBJECT_TYPES, TEXT_HALI * @example '75%' // coordinate as percentage of slide size */ export type Coord = number | string + +export type IDCoord = { + id:number + posistion: PositionProps +} + export type PositionProps = { /** * Horizontal position @@ -238,6 +244,32 @@ export interface ShapeLineProps extends ShapeFillProps { * @deprecated v3.3.0 - use `width` */ size?: number + /** + * Set line shape to be connector + * @default false + */ + isConnector?: boolean + /** + * connected source shape id + */ + sourceId?: number + /** + * connected target shape id + */ + targetId?: number + /** + * source shape connection position (dependent on available connection points on a shape) + */ + sourceAnchorPos?: number + /** + * target shape connection position (dependent on available connection points on a shape) + */ + targetAnchorPos?: number + /** + * adjustments to the curve + */ + curveadjust?: number[] + } // used by: chart, slide, table, text export interface TextBaseProps { @@ -263,80 +295,80 @@ export interface TextBaseProps { * @default false */ bullet?: - | boolean - | { - /** - * Bullet type - * @default bullet - */ - type?: 'bullet' | 'number' - /** - * Bullet character code (unicode) - * @since v3.3.0 - * @example '25BA' // 'BLACK RIGHT-POINTING POINTER' (U+25BA) - */ - characterCode?: string - /** - * Indentation (space between bullet and text) (points) - * @since v3.3.0 - * @default 27 // DEF_BULLET_MARGIN - * @example 10 // Indents text 10 points from bullet - */ - indent?: number - /** - * Number type - * @since v3.3.0 - * @example 'romanLcParenR' // roman numerals lower-case with paranthesis right - */ - numberType?: - | 'alphaLcParenBoth' - | 'alphaLcParenR' - | 'alphaLcPeriod' - | 'alphaUcParenBoth' - | 'alphaUcParenR' - | 'alphaUcPeriod' - | 'arabicParenBoth' - | 'arabicParenR' - | 'arabicPeriod' - | 'arabicPlain' - | 'romanLcParenBoth' - | 'romanLcParenR' - | 'romanLcPeriod' - | 'romanUcParenBoth' - | 'romanUcParenR' - | 'romanUcPeriod' - /** - * Number bullets start at - * @since v3.3.0 - * @default 1 - * @example 10 // numbered bullets start with 10 - */ - numberStartAt?: number + | boolean + | { + /** + * Bullet type + * @default bullet + */ + type?: 'bullet' | 'number' + /** + * Bullet character code (unicode) + * @since v3.3.0 + * @example '25BA' // 'BLACK RIGHT-POINTING POINTER' (U+25BA) + */ + characterCode?: string + /** + * Indentation (space between bullet and text) (points) + * @since v3.3.0 + * @default 27 // DEF_BULLET_MARGIN + * @example 10 // Indents text 10 points from bullet + */ + indent?: number + /** + * Number type + * @since v3.3.0 + * @example 'romanLcParenR' // roman numerals lower-case with paranthesis right + */ + numberType?: + | 'alphaLcParenBoth' + | 'alphaLcParenR' + | 'alphaLcPeriod' + | 'alphaUcParenBoth' + | 'alphaUcParenR' + | 'alphaUcPeriod' + | 'arabicParenBoth' + | 'arabicParenR' + | 'arabicPeriod' + | 'arabicPlain' + | 'romanLcParenBoth' + | 'romanLcParenR' + | 'romanLcPeriod' + | 'romanUcParenBoth' + | 'romanUcParenR' + | 'romanUcPeriod' + /** + * Number bullets start at + * @since v3.3.0 + * @default 1 + * @example 10 // numbered bullets start with 10 + */ + numberStartAt?: number - // DEPRECATED + // DEPRECATED - /** - * Bullet code (unicode) - * @deprecated v3.3.0 - use `characterCode` - */ - code?: string - /** - * Margin between bullet and text - * @since v3.2.1 - * @deplrecated v3.3.0 - use `indent` - */ - marginPt?: number - /** - * Number to start with (only applies to type:number) - * @deprecated v3.3.0 - use `numberStartAt` - */ - startAt?: number - /** - * Number type - * @deprecated v3.3.0 - use `numberType` - */ - style?: string - } + /** + * Bullet code (unicode) + * @deprecated v3.3.0 - use `characterCode` + */ + code?: string + /** + * Margin between bullet and text + * @since v3.2.1 + * @deplrecated v3.3.0 - use `indent` + */ + marginPt?: number + /** + * Number to start with (only applies to type:number) + * @deprecated v3.3.0 - use `numberStartAt` + */ + startAt?: number + /** + * Number type + * @deprecated v3.3.0 - use `numberType` + */ + style?: string + } /** * Text color * - `HexColor` or `ThemeColor` @@ -390,23 +422,23 @@ export interface TextBaseProps { */ underline?: { style?: - | 'dash' - | 'dashHeavy' - | 'dashLong' - | 'dashLongHeavy' - | 'dbl' - | 'dotDash' - | 'dotDashHeave' - | 'dotDotDash' - | 'dotDotDashHeavy' - | 'dotted' - | 'dottedHeavy' - | 'heavy' - | 'none' - | 'sng' - | 'wavy' - | 'wavyDbl' - | 'wavyHeavy' + | 'dash' + | 'dashHeavy' + | 'dashLong' + | 'dashLongHeavy' + | 'dbl' + | 'dotDash' + | 'dotDashHeave' + | 'dotDotDash' + | 'dotDotDashHeavy' + | 'dotted' + | 'dottedHeavy' + | 'heavy' + | 'none' + | 'sng' + | 'wavy' + | 'wavyDbl' + | 'wavyHeavy' color?: Color } /** @@ -632,6 +664,10 @@ export interface ShapeProps extends PositionProps { * @depreacted v3.3.0 */ lineTail?: 'arrow' | 'diamond' | 'none' | 'oval' | 'stealth' | 'triangle' + /** + * id of shape + */ + sId?: number } // tables ========================================================================================= @@ -1335,20 +1371,20 @@ export interface IChartPropsTitle extends TextBaseProps { } export interface IChartOpts extends IChartPropsAxisCat, - IChartPropsAxisSer, - IChartPropsAxisVal, - IChartPropsBase, - IChartPropsChartBar, - IChartPropsChartDoughnut, - IChartPropsChartLine, - IChartPropsChartPie, - IChartPropsChartRadar, - IChartPropsDataLabel, - IChartPropsDataTable, - IChartPropsLegend, - IChartPropsTitle, - OptsChartGridLine, - PositionProps { + IChartPropsAxisSer, + IChartPropsAxisVal, + IChartPropsBase, + IChartPropsChartBar, + IChartPropsChartDoughnut, + IChartPropsChartLine, + IChartPropsChartPie, + IChartPropsChartRadar, + IChartPropsDataLabel, + IChartPropsDataTable, + IChartPropsLegend, + IChartPropsTitle, + OptsChartGridLine, + PositionProps { /** * Alt Text value ("How would you describe this object and its contents to someone who is blind?") * - PowerPoint: [right-click on a chart] > "Edit Alt Text..." @@ -1487,15 +1523,15 @@ export interface SlideMasterProps { | { rect: {} } | { text: TextProps } | { - placeholder: { - options: PlaceholderProps - /** - * Text to be shown in placeholder (shown until user focuses textbox or adds text) - * - Leave blank to have powerpoint show default phrase (ex: "Click to add title") - */ - text?: string - } - } + placeholder: { + options: PlaceholderProps + /** + * Text to be shown in placeholder (shown until user focuses textbox or adds text) + * - Leave blank to have powerpoint show default phrase (ex: "Click to add title") + */ + text?: string + } + } )[] slideNumber?: SlideNumberProps diff --git a/src/gen-objects.ts b/src/gen-objects.ts index e20cd18ec..10d945698 100644 --- a/src/gen-objects.ts +++ b/src/gen-objects.ts @@ -640,6 +640,12 @@ export function addShapeDefinition(target: PresSlide, shapeName: SHAPE_NAME, opt dashType: options.line.dashType || 'solid', beginArrowType: options.line.beginArrowType || null, endArrowType: options.line.endArrowType || null, + sourceId: options.line.sourceId || null, + targetId: options.line.targetId || null, + sourceAnchorPos: options.line.sourceAnchorPos || (options.line.sourceAnchorPos === 0 ? 0 : null), + targetAnchorPos: options.line.targetAnchorPos || (options.line.targetAnchorPos === 0 ? 0 : null), + isConnector: options.line && (options.line.sourceId != null || options.line.targetId != null), + curveadjust: options.line.curveadjust || null } if (typeof options.line === 'object' && options.line.type !== 'none') options.line = newLineOpts diff --git a/src/gen-xml.ts b/src/gen-xml.ts index eebfee559..145fac282 100644 --- a/src/gen-xml.ts +++ b/src/gen-xml.ts @@ -3,6 +3,7 @@ */ import { + ANCHOR, BULLET_TYPES, CRLF, DEF_BULLET_MARGIN, @@ -18,6 +19,7 @@ import { } from './core-enums' import { IChartOpts, + IDCoord, ImageProps, IPresentationProps, ISlideObject, @@ -79,6 +81,23 @@ let imageSizingXml = { }, } +/** + * this function finds the elements of a list based on its id + * it is only really used for finding shapes in the coordinates list + * this is so the program can caluclate the posistion of connecting lines by itself + * @param {IDCoord[]} list + * @param {number} id + */ + +function FindById(list: IDCoord[], id: number): IDCoord { + for (const element of list) { + if (element.id == id) { + return element + } + } + console.error("no element in list matching id") +} + /** * Transforms a slide or slideLayout to resulting XML string - Creates `ppt/slide*.xml` * @param {PresSlide|SlideLayout} slideObject - slide object created within createSlideObject @@ -104,8 +123,43 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { strSlideXml += '' strSlideXml += '' + let IDs: number[] = [] + let coordinates: IDCoord[] = [] + // STEP 3: Loop over all Slide.data objects and add them to this slide slide._slideObjects.forEach((slideItemObj: ISlideObject, idx: number) => { + if (slideItemObj.options != undefined) { + if (slideItemObj.options.sId != undefined) { + if (IDs.indexOf(slideItemObj.options.sId) > -1) { + throw "ID is already in use, object / shape id cannot be the same for multiple objects / shapes"; + } else { + IDs.push(slideItemObj.options.sId); + } + + } else { + if (IDs.indexOf(idx + 2) > -1) { + throw "an sID used matched an automatically generated ID try using a higher number"; + } else { + IDs.push(idx + 2); + } + + } + } + + + + if (slideItemObj.options != undefined) { + coordinates.push({ + id: IDs.at(-1), + posistion: { + x: slideItemObj.options.x, + y: slideItemObj.options.y, + w: slideItemObj.options.w, + h: slideItemObj.options.h + } + }) + } + let x = 0, y = 0, cx = getSmartParseNumber('75%', 'X', slide._presLayout), @@ -127,10 +181,107 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { // A: Set option vars slideItemObj.options = slideItemObj.options || {} - if (typeof slideItemObj.options.x !== 'undefined') x = getSmartParseNumber(slideItemObj.options.x, 'X', slide._presLayout) - if (typeof slideItemObj.options.y !== 'undefined') y = getSmartParseNumber(slideItemObj.options.y, 'Y', slide._presLayout) - if (typeof slideItemObj.options.w !== 'undefined') cx = getSmartParseNumber(slideItemObj.options.w, 'X', slide._presLayout) - if (typeof slideItemObj.options.h !== 'undefined') cy = getSmartParseNumber(slideItemObj.options.h, 'Y', slide._presLayout) + //calculates the coordinates for connecting lines, only works for squares with number posistions ( so no percentages) + if (slideItemObj.options.line != undefined) { + if (slideItemObj.options.line.isConnector) { + const source = FindById(coordinates, slideItemObj.options.line.sourceId); + const target = FindById(coordinates, slideItemObj.options.line.targetId); + + + let deltaX = 0; + let deltaY = 0; + //this does not work for triangle because anchorpoint is outside the enum!!!! + switch (slideItemObj.options.line.sourceAnchorPos) { + case ANCHOR.TOP: + deltaX = Number(source.posistion.w) / 2; + x = Number(source.posistion.x) + deltaX; + y = Number(source.posistion.y) + break; + case ANCHOR.BOTTOM: + deltaX = Number(source.posistion.w) / 2; + x = Number(source.posistion.x) + deltaX; + deltaY = Number(source.posistion.h) + y = Number(source.posistion.y) + deltaY + break; + case ANCHOR.LEFT: + deltaY = Number(source.posistion.h) / 2; + y = Number(source.posistion.y) + deltaY; + x = Number(source.posistion.x); + break; + case ANCHOR.RIGHT: + deltaY = Number(source.posistion.h) / 2; + y = Number(source.posistion.y) + deltaY; + deltaX = Number(source.posistion.w) + x = Number(source.posistion.x) + deltaX; + break; + } + + let tx = 0; + let ty = 0; + + switch (slideItemObj.options.line.targetAnchorPos) { + case ANCHOR.TOP: + deltaX = Number(target.posistion.w) / 2; + tx = Number(target.posistion.x) + deltaX; + ty = Number(target.posistion.y) + break; + case ANCHOR.BOTTOM: + deltaX = Number(target.posistion.w) / 2; + tx = Number(target.posistion.x) + deltaX; + deltaY = Number(target.posistion.h) + ty = Number(target.posistion.y) + deltaY + break; + case ANCHOR.LEFT: + deltaY = Number(target.posistion.h) / 2; + ty = Number(target.posistion.y) + deltaY; + tx = Number(target.posistion.x); + break; + case ANCHOR.RIGHT: + deltaY = Number(target.posistion.h) / 2; + ty = Number(target.posistion.y) + deltaY; + deltaX = Number(target.posistion.w) + tx = Number(target.posistion.x) + deltaX; + break; + } + + if (tx > x) { + cx = tx - x + + slideItemObj.options.flipH = false; + } else { + slideItemObj.options.flipH = true; + cx = x - tx + x = tx; + } + if (ty > y) { + cy = ty - y; + slideItemObj.options.flipV = false; + } else { + cy = y - ty + y = ty; + slideItemObj.options.flipV = true; + } + + x = getSmartParseNumber(x, "X", slide._presLayout); + y = getSmartParseNumber(y, "Y", slide._presLayout); + cx = getSmartParseNumber(cx, "X", slide._presLayout); + cy = getSmartParseNumber(cy, "X", slide._presLayout); + + + } else { + if (typeof slideItemObj.options.x !== 'undefined') x = getSmartParseNumber(slideItemObj.options.x, 'X', slide._presLayout) + if (typeof slideItemObj.options.y !== 'undefined') y = getSmartParseNumber(slideItemObj.options.y, 'Y', slide._presLayout) + if (typeof slideItemObj.options.w !== 'undefined') cx = getSmartParseNumber(slideItemObj.options.w, 'X', slide._presLayout) + if (typeof slideItemObj.options.h !== 'undefined') cy = getSmartParseNumber(slideItemObj.options.h, 'Y', slide._presLayout) + } + } else { + if (typeof slideItemObj.options.x !== 'undefined') x = getSmartParseNumber(slideItemObj.options.x, 'X', slide._presLayout) + if (typeof slideItemObj.options.y !== 'undefined') y = getSmartParseNumber(slideItemObj.options.y, 'Y', slide._presLayout) + if (typeof slideItemObj.options.w !== 'undefined') cx = getSmartParseNumber(slideItemObj.options.w, 'X', slide._presLayout) + if (typeof slideItemObj.options.h !== 'undefined') cy = getSmartParseNumber(slideItemObj.options.h, 'Y', slide._presLayout) + } + + // If using a placeholder then inherit it's position if (placeholderObj) { @@ -168,9 +319,8 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { '' + ' ' + '' - strXml += `` + strXml += `` strXml += '' // + ' '; // TODO: Support banded rows, first/last row, etc. @@ -208,7 +358,7 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { // We have to build an actual grid now /* EX: (A0:rowspan=3, B1:rowspan=2, C1:colspan=2) - + /------|------|------|------\ | A0 | B0 | C0 | D0 | | | B1 | C1 | | @@ -217,7 +367,7 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { */ // A: add _hmerge cell for colspan. should reserve rowspan arrTabRows.forEach(cells => { - for (let cIdx = 0; cIdx < cells.length; ) { + for (let cIdx = 0; cIdx < cells.length;) { let cell = cells[cIdx] let colspan = cell.options?.colspan let rowspan = cell.options?.rowspan @@ -256,7 +406,7 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { else if (slideItemObj.options.cy || slideItemObj.options.h) intRowH = Math.round( (slideItemObj.options.h ? inch2Emu(slideItemObj.options.h) : typeof slideItemObj.options.cy === 'number' ? slideItemObj.options.cy : 1) / - arrTabRows.length + arrTabRows.length ) // B: Start row @@ -289,30 +439,30 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { let cellOpts = cell.options || ({} as TableCell['options']) cell.options = cellOpts - // B: Inherit some options from table when cell options dont exist - // @see: http://officeopenxml.com/drwTableCellProperties-alignment.php - ;['align', 'bold', 'border', 'color', 'fill', 'fontFace', 'fontSize', 'margin', 'underline', 'valign'].forEach(name => { - if (objTabOpts[name] && !cellOpts[name] && cellOpts[name] !== 0) cellOpts[name] = objTabOpts[name] - }) + // B: Inherit some options from table when cell options dont exist + // @see: http://officeopenxml.com/drwTableCellProperties-alignment.php + ;['align', 'bold', 'border', 'color', 'fill', 'fontFace', 'fontSize', 'margin', 'underline', 'valign'].forEach(name => { + if (objTabOpts[name] && !cellOpts[name] && cellOpts[name] !== 0) cellOpts[name] = objTabOpts[name] + }) let cellValign = cellOpts.valign ? ' anchor="' + - cellOpts.valign - .replace(/^c$/i, 'ctr') - .replace(/^m$/i, 'ctr') - .replace('center', 'ctr') - .replace('middle', 'ctr') - .replace('top', 't') - .replace('btm', 'b') - .replace('bottom', 'b') + - '"' + cellOpts.valign + .replace(/^c$/i, 'ctr') + .replace(/^m$/i, 'ctr') + .replace('center', 'ctr') + .replace('middle', 'ctr') + .replace('top', 't') + .replace('btm', 'b') + .replace('bottom', 'b') + + '"' : '' let fillColor = cell._optImp && cell._optImp.fill && cell._optImp.fill.color ? cell._optImp.fill.color : cell._optImp && cell._optImp.fill && typeof cell._optImp.fill === 'string' - ? cell._optImp.fill - : '' + ? cell._optImp.fill + : '' fillColor = fillColor || (cellOpts.fill && cellOpts.fill.color) ? cellOpts.fill.color : cellOpts.fill && typeof cellOpts.fill === 'string' ? cellOpts.fill : '' let cellFill = fillColor ? `${createColorElement(fillColor)}` : '' @@ -353,9 +503,8 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { if (cellOpts.border[obj.idx].type !== 'none') { strXml += `` strXml += `${createColorElement(cellOpts.border[obj.idx].color)}` - strXml += `` + strXml += `` strXml += `` } else { strXml += `` @@ -388,117 +537,50 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { case SLIDE_OBJECT_TYPES.text: case SLIDE_OBJECT_TYPES.placeholder: - let shapeName = slideItemObj.options.shapeName ? encodeXmlEntities(slideItemObj.options.shapeName) : `Object${idx + 1}` - - // Lines can have zero cy, but text should not - if (!slideItemObj.options.line && cy === 0) cy = EMU * 0.3 - - // Margin/Padding/Inset for textboxes - if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {} - if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) { - slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0) - slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0) - slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0) - slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0) - } else if (typeof slideItemObj.options.margin === 'number') { - slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin) - slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin) - slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin) - slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin) - } + if (slideItemObj.options.line.isConnector) { + let shapeName = slideItemObj.options.shapeName ? encodeXmlEntities(slideItemObj.options.shapeName) : `Object${idx + 1}` + + // Lines can have zero cy, but text should not + if (!slideItemObj.options.line && cy === 0) cy = EMU * 0.3 + + // Margin/Padding/Inset for textboxes + if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {} + if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) { + slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0) + slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0) + slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0) + slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0) + } else if (typeof slideItemObj.options.margin === 'number') { + slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin) + } - // A: Start SHAPE ======================================================= - strSlideXml += '' + // A: Start SHAPE ======================================================= + strSlideXml += '' - // B: The addition of the "txBox" attribute is the sole determiner of if an object is a shape or textbox - strSlideXml += `` - // - if (slideItemObj.options.hyperlink && slideItemObj.options.hyperlink.url) - strSlideXml += - '' - if (slideItemObj.options.hyperlink && slideItemObj.options.hyperlink.slide) - strSlideXml += - '' - // - strSlideXml += '' - strSlideXml += '' : '/>') - strSlideXml += `${slideItemObj._type === 'placeholder' ? genXmlPlaceholder(slideItemObj) : genXmlPlaceholder(placeholderObj)}` - strSlideXml += '' - strSlideXml += `` - strSlideXml += `` - strSlideXml += `` - - if (slideItemObj.shape === 'custGeom') { - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - - strSlideXml += '' - strSlideXml += `` - - slideItemObj.options.points?.map((point, i) => { - if ('curve' in point) { - switch (point.curve.type) { - case 'arc': - strSlideXml += `` - break - case 'cubic': - strSlideXml += ` - - - - ` - break - case 'quadratic': - strSlideXml += ` - - - ` - break - default: - break - } - } else if ('close' in point) { - strSlideXml += `` - } else if (point.moveTo || i === 0) { - strSlideXml += `` - } else { - strSlideXml += `` - } - }) + // B: The addition of the "txBox" attribute is the sole determiner of if an object is a shape or textbox + if (slideItemObj.options.sId != undefined) { + strSlideXml += `` + } + else { + strSlideXml += `` + } + strSlideXml += '' + //TODO add idx feature + strSlideXml += ` + + + ` ; + strSlideXml += '' + strSlideXml += '' + strSlideXml += `` + strSlideXml += `` + strSlideXml += `` - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - } else { strSlideXml += '' - if (slideItemObj.options.rectRadius) { - strSlideXml += `` - } else if (slideItemObj.options.angleRange) { + if (slideItemObj.options.angleRange) { for (let i = 0; i < 2; i++) { const angle = slideItemObj.options.angleRange[i] strSlideXml += `` @@ -508,61 +590,243 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { strSlideXml += `` } } + + if (slideItemObj.options.line.curveadjust) { + let i = 1 + for (const adjustments in slideItemObj.options.line.curveadjust) { + strSlideXml += `` + i++ + } + + } + + strSlideXml += '' - } - // Option: FILL - strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : '' - - // shape Type: LINE: line color - if (slideItemObj.options.line) { - strSlideXml += slideItemObj.options.line.width ? `` : '' - if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line) - if (slideItemObj.options.line.dashType) strSlideXml += `` - if (slideItemObj.options.line.beginArrowType) strSlideXml += `` - if (slideItemObj.options.line.endArrowType) strSlideXml += `` - // FUTURE: `endArrowSize` < a: headEnd type = "arrow" w = "lg" len = "lg" /> 'sm' | 'med' | 'lg'(values are 1 - 9, making a 3x3 grid of w / len possibilities) - strSlideXml += '' - } - // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php - if (slideItemObj.options.shadow) { - slideItemObj.options.shadow.type = slideItemObj.options.shadow.type || 'outer' - slideItemObj.options.shadow.blur = valToPts(slideItemObj.options.shadow.blur || 8) - slideItemObj.options.shadow.offset = valToPts(slideItemObj.options.shadow.offset || 4) - slideItemObj.options.shadow.angle = Math.round((slideItemObj.options.shadow.angle || 270) * 60000) - slideItemObj.options.shadow.opacity = Math.round((slideItemObj.options.shadow.opacity || 0.75) * 100000) - slideItemObj.options.shadow.color = slideItemObj.options.shadow.color || DEF_TEXT_SHADOW.color - - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - strSlideXml += '' - } + // Option: FILL + strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : '' - /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export) - // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so... - if ( slideItemObj.options.textWrap ) { - strSlideXml += '' - + '' - + '' - + '' - + ''; + // shape Type: LINE: line color + if (slideItemObj.options.line) { + strSlideXml += slideItemObj.options.line.width ? `` : '' + if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line) + if (slideItemObj.options.line.dashType) strSlideXml += `` + if (slideItemObj.options.line.beginArrowType) strSlideXml += `` + if (slideItemObj.options.line.endArrowType) strSlideXml += `` + // FUTURE: `endArrowSize` < a: headEnd type = "arrow" w = "lg" len = "lg" /> 'sm' | 'med' | 'lg'(values are 1 - 9, making a 3x3 grid of w / len possibilities) + strSlideXml += '' } - */ - // B: Close shape Properties - strSlideXml += '' + // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php + if (slideItemObj.options.shadow) { + slideItemObj.options.shadow.type = slideItemObj.options.shadow.type || 'outer' + slideItemObj.options.shadow.blur = valToPts(slideItemObj.options.shadow.blur || 8) + slideItemObj.options.shadow.offset = valToPts(slideItemObj.options.shadow.offset || 4) + slideItemObj.options.shadow.angle = Math.round((slideItemObj.options.shadow.angle || 270) * 60000) + slideItemObj.options.shadow.opacity = Math.round((slideItemObj.options.shadow.opacity || 0.75) * 100000) + slideItemObj.options.shadow.color = slideItemObj.options.shadow.color || DEF_TEXT_SHADOW.color + + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + } + + // B: Close shape Properties + strSlideXml += '' + + // C: Add formatted text (text body "bodyPr") + strSlideXml += genXmlTextBody(slideItemObj) + + // LAST: Close SHAPE ======================================================= + strSlideXml += '' + } else { + let shapeName = slideItemObj.options.shapeName ? encodeXmlEntities(slideItemObj.options.shapeName) : `Object${idx + 1}` + + // Lines can have zero cy, but text should not + if (!slideItemObj.options.line && cy === 0) cy = EMU * 0.3 + + // Margin/Padding/Inset for textboxes + if (!slideItemObj.options._bodyProp) slideItemObj.options._bodyProp = {} + if (slideItemObj.options.margin && Array.isArray(slideItemObj.options.margin)) { + slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin[0] || 0) + slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin[1] || 0) + slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin[2] || 0) + slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin[3] || 0) + } else if (typeof slideItemObj.options.margin === 'number') { + slideItemObj.options._bodyProp.lIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.rIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.bIns = valToPts(slideItemObj.options.margin) + slideItemObj.options._bodyProp.tIns = valToPts(slideItemObj.options.margin) + } + + // A: Start SHAPE ======================================================= + strSlideXml += '' + + // B: The addition of the "txBox" attribute is the sole determiner of if an object is a shape or textbox + if (slideItemObj.options.sId != undefined) { + strSlideXml += `` + } + else { + strSlideXml += `` + } + // + if (slideItemObj.options.hyperlink && slideItemObj.options.hyperlink.url) + strSlideXml += + '' + if (slideItemObj.options.hyperlink && slideItemObj.options.hyperlink.slide) + strSlideXml += + '' + // + strSlideXml += '' + strSlideXml += '' : '/>') + strSlideXml += `${slideItemObj._type === 'placeholder' ? genXmlPlaceholder(slideItemObj) : genXmlPlaceholder(placeholderObj)}` + strSlideXml += '' + strSlideXml += `` + strSlideXml += `` + strSlideXml += `` + + if (slideItemObj.shape === 'custGeom') { + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + + strSlideXml += '' + strSlideXml += `` + + slideItemObj.options.points?.map((point, i) => { + if ('curve' in point) { + switch (point.curve.type) { + case 'arc': + strSlideXml += `` + break + case 'cubic': + strSlideXml += ` + + + + ` + break + case 'quadratic': + strSlideXml += ` + + + ` + break + default: + break + } + } else if ('close' in point) { + strSlideXml += `` + } else if (point.moveTo || i === 0) { + strSlideXml += `` + } else { + strSlideXml += `` + } + }) + + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + } else { + strSlideXml += '' + if (slideItemObj.options.rectRadius) { + strSlideXml += `` + } else if (slideItemObj.options.angleRange) { + for (let i = 0; i < 2; i++) { + const angle = slideItemObj.options.angleRange[i] + strSlideXml += `` + } + + if (slideItemObj.options.arcThicknessRatio) { + strSlideXml += `` + } + } + strSlideXml += '' + } + + // Option: FILL + strSlideXml += slideItemObj.options.fill ? genXmlColorSelection(slideItemObj.options.fill) : '' + + // shape Type: LINE: line color + if (slideItemObj.options.line) { + strSlideXml += slideItemObj.options.line.width ? `` : '' + if (slideItemObj.options.line.color) strSlideXml += genXmlColorSelection(slideItemObj.options.line) + if (slideItemObj.options.line.dashType) strSlideXml += `` + if (slideItemObj.options.line.beginArrowType) strSlideXml += `` + if (slideItemObj.options.line.endArrowType) strSlideXml += `` + // FUTURE: `endArrowSize` < a: headEnd type = "arrow" w = "lg" len = "lg" /> 'sm' | 'med' | 'lg'(values are 1 - 9, making a 3x3 grid of w / len possibilities) + strSlideXml += '' + } + + // EFFECTS > SHADOW: REF: @see http://officeopenxml.com/drwSp-effects.php + if (slideItemObj.options.shadow) { + slideItemObj.options.shadow.type = slideItemObj.options.shadow.type || 'outer' + slideItemObj.options.shadow.blur = valToPts(slideItemObj.options.shadow.blur || 8) + slideItemObj.options.shadow.offset = valToPts(slideItemObj.options.shadow.offset || 4) + slideItemObj.options.shadow.angle = Math.round((slideItemObj.options.shadow.angle || 270) * 60000) + slideItemObj.options.shadow.opacity = Math.round((slideItemObj.options.shadow.opacity || 0.75) * 100000) + slideItemObj.options.shadow.color = slideItemObj.options.shadow.color || DEF_TEXT_SHADOW.color + + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + strSlideXml += '' + } + + /* TODO: FUTURE: Text wrapping (copied from MS-PPTX export) + // Commented out b/c i'm not even sure this works - current code produces text that wraps in shapes and textboxes, so... + if ( slideItemObj.options.textWrap ) { + strSlideXml += '' + + '' + + '' + + '' + + ''; + } + */ - // C: Add formatted text (text body "bodyPr") - strSlideXml += genXmlTextBody(slideItemObj) + // B: Close shape Properties + strSlideXml += '' - // LAST: Close SHAPE ======================================================= - strSlideXml += '' + // C: Add formatted text (text body "bodyPr") + strSlideXml += genXmlTextBody(slideItemObj) + + // LAST: Close SHAPE ======================================================= + strSlideXml += '' + } break case SLIDE_OBJECT_TYPES.image: @@ -574,15 +838,17 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { strSlideXml += '' strSlideXml += ' ' - strSlideXml += `` + if (slideItemObj.options.sId != undefined) { + strSlideXml += `` + } else { + strSlideXml += `` + } if (slideItemObj.hyperlink && slideItemObj.hyperlink.url) - strSlideXml += `` + strSlideXml += `` if (slideItemObj.hyperlink && slideItemObj.hyperlink.slide) - strSlideXml += `` + strSlideXml += `` strSlideXml += ' ' strSlideXml += ' ' strSlideXml += ' ' + genXmlPlaceholder(placeholderObj) + '' @@ -683,7 +949,11 @@ function slideObjectToXml(slide: PresSlide | SlideLayout): string { let chartOpts = slideItemObj.options as IChartOpts strSlideXml += '' strSlideXml += ' ' - strSlideXml += ` ` + if (slideItemObj.options.sId != undefined) { + strSlideXml += ` ` + } else { + strSlideXml += ` ` + } strSlideXml += ' ' strSlideXml += ` ${genXmlPlaceholder(placeholderObj)}` strSlideXml += ' ' @@ -812,41 +1082,41 @@ function slideObjectRelationsToXml(slide: PresSlide | SlideLayout, defaultRels: '' } }) - ;(slide._relsChart || []).forEach((rel: ISlideRelChart) => { - lastRid = Math.max(lastRid, rel.rId) - strXml += '' - }) - ;(slide._relsMedia || []).forEach((rel: ISlideRelMedia) => { - lastRid = Math.max(lastRid, rel.rId) - if (rel.type.toLowerCase().indexOf('image') > -1) { - strXml += '' - } else if (rel.type.toLowerCase().indexOf('audio') > -1) { - // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style - if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) - strXml += '' - else - strXml += - '' - } else if (rel.type.toLowerCase().indexOf('video') > -1) { - // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style - if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) - strXml += '' - else - strXml += - '' - } else if (rel.type.toLowerCase().indexOf('online') > -1) { - // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style - if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) - strXml += '' - else - strXml += - '' - } - }) + ; (slide._relsChart || []).forEach((rel: ISlideRelChart) => { + lastRid = Math.max(lastRid, rel.rId) + strXml += '' + }) + ; (slide._relsMedia || []).forEach((rel: ISlideRelMedia) => { + lastRid = Math.max(lastRid, rel.rId) + if (rel.type.toLowerCase().indexOf('image') > -1) { + strXml += '' + } else if (rel.type.toLowerCase().indexOf('audio') > -1) { + // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style + if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) + strXml += '' + else + strXml += + '' + } else if (rel.type.toLowerCase().indexOf('video') > -1) { + // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style + if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) + strXml += '' + else + strXml += + '' + } else if (rel.type.toLowerCase().indexOf('online') > -1) { + // As media has *TWO* rel entries per item, check for first one, if found add second rel with alt style + if (strXml.indexOf(' Target="' + rel.Target + '"') > -1) + strXml += '' + else + strXml += + '' + } + }) // STEP 2: Add default rels defaultRels.forEach((rel, idx) => { @@ -923,12 +1193,10 @@ function genXmlParagraphProperties(textObj: ISlideObject | TextProps, isDefault: if (textObj.options.bullet.type) { if (textObj.options.bullet.type.toString().toLowerCase() === 'number') { - paragraphPropXml += ` marL="${ - textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL - }" indent="-${bulletMarL}"` - strXmlBullet = `` + paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL + }" indent="-${bulletMarL}"` + strXmlBullet = `` } } else if (textObj.options.bullet.characterCode) { let bulletCode = `&#x${textObj.options.bullet.characterCode};` @@ -939,9 +1207,8 @@ function genXmlParagraphProperties(textObj: ISlideObject | TextProps, isDefault: bulletCode = BULLET_TYPES['DEFAULT'] } - paragraphPropXml += ` marL="${ - textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL - }" indent="-${bulletMarL}"` + paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL + }" indent="-${bulletMarL}"` strXmlBullet = '' } else if (textObj.options.bullet.code) { // @deprecated `bullet.code` v3.3.0 @@ -953,20 +1220,17 @@ function genXmlParagraphProperties(textObj: ISlideObject | TextProps, isDefault: bulletCode = BULLET_TYPES['DEFAULT'] } - paragraphPropXml += ` marL="${ - textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL - }" indent="-${bulletMarL}"` + paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL + }" indent="-${bulletMarL}"` strXmlBullet = '' } else { - paragraphPropXml += ` marL="${ - textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL - }" indent="-${bulletMarL}"` + paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL + }" indent="-${bulletMarL}"` strXmlBullet = `` } } else if (textObj.options.bullet === true) { - paragraphPropXml += ` marL="${ - textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL - }" indent="-${bulletMarL}"` + paragraphPropXml += ` marL="${textObj.options.indentLevel && textObj.options.indentLevel > 0 ? bulletMarL + bulletMarL * textObj.options.indentLevel : bulletMarL + }" indent="-${bulletMarL}"` strXmlBullet = `` } else if (textObj.options.bullet === false) { // We only add this when the user explicitely asks for no bullet, otherwise, it can override the master defaults! @@ -1045,13 +1309,11 @@ function genXmlTextRunProperties(opts: ObjectOptions | TextPropsOptions, isDefau else if (!opts.hyperlink.url && !opts.hyperlink.slide) throw new Error("ERROR: 'hyperlink requires either `url` or `slide`'") else if (opts.hyperlink.url) { //runProps += ''+ genXmlColorSelection('0000FF') +''; // Breaks PPT2010! (Issue#74) - runProps += `' : '/>'}` + runProps += `' : '/>'}` } else if (opts.hyperlink.slide) { - runProps += `' : '/>'}` + runProps += `' : '/>'}` } if (opts.color) { runProps += ' ' @@ -1419,7 +1681,7 @@ export function makeXmlContTypes(slides: PresSlide[], slideLayouts: SlideLayout[ strXml += '' // NOTE: Hard-Code this extension as it wont be created in loop below (as extn !== type) strXml += '' // NOTE: Hard-Code this extension as it wont be created in loop below (as extn !== type) slides.forEach(slide => { - ;(slide._relsMedia || []).forEach(rel => { + ; (slide._relsMedia || []).forEach(rel => { if (rel.type !== 'image' && rel.type !== 'online' && rel.type !== 'chart' && rel.extn !== 'm4v' && strXml.indexOf(rel.type) === -1) { strXml += '' } @@ -1455,9 +1717,9 @@ export function makeXmlContTypes(slides: PresSlide[], slideLayouts: SlideLayout[ '' - ;(layout._relsChart || []).forEach(rel => { - strXml += ' ' - }) + ; (layout._relsChart || []).forEach(rel => { + strXml += ' ' + }) }) // STEP 5: Add notes slide(s) diff --git a/src/pptxgen.ts b/src/pptxgen.ts index 427b29095..5a99143b2 100644 --- a/src/pptxgen.ts +++ b/src/pptxgen.ts @@ -76,6 +76,7 @@ import { SchemeColor, ShapeType, WRITE_OUTPUT_TYPE, + ANCHOR } from './core-enums' import { AddSlideProps, @@ -282,6 +283,10 @@ export default class PptxGenJS implements IPresentationProps { public get shapes(): typeof SHAPE_TYPE { return this._shapes } + private _anchor = ANCHOR + public get anchor(): typeof ANCHOR { + return this._anchor + } constructor() { // Set available layouts