TPP_SNAP

The fs-tpp-api/snap.js must be used in the markup of the third party app. There is no specific position defined, but if you want to use the API of window.TPP_SNAP, your own script must be loaded after the inclusion fs-tpp-api/snap.js, of course. You could define the optional [data-firstspirit-origin] attribute to ensure that the first(!) postMessage (aka. TPP handshake) is only committed to the real ContentCreator frame.

<script src="path/to/fs-tpp-api/snap.js" data-firstspirit-origin="http://firstspirit:8000"></script>

All you have to do is use the [data-preview-id] in your markup. This library automatically decorates those containers with the known borders and buttons. Content changes will force a refresh of the current page, so there is no need to implement in JS!

But of course you are able to do this by using the framework! That's what the docs are for...

As an example, a FirstSpirit Template delivers some HTML Markup, which you would like to update in-place in case of changes. You could use the [data-on-tpp-change] attribute which is a representation of the onContentChange~Handler. Or you use it directly, like this:

TPP_SNAP.onContentChange(($node, previewId, content) => {
  if ($node.matches('.content') && content !== null) {
    $node.innerHTML = content;
    return false;
  }
})

You can register multiple handlers. Further processing of the event is stopped after the first handler returns a value !== undefined.

<div class="content"
  data-preview-id="<previewId>"
  data-on-tpp-change="if (content !== null) { this.innerHTML = content; return false; }">
</div>

Preview Element

To change the current preview element, you should use the method TPP_SNAP.setPreviewElement. This also ensures that the ContentCreator keeps track of the route change and the shown workflows are those of the current element:

TPP_SNAP.setPreviewElement(pageRefPreviewId);

Note that for navigating to another page, you should use the preview ID for its PageRef instead for the page itself.

For being able to open pages from within the ContentCreator (e.g. to use the search function) you have to specify your own listener to handle this request. The method to use for this purpose is TPP_SNAP.onRequestPreviewElement. A handler registered with onRequestPreviewElement should implement the frontend specific way to route to the requested page using the given preview id.

TPP_SNAP.onRequestPreviewElement(async (previewId) => {
  // The following previewIdToPath function is just a placeholder! The mapping from previewId to route has
  // to be defined project-dependent!
  let path = previewIdToPath(previewId);

  // The routing mechanism depends on the frontend as well, thus the "route" function also has to
  // be understood as a placeholder!
  if (path) return route(path);
  // ... error handling (e.g. 404) ...
}

Nested Components

Nested components of input components such as FS_CATALOG or FS_INDEX can be edited as well. To enable the editing of such a component make sure to provide a custom preview id for both the nested component as well as the parent component using the syntax #PARENT_COMPONENT_NAME and #NESTED_COMPONENT_INDEX as the following example shows.

<div data-preview-id="8326527a-60ff-49a0-bc66-91671d660249">
    <ul data-preview-id="#pt_catalog">
        <li data-preview-id="#0">This is content from the first entry.</li>
        <li data-preview-id="#1">The second component does have different content.</li>
        <li data-preview-id="#2">And of course, this is also different.</li>
    </ul>
</div>

Notice that the presence of the preview id of the data provider that contains the FS_CATALOG component is needed. As long as the HTML complies with this structure the individual nested components will automatically provide buttons for interaction.

NOTE: This feature also works for multiple nested components (e.g. a Catalog$Card that is part of of a Index$Record). It must be noted that in such a scenario, the path to a nested component must always alternate between a component name and an index. Additionally the last part of the path must always be an index of a nested component.

NOTE: Currently only FS_INDEX components that use the DatasetDataAccessPlugin are supported.

Inline Editing

Some input components support inline editing when enabled. All input components named in the FirstSpirit Online Documentation are supported. To enable inline editing for such components a custom preview id needs to be provided as a data attribute on the related DOM node using the syntax #INPUT_COMPONENT_NAME.

The following example includes a DOM node that corresponds to a CMS_INPUT_TEXT component with the name pt_headline.

<div data-preview-id="8326527a-60ff-49a0-bc66-91671d660249">
    <span data-preview-id="#pt_headline">
        Our various services for sales, marketing and installation branches.
    </span>
</div>

Notice that the presence of the preview id of a data provider that contains the editable input component is needed. As long as the HTML complies with this structure the component will automatically become inline editable.

CaaS Mode

The CaaS mode is mainly useful for scenarios where your web app uses the CaaS as the source for FirstSpirit content and your app can't render changed content dynamically (e.g. using the respective event handlers).

Whenever a re-rendering of the current page or the preview of an element was requested, this mode waits for the changed content to be present in the CaaS and then triggers the related handlers (onRerenderView~Handler or onRequestPreviewElement~Handler).

To enable the CaaS mode use the TPP_SNAP.enableCaasMode function.

TPP_SNAP.onInit(async (success) => {
  if(success) {
    const { previewCollectionUrl, apiKey } = {
      previewCollectionUrl: "https://caas-host/my-tenant-id/f948bb48-4f6b-4a8a-b521-338c9d352f2b.preview.content",
      apiKey: "9afa9e21-d02f-4836-9e55-111fcf6521a3"
    }
    TPP_SNAP.enableCaasMode(previewCollectionUrl, apiKey)
  }
})

NOTE: When using the CaaS mode in combination with dynamically rendering changed content (e.g. using the onContentChange~Handler) make sure to avoid handling any content changes that should lead to a re-rendering of the current page.

NOTE: The CaaS mode requires a minimum version for the CaaS platform and CaaS Connect module, see TPP_SNAP.enableCaasMode for more information.

Default Buttons

(Click to expand) Overview of default buttons shipped with Snap:
Button Name Visibility Requirement Functionality
edit Visible on elements of type PageRef, Page, Section, GCAPage, GCASection, Dataset and SectionReference. The current user has the permission to change the element's content. This button opens a dialog which enables the user to edit the element's content.
translate Visible on elements of type PageRef, Page, Section, GCAPage, GCASection, Dataset and SectionReference being defined as multi-language. The current user has the permission to change the element's content. The user may select a translation from the current to another possible language, which will bring up the respective translation dialog.
metadata Not visible by default; for this button to be shown, it should be overridden by the developer. The current user has the permission to change the element's meta data. On click, this buttons opens the meta data dialog for the current element.
add-sibling-section Only visible on sections. The current user has the permission to change the page. Opens a dialog for creating a new sibling section.
add-child-section Only visible on elements of type Page with one Body element as a child. The current user has the permission to add a new leaf to the current page. Opens a dialog for creating a new section as a child of the page's Body element.
add-child-section-body Only visible on elements of type Body. The current user has the permission to add a new leaf to the current page. Opens a dialog for creating a new section as a child of the body.
workflows Only visible on elements supporting a release whilst not using a custom workflow; also, if no workflow has already been started for the element, at least one must be allowed. If visible, this button is always enabled. The user has to select either a new workflow to start, if none is already, or the next transition to take.
tpp-icon-delete Not visible on elements of type Body or Media. If a workflow is enabled, only usable if the element is neither of type Section nor SectionReference and a deletion workflow is defined. If no workflow is used, the user has to be granted the deletion permission. The button does either start a delete workflow if workflows are used, otherwise it deletes the selected element directly.
tpp-icon-crop-image Only visible on elements of type Media. Only applicable if the user has the permission to change this element. On click, opens a dialog which allows the user to crop the selected image. The default resolution used for this action is "ORIGINAL", but can be specified by the developer via setting the attribute data-tpp-context-image-resolution to res1, res2 in the DOM.
bookmark Only visible on elements of type Page, PageRef, Section, Dataset or SectionReference. If visible, this button is always enabled. On click, the selected element is added to (or removed from) the project bookmarks which can be viewed in the ContentCreator toolbar.
edit-component Visible on components lying inside a "nested component" structure. Usable if the current user has the permission to change the element's content. On click, this button opens a dialog which enables the user to edit the selected component.
create-component Visible on catalog or index components inside a "nested component" structure. Usable if the current user has the permission to add a new leaf to the page. The user may select an applicable template for a subordinate component to be created; on click, the Create dialog is shown.
delete-component Visible on components lying inside a "nested component" structure. Usable if the current user has the permission to delete the element. On click, this button deletes the selected element after confirmation.

TPP_SNAP
Static Members
isConnected
isLegacyCC
enableCaasMode(previewCollectionUrl, apiKey, options?)
onInit(handler)
onContentChange(handler)
onRerenderView(handler)
onNavigationChange(handler)
onRequestPreviewElement(handler)
registerButton(button, index)
overrideDefaultButton(defaultButtonName, buttonOverrides)
findPreviewNodes(previewId)
execute(identifier, params, result)
getPreviewElement()
setPreviewElement(previewId)
getPreviewLanguage()
showEditDialog(previewId)
showMetaDataDialog(previewId)
getElementStatus(previewId, refresh)
renderElement(previewId)
deleteElement(previewId, showConfirmDialog)
startWorkflow(previewId, workflow)
processWorkflow(previewId, transition)
createPage(path, uid, template, options?)
createSection(previewId, options?)
moveSection(source, target, options?)
createDataset(template, options?)
toggleBookmark(previewId)
cropImage(previewId, resolution, result)
languages()
locales()
previewUrl()
showTranslationDialog(previewId, source, target)
showMessage(message, kind, title?)
showQuestion(message, title?)
triggerChange(previewId, content)
triggerRerenderView()
mppAddParameterizedListener(listener)
mppAddParameterListener(listener)
mppAddTimeParameterListener(listener)
mppGetParameter(name)
mppGetTimeParameter()
mppIsParameterized()
mppSetParameter(name, value)
mppSetTimeParameter(date)

onInit~Handler

onInit~Handler(success: boolean, isLegacyCC: boolean)

Type: Function

Since: 1.2.0
Parameters
success (boolean) the TPP handshake was successful or not
isLegacyCC (boolean) if the TPP handshake was successful, whether the connected ContentCreator uses the legacy design

onContentChange~Handler

onContentChange~Handler($node: HTMLElement, previewId: string, content: any): any

Type: Function

Since: 1.2.0
Parameters
$node (HTMLElement) a DOM node which contains the [data-preview-id] attribute
previewId (string) the current PreviewId
content (any) the changed content, could be a string, or an object (if the FirstSpirit Template generates JSON) or null, if this PreviewElement was deleted.
Returns
any: if the handler doesn't return anything, onRerenderView~Handler will be triggered.

onRerenderView~Handler

onRerenderView~Handler()

Type: Function

onNavigationChange~Handler

onNavigationChange~Handler(previewId: (string | null))

Type: Function

Since: 1.2.0
Parameters
previewId ((string | null)) of a newly created page, otherwise null

onRequestPreviewElement~Handler

onRequestPreviewElement~Handler(previewId: string)

Type: Function

Since: 1.2.0
Parameters
previewId (string) the current PreviewId

ButtonScope

The ButtonScope is an object which is used by all button callbacks.

ButtonScope
Parameters
$node (HTMLElement) the DOM node where the decoration appears
$button (HTMLElement) the DOM node of the button (not available in Button#isVisible~Callback )
previewId (string) the PreviewId
status (Status) the current Status object of PreviewId
language (string) the current language

Button

Button
Parameters
button (object)
Name Description
button.label string (default '') simple labeling, used by the default of Button#getLabel~Callback
button.css string? simple css class definition, used by the default of Button#getIcon~Callback
button.icon string? simple icon url, used by the default of Button#getIcon~Callback
button.supportsComponentPath boolean? whether the button is applied to elements specifying no actual, but a component path preview ID prefixed with "#"
button.supportsInedit boolean? whether the button is applied to elements that can be edited in-place
button.isVisible Button#isVisible~Callback? whether this button should be rendered or not; the default is (scope) => true
button.isEnabled Button#isEnabled~Callback? whether this button should be enabled or not; the default is (scope) => false
button.getIcon Button#getIcon~Callback? use scope.$button to define the appearance of the button
button.getLabel Button#getLabel~Callback? the tooltip ( [title] ) for this button
button.getItems Button#getItems~Callback? if this is not an empty list, a dropdown will be rendered; the default is (scope) => []
button.beforeExecute Button#beforeExecute~Callback? will be called before Button#execute~Callback
button.execute Button#execute~Callback? will be called, when the button (or an item) is clicked
button.afterExecute Button#afterExecute~Callback? will be called after Button#execute~Callback

Button#isEnabled~Callback

Button#isEnabled~Callback(scope: ButtonScope): Promise<boolean>

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
Returns
Promise<boolean>:

Button#getIcon~Callback

Button#getIcon~Callback(scope: ButtonScope)

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
Example
// the default callback is defined as:
TPP_SNAP.registerButton({
  icon = null,
  css = null,
  getIcon = async ({ $button }) =>
    (css !== null && !$button.classList.add(css))
    || (icon !== null && ($button.style.backgroundImage = `url(${icon})`))
    || $button.classList.add('tpp-icon-action'),
  ...
});

Button#getLabel~Callback

Button#getLabel~Callback(scope: ButtonScope)

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
Example
// the default callback is defined as:
TPP_SNAP.registerButton({
  label = '',
  getLabel = () => label,
  ...
});
// localize
TPP_SNAP.registerButton({
  getLabel: ({ language }) => language.toLowerCase() === 'de' ? 'Deutsche Bezeichnung' : 'English Label',
  ...
});

Button#getItems~Callback

An Item could be anything, but it needs a property called label, which appears in the dropdown.

Button#getItems~Callback(scope: ButtonScope)

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
Example
TPP_SNAP.registerButton({
  label = '',
  getItems = () => {
    return [
      { label: 'Option 1', value: 1 },
      { label: 'Option 2', value: 2 },
      { label: 'Option 3', value: 3 },
    ];
  },
  execute = (scope, item) => console.log(item.value),
  ...
});

Button#beforeExecute~Callback

Button#beforeExecute~Callback(scope: ButtonScope, item: any)

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
item (any) see Button#getItems~Callback

Button#execute~Callback

Button#execute~Callback(scope: ButtonScope, item: any): Promise<void>

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
item (any) see Button#getItems~Callback
Returns
Promise<void>:

Button#afterExecute~Callback

Button#afterExecute~Callback(scope: ButtonScope, item: any, error: Error?)

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
item (any) see Button#getItems~Callback
error (Error?) if an uncatched error appears

Button#isVisible~Callback

Be careful with your Promise here: the rendering of all buttons only happens after all Button#isVisible calls. That's why ButtonScope.$button doesn't exists in ButtonScope this time.

Button#isVisible~Callback(scope: ButtonScope): Promise<boolean>

Type: Function

Since: 1.2.0
Parameters
scope (ButtonScope)
Returns
Promise<boolean>:

DuplicatePageError

An error that represents a duplication of a page (e.g. thrown by createPage in case a duplicate UID was detected). It encapsulates the preview ID of the duplicated element.

DuplicatePageError

Type: Error

Extends Error

Since: 1.2.24
Properties
previewId (string) : the preview ID of the already existing page
Example

This error can be handled by applying .catch((e) => {...}) on the returned promise. Via DuplicatePageError~getPreviewId method one can also retrieve the preview ID of the element that was duplicated.

TPP_SNAP.createPage("path/to/homepage", "homepage", "homepage", {
    forceUid: true
}).catch(e => {
   console.log(e.message);
   console.log(e.previewId);
});
Static Members
getPreviewId()

Status

The status is not a stable API, yet.

Status
Parameters
previewId (string) the raw PreviewId
custom ((string | null)) is null, if the status is delivered by FirstSpirit. Otherwise this contains information from the markup.

CustomStatus

A custom PreviewId starts with custom: and can be used in a URN Schema way to create custom buttons.

CustomStatus

Extends Status

Since: 1.2.0
Example
// <div data-preview-id="custom:create-page:my/firstspirit/path:myPageName"></div>

TPP_SNAP.registerButton({
  css: 'tpp-icon-create-page',
  label: 'addMyPageName',
  isVisible: ({ status }) => status.custom !== null && status.parts[0] === 'create-page',
  execute: ({ status }) => {
    const [, path, name] = status.parts;
    TPP_SNAP.createPage(path, name, 'page_template_uid');
  },
})

addInlineEditing

Elements could be decorated and undecorated several times, when the page initializes or is changing. So we have to check, if the current inline-decoration is still needed.

If in future here comes more initialization after getStatus() resolves, it might be better to refactor the whole decoration process and apply the whole decoration when getStatus() resolves.

addInlineEditing

matches

Manual polyfill. The elements provided by MutationObserver do not seem to be enhanced by babel-polyfills.

matches