All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.primefaces-extensions.monacoeditor.01-monaco-helper.js Maven / Gradle / Ivy

There is a newer version: 14.0.7.1
Show newest version
// @ts-check

window.monacoModule = window.monacoModule || {};

window.monacoModule.helper = (function () {

  /**
   * Map between the language label and the worker that should be used.
   * @type {Record}
   */
  const workerMap = {
    css: "css.worker.js",
    handlebars: "html.worker.js",
    html: "html.worker.js",
    javascript: "ts.worker.js",
    json: "json.worker.js",
    less: "css.worker.js",
    razor: "html.worker.js",
    scss: "css.worker.js",
    typescript: "ts.worker.js",
  }

  /**
   * Polyfill for older browsers.
   * @type {typeof Object.assign}
   */
  const assign = typeof Object.assign === "function"
    ? Object.assign
    :
    /**
     * @param {object} object 
     * @param  {...any[]} moreObjects 
     * @returns {any} 
     */
    (object, ...moreObjects) => {
      /** @type {Record} */
      const result = {};
      for (const obj of [object, ...moreObjects]) {
        for (const key of Object.keys(obj)) {
          result[key] = (/** @type {Record}*/(obj))[key];
        }
      }
      return result;
    }

  /**
   * The `??` operator.
   * @template T
   * @template S
   * @param {T | undefined | null} lhs 
   * @param {S} rhs 
   * @returns {T | S}
   */
  function defaultIfNullish(lhs, rhs) {
    return lhs !== undefined && lhs !== null ? lhs : rhs;
  }

  /**
   * @param {unknown} x 
   * @returns {x is null | undefined}
   */
  function isNullOrUndefined(x) {
    return x === null || x === undefined;
  }

  /**
   * @template T
   * @param {T | null | undefined} x 
   * @returns {x is T}
   */
  function isNotNullOrUndefined(x) {
    return x !== null && x !== undefined;
  }
  /**
   * @param {string} moduleId 
   * @param {string} label 
   * @returns {string}
   */
  function getScriptName(moduleId, label) {
    return defaultIfNullish(workerMap[label], "editor.worker.js");
  }

  /**
   * Checks whether a string ends with a certain suffix.
   * @param {string} string 
   * @param {string} suffix 
   * @returns {boolean}
   */
  function endsWith(string, suffix) {
    var this_len = string.length;
    return string.substring(this_len - suffix.length, this_len) === suffix;
  }

  /**
   * @returns {string} Base URL to the Faces resource servlet.
   */
  function getFacesResourceUri() {
    const res = PrimeFaces.resources.getFacesResource("", "", "0");
    const idx = res.lastIndexOf(`.${PrimeFaces.resources.getResourceUrlExtension()}`);
    return idx >= 0 ? res.substring(0, idx) : res;
  }

  /**
   * @param {string} url URL from which to load the script.
   * @returns {Promise} The loaded JavaScript.
   */
  async function getScript(url) {
    const script = await jQuery.ajax({
      type: "GET",
      url: url,
      dataType: "text",
      cache: true,
      async: true
    });
    PrimeFaces.csp.eval(script);
  }

  /**
   * Loads the extender for this monaco editor. It may be either
   *
   * - an empty string or undefined, in which case no extender is loaded
   * - a JavaScript expression that evaluates to an extender object
   * - a path to a resource (eg. `extender.js.xhtml?ln=mylib`) that evaluates to an extender object
   *
   * @template {import("monaco-editor").editor.IEditor} TEditor
   * @template {import("monaco-editor").editor.IEditorOptions} TEditorOpts
   * @template {PrimeFaces.widget.ExtMonacoEditor.SyncMonacoBaseEditorContext} TContext
   * @template {PrimeFaces.widget.ExtMonacoEditor.ExtenderBaseEditor} TExtender
   * @param {TEditor | undefined} editor
   * @param {Partial>} options The extender string as
   * specified on the component.
   * @param {TContext | undefined} context
   * @returns {Partial}
   */
  function loadExtender(editor, options, context) {
    const extenderString = options.extender;

    // No extender given
    if (typeof extenderString === "undefined" || extenderString === "") {
      return {};
    }

    // Extender was evaluated already
    if (typeof extenderString === "object") {
      return /** @type {Partial} */(extenderString);
    }

    // Try creating an extender from the factory
    if (typeof extenderString === "function") {
      try {
        const extender = extenderString();
        if (typeof extender === "object") {
          return extender;
        }
      }
      catch (e) {
        console.warn("[MonacoEditor] Extender must be an expression that evaluates to MonacoExtender", e);
      }
    }

    // Could not create extender.

    console.warn("[MonacoEditor] Extender was specified, but could not be loaded: ", extenderString);
    return {};
  }

  /**
   * @param {Partial>} options
   * @returns {string}
   */
  function resolveLocaleUrl(options) {
    // Load language file, if requested.
    if (options.locale) {
      // Determine URI for loading the locale.
      return options.localeUrl
        ? options.localeUrl
        : getMonacoResource(`locale/${options.locale}.js`);
    }
    else {
      return "";
    }
  }

  /**
   * @param {Partial>} options
   * @returns {Promise<{forceLibReload: boolean, localeUrl: string}>}
   */
  async function loadLanguage(options) {
    // Load language file, if requested.
    const localeUrl = resolveLocaleUrl(options);
    const language = typeof MonacoEnvironment === "object" && typeof MonacoEnvironment.Locale === "object"
      ? MonacoEnvironment.Locale.language
      : undefined;

    if (localeUrl && language !== options.locale) {
      try {
        await getScript(localeUrl);
        return {
          forceLibReload: true,
          localeUrl: localeUrl,
        };
      }
      catch (e) {
        console.error(`[MonacoEditor] Could not fetch localization file ${localeUrl}. Falling back to default English locale.`);
        options.locale = "en";
        options.localeUrl = undefined;
        return {
          forceLibReload: language !== options.locale,
          localeUrl: ""
        };
      }
    }
    else {
      return {
        forceLibReload: language !== options.locale,
        localeUrl: ""
      };
    }
  }

  /**
   * @param {string} resource
   * @param {Record} queryParams
   * @returns {string}
   */
  function getMonacoResource(resource, queryParams = {}) {
    // WARNING: Do not use the variable name "url", this will be replaced via maven during the build process!!!
    const resolvedUrl = PrimeFaces.resources.getFacesResource(`/monacoeditor/${resource}`, "primefaces-extensions", PrimeFacesExt.VERSION);
    const params = [];
    for (const key of Object.keys(queryParams)) {
      const queryParam = queryParams[key];
      const values = Array.isArray(queryParam) ? queryParam : [queryParam];
      for (const value of values) {
        if (value !== undefined && (typeof value !== "string" || value.length > 0)) {
          params.push(`${key}=${encodeURIComponent(value)}`);
        }
      }
    }
    // faces resources URL already contains at least one URL parameter "ln"
    // so no ? needed, we can use &
    return params.length > 0 ? `${resolvedUrl}&${params.join("&")}` : resolvedUrl;
  }

  /**
   * @param {Partial>} options
   * @param {boolean} forceLibReload If true, loads the monaco editor again, even if it is loaded already. This is
   * necessary in case the language changes.
   * @returns {Promise} Whether the monaco library was (re)loaded.
   */
  async function loadEditorLib(options, forceLibReload) {
    if (!("monaco" in window) || forceLibReload) {
      if (!options.locale) {
        MonacoEnvironment = typeof MonacoEnvironment === "object" ? MonacoEnvironment : {};
        MonacoEnvironment.Locale = {
          language: "",
          data: {},
        };
      }
      const uriEditor = getMonacoResource("editor.js");
      await getScript(uriEditor);
      return true;
    }
    else {
      return false;
    }
  }

  /**
   * Finds the default dir name for the Monaco text model for a widget component by using the ID of the component.
   * @template {import("monaco-editor").editor.IEditor} TEditor
   * @template {import("monaco-editor").editor.IEditorOptions} TEditorOpts
   * @param {PrimeFaces.widget.ExtMonacoEditor.AsyncMonacoBaseEditorContext} context 
   * @returns {string}
   */
  function deriveDirForComponent(context) {
    const id = context.getWidgetId();
    return String(id).replace(/[^a-zA-Z0-9_-]/g, "/");
  }

  /**
   * Evaluate the `beforeCreate` callback of the extender. It is passed the current options and may
   *
   * - not return anything, in which case the passed options are used (which may have been modified inplace)
   * - return an options object, in which case these options are used
   * - return a promise, in which case the editor is initialized only once that promise resolves. The promise
   *   may resolve to a new options object to be used.
   *
   * @template {PrimeFaces.widget.ExtMonacoEditor.SyncMonacoDiffEditorContext} TContext
   * @template {PrimeFaces.widget.ExtMonacoEditor.ExtenderDiffEditor} TExtender
   * @param {TContext} context
   * @param {TExtender} extender
   * @param {string} originalValue
   * @param {string} modifiedValue
   * @param {boolean} wasLibLoaded
   * @returns {Promise>}
   */
  async function createDiffEditorInitData(context, extender, originalValue, modifiedValue, wasLibLoaded) {
    const opts = context.getWidgetOptions();

    /** @type {import("monaco-editor").editor.IStandaloneDiffEditorConstructionOptions} */
    const incomingEditorOptions = typeof opts.editorOptions === "object" ?
      opts.editorOptions :
      typeof opts.editorOptions === "string" ?
        JSON.parse(opts.editorOptions) :
        {};

    const originalModel = createModel(context, incomingEditorOptions, extender, originalValue, {
      basename: opts.originalBasename,
      directory: opts.originalDirectory,
      extension: opts.originalExtension,
      language: opts.originalLanguage || opts.language,
      scheme: opts.originalScheme,
      createDefaultDir: context => deriveDirForComponent(context) + "/original",
      createExtenderModel: (context, extender, options) => typeof extender.createOriginalModel === "function"
        ? extender.createOriginalModel(context, options)
        : undefined,
    });

    const modifiedModel = createModel(context, incomingEditorOptions, extender, modifiedValue, {
      basename: opts.basename,
      directory: opts.directory,
      extension: opts.extension,
      language: opts.language,
      scheme: opts.scheme,
      createDefaultDir: context => deriveDirForComponent(context) + "/modified",
      createExtenderModel: (context, extender, options) => typeof extender.createModel === "function"
        ? extender.createModel(context, options)
        : undefined
    });


    // XHTML attribute takes precedence over the editor options
    const originalDisabled = opts.originalDisabled !== undefined
      ? opts.originalDisabled
      : !defaultIfNullish(incomingEditorOptions.originalEditable, false);
    const originalReadonly = defaultIfNullish(opts.originalReadonly, false);

    /** @type {import("monaco-editor").editor.IStandaloneDiffEditorConstructionOptions} */
    const baseEditorOptions = {
      domReadOnly: opts.readonly || opts.disabled,
      readOnly: opts.readonly || opts.disabled,
      originalEditable: !originalDisabled && !originalReadonly,
    };

    let editorOptions = assign(baseEditorOptions, incomingEditorOptions);
    if (incomingEditorOptions.tabIndex) {
      editorOptions.tabIndex = typeof incomingEditorOptions.tabIndex === "number"
        ? incomingEditorOptions.tabIndex
        : parseInt(String(incomingEditorOptions.tabIndex));
    }

    // Allow extender to modify / update the options
    if (typeof extender.beforeCreate === "function") {
      const result = extender.beforeCreate(context, editorOptions, wasLibLoaded);
      if (typeof result === "object" && result !== null) {
        editorOptions = "then" in result ? defaultIfNullish(await result, editorOptions) : result;
      }
    }

    return {
      custom: {
        modifiedModel,
        originalModel,
      },
      options: editorOptions,
    };
  }

  /**
   * Evaluate the `beforeCreate` callback of the extender. It is passed the current options and may
   *
   * - not return anything, in which case the passed options are used (which may have been modified inplace)
   * - return an options object, in which case these options are used
   * - return a promise, in which case the editor is initialized only once that promise resolves. The promise
   *   may resolve to a new options object to be used.
   *
   * @template {PrimeFaces.widget.ExtMonacoEditor.SyncMonacoCodeEditorContext} TContext
   * @template {PrimeFaces.widget.ExtMonacoEditor.ExtenderCodeEditor} TExtender
   * @param {TContext} context
   * @param {TExtender} extender
   * @param {string} editorValue
   * @param {boolean} wasLibLoaded
   * @returns {Promise}
   */
  async function createEditorConstructionOptions(context, extender, editorValue, wasLibLoaded) {
    const opts = context.getWidgetOptions();
    /** @type {import("monaco-editor").editor.IStandaloneEditorConstructionOptions} */
    const incomingEditorOptions = typeof opts.editorOptions === "object" ?
      opts.editorOptions :
      typeof opts.editorOptions === "string" ?
        JSON.parse(opts.editorOptions) :
        {};

    const model = createModel(context, incomingEditorOptions, extender, editorValue, {
      basename: opts.basename,
      directory: opts.directory,
      extension: opts.extension,
      language: opts.language,
      scheme: opts.scheme,
      createDefaultDir: context => deriveDirForComponent(context),
      createExtenderModel: (context, extender, options) => typeof extender.createModel === "function"
        ? extender.createModel(context, options)
        : undefined,
    });
    /** @type {import("monaco-editor").editor.IStandaloneEditorConstructionOptions} */
    const baseEditorOptions = {
      domReadOnly: defaultIfNullish(opts.readonly, false) || defaultIfNullish(opts.disabled, false),
      model: model,
      readOnly: defaultIfNullish(opts.readonly, false) || defaultIfNullish(opts.disabled, false),
    };
    const editorOptions = assign(baseEditorOptions, incomingEditorOptions);
    if (incomingEditorOptions.tabIndex) {
      editorOptions.tabIndex = typeof incomingEditorOptions.tabIndex === "number"
        ? incomingEditorOptions.tabIndex
        : parseInt(String(incomingEditorOptions.tabIndex));
    }

    if (typeof extender.beforeCreate === "function") {
      const result = extender.beforeCreate(context, editorOptions, wasLibLoaded);
      if (typeof result === "object" && result !== null) {
        return "then" in result ? defaultIfNullish(await result, editorOptions) : result;
      }
    }

    return editorOptions;
  }

  /**
   * @template {import("monaco-editor").editor.IEditor} TEditor
   * @template {import("monaco-editor").editor.IEditorOptions} TEditorOpts
   * @template {PrimeFaces.widget.ExtMonacoEditor.SyncMonacoBaseEditorContext} TContext
   * @template {PrimeFaces.widget.ExtMonacoEditor.ExtenderBaseEditor} TExtender
   * @param {TContext} context
   * @param {TEditorOpts} editorOptions
   * @param {TExtender} extender
   * @param {string} value
   * @param {ModelConfig} modelConfig
   * @returns {import("monaco-editor").editor.ITextModel}
   */
  function createModel(context, editorOptions, extender, value, modelConfig) {
    const language = modelConfig.language || "plaintext";
    // Scheme, path, basename and extension
    /** @type {string} */
    let scheme;
    /** @type {string} */
    let dir;
    /** @type {string} */
    let basename;
    /** @type {string} */
    let extension;

    if (modelConfig.directory) {
      dir = modelConfig.directory;
    }
    else {
      dir = modelConfig.createDefaultDir(context, extender);
      dir = dir + "/" + language.replace(/[^a-zA-Z0-9_-]/g, "/");
    }
    if (modelConfig.basename) {
      basename = modelConfig.basename;
    }
    else {
      basename = "file";
    }
    if (modelConfig.extension) {
      extension = modelConfig.extension;
    }
    else {
      const langInfo = monaco.languages.getLanguages().filter(lang => lang.id === language)[0];
      extension = langInfo !== undefined && langInfo.extensions !== undefined && langInfo.extensions.length > 0
        ? langInfo.extensions[0]
        : "";
    }

    if (modelConfig.scheme) {
      scheme = modelConfig.scheme;
    }
    else {
      scheme = "inmemory";
    }

    // Build path and uri
    if (dir.length > 0 && dir[dir.length - 1] !== "/") {
      dir = dir + "/";
    }
    // Replace multiple consecutive slashes with a single slash
    dir = dir.replace(/\/\/+/g, "/");
    if (extension.length > 0 && extension[0] !== ".") {
      extension = "." + extension;
    }
    if (endsWith(basename, extension)) {
      extension = "";
    }

    const uri = monaco.Uri.from({
      scheme: scheme,
      path: dir + basename + extension
    });

    // Allow extender to fetch model

    const modelFromExtender = modelConfig.createExtenderModel(context, extender, { editorOptions, language, uri, value });
    if (typeof modelFromExtender === "object" && modelFromExtender !== null) {
      return modelFromExtender;
    }

    // Get or create model, and set previous value
    let model = monaco.editor.getModel(uri);
    if (!model) {
      model = monaco.editor.createModel(value, language, uri);
    }
    model.setValue(value);
    return model;
  }

  /**
   * @template {import("monaco-editor").editor.IEditor} TEditor
   * @template TRet
   * @template {any[]} TArgs
   * @param {TEditor} editor
   * @param {((editor: TEditor, ...args: TArgs) => TRet) | string} script
   * @param {TArgs} args
   * @param {(script: string) => void} evalFn 
   * @returns {Promise>}
   */
  async function invokeMonacoScript(editor, script, args, evalFn) {
    // Make sure we always call the editor asynchronously
    await Promise.resolve(undefined);
    MonacoEnvironment = typeof MonacoEnvironment === "object" ? MonacoEnvironment : {};
    const wrappedScript = `try{MonacoEnvironment._INVOKE.return=(${script}).apply(window,MonacoEnvironment._INVOKE.args)}catch(e){MonacoEnvironment._INVOKE.error=e}`;
    try {
      MonacoEnvironment._INVOKE = {
        args: [editor, ...args],
        error: undefined,
        return: undefined,
      }
      evalFn(wrappedScript);
      if (MonacoEnvironment._INVOKE.error) {
        throw MonacoEnvironment._INVOKE.error;
      }
      else {
        return await MonacoEnvironment._INVOKE.return;
      }
    }
    finally {
      delete MonacoEnvironment._INVOKE;
    }
  }

  /**
   * @param {string} script 
   * @param {string} [nonce] 
   */
  function globalEval(script, nonce) {
    const scriptNode = document.createElement("script");
    scriptNode.type = "text/javascript";
    scriptNode.textContent = script;
    if (nonce !== undefined && nonce.length > 0) {
      scriptNode.nonce = nonce;
    }
    try {
      document.body.appendChild(scriptNode);
    }
    finally {
      document.body.removeChild(scriptNode);
    }
  }

  /**
   * @template {import("monaco-editor").editor.IEditor} TEditor
   * @template {PrimeFaces.MatchingKeys unknown>} K
   * @param {K} method
   * @param {TEditor} editor
   * @param {PrimeFaces.widget.ExtMonacoEditor.ParametersIfFn} args
   * @returns {Promise>>}
   */
  async function invokeMonaco(editor, method, args) {
    // Make sure we always call the editor asynchronously
    await Promise.resolve(undefined);
    if (editor) {
      const fn = editor[method];
      if (typeof fn === "function") {
        // @ts-ignore
        return await fn.apply(editor, args);
      }
      else {
        throw new Error(`Method ${method} does not exist on the editor.`);
      }
    }
    else {
      throw new Error("Method called on non-existing monaco editor.");
    }
  }

  /**
   * Register all given custom themes with the editor.
   * @param {Record | undefined} customThemes
   */
  function defineCustomThemes(customThemes) {
    /** @type {Record} */
    const customThemesData = customThemes !== undefined ? customThemes : {};
    for (const themeName of Object.keys(customThemesData || {})) {
      const themeData = assign({}, DefaultThemeData, customThemesData[themeName]);
      if (themeData !== undefined) {
        monaco.editor.defineTheme(themeName, themeData);
      }
    }
  }

  /**
   * @param {unknown} value 
   * @returns {value is MonacoMessage}
   */
  function isMonacoMessage(value) {
    if (value === null || value === undefined) {
      return false;
    }
    if (typeof value !== "object") {
      return false;
    }
    const message = /** @type {Record} */(value);
    if (typeof message.instanceId !== "number") {
      return false;
    }
    if (typeof message.payload !== "object" || message.payload === null) {
      return false;
    }
    const payload = /** @type {Record} */(message.payload);
    if (typeof payload.kind !== "string") {
      return false;
    }
    if (!("data" in payload)) {
      return false;
    }
    return true;
  }

  /**
   * Splits the given string at the first occurrence of the given separator. Returns the substring before and after
   * the separator.
   * @param {string} str String to split.
   * @param {string} separator Separator at which to split.
   * @return {[string, string | undefined]} The string before the separator and the string after the separator. When
   * the string does not contain the separator, the first element is the entire string and the second element is
   * `undefined`.
   */
  function splitFirst(str, separator) {
    const index = str.indexOf(separator);
    return index !== -1 ? [str.slice(0, index), str.slice(index + 1)] : [str, undefined];
  }

  /**
   * @param {string} str 
   * @return {Record}
   */
  function parseSimpleQuery(str) {
    str = str.trim().replace(/^[?#&]/, '');
    if (str.length === 0) {
      return {};
    }
    /** @type {Record} */
    const ret = {};
    for (const param of str.split('&')) {
      const [key, val] = splitFirst(param.replace(/\+/g, ' '), "=");
      ret[key] = val !== undefined ? decodeURIComponent(val) : val;
    };
    return ret;
  }

  /**
   * Default options for the monaco editor widget configuration.
   * @type {PrimeFaces.widget.ExtMonacoBaseEditorCfgBase}
   */
  const BaseEditorDefaults = {
    availableEvents: [],
    autoResize: false,
    basename: "",
    customThemes: {},
    directory: "",
    disabled: false,
    editorOptions: {},
    extender: undefined,
    extension: "",
    language: "plaintext",
    scheme: "inmemory",
    tabIndex: undefined,
    readonly: false,
    locale: "",
    localeUrl: "",
  };

  /** @type {import("monaco-editor").editor.IStandaloneThemeData} */
  const DefaultThemeData = {
    base: "vs",
    colors: {},
    inherit: true,
    rules: [],
  };

  /** @type {PrimeFaces.widget.ExtMonacoDiffEditorCfgBase} */
  const DiffEditorDefaults = {
    originalDisabled: true,
    originalReadonly: false,
    originalRequired: false,
    originalLanguage: "",
    originalBasename: "",
    originalDirectory: "",
    originalExtension: "",
    originalScheme: "inmemory",
  };

  /** @type {PrimeFaces.widget.ExtMonacoDiffEditorFramedCfgBase} */
  const FramedDiffEditorDefaults = assign({}, BaseEditorDefaults, DiffEditorDefaults, {
    extender: "",
    iframeUrlParams: {},
  });

  /** @type {PrimeFaces.widget.ExtMonacoCodeEditorFramedCfgBase} */
  const FramedEditorDefaults = assign({}, BaseEditorDefaults, {
    extender: "",
    iframeUrlParams: {},
  });

  /** @type {PrimeFaces.widget.ExtMonacoDiffEditorInlineCfgBase} */
  const InlineDiffEditorDefaults = assign({}, BaseEditorDefaults, DiffEditorDefaults, {
    extender: () => ({}),
    overflowWidgetsDomNode: "",
  });

  /** @type {PrimeFaces.widget.ExtMonacoCodeEditorInlineCfgBase} */
  const InlineEditorDefaults = assign({}, BaseEditorDefaults, {
    extender: () => ({}),
    overflowWidgetsDomNode: "",
  });

  return {
    assign,
    createDiffEditorInitData,
    createEditorConstructionOptions,
    createModel,
    defineCustomThemes,
    endsWith,
    getFacesResourceUri,
    getMonacoResource,
    getScript,
    getScriptName,
    globalEval,
    invokeMonaco,
    invokeMonacoScript,
    isMonacoMessage,
    isNotNullOrUndefined,
    isNullOrUndefined,
    parseSimpleQuery,
    loadEditorLib,
    loadExtender,
    loadLanguage,
    resolveLocaleUrl,
    BaseEditorDefaults,
    DefaultThemeData,
    FramedDiffEditorDefaults,
    FramedEditorDefaults,
    InlineDiffEditorDefaults,
    InlineEditorDefaults,
  };
})();




© 2015 - 2024 Weber Informatics LLC | Privacy Policy