@stackend/api
Version:
JS bindings to api.stackend.com
824 lines (741 loc) • 18.6 kB
text/typescript
import {
COMMUNITY_PARAMETER,
getJson,
post,
XcapJsonResult,
Thunk,
isRunningServerSide,
XcapOptionalParameters
} from '../api';
import ModerationStatus from '../api/ModerationStatus';
import { generatePermalink } from '../api/permalink';
import { PaginatedCollection } from '../api/PaginatedCollection';
import { Insertion, Category } from '../category';
import Order from '../api/Order';
import { Tree, Node, newTree, newTreeNode } from '../api/tree';
import { ModuleType } from '../stackend/modules';
import XcapObject from '../api/XcapObject';
import PermalinkAware from '../api/PermalinkAware';
import NameAware from '../api/NameAware';
import ModerationAware from '../api/ModerationAware';
import CreatedDateAware from '../api/CreatedDateAware';
import CreatorUserIdAware from '../api/CreatorUserIdAware';
import ModifiedDateAware from '../api/ModifiedDateAware';
import ModifiedByUserIdAware from '../api/ModifiedByUserIdAware';
import PublishDateAware from '../api/PublishDateAware';
/**
* Xcap Cms api constants and methods.
*
* @since 6 feb 2017
*/
/**
* Css Class for elements containing rich content
* @type {string}
*/
export const RICH_CONTENT_CSS_CLASS = 'stackend-rich-content';
/**
* A content object
*/
export interface Content
extends XcapObject,
PermalinkAware,
NameAware,
ModerationAware,
CreatedDateAware,
CreatorUserIdAware,
ModifiedDateAware,
ModifiedByUserIdAware,
PublishDateAware {
__type: 'se.josh.xcap.cms.Content';
body: string;
}
/**
* Component class (used to look up privileges, etc)
*/
export const COMPONENT_CLASS = 'se.josh.xcap.cms.CmsManager';
/**
* Component name
*/
export const COMPONENT_NAME = 'cms';
/**
* Default context
* @type {string}
*/
export const DEFAULT_CMS_CONTEXT = 'cms';
export interface GetContentResult extends XcapJsonResult {
content: Content | null;
}
/**
* Get CMS content
* @param id Content id (required)
* @param permalink Content permalink (optional)
*/
export function getContent({
id,
permalink
}: { id?: number; permalink?: string } & XcapOptionalParameters): Thunk<Promise<GetContentResult>> {
return getJson({ url: '/cms/get', parameters: arguments });
}
export interface PopulateTemplateContent extends XcapJsonResult {
result: string | null;
}
/**
* Get CMS content, populate template values.
* Any extra parameters passed down are used
* @param id Content id (required)
* @param permalink Content permalink (optional)
*/
export function populateTemplateContent({
id,
permalink
}: {
id?: number;
permalink?: string;
} & XcapOptionalParameters): Thunk<Promise<PopulateTemplateContent>> {
return getJson({ url: '/cms/populate-template', parameters: arguments });
}
export interface ListContentResult extends XcapJsonResult {
contentPaginated: PaginatedCollection<Content>;
isPage: boolean;
childCategories: Array<Category>;
}
/**
* List CMS content by category/permalink.
* @param permalink Content category permalink (optional)
* @param p Page number (optional)
* @param pageSize Page size (optional)
*/
export function listContent({
permalink,
p = 1,
pageSize
}: {
permalink?: string;
p?: number;
pageSize?: number;
} & XcapOptionalParameters): Thunk<Promise<ListContentResult>> {
return getJson({
url: '/cms/list',
parameters: {
permalink,
getPermalinkFromUrl: false,
p,
pageSize
}
});
}
export interface SearchResult extends XcapJsonResult {
results: PaginatedCollection<Content>;
}
export enum ContentSortCritera {
'CREATED' = 'CREATED',
'MODIFIED' = 'MODIFIED',
'NAME' = 'NAME',
'SORT' = 'SORT'
}
/**
* Search CMS content. Requires stack admin.
* @param q Search expression
* @param p Page number (optional)
* @param pageSize Page size (optional)
* @param orderBy ContentSortCritera by (optional)
* @param order: Order (optional)
*/
export function search({
q,
p = 1,
pageSize,
orderBy,
order
}: {
q?: string;
p?: number;
pageSize?: number;
orderBy?: ContentSortCritera;
order?: Order;
} & XcapOptionalParameters): Thunk<Promise<SearchResult>> {
return getJson({
url: '/cms/search',
parameters: {
q,
p,
pageSize,
orderBy,
order
}
});
}
export interface Page {
id: number;
parentPageId: number;
name: string;
permalink: string;
/** Number of sub pages */
childCount: number;
/** Is the page visible? */
enabled: boolean;
ogImageUrl: string | null;
metaDescription: string | null;
content: Array<PageContent>;
}
export enum PageContentType {
CMS = 'stackend-cms'
}
export interface PageContent {
name: string;
/** Simple reference type name */
type: ModuleType;
/** Is this content visible? */
visible: boolean;
/** Reference in string format */
reference: string;
/** Referenced object */
referenceRef: any;
/** Extra data. Type specific */
data: string | null;
}
export enum MenuVisibility {
HORIZONTAL = 'HORIZONTAL',
VERTICAL = 'VERTICAL',
FIXED = 'FIXED',
OFF = 'OFF'
}
export function parseMenuVisibility(v: string): MenuVisibility {
if (!v) {
return MenuVisibility.HORIZONTAL;
} else if (v === 'false') {
return MenuVisibility.OFF;
}
const x = v.toUpperCase();
if (x === MenuVisibility.VERTICAL) {
return MenuVisibility.VERTICAL;
} else if (x === MenuVisibility.FIXED) {
return MenuVisibility.FIXED;
}
return MenuVisibility.HORIZONTAL;
}
export interface EditContentResult extends XcapJsonResult {
content: Content | null;
}
/**
* Edit CMS content.
* @param id (may be 0 to create a new content object)
* @param permalink
* @param headline
* @param teaser
* @param body
* @param categoryId
*
* The body may be up to 65KB html.
*
* @returns {Promise}
*/
export function editContent({
id,
permalink,
headline,
teaser,
body,
categoryId
}: {
id?: number;
permalink?: string;
headline?: string;
teaser?: string;
body: string;
categoryId?: number;
} & XcapOptionalParameters): Thunk<Promise<EditContentResult>> {
return post({ url: '/cms/edit', parameters: arguments });
}
/**
* Set moderation status of cms content
* @param id
* @param moderationStatus
*/
export function setModerationStatus({
id,
moderationStatus
}: {
id: number;
moderationStatus: ModerationStatus;
} & XcapOptionalParameters): Thunk<Promise<XcapJsonResult>> {
return post({ url: '/cms/set-modstatus', parameters: arguments });
}
/**
* Remove CMS content.
*
* @param id Cms content id (required)
* @returns {Promise}
*/
export function removeContent({ id }: { id: number } & XcapOptionalParameters): Thunk<Promise<XcapJsonResult>> {
return post({ url: '/cms/remove', parameters: arguments });
}
/**
* Move CMS content
* @returns {Thunk<XcapJsonResult>}
*/
export function moveContent({
id,
newCategoryId,
oldCategoryId,
insertion,
insertionPoint
}: {
id: number;
newCategoryId: number;
oldCategoryId?: number;
insertion: Insertion;
insertionPoint: number;
} & XcapOptionalParameters): Thunk<Promise<XcapJsonResult>> {
return post({
url: '/cms/move',
parameters: {
referenceId: id,
newCategoryId,
oldCategoryId,
insertion,
insertionPoint
}
});
}
/**
* Create a new page
* @param name
* @param permalink
*/
export function newPage(name: string, permalink?: string): Page {
const pl = '/' + (permalink ? permalink : generatePermalink(name));
return {
id: 0,
name,
permalink: pl,
enabled: true,
metaDescription: null,
ogImageUrl: null,
content: [],
parentPageId: 0,
childCount: 0
};
}
export type EditPageResult = XcapJsonResult;
/**
* Edit a cms page
*
* @returns {Thunk<EditPageResult>}
*/
export function editPage({
page,
parentPageId
}: { page: Page; parentPageId?: number } & XcapOptionalParameters): Thunk<Promise<EditPageResult>> {
return post({
url: '/cms/pages/edit',
parameters: {
page: JSON.stringify(page),
parentPageId
}
});
}
/**
* Remove a cms page
* @param id
* @returns {Thunk<XcapJsonResult>}
*/
export function removePage({ id }: { id: number } & XcapOptionalParameters): Thunk<Promise<XcapJsonResult>> {
return post({ url: '/cms/pages/remove', parameters: arguments });
}
export interface GetPageResult extends XcapJsonResult {
page: Page | null;
}
/**
* List all content for a cms page.
* @param id
* @param permalink
* @param p
* @param pageSize
* @returns {Thunk<GetPageResult>}
*/
export function getPage({
id,
permalink,
p = 1,
pageSize = 100
}: {
id: number;
permalink?: string;
p?: number;
pageSize?: number;
} & XcapOptionalParameters): Thunk<Promise<GetPageResult>> {
return getJson({
url: '/cms/pages/get',
parameters: {
id,
permalink,
p,
pageSize
}
});
}
/**
* Search for pages where a content is used.
* @param contentId
* @param p
* @param pageSize
* @returns {Thunk<SearchPagesResult>}
*/
export function searchContentUse({
contentId,
p = 1,
pageSize = 10
}: {
contentId: number;
p?: number;
pageSize?: number;
} & XcapOptionalParameters): Thunk<Promise<SearchPagesResult>> {
return getJson({
url: '/cms/find-uses',
parameters: {
contentId,
p,
pageSize
}
});
}
export interface SearchPagesResult extends XcapJsonResult {
result: PaginatedCollection<Page>;
}
/**
* Search for cms pages
* @param q
* @param orderBy "name" or "createdDate"
* @param order
* @param p
* @param pageSize
* @returns {Thunk<SearchPagesResult>}
*/
export function searchPages({
q,
p,
pageSize,
orderBy,
order
}: {
q: string;
p: number | null;
pageSize: number | null;
orderBy: 'name' | 'createdDate' | null;
order: Order | null;
} & XcapOptionalParameters): Thunk<Promise<SearchPagesResult>> {
return getJson({
url: '/cms/pages/search',
parameters: arguments
});
}
export interface GetPagesResult extends XcapJsonResult {
pages: { [id: string]: Page };
}
/**
* Get multiple pages by id.
* @param pageIds
* @param permalinks
* @param communityPermalink
* @returns {Thunk<GetPagesResult>}
*/
export function getPages({
pageIds,
permalinks,
communityPermalink
}: {
pageIds?: Array<number>;
permalinks?: Array<string>;
communityPermalink?: string | null;
} & XcapOptionalParameters): Thunk<Promise<GetPagesResult>> {
return getJson({
url: '/cms/pages/get-multiple',
parameters: {
pageId: pageIds,
permalink: permalinks,
[COMMUNITY_PARAMETER]: communityPermalink
}
});
}
export interface SearchPageContentResult extends XcapJsonResult {
result: { [key: string]: Array<PageContent> };
}
/**
* Search for page content
* @param q
* @param codeBinOnly
* @returns {Thunk<SearchPageContentResult>}
*/
export function searchPageContent({
q,
codeBinOnly
}: {
q: string;
codeBinOnly: boolean;
} & XcapOptionalParameters): Thunk<Promise<SearchPageContentResult>> {
return getJson({
url: '/cms/pages/search-page-content',
parameters: arguments
});
}
export interface GetAvailablePagePermalinkResult extends XcapJsonResult {
availablePermalink: string;
}
/**
* Construct an available permalink for cms pages
* @param pageId
* @param permalink
* @returns {Thunk<XcapJsonResult>}
*/
export function getAvailablePagePermalink({
pageId,
permalink
}: {
pageId?: number | null;
permalink: string;
} & XcapOptionalParameters): Thunk<Promise<GetAvailablePagePermalinkResult>> {
return getJson({
url: '/cms/pages/get-available-permalink',
parameters: arguments
});
}
export type SubSiteNode = Node;
export type SubSite = Tree;
export interface GetSubSiteResult extends XcapJsonResult {
tree: SubSite | null;
referencedObjects: { [ref: string]: any };
}
/**
* Get a subsite by id or permalink
* @param id
*
*/
export function getSubSite({
id,
permalink
}: { id?: number; permalink?: string } & XcapOptionalParameters): Thunk<Promise<GetSubSiteResult>> {
return getJson({
url: '/cms/subsite/get',
parameters: arguments
});
}
/**
* Store a subsite
* @param subSite
*/
export function storeSubSite({
subSite
}: { subSite: SubSite } & XcapOptionalParameters): Thunk<Promise<GetSubSiteResult>> {
return post({
url: '/cms/subsite/store',
parameters: {
tree: JSON.stringify(subSite)
}
});
}
export interface RemoveSubSiteResult extends XcapJsonResult {
removed: boolean;
}
/**
* Remove a sub site
* @param id
*/
export function removeSubSite({ id }: { id: number } & XcapOptionalParameters): Thunk<Promise<RemoveSubSiteResult>> {
return post({
url: '/cms/subsite/remove',
parameters: arguments
});
}
export interface SearchSubSiteResult extends XcapJsonResult {
trees: PaginatedCollection<SubSite>;
}
/**
* Search for sub sites
* @param q
* @param p
* @param pageSize
*/
export function searchSubSites({
q,
p,
pageSize
}: {
q?: string | null;
p?: number;
pageSize?: number;
} & XcapOptionalParameters): Thunk<Promise<SearchSubSiteResult>> {
return getJson({
url: '/cms/subsite/list',
parameters: arguments
});
}
/**
* Create, but does not store a new sub site
*/
export function newSubSite(name: string): SubSite {
return newTree(name);
}
/**
* Create, but does not store a new sub site node
*/
export function newSubSiteNode(name: string): SubSiteNode {
return newTreeNode(name);
}
/**
* Get the default page (start page) id.
* @param subSite
* @returns {number}
*/
export function getDefaultPageId(subSite: SubSite): number {
if (!subSite) {
return 0;
}
if (subSite.referenceId) {
return subSite.referenceId;
}
for (let i = 0; i < subSite.children.length; i++) {
const c = subSite.children[i];
if (c.referenceId) {
return c.referenceId;
}
}
return 0;
}
/**
* Add content to the dom. Client side only
* @param contentId
* @param html
* @param css
* @param javascript
* @param parent
*/
export function _addContentToDom(
parent: Element,
contentId: number,
html: string | null,
css: string | null,
javascript: string | null
): void {
const document = parent.ownerDocument;
if (parent && (html || css)) {
const addHtml = (css ? css : '') + (html ? html : '');
try {
const f = document.createRange().createContextualFragment(addHtml);
// Replace children
while (parent.childNodes.length > 0) {
parent.removeChild(parent.childNodes[0]);
}
parent.appendChild(f);
} catch (x) {
console.warn('Error in javascript tag: ', addHtml, ' from cms module: ', contentId);
}
}
if (javascript) {
try {
const range = document.createRange();
range.setStart(document.getElementsByTagName('BODY')[0], 0);
document.getElementsByTagName('BODY')[0].appendChild(range.createContextualFragment(javascript));
} catch (x) {
console.warn('Error in javascript: ', x, ' from cms module: ', contentId);
}
}
}
/**
* Add content to the dom. Client side only
* @param parent
* @param content
*/
export function addContentToDom(parent: Element, content: Content): void {
if (!parent || !content) {
return;
}
if (isRunningServerSide()) {
throw Error('Stackend: addContentToDom can not be executed serverside');
}
const { htmlValue, javascriptValue, cssValue } = extractContentValues(content);
const style = `<style>${cssValue}</style>`;
let javascript = '';
if (javascriptValue !== '') {
javascript = `<script type="text/javascript" id="stackend-cms-js-${content.id}">${javascriptValue}</script>`;
}
const html = style + `<div class='${RICH_CONTENT_CSS_CLASS}'>${htmlValue}</div>`;
_addContentToDom(parent, content.id, html, '', javascript);
}
/**
* Remove all child nodes of an element
* @param element
*/
export function removeAllChildNodes(element: Element): void {
while (element.childNodes.length > 0) {
element.removeChild(element.childNodes[0]);
}
}
/**
* Construct a composite content value
* @param html
* @param css
* @param js
*/
export function createContentValue(html: string, css: string, js: string): string {
const stringStyle = `<style type="text/css" data-stackend-cms="css">${css}</style>`;
const stringJavascript = `<script type="text/javascript" data-stackend-cms="js">${js}</script>`;
const stringHtml = `<div data-stackend-cms="html">${html}</div>`;
// HTML should be the last one to not mess up the other two in case of backend tag balancing.
// However, javascript needs to be last for preview to work
return `${stringStyle}${stringHtml}${stringJavascript}`;
}
/**
* Split a content object into HTML, CSS and JS.
* NOTE: Browser only
* @param content
* @returns {{htmlValue: (*|string), javascriptValue: (*|string), cssValue: (*|string)}}
*/
export function extractContentValues(content: Content | null): {
htmlValue: string;
javascriptValue: string;
cssValue: string;
} {
let html: Element | null = null;
let javascript: Element | null = null;
let css: Element | null = null;
if (content && content.body) {
const xmlDoc = new DOMParser().parseFromString(content.body, 'text/html');
css = xmlDoc.evaluate('//style[@data-stackend-cms="css"]', xmlDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue as Element;
javascript = xmlDoc.evaluate(
'//script[@data-stackend-cms="js"]',
xmlDoc,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue as Element;
html = xmlDoc.evaluate('//div[@data-stackend-cms="html"]', xmlDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue as Element;
// Fall backs to old style code
if (css === null) {
css = xmlDoc.evaluate('/html/head/style', xmlDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue as Element;
}
if (html == null) {
html = xmlDoc.evaluate('/html/body/div', xmlDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue as Element;
}
if (javascript == null) {
javascript = xmlDoc.evaluate('/html/body/script', xmlDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue as Element;
}
}
return {
htmlValue: html ? html.innerHTML : '',
javascriptValue: javascript ? javascript.innerHTML : '',
cssValue: css ? css.innerHTML : ''
};
}
export interface ContentValues {
[id: string]: {
html: string | null;
javascript: string | null;
style: string | null;
};
}