diff --git a/.eslintrc b/.eslintrc index 89cb7da..ecaee5b 100755 --- a/.eslintrc +++ b/.eslintrc @@ -152,7 +152,6 @@ "no-with": 2, "one-var": 0, "operator-assignment": 0, - "operator-linebreak": [2, "after"], "padded-blocks": 0, "quote-props": 0, "quotes": [2, "single", "avoid-escape"], diff --git a/dist/wurd.cjs.js b/dist/wurd.cjs.js index 5a5d1b5..00968b4 100644 --- a/dist/wurd.cjs.js +++ b/dist/wurd.cjs.js @@ -130,11 +130,6 @@ class Block { this.wurd = wurd; this.path = path; - // Private shortcut to the main content getter - // TODO: Make a proper private variable - // See http://voidcanvas.com/es6-private-variables/ - but could require Babel Polyfill to be included - this._get = wurd.store.get.bind(wurd.store); - // Bind methods to the instance to enable 'this' to be available // to own methods and added helper methods; // This also allows object destructuring, for example: @@ -168,13 +163,13 @@ class Block { * @return {Mixed} */ get(path) { - const result = this._get(this.id(path)); + const result = this.wurd.store.get(this.id(path)); // If an item is missing, check that the section has been loaded if (typeof result === 'undefined' && this.wurd.draft) { const section = path.split('.')[0]; - if (!this._get(section)) { + if (!this.wurd.store.get(section)) { console.warn(`Tried to access unloaded section: ${section}`); } } @@ -474,6 +469,14 @@ class Wurd { // Pass main content Block to callbacks if (onLoad) onLoad(content); + return content; + }) + .catch(err => { + if (debug) console.info('Wurd: load error:', err); + + // If content fails to load (wurd app offline), still return cache + if (onLoad) onLoad(content); + return content; }); } diff --git a/dist/wurd.esm.js b/dist/wurd.esm.js index ff76dec..01fa73d 100644 --- a/dist/wurd.esm.js +++ b/dist/wurd.esm.js @@ -128,11 +128,6 @@ class Block { this.wurd = wurd; this.path = path; - // Private shortcut to the main content getter - // TODO: Make a proper private variable - // See http://voidcanvas.com/es6-private-variables/ - but could require Babel Polyfill to be included - this._get = wurd.store.get.bind(wurd.store); - // Bind methods to the instance to enable 'this' to be available // to own methods and added helper methods; // This also allows object destructuring, for example: @@ -166,13 +161,13 @@ class Block { * @return {Mixed} */ get(path) { - const result = this._get(this.id(path)); + const result = this.wurd.store.get(this.id(path)); // If an item is missing, check that the section has been loaded if (typeof result === 'undefined' && this.wurd.draft) { const section = path.split('.')[0]; - if (!this._get(section)) { + if (!this.wurd.store.get(section)) { console.warn(`Tried to access unloaded section: ${section}`); } } @@ -472,6 +467,14 @@ class Wurd { // Pass main content Block to callbacks if (onLoad) onLoad(content); + return content; + }) + .catch(err => { + if (debug) console.info('Wurd: load error:', err); + + // If content fails to load (wurd app offline), still return cache + if (onLoad) onLoad(content); + return content; }); } diff --git a/dist/wurd.js b/dist/wurd.js index 1e9c67e..29ae07d 100644 --- a/dist/wurd.js +++ b/dist/wurd.js @@ -4,70 +4,62 @@ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.wurd = factory()); })(this, (function () { 'use strict'; - function ownKeys(object, enumerableOnly) { - var keys = Object.keys(object); - - if (Object.getOwnPropertySymbols) { - var symbols = Object.getOwnPropertySymbols(object); - enumerableOnly && (symbols = symbols.filter(function (sym) { - return Object.getOwnPropertyDescriptor(object, sym).enumerable; - })), keys.push.apply(keys, symbols); - } - - return keys; + function _classCallCheck(a, n) { + if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } - - function _objectSpread2(target) { - for (var i = 1; i < arguments.length; i++) { - var source = null != arguments[i] ? arguments[i] : {}; - i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { - _defineProperty(target, key, source[key]); - }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { - Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); - }); + function _defineProperties(e, r) { + for (var t = 0; t < r.length; t++) { + var o = r[t]; + o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } - - return target; } - - function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } + function _createClass(e, r, t) { + return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { + writable: !1 + }), e; } - - 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 _defineProperty(e, r, t) { + return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { + value: t, + enumerable: !0, + configurable: !0, + writable: !0 + }) : e[r] = t, e; } - - function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; + function ownKeys(e, r) { + var t = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var o = Object.getOwnPropertySymbols(e); + r && (o = o.filter(function (r) { + return Object.getOwnPropertyDescriptor(e, r).enumerable; + })), t.push.apply(t, o); + } + return t; } - - function _defineProperty(obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true + function _objectSpread2(e) { + for (var r = 1; r < arguments.length; r++) { + var t = null != arguments[r] ? arguments[r] : {}; + r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { + _defineProperty(e, r, t[r]); + }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { + Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); - } else { - obj[key] = value; } - - return obj; + return e; + } + function _toPrimitive(t, r) { + if ("object" != typeof t || !t) return t; + var e = t[Symbol.toPrimitive]; + if (void 0 !== e) { + var i = e.call(t, r || "default"); + if ("object" != typeof i) return i; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return ("string" === r ? String : Number)(t); + } + function _toPropertyKey(t) { + var i = _toPrimitive(t, "string"); + return "symbol" == typeof i ? i : i + ""; } /** @@ -82,6 +74,7 @@ }); return parts.join('&'); } + /** * Replaces {{mustache}} style placeholders in text with variables * @@ -90,7 +83,6 @@ * * @return {String} */ - function replaceVars(text) { var vars = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (typeof text !== 'string') return text; @@ -107,25 +99,21 @@ */ function Store() { var _opts$ttl; - var rawContent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - _classCallCheck(this, Store); - this.rawContent = rawContent; this.storageKey = opts.storageKey || 'wurdContent'; this.ttl = (_opts$ttl = opts.ttl) !== null && _opts$ttl !== void 0 ? _opts$ttl : 3600000; } + /** * Get a specific piece of content, top-level or nested * * @param {String} path e.g. 'section','section.subSection','a.b.c.d' * @return {Mixed} */ - - - _createClass(Store, [{ + return _createClass(Store, [{ key: "get", value: function get(path) { if (!path) return this.rawContent; @@ -133,6 +121,7 @@ return acc && acc[k]; }, this.rawContent); } + /** * Load top-level sections of content from localStorage * @@ -142,34 +131,33 @@ * @param {String} [options.lang] Language * @return {Object} content */ - }, { key: "load", value: function load(sectionNames) { var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - lang = _ref.lang; - + lang = _ref.lang; var rawContent = this.rawContent, - storageKey = this.storageKey, - ttl = this.ttl; - + storageKey = this.storageKey, + ttl = this.ttl; try { // Find cached content var cachedContent = JSON.parse(localStorage.getItem(storageKey)); - var metaData = cachedContent && cachedContent._wurd; // Check if it has expired + var metaData = cachedContent && cachedContent._wurd; + // Check if it has expired if (!cachedContent || !metaData || metaData.savedAt + ttl < Date.now()) { return rawContent; - } // Check it's in the correct language - + } + // Check it's in the correct language if (metaData.lang !== lang) { return rawContent; - } // Remove metadata - + } - delete cachedContent['_wurd']; // Add cached content to memory content + // Remove metadata + delete cachedContent['_wurd']; + // Add cached content to memory content Object.assign(rawContent, cachedContent); return rawContent; } catch (err) { @@ -177,21 +165,20 @@ return rawContent; } } + /** * Save top-level sections of content to localStorage * * @param {Object} sections * @param {Boolean} [options.cache] Whether to save the content to cache */ - }, { key: "save", value: function save(sections) { var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - lang = _ref2.lang; - + lang = _ref2.lang; var rawContent = this.rawContent, - storageKey = this.storageKey; + storageKey = this.storageKey; Object.assign(rawContent, sections); localStorage.setItem(storageKey, JSON.stringify(_objectSpread2(_objectSpread2({}, rawContent), {}, { _wurd: { @@ -200,41 +187,35 @@ } }))); } + /** * Clears the localStorage cache */ - }, { key: "clear", value: function clear() { localStorage.removeItem(this.storageKey); } }]); - - return Store; }(); var Block = /*#__PURE__*/function () { function Block(wurd, path) { var _this = this; - _classCallCheck(this, Block); - this.wurd = wurd; - this.path = path; // Private shortcut to the main content getter - // TODO: Make a proper private variable - // See http://voidcanvas.com/es6-private-variables/ - but could require Babel Polyfill to be included + this.path = path; - this._get = wurd.store.get.bind(wurd.store); // Bind methods to the instance to enable 'this' to be available + // Bind methods to the instance to enable 'this' to be available // to own methods and added helper methods; // This also allows object destructuring, for example: // `const {text} = wurd.block('home')` - var methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); methodNames.forEach(function (name) { _this[name] = _this[name].bind(_this); }); } + /** * Gets the ID of a child content item by path (e.g. id('item') returns `block.item`) * @@ -242,14 +223,13 @@ * * @return {String} */ - - - _createClass(Block, [{ + return _createClass(Block, [{ key: "id", value: function id(path) { if (!path) return this.path; return this.path ? [this.path, path].join('.') : path; } + /** * Gets a content item by path (e.g. `section.item`). * Will return both text and/or objects, depending on the contents of the item @@ -258,23 +238,21 @@ * * @return {Mixed} */ - }, { key: "get", value: function get(path) { - var result = this._get(this.id(path)); // If an item is missing, check that the section has been loaded - + var result = this.wurd.store.get(this.id(path)); + // If an item is missing, check that the section has been loaded if (typeof result === 'undefined' && this.wurd.draft) { var section = path.split('.')[0]; - - if (!this._get(section)) { + if (!this.wurd.store.get(section)) { console.warn("Tried to access unloaded section: ".concat(section)); } } - return result; } + /** * Gets text content of an item by path (e.g. `section.item`). * If the item is not a string, e.g. you have passed the path of an object, @@ -285,27 +263,23 @@ * * @return {Mixed} */ - }, { key: "text", value: function text(path, vars) { var text = this.get(path); - if (typeof text === 'undefined') { return this.wurd.draft ? "[".concat(path, "]") : ''; } - if (typeof text !== 'string') { console.warn("Tried to get object as string: ".concat(path)); return this.wurd.draft ? "[".concat(path, "]") : ''; } - if (vars) { text = replaceVars(text, vars); } - return text; } + /** * Gets HTML from Markdown content of an item by path (e.g. `section.item`). * If the item is not a string, e.g. you have passed the path of an object, @@ -317,51 +291,44 @@ * * @return {Mixed} */ - }, { key: "markdown", value: function markdown(path, vars, opts) { var _this$wurd$markdown = this.wurd.markdown, - parse = _this$wurd$markdown.parse, - parseInline = _this$wurd$markdown.parseInline; + parse = _this$wurd$markdown.parse, + parseInline = _this$wurd$markdown.parseInline; var text = this.text(path, vars); - if (opts !== null && opts !== void 0 && opts.inline && parseInline) { return parseInline(text); } - if (parse) { return parse(text); } - return text; } + /** * Iterates over a collection / list object with the given callback. * * @param {String} path * @param {Function} fn Callback function with signature ({Function} itemBlock, {Number} index) */ - }, { key: "map", value: function map(path, fn) { var _this2 = this; - var listContent = this.get(path) || _defineProperty({}, Date.now(), {}); - var index = 0; var keys = Object.keys(listContent).sort(); return keys.map(function (key) { var currentIndex = index; index++; var itemPath = [path, key].join('.'); - var itemBlock = _this2.block(itemPath); - return fn.call(undefined, itemBlock, currentIndex); }); } + /** * Creates a new Block scoped to the child content. * Optionally runs a callback with the block as the argument @@ -371,19 +338,17 @@ * * @return {Block} */ - }, { key: "block", value: function block(path, fn) { var blockPath = this.id(path); var childBlock = new Block(this.wurd, blockPath); - if (typeof fn === 'function') { return fn.call(undefined, childBlock); } - return childBlock; } + /** * Returns an HTML string for an editable element. * @@ -400,7 +365,6 @@ * * @return {String} */ - }, { key: "el", value: function el(path, vars) { @@ -408,15 +372,14 @@ var id = this.id(path); var text = options.markdown ? this.markdown(path, vars) : this.text(path, vars); var editor = vars || options.markdown ? 'data-wurd-md' : 'data-wurd'; - if (this.wurd.draft) { var type = options.type || 'span'; if (options.markdown) type = 'div'; return "<".concat(type, " ").concat(editor, "=\"").concat(id, "\">").concat(text, ""); } - return text; } + /** * Returns the block helpers, bound to the block instance. * This is useful if using object destructuring for shortcuts, @@ -424,7 +387,6 @@ * * @return {Object} */ - /* helpers(path) { const block = path ? this.block(path) : this; @@ -437,10 +399,7 @@ return boundMethods; } */ - }]); - - return Block; }(); var Wurd = /*#__PURE__*/function () { @@ -449,22 +408,21 @@ */ function Wurd(appName) { var _this = this; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - _classCallCheck(this, Wurd); - this.widgetUrl = 'https://widget.wurd.io/widget.js'; this.apiUrl = 'https://api.wurd.io'; this.store = new Store(); - this.content = new Block(this, null); // Add block shortcut methods to the main Wurd instance + this.content = new Block(this, null); + // Add block shortcut methods to the main Wurd instance var methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(this.content)); methodNames.forEach(function (name) { _this[name] = _this.content[name].bind(_this.content); }); this.connect(appName, options); } + /** * Sets up the default connection/instance * @@ -479,91 +437,87 @@ * @param {Object} [options.rawContent] Content to populate the store with * @param {Function} [options.onLoad] Callback that runs whenever load() completes. Signature: onLoad(content) => {} */ - - - _createClass(Wurd, [{ + return _createClass(Wurd, [{ key: "connect", value: function connect(appName) { var _this2 = this; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; this.app = appName; this.draft = false; - this.editMode = false; // Set allowed options + this.editMode = false; + // Set allowed options ['draft', 'lang', 'markdown', 'debug', 'onLoad'].forEach(function (name) { var val = options[name]; if (typeof val !== 'undefined') _this2[name] = val; - }); // Activate edit mode if required + }); + // Activate edit mode if required switch (options.editMode) { // Edit mode always on case true: this.startEditor(); break; - // Activate edit mode if the querystring contains an 'edit' parameter e.g. '?edit' + // Activate edit mode if the querystring contains an 'edit' parameter e.g. '?edit' case 'querystring': if (/[?&]edit(&|$)/.test(location.search)) { this.startEditor(); } - break; } - if (options.rawContent) { this.store.save(options.rawContent, { lang: options.lang }); } - if (options.storageKey) this.store.storageKey = options.storageKey; if (options.ttl) this.store.ttl = options.ttl; - if (options.blockHelpers) { this.setBlockHelpers(options.blockHelpers); } - return this; } + /** * Loads sections of content so that items are ready to be accessed with #get(id) * * @param {String|Array} sectionNames Top-level sections to load e.g. `main,home` */ - }, { key: "load", value: function load(sectionNames) { var app = this.app, - store = this.store, - lang = this.lang, - editMode = this.editMode, - debug = this.debug, - onLoad = this.onLoad, - content = this.content; - + store = this.store, + lang = this.lang, + editMode = this.editMode, + debug = this.debug, + onLoad = this.onLoad, + content = this.content; if (!app) { return Promise.reject(new Error('Use wurd.connect(appName) before wurd.load()')); - } // Normalise string sectionNames to array - + } - var sections = typeof sectionNames === 'string' ? sectionNames.split(',') : sectionNames; // When in editMode we skip the cache completely + // Normalise string sectionNames to array + var sections = typeof sectionNames === 'string' ? sectionNames.split(',') : sectionNames; + // When in editMode we skip the cache completely if (editMode) { return this._fetchSections(sections).then(function (result) { store.save(result, { lang: lang - }); // Clear the cache so changes are reflected immediately when out of editMode + }); - store.clear(); // Pass main content Block to callbacks + // Clear the cache so changes are reflected immediately when out of editMode + store.clear(); + // Pass main content Block to callbacks if (onLoad) onLoad(content); return content; }); - } // Check for cached sections - + } + // Check for cached sections var cachedContent = store.load(sections, { lang: lang }); @@ -572,21 +526,29 @@ }); if (debug) console.info('Wurd: from cache:', sections.filter(function (section) { return cachedContent[section] !== undefined; - })); // Return now if all content was in cache + })); + // Return now if all content was in cache if (uncachedSections.length === 0) { // Pass main content Block to callbacks if (onLoad) onLoad(content); return Promise.resolve(content); - } // Otherwise fetch remaining sections - + } + // Otherwise fetch remaining sections return this._fetchSections(uncachedSections).then(function (result) { // Cache for next time store.save(result, { lang: lang - }); // Pass main content Block to callbacks + }); + + // Pass main content Block to callbacks + if (onLoad) onLoad(content); + return content; + })["catch"](function (err) { + if (debug) console.info('Wurd: load error:', err); + // If content fails to load (wurd app offline), still return cache if (onLoad) onLoad(content); return content; }); @@ -595,12 +557,13 @@ key: "_fetchSections", value: function _fetchSections(sectionNames) { var _this3 = this; - var app = this.app, - debug = this.debug; // Some sections not in cache; fetch them from server + debug = this.debug; - if (debug) console.info('Wurd: from server:', sectionNames); // Build request URL + // Some sections not in cache; fetch them from server + if (debug) console.info('Wurd: from server:', sectionNames); + // Build request URL var params = ['draft', 'lang'].reduce(function (memo, param) { if (_this3[param]) memo[param] = _this3[param]; return memo; @@ -629,25 +592,22 @@ key: "startEditor", value: function startEditor() { var app = this.app, - lang = this.lang; // Draft mode is always on if in edit mode + lang = this.lang; + // Draft mode is always on if in edit mode this.editMode = true; this.draft = true; var script = document.createElement('script'); script.src = this.widgetUrl; script.async = true; script.setAttribute('data-app', app); - if (lang) { script.setAttribute('data-lang', lang); } - var prevScript = document.body.querySelector("script[src=\"".concat(this.widgetUrl, "\"]")); - if (prevScript) { document.body.removeChild(prevScript); } - document.body.appendChild(script); } }, { @@ -656,8 +616,6 @@ Object.assign(Block.prototype, helpers); } }]); - - return Wurd; }(); var instance = new Wurd(); instance.Wurd = Wurd; diff --git a/dist/wurd.min.js b/dist/wurd.min.js index fb500bb..5768eb5 100644 --- a/dist/wurd.min.js +++ b/dist/wurd.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).wurd=e()}(this,(function(){"use strict";function t(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function e(e){for(var r=1;r0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};r(this,t),this.rawContent=n,this.storageKey=o.storageKey||"wurdContent",this.ttl=null!==(e=o.ttl)&&void 0!==e?e:36e5}return o(t,[{key:"get",value:function(t){return t?t.split(".").reduce((function(t,e){return t&&t[e]}),this.rawContent):this.rawContent}},{key:"load",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=e.lang,n=this.rawContent,o=this.storageKey,i=this.ttl;try{var a=JSON.parse(localStorage.getItem(o)),c=a&&a._wurd;return!a||!c||c.savedAt+i1&&void 0!==arguments[1]?arguments[1]:{},n=r.lang,o=this.rawContent,i=this.storageKey;Object.assign(o,t),localStorage.setItem(i,JSON.stringify(e(e({},o),{},{_wurd:{savedAt:Date.now(),lang:n}})))}},{key:"clear",value:function(){localStorage.removeItem(this.storageKey)}}]),t}(),c=function(){function t(e,n){var o=this;r(this,t),this.wurd=e,this.path=n,this._get=e.store.get.bind(e.store),Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach((function(t){o[t]=o[t].bind(o)}))}return o(t,[{key:"id",value:function(t){return t?this.path?[this.path,t].join("."):t:this.path}},{key:"get",value:function(t){var e=this._get(this.id(t));if(void 0===e&&this.wurd.draft){var r=t.split(".")[0];this._get(r)||console.warn("Tried to access unloaded section: ".concat(r))}return e}},{key:"text",value:function(t,e){var r=this.get(t);return void 0===r?this.wurd.draft?"[".concat(t,"]"):"":"string"!=typeof r?(console.warn("Tried to get object as string: ".concat(t)),this.wurd.draft?"[".concat(t,"]"):""):(e&&(r=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"!=typeof t?t:t.replace(/{{([\w.-]+)}}/g,(function(t,r){return e[r]||""}))}(r,e)),r)}},{key:"markdown",value:function(t,e,r){var n=this.wurd.markdown,o=n.parse,i=n.parseInline,a=this.text(t,e);return null!=r&&r.inline&&i?i(a):o?o(a):a}},{key:"map",value:function(t,e){var r=this,n=this.get(t)||i({},Date.now(),{}),o=0;return Object.keys(n).sort().map((function(n){var i=o;o++;var a=[t,n].join("."),c=r.block(a);return e.call(void 0,c,i)}))}},{key:"block",value:function(e,r){var n=this.id(e),o=new t(this.wurd,n);return"function"==typeof r?r.call(void 0,o):o}},{key:"el",value:function(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=this.id(t),o=r.markdown?this.markdown(t,e):this.text(t,e),i=e||r.markdown?"data-wurd-md":"data-wurd";if(this.wurd.draft){var a=r.type||"span";return r.markdown&&(a="div"),"<".concat(a," ").concat(i,'="').concat(n,'">').concat(o,"")}return o}}]),t}(),s=function(){function t(e){var n=this,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};r(this,t),this.widgetUrl="https://widget.wurd.io/widget.js",this.apiUrl="https://api.wurd.io",this.store=new a,this.content=new c(this,null);var i=Object.getOwnPropertyNames(Object.getPrototypeOf(this.content));i.forEach((function(t){n[t]=n.content[t].bind(n.content)})),this.connect(e,o)}return o(t,[{key:"connect",value:function(t){var e=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};switch(this.app=t,this.draft=!1,this.editMode=!1,["draft","lang","markdown","debug","onLoad"].forEach((function(t){var n=r[t];void 0!==n&&(e[t]=n)})),r.editMode){case!0:this.startEditor();break;case"querystring":/[?&]edit(&|$)/.test(location.search)&&this.startEditor()}return r.rawContent&&this.store.save(r.rawContent,{lang:r.lang}),r.storageKey&&(this.store.storageKey=r.storageKey),r.ttl&&(this.store.ttl=r.ttl),r.blockHelpers&&this.setBlockHelpers(r.blockHelpers),this}},{key:"load",value:function(t){var e=this.app,r=this.store,n=this.lang,o=this.editMode,i=this.debug,a=this.onLoad,c=this.content;if(!e)return Promise.reject(new Error("Use wurd.connect(appName) before wurd.load()"));var s="string"==typeof t?t.split(","):t;if(o)return this._fetchSections(s).then((function(t){return r.save(t,{lang:n}),r.clear(),a&&a(c),c}));var u=r.load(s,{lang:n}),l=s.filter((function(t){return void 0===u[t]}));return i&&console.info("Wurd: from cache:",s.filter((function(t){return void 0!==u[t]}))),0===l.length?(a&&a(c),Promise.resolve(c)):this._fetchSections(l).then((function(t){return r.save(t,{lang:n}),a&&a(c),c}))}},{key:"_fetchSections",value:function(t){var e=this,r=this.app;this.debug&&console.info("Wurd: from server:",t);var n,o=["draft","lang"].reduce((function(t,r){return e[r]&&(t[r]=e[r]),t}),{}),i="".concat(this.apiUrl,"/apps/").concat(r,"/content/").concat(t,"?").concat((n=o,Object.keys(n).map((function(t){var e=n[t];return encodeURIComponent(t)+"="+encodeURIComponent(e)})).join("&")));return this._fetch(i).then((function(e){if(e.error)throw e.error.message?new Error(e.error.message):new Error("Error loading ".concat(t));return e}))}},{key:"_fetch",value:function(t){return fetch(t).then((function(e){if(!e.ok)throw new Error("Error loading ".concat(t,": ").concat(e.statusText));return e.json()}))}},{key:"startEditor",value:function(){var t=this.app,e=this.lang;this.editMode=!0,this.draft=!0;var r=document.createElement("script");r.src=this.widgetUrl,r.async=!0,r.setAttribute("data-app",t),e&&r.setAttribute("data-lang",e);var n=document.body.querySelector('script[src="'.concat(this.widgetUrl,'"]'));n&&document.body.removeChild(n),document.body.appendChild(r)}},{key:"setBlockHelpers",value:function(t){Object.assign(c.prototype,t)}}]),t}(),u=new s;return u.Wurd=s,u})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).wurd=e()}(this,(function(){"use strict";function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t(this,e),this.rawContent=n,this.storageKey=o.storageKey||"wurdContent",this.ttl=null!==(r=o.ttl)&&void 0!==r?r:36e5}),[{key:"get",value:function(t){return t?t.split(".").reduce((function(t,e){return t&&t[e]}),this.rawContent):this.rawContent}},{key:"load",value:function(t){var e=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).lang,r=this.rawContent,n=this.storageKey,o=this.ttl;try{var i=JSON.parse(localStorage.getItem(n)),a=i&&i._wurd;return!i||!a||a.savedAt+o1&&void 0!==arguments[1]?arguments[1]:{}).lang,r=this.rawContent,n=this.storageKey;Object.assign(r,t),localStorage.setItem(n,JSON.stringify(i(i({},r),{},{_wurd:{savedAt:Date.now(),lang:e}})))}},{key:"clear",value:function(){localStorage.removeItem(this.storageKey)}}])}(),s=function(){function e(r,n){var o=this;t(this,e),this.wurd=r,this.path=n,Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach((function(t){o[t]=o[t].bind(o)}))}return r(e,[{key:"id",value:function(t){return t?this.path?[this.path,t].join("."):t:this.path}},{key:"get",value:function(t){var e=this.wurd.store.get(this.id(t));if(void 0===e&&this.wurd.draft){var r=t.split(".")[0];this.wurd.store.get(r)||console.warn("Tried to access unloaded section: ".concat(r))}return e}},{key:"text",value:function(t,e){var r=this.get(t);return void 0===r?this.wurd.draft?"[".concat(t,"]"):"":"string"!=typeof r?(console.warn("Tried to get object as string: ".concat(t)),this.wurd.draft?"[".concat(t,"]"):""):(e&&(r=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"!=typeof t?t:t.replace(/{{([\w.-]+)}}/g,(function(t,r){return e[r]||""}))}(r,e)),r)}},{key:"markdown",value:function(t,e,r){var n=this.wurd.markdown,o=n.parse,i=n.parseInline,a=this.text(t,e);return null!=r&&r.inline&&i?i(a):o?o(a):a}},{key:"map",value:function(t,e){var r=this,o=this.get(t)||n({},Date.now(),{}),i=0;return Object.keys(o).sort().map((function(n){var o=i;i++;var a=[t,n].join("."),c=r.block(a);return e.call(void 0,c,o)}))}},{key:"block",value:function(t,r){var n=this.id(t),o=new e(this.wurd,n);return"function"==typeof r?r.call(void 0,o):o}},{key:"el",value:function(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=this.id(t),o=r.markdown?this.markdown(t,e):this.text(t,e),i=e||r.markdown?"data-wurd-md":"data-wurd";if(this.wurd.draft){var a=r.type||"span";return r.markdown&&(a="div"),"<".concat(a," ").concat(i,'="').concat(n,'">').concat(o,"")}return o}}])}(),u=function(){return r((function e(r){var n=this,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t(this,e),this.widgetUrl="https://widget.wurd.io/widget.js",this.apiUrl="https://api.wurd.io",this.store=new c,this.content=new s(this,null),Object.getOwnPropertyNames(Object.getPrototypeOf(this.content)).forEach((function(t){n[t]=n.content[t].bind(n.content)})),this.connect(r,o)}),[{key:"connect",value:function(t){var e=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};switch(this.app=t,this.draft=!1,this.editMode=!1,["draft","lang","markdown","debug","onLoad"].forEach((function(t){var n=r[t];void 0!==n&&(e[t]=n)})),r.editMode){case!0:this.startEditor();break;case"querystring":/[?&]edit(&|$)/.test(location.search)&&this.startEditor()}return r.rawContent&&this.store.save(r.rawContent,{lang:r.lang}),r.storageKey&&(this.store.storageKey=r.storageKey),r.ttl&&(this.store.ttl=r.ttl),r.blockHelpers&&this.setBlockHelpers(r.blockHelpers),this}},{key:"load",value:function(t){var e=this.app,r=this.store,n=this.lang,o=this.editMode,i=this.debug,a=this.onLoad,c=this.content;if(!e)return Promise.reject(new Error("Use wurd.connect(appName) before wurd.load()"));var s="string"==typeof t?t.split(","):t;if(o)return this._fetchSections(s).then((function(t){return r.save(t,{lang:n}),r.clear(),a&&a(c),c}));var u=r.load(s,{lang:n}),l=s.filter((function(t){return void 0===u[t]}));return i&&console.info("Wurd: from cache:",s.filter((function(t){return void 0!==u[t]}))),0===l.length?(a&&a(c),Promise.resolve(c)):this._fetchSections(l).then((function(t){return r.save(t,{lang:n}),a&&a(c),c})).catch((function(t){return i&&console.info("Wurd: load error:",t),a&&a(c),c}))}},{key:"_fetchSections",value:function(t){var e=this,r=this.app;this.debug&&console.info("Wurd: from server:",t);var n,o=["draft","lang"].reduce((function(t,r){return e[r]&&(t[r]=e[r]),t}),{}),i="".concat(this.apiUrl,"/apps/").concat(r,"/content/").concat(t,"?").concat((n=o,Object.keys(n).map((function(t){var e=n[t];return encodeURIComponent(t)+"="+encodeURIComponent(e)})).join("&")));return this._fetch(i).then((function(e){if(e.error)throw e.error.message?new Error(e.error.message):new Error("Error loading ".concat(t));return e}))}},{key:"_fetch",value:function(t){return fetch(t).then((function(e){if(!e.ok)throw new Error("Error loading ".concat(t,": ").concat(e.statusText));return e.json()}))}},{key:"startEditor",value:function(){var t=this.app,e=this.lang;this.editMode=!0,this.draft=!0;var r=document.createElement("script");r.src=this.widgetUrl,r.async=!0,r.setAttribute("data-app",t),e&&r.setAttribute("data-lang",e);var n=document.body.querySelector('script[src="'.concat(this.widgetUrl,'"]'));n&&document.body.removeChild(n),document.body.appendChild(r)}},{key:"setBlockHelpers",value:function(t){Object.assign(s.prototype,t)}}])}(),l=new u;return l.Wurd=u,l})); diff --git a/src/block.js b/src/block.js index 48fad0f..c8518db 100644 --- a/src/block.js +++ b/src/block.js @@ -7,11 +7,6 @@ export default class Block { this.wurd = wurd; this.path = path; - // Private shortcut to the main content getter - // TODO: Make a proper private variable - // See http://voidcanvas.com/es6-private-variables/ - but could require Babel Polyfill to be included - this._get = wurd.store.get.bind(wurd.store); - // Bind methods to the instance to enable 'this' to be available // to own methods and added helper methods; // This also allows object destructuring, for example: @@ -45,13 +40,13 @@ export default class Block { * @return {Mixed} */ get(path) { - const result = this._get(this.id(path)); + const result = this.wurd.store.get(this.id(path)); // If an item is missing, check that the section has been loaded if (typeof result === 'undefined' && this.wurd.draft) { const section = path.split('.')[0]; - if (!this._get(section)) { + if (!this.wurd.store.get(section)) { console.warn(`Tried to access unloaded section: ${section}`); } } diff --git a/src/index.js b/src/index.js index 053cbac..0f7e91f 100644 --- a/src/index.js +++ b/src/index.js @@ -139,6 +139,14 @@ class Wurd { // Pass main content Block to callbacks if (onLoad) onLoad(content); + return content; + }) + .catch(err => { + if (debug) console.info('Wurd: load error:', err); + + // If content fails to load (wurd app offline), still return cache + if (onLoad) onLoad(content); + return content; }); } diff --git a/src/index.spec.js b/src/index.spec.js index f7142e7..3a344a0 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -87,7 +87,8 @@ describe('Wurd', function() { }); it('resolves the main content Block', function (done) { - client.load(['lorem', 'ipsum']) + Promise.resolve() + .then(() => client.load(['lorem', 'ipsum'])) .then(content => { test.ok(content instanceof Block); @@ -99,8 +100,9 @@ describe('Wurd', function() { }); it('loads content from cache and server', function (done) { - client.load(['lorem','ipsum','dolor','amet']) - .then(content => { + Promise.resolve() + .then(() => client.load(['lorem','ipsum','dolor','amet'])) + .then(async content => { // Should only call to server for missing sections same(client._fetchSections.callCount, 1); test.deepEqual(client._fetchSections.args[0][0], ['ipsum', 'amet']); @@ -127,7 +129,8 @@ describe('Wurd', function() { }); it('does not fetch from server if all content is available', function (done) { - client.load(['lorem', 'dolor']) + Promise.resolve() + .then(() => client.load(['lorem', 'dolor'])) .then(content => { // Should not call to server same(client._fetchSections.callCount, 0); @@ -150,8 +153,29 @@ describe('Wurd', function() { }).catch(done); }); + it('returns cached content if loading fails', function (done) { + client._fetch.rejects(new Error('timeout')); + Promise.resolve() + .then(() => client.load(['lorem','ipsum','dolor','amet'])) + .then(content => { + same(client._fetchSections.callCount, 1); + + test.deepEqual(content.get(), { + lorem: { title: 'Lorem' }, + dolor: { title: 'Dolor' }, + }); + + // Should pass the main content Block to the onLoad() callback + same(client.onLoad.callCount, 1); + same(client.onLoad.args[0][0], content); + + done(); + }).catch(done); + }); + it('works with an array of sectionNames', function (done) { - client.load(['lorem', 'ipsum']) + Promise.resolve() + .then(() => client.load(['lorem', 'ipsum'])) .then(content => { test.deepEqual(content.get('lorem.title'), 'Lorem'); test.deepEqual(content.get('ipsum.title'), 'Ipsum'); @@ -166,7 +190,8 @@ describe('Wurd', function() { }) it('works with a comma separated string', function (done) { - client.load('dolor,amet') + Promise.resolve() + .then(() => client.load('dolor,amet')) .then(content => { test.deepEqual(content.get('dolor.title'), 'Dolor'); test.deepEqual(content.get('amet.title'), 'Amet');