toolshed-8.x-1.x-dev/assets/toolshed.es6.js

assets/toolshed.es6.js
// Define the Toolshed object scope.
Drupal.Toolshed = {
  /**
   * Check if this value is a string or not. Helps to encapsulate a safe way to
   * test for string.
   *
   * @param {*} str
   *   The variable to check if this is a string.
   *
   * @return {bool}
   *   TRUE if the parameter is a string, FALSE otherwise.
   */
  isString(str) {
    return typeof str === 'string' || str instanceof String;
  },

  /**
   * Simple escaping of RegExp strings.
   *
   * Does not handle advanced regular expressions, but will take care of
   * most cases. Meant to be used when concatenating string to create a
   * regular expressions.
   *
   * @param  {string} str
   *  String to escape.
   *
   * @return {string}
   *  String with the regular expression special characters escaped.
   */
  escapeRegex(str) {
    return str.replace(/[\^$+*?[\]{}()\\]/g, '\\$&');
  },

  /**
   * Helper function to uppercase the first letter of a string.
   *
   * @param {string} str
   *  String to transform.
   *
   * @return {string}
   *  String which has the first letter uppercased.
   */
  ucFirst(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  },

  /**
   * Transform a string into camel case. It will remove spaces, underscores
   * and hyphens, and uppercase the letter directly following them.
   *
   * @param {string} str
   *  The string to try to transform into camel case.
   *
   * @return {string}
   *  The string transformed into camel case.
   */
  camelCase(str) {
    return str.replace(/(?:[ _-]+)([a-z])/g, (match, p1) => p1.toUpperCase());
  },

  /**
   * Transforms a string into Pascal case. This is basically the same as
   * camel case, except that it will upper case the first letter as well.
   *
   * @param {string} str
   *  The original string to transform into Pascal case.
   *
   * @return {string}
   *  The transformed string.
   */
  pascalCase(str) {
    return str.replace(/(?:^|[ _-]+)([a-z])/g, (match, p1) => p1.toUpperCase());
  },

  /**
   * Gets the current page Drupal URL (excluding the query or base path).
   *
   * @return {string}
   *  The current internal path for Drupal. This would be the path
   *  without the "base path", however, it can still be a path alias.
   */
  getCurrentPath() {
    if (!this.getCurrentPath.path) {
      this.getCurrentPath.path = null;

      if (drupalSettings.path.baseUrl) {
        const regex = new RegExp(`^${this.escapeRegex(drupalSettings.path.baseUrl)}`, 'i');
        this.getCurrentPath.path = window.location.pathname.replace(regex, '');
      }
      else {
        throw Error('Base path is unavailable. This usually occurs if getCurrentPath() is run before the DOM is loaded.');
      }
    }

    return this.getCurrentPath.path;
  },

  /**
   * Parse URL query paramters from a URL.
   *
   * @param {string} url
   *  Full URL including the query parameters starting with '?' and
   *  separated with '&' characters.
   *
   * @return {Object}
   *  JSON formatted object which has the property names as the query
   *  key, and the property value is the query value.
   */
  getUrlParams(url) {
    const params = {};
    const [uri] = (url || window.location.search).split('#', 2);
    const [, query = null] = uri.split('?', 2);

    if (query) {
      query.split('&').forEach((param) => {
        const matches = /^([^=]+)=(.*)$/.exec(param);

        if (matches) {
          params[decodeURIComponent(matches[1])] = decodeURIComponent(matches[2]);
        }
      });
    }

    return params;
  },

  /**
   * Build a URL based on a Drupal internal path. This function will test
   * for the availability of clean URL's and prefer them if available.
   * The URL components will be run through URL encoding.
   *
   * @param {string} rawUrl
   *  The URL to add the query parameters to. The URL can previously have
   *  query parameters already include. This will append additional params.
   * @param {Object|string} params
   *  An object containing parameters to use as the URL query. Object
   *  property keys are the query variable names, and the object property
   *  value is the value to use for the query.
   *
   * @return {string}
   *  The valid Drupal URL based on values passed.
   */
  buildUrl(rawUrl, params) {
    let url = rawUrl || '';

    // leave absolute URL's alone.
    if (!(/^([a-z]{2,5}:)?\/\//i).test(url)) {
      const baseUrl = (drupalSettings.path.baseUrl ? drupalSettings.path.baseUrl : '/');
      url = url.replace(/^[/,\s]+|<front>|([/,\s]+$)/g, '');
      url = `${baseUrl}${drupalSettings.path.pathPrefix}${url}`;
    }

    if (params) {
      const paramConcat = (acc, entry) => `${acc}&${encodeURIComponent(entry[0])}=${encodeURIComponent(entry[1])}`;
      const qry = this.isString(params) ? params : Object.entries(params).reduce(paramConcat, '').substring(1);

      if (qry.length) {
        const [base, fragment] = url.split('#', 2);
        url = base + (base.indexOf('?') === -1 ? '?' : '&') + qry + (fragment ? `#${fragment}` : '');
      }
    }

    return url;
  },

  /**
   * Create a lambda that can make requests GET to a specified URI.
   *
   * The resulting high order function will return a Promise encapsulating a
   * HTTP request to the URL with the parameters passed, and the XHR reference
   * to allow aborting the request or listening for events.
   *
   * This allows for aborting the request, which at the time of this writing
   * "fetch" does not support.
   *
   * @param {string} uri
   *   URI to create a request function for.
   * @param {object} opts
   *   Options for how the request should be sent and the expected data
   *   results should appear.
   *
   *   The following options are available:
   *    - method: The HTTP method to use when creating the request.
   *    - format: The expected response format (JSON, HTML, URL encoded, etc...)
   *    - encoding(optional): Encoding of content for POST and PUT requests.
   *
   * @return {function(query:object):Promise|function(content, query:object):Promise}
   *   A lambda that creates a request to specified URI with
   *   passed in URL query params.
   *
   *   If the request method is POST or PUT then the resulting lambda expects
   *   a content and query parameter that contains the request body and
   *   aditional URI query parameters respectively. GET and other methods are
   *   not expecting to send data, and therefore exclude the content parameter.
   *
   *   The lamba returns a Promise which can be used to process the
   *   results or errors.
   */
  createRequester(uri, opts = {}) {
    const ts = this;

    opts = {
      method: 'GET',
      format: 'json',
      encoding: 'urlencoded',
      ...opts,
    };

    // Only needed for legacy support because IE 11 and older does not handle
    // the JSON response type correctly and just returns a text response.
    const formatResponse = (resp) => (ts.isString(resp) && opts.format === 'json') ? JSON.parse(resp) : resp;

    if (opts.method === 'POST' || opts.method === 'PUT') {
      let bodyType;
      let bodyFormatter;

      // Default to the same data text encoding as the response format.
      switch (opts.encoding) {
        case 'json':
          bodyType = 'application/json';
          bodyFormatter = JSON.stringify;
          break;

        case 'html':
        case 'urlencoded':
        default:
          bodyType = 'application/x-www-form-urlencoded';
          bodyFormatter = (data) => Object.entries(data).reduce((acc, [key, val]) => `${acc}&${encodeURIComponent(key)}=${encodeURIComponent(val)}`, '').substring(1);
      }

      return (content, query) => {
        const xhr = new XMLHttpRequest();
        const promise = new Promise((resolve, reject) => {
          xhr.open(opts.method, ts.buildUrl(uri, query), true);
          xhr.responseType = opts.format;
          xhr.onreadystatechange = function onStateChange() {
            if (this.readyState === XMLHttpRequest.DONE) {
              if (this.status === 200) resolve(formatResponse(this.response));
              else reject(new Error(`${this.status}: ${this.statusText}`));
            }
          };

          // Unable to contact the server or no response.
          xhr.onerror = () => reject(new Error('Unable to connect'));
          xhr.onabort = () => reject(new Error('Cancelled'));
          xhr.ontimeout = () => reject(new Error('Timeout'));

          // Convert parameters into URL encoded values for returning.
          if (content instanceof FormData) {
            xhr.send(content);
          }
          else {
            xhr.setRequestHeader('Content-Type', bodyType);
            xhr.send(ts.isString(content) ? content : bodyFormatter(content));
          }
        });

        return { promise, xhr };
      };
    }

    // Basic GET or HEAD HTTP requests.
    return (query) => {
      const xhr = new XMLHttpRequest();
      const promise = new Promise((resolve, reject) => {
        xhr.open(opts.method, ts.buildUrl(uri, query), true);
        xhr.responseType = opts.format;

        xhr.onload = () => {
          if (xhr.status === 200) resolve(formatResponse(xhr.response));
          else reject(new Error(`${xhr.status}: ${xhr.statusText}`));
        };

        // Unable to contact the server or no response.
        xhr.onerror = () => reject(new Error('Unable to connect'));
        xhr.onabort = () => reject(new Error('Cancelled'));
        xhr.ontimeout = () => reject(new Error('Timeout'));
        xhr.send();
      });

      return { promise, xhr };
    };
  },

  /**
   * Send a Request to URI with provided parameters and return Promise.
   *
   * @param {string} uri
   *   URI to build the request for.
   * @param {array|null} params
   *   Parameters to include when making the request.
   * @param {object} opts
   *  Same set of options as for createRequester() function.
   *
   * @return {Promise}
   *   Promise wrapping the HTTP request.
   *
   * @see Drupal.Toolshed.createRequester()
   */
  sendRequest(uri, params, opts = { }) {
    const { promise } = this.createRequester(uri, opts)(params);
    return promise;
  },

  /**
   * Utility function used to find an object based on a string name.
   *
   * @param {string} name
   *  Fully qualified name of the object to fetch.
   *
   * @return {Object}
   *  the object matching the name, or NULL if it cannot be found.
   */
  getObject(name) {
    if (!(name && name.split)) return null;

    const fetchObj = (obj, items) => {
      const part = items.shift();

      if (obj[part]) return items.length ? fetchObj(obj[part], items) : obj[part];
      return null;
    };

    return fetchObj(window, name.split('.'));
  },

  /**
   * Apply a callback to DOM elements with a specified CSS class name.
   *
   * @param {Element} context
   *   The DOM Element which either has the class or should be searched within
   *   for elements with the class name.
   * @param {string} className
   *   The class name to search for.
   * @param {Function} callback
   *   The callback function to apply to all the matching elements. The
   *   callback should accept the DOM element as its single parameter.
   * @param {string} [once=null]
   *   If a non-null string then use this string to as a class name to
   *   indicate if this callback has been called previously on these elemnts.
   */
  walkByClass(context, className, callback, once = null) {
    const items = context.classList && context.classList.contains(className)
      ? [context] : context.getElementsByClassName(className);

    this._applyToElements(items, callback, once);
  },

  /**
   * Apply a callback to all DOM elements that match a selector query.
   *
   * @param {Element} context
   *   The DOM Element which either matches the selector or should be searched
   *   within for elements which match the selector.
   * @param {string} query
   *   The select query to use in order to locate elements to apply the
   *   callback function to.
   * @param {Function} callback
   *   The callback function to apply to all the matching elements. The
   *   callback should accept the DOM element as its single parameter.
   * @param {string} [once=null]
   *   If a non-null string then use this string to as a class name to
   *   indicate if this callback has been called previously on these elemnts.
   */
  walkBySelector(context, query, callback, once = null) {
    const items = context.matches && context.matches(query)
      ? [context] : context.querySelectorAll(query);

    this._applyToElements(items, callback, once);
  },

  /**
   * A browser compliant method for applying a function to an iterable object.
   *
   * NOTE: Though the underlying call to Array.foreach() method supports
   * additional parameters such as the index and the iterable object, this
   * method could be applying to either a NodeList or a HTMLCollection depending
   * on how we got here and it shouldn't be assumed the index is meaningful or
   * can be relied on, especially in cases where elements might get removed.
   *
   * @param {Iterable<Element>} elements
   *   An iterable list of HTML Elements to apply the callback to. If a value
   *   is assigned to once, the once class name is checked before applying
   *   the callback to the element.
   * @param {Function} callback
   *   A callback to apply to each of the DOM elements.
   * @param {[type]} [once=null]
   *   A class name to check for, and apply to the elements to ensure they
   *   do not get processed multiple times with the same "once" value.
   */
  _applyToElements(elements, callback, once = null) {
    if (once) {
      const origFunc = callback;

      callback = (item) => {
        if (!item.classList.contains(once)) {
          item.classList.add(once);
          return origFunc(item);
        }
      };
    }

    Array.prototype.forEach.call(elements, callback);
  },
};

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc