/**
*
* ## Kiss application manager
*
* This module allows to:
* - define and access **models**
* - define and access **collections**
* - define **views**
* - define **view controllers**
* - **init** the application using kiss.app.init()
*
* Once the models and collections are defined, they are stored in the **app** object.
* ```
* // Getting models and collections
* const appModels = kiss.app.models
* const appCollections = kiss.app.collections
*
* // Getting a model definition or a collection
* const userModel = appModels.user
* const userCollection = appCollections.user
*
* // Using the model
* const Bob = userModel.create({firstName: "Bob", lastName: "Wilson"})
* await Bob.save()
*
* // Using the collection if you're not sure it's loaded in memory (async method)
* const John = await userCollection.findOne("123")
*
* // Using the collection if it's already loaded in memory (sync method)
* const Will = userCollection.getRecord("456")
* ```
*
* @namespace
*/
kiss.app = {
// Reserved namespace for form templates
formTemplates: {},
// Reserved namespace for view templates
viewTemplates: {},
/**
* Store the application models.
* [More about models here.](kiss.data.Model.html)
*
* @example
* const userModel = kiss.app.models.user
*/
models: {},
/**
* Store the application collections.
* [More about collections here.](kiss.data.Collection.html)
*
* @example
* const userCollection = kiss.app.collections.user
*/
collections: {},
/**
* Define all the application texts that can be translated.
* Each text will be used as an English text if it doesn't have any english translation.
*
* @param {object} texts
*
* @example
* kiss.app.defineTexts({
* "hello": {
* fr: "bonjour"
* },
* "#thank": {
* en: "thank you",
* fr: "merci"
* }
* })
*/
defineTexts(texts) {
if (texts) {
Object.assign(kiss.language.texts, texts)
log.info(`kiss.language - Loaded ${Object.keys(texts).length} translated texts`)
}
},
/**
* Define a new model in the application
*
* This automatically:
* - references the model in **kiss.app.models**
* - references a new collection for the model in **kiss.app.collections**
* - references a new record class to instanciate the model
* - generates the basic api for the record class (create, save, update, delete)
* - generates getters/setters for record's computed fields (= virtual fields)
*
* Check the class [Model documentation](kiss.data.Model.html) for more informations about models.
*
* @param {object} model - The model configuration object
* @returns {Model} The newly defined model
*/
defineModel(model) {
if (kiss.app.models[model.id]) return kiss.app.models[model.id]
return new kiss.data.Model(model)
},
/**
* Define the model relationships
*
* This methods explores all the application models and finds automatically the relationships between the models.
* When exploring the models, specific field types are generating the relationships:
* - **link**: field used to link one or many foreign records
* - **lookup**: field that looks up a field in a foreign record
* - **summary**: field that looks up and summarizes data from multiple foreign records
*
* @example
* kiss.app.defineModelRelationships()
*/
defineModelRelationships() {
Object.values(kiss.app.models).forEach(model => model._defineRelationships())
},
/**
* Get a Model by id or by name
*
* Note: if the model is not found, the methods tries to find the model by its name
*
* @param {string} modelId - id or name (case insensitive) of the model
* @returns {object} Model
*/
getModel(modelId) {
const model = kiss.app.models[modelId]
if (model) return model
return kiss.app.getModelByName(modelId)
},
/**
* Get a Model by name
*
* Models are normally retrieved by id like this:
* ```
* const projectModel = kiss.app.models[modelId]
* const projectRecord = projectModel.create({projectName: "foo"})
* ```
*
* In some situation, we just know the model name and have to use this method:
* ```
* const projectModel = kiss.app.getModelByName("Project")
* const projectRecord = projectModel.create({projectName: "foo"})
* ```
*
* @param {string} modelName - The name of the model (case insensitive)
* @returns {object} Model
*/
getModelByName(modelName) {
const model = Object.values(kiss.app.models).find(model => (model.name.toLowerCase() == modelName.toLowerCase()))
return model
},
/**
* Get a Collection by its model's name
*
* Collections are normally retrieved by id like this:
* ```
* const projectCollection = kiss.app.collections[collectionId]
* const projects = await projectCollection.find()
* ```
*
* In some situation, we just know the name of the collection's model, so we have to use this method:
* ```
* const projectCollection = kiss.app.getCollectionByModelName("Project")
* const projects = await projectCollection.find()
* ```
*
* @param {string} modelName - The name of the collection's model (case insensitive)
* @returns {object} Collection
*/
getCollectionByModelName(modelName) {
return Object.values(kiss.app.collections).find(collection => (collection.model.name.toLowerCase() == modelName.toLowerCase()))
},
/**
* List all the application collections.
* Give their name and number of records.
*/
listCollections() {
Object.values(kiss.app.collections).forEach(collection => {
console.log(`id: ${collection.id}, name: ${collection.model.name}, records: ${collection.records.length}`)
})
},
/**
* Define a view by storing its renderer function into the list of view renderers.
* It does NOT store a view, but instead stores a view 'renderer' function that will generate the view later, when needed.
*
* @param {object} config
* @param {string} config.id - The id of the view to add
* @param {function} config.renderer - The function that will render the view when needed
* @param {object} config.meta - Meta informations injected in the HTML header. Can be localized or not. See examples in kiss.views module.
*
* @example
* kiss.app.defineView({
* id: "home",
* renderer: function (id, target) {
* // ... build your view here
* return createPanel({
*
* id, // Very important. Can't work without it.
* target, // Optional insertion point in the DOM. You can omit if you don't need it.
*
* title: "My first panel",
*
* // A few panel properties
* draggable: true,
* closable: true,
* width: 300,
* height: 200,
* boxShadow: "5px 5px 10px #cccccc",
*
* // Panel content
* items: [
* {
* type: "html",
* html: "<center>Hello world?</center>",
*
* // W3C events attached to the html element
* // It works only if you've attached the "hello" viewController (see below)
* events: {
* onclick: function() {
* $(id).hello()
* }
* }
* }
* ]
* })
* }
* })
*/
defineView({
id,
renderer,
meta
}) {
kiss.views.addView({
id,
renderer,
meta
})
},
/**
* Define a controller for a specific view
*
* The view controller must have the same name as the controlled view.
* They will be paired automatically.
*
* @param {string} id
* @param {object} viewController - Object containing all the controller methods
*
* @example
* // This controller has 4 methods, hello(), world(), foo() and bar()
* kiss.app.defineViewController("home", {
*
* hello: function() {
* createNotification({
* message: "Hello!",
* duration: 2000
* })
* },
*
* // ... or using an arrow function:
* world: () => console.log("World!"),
*
* // ... or class member notation:
* foo() {
* console.log("Foo!")
* },
*
* // Methods can be async, too:
* async bar() {
* return await 42
* }
* })
*/
defineViewController(id, viewController) {
kiss.views.addViewController(id, viewController)
},
/**
* Add a plugin definition to the application
*
* @param {object} plugin
*/
definePlugin(plugin) {
kiss.plugins.add(plugin)
},
/**
* Init KissJS application
*
* @async
* @param {object} config - The application configuration object
* @param {string} [config.name] - Optional application name (will be stored in kiss.app.name)
* @param {string} [config.logo] - Optional application logo (will be stored in kiss.app.logo and use in login screens)
* @param {string} [config.mode] - "online", "offline", "memory". Default is "online". Don't use "online" for local projects.
* @param {string} [config.host] - The host for online requests. Can be localhost or "" in developement.
* @param {boolean} [config.https] - Set to false if the application doesn't use https. Default is true. Ignored for "memory" or "offline" modes.
* @param {string[]} [config.loginMethods] - The list of login methods to use. Default is ["internal", "google", "microsoft365"]
* @param {string|object} [config.startRoute] - The route to start with. Can be a string (= viewId) or an object (check router documentation).
* @param {string[]} [config.publicRoutes] - The list of public routes which doesn't require authentication
* @param {object} [config.undoRedo] - The undo/redo configuration object
* @param {function} [config.loader] - The async function used to load your custom resources at startup. Must *absolutely* return a boolean to indicate success.
* @param {boolean} [config.useDirectory] - Set to true if your app uses KissJS directory to manage users, groups and apiClients. Default is false.
* @param {boolean} [config.useDynamicModels] - Set to true if your app needs dynamic models. Default is false.
* @param {boolean} [config.useFormPlugins] - Set to true if your app needs form plugins. Default is false.
* @param {object} [config.theme] - The theme to use. Ex: {color: "light", geometry: "sharp"}
* @param {string} [config.language] - "en", "fr" or "es". Default is "en" or the last language used by the user.
* @param {boolean} [config.debug] - Enable debug mode if true (default is false)
*
* @example
* await kiss.app.init({
* debug: true,
* name: "pickaform",
* language: "fr",
* logo: "./resources/img/logo 256x128.png",
* mode: "online",
* https: true,
* useDirectory: true,
* useDynamicModels: true,
* useFormPlugins: true,
* startRoute: "home-start",
* publicRoutes: [
* "form-public"
* ],
* undoRedo: {
* async undo() {
* // Undo code here
* },
* async redo() {
* // Redo code here
* }
* },
* theme: {
* color: "light",
* geometry: "sharp"
* },
* loader: async function() {
* // Load your resources here
* return true // IMPORTANT: return true if everything is loaded correctly, false otherwise
* }
* })
*/
async init(config) {
if (!config) return false
kiss.app.name = config.name
kiss.app.logo = config.logo
kiss.app.useDirectory = !!config.useDirectory
kiss.app.useDynamicModels = !!config.useDirectory
kiss.app.useFormPlugins = !!config.useFormPlugins
kiss.app.loader = config.loader
// Init texts
await kiss.language.init()
// Init plugins texts once the language is set
kiss.plugins.initTexts()
// Init global mode and database mode:
// - the mode automatically switch depending on the html file used to start the application
// - by default, starting with **index.html** will work online
// - demo.html is used to show application templates in memory (no server resource consumption)
// - memory.html is to test the application locally without saving anything: a browser refresh will wipe data
// - offline.html is to save the data inside the browser
const location = window.location.pathname
if (location.includes("demo")) {
kiss.global.mode = "demo"
kiss.db.setMode("memory")
} else if (location.includes("memory")) {
kiss.global.mode = "memory"
kiss.db.setMode("memory")
} else if (location.includes("offline")) {
kiss.global.mode = "offline"
kiss.db.setMode("offline")
} else {
kiss.global.mode = config.mode || "online"
kiss.db.setMode(kiss.global.mode)
}
// Init KissJS logger
let categories = ["😘"]
if (config.debug) {
categories = categories.concat([
"*",
// "kiss.ajax",
// "kiss.session",
// "kiss.websocket",
// "kiss.pubsub",
// "kiss.db",
// "kiss.data.Model",
// "kiss.data.Record",
// "kiss.data.Collection",
// "kiss.data.Transaction",
// "kiss.data.trash",
// "kiss.acl",
// "kiss.ui",
// "kiss.views",
// "kiss.language",
// "kiss.plugins"
])
}
kiss.logger.init({
data: true,
types: [0, 1, 2, 3, 4],
categories
})
// Init undo/redo
if (config.undoRedo) kiss.undoRedo.init(config.undoRedo)
// Init the theme
if (config.theme) kiss.theme.set(config.theme)
kiss.theme.init()
// Init screen size listener
kiss.screen.init()
// Init the application router
kiss.router.init()
if (config.publicRoutes) {
if (Array.isArray(config.publicRoutes) && config.publicRoutes.length > 0) {
kiss.router.addPublicRoutes(config.publicRoutes)
}
}
if (config.routerGuards) {
if (Array.isArray(config.routerGuards) && config.routerGuards.length > 0) {
kiss.router.addRoutingGuards(config.routerGuards)
}
}
if (config.routerActions) {
if (Array.isArray(config.routerActions) && config.routerActions.length > 0) {
kiss.router.addRoutingActions(config.routerActions)
}
}
// Get the requested route
if (config.startRoute) {
let route = config.startRoute
if (typeof route === "string") {
route = {
ui: config.startRoute
}
}
kiss.router.updateUrlHash(route, true)
}
const newRoute = kiss.router.getRoute()
// Init host
if (config.host) {
let host = config.host
// Session host
if (typeof host === "string") {
host = {
host: config.host
}
}
kiss.session.setHost(host)
// Ajax host
kiss.session.secure = (config.https === false) ? false : true
let ajaxHost = kiss.session.getHttpHost()
kiss.ajax.setHost(ajaxHost)
}
if (config.loginMethods) {
if (Array.isArray(config.loginMethods) && config.loginMethods.length > 0) {
kiss.session.setLoginMethods(config.loginMethods)
}
}
if (!kiss.router.isPublicRoute()) await kiss.session.restore()
// Jump to the first route
kiss.router.navigateTo(newRoute)
// Remove the splash screen, if any
if ($("splash")) $("splash").remove()
// Welcome message
console.log("😘 Powered with ❤ by KissJS, Keep It Simple Stupid Javascript (version " + kiss.version + ")")
},
/**
* Load core application data:
* - load the directory
* - load dynamic models
* - define model relationships
* - load links between records
* - load form plugins
*
* @returns {boolean} - True if the core application data could be loaded properly, false otherwise
*/
async load() {
// Load the directory
if (kiss.app.useDirectory) {
let success = await kiss.app.loadDirectory()
if (!success) return false
}
// Load dynamic models
if (kiss.app.useDynamicModels) {
let success = await kiss.app.loadDynamicModels()
if (!success) return false
}
// Discover model relationships dynamically
kiss.app.defineModelRelationships()
// Load links between records
await kiss.app.collections.link.find()
// Load the form plugins
if (kiss.app.useFormPlugins) {
await kiss.plugins.init()
}
return true
},
/**
* Load the application directory (users, groups, apiClients)
* @returns {boolean} - True if the directory is loaded, false otherwise
*/
async loadDirectory() {
return await kiss.directory.init()
},
/**
* Load the dynamic models.
* Dynamic models are created by the users and have an unpredictable schema.
*
* @returns {boolean} - True if the dynamic models are loaded, false otherwise
*/
async loadDynamicModels() {
if (!await kiss.app.collections.model) return true
const models = await kiss.app.collections.model.find()
// Exit if error (meaning the user is not properly logged in)
if (!models) return false
models.forEach(model => {
if (model.items) kiss.app.defineModel(model)
})
// React to the creation of new models
kiss.pubsub.subscribe("EVT_DB_INSERT:MODEL", msgData => {
kiss.app.defineModel(msgData.data)
})
return true
}
}
;
Source