as-pdf.js

import {Component_A} from './basic/as-component.js';
import {Popup_A} from './as-popup.js';
import {ErrorCode, ExceptionIdMap} from '../exception/exceptionDesc.js';
import {isChromiumBased, isInAppStudio} from '../utils/utils.js';

/**
 * @description Properties accepted by the Pdf component, defining its appearance, behavior, and lifecycle hooks.
 * This class focuses on the specific properties of the Pdf component.
 * Since it inherits from Accessor_A, all basic properties (e.g., height, width) are available but documented in the Accessor_A part.
 * @typedef TComponents.PdfProps
 * @prop {object} [options] Additional options for the pdf component.
 * Items in this object include:
 * - **responsive** (boolean, default: false): Whether the pdf container should be responsive.
 * @prop {Function|string} [onCreated] Lifecycle hook invoked after component instantiation.
 * @prop {Function|string} [onMounted] Lifecycle hook invoked after component is attached to the DOM.
 * @prop {string} [position] CSS positioning of the element.
 * @prop {number} [width] Component width.
 * @prop {number} [height] Component height.
 * @prop {number} [top] Top offset in pixels.
 * @prop {number} [left] Left offset in pixels.
 * @prop {number} [rotation] Rotation angle in degrees for visual transform.
 * @prop {number} [borderRadius] Corner radius in pixels.
 * @prop {number} [zIndex] z-index stacking order.
 * @prop {string} [pdfSrc] The source location of the pdf component.
 * @prop {string} [emptyMessage] Message displayed when the content is empty.
 * @memberof TComponents
 */

/**
 * @ignore
 */
const logModule = 'as-pdf';

/**
 * PDF viewer component rendered through the browser's native PDF support.
 * @class TComponents.Pdf
 * @extends TComponents.Component_A
 * @memberof TComponents
 * @param {HTMLElement} parent - HTML element that is going to be the parent of the component.
 * @param {TComponents.PdfProps} [props] - The properties object.
 * @example
 * const pdf = new TComponents.Pdf(document.body, {
 *   position: 'absolute',
 *   width: 200,
 *   height: 200,
 *   zIndex: 1000,
 *   pdfSrc: 'xxxx',
 * });
 *
 * // Render the component.
 * await pdf.render();
 */
export class Pdf extends Component_A {
  constructor(parent, props = {}) {
    super(parent, props);

    /**
     * @type {HTMLIFrameElement|null}
     * @private
     */
    this._frame = null;

    /**
     * @private
     */
    this._lastSourceToken = null;

    /**
     * @private
     */
    this._resolvedSrc = null;

    /**
     * @private
     */
    this._objectUrl = null;
    this._fUpdate = true;
  }

  /**
   * @description Returns the default properties of the pdf component.
   * @member TComponents.Pdf#defaultProps
   * @method
   * @protected
   * @returns {TComponents.PdfProps} Default properties.
   */
  defaultProps() {
    return {
      options: {
        responsive: false,
      },
      // life cycle
      onCreated: '',
      onMounted: '',
      onDispose: '',

      pdfSrc: '',
      emptyMessage: '',

      // component layout defaults
      position: 'static',
      width: 600,
      height: 400,
      top: 0,
      left: 0,
      zIndex: 0,
      rotation: 0,
      border: '1px solid #dbdbdb',
    };
  }

  /**
   * @description Renders the pdf component, applying styles and attaching event listeners.
   * @member TComponents.Pdf#onRender
   * @method
   * @async
   * @returns {Promise<void>}
   */
  async onRender() {
    try {
      this.removeAllEventListeners();
      const root = this.find('.tc-pdf');
      const loadingEl = this.find('[data-role="loading"]');

      this._frame = this.find('[data-role="frame"]');

      if (!root || !this._frame) {
        Logger.e(logModule, ErrorCode.FailedToRunOnRender, 'Frame element not found in PDF component.');
        return;
      }
      const bInShell = isInAppStudio();
      const overlayEl = this.find('[data-role="overlay"]');
      overlayEl && (overlayEl.hidden = !isInAppStudio());

      // TPU standard mode is not compatible
      if (!isChromiumBased()) {
        Logger.e(logModule, ErrorCode.FailedToRunOnRender, 'Frame element not supported in TPU standard mode');
        this._setError(Component_A.t(`framework:pDFComponent.incompatibleBrowser`));
        return;
      }
      root.classList.toggle('tc-pdf-disabled', !this.enabled);
      root.style.border = this._props.border;

      this._setError('');
      this._setLoading('');

      if (!this._props.pdfSrc) {
        this._cleanupFrame();
        this._setError(bInShell ? 'No PDF source provided.' : Component_A.t(`framework:pDFComponent.emptyPDFSource`));
        return;
      }

      const resolvedSrc = this._prepareResolvedSrc(this._props.pdfSrc);

      if (!resolvedSrc) {
        this._cleanupFrame();
        this._setError(
          bInShell ? 'Failed to resolve PDF source.' : Component_A.t(`framework:pDFComponent.failedToResolvePdfSrc`),
        );
        return;
      }

      if (this._frame.getAttribute('src') !== resolvedSrc) {
        this._setLoading(bInShell ? 'Loading...' : Component_A.t(`framework:pDFComponent.loading`));
        this._attachFrameEvents(loadingEl);
        this._frame.setAttribute('src', resolvedSrc);
      }
    } catch (error) {
      this._handleError(error);
      this._cleanupFrame();
    }
  }

  /**
   * @description Operations performed during component destruction
   * @member TComponents.Pdf#onDestroy
   * @method
   * @returns {void}
   */
  onDestroy() {
    this._cleanupFrame();
  }

  /**
   * @description Add events to the frame
   * @member TComponents.Pdf~_attachFrameEvents
   * @method
   * @private
   * @returns {void}
   */
  _attachFrameEvents() {
    if (!this._frame) return;

    this.addEventListener(this._frame, 'load', () => {
      this._setLoading('');
      this._setError('');
    });

    this.addEventListener(this._frame, 'error', () => {
      this._setLoading('');
      this._setError(isInAppStudio() ? 'Failed to load PDF.' : Component_A.t(`framework:pDFComponent.failedToLoadPDF`));
    });
  }

  /**
   * @description Prepare the source resources for the PDF.
   * @member TComponents.Pdf~_prepareResolvedSrc
   * @method
   * @private
   * @param {string|Uint8Array|ArrayBuffer|Blob} source
   * @returns {object|string}
   */
  _prepareResolvedSrc(source) {
    if (!source) return '';

    if (this._lastSourceToken === source && this._resolvedSrc) {
      return this._resolvedSrc;
    }

    this._revokeObjectUrl();

    let resolved = '';

    if (source instanceof Uint8Array) {
      resolved = this._createObjectUrl(source);
    } else if (source instanceof ArrayBuffer) {
      resolved = this._createObjectUrl(new Uint8Array(source));
    } else if (source instanceof Blob) {
      resolved = this._createObjectUrl(source);
    } else if (typeof source === 'string') {
      resolved = this._resolveStringSource(source);
    } else {
      Logger.w(logModule, ErrorCode.FailedToFindSourceImage, 'Unsupported PDF source type.');
    }

    this._lastSourceToken = source;
    this._resolvedSrc = resolved;

    return resolved;
  }

  /**
   * @description Create a URL object
   * @member TComponents.Pdf~_createObjectUrl
   * @method
   * @private
   * @param {Blob|Uint8Array} data
   * @returns {string}
   */
  _createObjectUrl(data) {
    const blob = data instanceof Blob ? data : new Blob([data], {type: 'application/pdf'});
    this._objectUrl = URL.createObjectURL(blob);
    return this._objectUrl;
  }

  /**
   * @description The source file for parsing the PDF is determined by the input string path.
   * @member TComponents.Pdf~_resolveStringSource
   * @method
   * @private
   * @param {string} raw
   * @returns {object|string}
   */
  _resolveStringSource(raw) {
    try {
      if (typeof window !== 'undefined') {
        const globalValue = window[raw];
        if (typeof globalValue === 'string') {
          return globalValue;
        }
      }
      return raw;
    } catch (error) {
      Logger.e(logModule, ErrorCode.FailedToFindSourceImage, 'Failed to resolve PDF string source.', error);
      return raw;
    }
  }

  /**
   * @description Clean up frame.
   * @member TComponents.Pdf~_cleanupFrame
   * @method
   * @private
   * @returns {void}
   */
  _cleanupFrame() {
    if (this._frame) {
      this._frame.removeAttribute('src');
    }
    this._revokeObjectUrl();
    this._lastSourceToken = null;
    this._resolvedSrc = null;
  }

  /**
   * @description Revoke object url
   * @member TComponents.Pdf~_revokeObjectUrl
   * @method
   * @private
   * @returns {void}
   */
  _revokeObjectUrl() {
    if (this._objectUrl) {
      URL.revokeObjectURL(this._objectUrl);
      this._objectUrl = null;
    }
  }

  /**
   * @description Handle errors.
   * @member TComponents.Pdf~_handleError
   * @method
   * @private
   * @returns {void}
   */
  _handleError(error) {
    Logger.e(logModule, ErrorCode.FailedToRunOnRender, 'PDF component rendering error.', error);
    Popup_A.danger(
      `${ExceptionIdMap.FailedToRunOnRender}-${Component_A.t(`framework:${ExceptionIdMap.FailedToRunOnRender}.title`, {
        name: 'Pdf',
      })}`,
      Component_A.t(`framework:${ExceptionIdMap.FailedToRunOnRender}.causes`),
    );
  }

  /**
   * @description Set errors
   * @member TComponents.Pdf~_setError
   * @method
   * @private
   * @returns {void}
   */
  _setError(message) {
    const errorEl = this.find('[data-role="error"]');
    if (!errorEl) return;
    if (message) {
      errorEl.textContent = message;
      errorEl.hidden = false;
    } else {
      errorEl.textContent = '';
      errorEl.hidden = true;
    }
  }

  /**
   * @description Set loading message.
   * @member TComponents.Pdf~_setLoading
   * @method
   * @private
   * @param {string} message
   * @returns {void}
   */
  _setLoading(message) {
    const loadingEl = this.find('[data-role="loading"]');
    if (!loadingEl) return;
    if (message) {
      loadingEl.textContent = message;
      loadingEl.hidden = false;
    } else {
      loadingEl.textContent = '';
      loadingEl.hidden = true;
    }
  }

  /**
   * Gets pdf source.
   * @returns {string|object}
   */
  get pdfSrc() {
    return this._props.pdfSrc;
  }

  /**
   * @description Set pdf source.
   * @member {string} TComponents.Pdf#pdfSrc
   * @instance
   * @param {string|Uint8Array} value
   * @example
   * const pdf = new TComponents.Pdf(document.body, {
   *   position: 'absolute',
   *   width: 200,
   *   height: 200,
   *   zIndex: 1000,
   *   pdfSrc: 'xxxx',
   * });
   *
   * // Render the component.
   * await pdf.render();
   *  // Set pdf source
   * pdf.pdfSrc = 'xxxx';
   */
  set pdfSrc(value) {
    this.setProps({pdfSrc: value});
  }

  /**
   * @description Returns the HTML markup for the tab component.
   * @member TComponents.Pdf#markup
   * @method
   * @returns {string} The HTML markup
   */
  markup() {
    return /*html*/ `
      <div class="tc-pdf">
        <div class="tc-pdf-viewer">
          <div class="tc-pdf-overlay" data-role="overlay"></div>
          <div class="tc-pdf-loading" data-role="loading" hidden></div>
          <iframe class="tc-pdf-frame" data-role="frame" title="PDF document" loading="lazy"></iframe>
          <div class="tc-pdf-error" data-role="error" hidden></div>
        </div>
      </div>
    `;
  }
}

/**
 * @description Add css properties to the component
 * @alias loadCssClassFromString
 * @member TComponents.Pdf.loadCssClassFromString
 * @method
 * @static
 * @param {string} css - The css string to be loaded into style tag
 * @returns {void}
 * @example
 * TComponents.Pdf.loadCssClassFromString(`
 *   .tc-pdf {
 *     background-color: #f0f0f0;
 *     border: 1px solid #ccc;
 *     border-radius: 4px;
 *   }
 * }`);
 */
Pdf.loadCssClassFromString(/*css*/ `
.tc-pdf {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
  box-sizing: border-box;
  border: 1px solid #dbdbdb;
}


.tc-pdf-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0);
  pointer-events: auto;
}

.tc-pdf-viewer {
  position: relative;
  flex: 1;
  overflow: hidden;
  display: flex;
  justify-content: stretch;
  align-items: stretch;
  background-color: #ffffff;
}

.tc-pdf-frame {
  border: none;
  width: 100%;
  height: 100%;
  background-color: #ffffff;
}

.tc-pdf-loading,
.tc-pdf-error {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 4px;
  font-size: 12px;
  color: rgba(0, 0, 0, 0.6);
  background-color: rgba(255, 255, 255, 0.9);
  padding: 0.5rem 0.75rem;
  z-index: 2;
  text-align: center;
}

.tc-pdf-disabled .tc-pdf-viewer {
  pointer-events: none;
  opacity: 0.6;
}
`);