Table of contents

Getting Started - Surfer Guidelines

FAQ / Troubleshooting - Surfer Guidelines

API reference and TypeScript Declaration

interface Window {
  surferGuidelines: {
    /**
     * Initializes and injects Surfer Guidelines into target.
     *
     * It's a simple wrapper around `initWithOptions`.
     *
     * When using this function Surfer's iframe will always have
     * `surfer-guidelines` attached and `surfer-guidelines-wide`
     * will be added when presented view won't fit into sidebar.
     *
     * ### Example
     *
     * ```js
     * window.surferGuidelines.init(document.body, "sharing key");
     * ```
     *
     *
     * @param target HTMLElement reference to which Surfer
     *  guidelines will be appended
     *
     * @param sharingToken A prefix to your draft's url which enables
     *  users who are not logged into Surfer to access guidelines
     */
    init(
      target: HTMLElement,
      sharingToken?: string,
      options?: {
        permalink?: string
        anonymous?: boolean
        partner?: string
        onContentScoreChanged?: (contentScore: number) => void
        onContentScoreDetailsChanged?: (
          scoreDetails: ContentScoreDetails
        ) => void
        onTermsToUseChanged?: (termsToUse: any[]) => void
        onFocusTermChanged?: (focusTerm: any) => void
      }
    ): void

    /**
     * Sets html
     *
     * Provided html should contain a draft only,
     * Surfer won't try to extract it from the entire page.
     *
     * This function works only if Surfer Guidelines
     * were created with the `init` function.
     *
     * ```js
     * const editor = document.getElementById("#editor");
     *
     * surferGuidelines.init(document.body);
     *
     * editor
     *   .addEventListener(
     *       'keyup',
     *       (e) => surferGuidelines.setHtml(e.currentTarget.value)
     *   )
     *
     * surferGuidelines.setHtml(editor.value)
     * ```
     */
    setHtml(html: string): void

    /**
     * Creates an iframe with Surfer guidelines and returns
     * reference and callbacks.
     *
     * Iframe element has to be manually placed into the DOM.
     *
     * ### Example
     *
     * ```js
     * const {
     *   $iframe,
     *   setPermalink,
     *   setHtml,
     *   requestView,
     * } = window.surferGuidelines.initWithOptions({
     *   onNavigation(view) {
     *     if (view === "draft_configuration") {
     *       // make the iframe wider so it has space for configuration
     *       $iframe.classList.add("surfer-guidelines-wide");
     *       window.addEventListener("click", goToGuidelines);
     *     } else {
     *       // place it into the sidebar
     *       $iframe.classList.remove("surfer-guidelines-wide");
     *       window.removeEventListener("click", goToGuidelines);
     *     }
     *   },
     * });
     *
     * const goToGuidelines = () => {
     *   requestView("guidelines");
     * };
     *
     * const target = document.getElementById("surfer_guidelines");
     * target.appendChild($iframe);
     *
     * setPermalink(
     *   <https://your.cms.com/draft/some-draft-id>"
     * );
     *
     * setHtml("<p>My HTML</p>")
     * ```
     *
     */
    initWithOptions(opts?: FrameCommunicatorOptions): {
      /**
       * Reference to Surfer's iframe. It isn't attached to the DOM yet.
       *
       * ### Example
       *
       * ```js
       * const target = document.getElementById("surfer_guidelines");
       * target.appendChild($iframe);
       * ```
       */
      $iframe: HTMLIFrameElement

      /**
       * Sets new permalink
       *
       * If new permalink is different than the old one html
       * will be set to `null`
       *
       * ## Secure vs not secure permalink
       *
       * All permalinks with `http://` or `https://` are treated as
       * not secure which means that in order to access them user
       * needs to be logged into owners Surfer account.
       *
       * This is because a lot of CMS websites have easily guessable
       * editor links potentially allowing malicious party to get access
       * to your drafts.
       *
       * If you want to allow other users who don't have access to your
       * Surfer account edit drafts add some random prefix to your
       * drafts url. Such integration key will be managed only by
       * you and has to be constant for given draft.
       *
       * The best practice would be to generate some random string for each
       * new draft and add it before your url.
       * Such as `some-random-uuid:<https://your.cms.com/draft/123`>.
       *
       * Permalink can also be just a random string such as `some-random-uuid`.
       *
       * It's also possible to generate one constant key for your entire
       * integration but when you share one draft with third party
       * they will be able to guess other links to your drafts.
       * `CMS-wide-sharing-key:<https://your.cms.com/draft/123`>
       *
       * ### Example
       *
       * Not secure permalink
       *
       * ```js
       * setPermalink("<https://your.cms.com/draft/some-draft-id>");
       * ```
       *
       * Secure permalink
       *
       * ```js
       * setPermalink(
       *   encodeURIComponent(
       *     "sharing key" + "<https://your.cms.com/draft/some-draft-id>"
       *   )
       * );
       * ```
       */
      setPermalink: (permalink: string | null) => void

      /**
       * Sets current html
       *
       * Provided html should contain a draft only, Surfer won't try to
       * extract it from whole page.
       *
       * Note that since calling `setPermalink` with different permalink
       * drops html content from the state this function
       * should be called after such change
       *
       * ### Example
       *
       * ```js
       *
       * ```
       */
      setHtml: (html: string | null) => void

      /**
       * Enables client to change view inside the iFrame
       */
      requestView: (requestedView: RpcRequestedView) => void

      /**
       * Indicates that some draft properties were changed and it should be reloaded
       */
      refreshDraft: () => void

      /**
       * Configures the `guidelines` view
       */
      configureView: (
        config: Readonly<{
          disableDraftConfiguration?: boolean
          disableBatchContentEditorCreation?: boolean
          disableHelpMenu?: boolean
          alwaysDisplayContentScoreDetails?: boolean
          enableGuidelinesFilePasting?: boolean
        }>
      ) => void

      /*
       * Returns terms that were matched in the text, with their positions.
       */
      getMatchedTerms: (terms: string[]) => Readonly<{
        terms: string[]
        matches: Array<{
          term: string
          wordSegments: Array<{
            index: number
            originalSegment: string
          }>
        }>
      }>
    }
  }
}

interface FrameCommunicatorOptions {
  /**
   * Disables attempt to authorize, showing guidelines for a not logged in user
   */
  anonymous?: boolean
  /**
   * Allows to customize iframe appearance based on who is using it
   */
  partner?: string

  /**
   * Callback triggered each time iframe navigates to different view
   */
  onNavigation?(view: RpcView): void

  /**
   * Callback triggered each time iframe finishes loading some draft
   *
   * null means that no draft with such permalink was found
   */
  onDraftLoaded?(permalink: string | null): void

  /**
   * Callback triggered when the content score is updated
   */
  onContentScoreChanged?(contentScore: number): void

  /**
   * Callback triggered when the content score is updated
   */
  onContentScoreDetailsChanged?(contentScoreDetails: ContentScoreDetails): void

  /**
   * Callback triggered when terms to use change (e.g. specific term was used OR terms were changed in configuration view)
   */
  onTermsToUseChanged?(termsToUse: any[]): void

  /**
   * Callback triggered when focus term changes
   */
  onFocusTermChanged?(focusTerm: any): void

  /**
   * Callback triggered when structural guidelines (i.e. headings or paragraphs count) change
   */
  onStructuralGuidelinesChanged?(
    structuralGuidelines: Array<{
      id: string
      factor: string
      target: { value: number }
      value: number
      ranges: { [key: string]: [number, number] }
    }>
  ): void

  /**
   * Callback triggered when Draft wants to paste guidelines file
   */
  onPasteGuidelinesFile?(guidelines: string): void
}

type RpcView =
  | 'guidelines'
  | 'draft_configuration'
  | 'draft_creation'
  | 'draft_not_found'
  | 'draft_loading'

type RpcRequestedView = 'guidelines' | 'draft_configuration'

interface Group {
  value: number
  maxValue: number
  groupName: string
}

type ContentScoreDetails = {
  score: number
  competitors_avg?: number
  competitors_max?: number
  details?: Group[]
}