Skip to content

Commit

Permalink
Merge pull request #36 from heavysixer/org-chart-composer-support
Browse files Browse the repository at this point in the history
Org chart composer support
  • Loading branch information
gregdolley committed Feb 20, 2019
2 parents ae070a5 + 71227da commit 9cf323a
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 12 deletions.
10 changes: 10 additions & 0 deletions __tests__/presentation.test.js
Expand Up @@ -255,6 +255,16 @@ describe('Presentation Module', () => {
.cy(100)
.href('http://www.google.com');
})
.addShape(shape => {
shape
.type(PPTX.ShapeTypes.TRIANGLE)
.x(300)
.y(400)
.cx(250)
.cy(100)
.text('link to slide 2')
.href('#2');
})
.addText(text => {
text
.value('This is a hyperlink! Will this go to google?')
Expand Down
27 changes: 22 additions & 5 deletions __tests__/shapes.test.js
Expand Up @@ -35,15 +35,32 @@ describe('Shape Module', () => {
'Auto-fit test - width and height of shape object should not be specified. This shape should auto-grow to fit this text. ',
autoFit: true,
});
// TODO: FIXME: auto-shrink text broke (well, it didn't work perfectly before either - when opening a PPTX with an auto-shrink text shape, the text
// never auto-shrunk until you changed a property on _any_ other shape manually [this was with an older Office version though]; then
// PowerPoint would re-render the slide and auto-resize the text.) _Now_, the entire PPTX gets corrupted and it opens up blank. This
// seems to coincide with a Windows 10 Office update which also broke the slide linking hack of being able to use "#<slide num>" as
// the URL of a hyperlink to link to another slide. However, the slide link hack doesn't corrupt the PPTX, it simply makes the link
// not do anything.
// slide.addShape({
// type: PPTX.ShapeTypes.TRIANGLE,
// x: 250,
// y: 400,
// cx: 200,
// cy: 100,
// text: 'auto-shrink text: this text should shrink',
// shrinkText: true,
// color: 'FF00AA',
// });

slide.addShape({
type: PPTX.ShapeTypes.TRIANGLE,
type: PPTX.ShapeTypes.RECTANGLE,
x: 250,
y: 400,
y: 25,
cx: 200,
cy: 100,
text: 'auto-shrink text: this text should shrink',
shrinkText: true,
color: 'FF00AA',
text: { textSegments: [{ text: 'Greg Dolley\r\n', fontBold: true }, { text: 'CEO' }] },
color: 'D9D9D9',
textVerticalAlign: 'top',
});
});
});
Expand Down
32 changes: 29 additions & 3 deletions lib/factories/ppt/slides.js
Expand Up @@ -92,6 +92,8 @@ class SlideFactory {
}

addHyperlinkToSlideRelationship(slide, target) {
if (!target) return '';

let relsKey = `ppt/slides/_rels/${slide.name}.xml.rels`;
let rId = `rId${this.content[relsKey]['Relationships']['Relationship'].length + 1}`;

Expand All @@ -107,6 +109,23 @@ class SlideFactory {
return rId;
}

addSlideTargetRelationship(slide, target) {
if (!target) return '';

let relsKey = `ppt/slides/_rels/${slide.name}.xml.rels`;
let rId = `rId${this.content[relsKey]['Relationships']['Relationship'].length + 1}`;

this.content[relsKey]['Relationships']['Relationship'].push({
$: {
Id: rId,
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide',
Target: target,
},
});

return rId;
}

addChartToSlideRelationship(slide, chartName) {
let relsKey = `ppt/slides/_rels/${slide.name}.xml.rels`;
let rId = `rId${this.content[relsKey]['Relationships']['Relationship'].length + 1}`;
Expand Down Expand Up @@ -193,8 +212,9 @@ class SlideFactory {
],
};

if (image.options.url) {
if (typeof image.options.url === 'string' && image.options.url.length > 0) {
newImageBlock['p:nvPicPr'][0]['p:cNvPr'][0]['a:hlinkClick'] = [{ $: { 'r:id': image.options.rIdForHyperlink } }];
if (image.options.url[0] === '#') newImageBlock['p:nvPicPr'][0]['p:cNvPr'][0]['a:hlinkClick'][0]['$'].action = 'ppaction://hlinksldjump';
}

this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0]['p:pic'].push(newImageBlock);
Expand Down Expand Up @@ -228,7 +248,12 @@ class SlideFactory {
PptFactoryHelper.setTextBodyProperties(newTextBlock['p:txBody'][0]['a:bodyPr'][0], options);
PptFactoryHelper.addLinePropertiesToBlock(newTextBlock['p:spPr'][0], options.line);

if (options.url) newTextBlock['p:txBody'][0]['a:p'][0]['a:r'][0]['a:rPr'][0]['a:hlinkClick'] = [{ $: { 'r:id': options.rIdForHyperlink } }];
if (typeof options.url === 'string' && options.url.length > 0) {
newTextBlock['p:txBody'][0]['a:p'][0]['a:r'][0]['a:rPr'][0]['a:hlinkClick'] = [{ $: { 'r:id': options.rIdForHyperlink } }];

if (options.url[0] === '#')
newTextBlock['p:txBody'][0]['a:p'][0]['a:r'][0]['a:rPr'][0]['a:hlinkClick'][0]['$'].action = 'ppaction://hlinksldjump';
}

this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0]['p:sp'].push(newTextBlock);

Expand Down Expand Up @@ -263,8 +288,9 @@ class SlideFactory {

this.content[slideKey]['p:sld']['p:cSld'][0]['p:spTree'][0]['p:sp'].push(newShapeBlock);

if (options.url) {
if (typeof options.url === 'string' && options.url.length > 0) {
newShapeBlock['p:nvSpPr'][0]['p:cNvPr'][0]['a:hlinkClick'] = [{ $: { 'r:id': options.rIdForHyperlink } }];
if (options.url[0] === '#') newShapeBlock['p:nvSpPr'][0]['p:cNvPr'][0]['a:hlinkClick'][0]['$'].action = 'ppaction://hlinksldjump';
}

return newShapeBlock;
Expand Down
68 changes: 64 additions & 4 deletions lib/helpers/ppt-factory-helper.js
Expand Up @@ -37,7 +37,12 @@ class PptFactoryHelper {
}
}

options.rIdForHyperlink = pptFactory.slideFactory.addHyperlinkToSlideRelationship(slide, options.url);
if (options.url[0] === '#') {
let slideNum = options.url.substr(1);
options.rIdForHyperlink = pptFactory.slideFactory.addSlideTargetRelationship(slide, `slide${slideNum}.xml`);
} else {
options.rIdForHyperlink = pptFactory.slideFactory.addHyperlinkToSlideRelationship(slide, options.url);
}
}

static createBaseShapeBlock(objectId, objectName, x, y, cx, cy) {
Expand Down Expand Up @@ -880,6 +885,9 @@ class PptFactoryHelper {

// block should be the <p:txBody> node
static addTextValuesToBlock(block, textBox, options) {
const CRLF = '\r\n';

let textLines;
let textValues = textBox.textValue || textBox.bulletPoints;

if (Array.isArray(textValues)) {
Expand All @@ -889,7 +897,14 @@ class PptFactoryHelper {
PptFactoryHelper.addBulletPointsToBlock(block['a:p'], textValues[i], 0, options);
}
} else if (typeof textValues === 'string' || typeof textValues === 'number') {
PptFactoryHelper.createParagraphBlock(block, textValues, options);
block['a:p'] = [];
textValues = textValues.replace(/\r*\n/g, CRLF);
textLines = textValues.indexOf(CRLF) > -1 ? textValues.split(CRLF) : [textValues];

textLines.forEach(t => block['a:p'].push(PptFactoryHelper.createParagraphBlock(block, t, options)));
} else if (typeof textValues === 'object') {
block['a:p'] = [];
PptFactoryHelper.addTextSegmentsBlock(block['a:p'], textValues, options);
}
}

Expand All @@ -905,12 +920,14 @@ class PptFactoryHelper {
}

static createParagraphBlock(block, textValue, options) {
let paragraphBlock = (block['a:p'] = PptFactoryHelper.createEmptyParagraphPropertiesBlock())[0];
let paragraphBlock = PptFactoryHelper.createEmptyParagraphPropertiesBlock()[0];
paragraphBlock['a:r'] = [{ 'a:rPr': [{ $: { lang: 'en-US', smtClean: '0' } }], 'a:t': textValue }];
let textRunPropertyBlock = paragraphBlock['a:r'][0]['a:rPr'][0];

PptFactoryHelper.setupTextRunPropertiesBlock(textRunPropertyBlock, options);
PptFactoryHelper.addParagraphPropertiesToBlock(paragraphBlock, options);

return paragraphBlock;
}

static addBulletPointsToBlock(masterParagraphNode, textValue, indentLevel, options) {
Expand Down Expand Up @@ -1003,11 +1020,54 @@ class PptFactoryHelper {
}
}

static addTextSegmentsBlock(masterParagraphNode, textValue, options) {
const CRLF = '\r\n';

if (typeof textValue === 'object') {
PptFactoryHelper.convertTextPropertiesToOptionsAndMerge(textValue, options);

if (textValue.textSegments !== undefined) {
for (let segment of textValue.textSegments) {
let line = segment.text.replace(/\r*\n/g, CRLF);
let splitLines = line.indexOf(CRLF) > -1 ? line.split(CRLF) : [line];
let textSegments = [];

splitLines.forEach(t => {
if (t) textSegments.push({ textSegments: [Object.assign(segment, { text: t })], options: textValue.options });
});
textSegments.forEach(ts => PptFactoryHelper.createMultiFormattedText(masterParagraphNode, ts, ts.options));
}
}
}
}

static createMultiFormattedText(masterParagraphNode, textObject, options) {
masterParagraphNode.push({});

let paragraphNodeIndex = masterParagraphNode.length - 1;
let paragraphBlock = (masterParagraphNode[paragraphNodeIndex] = PptFactoryHelper.createEmptyParagraphPropertiesBlock()[0]);

PptFactoryHelper.addParagraphPropertiesToBlock(paragraphBlock, options);

let textSegmentsArray = textObject.textSegments;

masterParagraphNode[paragraphNodeIndex]['a:r'] = [];

for (let i = 0; i < textSegmentsArray.length; i++) {
let segment = textSegmentsArray[i];
let textRunNode = masterParagraphNode[paragraphNodeIndex]['a:r'];

PptFactoryHelper.convertTextPropertiesToOptionsAndMerge(segment, options);
textRunNode.push({ 'a:rPr': [{ $: { lang: 'en-US', smtClean: '0' } }], 'a:t': segment.text });
PptFactoryHelper.setupTextRunPropertiesBlock(textRunNode[i]['a:rPr'][0], segment.options);
}
}

static convertTextPropertiesToOptionsAndMerge(textObject, options) {
textObject.options = {};

for (let prop in textObject) {
if (textObject.hasOwnProperty(prop) && !['text', 'x', 'y', 'cx', 'cy'].includes(prop)) {
if (textObject.hasOwnProperty(prop) && !['text', 'textSegments', 'options', 'x', 'y', 'cx', 'cy'].includes(prop)) {
textObject.options[prop] = textObject[prop];
}
}
Expand Down

0 comments on commit 9cf323a

Please sign in to comment.