diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5950171 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +src +docs +postcss.config.js +tailwind.config.js +.editorconfig +.github diff --git a/README.md b/README.md index 49aba39..44d4f18 100644 --- a/README.md +++ b/README.md @@ -190,11 +190,11 @@ When you set this and the banner is dismissed, the UTC milliseconds are stored i If you fail to set the value properly, it won't dismiss and the banner will show by default. -### Testing +## Testing _to come_ please make a PR if you know how to do it on JS. -### Changelog +## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. @@ -202,9 +202,9 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re Please see [CONTRIBUTING](CONTRIBUTING.md) for details. -### Security +## Security -If you discover any security related issues, please email zsh.rce@gmail.com instead of using the issue tracker. +If you discover any security related issues, please email zsh.rce@gmail.com instead of using the issue tracker. You can also use the [SECURITY](SECURITY.md) doc. ## Credits diff --git a/dist/hBar.js b/dist/hBar.js index 8bfd25a..36544b6 100644 --- a/dist/hBar.js +++ b/dist/hBar.js @@ -403,7 +403,7 @@ module.exports = function (list, options) { var ___CSS_LOADER_API_IMPORT___ = __webpack_require__(3); exports = ___CSS_LOADER_API_IMPORT___(false); // Module -exports.push([module.i, ".hb-bg-white{background-color:#fff}.hb-bg-gray-400{background-color:#cbd5e0}.hb-bg-gray-900{background-color:#1a202c}.hb-bg-red-100{background-color:#fff5f5}.hb-bg-red-400{background-color:#fc8181}.hb-bg-orange-300{background-color:#fbd38d}.hb-bg-orange-800{background-color:#9c4221}.hb-bg-yellow-100{background-color:ivory}.hb-bg-yellow-300{background-color:#faf089}.hb-bg-green-100{background-color:#f0fff4}.hb-bg-green-600{background-color:#38a169}.hb-bg-teal-500{background-color:#38b2ac}.hb-bg-teal-900{background-color:#234e52}.hb-bg-blue-100{background-color:#ebf8ff}.hb-bg-blue-900{background-color:#2a4365}.hb-bg-indigo-100{background-color:#ebf4ff}.hb-bg-indigo-800{background-color:#434190}.hover\\:hb-bg-gray-800:hover{background-color:#2d3748}.focus\\:hb-bg-gray-800:focus{background-color:#2d3748}.hb-rounded-md{border-radius:.375rem}.hb-rounded-full{border-radius:9999px}.hb-cursor-pointer{cursor:pointer}.hb-flex{display:flex}.hb-inline-flex{display:inline-flex}.hb-items-center{align-items:center}.hb-justify-between{justify-content:space-between}.hb-font-semibold{font-weight:600}.hb-h-3{height:.75rem}.hb-h-4{height:1rem}.hb-leading-relaxed{line-height:1.625}.hb-mx-2{margin-left:.5rem;margin-right:.5rem}.hb-mx-5{margin-left:1.25rem;margin-right:1.25rem}.hb--mr-2{margin-right:-.5rem}.focus\\:hb-outline-none:focus{outline:0}.hb-p-1{padding:.25rem}.hb-px-1{padding-left:.25rem;padding-right:.25rem}.hb-py-2{padding-top:.5rem;padding-bottom:.5rem}.hb-px-2{padding-left:.5rem;padding-right:.5rem}.hb-text-gray-100{color:#f7fafc}.hb-text-gray-800{color:#2d3748}.hb-text-gray-900{color:#1a202c}.hb-text-red-900{color:#742a2a}.hb-text-orange-100{color:#fffaf0}.hb-text-orange-900{color:#7b341e}.hb-text-yellow-900{color:#744210}.hb-text-green-100{color:#f0fff4}.hb-text-green-900{color:#22543d}.hb-text-teal-100{color:#e6fffa}.hb-text-blue-100{color:#ebf8ff}.hb-text-blue-900{color:#2a4365}.hb-text-indigo-100{color:#ebf4ff}.hb-text-indigo-900{color:#3c366b}.hover\\:hb-text-white:hover{color:#fff}.hover\\:hb-text-gray-300:hover{color:#e2e8f0}.hover\\:hb-text-gray-600:hover{color:#718096}.hover\\:hb-text-red-100:hover{color:#fff5f5}.hover\\:hb-text-orange-700:hover{color:#c05621}.hover\\:hb-text-yellow-700:hover{color:#b7791f}.hover\\:hb-text-green-300:hover{color:#9ae6b4}.hover\\:hb-text-teal-300:hover{color:#81e6d9}.hover\\:hb-text-blue-300:hover{color:#90cdf4}.hover\\:hb-text-indigo-300:hover{color:#a3bffa}.hb-text-xs{font-size:.75rem}.hb-text-sm{font-size:.875rem}.hb-uppercase{text-transform:uppercase}.hover\\:hb-underline:hover{text-decoration:underline}.hb-tracking-wider{letter-spacing:.05em}.hb-w-3{width:.75rem}.hb-w-4{width:1rem}.hb-w-full{width:100%}a{color:inherit;text-decoration:inherit;background-color:transparent}*{box-sizing:border-box;border:0 solid #e2e8f0}.fade-in-top{-webkit-animation:fade-in-top .8s cubic-bezier(.39,.575,.565,1);animation:fade-in-top .8s cubic-bezier(.39,.575,.565,1)}@-webkit-keyframes fade-in-top{to{transform:translateY(0);opacity:1}}@keyframes fade-in-top{0%{transform:translateY(-50px);opacity:0}to{transform:translateY(0);opacity:1}}@media (min-width:640px){.sm\\:hb-flex-row{flex-direction:row}}@media (min-width:768px){.md\\:hb-flex-row{flex-direction:row}.md\\:hb-px-20{padding-left:5rem;padding-right:5rem}}", ""]); +exports.push([module.i, ".hb-bg-white{background-color:#fff}.hb-bg-gray-400{background-color:#cbd5e0}.hb-bg-gray-900{background-color:#1a202c}.hb-bg-red-100{background-color:#fff5f5}.hb-bg-red-400{background-color:#fc8181}.hb-bg-orange-300{background-color:#fbd38d}.hb-bg-orange-800{background-color:#9c4221}.hb-bg-yellow-100{background-color:ivory}.hb-bg-yellow-300{background-color:#faf089}.hb-bg-green-100{background-color:#f0fff4}.hb-bg-green-600{background-color:#38a169}.hb-bg-teal-500{background-color:#38b2ac}.hb-bg-teal-900{background-color:#234e52}.hb-bg-blue-100{background-color:#ebf8ff}.hb-bg-blue-900{background-color:#2a4365}.hb-bg-indigo-100{background-color:#ebf4ff}.hb-bg-indigo-800{background-color:#434190}.hover\\:hb-bg-gray-800:hover{background-color:#2d3748}.focus\\:hb-bg-gray-800:focus{background-color:#2d3748}.hb-rounded-md{border-radius:.375rem}.hb-rounded-full{border-radius:9999px}.hb-cursor-pointer{cursor:pointer}.hb-flex{display:flex}.hb-inline-flex{display:inline-flex}.hb-items-center{align-items:center}.hb-justify-between{justify-content:space-between}.hb-font-semibold{font-weight:600}.hb-h-3{height:.75rem}.hb-h-4{height:1rem}.hb-leading-relaxed{line-height:1.625}.hb-mx-2{margin-left:.5rem;margin-right:.5rem}.hb-mx-5{margin-left:1.25rem;margin-right:1.25rem}.hb--mr-2{margin-right:-.5rem}.focus\\:hb-outline-none:focus{outline:0}.hb-p-1{padding:.25rem}.hb-px-1{padding-left:.25rem;padding-right:.25rem}.hb-py-2{padding-top:.5rem;padding-bottom:.5rem}.hb-px-2{padding-left:.5rem;padding-right:.5rem}.hb-text-gray-100{color:#f7fafc}.hb-text-gray-800{color:#2d3748}.hb-text-gray-900{color:#1a202c}.hb-text-red-900{color:#742a2a}.hb-text-orange-100{color:#fffaf0}.hb-text-orange-900{color:#7b341e}.hb-text-yellow-900{color:#744210}.hb-text-green-100{color:#f0fff4}.hb-text-green-900{color:#22543d}.hb-text-teal-100{color:#e6fffa}.hb-text-blue-100{color:#ebf8ff}.hb-text-blue-900{color:#2a4365}.hb-text-indigo-100{color:#ebf4ff}.hb-text-indigo-900{color:#3c366b}.hover\\:hb-text-white:hover{color:#fff}.hover\\:hb-text-gray-300:hover{color:#e2e8f0}.hover\\:hb-text-gray-600:hover{color:#718096}.hover\\:hb-text-red-100:hover{color:#fff5f5}.hover\\:hb-text-orange-700:hover{color:#c05621}.hover\\:hb-text-yellow-700:hover{color:#b7791f}.hover\\:hb-text-green-300:hover{color:#9ae6b4}.hover\\:hb-text-teal-300:hover{color:#81e6d9}.hover\\:hb-text-blue-300:hover{color:#90cdf4}.hover\\:hb-text-indigo-300:hover{color:#a3bffa}.hb-text-xs{font-size:.75rem}.hb-text-sm{font-size:.875rem}.hb-uppercase{text-transform:uppercase}.hover\\:hb-underline:hover{text-decoration:underline}.hb-tracking-wider{letter-spacing:.05em}.hb-w-3{width:.75rem}.hb-w-4{width:1rem}.hb-w-full{width:100%}a{color:inherit;text-decoration:inherit;background-color:transparent}*{box-sizing:border-box;border:0 solid #e2e8f0}.fade-in-top{-webkit-animation:fade-in-top .8s cubic-bezier(.39,.575,.565,1);animation:fade-in-top .8s cubic-bezier(.39,.575,.565,1)}@-webkit-keyframes fade-in-top{to{transform:translateY(0);opacity:1}}@keyframes fade-in-top{0%{transform:translateY(-50px);opacity:0}to{transform:translateY(0);opacity:1}}@media (min-width:768px){.md\\:hb-flex-row{flex-direction:row}.md\\:hb-px-20{padding-left:5rem;padding-right:5rem}}", ""]); // Exports module.exports = exports; @@ -519,119 +519,7 @@ __webpack_require__.r(__webpack_exports__); // EXTERNAL MODULE: ./src/styles.css var styles = __webpack_require__(0); -// CONCATENATED MODULE: ./src/config/config.js -/** - * The default configuration for the package - * - * @var {object} config - * @var {string} config.url - * @var {string} config.ele - * @var {boolean} config.dismissible - * @var {Date} config.dismissFor - * @var {string} config.badge - * @var {string} config.theme - * @var {array} config.secondaryLinks - * @var {object} config.customStyles - * @var {function} config.onCompleted - * @var {function} config.parser - * @var {string} config.link - * @var {string} config.title - * @var {object} config.fetchOptions - */ -var config = { - url: '', - ele: 'h-bar', - dismissible: false, - dismissFor: null, - badge: 'New', - theme: "gray", - secondaryLinks: [], - customStyles: {}, - onCompleted: function onCompleted() {}, - parser: null, - link: null, - title: null, - fetchOptions: { - method: 'GET', - mode: 'cors', - // no-cors, *cors, same-origin - cache: 'no-cache', - // *default, no-cache, reload, force-cache, only-if-cached - headers: { - 'Accept': 'application/json' - }, - redirect: 'follow' // manual, *follow, error - - } -}; -// CONCATENATED MODULE: ./src/functions/normalise.js -/** - * Gets the normal format of the returned API data - * - * This is split up just incase it needs to be expanded feature wise - */ - -/** - * Converts the JSON result form the Wordpress wp-json api. - * - * @param {string} data.title.rendered - * @param {string} data.link - * @return {object} {title, link} - */ -function wpJsonParser(data) { - var _data$ = data[0], - rendered = _data$.title.rendered, - link = _data$.link; - - if (rendered == undefined) { - console.error("WP-json response doesn't have real values %o", data[0]); - } - - return { - title: rendered, - link: link - }; -} -/** - * Default normaliser parser assignment. - */ - - -var normaliseParser = wpJsonParser; -/** - * Sets the parser for the data normaliser. - * - * @param {Function} parser - */ - -function initNormalise(parser) { - // if the parser is a function set it - // if not just use the default one - normaliseParser = typeof parser == 'function' ? parser : wpJsonParser; -} -/** - * Uses the defined parser to normalise the data that comes out. - * - * @param {object} data Mixed incoming data - */ - -function normaliser(data) { - return new Promise(function (resolve, reject) { - if (data) { - try { - resolve(normaliseParser(data)); - } catch (error) { - reject(error); - } - } - - reject({ - "error": "No Data", - data: data - }); - }); -} -// CONCATENATED MODULE: ./src/config/styling.js +// CONCATENATED MODULE: ./src/banner/styling.js /** * The class styling configurations. * @@ -640,7 +528,7 @@ function normaliser(data) { * @var {object} styling */ var styling = { - wrapper: "hb-flex hb-w-full hd-flex-col md:hb-flex-row sm:hb-flex-row hb-text-sm hb-py-2 md:hb-px-20 hb-px-1 hb-items-center hb-justify-between", + wrapper: "hb-flex hb-w-full hd-flex-col md:hb-flex-row hb-text-sm hb-py-2 md:hb-px-20 hb-px-1 hb-items-center hb-justify-between", linkWrapper: "hb-flex hb-items-center", badge: "hb-px-2 hb-mx-2 hb-leading-relaxed hb-tracking-wider hb-uppercase hb-font-semibold hb-rounded-full hb-text-xs", postTitle: "hover:hb-underline hb-inline-flex hb-items-center", @@ -726,15 +614,105 @@ var themes = { dismiss: "hb-text-gray-800" } }; +// CONCATENATED MODULE: ./src/config/config.js +/** + * The default configuration for the package + * + * @var {object} config + * @var {object} config.fetchOptions + */ +var config = { + fetchOptions: { + method: 'GET', + mode: 'cors', + // no-cors, *cors, same-origin + cache: 'no-cache', + // *default, no-cache, reload, force-cache, only-if-cached + headers: { + 'Accept': 'application/json' + }, + redirect: 'follow' // manual, *follow, error + + } +}; +// CONCATENATED MODULE: ./src/functions/normalise.js +/** + * Gets the normal format of the returned API data + * + * This is split up just incase it needs to be expanded feature wise + */ + +/** + * Converts the JSON result form the Wordpress wp-json api. + * + * @param {string} data.title.rendered + * @param {string} data.link + * @return {object} {title, link} + */ +function wpJsonParser(data) { + var _data$ = data[0], + rendered = _data$.title.rendered, + link = _data$.link; + + if (rendered == undefined) { + console.error("WP-json response doesn't have real values %o", data[0]); + } + + return { + title: rendered, + link: link + }; +} +/** + * Default normaliser parser assignment. + */ + + +var normaliseParser = wpJsonParser; +/** + * Sets the parser for the data normaliser. + * + * @param {Function} parser + */ + +function initNormalise(parser) { + // if the parser is a function set it + // if not just use the default one + normaliseParser = typeof parser == 'function' ? parser : wpJsonParser; +} +/** + * Uses the defined parser to normalise the data that comes out. + * + * @param {object} data Mixed incoming data + */ + +function normaliser(data) { + return new Promise(function (resolve, reject) { + if (data) { + try { + resolve(normaliseParser(data)); + } catch (error) { + reject(error); + } + } + + reject({ + "error": "No Data", + data: data + }); + }); +} // CONCATENATED MODULE: ./src/functions/init.js /** + * Set all the configuration options for the hBar library * * @param {object} options + * @param {string} options.el The element id + * @param {string} options.template The template id * @param {string} options.url - * @param {string} options.ele The element id * @param {boolean} options.dismissible * @param {Date|boolean} options.dismissFor * @param {string} options.badge @@ -743,30 +721,43 @@ var themes = { * @param {object} options.customStyles * @param {function} options.parser * @param {function} options.onCompleted + * @param {function} options.onFailure * @param {string} options.link Manual override * @param {string} options.title Manual Override */ -function init() { +function init_init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - this.url = options.url; - this.ele = options.ele || 'h-bar'; // we will default to false for this - - this.dismissible = options.dismissible || false; - this.dismissFor = options.dismissFor || false; - this.config = config; - this.config.fetchOptions.headers = Object.assign(config.fetchOptions.headers, options.headers); - this.styling = Object.assign(styling, options.customStyles); - this.secondaryLinks = options.secondaryLinks; - - this.onCompleted = options.onCompleted || function () {}; - - this.badge = options.badge || 'New'; - this.postLink = options.link; - this.postTitle = options.title; - this.theme = options.theme; - initNormalise(options.parser); - return this; + var configuration = {}; + configuration.$el = options.el; + configuration.url = options.url; // if the user has dompurify installed. It can be optional + + configuration.DOMPurify = options.DOMPurify || null; + configuration.theme = themes[options.theme] || 'grey'; + configuration.badge = options.badge || 'New'; // we will default to false for configuration + + configuration.dismissible = options.dismissible || false; + configuration.dismissFor = options.dismissFor || false; + configuration.secondaryLinks = options.secondaryLinks || []; + /** + * These will be the fallbacks if something isn't found. + */ + + configuration.title = options.title || null; + configuration.link = options.link || null; + + configuration.onCompleted = options.onCompleted || function () {}; + + configuration.onFailure = options.onFailure || function () {}; + + if (typeof options.fetch == 'function') { + configuration.fetch = options.fetch; + } + + configuration.fetchOptions = config.fetchOptions; + configuration.fetchOptions.headers = Object.assign(config.fetchOptions.headers, options.headers); + initNormalise(options.parser || null); + return configuration; } // CONCATENATED MODULE: ./src/utils.js function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } @@ -781,7 +772,7 @@ function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToAr function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } -// Thanks @stimulus: +// Thanks @stimulus: and I got it from @alpinejs // https://github.com/stimulusjs/stimulus/blob/master/packages/%40stimulus/core/src/application.ts function domReady() { return new Promise(function (resolve) { @@ -857,212 +848,469 @@ function autoBind(self) { return self; } -// CONCATENATED MODULE: ./src/index.js -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } +/** + * Gets the data-* values that area related to the config of the template options. + * + * @param {HTMLElement} element + * @return {Object} + */ +function getElementOptions(element) { + return { + template: element.dataset.template, + html: element.getAttribute('has-html') == "" ? true : false, + dismissFor: element.dataset.dismissFor || null + }; +} /** - * h-bar announcement banner + * Determines if the banner has been dismissed. * - * @version 1.1.0 - * @author ReeceM + * @returns boolean */ +function isDismissed() { + if (localStorage) { + var dismissDate = localStorage.getItem('h-bar_dismiss_for'); + if (!dismissDate) { + return false; + } + dismissDate = dismissDate; + var ourDate = new Date().getTime(); + if (ourDate <= dismissDate) { + return true; + } + } -var hBar = { - /** - * h-bar version number - */ - version: "1.1.0", + return false; +} +// CONCATENATED MODULE: ./src/functions/renderer.js +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } + +var Renderer = /*#__PURE__*/function () { /** - * Initialise the hBar package + * Creates a new simple renderer for the templates * - * @inheritdoc - * @returns {hBar} + * @param {string} el the element ID that has the template data + * @param {object} data The key:value pair of the data to replace in the template + * @param {DOMPurify} DOMPurify the DOMPurify library */ - init: init, + function Renderer(el, DOMPurify) { + _classCallCheck(this, Renderer); - /** - * Fetch the data from the endpoint - */ - fetchData: function fetchData() { - var _this = this; + this.template = document.querySelector(el); + this.DOMPurify = DOMPurify; + } - if (this.isDismissed()) return; - fetch(this.url, this.config.fetchOptions).then(function (response) { - return response.json(); - }).then(function (json) { - if (_typeof(json) == "object") { - normaliser(json).then(function (_ref) { - var title = _ref.title, - link = _ref.link, - secondaryLinks = _ref.secondaryLinks; - _this.postTitle = title; - _this.postLink = link; - _this.secondaryLinks = secondaryLinks || []; - - _this.render(); - })["catch"](function (error) { - console.error(error); - - _this.destroy(); - }); - } else { - console.error("".concat(_this.url, " Did not return an object")); + _createClass(Renderer, [{ + key: "resolve", + value: function resolve(data) { + var _this = this; + + var templateHTML = this.template.innerHTML; + /** + * Don't do anything if there is a no content + */ + + if (templateHTML == undefined) { + return null; + } + + Object.keys(data).forEach(function (key) { + // skip any array things. + // make the thing recursive in x version xD + if (!Array.isArray(data[key])) { + var regex = _this.regex(key); + + templateHTML = templateHTML.replace(regex, data[key]); + } + }); + + if (this.DOMPurify) { + return this.DOMPurify.sanitize(templateHTML); } - }); - }, + return templateHTML; + } + /** + * Create the matching regex for the template tags + * + * @param {string} key The key to search in the template data + * @returns {RegExp} + */ + + }, { + key: "regex", + value: function regex(key) { + // current tag is {% value %} + return new RegExp("({%\\s*(".concat(key, ")\\s*%})"), 'g'); + } + }]); + + return Renderer; +}(); + + +// CONCATENATED MODULE: ./src/banner/banner.js +function banner_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function banner_defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function banner_createClass(Constructor, protoProps, staticProps) { if (protoProps) banner_defineProperties(Constructor.prototype, protoProps); if (staticProps) banner_defineProperties(Constructor, staticProps); return Constructor; } + + + + +var banner_Banner = /*#__PURE__*/function () { + /** + * + * @param {object} param0 + */ + function Banner(_ref) { + var $el = _ref.$el, + dismissible = _ref.dismissible, + dismissFor = _ref.dismissFor, + theme = _ref.theme, + badge = _ref.badge; + + banner_classCallCheck(this, Banner); + + this.$el = $el; + this.dismissible = dismissible; + this.dismissFor = dismissFor; + this.badge = badge; + this.theme = theme; + } /** * Render the element. */ - render: function render() { - var _this2 = this; - if (this.isDismissed()) return; - domReady().then(function () { - if (!_this2.postTitle) { - console.error('[h-bar] no post data, unable to render'); - return; - } - var secondaryElement = null; + banner_createClass(Banner, [{ + key: "resolve", + value: function resolve(_ref2) { + var _this = this; + + var title = _ref2.title, + link = _ref2.link, + secondaryLinks = _ref2.secondaryLinks; + if (isDismissed()) return; + domReady().then(function () { + if (!title) { + console.error('[h-bar] no post data, unable to render'); + return; + } + + var secondaryElement = null; + + if (!_this.dismissible) { + var secondaryLinkList = _this.createSecondaryLinks(secondaryLinks); - if (!_this2.dismissible) { - var secondaryLinkList = _this2.createSecondaryLinks(); + secondaryElement = newElement('div', { + children: secondaryLinkList, + classes: "".concat(styling.linkWrapper, " ").concat(_this.theme.linkWrapper) + }); + } else { + secondaryElement = _this.dismissibleButton(); + } - secondaryElement = newElement('div', { - children: secondaryLinkList, - classes: "".concat(_this2.styling.linkWrapper, " ").concat(themes[_this2.theme].linkWrapper) + var badgeElement = newElement('span', { + classes: "".concat(styling.badge, " ").concat(_this.theme.badge) + }); + var postLink = newElement('a', { + classes: "".concat(styling.postTitle, " ").concat(_this.theme.postTitle) + }); + badgeElement.innerText = _this.badge; + postLink.href = link; + postLink.innerText = title; + postLink.innerHTML += "\n \n \n \n "; + var postElement = newElement('div', { + classes: "".concat(styling.linkWrapper, " ").concat(_this.theme.linkWrapper), + children: [badgeElement, postLink] }); - } else { - secondaryElement = _this2.dismissibleButton(); - } - var badge = newElement('span', { - classes: "".concat(_this2.styling.badge, " ").concat(themes[_this2.theme].badge) - }); - var postLink = newElement('a', { - classes: "".concat(_this2.styling.postTitle, " ").concat(themes[_this2.theme].postTitle) - }); - badge.innerText = _this2.badge; - postLink.href = _this2.postLink; - postLink.innerText = _this2.postTitle; - postLink.innerHTML += "\n \n \n \n "; - var postElement = newElement('div', { - classes: "".concat(_this2.styling.linkWrapper, " ").concat(themes[_this2.theme].linkWrapper), - children: [badge, postLink] + var _hbar = newElement('div', { + classes: "".concat(styling.wrapper, " ").concat(_this.theme.wrapper), + children: [postElement, secondaryElement] + }); + + var container = document.querySelector(_this.$el); + container.innerHTML = ""; + container.appendChild(_hbar); }); + } + /** + * Removes the element in the case of it having issues. + * Rather an aggressive option. + * + * Also used when dismissing. + */ + + }, { + key: "destroy", + value: function destroy() { + try { + document.querySelector(this.$el).innerHTML = ''; + return true; + } catch (error) { + console.error('Unable to destroy the h-bar wrapper'); + console.error(error); + } - var _hbar = newElement('div', { - classes: "".concat(_this2.styling.wrapper, " ").concat(themes[_this2.theme].wrapper), - children: [postElement, secondaryElement] + return false; + } + /** + * Creates the HTML node for a dismissible button. + * + * @returns HTMLElement + */ + + }, { + key: "dismissibleButton", + value: function dismissibleButton() { + var _this2 = this; + + var dismiss = newElement('button', { + classes: "hb--mr-2 hb-flex hb-p-1 hb-rounded-md ".concat(this.theme.dismiss, " hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800") }); + dismiss.innerHTML = "\n \n "; + + dismiss.onclick = function (e) { + e.preventDefault(); // just do it early if we not logging time. + + if (!_this2.dismissFor) return _this2.destroy(); + + if (localStorage) { + localStorage.setItem('h-bar_dismiss_for', _this2.dismissFor.getTime()); + } + + return _this2.destroy(); + }; + + return dismiss; + } + /** + * Creates the secondary links for the bar. + */ + + }, { + key: "createSecondaryLinks", + value: function createSecondaryLinks(secondaryLinks) { + var _this3 = this; + + if (!secondaryLinks) return []; + return secondaryLinks.map(function (_ref3) { + var title = _ref3.title, + link = _ref3.link; + var style = "".concat(styling.secondaryLink, " ").concat(_this3.theme.secondaryLink); + var butter = newElement('a', { + classes: style + }); + butter.href = link; + butter.innerText = title; + return butter; + }, this); + } + }]); + + return Banner; +}(); + + +// CONCATENATED MODULE: ./src/index.js +function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +/** + * h-bar banner and dynamic announcement library + * + * @version 2.0.0 + * @license MIT + * @copyright @ReeceM + */ + + - var container = document.getElementById(_this2.ele); - container.innerHTML = ""; - container.appendChild(_hbar); // ? what to send out - _this2.onCompleted({ - element: container, - id: _this2.ele - }); - }); - }, + + +/** + * Set all the configuration options for the hBar library + * + * @property {string} el The element id + * @property {string} url + * @property {boolean} dismissible + * @property {Date|boolean} dismissFor + * @property {string} badge + * @property {DOMPurify} DOMPurify the DOMPurify library + * @property {array} secondaryLinks + * @property {object} headers + * @property {object} customStyles + * @property {function} parser + * @property {object} renderer + * @property {function} onCompleted + * @property {function} onFailure + * @property {string} link Manual override + * @property {string} title Manual Override + */ + +var hBar = { + version: "2.0.0", + rendered: false, + fetching: false, + usingBanner: true, /** - * Removes the element in the case of it having issues. - * Rather an aggressive option. + * Set all the configuration options for the hBar library * - * Also used when dismissing. + * @param {object} options + * @param {string} options.el The element id + * @param {string} options.url + * @param {boolean} options.dismissible + * @param {Date|boolean} options.dismissFor + * @param {string} options.badge + * @param {array} options.secondaryLinks + * @param {object} options.headers + * @param {object} options.customStyles + * @param {function} options.parser + * @param {function} options.onCompleted + * @param {string} options.link Manual override + * @param {string} options.title Manual Override */ - destroy: function destroy() { - try { - document.getElementById(this.ele).remove(); - return true; - } catch (error) { - console.error('Unable to destroy the h-bar wrapper'); - console.error(error); + init: function init() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + Object.assign(this, init_init(options)); + this.$elementOpt = getElementOptions(document.querySelector(this.$el)); + + if (this.$elementOpt.template) { + this.renderer = new Renderer(this.$elementOpt.template, this.DOMPurify); + } else if (options.renderer) { + /** + * @todo this was added on a whim... bad idea possibly + */ + this.renderer = new options.renderer(this); + } else { + this.renderer = new banner_Banner(this); } - return false; + Object.defineProperties(this, { + 'renderer': { + configurable: false, + writable: false + } + }); }, /** - * Creates the HTML node for a dismissible button. + * Gets the data from the url endpoint. * - * @returns HTMLElement + * This is called by the */ - dismissibleButton: function dismissibleButton() { - var _this3 = this; - - var dismiss = newElement('button', { - classes: 'hb--mr-2 hb-flex hb-p-1 hb-rounded-md hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800' - }); - dismiss.innerHTML = "\n \n "); + fetch: function (_fetch) { + function fetch() { + return _fetch.apply(this, arguments); + } - dismiss.onclick = function (e) { - e.preventDefault(); // just do it early if we not logging time. + fetch.toString = function () { + return _fetch.toString(); + }; - if (!_this3.dismissFor) return _this3.destroy(); + return fetch; + }(function () { + var _this = this; - if (localStorage) { - localStorage.setItem('h-bar_dismiss_for', _this3.dismissFor.getTime()); + if (this.rendered) return; + if (isDismissed()) return; + this.fetching = true; + fetch(this.url, this.fetchOptions).then(function (response) { + return response.json(); + }).then(function (json) { + if (_typeof(json) == "object") { + _this.render(json); + } else { + console.error("".concat(_this.url, " Did not return an object")); } - return _this3.destroy(); - }; - - return dismiss; - }, + _this.fetching = false; + })["catch"](function (error) { + console.error(error); + _this.fetching = false; + _this.rendered = false; + }); + }), /** - * Determines if the banner has been dismissed. + * Render the response to the actual message * - * @returns boolean + * @param {Object} result */ - isDismissed: function isDismissed() { - if (localStorage) { - var dismissDate = localStorage.getItem('h-bar_dismiss_for'); + render: function render(result) { + var _this2 = this; - if (!dismissDate) { - return false; - } + normaliser(result).then(function (result) { + var element = document.querySelector(_this2.$el); + element.innerHTML = _this2.renderer.resolve(result); + element.__hbar__ = _this2; + _this2.rendered = true; - dismissDate = dismissDate; - var ourDate = new Date().getTime(); + _this2.onCompleted({ + __hbar__: _this2, + result: element + }); + })["catch"](function (error) { + console.error(error); - if (ourDate <= dismissDate) { - return true; - } - } + _this2.destroy(); - return false; + _this2.fetching = false; + _this2.rendered = false; + + _this2.onFailure({ + __hbar__: _this2 + }); + }); }, /** - * Creates the secondary links for the bar. + * Removes the element in the case of it having issues. + * Rather an aggressive option. + * + * Also used when dismissing. */ - createSecondaryLinks: function createSecondaryLinks() { - var _this4 = this; + destroy: function destroy() { + try { + document.querySelector(this.$el).innerHTML = ''; + return true; + } catch (error) { + console.error('Unable to destroy the h-bar wrapper'); + console.error(error); + } - if (!this.secondaryLinks) return []; - return this.secondaryLinks.map(function (_ref2) { - var title = _ref2.title, - link = _ref2.link; - var style = "".concat(_this4.styling.secondaryLink, " ").concat(themes[_this4.theme].secondaryLink); - var butter = newElement('a', { - classes: style - }); - butter.href = link; - butter.innerText = title; - return butter; - }, this); + return false; } }; +Object.defineProperties(hBar, { + /** + * Config method should not be changed + */ + 'init': { + writable: false, + configurable: false + }, + 'destroy': { + writable: false, + configurable: false + }, + 'fetch': { + writable: false, + configurable: false + } +}); /* harmony default export */ var src = __webpack_exports__["default"] = (hBar); /***/ }) diff --git a/dist/hBar.min.js b/dist/hBar.min.js index 9674799..74df915 100644 --- a/dist/hBar.min.js +++ b/dist/hBar.min.js @@ -1 +1,8 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.hBar=t():e.hBar=t()}(window,function(){return n={},o.m=r=[function(e,t,r){var n=r(1),o=r(2);"string"==typeof(o=o.__esModule?o.default:o)&&(o=[[e.i,o,""]]);var i={insert:"head",singleton:!1},a=(n(o,i),o.locals?o.locals:{});e.exports=a},function(e,t,i){"use strict";var r,n,o=function(){return void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r},a=(n={},function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}n[e]=t}return n[e]}),b=[];function d(e){for(var t=-1,r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r\n \n \n ';var n=h("div",{classes:"".concat(a.styling.linkWrapper," ").concat(s[a.theme].linkWrapper),children:[t,r]}),o=h("div",{classes:"".concat(a.styling.wrapper," ").concat(s[a.theme].wrapper),children:[n,e]}),i=document.getElementById(a.ele);i.innerHTML="",i.appendChild(o),a.onCompleted({element:i,id:a.ele})}else console.error("[h-bar] no post data, unable to render")})},destroy:function(){try{return document.getElementById(this.ele).remove(),!0}catch(e){console.error("Unable to destroy the h-bar wrapper"),console.error(e)}return!1},dismissibleButton:function(){var t=this,e=h("button",{classes:"hb--mr-2 hb-flex hb-p-1 hb-rounded-md hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800"});return e.innerHTML='\n \n '),e.onclick=function(e){return e.preventDefault(),t.dismissFor&&localStorage&&localStorage.setItem("h-bar_dismiss_for",t.dismissFor.getTime()),t.destroy()},e},isDismissed:function(){if(localStorage){var e=localStorage.getItem("h-bar_dismiss_for");if(!e)return!1;if(e=e,(new Date).getTime()<=e)return!0}return!1},createSecondaryLinks:function(){var o=this;return this.secondaryLinks?this.secondaryLinks.map(function(e){var t=e.title,r=e.link,n=h("a",{classes:"".concat(o.styling.secondaryLink," ").concat(s[o.theme].secondaryLink)});return n.href=r,n.innerText=t,n},this):[]}};t.default=d}],o.c=n,o.d=function(e,t,r){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(o.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)o.d(r,n,function(e){return t[e]}.bind(null,n));return r},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=4).default;function o(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return r[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}var r,n}); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.hBar=t():e.hBar=t()}(window,function(){return n={},o.m=r=[function(e,t,r){var n=r(1),o=r(2);"string"==typeof(o=o.__esModule?o.default:o)&&(o=[[e.i,o,""]]);var i={insert:"head",singleton:!1},a=(n(o,i),o.locals?o.locals:{});e.exports=a},function(e,t,i){"use strict";var r,n,o=function(){return void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r},a=(n={},function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}n[e]=t}return n[e]}),b=[];function d(e){for(var t=-1,r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r\n \n \n ';var n=g("div",{classes:"".concat(b," ").concat(a.theme.linkWrapper),children:[t,r]}),o=g("div",{classes:"".concat(h," ").concat(a.theme.wrapper),children:[n,e]}),i=document.querySelector(a.$el);i.innerHTML="",i.appendChild(o)}else console.error("[h-bar] no post data, unable to render")})}},{key:"destroy",value:function(){try{return!(document.querySelector(this.$el).innerHTML="")}catch(e){console.error("Unable to destroy the h-bar wrapper"),console.error(e)}return!1}},{key:"dismissibleButton",value:function(){var t=this,e=g("button",{classes:"hb--mr-2 hb-flex hb-p-1 hb-rounded-md ".concat(this.theme.dismiss," hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800")});return e.innerHTML='\n \n ',e.onclick=function(e){return e.preventDefault(),t.dismissFor&&localStorage&&localStorage.setItem("h-bar_dismiss_for",t.dismissFor.getTime()),t.destroy()},e}},{key:"createSecondaryLinks",value:function(e){var o=this;return e?e.map(function(e){var t=e.title,r=e.link,n=g("a",{classes:"".concat(i," ").concat(o.theme.secondaryLink)});return n.href=r,n.innerText=t,n},this):[]}}])&&v(e.prototype,t),r&&v(e,r),a}();function w(e){return(w="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)} +/** + * h-bar banner and dynamic announcement library + * + * @version 2.0.0 + * @license MIT + * @copyright @ReeceM + */var k,j={version:"2.0.0",rendered:!1,fetching:!1,usingBanner:!0,init:function(e){var t,r=0 +
+ h-bar announcements +
+
+ h-bar Announcement banner +
+ + +The announcement bar uses native methods to make the library lightweight so that it can be loaded quickly, bar the webpack stuff. + +**Note** +The initial version makes use of the WordPress API to be able to get the latest post. + +i.e. it expects a json structure like so: +From a url like `http://blog.example.com/wp-json/wp/v2/posts?per_page=1&_fields=id,title,link` +```json +[ + { + "id": 175, + "link": "https:\/\/blog.example.com\/how-to-hunt-a-vole\/", + "title": { + "rendered": "How To Hunt A Vole" + } + } +] +``` + +I plan to add more options and a parser callback that can be defined to extract a standard format. + +## Installation + +You can install the package via npm: + +```bash +npm i @reecem/h-bar +``` + +Or use jsDelivr: +```html + ... + + ... +``` + +> If you are customising the styling and overriding it with your own styling then you will also need an instance of your css or a tailwindcss file installed as only the classes needed are packaged with h-bar + +## Example page + +You can view an [example page](https://reecem.github.io/h-bar/example.html) + +## Usage + +You can import it directly into your javascript app or use it in the html. + +```html + + + + +``` + +The initialization object currently has this structure and defaults: + +```javascript +{ + url: "https://your.blog/api/....", + onCompleted: "callback function", + link: "The link url, can be force and no need to fetch from API", + title: "The link url, can be force and no need to fetch from API", + secondaryLinks: [ + { + title: "Docs", + link: "http:://docs.example.com" + } + ], + parser: (data) => {/** Parser function */} + dismissible: false, // dismissible banner flag + dismissFor: new Date('2020-03-30'), // would dismiss it till end of March 30th 2020 + theme: "gray", + headers: { + "Authorization": "Bearer {TOKEN}" + }, + customStyles: { + wrapper: "hb-flex hb-w-full hd-flex-col md:hb-flex-row sm:hb-flex-row hb-text-sm hb-py-2 md:hb-px-20 hb-px-1 hb-items-center hb-justify-between", + linkWrapper: "hb-flex hb-items-center", + badge: "hb-px-2 hb-mx-2 hb-leading-relaxed hb-tracking-wider hb-uppercase hb-font-semibold hb-rounded-full hb-text-xs", + postTitle: "hover:hb-underline", + secondaryLink: "hb-mx-5 hb-cursor-pointer hover:hb-underline", + } +} +``` + +### Parser function + +There is the availability of adding a custom parser function to override any of the default ones provided by the package. + +This is handy if you have a custom endpoint that say would return also the secondary links or has a different data structure. + +The parser function should always return an object with the structure: +```javascript +{ + title: String, + link: String, + /** the secondaryLinks is optional. + * It will also override the links parsed in the init() arguments. + */ + secondaryLinks: [ + { + title: String, + link: String, + }, + ] +} +``` + +You can define the function inside the `init()` method as follows: +```javascript + +hBar.init({ + url: "https://api.github.com/repos/ReeceM/h-bar/releases", + parser: (data) => { + // getting the first release on the list of releases from github. + const {name, html_url} = data[0]; + + return { + title: `Lateset version available ${name}`, + link: html_url + }; + } +}) +``` + +### Dismissing Notifications + +> Available from `v0.3.0`/`v1.0.0` + +**Temporary Dismissing** +To be able to dismiss a notification, please note it currently removes secondary links. It is therefore useful that you use this feature when just making announcements of a event or brief notification. + +The way to activate session based dismissal is: + +```javascript +{ + //... rest of config + dismissible: true, + //... rest of config +} +``` + +This will just disable the banner for the current page visit, if the user reloads, its back. + +**Time based dismissing** + +To dismiss the banner until another time, you can set the `dismissFor` variable, this requires a `Date()` object. + +When you set this and the banner is dismissed, the UTC milliseconds are stored in the localStorage, this is then read back when loading h-Bar. + +```javascript +{ + //... rest of config + dismissible: true, + dismissFor: new Date('2020-03-30'), // would dismiss it till end of march 30th +} +``` + +If you fail to set the value properly, it won't dismiss and the banner will show by default. + + +## Testing + +_to come_ please make a PR if you know how to do it on JS. + +## Changelog + +Please see [CHANGELOG](https://github.com/ReeceM/h-bar/blob/master/CHANGELOG.md) for more information on what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](https://github.com/ReeceM/h-bar/blob/master/CONTRIBUTING.md) for details. + +## Security + +[SECURITY](https://github.com/ReeceM/h-bar/security/policy) + +If you discover any security related issues, please email zsh.rce@gmail.com instead of using the issue tracker. + +## Credits + +- [ReeceM](https://github.com/ReeceM) +- [All Contributors](../../contributors) + +## Support + +Buy Me A Coffee + +[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/S6S7UQ66) + +## License + +The MIT License (MIT). Please see [License File](https://github.com/ReeceM/h-bar/blob/master/LICENSE.md) for more information. diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 0000000..bc02aae --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,14 @@ + + +📣 + +# h-bar v2.0 + +> An Announcement Banner that is lightweight and customizable + +- Simple and lightweight (~5.6kB gzipped) +- Customizable with personal templates +- Multiple default themes ready to go + +[GitHub](https://github.com/reecem/h-bar/) +[Get Started](#h-bar-lightweight-announcement-bar) diff --git a/docs/example.html b/docs/example.html index 8002942..a56d9a6 100644 --- a/docs/example.html +++ b/docs/example.html @@ -7,7 +7,7 @@ -
+
@@ -65,7 +65,39 @@

Features

- + + + + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..737637e --- /dev/null +++ b/docs/index.html @@ -0,0 +1,86 @@ + + + + + + + + + + + + + An Announcement Banner that is lightweight and customizable + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/package.json b/package.json index 8e23a60..8642820 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@reecem/h-bar", - "version": "1.1.0", - "description": "Lightweight Announcement Bar with Tailwindcss, extendable too", - "main": "dist/hBar.js", + "version": "2.0.0-rc1", + "description": "Lightweight Announcement Bar", + "main": "dist/hBar.min.js", "scripts": { "dev": "cross-env NODE_ENV=development webpack --mode development --config webpack.config.js", "build": "cross-env NODE_ENV=production webpack --mode production --config webpack.config.js", @@ -24,7 +24,7 @@ "bugs": { "url": "https://github.com/ReeceM/h-bar/issues" }, - "homepage": "https://github.com/ReeceM/h-bar#readme", + "homepage": "https://reecem.github.io/h-bar", "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.5", diff --git a/src/banner/banner.js b/src/banner/banner.js new file mode 100644 index 0000000..424ae32 --- /dev/null +++ b/src/banner/banner.js @@ -0,0 +1,135 @@ +import { domReady, newElement, isDismissed } from "../utils"; +import { styling } from './styling'; + +export default class Banner { + + /** + * + * @param {object} param0 + */ + constructor({ $el, dismissible, dismissFor, theme, badge }) { + this.$el = $el; + this.dismissible = dismissible; + this.dismissFor = dismissFor; + this.badge = badge; + this.theme = theme + } + + /** + * Render the element. + */ + resolve({ title, link, secondaryLinks}) { + if (isDismissed()) return; + + domReady().then(() => { + + if (!title) { + console.error('[h-bar] no post data, unable to render'); + return; + } + + let secondaryElement = null; + + if (!this.dismissible) { + let secondaryLinkList = this.createSecondaryLinks(secondaryLinks) + secondaryElement = newElement('div', { + children: secondaryLinkList, + classes: `${styling.linkWrapper} ${this.theme.linkWrapper}` + }) + } else { + secondaryElement = this.dismissibleButton(); + } + + let badgeElement = newElement('span', { classes: `${styling.badge} ${this.theme.badge}` }) + let postLink = newElement('a', { classes: `${styling.postTitle} ${this.theme.postTitle}` }) + + badgeElement.innerText = this.badge; + postLink.href = link; + postLink.innerText = title; + + postLink.innerHTML += ` + + + + ` + + let postElement = newElement('div', { + classes: `${styling.linkWrapper} ${this.theme.linkWrapper}`, + children: [badgeElement, postLink] + }) + + let _hbar = newElement('div', { + classes: `${styling.wrapper} ${this.theme.wrapper}`, + children: [postElement, secondaryElement] + }) + + let container = document.querySelector(this.$el); + + container.innerHTML = "" + container.appendChild(_hbar) + }) + } + + /** + * Removes the element in the case of it having issues. + * Rather an aggressive option. + * + * Also used when dismissing. + */ + destroy() { + try { + document.querySelector(this.$el).innerHTML = ''; + return true; + } catch (error) { + console.error('Unable to destroy the h-bar wrapper') + console.error(error) + } + return false; + } + + /** + * Creates the HTML node for a dismissible button. + * + * @returns HTMLElement + */ + dismissibleButton() { + let dismiss = newElement('button', { + classes: `hb--mr-2 hb-flex hb-p-1 hb-rounded-md ${this.theme.dismiss} hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800`, + }); + + dismiss.innerHTML = ` + + ` + + dismiss.onclick = (e) => { + e.preventDefault(); + + // just do it early if we not logging time. + if (!this.dismissFor) return this.destroy(); + + if (localStorage) { + localStorage.setItem('h-bar_dismiss_for', this.dismissFor.getTime()); + } + + return this.destroy(); + } + + return dismiss; + } + + /** + * Creates the secondary links for the bar. + */ + createSecondaryLinks(secondaryLinks) { + if (!secondaryLinks) return []; + + return secondaryLinks.map(({ title, link }) => { + let style = `${styling.secondaryLink} ${this.theme.secondaryLink}`; + let butter = newElement('a', { classes: style }) + butter.href = link; + butter.innerText = title; + + return butter; + }, this); + } +} diff --git a/src/config/styling.js b/src/banner/styling.js similarity index 95% rename from src/config/styling.js rename to src/banner/styling.js index 4286326..9511ed2 100644 --- a/src/config/styling.js +++ b/src/banner/styling.js @@ -6,7 +6,7 @@ * @var {object} styling */ export const styling = { - wrapper: "hb-flex hb-w-full hd-flex-col md:hb-flex-row sm:hb-flex-row hb-text-sm hb-py-2 md:hb-px-20 hb-px-1 hb-items-center hb-justify-between", + wrapper: "hb-flex hb-w-full hd-flex-col md:hb-flex-row hb-text-sm hb-py-2 md:hb-px-20 hb-px-1 hb-items-center hb-justify-between", linkWrapper: "hb-flex hb-items-center", badge: "hb-px-2 hb-mx-2 hb-leading-relaxed hb-tracking-wider hb-uppercase hb-font-semibold hb-rounded-full hb-text-xs", postTitle: "hover:hb-underline hb-inline-flex hb-items-center", diff --git a/src/config/config.js b/src/config/config.js index 672dcc5..b4a081a 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -2,33 +2,9 @@ * The default configuration for the package * * @var {object} config - * @var {string} config.url - * @var {string} config.ele - * @var {boolean} config.dismissible - * @var {Date} config.dismissFor - * @var {string} config.badge - * @var {string} config.theme - * @var {array} config.secondaryLinks - * @var {object} config.customStyles - * @var {function} config.onCompleted - * @var {function} config.parser - * @var {string} config.link - * @var {string} config.title * @var {object} config.fetchOptions */ export const config = { - url: '', - ele: 'h-bar', - dismissible: false, - dismissFor: null, - badge: 'New', - theme: "gray", - secondaryLinks: [], - customStyles: {}, - onCompleted: () => { }, - parser: null, - link: null, - title: null, fetchOptions: { method: 'GET', mode: 'cors', // no-cors, *cors, same-origin diff --git a/src/functions/init.js b/src/functions/init.js index 98a5178..a99a0e4 100644 --- a/src/functions/init.js +++ b/src/functions/init.js @@ -1,12 +1,14 @@ +import { themes } from '../banner/styling'; import { config } from '../config/config' import { initNormalise } from "./normalise" -import { styling } from "../config/styling" /** + * Set all the configuration options for the hBar library * * @param {object} options + * @param {string} options.el The element id + * @param {string} options.template The template id * @param {string} options.url - * @param {string} options.ele The element id * @param {boolean} options.dismissible * @param {Date|boolean} options.dismissFor * @param {string} options.badge @@ -15,34 +17,44 @@ import { styling } from "../config/styling" * @param {object} options.customStyles * @param {function} options.parser * @param {function} options.onCompleted + * @param {function} options.onFailure * @param {string} options.link Manual override * @param {string} options.title Manual Override */ export function init(options = {}) { - this.url = options.url; + let configuration = {}; - this.ele = options.ele || 'h-bar'; + configuration.$el = options.el; - // we will default to false for this - this.dismissible = options.dismissible || false; + configuration.url = options.url; + // if the user has dompurify installed. It can be optional + configuration.DOMPurify = options.DOMPurify || null; - this.dismissFor = options.dismissFor || false; + configuration.theme = themes[options.theme] || 'grey'; + configuration.badge = options.badge || 'New'; - this.config = config; - this.config.fetchOptions.headers = Object.assign(config.fetchOptions.headers, options.headers) - this.styling = Object.assign(styling, options.customStyles); + // we will default to false for configuration + configuration.dismissible = options.dismissible || false; + configuration.dismissFor = options.dismissFor || false; - this.secondaryLinks = options.secondaryLinks + configuration.secondaryLinks = options.secondaryLinks || []; - this.onCompleted = options.onCompleted || function () { }; + /** + * These will be the fallbacks if something isn't found. + */ + configuration.title = options.title || null; + configuration.link = options.link || null; - this.badge = options.badge || 'New'; - this.postLink = options.link - this.postTitle = options.title + configuration.onCompleted = options.onCompleted || function () { }; + configuration.onFailure = options.onFailure || function () { }; - this.theme = options.theme + if (typeof options.fetch == 'function') { + configuration.fetch = options.fetch; + } + configuration.fetchOptions = config.fetchOptions; + configuration.fetchOptions.headers = Object.assign(config.fetchOptions.headers, options.headers) - initNormalise(options.parser) + initNormalise(options.parser || null) - return this + return configuration; } diff --git a/src/functions/renderer.js b/src/functions/renderer.js new file mode 100644 index 0000000..37649a9 --- /dev/null +++ b/src/functions/renderer.js @@ -0,0 +1,51 @@ +export default class Renderer { + /** + * Creates a new simple renderer for the templates + * + * @param {string} el the element ID that has the template data + * @param {object} data The key:value pair of the data to replace in the template + * @param {DOMPurify} DOMPurify the DOMPurify library + */ + constructor(el, DOMPurify) { + this.template = document.querySelector(el) + this.DOMPurify = DOMPurify; + } + + resolve(data) { + let templateHTML = this.template.innerHTML + + /** + * Don't do anything if there is a no content + */ + if (templateHTML == undefined) { + return null; + } + + Object.keys(data) + .forEach((key) => { + // skip any array things. + // make the thing recursive in x version xD + if (!Array.isArray(data[key])) { + var regex = this.regex(key) + templateHTML = templateHTML.replace(regex, data[key]); + } + }) + + if (this.DOMPurify) { + return this.DOMPurify.sanitize(templateHTML); + } + + return templateHTML; + } + + /** + * Create the matching regex for the template tags + * + * @param {string} key The key to search in the template data + * @returns {RegExp} + */ + regex(key) { + // current tag is {% value %} + return new RegExp(`({%\\s*(${key})\\s*%})`, 'g'); + } +} diff --git a/src/index.js b/src/index.js index f0fd865..06b6b67 100644 --- a/src/index.js +++ b/src/index.js @@ -1,114 +1,142 @@ /** - * h-bar announcement banner + * h-bar banner and dynamic announcement library * - * @version 1.1.0 - * @author ReeceM + * @version 2.0.0 + * @license MIT + * @copyright @ReeceM */ import "./styles.css" -import { init } from "./functions/init" -import { themes } from "./config/styling" -import { domReady, newElement } from "./utils" + +import { init } from './functions/init'; import { normaliser } from "./functions/normalise" +import { getElementOptions, isDismissed } from "./utils"; +import Renderer from './functions/renderer'; +import Banner from './banner/banner'; +/** + * Set all the configuration options for the hBar library + * + * @property {string} el The element id + * @property {string} url + * @property {boolean} dismissible + * @property {Date|boolean} dismissFor + * @property {string} badge + * @property {DOMPurify} DOMPurify the DOMPurify library + * @property {array} secondaryLinks + * @property {object} headers + * @property {object} customStyles + * @property {function} parser + * @property {object} renderer + * @property {function} onCompleted + * @property {function} onFailure + * @property {string} link Manual override + * @property {string} title Manual Override + */ const hBar = { - /** - * h-bar version number - */ - version: "1.1.0", + version: "2.0.0", + rendered: false, + fetching: false, + usingBanner: true, /** - * Initialise the hBar package + * Set all the configuration options for the hBar library * - * @inheritdoc - * @returns {hBar} + * @param {object} options + * @param {string} options.el The element id + * @param {string} options.url + * @param {boolean} options.dismissible + * @param {Date|boolean} options.dismissFor + * @param {string} options.badge + * @param {array} options.secondaryLinks + * @param {object} options.headers + * @param {object} options.customStyles + * @param {function} options.parser + * @param {function} options.onCompleted + * @param {string} options.link Manual override + * @param {string} options.title Manual Override */ - init: init, + init: function (options = {}) { + Object.assign(this, init(options)) + + this.$elementOpt = getElementOptions(document.querySelector(this.$el)); + + if (this.$elementOpt.template) { + this.renderer = new Renderer(this.$elementOpt.template, this.DOMPurify); + } else if (options.renderer) { + /** + * @todo this was added on a whim... bad idea possibly + */ + this.renderer = new options.renderer(this); + } else { + this.renderer = new Banner(this); + } + + Object.defineProperties(this, { + 'renderer': { + configurable: false, + writable: false, + } + }) + }, /** - * Fetch the data from the endpoint + * Gets the data from the url endpoint. + * + * This is called by the */ - fetchData() { - if (this.isDismissed()) return; + fetch: function () { + + if (this.rendered) return; - fetch(this.url, this.config.fetchOptions) + if (isDismissed()) return; + + this.fetching = true; + + fetch(this.url, this.fetchOptions) .then(response => { return response.json() }) .then(json => { if (typeof json == "object") { - normaliser(json) - .then(({ title, link, secondaryLinks }) => { - this.postTitle = title - this.postLink = link - this.secondaryLinks = secondaryLinks || [] - - this.render() - }) - .catch(error => { - console.error(error) - this.destroy(); - }); + this.render(json); } else { console.error(`${this.url} Did not return an object`); } + + this.fetching = false; + }) + .catch(error => { + console.error(error); + this.fetching = false; + this.rendered = false; }); }, /** - * Render the element. + * Render the response to the actual message + * + * @param {Object} result */ - render() { - if (this.isDismissed()) return; - domReady().then(() => { - - if (!this.postTitle) { - console.error('[h-bar] no post data, unable to render'); - return; - } + render: function (result) { - let secondaryElement = null; + normaliser(result) + .then((result) => { + let element = document.querySelector(this.$el); - if (!this.dismissible) { - let secondaryLinkList = this.createSecondaryLinks() - secondaryElement = newElement('div', { - children: secondaryLinkList, - classes: `${this.styling.linkWrapper} ${themes[this.theme].linkWrapper}` - }) - } else { - secondaryElement = this.dismissibleButton(); - } - - let badge = newElement('span', { classes: `${this.styling.badge} ${themes[this.theme].badge}` }) - let postLink = newElement('a', { classes: `${this.styling.postTitle} ${themes[this.theme].postTitle}` }) + element.innerHTML = this.renderer.resolve(result) + element.__hbar__ = this; - badge.innerText = this.badge; - postLink.href = this.postLink; - postLink.innerText = this.postTitle; + this.rendered = true - postLink.innerHTML += ` - - - - ` - - let postElement = newElement('div', { - classes: `${this.styling.linkWrapper} ${themes[this.theme].linkWrapper}`, - children: [badge, postLink] + this.onCompleted({ __hbar__: this, result: element }); }) - - let _hbar = newElement('div', { - classes: `${this.styling.wrapper} ${themes[this.theme].wrapper}`, - children: [postElement, secondaryElement] - }) - - let container = document.getElementById(this.ele); - - container.innerHTML = "" - container.appendChild(_hbar) - - // ? what to send out - this.onCompleted({ element: container, id: this.ele }); - }) + .catch(error => { + console.error(error) + this.destroy(); + this.fetching = false; + this.rendered = false; + this.onFailure({__hbar__: this}) + }); }, /** @@ -117,88 +145,34 @@ const hBar = { * * Also used when dismissing. */ - destroy() { + destroy: function () { try { - document.getElementById(this.ele).remove() - + document.querySelector(this.$el).innerHTML = ''; return true; } catch (error) { console.error('Unable to destroy the h-bar wrapper') console.error(error) } - return false; }, +} +Object.defineProperties(hBar, { /** - * Creates the HTML node for a dismissible button. - * - * @returns HTMLElement + * Config method should not be changed */ - dismissibleButton() { - let dismiss = newElement('button', { - classes: 'hb--mr-2 hb-flex hb-p-1 hb-rounded-md hover:hb-text-white hover:hb-bg-gray-800 focus:hb-outline-none focus:hb-bg-gray-800', - }); - - dismiss.innerHTML = ` - - ` - - dismiss.onclick = (e) => { - e.preventDefault(); - - // just do it early if we not logging time. - if (!this.dismissFor) return this.destroy(); - - if (localStorage) { - localStorage.setItem('h-bar_dismiss_for', this.dismissFor.getTime()); - } - - return this.destroy(); - } - - return dismiss; + 'init': { + writable: false, + configurable: false, }, - - /** - * Determines if the banner has been dismissed. - * - * @returns boolean - */ - isDismissed() { - - if (localStorage) { - var dismissDate = localStorage.getItem('h-bar_dismiss_for'); - if (!dismissDate) { - return false; - } - - dismissDate = dismissDate; - var ourDate = (new Date()).getTime(); - - if (ourDate <= dismissDate) { - return true; - } - } - - return false; + 'destroy': { + writable: false, + configurable: false, }, - - /** - * Creates the secondary links for the bar. - */ - createSecondaryLinks() { - if (!this.secondaryLinks) return []; - - return this.secondaryLinks.map(({ title, link }) => { - let style = `${this.styling.secondaryLink} ${themes[this.theme].secondaryLink}`; - let butter = newElement('a', { classes: style }) - butter.href = link; - butter.innerText = title; - - return butter; - }, this); + 'fetch': { + writable: false, + configurable: false, } -} +}); -export default hBar +export default hBar; diff --git a/src/utils.js b/src/utils.js index 1ed3900..d53d1d3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,4 @@ -// Thanks @stimulus: +// Thanks @stimulus: and I got it from @alpinejs // https://github.com/stimulusjs/stimulus/blob/master/packages/%40stimulus/core/src/application.ts export function domReady() { return new Promise(resolve => { @@ -47,14 +47,13 @@ export function addClasses(element, classes = '') { return element } - /** * Binds all the methods on a JS Class to the `this` context of the class. * Adapted from https://github.com/sindresorhus/auto-bind * @param {object} self The `this` context of the class * @return {object} The `this` context of the class */ -export default function autoBind(self) { +export function autoBind(self) { const keys = Object.getOwnPropertyNames(self.constructor.prototype); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -66,3 +65,41 @@ export default function autoBind(self) { return self; } + +/** + * Gets the data-* values that area related to the config of the template options. + * + * @param {HTMLElement} element + * @return {Object} + */ +export function getElementOptions(element) { + return { + template: element.dataset.template, + html: element.getAttribute('has-html') == "" ? true : false, + dismissFor: element.dataset.dismissFor || null, + }; +} + +/** + * Determines if the banner has been dismissed. + * + * @returns boolean + */ +export function isDismissed() { + + if (localStorage) { + var dismissDate = localStorage.getItem('h-bar_dismiss_for'); + if (!dismissDate) { + return false; + } + + dismissDate = dismissDate; + var ourDate = (new Date()).getTime(); + + if (ourDate <= dismissDate) { + return true; + } + } + + return false; +}