UNPKG

gsheetcmslight

Version:

A library to read a Google Sheet with multilingal content

1,181 lines (1,093 loc) 37.5 kB
/*! gsheetcmslight v1.1.0 | (c) 2020 | GPL-3.0-or-later License | git+https://github.com/Puzzlout/GoogleSheetCmsLight.git */ var ConvertionUtility = function () { /** * The Seperator used for ParseValueAsArray method. */ this.SEPERATOR = ","; }; ConvertionUtility.prototype = { /** * Return string after checking it is a string, otherwise undefined. * @param {string} value The string to check before returning it. * @returns {string} */ CheckAndReturnString: function (value) { const isString = typeof value === "string"; if (isString) return value; return undefined; }, /** * Parse a string to an array of string using the seperator, otherwise undefined. * @param {string} value The string to parse * @returns {array} */ ParseValueAsArray: function (value) { const valueContainsSeperator = value.indexOf(this.SEPERATOR) !== -1; if (!valueContainsSeperator) { console.warn( `Value ${value} doesn't contain the seperator ${this.SEPERATOR}` ); return undefined; } const valueArray = value .split(this.SEPERATOR) .map((newValue) => newValue.trim()); return valueArray; }, /** * Parse a string to boolean, otherwise undefined. * @param {string} value The string to convert to a boolean. * @returns {bool} */ ParseValueAsBoolean: function (value) { const isTruthy = value.trim().toLowerCase() === "true"; const isFalsy = value.trim().toLowerCase() === "false"; if (!isTruthy && !isFalsy) { console.warn(`Value "${value}" is not true or false`); return undefined; } return isTruthy ? true : false; }, /** * Parse a string to an integer, otherwise undefined. * @param {string} value The value to parse * @returns {int} * @see https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/parseInt#Une_fonction_plus_stricte */ ParseValueAsInt: function (value) { if (/^(-|\+)?(\d+|Infinity)$/.test(value)) return Number(value); return undefined; }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = ConvertionUtility; } else if (typeof define === "function" && define.amd) { define((function () { return ConvertionUtility; })); } else { window.ConvertionUtility = ConvertionUtility; } var QueryStringUtility = function (options) { this.enableLog = false; if (options !== undefined && options.enableLog !== undefined) this.enableLog = op.enableLog; }; QueryStringUtility.prototype = { /** * Finds a value for the key inside the query string. * @param {string} key The key of the value to find * @returns {string} The value matching the key. */ GetValue: function (key) { const queryStringArray = this.GetKeyValuePairs(); const value = queryStringArray[key]; if (value === undefined && this.enableLog) console.warn(`Key "${key}" is not found in the query string`); return value; }, /** * Converts the query string array into a key/value pairs. * @returns {array} The key/value representation of the query string. */ GetKeyValuePairs: function () { const queryStringArray = this.GetArray(); let queryStringKeyValueArray = []; queryStringArray.forEach((parameter) => { var paramterArray = parameter.split("="); Object.defineProperty(queryStringKeyValueArray, paramterArray[0], { value: paramterArray[1], }); }); return queryStringKeyValueArray; }, /** * Converts the query string into an array after splitting it using the seperator "&" * @returns {array} The raw values in the query string */ GetArray: function () { const requestUrl = document.location.href; if (requestUrl.indexOf("?") === -1) return []; const queryString = requestUrl.substring( requestUrl.indexOf("?") + 1, requestUrl.length ); if (this.enableLog) console.log("Query is", queryString); return queryString.split("&"); }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = QueryStringUtility; } else if (typeof define === "function" && define.amd) { define((function () { return QueryStringUtility; })); } else { window.QueryStringUtility = QueryStringUtility; } var BrowserLanguageParser = function (options) { /** * Flag to enable console logs */ this.enableLog = false; if (options !== undefined) { this.enableLog = options.enableLog | false; } }; BrowserLanguageParser.prototype = { /** * Returns the default language since navigator.languages isn't supported. * @returns {string} The default language */ ReturnDefaultIfNavigatorLangsUnsupported: function () { if (this.enableLog) { console.log("navigator.languages not supported..."); } return undefined; }, /** * Returns the default language since navigator.languages is undefined or null. * @returns {string} The default language */ ReturnDefaultIfNavigatorLangsUndefinedOrNull: function () { if (this.enableLog) { console.log("navigator.languages is undefined or null..."); } return undefined; }, /** * Returns the default language since navigator.languages is empty. * @returns {string} The default language */ ReturnDefaultIfNavigatorLangsEmpty: function () { if (this.enableLog) { console.log("navigator.languages is empty..."); } return undefined; }, /** * Read first language from navigator.languages when available * to load the site in the prefered user language. * @returns {string} navigator.languages[0] | DEFAULT_LANG */ GetBrowserFirstLang: function () { if (!navigator.languages) { return this.ReturnDefaultIfNavigatorLangsUnsupported(); } if (navigator.languages === undefined || navigator.languages === null) { return this.ReturnDefaultIfNavigatorLangsUndefinedOrNull(); } if (navigator.languages.length === 0) { return this.ReturnDefaultIfNavigatorLangsEmpty(); } return navigator.languages[0]; }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = BrowserLanguageParser; } else if (typeof define === "function" && define.amd) { define((function () { return BrowserLanguageParser; })); } else { window.BrowserLanguageParser = BrowserLanguageParser; } var SheetValidator = function (options) { this.CheckI8n = true; if (options === undefined) return; if (options.checkI8n !== undefined) this.CheckI8n = options.checkI8n; if (options.sheet === undefined) throw new Error("Sheet is absent"); if (options.sheet === {}) throw new Error("Sheet is empty"); if (Object.keys(options.sheet).length === 0) throw new Error("Sheet has not properties"); if (options.sheet.columnNames === undefined) throw new Error("Sheet column names is not set"); if (options.sheet.columnNames.length === 0) throw new Error("Sheet has no columns"); /** * The sheet columns */ this.ColumnNames = options.sheet.columnNames; /** * The Configuration */ this.Config = options.config; /** * The default Value column name */ this.DEFAULT_COLUMN_NAME = "Value"; }; SheetValidator.prototype = { /** * Retrieve the first browser language * @returns {string} The language */ RetrieveBrowserLang: function () { const firstLang = new BrowserLanguageParser({ enableLog: this.enableLog, }).GetBrowserFirstLang(); return firstLang; }, /** * Retrieve the language in the GET parameter "lang" * @returns {string} The language */ RetrieveRequestLang: function () { const requestLang = new QueryStringUtility({ enableLog: this.enableLog, }).GetValue("lang"); return requestLang; }, /** * Decides which language that the app will used to retrieve the resource label. * If the Google Sheet configuration has one set, it is used. * Otherwise, the browser language is used. * @returns {string} The language */ GetUsedLanguage: function () { const browserLang = this.RetrieveBrowserLang(); const requestLang = this.RetrieveRequestLang(); const configDefaultLang = this.Config.DefaultLanguage; const noLanguageDefined = configDefaultLang === undefined && browserLang === undefined && requestLang === undefined; if (noLanguageDefined && this.enableLog) { console.warn("No language found in the Google Sheet or the Browser."); } //If request lang is read, return it if (requestLang !== undefined) { this.SetDocumentLang(requestLang); return requestLang; } //If browser lang is read, return it if (browserLang !== undefined) { this.SetDocumentLang(browserLang); return browserLang; } //If config lang is read, return it if (configDefaultLang !== undefined) { this.SetDocumentLang(configDefaultLang); return configLang; } //Otherwise, return undefined. return undefined; }, /** * Set the attribut lang in the HTML tag. * @param {string} language The language value */ SetDocumentLang: function (language) { const docLang = document.querySelector("html"); docLang.lang = language; }, /** * Builds the column name of the value's row to read from the browser language * @returns {string} */ BuildExpectedI8nColumnName: function () { const lang = this.GetUsedLanguage(); return `${this.DEFAULT_COLUMN_NAME}_${lang}`; }, /** * Builds the default column name of the value's row * @returns {string} */ GetDefaultColumnValueName: function () { return this.DEFAULT_COLUMN_NAME; }, /** * Finds the localized colunm name. * * @returns {string} The column name found or False */ GetI8nColumnExistsForUserLanguage: function () { const I8nColumnName = this.BuildExpectedI8nColumnName(); return this.GetColumnName(this.ColumnNames, I8nColumnName); }, GetValueColumnIdentity: function (sheet) { if (!this.CheckI8n) return this.DEFAULT_COLUMN_NAME; const i8nResult = this.GetI8nColumnExistsForUserLanguage(); if (i8nResult.isValid) return i8nResult.columnName; console.info("Falling back to default column..."); const defaultResult = this.GetColumnName( this.ColumnNames, this.DEFAULT_COLUMN_NAME, true ); if (defaultResult.isValid) return defaultResult.columnName; const errorMsg = new Error( `The sheet "${sheet.name}" must at least contain a Value column` ); throw new Error(errorMsg); }, /** * Search the column name matching the closest the input in a list. * * @param {array} avaibleColumns The columns available in a sheet * @param {string} columnName The column to match against the row keys (which * represent the colunm name of the sheet) * @param {bool} matchExactly Use an exact match or loose filtering. * @returns {string} The column matching the closest the input. Ex: if input * was 'Value_fr and 'Value_fr-FR' existed, then 'Value_fr-FR' is returned. * Otherwise, false is returned. */ GetColumnName: function (avaibleColumns, columnName, matchExactly = false) { if (this.enableLog) console.log("Row keys", avaibleColumns); var rowKeysFiltered = matchExactly ? this.FilterKeysExactly(columnName, avaibleColumns) : this.FilterKeysLoosely(columnName, avaibleColumns); if (this.enableLog) console.log("rowKeysFiltered", rowKeysFiltered); let columnExists = rowKeysFiltered.length > 0; if (!columnExists) { if (this.enableLog) console.log( `The sheet doesn't contain any column matching the column '${columnName}'` ); return { isValid: false }; } return { isValid: true, columnName: rowKeysFiltered[0] }; }, /** * Filter exactly matching the filter value. * * @param {string} filter The filter * @param {array} array The array of values to filter * @returns {array} The new array */ FilterKeysExactly: function (filter, array) { if (filter === undefined) { throw new Error("Parameter filter is required"); } if (typeof filter !== "string") { throw new Error("Parameter filter must be string"); } if (array === undefined) { throw new Error("Parameter array is required"); } if (!Array.isArray(array)) { throw new Error("Parameter array must be array"); } if (array.length === 0) { throw new Error("Parameter array must have values"); } let arrayFiltered = []; const regex = new RegExp("^" + filter.toLowerCase() + "$", "g"); array.reduce((accumulator, key) => { return key.toLowerCase().match(regex) ? arrayFiltered.push(key) : arrayFiltered; }); return arrayFiltered; }, /** * Filter loosely matching the filter value. * * @param {string} filter The filter * @param {array} array The array of values to filter * @returns {array} The new array */ FilterKeysLoosely: function (filter, array) { let arrayFiltered = []; array.reduce((accumulator, key) => { return key.toLowerCase().indexOf(filter.toLowerCase()) != -1 ? arrayFiltered.push(key) : arrayFiltered; }); return arrayFiltered; }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = SheetValidator; } else if (typeof define === "function" && define.amd) { define((function () { return SheetValidator; })); } else { window.SheetValidator = SheetValidator; } var DataTransformer = function (options) { if (options === undefined) throw new Error("options must be present"); if (options.config === undefined) throw new Error("options must contains a config object"); if (options.config.GoogleSheetsCmsVersion === undefined) throw new Error( "options.config must contain at least the version (GoogleSheetsCmsVersion)" ); if (options.config.DefaultLanguage === undefined) throw new Error( "options.config must contain the default language (DefaultLanguage)" ); /** * The Configuration */ this.Config = options.config; /** * Flag to enable console logs */ this.enableLog = options.enableLog | false; }; /** * Define the prototype of DataTransformer */ DataTransformer.prototype = { TransformSheetData: function (sheetData, dataType) { if (dataType === "ignore") return false; const valueColName = new SheetValidator({ checkI8n: true, sheet: sheetData, config: this.Config, }).GetValueColumnIdentity(sheetData); if (dataType === "array") return this.TransformDataToArray(sheetData, valueColName); if (dataType === "object") return this.TransformDataToObject(sheetData, valueColName); if (dataType === "nestedObject") return this.TransformDataToNestedObject(sheetData, valueColName); console.warn("Type " + dataType + " is not implemented at the moment. "); console.warn("Sheet '" + sheetData.name + "' will be ignored."); }, TransformDataToNestedObject: function (sheetData, valueColName) { const dataTransformer = this; var arrObjs = []; sheetData.elements.forEach((function (row) { var sectionName = row.Section; if (!arrObjs[sectionName]) { Object.defineProperty(arrObjs, sectionName, { value: {}, }); } Object.defineProperty(arrObjs[sectionName], row.Key, { value: dataTransformer.GetValueI8n(row, valueColName), }); })); return arrObjs; }, TransformDataToArray: function (sheetData, valueColName) { const dataTransformer = this; var labels = []; sheetData.elements.forEach((function (row) { labels.push({ key: row.Key, value: dataTransformer.GetValueI8n(row, valueColName), href: row.Href, order: row.Order, isActive: dataTransformer.GetTruthyValueFromStr(row.IsActive), openNewTab: dataTransformer.GetTruthyValueFromStr(row.OpenNewTab), }); })); return labels; }, TransformDataToObject: function (sheetData, valueColName) { var newObject = {}; const dataTransformer = this; sheetData.elements.forEach((function (row) { Object.defineProperty(newObject, row.Key, { value: dataTransformer.GetValueI8n(row, valueColName), }); })); return newObject; }, GetValueI8n: function (row, valueColName) { if (this.enableLog) console.log("Value column is:", valueColName); const value = row[valueColName]; if (this.enableLog) console.log("Value read:", value); return value; }, GetTruthyValueFromStr: function (booleanStr) { if (booleanStr === undefined) return false; if (booleanStr.toLowerCase() !== "true") return false; return true; }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = DataTransformer; } else if (typeof define === "function" && define.amd) { define((function () { return DataTransformer; })); } else { window.DataTransformer = DataTransformer; } var SheetConfigChecker = function (keyColName, valueColName, typeColName) { if (keyColName === "") throw new Error("Please provide the keyColName parameter"); if (valueColName === "") throw new Error("Please provide the valueColName parameter"); if (typeColName === "") throw new Error("Please provide the typeColName parameter"); this.KeyColName = keyColName; this.ValueColName = valueColName; this.TypeColName = typeColName; /** * Defines the name of the setting DefaultLanguage */ this.DefaultLanguageSetting = "DefaultLanguage"; /** * Defines the name of the setting SupportedLanguages */ this.SupportedLanguagesSetting = "SupportedLanguages"; /** * Defines the name of the setting UseLanguageMenu */ this.UseLanguageMenuSetting = "UseLanguageMenu"; }; SheetConfigChecker.prototype = { /** * Look up the length of the array. * @param {array} elements The tabletop elements representing the rows of the * Configuration sheet. * @returns {bool} */ SomeVariablesExist: function (elements) { if (elements === undefined) return false; return elements.length > 0; }, /** * Check the column name * @param {string} columnName The string value of the column to ckeck * @param {string} whichColumn indicate which column we want to check. */ CheckColumnFormat: function (columnName, whichColumn) { const undefinedMessage = `The ${whichColumn} colum must be named. It is undefined.`; const emptyMessage = `The ${whichColumn} colum must be named. It is empty.`; const badNameMessage = `The colum isn't named ${whichColumn}. It equals to "${columnName}".`; if (columnName === undefined) throw new Error(undefinedMessage); if (columnName.trim() === "") throw new Error(emptyMessage); switch (whichColumn) { case this.KeyColName: if (columnName !== this.KeyColName) throw new Error(badNameMessage); break; case this.ValueColName: if (columnName !== this.ValueColName) throw new Error(badNameMessage); break; case this.TypeColName: if (columnName !== this.TypeColName) throw new Error(badNameMessage); break; default: throw new Error("whichColumn parameter isn't right..."); } }, /** * Checks the setting is not empty * @param {string} settingPartValue The setting part value (Key, Value or Type) depending on whichColumn * @param {int} rowNumber The row in the sheet * @param {string} whichColumn indicate which column we want to check. */ CheckSettingPart: function (settingPartValue, rowNumber, whichColumn) { const emptyMessage = `The ${whichColumn} in row ${rowNumber} is empty.`; const settingIsEmpty = settingPartValue.trim() === ""; if (settingIsEmpty) { console.warn(emptyMessage); return false; } return true; }, /** * */ CheckIntegrity: function (config) { this.CheckSupportedLanguagesSetting(config); this.CheckUseLanguageMenuSetting(config); }, CheckUseLanguageMenuSetting: function (config) { if (config[this.UseLanguageMenuSetting]) { const settingExists = config[this.SupportedLanguagesSetting] !== undefined; if (!settingExists) throw new Error( "SupportedLanguages must be set if UseLanguageMenu is TRUE" ); } }, CheckSupportedLanguagesSetting: function (config) { if (config[this.SupportedLanguagesSetting]) { const configValue = config[this.UseLanguageMenuSetting]; const settingExists = configValue !== undefined; if (!settingExists) { console.warn("UseLanguageMenu must be set to use SupportedLanguages"); return; } if (!configValue) console.warn("UseLanguageMenu must be TRUE to use SupportedLanguages"); } }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = SheetConfigChecker; } else if (typeof define === "function" && define.amd) { define((function () { return SheetConfigChecker; })); } else { window.SheetConfigChecker = SheetConfigChecker; } var SheetConfigReader = function (options) { if (options === undefined) throw new Error("options must contains sheetUrl"); if (options.sourceData === undefined) throw new Error( "options must contains raw data of the configuration sheet" ); /** * The source data */ this.SourceData = options.sourceData; /** * Flag to enable console logs */ this.enableLog = options.enableLog | false; /** * Define the name of the Key column. * It must match in the Configuration sheet. */ this.COLUMN_KEY = "Key"; /** * Define the name of the Value column. * It must match in the Configuration sheet. */ this.COLUMN_VALUE = "Value"; /** * Define the name of the Type column. * It must match in the Configuration sheet. */ this.COLUMN_TYPE = "Type"; }; SheetConfigReader.prototype = { /** * Parse the data into an object. * @returns {object} */ GetConfig: function () { if (this.enableLog) console.log(this.SourceData); const configChecker = new SheetConfigChecker( this.COLUMN_KEY, this.COLUMN_VALUE, this.COLUMN_TYPE ); const KeyCol = this.SourceData.columnNames[0]; configChecker.CheckColumnFormat(KeyCol, this.COLUMN_KEY); const ValueCol = this.SourceData.columnNames[1]; configChecker.CheckColumnFormat(ValueCol, this.COLUMN_VALUE); const TypeCol = this.SourceData.columnNames[2]; configChecker.CheckColumnFormat(TypeCol, this.COLUMN_TYPE); if (!configChecker.SomeVariablesExist(this.SourceData.elements)) return {}; let config = {}; let rowNumber = 2; this.SourceData.elements.forEach((variableRaw) => { this.ParseVariable(config, variableRaw, rowNumber, configChecker); rowNumber += 1; }); if (this.enableLog) console.log("Config", config); configChecker.CheckIntegrity(config); return config; }, /** * Parse the variable into the config object. * @param {object} config The config object filled from the parsed data. * @param {object} settingRow A tabletop row element. * @param {int} currentRowNum The number of the row being read. It is start at 2 as the row 1 is the headers * @param {object} configChecker The instance performing checks on the config. */ ParseVariable: function (config, settingRow, currentRowNum, configChecker) { let keyStr = settingRow[this.COLUMN_KEY]; const keyIsOk = configChecker.CheckSettingPart( keyStr, currentRowNum, this.COLUMN_KEY ); let valueStr = settingRow[this.COLUMN_VALUE]; const valueIsOk = configChecker.CheckSettingPart( valueStr, currentRowNum, this.COLUMN_VALUE ); let typeStr = settingRow[this.COLUMN_TYPE]; const typeIsOk = configChecker.CheckSettingPart( typeStr, currentRowNum, this.COLUMN_TYPE ); const valueFinal = this.ParseValueAsType(valueStr, typeStr); if (!keyIsOk || !valueIsOk || !typeIsOk || valueFinal === undefined) return; Object.defineProperty(config, keyStr, { value: valueFinal, }); }, /** * Parse the value into the requested type. * @param {string} value The value as string * @param {string} type The type as a string * @returns {mixed} The value parsed into the type. */ ParseValueAsType: function (value, type) { const convertUtil = new ConvertionUtility(); switch (type) { case "string": return convertUtil.CheckAndReturnString(value); case "array": return convertUtil.ParseValueAsArray(value); case "boolean": return convertUtil.ParseValueAsBoolean(value); case "int": return convertUtil.ParseValueAsInt(value); default: console.warn(`The type ${type} is not implemented.`); return undefined; } }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = SheetConfigReader; } else if (typeof define === "function" && define.amd) { define((function () { return SheetConfigReader; })); } else { window.SheetConfigReader = SheetConfigReader; } var GoogleSheetReader = function (options) { if (options === "undefined") throw new Error("options must contains sheetUrl"); /** * The url of the Google Sheet to read */ this.sheetUrl = options.sheetUrl; /** * Flag to enable console logs */ this.enableLog = options.enableLog | false; /** * The Google Sheet configuration */ this.config = {}; /** * The sheet defining the other sheet types */ this.SHEET_DATATYPE = "Sheet_DataType"; /** * The sheet defining the other sheet types */ this.SHEET_CONFIGURATION = "Configuration"; }; GoogleSheetReader.prototype = { /** * Check that the source Google Sheets document contains a sheet named after SHEET_DATATYPE constant. * * @param {tabletop} tabletop instance of Table */ CheckSheetTypeExists: function (tabletop) { if (tabletop.models[this.SHEET_DATATYPE] === undefined) { const invalidGoogleSheetMsg = "Please create a sheet 'Sheet_DataType' to define how should be transformed each sheet data"; alert(invalidGoogleSheetMsg); throw new Error(invalidGoogleSheetMsg); } }, /** * Checks that the sheet is declared in the sheet declaring the DataType of the data * contained in the sheet requested. * * @param {string} sheetDataType The type of data contained in the sheet * @param {string} sheetName The sheet name */ CheckSheetType: function (sheetDataType, sheetName) { if (sheetDataType[sheetName] === undefined) { const sheetNotDeclaredInSheetDataType = "Please add " + sheetName + " in sheet 'Sheet_DataType' to define how it should be transformed from the Google Sheet document"; alert(sheetNotDeclaredInSheetDataType); throw new Error(sheetNotDeclaredInSheetDataType); } }, /** * Loads the configuration sheet data. * @param {tabletop} tabletop The instance of TableTop */ LoadConfig: function (tabletop) { this.config = new SheetConfigReader({ sourceData: tabletop.models[this.SHEET_CONFIGURATION], enableLog: this.enableLog, }).GetConfig(); }, /** * Load the Google sheet data in a promise using gsheet2json */ GetData: function () { self = this; return new Promise(function (resolve, reject) { var options = { key: self.sheetUrl, callback: onLoad, simpleSheet: true, }; function onLoad(data, tabletop) { // 'data' is the array for a simple spreadsheet // 'tabletop' is the whole object with sheets and more. // could resolve(tabletop) too. // probably should do an error check here and then: // if (err) {reject(err); } resolve(tabletop); return; } Tabletop.init(options); }); }, /** * Transform the data read with gsheet2json into the * desired structure to use in the application. * * @param {object} tabletop instance of TableTop * @returns {object} The output data to return the application */ TransformData: function (tabletop) { if (this.enableLog) console.log(tabletop); this.CheckSheetTypeExists(tabletop); //since forEach doesn't use arrow function, //"this" in the forEach is not Vue instance! //so create a copy of this (Vue instance) to use into the forEach. const gSheetReaderInstance = this; let viewModel = {}; this.LoadConfig(tabletop); tabletop.modelNames.forEach((function (sheetName) { gSheetReaderInstance.TransformCurrentSheetData( tabletop, sheetName, viewModel ); })); if (this.enableLog) console.log("Formatted data", viewModel); return viewModel; }, /** * * @param {object} tabletop The object representing the Google Sheet contents * @param {string} sheetName The current sheet name being processed * @param {object} viewModel The output data to return the application */ TransformCurrentSheetData: function (tabletop, sheetName, viewModel) { const dataTransformerInput = { config: this.config, enableLog: this.enableLog, }; var sheetDataType = this.GetSheetDataType(tabletop, dataTransformerInput); this.CheckSheetType(sheetDataType, sheetName); var sheet = tabletop.models[sheetName]; var transformedData = new DataTransformer( dataTransformerInput ).TransformSheetData(sheet, sheetDataType[sheetName]); this.UpdateViewModel(viewModel, sheetName, transformedData); }, /** * Reads the sheet SheetDataType in the Google Sheet. * @param {object} tabletop The object representing the Google Sheet contents * @param {object} request The options for the DataTransformer * @returns {object} The sheet SheetDataType data */ GetSheetDataType: function (tabletop, request) { var result = new DataTransformer(request).TransformDataToObject( tabletop.models[this.SHEET_DATATYPE], "Value" ); return result; }, /** * Update the view model with the data read from a sheet in the Google Sheet document. * @param {object} viewModel The output data to return the application * @param {string} sheetName The current sheet name being processed * @param {object} newData The current sheet data formatted */ UpdateViewModel: function (viewModel, sheetName, newData) { if (newData) { Object.defineProperty(viewModel, sheetName, { value: newData, }); } }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = GoogleSheetReader; } else if (typeof define === "function" && define.amd) { define((function () { return GoogleSheetReader; })); } else { window.GoogleSheetReader = GoogleSheetReader; } var DomHeadUpdater = function (options) { if (options === undefined) throw new Error("options must contains sheetUrl"); /** * Flag to active the feature to update the head meta tags */ this.enableFeature = false; if (options.enableFeature) this.enableFeature = options.enableFeature; /** * Flag to log logs and warning in the console. */ this.enableLog = false; if (options.enableLog !== undefined) this.enableLog = options.enableLog; /** * Flag to say if the data is present. */ this.dataPresent = false; if (options.data) this.dataPresent = true; /** * The data to use to fill the tags. */ this.data = options.data; this.ATTR_TEXT = "text"; this.ATTR_CONTENT = "content"; }; DomHeadUpdater.prototype = { /** * Update Twitter tags */ UpdateTwitterTags: function () { if (!this.dataPresent) return; this.UpdateHeadMetaTag( 'meta[name="twitter:card"]', this.ATTR_CONTENT, this.data.twitterCard, "twitter:card tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[name="twitter:site"]', this.ATTR_CONTENT, this.data.twitterSite, "twitter:site tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[name="twitter:creator"]', this.ATTR_CONTENT, this.data.twitterCreator, "twitter:creator tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[name="twitter:description"]', this.ATTR_CONTENT, this.data.twitterDescription, "twitter:description tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[name="twitter:image"]', this.ATTR_CONTENT, this.data.twitterImage, "twitter:image tag is missing in head element" ); }, /** * Update the Open Graph tags * To optimise the SEO using Open Graph protocol: https://ogp.me/ * */ UpdateOgTags: function () { if (!this.dataPresent) return; this.UpdateHeadMetaTag( 'meta[property="og:title"]', this.ATTR_CONTENT, this.data.ogTitle, "og:title tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[property="og:description"]', this.ATTR_CONTENT, this.data.ogDescription, "og:description tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[property="og:url"]', this.ATTR_CONTENT, this.data.ogUrl, "og:url tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[property="og:type"]', this.ATTR_CONTENT, this.data.ogType, "og:type tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[property="og:image"]', this.ATTR_CONTENT, this.data.ogImage, "og:image tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[property="og:image:alt"]', this.ATTR_CONTENT, this.data.ogImageAlt, "og:image:alt tag is missing in head element" ); }, UpdateGeneralTags: function () { if (!this.dataPresent) return; this.UpdateHeadMetaTag( "head title", "text", this.data.title, "title tag is missing in head element" ); this.UpdateHeadMetaTag( 'meta[name="description"]', "content", this.data.description, "description tag is missing in head element" ); }, /** * Update the value of the meta tag * @param {string} cssSelector The CSS selector for the tag to retrieve. * @param {string} metaTagValueProp The property name containing the value in the meta tag * @param {string} newValue The new value to set. * @param {string} errorMsg The message to log in error. */ UpdateHeadMetaTag: function ( cssSelector, metaTagValueProp, newValue, errorMsg ) { const title = document.querySelector(cssSelector); if (!title) { console.error(errorMsg); return; } if (title[metaTagValueProp] === undefined) throw new Error( `"${metaTagValueProp}" is not present from meta tag selected by "${cssSelector}""` ); if (newValue === undefined) console.error( `${cssSelector} won't be updated because no value is defined in the MetaData sheet.` ); if (newValue) title[metaTagValueProp] = newValue; }, /** * Run all methods to update tags in head element when some data is provided. */ Run: function () { if (!this.enableFeature) return; if (!this.dataPresent) return; this.UpdateGeneralTags(); this.UpdateTwitterTags(); this.UpdateOgTags(); }, }; if (typeof module !== "undefined" && module.exports) { //don't just use inNodeJS, we may be in Browserify module.exports = DomHeadUpdater; } else if (typeof define === "function" && define.amd) { define((function () { return DomHeadUpdater; })); } else { window.DomHeadUpdater = DomHeadUpdater; }