/** * parasails.js * (lightweight structures for apps with more than one page) * * v0.5.0 * * Copyright 2017, Mike McNeil (@mikermcneil) * MIT License * https://www.npmjs.com/package/parasails * https://sailsjs.com/support * * > Parasails is a tiny (but opinionated) and pipeline-agnostic wrapper * > around Vue.js, jQuery, and Lodash. */ (function(global, factory){ var Vue; var _; var VueRouter; var $; //˙°˚°·. //‡CJS ˚°˚°·˛ if (typeof exports === 'object' && typeof module !== 'undefined') { var _require = require;// eslint-disable-line no-undef var _module = module;// eslint-disable-line no-undef // required deps: Vue = _require('vue'); _ = _require('lodash'); // optional deps: try { VueRouter = _require('vue-router'); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } } try { $ = _require('jquery'); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') {/* ok */} else { throw e; } } // export: _module.exports = factory(Vue, _, VueRouter, $); } //˙°˚°· //‡AMD ˚¸ else if(typeof define === 'function' && define.amd) {// eslint-disable-line no-undef // Register as an anonymous module. define([], function () {// eslint-disable-line no-undef // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: maybe use optional dep. loading here instead? // e.g. `function('vue', 'lodash', 'vue-router', 'jquery')` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // required deps: if (!global.Vue) { throw new Error('`Vue` global does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the Vue.js library is getting brought in before `parasails`.)'); } Vue = global.Vue; if (!global._) { throw new Error('`_` global does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the Lodash library is getting brought in before `parasails`.)'); } _ = global._; // optional deps: VueRouter = global.VueRouter || undefined; $ = global.$ || global.jQuery || undefined; // So... there's not really a huge point to supporting AMD here-- // except that if you're using it in your project, it makes this // module fit nicely with the others you're using. And if you // really hate globals, I guess there's that. // ¯\_(ツ)_/¯ return factory(Vue, _, VueRouter, $); });//ƒ } //˙°˚˙°· //‡NUDE ˚°·˛ else { // required deps: if (!global.Vue) { throw new Error('`Vue` global does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the Vue.js library is getting brought in before `parasails`.)'); } Vue = global.Vue; if (!global._) { throw new Error('`_` global does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the Lodash library is getting brought in before `parasails`.)'); } _ = global._; // optional deps: VueRouter = global.VueRouter || undefined; $ = global.$ || global.jQuery || undefined; // export: if (global.parasails) { throw new Error('Conflicting global (`parasails`) already exists!'); } global.parasails = factory(Vue, _, VueRouter, $); } })(this, function (Vue, _, VueRouter, $){ // ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // // ███████╗████████╗ █████╗ ████████╗███████╗ // ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝ // ███████╗ ██║ ███████║ ██║ █████╗ // ╚════██║ ██║ ██╔══██║ ██║ ██╔══╝ // ███████║ ██║ ██║ ██║ ██║ ███████╗ // ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // /** * Module state */ // Keep track of whether or not a page script has already been loaded in the DOM. var didAlreadyLoadPageScript; // The variable we'll be exporting. var parasails; // ██████╗ ██████╗ ██╗██╗ ██╗ █████╗ ████████╗███████╗ // ██╔══██╗██╔══██╗██║██║ ██║██╔══██╗╚══██╔══╝██╔════╝ // ██████╔╝██████╔╝██║██║ ██║███████║ ██║ █████╗ // ██╔═══╝ ██╔══██╗██║╚██╗ ██╔╝██╔══██║ ██║ ██╔══╝ // ██║ ██║ ██║██║ ╚████╔╝ ██║ ██║ ██║ ███████╗ // ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // // ██╗ ██╗████████╗██╗██╗ ███████╗ // ██║ ██║╚══██╔══╝██║██║ ██╔════╝ // ██║ ██║ ██║ ██║██║ ███████╗ // ██║ ██║ ██║ ██║██║ ╚════██║ // ╚██████╔╝ ██║ ██║███████╗███████║ // ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ // /** * Module utilities (private) */ function _ensureGlobalCache(){ parasails._cache = parasails._cache || {}; } function _exportOnGlobalCache(moduleName, moduleDefinition){ _ensureGlobalCache(); if (parasails._cache[moduleName]) { throw new Error('Something else (e.g. a utility or constant) has already been registered under that name (`'+moduleName+'`)'); } parasails._cache[moduleName] = moduleDefinition; } function _exposeJQueryPoweredMethods(def, currentModuleEntityNoun){ if (!currentModuleEntityNoun) { throw new Error('Consistency violation: Bad internal usage. '); } if (def.methods && def.methods.$get) { throw new Error('This '+currentModuleEntityNoun+' contains `methods` with a `$get` key, but you\'re not allowed to override that'); } if (def.methods && def.methods.$find) { throw new Error('This '+currentModuleEntityNoun+' contains `methods` with a `$find` key, but you\'re not allowed to override that'); } if (def.methods && def.methods.$focus) { throw new Error('This '+currentModuleEntityNoun+' contains `methods` with a `$focus` key, but you\'re not allowed to override that'); } def.methods = def.methods || {}; if ($) { def.methods.$get = function (){ return $(this.$el); }; def.methods.$find = function (subSelector){ return $(this.$el).find(subSelector); }; def.methods.$focus = function (subSelector){ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: If the current Vue thing hasn't mounted yet, throw an error. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var $fieldToAutoFocus = $(this.$el).find(subSelector); if ($fieldToAutoFocus.length === 0) { throw new Error('Could not autofocus-- no such element exists within this '+currentModuleEntityNoun+'.'); } if ($fieldToAutoFocus.length > 1) { throw new Error('Could not autofocus `'+subSelector+'`-- too many elements matched!'); } $fieldToAutoFocus.focus(); }; } else { def.methods.$get = function (){ throw new Error('Cannot use .$get() method because, at the time when this '+currentModuleEntityNoun+' was registered, jQuery (`$`) did not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure jQuery is getting brought in before `parasails`.)'); }; def.methods.$find = function (){ throw new Error('Cannot use .$find() method because, at the time when this '+currentModuleEntityNoun+' was registered, jQuery (`$`) did not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure jQuery is getting brought in before `parasails`.)'); }; def.methods.$focus = function (){ throw new Error('Cannot use .$focus() method because, at the time when this '+currentModuleEntityNoun+' was registered, jQuery (`$`) did not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure jQuery is getting brought in before `parasails`.)'); }; } } function _wrapMethodsAndVerifyNoArrowFunctions(def){ def.methods = def.methods || {}; _.each(_.keys(def.methods), function (methodName) { if (!_.isFunction(def.methods[methodName])) { throw new Error('Unexpected definition for Vue method `'+methodName+'`. Expecting a function, but got "'+def.methods[methodName]+'"'); } var isArrowFunction; try { var asString = def.methods[methodName].toString(); isArrowFunction = asString.match(/^\s*\(\s*/) || asString.match(/^\s*async\s*\(\s*/); } catch (err) { console.warn('Consistency violation: Encountered unexpected error when attempting to verify that Vue method `'+methodName+'` is not an arrow function. (What browser is this?!) Anyway, error details:', err); } if (isArrowFunction) { throw new Error('Unexpected definition for Vue method `'+methodName+'`. Vue methods cannot be specified as arrow functions, because then you wouldn\'t have access to `this` (i.e. the Vue vm instance). Please use a function like `function(){…}` instead.'); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: // Inject a wrapper function in order to provide more advanced / cleaner error handling. // (especially for AsyncFunctions) // ``` // var _originalMethod = def.methods[methodName]; // def.methods[methodName] = function(){ // // var rawResult; // var originalCtx = this; // (function(proceed){ // if (_originalMethod.constructor.name === 'AsyncFunction') { // rawResult = _originalMethod.apply(originalCtx, arguments); // // The result of an AsyncFunction is always a promise: // rawResult.catch(function(err) { // proceed(err); // });//_∏_ // rawResult.then(function(actualResult){ // return proceed(undefined, actualResult); // }); // } // else { // try { // rawResult = _originalMethod.apply(originalCtx, arguments); // } catch (err) { return proceed(err); } // return proceed(undefined, rawResult); // } // })(function(err, actualResult){//eslint-disable-line no-unused-vars // if (err) { // // FUTURE: perform more advanced error handling here // throw err; // } // // // Otherwise do nothing. // // });//_∏_ (†) // // // For compatibility, return the raw result. // return rawResult; // // };//ƒ // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - });//∞ } // ███████╗██╗ ██╗██████╗ ██████╗ ██████╗ ████████╗███████╗ // ██╔════╝╚██╗██╔╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝ // █████╗ ╚███╔╝ ██████╔╝██║ ██║██████╔╝ ██║ ███████╗ // ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║██╔══██╗ ██║ ╚════██║ // ███████╗██╔╝ ██╗██║ ╚██████╔╝██║ ██║ ██║ ███████║ // ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ // /** * Module exports */ parasails = {}; /** * registerUtility() * * Build a callable utility function, then attach it to the global namespace * so that it can be accessed later via `.require()`. * * @param {String} utilityName * @param {Function} def */ parasails.registerUtility = function(utilityName, def){ // Usage if (!utilityName) { throw new Error('1st argument (utility name) is required'); } if (!def) { throw new Error('2nd argument (utility function definition) is required'); } if (!_.isFunction(def)) { throw new Error('2nd argument (utility function definition) should be a function'); } // Build callable utility // > FUTURE: also support machine defs? var callableUtility = def; callableUtility.name = utilityName; // Attach to global cache _exportOnGlobalCache(utilityName, callableUtility); }; /** * registerConstant() * * Attach a constant to the global namespace so that it can be accessed * later via `.require()`. * * @param {String} constantName * @param {Ref} value */ parasails.registerConstant = function(constantName, value){ // Usage if (!constantName) { throw new Error('1st argument (constant name) is required'); } if (value === undefined) { throw new Error('2nd argument (the constant value) is required'); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: deep-freeze constant, if supported // (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Attach to global cache _exportOnGlobalCache(constantName, value); }; /** * registerComponent() * * Define a Vue component. * * @param {String} componentName * @param {Dictionary} def * * @returns {Ref} [new vue component for this page] */ parasails.registerComponent = function(componentName, def){ // Expose extra methods on component def, if jQuery is available. _exposeJQueryPoweredMethods(def, 'component'); // Make sure none of the specified Vue methods are defined with any naughty arrow functions. _wrapMethodsAndVerifyNoArrowFunctions(def); Vue.component(componentName, def); }; /** * require() * * Require a utility function or constant from the global namespace. * * @param {String} moduleName * @returns {Ref} [e.g. the callable utility function, or the value of the constant] * @throws {Error} if no such module has been registered */ parasails.require = function(moduleName) { // Usage if (!moduleName) { throw new Error('1st argument (module name -- i.e. the name of a utility or constant) is required'); } // Fetch from global cache _ensureGlobalCache(); if (parasails._cache[moduleName] === undefined) { var err = new Error('No utility or constant is registered under that name (`'+moduleName+'`)'); err.name = 'RequireError'; err.code = 'MODULE_NOT_FOUND'; throw err; } return parasails._cache[moduleName]; }; /** * registerPage() * * Define a page script, if applicable for the current contents of the DOM. * * @param {String} pageName * @param {Dictionary} def * * @returns {Ref} [new vue app thing for this page] */ parasails.registerPage = function(pageName, def){ // Usage if (!pageName) { throw new Error('1st argument (page name) is required'); } if (!def) { throw new Error('2nd argument (page script definition) is required'); } // Only actually build+load this page script if it is relevant for the current contents of the DOM. if (!document.getElementById(pageName)) { return; }//eslint-disable-line no-undef // Spinlock if (didAlreadyLoadPageScript) { throw new Error('Cannot load page script (`'+pageName+') because a page script has already been loaded on this page.'); } didAlreadyLoadPageScript = true; // Automatically set `el` if (def.el) { throw new Error('Page script definition contains `el`, but you\'re not allowed to override that'); } def.el = '#'+pageName; // Expose extra methods, if jQuery is available. _exposeJQueryPoweredMethods(def, 'page script'); // Make sure none of the specified Vue methods are defined with any naughty arrow functions. _wrapMethodsAndVerifyNoArrowFunctions(def); // Automatically attach `pageName` to `data`, for convenience. if (def.data && def.data.pageName) { throw new Error('Page script definition contains `data` with a `pageName` key, but you\'re not allowed to override that'); } def.data = _.extend({ pageName: pageName }, def.data||{}); // Attach `goto` method, for convenience. if (def.methods && def.methods.goto) { throw new Error('Page script definition contains `methods` with a `goto` key-- but you\'re not allowed to override that'); } def.methods = def.methods || {}; if (VueRouter) { def.methods.goto = function (rootRelativeUrlOrOpts){ return this.$router.push(rootRelativeUrlOrOpts); }; } else { def.methods.goto = function (){ throw new Error('Cannot use .goto() method because, at the time when this page script was registered, VueRouter did not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure VueRouter is getting brought in before `parasails`.)'); }; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Make sure we didn't type "beforeMounted" or "beforeDestroyed" because those aren't real things // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // If virtualPages was specified, check usage and then... if (def.virtualPages && def.router) { throw new Error('Cannot specify both `virtualPages` AND an actual Vue `router`! Use one or the other.'); } if (def.router && !VueRouter) { throw new Error('Cannot use `router`, because that depends on the Vue Router. But `VueRouter` does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the VueRouter plugin is getting brought in before `parasails`.)'); } if (!def.virtualPages && def.html5HistoryMode !== undefined) { throw new Error('Cannot specify `html5HistoryMode` without also specifying `virtualPages`!'); } if (!def.virtualPages && def.beforeEach !== undefined) { throw new Error('Cannot specify `beforeEach` without also specifying `virtualPages`!'); } if ((def.beforeNavigate || def.afterNavigate) && def.virtualPages !== true) { throw new Error('Cannot specify `beforeNavigate` or `afterNavigate` unless you set `virtualPages: true`!'); } if (def.virtualPages) { if (!VueRouter) { throw new Error('Cannot use `virtualPages`, because it depends on the Vue Router. But `VueRouter` does not exist on the page yet. (If you\'re using Sails, please check dependency loading order in pipeline.js and make sure the VueRouter plugin is getting brought in before `parasails`.)'); } // Now we'll replace `virtualPages` in our def with the thing that VueRouter actually expects: // If `virtualPages: true` was specified, then use reasonable defaults: // // > Note: This assumes that, somewhere within the parent page's template, there is: // > ``` // > // > ``` if (def.virtualPages === true) { if (def.beforeEach !== undefined) { throw new Error('Cannot specify `virtualPages: true` AND `beforeEach` at the same time!'); } if (!def.virtualPagesRegExp && def.html5HistoryMode === 'history') { throw new Error('If `html5HistoryMode: \'history\'` is specified, then virtualPagesRegExp must also be specified!'); } if (def.virtualPagesRegExp && !_.isRegExp(def.virtualPagesRegExp)) { throw new Error('Invalid `virtualPagesRegExp`: If specified, this must be a regular expression -- e.g. `/^\/manage\/access\/?([^\/]+)?/`'); } // Check for element // (to provide a better error msg if it was omitted) var customBeforeMountLC; if (def.beforeMount) { customBeforeMountLC = def.beforeMount; }//fi def.beforeMount = function(){ // Inject additional code to check for element: // console.log('this.$find(\'router-view\').length', this.$find('router-view').length); if (this.$find('router-view').length === 0) { throw new Error( 'Cannot mount this page with `virtualPages: true` because no '+ ' element exists in this page\'s HTML.\n'+ 'Please be sure the HTML includes:\n'+ '\n'+ '```\n'+ '\n'+ '```\n' ); }//• // Then call the original, custom "beforeMount" function, if there was one. if (customBeforeMountLC) { customBeforeMountLC.apply(this, []); } };//ƒ if (def.methods._navigate) { throw new Error('Could not use `virtualPages: true`, because a conflicting `_navigate` method is defined. Please remove it, or do something else.'); } // Set up local variables to refer to things in `def`, since it will be changing below. var pathMatchingRegExp; if (def.html5HistoryMode === 'history') { pathMatchingRegExp = def.virtualPagesRegExp; } else { pathMatchingRegExp = /.*/; } var beforeNavigate = def.beforeNavigate; var afterNavigate = def.afterNavigate; // Now modify the definition's methods and remove all relevant top-level props understood // by parasails (but not by Vue.js) to avoid creating any weird additional dependence on // parasails features beyond the expected usage. def.methods = _.extend(def.methods||{}, { _navigate: function(virtualPageSlug){ if (beforeNavigate) { var doCancelNavigate = beforeNavigate.apply(this, [ virtualPageSlug ]); if (doCancelNavigate === false) { return; }//• } this.virtualPageSlug = virtualPageSlug; // console.log('navigate! Got:', arguments); // console.log('Navigated. (Set `this.virtualPageSlug=\''+virtualPageSlug+'\'`)'); if (afterNavigate) { afterNavigate.apply(this, [ virtualPageSlug ]); } } }); def = _.extend({ router: new VueRouter({ mode: def.html5HistoryMode || 'hash', routes: [ { path: '*', component: (function(){ var vueComponentDef = { render: function(){}, beforeRouteUpdate: function (to,from,next){ // this.$emit('navigate', to.path); <