UNPKG

45 kBJavaScriptView Raw
1import { body, headers } from "@pnp/queryable";
2import { getGUID, hOP, stringIsNullOrEmpty, objectDefinedNotNull, combine, isUrlAbsolute, isArray } from "@pnp/core";
3import { Item } from "../items/types.js";
4import { _SPQueryable, SPQueryable, SPCollection } from "../spqueryable.js";
5import { List } from "../lists/types.js";
6import { odataUrlFrom } from "../utils/odata-url-from.js";
7import { Web } from "../webs/types.js";
8import { extractWebUrl } from "../utils/extract-web-url.js";
9import { Site } from "../sites/types.js";
10import { spPost } from "../operations.js";
11import { getNextOrder, reindex } from "./funcs.js";
12import "../files/web.js";
13import "../comments/item.js";
14import { createBatch } from "../batching.js";
15/**
16 * Page promotion state
17 */
18export var PromotedState;
19(function (PromotedState) {
20 /**
21 * Regular client side page
22 */
23 PromotedState[PromotedState["NotPromoted"] = 0] = "NotPromoted";
24 /**
25 * Page that will be promoted as news article after publishing
26 */
27 PromotedState[PromotedState["PromoteOnPublish"] = 1] = "PromoteOnPublish";
28 /**
29 * Page that is promoted as news article
30 */
31 PromotedState[PromotedState["Promoted"] = 2] = "Promoted";
32})(PromotedState || (PromotedState = {}));
33/**
34 * Represents the data and methods associated with client side "modern" pages
35 */
36export class _ClientsidePage extends _SPQueryable {
37 /**
38 * PLEASE DON'T USE THIS CONSTRUCTOR DIRECTLY, thank you 🐇
39 */
40 constructor(base, path, json, noInit = false, sections = [], commentsDisabled = false) {
41 super(base, path);
42 this.json = json;
43 this.sections = sections;
44 this.commentsDisabled = commentsDisabled;
45 this._bannerImageDirty = false;
46 this._bannerImageThumbnailUrlDirty = false;
47 this.parentUrl = "";
48 // we need to rebase the url to always be the web url plus the path
49 // Queryable handles the correct parsing of the SPInit, and we pull
50 // the weburl and combine with the supplied path, which is unique
51 // to how ClientsitePages works. This class is a special case.
52 this._url = combine(extractWebUrl(this._url), path);
53 // set a default page settings slice
54 this._pageSettings = { controlType: 0, pageSettingsSlice: { isDefaultDescription: true, isDefaultThumbnail: true } };
55 // set a default layout part
56 this._layoutPart = _ClientsidePage.getDefaultLayoutPart();
57 if (typeof json !== "undefined" && !noInit) {
58 this.fromJSON(json);
59 }
60 }
61 static getDefaultLayoutPart() {
62 return {
63 dataVersion: "1.4",
64 description: "Title Region Description",
65 id: "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
66 instanceId: "cbe7b0a9-3504-44dd-a3a3-0e5cacd07788",
67 properties: {
68 authorByline: [],
69 authors: [],
70 layoutType: "FullWidthImage",
71 showPublishDate: false,
72 showTopicHeader: false,
73 textAlignment: "Left",
74 title: "",
75 topicHeader: "",
76 enableGradientEffect: true,
77 },
78 reservedHeight: 280,
79 serverProcessedContent: { htmlStrings: {}, searchablePlainTexts: {}, imageSources: {}, links: {} },
80 title: "Title area",
81 };
82 }
83 get pageLayout() {
84 return this.json.PageLayoutType;
85 }
86 set pageLayout(value) {
87 this.json.PageLayoutType = value;
88 }
89 get bannerImageUrl() {
90 return this.json.BannerImageUrl;
91 }
92 set bannerImageUrl(value) {
93 this.setBannerImage(value);
94 }
95 get thumbnailUrl() {
96 return this._pageSettings.pageSettingsSlice.isDefaultThumbnail ? this.json.BannerImageUrl : this.json.BannerThumbnailUrl;
97 }
98 set thumbnailUrl(value) {
99 this.json.BannerThumbnailUrl = value;
100 this._bannerImageThumbnailUrlDirty = true;
101 this._pageSettings.pageSettingsSlice.isDefaultThumbnail = false;
102 }
103 get topicHeader() {
104 return objectDefinedNotNull(this.json.TopicHeader) ? this.json.TopicHeader : "";
105 }
106 set topicHeader(value) {
107 this.json.TopicHeader = value;
108 this._layoutPart.properties.topicHeader = value;
109 if (stringIsNullOrEmpty(value)) {
110 this.showTopicHeader = false;
111 }
112 }
113 get title() {
114 return this.json.Title;
115 }
116 set title(value) {
117 this.json.Title = value;
118 this._layoutPart.properties.title = value;
119 }
120 get reservedHeight() {
121 return this._layoutPart.reservedHeight;
122 }
123 set reservedHeight(value) {
124 this._layoutPart.reservedHeight = value;
125 }
126 get description() {
127 return this.json.Description;
128 }
129 set description(value) {
130 if (!stringIsNullOrEmpty(value) && value.length > 255) {
131 throw Error("Modern Page description is limited to 255 chars.");
132 }
133 this.json.Description = value;
134 if (!hOP(this._pageSettings, "htmlAttributes")) {
135 this._pageSettings.htmlAttributes = [];
136 }
137 if (this._pageSettings.htmlAttributes.indexOf("modifiedDescription") < 0) {
138 this._pageSettings.htmlAttributes.push("modifiedDescription");
139 }
140 this._pageSettings.pageSettingsSlice.isDefaultDescription = false;
141 }
142 get layoutType() {
143 return this._layoutPart.properties.layoutType;
144 }
145 set layoutType(value) {
146 this._layoutPart.properties.layoutType = value;
147 }
148 get headerTextAlignment() {
149 return this._layoutPart.properties.textAlignment;
150 }
151 set headerTextAlignment(value) {
152 this._layoutPart.properties.textAlignment = value;
153 }
154 get showTopicHeader() {
155 return this._layoutPart.properties.showTopicHeader;
156 }
157 set showTopicHeader(value) {
158 this._layoutPart.properties.showTopicHeader = value;
159 }
160 get showPublishDate() {
161 return this._layoutPart.properties.showPublishDate;
162 }
163 set showPublishDate(value) {
164 this._layoutPart.properties.showPublishDate = value;
165 }
166 get hasVerticalSection() {
167 return this.sections.findIndex(s => s.layoutIndex === 2) > -1;
168 }
169 get authorByLine() {
170 if (isArray(this._layoutPart.properties.authorByline) && this._layoutPart.properties.authorByline.length > 0) {
171 return this._layoutPart.properties.authorByline[0];
172 }
173 return null;
174 }
175 get verticalSection() {
176 if (this.hasVerticalSection) {
177 return this.addVerticalSection();
178 }
179 return null;
180 }
181 /**
182 * Add a section to this page
183 */
184 addSection() {
185 const section = new CanvasSection(this, getNextOrder(this.sections), 1);
186 this.sections.push(section);
187 return section;
188 }
189 /**
190 * Add a section to this page
191 */
192 addVerticalSection() {
193 // we can only have one vertical section so we find it if it exists
194 const sectionIndex = this.sections.findIndex(s => s.layoutIndex === 2);
195 if (sectionIndex > -1) {
196 return this.sections[sectionIndex];
197 }
198 const section = new CanvasSection(this, getNextOrder(this.sections), 2);
199 this.sections.push(section);
200 return section;
201 }
202 /**
203 * Loads this instance from the appropriate JSON data
204 *
205 * @param pageData JSON data to load (replaces any existing data)
206 */
207 fromJSON(pageData) {
208 this.json = pageData;
209 const canvasControls = JSON.parse(pageData.CanvasContent1);
210 const layouts = JSON.parse(pageData.LayoutWebpartsContent);
211 if (layouts && layouts.length > 0) {
212 this._layoutPart = layouts[0];
213 }
214 this.setControls(canvasControls);
215 return this;
216 }
217 /**
218 * Loads this page's content from the server
219 */
220 async load() {
221 const item = await this.getItem("Id", "CommentsDisabled");
222 const pageData = await SPQueryable(this, `_api/sitepages/pages(${item.Id})`)();
223 this.commentsDisabled = item.CommentsDisabled;
224 return this.fromJSON(pageData);
225 }
226 /**
227 * Persists the content changes (sections, columns, and controls) [does not work with batching]
228 *
229 * @param publish If true the page is published, if false the changes are persisted to SharePoint but not published [Default: true]
230 */
231 async save(publish = true) {
232 if (this.json.Id === null) {
233 throw Error("The id for this page is null. If you want to create a new page, please use ClientSidePage.Create");
234 }
235 const previewPartialUrl = "_layouts/15/getpreview.ashx";
236 // If new banner image, and banner image url is not in getpreview.ashx format
237 if (this._bannerImageDirty && !this.bannerImageUrl.includes(previewPartialUrl)) {
238 const serverRelativePath = this.bannerImageUrl;
239 let imgInfo;
240 let webUrl;
241 const web = Web(this);
242 const [batch, execute] = createBatch(web);
243 web.using(batch);
244 web.getFileByServerRelativePath(serverRelativePath.replace(/%20/ig, " "))
245 .select("ListId", "WebId", "UniqueId", "Name", "SiteId")().then(r1 => imgInfo = r1);
246 web.select("Url")().then(r2 => webUrl = r2.Url);
247 // we know the .then calls above will run before execute resolves, ensuring the vars are set
248 await execute();
249 const f = SPQueryable(webUrl, previewPartialUrl);
250 f.query.set("guidSite", `${imgInfo.SiteId}`);
251 f.query.set("guidWeb", `${imgInfo.WebId}`);
252 f.query.set("guidFile", `${imgInfo.UniqueId}`);
253 this.bannerImageUrl = f.toRequestUrl();
254 if (!objectDefinedNotNull(this._layoutPart.serverProcessedContent)) {
255 this._layoutPart.serverProcessedContent = {};
256 }
257 this._layoutPart.serverProcessedContent.imageSources = { imageSource: serverRelativePath };
258 if (!objectDefinedNotNull(this._layoutPart.serverProcessedContent.customMetadata)) {
259 this._layoutPart.serverProcessedContent.customMetadata = {};
260 }
261 this._layoutPart.serverProcessedContent.customMetadata.imageSource = {
262 listId: imgInfo.ListId,
263 siteId: imgInfo.SiteId,
264 uniqueId: imgInfo.UniqueId,
265 webId: imgInfo.WebId,
266 };
267 this._layoutPart.properties.webId = imgInfo.WebId;
268 this._layoutPart.properties.siteId = imgInfo.SiteId;
269 this._layoutPart.properties.listId = imgInfo.ListId;
270 this._layoutPart.properties.uniqueId = imgInfo.UniqueId;
271 }
272 // we try and check out the page for the user
273 if (!this.json.IsPageCheckedOutToCurrentUser) {
274 await spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/checkoutpage`));
275 }
276 // create the body for the save request
277 let saveBody = {
278 AuthorByline: this.json.AuthorByline || [],
279 CanvasContent1: this.getCanvasContent1(),
280 Description: this.description,
281 LayoutWebpartsContent: this.getLayoutWebpartsContent(),
282 Title: this.title,
283 TopicHeader: this.topicHeader,
284 BannerImageUrl: this.bannerImageUrl,
285 };
286 if (this._bannerImageDirty || this._bannerImageThumbnailUrlDirty) {
287 const bannerImageUrlValue = this._bannerImageThumbnailUrlDirty ? this.thumbnailUrl : this.bannerImageUrl;
288 saveBody = {
289 BannerImageUrl: bannerImageUrlValue,
290 ...saveBody,
291 };
292 }
293 const updater = ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/savepage`);
294 await spPost(updater, headers({ "if-match": "*" }, body(saveBody)));
295 let r = true;
296 if (publish) {
297 r = await spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/publish`));
298 if (r) {
299 this.json.IsPageCheckedOutToCurrentUser = false;
300 }
301 }
302 this._bannerImageDirty = false;
303 this._bannerImageThumbnailUrlDirty = false;
304 // we need to ensure we reload from the latest data to ensure all urls are updated and current in the object (expecially for new pages)
305 await this.load();
306 return r;
307 }
308 /**
309 * Discards the checkout of this page
310 */
311 async discardPageCheckout() {
312 if (this.json.Id === null) {
313 throw Error("The id for this page is null. If you want to create a new page, please use ClientSidePage.Create");
314 }
315 const d = await spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/discardPage`));
316 this.fromJSON(d);
317 }
318 /**
319 * Promotes this page as a news item
320 */
321 async promoteToNews() {
322 return this.promoteNewsImpl("promoteToNews");
323 }
324 // API is currently broken on server side
325 // public async demoteFromNews(): Promise<boolean> {
326 // return this.promoteNewsImpl("demoteFromNews");
327 // }
328 /**
329 * Finds a control by the specified instance id
330 *
331 * @param id Instance id of the control to find
332 */
333 findControlById(id) {
334 return this.findControl((c) => c.id === id);
335 }
336 /**
337 * Finds a control within this page's control tree using the supplied predicate
338 *
339 * @param predicate Takes a control and returns true or false, if true that control is returned by findControl
340 */
341 findControl(predicate) {
342 // check all sections
343 for (let i = 0; i < this.sections.length; i++) {
344 // check all columns
345 for (let j = 0; j < this.sections[i].columns.length; j++) {
346 // check all controls
347 for (let k = 0; k < this.sections[i].columns[j].controls.length; k++) {
348 // check to see if the predicate likes this control
349 if (predicate(this.sections[i].columns[j].controls[k])) {
350 return this.sections[i].columns[j].controls[k];
351 }
352 }
353 }
354 }
355 // we found nothing so give nothing back
356 return null;
357 }
358 /**
359 * Creates a new page with all of the content copied from this page
360 *
361 * @param web The web where we will create the copy
362 * @param pageName The file name of the new page
363 * @param title The title of the new page
364 * @param publish If true the page will be published (Default: true)
365 */
366 async copy(web, pageName, title, publish = true, promotedState) {
367 const page = await CreateClientsidePage(web, pageName, title, this.pageLayout, promotedState);
368 return this.copyTo(page, publish);
369 }
370 /**
371 * Copies the content from this page to the supplied page instance NOTE: fully overwriting any previous content!!
372 *
373 * @param page Page whose content we replace with this page's content
374 * @param publish If true the page will be published after the copy, if false it will be saved but left unpublished (Default: true)
375 */
376 async copyTo(page, publish = true) {
377 // we know the method is on the class - but it is protected so not part of the interface
378 page.setControls(this.getControls());
379 // copy page properties
380 if (this._layoutPart.properties) {
381 if (hOP(this._layoutPart.properties, "topicHeader")) {
382 page.topicHeader = this._layoutPart.properties.topicHeader;
383 }
384 if (hOP(this._layoutPart.properties, "imageSourceType")) {
385 page._layoutPart.properties.imageSourceType = this._layoutPart.properties.imageSourceType;
386 }
387 if (hOP(this._layoutPart.properties, "layoutType")) {
388 page._layoutPart.properties.layoutType = this._layoutPart.properties.layoutType;
389 }
390 if (hOP(this._layoutPart.properties, "textAlignment")) {
391 page._layoutPart.properties.textAlignment = this._layoutPart.properties.textAlignment;
392 }
393 if (hOP(this._layoutPart.properties, "showTopicHeader")) {
394 page._layoutPart.properties.showTopicHeader = this._layoutPart.properties.showTopicHeader;
395 }
396 if (hOP(this._layoutPart.properties, "showPublishDate")) {
397 page._layoutPart.properties.showPublishDate = this._layoutPart.properties.showPublishDate;
398 }
399 if (hOP(this._layoutPart.properties, "enableGradientEffect")) {
400 page._layoutPart.properties.enableGradientEffect = this._layoutPart.properties.enableGradientEffect;
401 }
402 }
403 // we need to do some work to set the banner image url in the copied page
404 if (!stringIsNullOrEmpty(this.json.BannerImageUrl)) {
405 // use a URL to parse things for us
406 const url = new URL(this.json.BannerImageUrl);
407 // helper function to translate the guid strings into properly formatted guids with dashes
408 const makeGuid = (s) => s.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/g, "$1-$2-$3-$4-$5");
409 // protect against errors because the serverside impl has changed, we'll just skip
410 if (url.searchParams.has("guidSite") && url.searchParams.has("guidWeb") && url.searchParams.has("guidFile")) {
411 const guidSite = makeGuid(url.searchParams.get("guidSite"));
412 const guidWeb = makeGuid(url.searchParams.get("guidWeb"));
413 const guidFile = makeGuid(url.searchParams.get("guidFile"));
414 const site = Site(this);
415 const id = await site.select("Id")();
416 // the site guid must match the current site's guid or we are unable to set the image
417 if (id.Id === guidSite) {
418 const openWeb = await site.openWebById(guidWeb);
419 const file = await openWeb.web.getFileById(guidFile).select("ServerRelativeUrl")();
420 const props = {};
421 if (this._layoutPart.properties) {
422 if (hOP(this._layoutPart.properties, "translateX")) {
423 props.translateX = this._layoutPart.properties.translateX;
424 }
425 if (hOP(this._layoutPart.properties, "translateY")) {
426 props.translateY = this._layoutPart.properties.translateY;
427 }
428 if (hOP(this._layoutPart.properties, "imageSourceType")) {
429 props.imageSourceType = this._layoutPart.properties.imageSourceType;
430 }
431 if (hOP(this._layoutPart.properties, "altText")) {
432 props.altText = this._layoutPart.properties.altText;
433 }
434 }
435 page.setBannerImage(file.ServerRelativeUrl, props);
436 }
437 }
438 }
439 await page.save(publish);
440 return page;
441 }
442 /**
443 * Sets the modern page banner image
444 *
445 * @param url Url of the image to display
446 * @param altText Alt text to describe the image
447 * @param bannerProps Additional properties to control display of the banner
448 */
449 setBannerImage(url, props) {
450 if (isUrlAbsolute(url)) {
451 // do our best to make this a server relative url by removing the x.sharepoint.com part
452 url = url.replace(/^https?:\/\/[a-z0-9.]*?\.[a-z]{2,3}\//i, "/");
453 }
454 this.json.BannerImageUrl = url;
455 // update serverProcessedContent (page behavior change 2021-Oct-13)
456 this._layoutPart.serverProcessedContent = { imageSources: { imageSource: url } };
457 this._bannerImageDirty = true;
458 /*
459 setting the banner image resets the thumbnail image (matching UI functionality)
460 but if the thumbnail is dirty they are likely trying to set them both to
461 different values, so we allow that here.
462 Also allows the banner image to be updated safely with the calculated one in save()
463 */
464 if (!this._bannerImageThumbnailUrlDirty) {
465 this.thumbnailUrl = url;
466 this._pageSettings.pageSettingsSlice.isDefaultThumbnail = true;
467 }
468 // this seems to always be true, so default
469 this._layoutPart.properties.imageSourceType = 2;
470 if (objectDefinedNotNull(props)) {
471 if (hOP(props, "translateX")) {
472 this._layoutPart.properties.translateX = props.translateX;
473 }
474 if (hOP(props, "translateY")) {
475 this._layoutPart.properties.translateY = props.translateY;
476 }
477 if (hOP(props, "imageSourceType")) {
478 this._layoutPart.properties.imageSourceType = props.imageSourceType;
479 }
480 if (hOP(props, "altText")) {
481 this._layoutPart.properties.altText = props.altText;
482 }
483 }
484 }
485 /**
486 * Sets the banner image url from an external source. You must call save to persist the changes
487 *
488 * @param url absolute url of the external file
489 * @param props optional set of properties to control display of the banner image
490 */
491 async setBannerImageFromExternalUrl(url, props) {
492 // validate and parse our input url
493 const fileUrl = new URL(url);
494 // get our page name without extension, used as a folder name when creating the file
495 const pageName = this.json.FileName.replace(/\.[^/.]+$/, "");
496 // get the filename we will use
497 const filename = fileUrl.pathname.split(/[\\/]/i).pop();
498 const request = ClientsidePage(this, "_api/sitepages/AddImageFromExternalUrl");
499 request.query.set("imageFileName", `'${encodeURIComponent(filename)}'`);
500 request.query.set("pageName", `'${encodeURIComponent(pageName)}'`);
501 request.query.set("externalUrl", `'${encodeURIComponent(url)}'`);
502 request.select("ServerRelativeUrl");
503 const result = await spPost(request);
504 // set with the newly created relative url
505 this.setBannerImage(result.ServerRelativeUrl, props);
506 }
507 /**
508 * Sets the authors for this page from the supplied list of user integer ids
509 *
510 * @param authorId The integer id of the user to set as the author
511 */
512 async setAuthorById(authorId) {
513 const userLoginData = await SPCollection([this, extractWebUrl(this.toUrl())], "/_api/web/siteusers")
514 .filter(`Id eq ${authorId}`)
515 .select("LoginName")();
516 if (userLoginData.length < 1) {
517 throw Error(`Could not find user with id ${authorId}.`);
518 }
519 return this.setAuthorByLoginName(userLoginData[0].LoginName);
520 }
521 /**
522 * Sets the authors for this page from the supplied list of user integer ids
523 *
524 * @param authorLoginName The login name of the user (ex: i:0#.f|membership|name@tenant.com)
525 */
526 async setAuthorByLoginName(authorLoginName) {
527 const userLoginData = await SPCollection([this, extractWebUrl(this.toUrl())], "/_api/web/siteusers")
528 .filter(`LoginName eq '${authorLoginName}'`)
529 .select("UserPrincipalName", "Title")();
530 if (userLoginData.length < 1) {
531 throw Error(`Could not find user with login name '${authorLoginName}'.`);
532 }
533 this.json.AuthorByline = [userLoginData[0].UserPrincipalName];
534 this._layoutPart.properties.authorByline = [userLoginData[0].UserPrincipalName];
535 this._layoutPart.properties.authors = [{
536 id: authorLoginName,
537 name: userLoginData[0].Title,
538 role: "",
539 upn: userLoginData[0].UserPrincipalName,
540 }];
541 }
542 /**
543 * Gets the list item associated with this clientside page
544 *
545 * @param selects Specific set of fields to include when getting the item
546 */
547 async getItem(...selects) {
548 const initer = ClientsidePage(this, "/_api/lists/EnsureClientRenderedSitePagesLibrary").select("EnableModeration", "EnableMinorVersions", "Id");
549 const listData = await spPost(initer);
550 const item = List([this, listData["odata.id"]]).items.getById(this.json.Id);
551 const itemData = await item.select(...selects)();
552 return Object.assign(Item([this, odataUrlFrom(itemData)]), itemData);
553 }
554 /**
555 * Recycle this page
556 */
557 async recycle() {
558 const item = await this.getItem();
559 await item.recycle();
560 }
561 /**
562 * Delete this page
563 */
564 async delete() {
565 const item = await this.getItem();
566 await item.delete();
567 }
568 /**
569 * Schedules a page for publishing
570 *
571 * @param publishDate Date to publish the item
572 * @returns Version which was scheduled to be published
573 */
574 async schedulePublish(publishDate) {
575 return spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/SchedulePublish`), body({
576 sitePage: { PublishStartDate: publishDate },
577 }));
578 }
579 /**
580 * Saves a copy of this page as a template in this library's Templates folder
581 *
582 * @param publish If true the template is published, false the template is not published (default: true)
583 * @returns IClientsidePage instance representing the new template page
584 */
585 async saveAsTemplate(publish = true) {
586 const data = await spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/SavePageAsTemplate`));
587 const page = ClientsidePage(this, null, data);
588 page.title = this.title;
589 await page.save(publish);
590 return page;
591 }
592 /**
593 * Share this Page's Preview content by Email
594 *
595 * @param emails Set of emails to which the preview is shared
596 * @param message The message to include
597 * @returns void
598 */
599 share(emails, message) {
600 return spPost(ClientsidePage(this, "_api/SP.Publishing.RichSharing/SharePageByEmail"), body({
601 recipientEmails: emails,
602 message,
603 url: this.json.AbsoluteUrl,
604 }));
605 }
606 getCanvasContent1() {
607 return JSON.stringify(this.getControls());
608 }
609 getLayoutWebpartsContent() {
610 if (this._layoutPart) {
611 return JSON.stringify([this._layoutPart]);
612 }
613 else {
614 return JSON.stringify(null);
615 }
616 }
617 setControls(controls) {
618 // reset the sections
619 this.sections = [];
620 if (controls && controls.length) {
621 for (let i = 0; i < controls.length; i++) {
622 // if no control type is present this is a column which we give type 0 to let us process it
623 const controlType = hOP(controls[i], "controlType") ? controls[i].controlType : 0;
624 switch (controlType) {
625 case 0:
626 // empty canvas column or page settings
627 if (hOP(controls[i], "pageSettingsSlice")) {
628 this._pageSettings = controls[i];
629 }
630 else {
631 // we have an empty column
632 this.mergeColumnToTree(new CanvasColumn(controls[i]));
633 }
634 break;
635 case 3: {
636 const part = new ClientsideWebpart(controls[i]);
637 this.mergePartToTree(part, part.data.position);
638 break;
639 }
640 case 4: {
641 const textData = controls[i];
642 const text = new ClientsideText(textData.innerHTML, textData);
643 this.mergePartToTree(text, text.data.position);
644 break;
645 }
646 }
647 }
648 reindex(this.sections);
649 }
650 }
651 getControls() {
652 // reindex things
653 reindex(this.sections);
654 // rollup the control changes
655 const canvasData = [];
656 this.sections.forEach(section => {
657 section.columns.forEach(column => {
658 if (column.controls.length < 1) {
659 // empty column
660 canvasData.push({
661 displayMode: column.data.displayMode,
662 emphasis: this.getEmphasisObj(section.emphasis),
663 position: column.data.position,
664 });
665 }
666 else {
667 column.controls.forEach(control => {
668 control.data.emphasis = this.getEmphasisObj(section.emphasis);
669 canvasData.push(this.specialSaveHandling(control).data);
670 });
671 }
672 });
673 });
674 canvasData.push(this._pageSettings);
675 return canvasData;
676 }
677 getEmphasisObj(value) {
678 if (value < 1 || value > 3) {
679 return {};
680 }
681 return { zoneEmphasis: value };
682 }
683 async promoteNewsImpl(method) {
684 if (this.json.Id === null) {
685 throw Error("The id for this page is null.");
686 }
687 // per bug #858 if we promote before we have ever published the last published date will
688 // forever not be updated correctly in the modern news web part. Because this will affect very
689 // few folks we just go ahead and publish for them here as that is likely what they intended.
690 if (stringIsNullOrEmpty(this.json.VersionInfo.LastVersionCreatedBy)) {
691 const lastPubData = new Date(this.json.VersionInfo.LastVersionCreated);
692 // no modern page should reasonable be published before the year 2000 :)
693 if (lastPubData.getFullYear() < 2000) {
694 await this.save(true);
695 }
696 }
697 return await spPost(ClientsidePage(this, `_api/sitepages/pages(${this.json.Id})/${method}`));
698 }
699 /**
700 * Merges the control into the tree of sections and columns for this page
701 *
702 * @param control The control to merge
703 */
704 mergePartToTree(control, positionData) {
705 var _a, _b, _c;
706 let column = null;
707 let sectionFactor = 12;
708 let sectionIndex = 0;
709 let zoneIndex = 0;
710 let layoutIndex = 1;
711 // handle case where we don't have position data (shouldn't happen?)
712 if (positionData) {
713 if (hOP(positionData, "zoneIndex")) {
714 zoneIndex = positionData.zoneIndex;
715 }
716 if (hOP(positionData, "sectionIndex")) {
717 sectionIndex = positionData.sectionIndex;
718 }
719 if (hOP(positionData, "sectionFactor")) {
720 sectionFactor = positionData.sectionFactor;
721 }
722 if (hOP(positionData, "layoutIndex")) {
723 layoutIndex = positionData.layoutIndex;
724 }
725 }
726 const zoneEmphasis = (_c = (_b = (_a = control.data) === null || _a === void 0 ? void 0 : _a.emphasis) === null || _b === void 0 ? void 0 : _b.zoneEmphasis) !== null && _c !== void 0 ? _c : 0;
727 const section = this.getOrCreateSection(zoneIndex, layoutIndex, zoneEmphasis);
728 const columns = section.columns.filter(c => c.order === sectionIndex);
729 if (columns.length < 1) {
730 column = section.addColumn(sectionFactor, layoutIndex);
731 }
732 else {
733 column = columns[0];
734 }
735 control.column = column;
736 column.addControl(control);
737 }
738 /**
739 * Merges the supplied column into the tree
740 *
741 * @param column Column to merge
742 * @param position The position data for the column
743 */
744 mergeColumnToTree(column) {
745 var _a, _b;
746 const order = hOP(column.data, "position") && hOP(column.data.position, "zoneIndex") ? column.data.position.zoneIndex : 0;
747 const layoutIndex = hOP(column.data, "position") && hOP(column.data.position, "layoutIndex") ? column.data.position.layoutIndex : 1;
748 const section = this.getOrCreateSection(order, layoutIndex, ((_b = (_a = column.data) === null || _a === void 0 ? void 0 : _a.emphasis) === null || _b === void 0 ? void 0 : _b.zoneEmphasis) || 0);
749 column.section = section;
750 section.columns.push(column);
751 }
752 /**
753 * Handle the logic to get or create a section based on the supplied order and layoutIndex
754 *
755 * @param order Section order
756 * @param layoutIndex Layout Index (1 === normal, 2 === vertical section)
757 * @param emphasis The section emphasis
758 */
759 getOrCreateSection(order, layoutIndex, emphasis) {
760 let section = null;
761 const sections = this.sections.filter(s => s.order === order && s.layoutIndex === layoutIndex);
762 if (sections.length < 1) {
763 section = layoutIndex === 2 ? this.addVerticalSection() : this.addSection();
764 section.order = order;
765 section.emphasis = emphasis;
766 }
767 else {
768 section = sections[0];
769 }
770 return section;
771 }
772 /**
773 * Based on issue #1690 we need to take special case actions to ensure some things
774 * can be saved properly without breaking existing pages.
775 *
776 * @param control The control we are ensuring is "ready" to be saved
777 */
778 specialSaveHandling(control) {
779 var _a, _b, _c;
780 // this is to handle the special case in issue #1690
781 // must ensure that searchablePlainTexts values have < replaced with &lt; in links web part
782 if (control.data.controlType === 3 && control.data.webPartId === "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd") {
783 const texts = ((_c = (_b = (_a = control.data) === null || _a === void 0 ? void 0 : _a.webPartData) === null || _b === void 0 ? void 0 : _b.serverProcessedContent) === null || _c === void 0 ? void 0 : _c.searchablePlainTexts) || null;
784 if (objectDefinedNotNull(texts)) {
785 const keys = Object.getOwnPropertyNames(texts);
786 for (let i = 0; i < keys.length; i++) {
787 texts[keys[i]] = texts[keys[i]].replace(/</ig, "&lt;");
788 control.data.webPartData.serverProcessedContent.searchablePlainTexts = texts;
789 }
790 }
791 }
792 return control;
793 }
794}
795/**
796 * Invokable factory for IClientSidePage instances
797 */
798const ClientsidePage = (base, path, json, noInit = false, sections = [], commentsDisabled = false) => {
799 return new _ClientsidePage(base, path, json, noInit, sections, commentsDisabled);
800};
801/**
802 * Loads a client side page from the supplied IFile instance
803 *
804 * @param file Source IFile instance
805 */
806export const ClientsidePageFromFile = async (file) => {
807 const item = await file.getItem();
808 const page = ClientsidePage([file, extractWebUrl(file.toUrl())], "", { Id: item.Id }, true);
809 return page.load();
810};
811/**
812 * Creates a new client side page
813 *
814 * @param web The web or list
815 * @param pageName The name of the page (filename)
816 * @param title The page's title
817 * @param PageLayoutType Layout to use when creating the page
818 */
819export const CreateClientsidePage = async (web, pageName, title, PageLayoutType = "Article", promotedState = 0) => {
820 // patched because previously we used the full page name with the .aspx at the end
821 // this allows folk's existing code to work after the re-write to the new API
822 pageName = pageName.replace(/\.aspx$/i, "");
823 // initialize the page, at this point a checked-out page with a junk filename will be created.
824 const pageInitData = await spPost(ClientsidePage(web, "_api/sitepages/pages"), body({
825 PageLayoutType,
826 PromotedState: promotedState,
827 }));
828 // now we can init our page with the save data
829 const newPage = ClientsidePage(web, "", pageInitData);
830 newPage.title = pageName;
831 await newPage.save(false);
832 newPage.title = title;
833 return newPage;
834};
835export class CanvasSection {
836 constructor(page, order, layoutIndex, columns = [], _emphasis = 0) {
837 this.page = page;
838 this.columns = columns;
839 this._emphasis = _emphasis;
840 this._memId = getGUID();
841 this._order = order;
842 this._layoutIndex = layoutIndex;
843 }
844 get order() {
845 return this._order;
846 }
847 set order(value) {
848 this._order = value;
849 for (let i = 0; i < this.columns.length; i++) {
850 this.columns[i].data.position.zoneIndex = value;
851 }
852 }
853 get layoutIndex() {
854 return this._layoutIndex;
855 }
856 set layoutIndex(value) {
857 this._layoutIndex = value;
858 for (let i = 0; i < this.columns.length; i++) {
859 this.columns[i].data.position.layoutIndex = value;
860 }
861 }
862 /**
863 * Default column (this.columns[0]) for this section
864 */
865 get defaultColumn() {
866 if (this.columns.length < 1) {
867 this.addColumn(12);
868 }
869 return this.columns[0];
870 }
871 /**
872 * Adds a new column to this section
873 */
874 addColumn(factor, layoutIndex = this.layoutIndex) {
875 const column = new CanvasColumn();
876 column.section = this;
877 column.data.position.zoneIndex = this.order;
878 column.data.position.layoutIndex = layoutIndex;
879 column.data.position.sectionFactor = factor;
880 column.order = getNextOrder(this.columns);
881 this.columns.push(column);
882 return column;
883 }
884 /**
885 * Adds a control to the default column for this section
886 *
887 * @param control Control to add to the default column
888 */
889 addControl(control) {
890 this.defaultColumn.addControl(control);
891 return this;
892 }
893 get emphasis() {
894 return this._emphasis;
895 }
896 set emphasis(value) {
897 this._emphasis = value;
898 }
899 /**
900 * Removes this section and all contained columns and controls from the collection
901 */
902 remove() {
903 this.page.sections = this.page.sections.filter(section => section._memId !== this._memId);
904 reindex(this.page.sections);
905 }
906}
907export class CanvasColumn {
908 constructor(json = JSON.parse(JSON.stringify(CanvasColumn.Default)), controls = []) {
909 this.json = json;
910 this.controls = controls;
911 this._section = null;
912 this._memId = getGUID();
913 }
914 get data() {
915 return this.json;
916 }
917 get section() {
918 return this._section;
919 }
920 set section(section) {
921 this._section = section;
922 }
923 get order() {
924 return this.data.position.sectionIndex;
925 }
926 set order(value) {
927 this.data.position.sectionIndex = value;
928 for (let i = 0; i < this.controls.length; i++) {
929 this.controls[i].data.position.zoneIndex = this.data.position.zoneIndex;
930 this.controls[i].data.position.layoutIndex = this.data.position.layoutIndex;
931 this.controls[i].data.position.sectionIndex = value;
932 }
933 }
934 get factor() {
935 return this.data.position.sectionFactor;
936 }
937 set factor(value) {
938 this.data.position.sectionFactor = value;
939 }
940 addControl(control) {
941 control.column = this;
942 this.controls.push(control);
943 return this;
944 }
945 getControl(index) {
946 return this.controls[index];
947 }
948 remove() {
949 this.section.columns = this.section.columns.filter(column => column._memId !== this._memId);
950 reindex(this.section.columns);
951 }
952}
953CanvasColumn.Default = {
954 controlType: 0,
955 displayMode: 2,
956 emphasis: {},
957 position: {
958 layoutIndex: 1,
959 sectionFactor: 12,
960 sectionIndex: 1,
961 zoneIndex: 1,
962 },
963};
964export class ColumnControl {
965 constructor(json) {
966 this.json = json;
967 }
968 get id() {
969 return this.json.id;
970 }
971 get data() {
972 return this.json;
973 }
974 get column() {
975 return this._column;
976 }
977 set column(value) {
978 this._column = value;
979 this.onColumnChange(this._column);
980 }
981 remove() {
982 this.column.controls = this.column.controls.filter(control => control.id !== this.id);
983 reindex(this.column.controls);
984 }
985 setData(data) {
986 this.json = data;
987 }
988}
989export class ClientsideText extends ColumnControl {
990 constructor(text, json = JSON.parse(JSON.stringify(ClientsideText.Default))) {
991 if (stringIsNullOrEmpty(json.id)) {
992 json.id = getGUID();
993 json.anchorComponentId = json.id;
994 }
995 super(json);
996 this.text = text;
997 }
998 get text() {
999 return this.data.innerHTML;
1000 }
1001 set text(value) {
1002 this.data.innerHTML = value;
1003 }
1004 get order() {
1005 return this.data.position.controlIndex;
1006 }
1007 set order(value) {
1008 this.data.position.controlIndex = value;
1009 }
1010 onColumnChange(col) {
1011 this.data.position.sectionFactor = col.factor;
1012 this.data.position.controlIndex = getNextOrder(col.controls);
1013 this.data.position.zoneIndex = col.data.position.zoneIndex;
1014 this.data.position.sectionIndex = col.order;
1015 this.data.position.layoutIndex = col.data.position.layoutIndex;
1016 }
1017}
1018ClientsideText.Default = {
1019 addedFromPersistedData: false,
1020 anchorComponentId: "",
1021 controlType: 4,
1022 displayMode: 2,
1023 editorType: "CKEditor",
1024 emphasis: {},
1025 id: "",
1026 innerHTML: "",
1027 position: {
1028 controlIndex: 1,
1029 layoutIndex: 1,
1030 sectionFactor: 12,
1031 sectionIndex: 1,
1032 zoneIndex: 1,
1033 },
1034};
1035export class ClientsideWebpart extends ColumnControl {
1036 constructor(json = JSON.parse(JSON.stringify(ClientsideWebpart.Default))) {
1037 super(json);
1038 }
1039 static fromComponentDef(definition) {
1040 const part = new ClientsideWebpart();
1041 part.import(definition);
1042 return part;
1043 }
1044 get title() {
1045 return this.data.webPartData.title;
1046 }
1047 set title(value) {
1048 this.data.webPartData.title = value;
1049 }
1050 get description() {
1051 return this.data.webPartData.description;
1052 }
1053 set description(value) {
1054 this.data.webPartData.description = value;
1055 }
1056 get order() {
1057 return this.data.position.controlIndex;
1058 }
1059 set order(value) {
1060 this.data.position.controlIndex = value;
1061 }
1062 get height() {
1063 return this.data.reservedHeight;
1064 }
1065 set height(value) {
1066 this.data.reservedHeight = value;
1067 }
1068 get width() {
1069 return this.data.reservedWidth;
1070 }
1071 set width(value) {
1072 this.data.reservedWidth = value;
1073 }
1074 get dataVersion() {
1075 return this.data.webPartData.dataVersion;
1076 }
1077 set dataVersion(value) {
1078 this.data.webPartData.dataVersion = value;
1079 }
1080 setProperties(properties) {
1081 this.data.webPartData.properties = {
1082 ...this.data.webPartData.properties,
1083 ...properties,
1084 };
1085 return this;
1086 }
1087 getProperties() {
1088 return this.data.webPartData.properties;
1089 }
1090 setServerProcessedContent(properties) {
1091 this.data.webPartData.serverProcessedContent = {
1092 ...this.data.webPartData.serverProcessedContent,
1093 ...properties,
1094 };
1095 return this;
1096 }
1097 getServerProcessedContent() {
1098 return this.data.webPartData.serverProcessedContent;
1099 }
1100 onColumnChange(col) {
1101 this.data.position.sectionFactor = col.factor;
1102 this.data.position.controlIndex = getNextOrder(col.controls);
1103 this.data.position.zoneIndex = col.data.position.zoneIndex;
1104 this.data.position.sectionIndex = col.data.position.sectionIndex;
1105 this.data.position.layoutIndex = col.data.position.layoutIndex;
1106 }
1107 import(component) {
1108 const id = getGUID();
1109 const componendId = component.Id.replace(/^\{|\}$/g, "").toLowerCase();
1110 const manifest = JSON.parse(component.Manifest);
1111 const preconfiguredEntries = manifest.preconfiguredEntries[0];
1112 this.setData(Object.assign({}, this.data, {
1113 id,
1114 webPartData: {
1115 dataVersion: "1.0",
1116 description: preconfiguredEntries.description.default,
1117 id: componendId,
1118 instanceId: id,
1119 properties: preconfiguredEntries.properties,
1120 title: preconfiguredEntries.title.default,
1121 },
1122 webPartId: componendId,
1123 }));
1124 }
1125}
1126ClientsideWebpart.Default = {
1127 addedFromPersistedData: false,
1128 controlType: 3,
1129 displayMode: 2,
1130 emphasis: {},
1131 id: null,
1132 position: {
1133 controlIndex: 1,
1134 layoutIndex: 1,
1135 sectionFactor: 12,
1136 sectionIndex: 1,
1137 zoneIndex: 1,
1138 },
1139 reservedHeight: 500,
1140 reservedWidth: 500,
1141 webPartData: null,
1142 webPartId: null,
1143};
1144//# sourceMappingURL=types.js.map
\No newline at end of file