Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • oss/minerva/minerva-app
1 result
Show changes
Commits on Source (3)
Showing
with 2815 additions and 66 deletions
/development/build/
node_modules/
development/temp/
development/log/
/application/source/server/web/node_modules/
##################### IDE / Tools ##
# @see https://github.com/github/gitignore/
# PHPUnit
.phpunit.result.cache
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.cache
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Cloud9 IDE - http://c9.io
.c9revisions
.c9
##Exclipse
.metadata
#bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
#local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
.apt_generated_test/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# Uncomment this line if you wish to ignore the project description file.
# Typically, this file would be tracked if it contains build/dependency configurations:
#.project
##Kate
# Swap Files #
.*.kate-swp
.swp.*
## KDEDevelop
*.kdev4
.kdev4/
## LibreOffice
# LibreOffice locks
.~lock.*#
### LINUX
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
## Patches
*.orig
*.rej
# Private key
*.ppk
## Sublime
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
sftp-config-alt*.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
## VIM
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# gitignore template for AWS Serverless Application Model project
# website: https://docs.aws.amazon.com/serverless-application-model
# Ignore build folder
.aws-sam/
# Netbeans
**/nbproject/private/
**/nbproject/Makefile-*.mk
**/nbproject/Package-*.bash
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
## GOLANG
go.work
......@@ -3,7 +3,17 @@
<component name="Go" enabled="true" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/application/source" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/command" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/config" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/server" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/server/web" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/server/web/app" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/application/source/utils" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/development/examples" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/development/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
......
......@@ -2,7 +2,7 @@
<configuration default="false" name="minerva serve" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="minerva" />
<working_directory value="$PROJECT_DIR$/application/source" />
<parameters value="serve --config=../../development/examples/theme1/config.yaml" />
<parameters value="serve --config=../../development/examples/theme1/config.yaml --path=../../development/examples/theme1/" />
<kind value="DIRECTORY" />
<directory value="$PROJECT_DIR$/application/source" />
<filePath value="$PROJECT_DIR$" />
......
......@@ -30,4 +30,10 @@ func intiParser() {
config.InitializeConfigFromFile(def.Config)
}
cfg := config.GetConfiguration()
if def.Serve.Path != "" {
cfg.Server.Path = def.Serve.Path
}
}
......@@ -5,24 +5,6 @@ type options struct {
Version struct {
} `command:"version"`
Serve struct {
Path string `short:"p" long:"path" description:"Path to the project files"`
} `command:"serve"`
}
//// DebugLevel logs are typically voluminous, and are usually disabled in production.
// DebugLevel = zapcore.DebugLevel
//// InfoLevel is the default logging priority.
// InfoLevel = zapcore.InfoLevel
//// WarnLevel logs are more important than Info, but don't need individual
//// human review.
// WarnLevel = zapcore.WarnLevel
//// ErrorLevel logs are high-priority. If an application is running smoothly,
//// it shouldn't generate any error-level logs.
// ErrorLevel = zapcore.ErrorLevel
//// DPanicLevel logs are particularly important errors. In development the
//// logger panics after writing the message.
// DPanicLevel = zapcore.DPanicLevel
//// PanicLevel logs a message, then panics.
// PanicLevel = zapcore.PanicLevel
//// FatalLevel logs a message, then calls os.Exit(1).
// FatalLevel = zapcore.FatalLevel
// ALL > TRACE > DEBUG > INFO > WARN > ERROR > FATAL > OFF
......@@ -14,6 +14,7 @@ func Do() {
printVersion()
case "serve":
serve(active)
}
......
package config
import (
"github.com/creasty/defaults"
"github.com/kelseyhightower/envconfig"
"gitlab.schukai.com/oss/minerva/utils"
"gopkg.in/yaml.v3"
......@@ -8,24 +9,44 @@ import (
"path"
)
func GetServerPort() string {
return configuration.Server.Port
}
type Configuration struct {
//LogLevel string `yaml:"logLevel" envconfig:"LOG_LEVEL"`
Server struct {
Port string `yaml:"Port" envconfig:"SERVER_PORT" default:"80"`
Port int `yaml:"Port" envconfig:"SERVER_PORT" default:"8080"`
Path string `yaml:"Path" envconfig:"SERVER_PATH" default:""`
} `yaml:"Server"`
}
Paths struct {
Web string `yaml:"Web" envconfig:"PATH_WEB" default:"/srv/web/"`
} `yaml:"Paths"`
// SetDefaults implements defaults.Setter interface
func (s *Configuration) SetDefaults() {
if defaults.CanUpdate(s.Server.Path) {
path, err := os.Getwd()
if err != nil {
utils.PrintErrorAndExit("something went wrong: %s", err.Error())
}
s.Server.Path = path
}
}
func GetServerPort() int {
return configuration.Server.Port
}
func GetServerPath() string {
return configuration.Server.Path
}
var configuration *Configuration
func newConfiguration() *Configuration {
config := &Configuration{}
if err := defaults.Set(config); err != nil {
utils.PrintErrorAndExit("we have a problem: %s", err.Error())
}
return config
}
func GetConfiguration() *Configuration {
if configuration == nil {
......@@ -63,7 +84,7 @@ func intiConfiguration() {
return
}
configuration = &Configuration{}
configuration = newConfiguration()
}
......@@ -98,10 +119,10 @@ func readFile(filename string) {
}
}()
c := Configuration{}
err = yaml.Unmarshal([]byte(data), &c)
c := newConfiguration()
err = yaml.Unmarshal([]byte(data), c)
configuration = &c
configuration = c
if err != nil {
utils.PrintErrorAndExit(err.Error())
......
......@@ -13,6 +13,7 @@ require (
)
require (
github.com/creasty/defaults v1.6.0 // indirect
github.com/go-chi/chi/v5 v5.0.7 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
......
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc=
github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
......
package server
import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"net/http"
)
func appHandler() http.Handler {
r := chi.NewRouter()
r.Use(middleware.NoCache)
r.Get("/editor", renderEditor)
r.Handle("/web/node_modules/*",
http.StripPrefix("/app", http.FileServer(http.FS(embeddedFiles))))
//r.Get("/node_modules/*", serveNodeModules)
//r.Route("/node_modules/*", http.StripPrefix("/assets/",
// http.FileServer(http.Dir("assets/"))))
//
//http.Handle("/assets/", http.StripPrefix("/assets/",
// http.FileServer(http.Dir("assets/"))))
//r.Post("/convert", doV1Convert)
//r.Post("/create", doV1Create)
return r
}
package server
import (
"net/http"
)
func serveNodeModules(w http.ResponseWriter, r *http.Request) {
content := lookUp(r.URL.Path)
if content != nil {
//w.Header().Add("Content-Type", "application/json")
//mimeType := ""
//
//ext := filepath.Ext(r.URL.Path)
//switch ext {
//case ".htm", ".html":
// return "text/html"
//case ".css":
// return "text/css"
//case ".js":
// return "application/javascript"
//
//
//
//filetype := http.DetectContentType(content)
//fmt.Println(filetype)
w.Write(content)
return
}
w.WriteHeader(404)
}
func renderEditor(w http.ResponseWriter, r *http.Request) {
content, err := embeddedFiles.ReadFile("web/app/editor.html")
if err != nil {
logError("something went wrong: %s", err.Error())
w.WriteHeader(500)
return
}
w.Write(content)
}
package server
import (
"net/http"
"path"
"strings"
)
//// FSHandler404 provides the function signature for passing to the FileServerWith404
//type handler404 = func(w http.ResponseWriter, r *http.Request) (doDefaultFileServe bool)
func fileServer(root string) http.Handler {
fs := http.FileServer(http.Dir(root))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//make sure the url path starts with /
upath := r.URL.Path
if !strings.HasPrefix(upath, "/") {
upath = "/" + upath
r.URL.Path = upath
}
upath = path.Clean(upath)
//
////// attempt to open the file via the http.FileSystem
////f, err := root.Open(upath)
////// close if successfully opened
////if f != nil {
//// defer f.Close()
////}
//if !utils.DirectoryExists(root) {
// if os.IsNotExist(err) {
// // call handler
// if h404 != nil {
// doDefault := h404(w, r)
// if !doDefault {
// return
// }
// }
// }
//}
//
//s, err := f.Stat()
//
//if err != nil {
// if os.IsNotExist(err) {
// // call handler
// if h404 != nil {
// doDefault := h404(w, r)
// if !doDefault {
// return
// }
// }
// }
//}
//
//if s.IsDir() && false {
// return
//}
// default serve
fs.ServeHTTP(w, r)
})
}
......@@ -8,6 +8,10 @@ import (
"github.com/go-chi/chi/v5/middleware"
"gitlab.schukai.com/oss/minerva/config"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"sync"
)
......@@ -17,26 +21,45 @@ var (
embeddedFiles embed.FS
)
func lookUp(path string) (content []byte) {
// Zuerst im Verzeichnis /web/asserts schauen (wird beim build inkludiert)
c, _ := embeddedFiles.ReadFile("web" + path)
func lookUp(p string) (content []byte) {
c, _ := embeddedFiles.ReadFile("web" + p)
if len(c) > 0 {
return c
}
base, err := filepath.Abs(config.GetServerPath())
if err != nil {
logError("%s", err.Error())
return nil
}
filename := path.Clean(path.Join(base, p))
data, _ := os.ReadFile(filename)
if len(data) > 0 {
return data
}
return nil
}
func serveStaticFiles(next http.Handler) func(http.ResponseWriter, *http.Request) {
//func serveStaticFiles() func(http.ResponseWriter, *http.Request) {
// return func(w http.ResponseWriter, r *http.Request) {
//
// content := lookUp(r.URL.Path)
// if content != nil {
// w.Write(content)
// return
// }
//
// w.WriteHeader(404)
//
// }
//}
func redirectToApp() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
content := lookUp(r.URL.Path)
if len(content) > 0 {
w.Write(content)
return
}
next.ServeHTTP(w, r)
http.Redirect(w, r, "app/editor", http.StatusTemporaryRedirect)
}
}
......@@ -57,21 +80,21 @@ func initRouting() *chi.Mux {
/** x509: certificate signed by unknown authority */
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
mux := chi.NewRouter()
router := chi.NewRouter()
mux.Use(waitGroupMiddleware())
router.Use(waitGroupMiddleware())
mux.Use(RequestIDMiddleware)
mux.Use(middleware.RealIP)
mux.Use(middleware.Heartbeat("/ping"))
router.Use(RequestIDMiddleware)
router.Use(middleware.RealIP)
router.Use(middleware.Heartbeat("/ping"))
mux.Use(middleware.Compress(5))
mux.Use(middleware.AllowContentType("application/json", "text/html"))
mux.Use(loggerMiddleware)
router.Use(middleware.Compress(5))
router.Use(middleware.AllowContentType("application/json", "text/html"))
router.Use(loggerMiddleware)
// mux.Mount("/api/v1", v1.V1Api(cfg))
router.Mount("/app", appHandler())
//mux.Get("/", func(w http.ResponseWriter, r *http.Request) {
//router.Get("/", func(w http.ResponseWriter, r *http.Request) {
// if negotiator.New(r.Header).Type("text/html") == "text/html" {
// http.Redirect(w, r, "https://www.alvine.cloud/en/products/media-services/juno/", http.StatusPermanentRedirect)
// } else {
......@@ -92,17 +115,30 @@ func initRouting() *chi.Mux {
// return nil
//}
//router.Get("/*", redirectToApp())
//
//router.Path("/whatever").Handler(func(writer http.ResponseWriter, req *http.Request) {
// http.Redirect(writer, req, "localhost:8080/whatever", http.StatusMovedPermanently)
// ))
//
//d := http.Dir(p)
//mux.Get("/*",
// serveStaticFiles(fileserver.FileServerWith404(d, func(w http.ResponseWriter, r *http.Request) (doDefaultFileServe bool) {
// error2.StandardErrorResponse(w, r, error2.ErrorResponse{
// Status: 404,
// })
// return false
// router.Get("/*",
//serveStaticFiles(
// func(w http.ResponseWriter, r *http.Request) (doDefaultFileServe bool) {
//
// }
//
// })))
// )
// //serveStaticFiles(fileServerWith404("/tmp", func(w http.ResponseWriter, r *http.Request) (doDefaultFileServe bool) {
// // error2.StandardErrorResponse(w, r, error2.ErrorResponse{
// // Status: 404,
// // })
// // return false
// //
// //})))
return mux
return router
}
......@@ -113,7 +149,7 @@ func Serve() {
c := config.GetConfiguration()
address := ":" + c.Server.Port
address := ":" + strconv.FormatInt(int64(c.Server.Port), 10)
logInfo("server listen on address %s", address)
server := http.Server{
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/app/web/node_modules/grapesjs/dist/css/grapes.min.css">
<link rel="stylesheet" href="/app/web/node_modules/grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css">
<script src="/app/web/node_modules/grapesjs/dist/grapes.min.js"></script>
<script src="/app/web/node_modules/grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.js"></script>
<style>
body,
html {<
height: 100%;
margin: 0;
overflow: hidden;
}
#toast-container {
font-size: 13px;
font-weight: lighter;
}
#toast-container > div {
opacity: 0.95;
}
#toast-container > div,
#toast-container > div:hover {
box-shadow: 0 0 12px rgba(0, 0, 0, 0.1);
font-family: Helvetica, sans-serif;
}
/* LOGO VERSION */
.gjs-pn-commands .gjs-pn-buttons,
#gjs-pn-commands .gjs-pn-buttons {
display: none;
}
.gjs-logo {
height: 25px;
}
.gjs-logo-cont {
position: relative;
display: inline-block;
top: 3px;
}
.gjs-logo-version {
position: absolute;
font-size: 10px;
padding: 1px 7px;
border-radius: 15px;
bottom: 2px;
right: -43px;
}
/* INFO PANEL */
.gjs-mdl-dialog-sm {
width: 300px;
}
#info-panel {
line-height: 17px;
}
.info-panel-logo {
display: block;
height: 90px;
margin: 0 auto;
width: 90px;
}
.info-panel-logo path {
stroke: #eee !important;
stroke-width: 8 !important;
}
.info-panel-label {
margin-bottom: 10px;
font-size: 13px;
}
.info-panel-link {
text-decoration: none;
}
/* ADS */
.gjs-pn-panel#gjs-pn-views-container,
.gjs-pn-panel.gjs-pn-views-container {
height: calc(100% - 150px);
}
.ad-cont {
position: absolute;
right: 0;
bottom: 0;
z-index: 2;
width: 15%;
height: 150px;
}
#carbonads {
font: caption;
padding: 20px 10px;
}
.carbon-link {
text-decoration: none;
font: caption;
}
.carbon-img {
float: right;
margin-left: 10px;
}
.carbon-img img {
border-radius: 3px;
max-width: 100px !important;
max-height: 77px;
}
.carbon-text {
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
font-weight: lighter;
}
.carbon-poweredby {
color: rgba(255, 255, 255, 0.55);
text-decoration: none;
float: right;
}
.carbon-cta-c {
text-align: right;
padding-top: 5px;
}
.carbon-cta {
display: inline-block;
padding: 4px 10px;
border-radius: 3px;
font-weight: bold;
font-size: 12px;
}
.gjs-block-label svg,
.gjs-block__media svg {
width: 54px;
}
/* Temporary fix #2490 */
.gjs-clm-tag-status,
.gjs-clm-tag-close {
width: 12px;
height: 12px;
}
.gjs-clm-tags-btn {
width: 24px;
}
</style>
</head>
<body>
<div id="gjs"></div>
<div id="blocks"></div>
<script type="text/javascript">
const editor = grapesjs.init({
// Indicate where to init the editor. You can also pass an HTMLElement
container: '#gjs',
// Get the content for the canvas directly from the element
// As an alternative we could use: `components: '<h1>Hello World Component!</h1>'`,
fromElement: true,
// Size of the editor
height: '300px',
width: 'auto',
// Disable the storage manager for the moment
storageManager: false,
// Avoid any default panel
panels: {defaults: []},
blockManager: {
appendTo: '#blocks',
blocks: [
{
id: 'section', // id is mandatory
label: '<b>Section</b>', // You can use HTML/SVG inside labels
attributes: {class: 'gjs-block-section'},
content: `<section>
<h1>This is a simple title</h1>
<div>This is just a Lorem text: Lorem ipsum dolor sit amet</div>
</section>`,
}, {
id: 'text',
label: 'Text',
content: '<div data-gjs-type="text">Insert your text here</div>',
}, {
id: 'image',
label: 'Image',
// Select the component once it's dropped
select: true,
// You can pass components as a JSON instead of a simple HTML string,
// in this case we also use a defined component type `image`
content: {type: 'image'},
// This triggers `active` event on dropped components and the `image`
// reacts by opening the AssetManager
activate: true,
}
]
},
});
</script>
</body>
</html>
{
"name": "minerva",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/backbone": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.3.3.tgz",
"integrity": "sha1-TMgOp8sWMaxHSInOQPL4vGg7KZk=",
"dependencies": {
"underscore": ">=1.8.3"
}
},
"node_modules/backbone-undo": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/backbone-undo/-/backbone-undo-0.2.5.tgz",
"integrity": "sha1-VbJSMPkDGcpiJGXomoAki4k8LOI=",
"dependencies": {
"backbone": "1.0.0 - 1.2.1",
"underscore": "1.4.4 - 1.8.3"
}
},
"node_modules/backbone-undo/node_modules/backbone": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.1.tgz",
"integrity": "sha1-1yGcXtSeXhMdv/ryXJbW0sw8oD4=",
"dependencies": {
"underscore": ">=1.7.0"
}
},
"node_modules/backbone-undo/node_modules/underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"node_modules/codemirror": {
"version": "5.65.3",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.3.tgz",
"integrity": "sha512-kCC0iwGZOVZXHEKW3NDTObvM7pTIyowjty4BUqeREROc/3I6bWbgZDA3fGDwlA+rbgRjvnRnfqs9SfXynel1AQ=="
},
"node_modules/codemirror-formatting": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/codemirror-formatting/-/codemirror-formatting-1.0.0.tgz",
"integrity": "sha1-h5zB/dkBg0PB1VEXac5TYNcF6/I="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/file-saver": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
"integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg=="
},
"node_modules/grapesjs": {
"version": "0.18.4",
"resolved": "https://registry.npmjs.org/grapesjs/-/grapesjs-0.18.4.tgz",
"integrity": "sha512-mOkmOc7Q9OyuW4H8h4NDhFK1cxBnw/En+sD60enOKQrNy5lfua6Lh5I0oTm93RU6ahQy2QPzn1ZbZWAQaDCKQw==",
"dependencies": {
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"codemirror": "^5.63.0",
"codemirror-formatting": "^1.0.0",
"promise-polyfill": "^8.1.3",
"underscore": "^1.13.1"
}
},
"node_modules/grapesjs-aviary": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/grapesjs-aviary/-/grapesjs-aviary-0.1.2.tgz",
"integrity": "sha512-F7sYQWj5s9eyUqoOX6q1z2J06AqgXhQqdjQhM8b5BptxlbDsvDTCbqSRmLGkEQKaIF1ca5o9FFi9eKBeUAfLEw==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-blocks-basic": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/grapesjs-blocks-basic/-/grapesjs-blocks-basic-0.1.8.tgz",
"integrity": "sha512-Iy5M4qGkEE2llb5ZFxitDolRJ3CrQkcK1SQIjRRlkWIAC99On3T/09iFR7NkpTBfsgfdNwcwnSVOMOhSnVLDtw==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-component-countdown": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/grapesjs-component-countdown/-/grapesjs-component-countdown-0.1.3.tgz",
"integrity": "sha512-TmxDETedUDv1jGqNLn6E768iyp+xAK/XcTId5WpLrmKPFB6aCuWqfTowhtSvyRJJs83OAFMiQIwIu+tEtSWOeA==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-navbar": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/grapesjs-navbar/-/grapesjs-navbar-0.1.5.tgz",
"integrity": "sha512-tVKPtzlTUNCpzk3lHW69II2NsSzLNQd3K32CXssWCP3M1V8fvc9jG/MQECBkd/gMS+DjldD4/4b00ri9F3tKSg==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-plugin-export": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/grapesjs-plugin-export/-/grapesjs-plugin-export-0.1.5.tgz",
"integrity": "sha512-BgvPY68vIDxVv0Z3v1TTikHseMxbfvUvhk5qUHq3a9kFmc2sFfNA6f5NHP9A4v3Tnd4kuwOd2aIn+QNutZrLRg==",
"dependencies": {
"file-saver": "^1.3.2",
"jszip": "^3.0.0"
},
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-plugin-filestack": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/grapesjs-plugin-filestack/-/grapesjs-plugin-filestack-0.1.1.tgz",
"integrity": "sha512-zRwgHUZd7qDVk92dtizOHxnPiCjBBmsA2BHm0J86tz8uiXf6ou0OoQiB4x4UDewb9ipTkBI6X5nGh/0YET6WLw==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-plugin-forms": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/grapesjs-plugin-forms/-/grapesjs-plugin-forms-0.3.6.tgz",
"integrity": "sha512-JGIxky3if7FLlXBDuX2T2UoWQnvm55PcNp5P2qoX64zlA0+2npz9zbPcj9g1PUHXVPlTXwTr2qu5x3U/pb+xdw==",
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/grapesjs-preset-webpage": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/grapesjs-preset-webpage/-/grapesjs-preset-webpage-0.1.11.tgz",
"integrity": "sha512-36SuSPwu3a09R6YR31Sklmowz/LvzxzGM74WcPd8KPwbs1mAQ4fFuqWKkX/wv8+GhY58Se9EO42yQVMwsZzELA==",
"dependencies": {
"grapesjs-aviary": "^0.1.2",
"grapesjs-blocks-basic": "^0.1.7",
"grapesjs-component-countdown": "^0.1.2",
"grapesjs-navbar": "^0.1.5",
"grapesjs-plugin-export": "^0.1.5",
"grapesjs-plugin-filestack": "^0.1.1",
"grapesjs-plugin-forms": "^0.3.5"
},
"peerDependencies": {
"grapesjs": "0.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/jszip": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz",
"integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/promise-polyfill": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz",
"integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg=="
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/underscore": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz",
"integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}
}
}
.DS_Store
node_modules
\ No newline at end of file
/*!
* Backbone.Undo.js v0.2
*
* Copyright (c)2013 Oliver Sartun
* Released under the MIT License
*
* Documentation and full license available at
* https://github.com/osartun/Backbone.Undo.js
*/
(function (factory) {
if (typeof define === "function" && define.amd) {
// AMD support
define(["underscore", "backbone"], factory);
} else if (typeof exports !== 'undefined') {
// CommonJS support
module.exports = factory(
require("underscore"),
require("backbone")
);
} else {
// Non-modular execution
factory(_, Backbone);
}
})(function (_, Backbone) {
var core_slice = Array.prototype.slice;
/**
* As call is faster than apply, this is a faster version of apply as it uses call.
*
* @param {Function} fn The function to execute
* @param {Object} ctx The context the function should be called in
* @param {Array} args The array of arguments that should be applied to the function
* @return Forwards whatever the called function returns
*/
function apply (fn, ctx, args) {
return args.length <= 4 ?
fn.call(ctx, args[0], args[1], args[2], args[3]) :
fn.apply(ctx, args);
}
/**
* Uses slice on an array or an array-like object.
*
* @param {Array|Object} arr The array or array-like object.
* @param {Number} [index] The index from where the array should be sliced. Default is 0.
* @return {Array} The sliced array
*/
function slice (arr, index) {
return core_slice.call(arr, index);
}
/**
* Checks if an object has one or more specific keys. The keys
* don't have to be an owned property.
* You can call this function either this way:
* hasKeys(obj, ["a", "b", "c"])
* or this way:
* hasKeys(obj, "a", "b", "c")
*
* @param {Object} obj The object to check on
* @param {Array} keys The keys to check for
* @return {Boolean} True, if the object has all those keys
*/
function hasKeys (obj, keys) {
if (obj == null) return false;
if (!_.isArray(keys)) {
keys = slice(arguments, 1);
}
return _.all(keys, function (key) {
return key in obj;
});
}
/**
* Returns a number that is unique per call stack. The number gets
* changed after the call stack has been completely processed.
*
* @return {number} MagicFusionIndex
*/
var getMagicFusionIndex = (function () {
// If you add several models to a collection or set several
// attributes on a model all in sequence and yet all for
// example in one function, then several Undo-Actions are
// generated.
// If you want to undo your last action only the last model
// would be removed from the collection or the last set
// attribute would be changed back to its previous value.
// To prevent that we have to figure out a way to combine
// all those actions that happened "at the same time".
// Timestamps aren't exact enough. A complex routine could
// run several milliseconds and in that time produce a lot
// of actions with different timestamps.
// Instead we take advantage of the single-threadedness of
// JavaScript:
var callstackWasIndexed = false, magicFusionIndex = -1;
function indexCycle() {
magicFusionIndex++;
callstackWasIndexed = true;
_.defer(function () {
// Here comes the magic. With a Timeout of 0
// milliseconds this function gets called whenever
// the current callstack is completed
callstackWasIndexed = false;
})
}
return function () {
if (!callstackWasIndexed) {
indexCycle();
}
return magicFusionIndex;
}
})();
/**
* To prevent binding a listener several times to one
* object, we register the objects in an ObjectRegistry
*
* @constructor
*/
function ObjectRegistry () {
// This uses two different ways of storing
// objects: In case the object has a cid
// (which Backbone objects typically have)
// it uses this cid as an index. That way
// the Array's length attribute doesn't
// change and the object isn't an item
// in the array, but an object-property.
// Otherwise it's added to the Array as an
// item.
// That way we can use the fast property-
// lookup and only have to fall back to
// iterating over the array in case
// non-Backbone-objects are registered.
this.registeredObjects = [];
// To return a list of all registered
// objects in the 'get' method we have to
// store the objects that have a cid in
// an additional array.
this.cidIndexes = [];
}
ObjectRegistry.prototype = {
/**
* Returns whether the object is already registered in this ObjectRegistry or not.
*
* @this {ObjectRegistry}
* @param {Object} obj The object to check
* @return {Boolean} True if the object is already registered
*/
isRegistered: function (obj) {
// This is where we get a performance boost
// by using the two different ways of storing
// objects.
return obj && obj.cid ? this.registeredObjects[obj.cid] : _.contains(this.registeredObjects, obj);
},
/**
* Registers an object in this ObjectRegistry.
*
* @this {ObjectRegistry}
* @param {Object} obj The object to register
* @return {undefined}
*/
register: function (obj) {
if (!this.isRegistered(obj)) {
if (obj && obj.cid) {
this.registeredObjects[obj.cid] = obj;
this.cidIndexes.push(obj.cid);
} else {
this.registeredObjects.push(obj);
}
return true;
}
return false;
},
/**
* Unregisters an object from this ObjectRegistry.
*
* @this {ObjectRegistry}
* @param {Object} obj The object to unregister
* @return {undefined}
*/
unregister: function (obj) {
if (this.isRegistered(obj)) {
if (obj && obj.cid) {
delete this.registeredObjects[obj.cid];
this.cidIndexes.splice(_.indexOf(this.cidIndexes, obj.cid), 1);
} else {
var i = _.indexOf(this.registeredObjects, obj);
this.registeredObjects.splice(i, 1);
}
return true;
}
return false;
},
/**
* Returns an array of all objects that are currently in this ObjectRegistry.
*
* @return {Array} An array of all the objects which are currently in the ObjectRegistry
*/
get: function () {
return (_.map(this.cidIndexes, function (cid) {return this.registeredObjects[cid];}, this)).concat(this.registeredObjects);
}
}
/**
* Binds or unbinds the "all"-listener for one or more objects.
*
* @param {String} which Either "on" or "off"
* @param {Object[]} objects Array of the objects on which the "all"-listener should be bound / unbound to
* @param {Function} [fn] The function that should be bound / unbound. Optional in case of "off"
* @param {Object} [ctx] The context the function should be called in
* @return {undefined}
*/
function onoff(which, objects, fn, ctx) {
for (var i = 0, l = objects.length, obj; i < l; i++) {
obj = objects[i];
if (!obj) continue;
if (which === "on") {
if (!ctx.objectRegistry.register(obj)) {
// register returned false, so obj was already registered
continue;
}
} else {
if (!ctx.objectRegistry.unregister(obj)) {
// unregister returned false, so obj wasn't registered
continue;
}
}
if (_.isFunction(obj[which])) {
obj[which]("all", fn, ctx);
}
}
}
/**
* Calls the undo/redo-function for a specific action.
*
* @param {String} which Either "undo" or "redo"
* @param {Object} action The Action's attributes
* @return {undefined}
*/
function actionUndoRedo (which, action) {
var type = action.type, undoTypes = action.undoTypes, fn = !undoTypes[type] || undoTypes[type][which];
if (_.isFunction(fn)) {
fn(action.object, action.before, action.after, action.options);
}
}
/**
* The main undo/redo function.
*
* @param {String} which Either "undo" or "redo"
* @param {UndoManager} manager The UndoManager-instance on which an "undo"/"redo"-Event is triggered afterwards
* @param {UndoStack} stack The UndoStack on which we perform
* @param {Boolean} magic If true, undoes / redoes all actions with the same magicFusionIndex
* @param {Boolean} everything If true, undoes / redoes every action that had been tracked
* @return {undefined}
*/
function managerUndoRedo (which, manager, stack, magic, everything) {
if (stack.isCurrentlyUndoRedoing ||
(which === "undo" && stack.pointer === -1) ||
(which === "redo" && stack.pointer === stack.length - 1)) {
// We're either currently in an undo- / redo-process or
// we reached the end of the stack
return;
}
stack.isCurrentlyUndoRedoing = true;
var action, actions, isUndo = which === "undo";
if (everything) {
// Undo / Redo all steps until you reach the stack's beginning / end
actions = isUndo && stack.pointer === stack.length - 1 || // If at the stack's end calling undo
!isUndo && stack.pointer === -1 ? // or at the stack's beginning calling redo
_.clone(stack.models) : // => Take all the models. Otherwise:
core_slice.apply(stack.models, isUndo ? [0, stack.pointer] : [stack.pointer, stack.length - 1]);
} else {
// Undo / Redo only one step
action = stack.at(isUndo ? stack.pointer : stack.pointer + 1);
actions = magic ? stack.where({"magicFusionIndex": action.get("magicFusionIndex")}) : [action];
}
stack.pointer += (isUndo ? -1 : 1) * actions.length;
while (action = isUndo ? actions.pop() : actions.shift()) {
// Here we're calling the Action's undo / redo method
action[which]();
}
stack.isCurrentlyUndoRedoing = false;
manager.trigger(which, manager);
}
/**
* Checks whether an UndoAction should be created or not. Therefore it checks
* whether a "condition" property is set in the undoTypes-object of the specific
* event type. If not, it returns true. If it's set and a boolean, it returns it.
* If it's a function, it returns its result, converting it into a boolean.
* Otherwise it returns true.
*
* @param {Object} undoTypesType The object within the UndoTypes that holds the function for this event type (i.e. "change")
* @param {Arguments} args The arguments the "condition" function is called with
* @return {Boolean} True, if an UndoAction should be created
*/
function validateUndoActionCreation (undoTypesType, args) {
var condition = undoTypesType.condition, type = typeof condition;
return type === "function" ? !!apply(condition, undoTypesType, args) :
type === "boolean" ? condition : true;
}
/**
* Adds an Undo-Action to the stack.
*
* @param {UndoStack} stack The undostack the action should be added to.
* @param {String} type The event type (i.e. "change")
* @param {Arguments} args The arguments passed to the undoTypes' "on"-handler
* @param {OwnedUndoTypes} undoTypes The undoTypes-object which has the "on"-handler
* @return {undefined}
*/
function addToStack(stack, type, args, undoTypes) {
if (stack.track && !stack.isCurrentlyUndoRedoing && type in undoTypes &&
validateUndoActionCreation(undoTypes[type], args)) {
// An UndoAction should be created
var res = apply(undoTypes[type]["on"], undoTypes[type], args), diff;
if (hasKeys(res, "object", "before", "after")) {
res.type = type;
res.magicFusionIndex = getMagicFusionIndex();
res.undoTypes = undoTypes;
if (stack.pointer < stack.length - 1) {
// New Actions must always be added to the end of the stack.
// If the pointer is not pointed to the last action in the
// stack, presumably because actions were undone before, then
// all following actions must be discarded
var diff = stack.length - stack.pointer - 1;
while (diff--) {
stack.pop();
}
}
stack.pointer = stack.length;
stack.add(res);
if (stack.length > stack.maximumStackLength) {
stack.shift();
stack.pointer--;
}
}
}
}
/**
* Predefined UndoTypes object with default handlers for the most common events.
* @type {Object}
*/
var UndoTypes = {
"add": {
"undo": function (collection, ignore, model, options) {
// Undo add = remove
collection.remove(model, options);
},
"redo": function (collection, ignore, model, options) {
// Redo add = add
if (options.index) {
options.at = options.index;
}
collection.add(model, options);
},
"on": function (model, collection, options) {
return {
object: collection,
before: undefined,
after: model,
options: _.clone(options)
};
}
},
"remove": {
"undo": function (collection, model, ignore, options) {
if ("index" in options) {
options.at = options.index;
}
collection.add(model, options);
},
"redo": function (collection, model, ignore, options) {
collection.remove(model, options);
},
"on": function (model, collection, options) {
return {
object: collection,
before: model,
after: undefined,
options: _.clone(options)
};
}
},
"change": {
"undo": function (model, before, after, options) {
if (_.isEmpty(before)) {
_.each(_.keys(after), model.unset, model);
} else {
model.set(before);
if (options && options.unsetData && options.unsetData.before && options.unsetData.before.length) {
_.each(options.unsetData.before, model.unset, model);
}
}
},
"redo": function (model, before, after, options) {
if (_.isEmpty(after)) {
_.each(_.keys(before), model.unset, model);
} else {
model.set(after);
if (options && options.unsetData && options.unsetData.after && options.unsetData.after.length) {
_.each(options.unsetData.after, model.unset, model);
}
}
},
"on": function (model, options) {
var
afterAttributes = model.changedAttributes(),
keysAfter = _.keys(afterAttributes),
previousAttributes = _.pick(model.previousAttributes(), keysAfter),
keysPrevious = _.keys(previousAttributes),
unsetData = (options || (options = {})).unsetData = {
after: [],
before: []
};
if (keysAfter.length != keysPrevious.length) {
// There are new attributes or old attributes have been unset
if (keysAfter.length > keysPrevious.length) {
// New attributes have been added
_.each(keysAfter, function (val) {
if (!(val in previousAttributes)) {
unsetData.before.push(val);
}
}, this);
} else {
// Old attributes have been unset
_.each(keysPrevious, function (val) {
if (!(val in afterAttributes)) {
unsetData.after.push(val);
}
})
}
}
return {
object: model,
before: previousAttributes,
after: afterAttributes,
options: _.clone(options)
};
}
},
"reset": {
"undo": function (collection, before, after) {
collection.reset(before);
},
"redo": function (collection, before, after) {
collection.reset(after);
},
"on": function (collection, options) {
return {
object: collection,
before: options.previousModels,
after: _.clone(collection.models)
};
}
}
};
/**
* Every UndoManager instance has an own undoTypes object
* which is an instance of OwnedUndoTypes. OwnedUndoTypes'
* prototype is the global UndoTypes object. Changes to the
* global UndoTypes object take effect on every instance of
* UndoManager as the object is its prototype. And yet every
* local UndoTypes object can be changed individually.
*
* @constructor
*/
function OwnedUndoTypes () {}
OwnedUndoTypes.prototype = UndoTypes;
/**
* Adds, changes or removes an undo-type from an UndoTypes-object.
* You can call it this way:
* manipulateUndoType (1, "reset", {"on": function () {}}, undoTypes)
* or this way to perform bulk actions:
* manipulateUndoType (1, {"reset": {"on": function () {}}}, undoTypes)
* In case of removing undo-types you can pass an Array for performing
* bulk actions:
* manipulateUndoType(2, ["reset", "change"], undoTypes)
*
* @param {Number} manipType Indicates the kind of action to execute: 0 for add, 1 for change, 2 for remove
* @param {String|Object|Array} undoType The type of undoType that should be added/changed/removed. Can be an object / array to perform bulk actions
* @param {Object} [fns] Object with the functions to add / change. Is optional in case you passed an object as undoType that contains these functions
* @param {OwnedUndoTypes|UndoTypes} undoTypesInstance The undoTypes object to act on
* @return {undefined}
*/
function manipulateUndoType (manipType, undoType, fns, undoTypesInstance) {
// manipType, passed by the calling function
// 0: add
// 1: change
// 2: remove
if (typeof undoType === "object") {
// bulk action. Iterate over this data.
return _.each(undoType, function (val, key) {
if (manipType === 2) { // remove
// undoType is an array
manipulateUndoType (manipType, val, fns, undoTypesInstance);
} else {
// undoType is an object
manipulateUndoType (manipType, key, val, fns);
}
})
}
switch (manipType) {
case 0: // add
if (hasKeys(fns, "undo", "redo", "on") && _.all(_.pick(fns, "undo", "redo", "on"), _.isFunction)) {
undoTypesInstance[undoType] = fns;
}
break;
case 1: // change
if (undoTypesInstance[undoType] && _.isObject(fns)) {
// undoTypeInstance[undoType] may be a prototype's property
// So, if we did this _.extend(undoTypeInstance[undoType], fns)
// we would extend the object on the prototype which means
// that this change would have a global effect
// Instead we just want to manipulate this instance. That's why
// we're doing this:
undoTypesInstance[undoType] = _.extend({}, undoTypesInstance[undoType], fns);
}
break;
case 2: // remove
delete undoTypesInstance[undoType];
break;
}
return this;
}
/**
* Instantiating "Action" creates the UndoActions that
* are collected in an UndoStack. It holds all relevant
* data to undo / redo an action and has an undo / redo
* method.
*/
var Action = Backbone.Model.extend({
defaults: {
type: null, // "add", "change", "reset", etc.
object: null, // The object on which the action occurred
before: null, // The previous values which were changed with this action
after: null, // The values after this action
magicFusionIndex: null // The magicFusionIndex helps to combine
// all actions that occurred "at the same time" to undo/redo them altogether
},
/**
* Undoes this action.
* @param {OwnedUndoTypes|UndoTypes} undoTypes The undoTypes object which contains the "undo"-handler that should be used
* @return {undefined}
*/
undo: function (undoTypes) {
actionUndoRedo("undo", this.attributes);
},
/**
* Redoes this action.
* @param {OwnedUndoTypes|UndoTypes} undoTypes The undoTypes object which contains the "redo"-handler that should be used
* @return {undefined}
*/
redo: function (undoTypes) {
actionUndoRedo("redo", this.attributes);
}
}),
/**
* An UndoStack is a collection of UndoActions in
* chronological order.
*/
UndoStack = Backbone.Collection.extend({
model: Action,
pointer: -1, // The pointer indicates the index where we are located within the stack. We start at -1
track: false,
isCurrentlyUndoRedoing: false,
maximumStackLength: Infinity,
setMaxLength: function (val) {
this.maximumStackLength = val;
}
}),
/**
* An instance of UndoManager can keep track of
* changes to objects and helps to undo them.
*/
UndoManager = Backbone.Model.extend({
defaults: {
maximumStackLength: Infinity,
track: false
},
/**
* The constructor function.
* @param {attr} [attr] Object with parameters. The available parameters are:
* - maximumStackLength {number} Set the undo-stack's maximum size
* - track {boolean} Start tracking changes right away
* @return {undefined}
*/
initialize: function (attr) {
this.stack = new UndoStack;
this.objectRegistry = new ObjectRegistry();
this.undoTypes = new OwnedUndoTypes();
// sync the maximumStackLength attribute with our stack
this.stack.setMaxLength(this.get("maximumStackLength"));
this.on("change:maximumStackLength", function (model, value) {
this.stack.setMaxLength(value);
}, this);
// Start tracking, if attr.track == true
if (attr && attr.track) {
this.startTracking();
}
// Register objects passed in the "register" attribute
if (attr && attr.register) {
if (_.isArray(attr.register) || _.isArguments(attr.register)) {
apply(this.register, this, attr.register);
} else {
this.register(attr.register);
}
}
},
/**
* Starts tracking. Changes of registered objects won't be processed until you've called this function
* @return {undefined}
*/
startTracking: function () {
this.set("track", true);
this.stack.track = true;
},
/**
* Stops tracking. Afterwards changes of registered objects won't be processed.
* @return {undefined}
*/
stopTracking: function () {
this.set("track", false);
this.stack.track = false;
},
/**
* Return the state of the tracking
* @return {boolean}
*/
isTracking: function () {
return this.get("track");
},
/**
* This is the "all"-handler which is bound to registered
* objects. It creates an UndoAction from the event and adds
* it to the stack.
*
* @param {String} type The event type
* @return {undefined}
*/
_addToStack: function (type) {
addToStack(this.stack, type, slice(arguments, 1), this.undoTypes);
},
/**
* Registers one or more objects to track their changes.
* @param {...Object} obj The object or objects of which changes should be tracked
* @return {undefined}
*/
register: function () {
onoff("on", arguments, this._addToStack, this);
},
/**
* Unregisters one or more objects.
* @param {...Object} obj The object or objects of which changes shouldn't be tracked any longer
* @return {undefined}
*/
unregister: function () {
onoff("off", arguments, this._addToStack, this);
},
/**
* Unregisters all previously registered objects.
* @return {undefined}
*/
unregisterAll: function () {
apply(this.unregister, this, this.objectRegistry.get());
},
/**
* Undoes the last action or the last set of actions in case 'magic' is true.
* @param {Boolean} [magic] If true, all actions that happened basically at the same time are undone together
* @return {undefined}
*/
undo: function (magic) {
managerUndoRedo("undo", this, this.stack, magic);
},
/**
* Undoes all actions ever tracked by the undo manager
* @return {undefined}
*/
undoAll: function () {
managerUndoRedo("undo", this, this.stack, false, true);
},
/**
* Redoes a previously undone action or a set of actions.
* @param {Boolean} [magic] If true, all actions that happened basically at the same time are redone together
* @return {undefined}
*/
redo: function (magic) {
managerUndoRedo("redo", this, this.stack, magic);
},
/**
* Redoes all actions ever tracked by the undo manager
* @return {undefined}
*/
redoAll: function () {
managerUndoRedo("redo", this, this.stack, false, true);
},
/**
* Checks if there's an action in the stack that can be undone / redone
* @param {String} type Either "undo" or "redo"
* @return {Boolean} True if there is a set of actions which can be undone / redone
*/
isAvailable: function (type) {
var s = this.stack, l = s.length;
switch (type) {
case "undo": return l > 0 && s.pointer > -1;
case "redo": return l > 0 && s.pointer < l - 1;
default: return false;
}
},
/**
* Sets the stack-reference to the stack of another undoManager.
* @param {UndoManager} undoManager The undoManager whose stack-reference is set to this stack
* @return {undefined}
*/
merge: function (undoManager) {
// This sets the stack-reference to the stack of another
// undoManager so that the stack of this other undoManager
// is used by two different managers.
// This enables to set up a main-undoManager and besides it
// several others for special, exceptional cases (by using
// instance-based custom UndoTypes). Models / collections
// which need this special treatment are only registered at
// those special undoManagers. Those special ones are then
// merged into the main-undoManager to write on its stack.
// That way it's easier to manage exceptional cases.
var args = _.isArray(undoManager) ? undoManager : slice(arguments), manager;
while (manager = args.pop()) {
if (manager instanceof UndoManager &&
manager.stack instanceof UndoStack) {
// set the stack reference to our stack
manager.stack = this.stack;
}
}
},
/**
* Add an UndoType to this specific UndoManager-instance.
* @param {String} type The event this UndoType is made for
* @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. Must have the properties "undo", "redo" and "on". Can have the property "condition".
* @return {undefined}
*/
addUndoType: function (type, fns) {
manipulateUndoType(0, type, fns, this.undoTypes);
},
/**
* Overwrite properties of an existing UndoType for this specific UndoManager-instance.
* @param {String} type The event the UndoType is made for
* @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. It extends the existing object.
* @return {undefined}
*/
changeUndoType: function (type, fns) {
manipulateUndoType(1, type, fns, this.undoTypes);
},
/**
* Remove one or more UndoTypes of this specific UndoManager-instance to fall back to the global UndoTypes.
* @param {String|Array} type The event the UndoType that should be removed is made for. You can also pass an array of events.
* @return {undefined}
*/
removeUndoType: function (type) {
manipulateUndoType(2, type, undefined, this.undoTypes);
},
/**
* Removes all actions from the stack.
* @return {undefined}
*/
clear: function() {
this.stack.reset();
this.stack.pointer = -1;
}
});
_.extend(UndoManager, {
/**
* Change the UndoManager's default attributes
* @param {Object} defaultAttributes An object with the new default values.
* @return {undefined}
*/
defaults: function (defaultAttributes) {
_.extend(UndoManager.prototype.defaults, defaultAttributes);
},
/**
* Add an UndoType to the global UndoTypes-object.
* @param {String} type The event this UndoType is made for
* @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. Must have the properties "undo", "redo" and "on". Can have the property "condition".
* @return {undefined}
*/
"addUndoType": function (type, fns) {
manipulateUndoType(0, type, fns, UndoTypes);
},
/**
* Overwrite properties of an existing UndoType in the global UndoTypes-object.
* @param {String} type The event the UndoType is made for
* @param {Object} fns An object of functions that are called to generate the data for an UndoAction or to process it. It extends the existing object.
* @return {undefined}
*/
"changeUndoType": function (type, fns) {
manipulateUndoType(1, type, fns, UndoTypes)
},
/**
* Remove one or more UndoTypes of this specific UndoManager-instance to fall back to the global UndoTypes.
* @param {String|Array} type The event the UndoType that should be removed is made for. You can also pass an array of events.
* @return {undefined}
*/
"removeUndoType": function (type) {
manipulateUndoType(2, type, undefined, UndoTypes);
}
})
return Backbone.UndoManager = UndoManager;
});
Backbone.Undo.js
================
A simple Backbone undo manager for simple apps
**Go to [backbone.undojs.com](http://backbone.undojs.com) for demos and a video tutorial!**
***
#### Advantages of Backbone.Undo.js
* **The Drop-In Manager**
In comparison to undo managers that implement the memento pattern you don't have to modify your models and collections to use Backbone.Undo.js. Just drop in Backbone.Undo.js and register the objects whose actions you want to undo. That way it's not only easy to include Backbone.Undo.js, but also to exclude it in case you don't want to use it any longer at some point.
* **Ready To Go: Based on Backbone-Events**
You don't have to manually call methods to `store()` or `restore()` certain states. To detect an undoable action, Backbone.Undo.js listens to the events Backbone triggeres automatically. You don't have to do anything.
* **Magic Fusion**
In a more complex web application the click of a button might trigger several changes which dispatch several events which in Backbone.Undo.js are turned into several undoable actions. If the user wants to undo what he caused with his click he wants to undo all of those actions. Backbone.Undo.js has an internal feature called *Magic Fusion* that detects actions that were created in one flow and undoes or redoes all of them.
#### Who should use Backbone.Undo.js
Backbone.Undo.js is a simple undo manager that should be used for rather simple web applications. It has mechanisms that make it extensible and suitable for more complex applications. However, it might not be adequate for very large-scale applications with vast amounts of lines of code.
## Getting started
Backbone.Undo.js depends on Backbone.js which depends on Underscore.js (or Lo-Dash.js). Make sure to include these two files before you include Backbone.Undo.js:
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<!-- Backbone.Undo.js is included *after* Backbone and Underscore -->
<script src="Backbone.Undo.js"></script>
#### Backbone Version
Backbone.Undo.js was developed for Backbone 1.0.0 or higher.
#### Underscore Version
Backbone.Undo.js was developed for Underscore 1.4.4 or higher.
## Setting up your UndoManager
In order to set up your UndoManager you have to do the following steps:
1. __Instantiate__ your UndoManager
var myUndoManager = new Backbone.UndoManager();
2. __Register__ the models and collections you want to observe
var model = new Backbone.Model,
collection = new Backbone.Collection;
myUndoManager.register(model, collection); // You can pass several objects as arguments
// You can prepare your objects here. Changes won't be tracked yet.
model.set("foo", "bar");
collection.add([{"something": "blue"}]);
// These changes can't be undone.
3. __Start tracking__ the changes
myUndoManager.startTracking(); // Every change that happens to the model and the collection can now be undone
__Shorthand__: If you already have the objects you want to observe at hand when you instantiate the undo manager or if you don't need to prepare them you can pass them on instantiation:
// Shorthand
var myUndoManager = new Backbone.UndoManager({
track: true, // changes will be tracked right away
register: [model, collection] // pass an object or an array of objects
})
## Backbone.Undo.js methods
Methods you can call on an instance of `Backbone.UndoManager`:
#### Constructor `new Backbone.UndoManager([object])`
The constructor can be called with an object of attributes as an optional argument. Each attribute is optional and has a default value.
var undoManager = new Backbone.UndoManager; // possible, because the argument is optional
var undoManager = new Backbone.UndoManager({
maximumStackLength: 30, // default: Infinity; Maximum number of undoable actions
track: true, // default: false; If true, changes will be tracked right away
register: myObj // default: undefined; Pass the object or an array of objects that you want to observe
});
#### register `undoManager.register(obj, [obj, ...])`
Your undo manager must know the objects whose actions should be undoable/redoable. Therefore you have to register these
objects:
var model = new Backbone.Model;
var collection = new Backbone.Collection;
undoManager.register(model, collection);
The register-method doesn't check whether the object is an instance of Backbone.Model or Backbone.Collection. That makes
it possible to bind other objects as well. However, make sure they have an `on()` and an `off()` method and trigger an `"all"` event in the fashion of Backbone's `"all"` event.
#### unregister `undoManager.unregister(obj, [obj, …])`
Previously registered objects can be unregistered using the `unregister()` method. Changes to those objects can't be
undone after they have been unregistered.
var myModel = new Backbone.Model;
undoManager.register(myModel);
undoManager.startTracking();
myModel.set("foo", "bar"); // Can be undone
undoManager.unregister(myModel);
myModel.set("foo", "baz"); // Can't be undone
#### unregisterAll `undoManager.unregisterAll()`
Unregister all objects that have been registered at this undoManager so far.
#### startTracking `undoManager.startTracking()`
Changes must be tracked in order to create UndoActions. You can either set `{track: true}` on instantiation or call `startTracking()` later.
var myModel = new Backbone.Model;
undoManager.register(myModel);
myModel.set("foo", "bar"); // Can't be undone because tracking didn't start yet
undoManager.startTracking();
myModel.set("foo", "baz"); // Can be undone
#### stopTracking `undoManager.stopTracking();`
If you want to stop tracking changes for whatever reason, you can do that by calling `stopTracking()`.
myModel.set("foo", 1);
undoManager.startTracking();
myModel.set("foo", 2);
undoManager.stopTracking();
myModel.set("foo", 3);
undoManager.undo(); // "foo" is 1 instead of 2, because the last change wasn't tracked
#### undo `undoManager.undo([magic]);`
The method to undo the last action is `undo()`.
myModel.get("foo"); // => 1
myModel.set("foo", 2);
undoManager.undo();
myModel.get("foo"); // => 1
Pass `true` to activate *Magic Fusion*. That way you undo the complete last set of actions that happened at once.
#### undoAll `undoManager.undoAll();`
Undoes all actions ever tracked by the undo manager.
#### redo `undoManager.redo([magic])`
The method to redo the latest undone action is `redo()`.
myModel.set("foo", 2);
undoManager.undo();
myModel.get("foo"); // => 1
undoManager.redo();
myModel.get("foo"); // => 2
Like with `undo()` you can pass `true` to activate *Magic Fusion* and to redo the complete last set of actions that were undone.
#### redoAll `undoManager.redoAll();`
Redoes all actions ever tracked by the undo manager.
#### isAvailable `undoManager.isAvailable(type)`
This method checks if there's an UndoAction in the stack that can be undone / redone. Pass `"undo"` or `"redo"` as the argument.
undoManager.isAvailable("undo") // => true; You can undo actions
If you use undo- and redo-buttons in your gui this method is helpful for determining whether to display them in an enabled or disabled state.
#### merge `undoManager.merge(otherManager1, [otherManager2, …])`
This is a feature for the advanced use of Backbone.Undo.js. Using the UndoTypes-API (see below) for specific instances of `Backbone.UndoManager` you can create undo managers with special behavior for special cases. But as having several undo managers side by side doesn't make any sense you need a way to combine them. That's what `merge` is for.
The method `merge` sets the stack-reference of other undo managers to its stack.
var mainUndoManager = new Backbone.UndoManager,
specialUndoManager = new Backbone.UndoManager;
// Implementing special behavior
specialUndoManager.addUndoType(…)
// Making both write on one stack
mainUndoManager.merge(specialUndoManager);
mainUndoManager.stack === specialUndoManager.stack // => true
You can pass one or more undo managers or an array with one or more undo managers when calling this function.
#### addUndoType `undoManager.addUndoType(type, fns)`
This adds an UndoType that only works for this specific undo manager and won't affect other instances of Backbone.UndoManager. See the UndoTypes-API for a more thorough documentation on this function.
#### changeUndoType `undoManager.changeUndoType(type, fns)`
This changes an UndoType only on this specific undo manager and won't affect other instances of Backbone.UndoManager. See the UndoTypes-API for a more thorough documentation on this function.
#### removeUndoType `undoManager.removeUndoType(type)`
This removes an UndoType only from from this specific undo manager. See the UndoTypes-API for a more thorough documentation on this function.
#### clear `undoManager.clear()`
This removes all actions from the stack of actions.
***
Methods you can call on the object `Backbone.UndoManager`:
#### defaults `Backbone.UndoManager.defaults(obj)`
Extend or overwrite the default values of an undo manager.
Backbone.UndoManager.defaults({
track: true
});
var undoManager = new Backbone.UndoManager; // tracking has now already started
#### addUndoType `Backbone.UndoManager.addUndoType(type, fns)`
This adds an UndoType that works for all undo managers whether they've already been instantiated or not. See the UndoTypes-API for a more thorough documentation on this function.
#### changeUndoType `Backbone.UndoManager.changeUndoType(type, fns)`
This changes an UndoType for all undo managers whether they've already been instantiated or not. See the UndoTypes-API for a more thorough documentation on this function.
#### removeUndoType `Backbone.UndoManager.removeUndoType(type)`
This removes an UndoType from all undo managers whether they've already been instantiated or not. See the UndoTypes-API for a more thorough documentation on this function.
## Supported Events
Backbone.Undo.js uses Backbone's events to generate UndoActions. It has built-in support for the following events
* `add` When a model is added to a collection
* `remove` When a model is removed from a collection
* `reset` When a collection is reset and all models are replaced by new models (or no models) at once
* `change` When a model's attribute is changed or set
### Supporting other events and modifying built-in behavior
Backbone.Undo.js has an API to extend and modify the generation of UndoActions. In order to use the API it's important to understand the concept of creating UndoActions:
#### UndoTypes
Backbone.Undo.js retrieves the data of the undoable states from the events Backbone triggers and their arguments. However, different events have different arguments and thus need different approaches in retrieving the necessary data. Additionally, different types of actions require different behavior to undo and redo them.
That's what the *UndoTypes* are for. An *UndoType* is an object of functions for a specific type of event. The functions retrieve the data necessary to create an UndoAction and are able to undo an action of this type and redo it.
An *UndoType* needs to have the following functions:
* **on** `([…])`
This function is called when the event this UndoType is made for was triggered on an observed object. It gets all the arguments that were triggered with the event. The `"on"`-function must return an object with the properties `object`, `before`, `after` and optionally `options`.
return {
"object": … // The object the event was triggered on
"before": … // The object's state before the concerning action occured
"after": … // The object's current state, after the concerning action occured
"options": … // Optionally: Some 'options'-object
}
* **undo** `(obj, before, after, options)`
The `undo` function is called when the action this UndoType is made for should be undone. The data returned by the `"on"` function is passed to `"undo"` as arguments:
* `obj` is the model, collection or other kind of object that should be acted on
* `before` is the the data before the action occured and defines the state that should be created within this function
* `after` is the data after the action had occured and represents obj's current state
* `options` are the options the `on` function returned
* **redo** `(obj, before, after, options)`
The `redo` function is called when the action this UndoType is made for should be redone. As with `"undo"` the data returned by the `"on"` function is passed to `"redo"` as arguments
* `obj` is the model, collection or other kind of object that should be acted on
* `before` is the the data before the action occured and represents the current state as the action was previously undone
* `after` is the data after the action had occured and is the state wich should be recreated
* `options` are the options the `"on"` function returned
It can have an optional property:
* **condition** `([…])`
`"condition"` can be a function or a boolean value that defines whether an UndoAction should be created or not. If it's false or if it returns false `"on"` won't be called and no UndoAction is created. If it's not set, condition is always `true`.
##### UndoType example
This is an example of an UndoType for the `"reset"` event.
{
"reset": {
"condition": true, // This isn't necessary as condition is true by default
"on": function (collection, options) {
// The 'on'-method gets the same arguments a listener for the
// Backbone 'reset'-event would get: collection.on("reset", listener)
// The 'on'-method has to return an object with the properties
// 'object', 'before', 'after' and optionally 'options'
return {
object: collection,
before: options.previousModels,
after: _.clone(collection.models)
}
},
"undo": function (collection, before, after) {
// To restore the previous state we just reset the collection
// with the previous models
collection.reset(before);
}
"redo": function (collection, before, after) {
// To restore the subsequent state we reset the collection to
// the 'after'-array of models
collection.reset(after);
}
}
}
#### UndoTypes API
To create your own UndoTypes for custom events or for extending the support of Backbone-events or if you just want to modify the built-in behavior, you can either do that on a global level to affect all current and future instances of Backbone.UndoManager or do that per instance to change only a specific undo manager.
Either way you have three methods to extend or change the UndoTypes. Below the functions for global changes are presented:
#### addUndoType
Backbone.UndoManager.addUndoType(type, callbacks);
// or
Backbone.UndoManager.addUndoType(types);
With the `addUndoType()` method you can add or overwrite one or more UndoTypes. You can call it with the two arguments `type` and `callbacks` or with an object in which all keys are `type`s and their values `callbacks` to perform a bulk action.
* `type` The name of the event this UndoType is made for. In terms of Backbone events: `"add"`, `"remove"`, `"reset"`, `"change"`, etc.
* `callbacks` An object with the funcitions `"on"`, `"undo"`, `"redo"` and optionally `"condition"`
*Example*: If we want to overwrite the UndoType `"reset"` with the functions defined in the example above we can do the following:
Backbone.UndoManager.addUndoType("reset", {
"on": function (collection, options) {
},
"undo": function (collection, before, after) {
},
"redo": function (collection, before, after) {
}
});
You can also define several UndoTypes at once by passing an object to `addUndoType`
Backbone.UndoManager.addUndoType({
"reset": {
"on": …
"undo": …
"redo": …
},
"add": {
"on": …
"undo": …
"redo": …
},
"customevent": {
"on": …
"undo": …
"redo": …
}
});
#### changeUndoType
Backbone.UndoManager.changeUndoType(type, callbacks);
// or
Backbone.UndoManager.changeUndoType(types);
If you want to change just one or more functions of an already added or built-in UndoType `changeUndoType` is the way to go. It works just like `addUndoType` with the difference that there must already be an UndoType for the specified `type` and you don't have to pass all `callbacks` functions.
Backbone.UndoManager.changeUndoType("reset", {
"condition": …
})
Pass an object to perform a bulk action:
Backbone.UndoManager.changeUndoType({
"reset": {
"condition": …
},
"add": {
"on": …
"undo": …
},
"customevent": {
"redo": …
}
})
#### removeUndoType
Backbone.UndoManager.removeUndoType(type);
// or
Backbone.UndoManager.removeUndoType(types);
Call `removeUndoType` to remove an existing UndoType. Pass the type of the UndoType you want to remove as the argument or pass an array of types if you want to remove several at once.
Backbone.UndoManager.removeUndoType("reset");
Pass an array to perform a bulk action:
Backbone.UndoManager.removeUndoType(["reset", "add", "customevent"]);
If you just want to suspend an UndoType for a limited amount of time, making use of the `"condition"` property might be more adequate:
Backbone.UndoManager.changeUndoType("reset", {"condition": false});
#### Using the UndoTypes API per instance
As stated above you can also add, change and remove UndoTypes for a specific instance of Backbone.Undo without affecting other instances. The methods and arguments are exactly the same.
var undoManager = new Backbone.UndoManager;
undoManager.addUndoType("reset", {
"on": …
"undo": …
"redo": …
})
undoManager.changeUndoType("reset", {
"undo": …
})
undoManager.removeUndoType("reset");
Please note that removing an UndoType on a per instance level just causes a fallback to the global UndoTypes and won't take away the support for this type. You have to overwrite the type with an UndoType of empty functions to accomplish that.
Using the UndoTypes-API for a specific instance is especially useful if you have several undo managers.
var undoA = new Backbone.UndoManager,
undoB = new Backbone.UndoManager,
undoC = new Backbone.UndoManager;
undoA.addUndoType(…); // behavior A
undoB.addUndoType(…); // behavior B
undoC.addUndoType(…); // behavior C
However, several undo managers cause the problem that you don't know on which undo manager you should call `undo()` and `redo()`.
That's what the `merge()` function is for: It merges several undo managers by making them write on a single stack.
var mainUndo = new Backbone.UndoManager;
mainUndo.merge(undoA, undoB, undoC)
Now, you just need to call `undo()` and `redo()` on the main undo manager.
## License (MIT License)
Copyright (c) 2013 Oliver Sartun
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>UndoManager Tests</title>
<link rel="stylesheet" href="resources/qunit.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="resources/jquery-1.9.1.js"></script>
<script src="resources/underscore.js"></script>
<script src="resources/backbone.js"></script>
<script src="../Backbone.Undo.js"></script>
<script src="resources/qunit.js"></script>
<script src="Backbone.Undo.Tests.js"></script>
</body>
</html>
\ No newline at end of file
test("Register and unregister", function () {
var UndoManager = new Backbone.UndoManager;
var model = new Backbone.Model,
collection = new Backbone.Collection;
UndoManager.register(model, collection);
function getRegisteredObjects () {
return UndoManager.objectRegistry.get();
}
strictEqual(getRegisteredObjects().length, 2, "Registering objects with the register method was successful");
UndoManager.unregister(model);
deepEqual(getRegisteredObjects(), [collection], "Unregistering an object with the unregister method was successful");
UndoManager.register(model);
UndoManager.unregisterAll();
strictEqual(getRegisteredObjects().length, 0, "The unregisterAll function worked properly");
var u1 = new Backbone.UndoManager({
register: model
}),
u2 = new Backbone.UndoManager({
register: collection
}),
u3 = new Backbone.UndoManager({
register: [model, collection]
});
deepEqual(u1.objectRegistry.get(), [model], "Registering a single model over the 'register' attribute on instantiation was successful");
deepEqual(u2.objectRegistry.get(), [collection], "Registering a single collection over the 'register' attribute on instantiation was successful");
deepEqual(u3.objectRegistry.get(), [model, collection], "Registering multiple objects over the 'register' attribute on instantiation was successful");
})
test("Start and stop tracking", function () {
var model = new Backbone.Model({
"foo": "bar"
})
var collection = new Backbone.Collection([{"a": "b"}, {"c": "d"}]);
var UndoManager = new Backbone.UndoManager({
register: [model, collection]
});
var before = UndoManager.stack.length;
model.set("hello", "world");
collection.add({"e": "f"});
strictEqual(UndoManager.stack.length, before, "Actions weren't added to the stack, because tracking hasn't started yet");
ok(!UndoManager.isTracking(), "Tracking has not started yet");
UndoManager.startTracking();
ok(UndoManager.isTracking(), "Tracking has now started");
model.set("hello", "you");
collection.remove(collection.last());
var after = UndoManager.stack.length;
strictEqual(after, 2, "Two actions have been added to the stack, because tracking has started");
UndoManager.stopTracking();
model.set("hello", "kitty");
collection.add({"e": "f"});
model.set("hello", "you");
collection.remove(collection.last());
UndoManager.startTracking();
strictEqual(UndoManager.stack.length, after, "No actions were added, because tracking was paused");
UndoManager.unregisterAll();
UndoManager = new Backbone.UndoManager({track: true});
UndoManager.register(model, collection);
model.set("hello", "hello");
collection.add({"g": "h"});
strictEqual(UndoManager.stack.length, 2, "{track:true} on instantiation started tracking at once");
});
test("Undo and redo Model-Changes", function () {
var model = new Backbone.Model({
"t": 1
});
var UndoManager = new Backbone.UndoManager({
track: true,
register: model
});
model.set("t", 2);
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 1}, "Undoing changing a model attribute was successful");
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2}, "Redoing changing a model attribute was successful");
model.set("x", 1);
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 2}, "Undoing setting a model attribute was successful");
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2, "x": 1}, "Redoing setting a model attribute was successful");
model.set({
"y": 1,
"x": 2
})
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 2, "x": 1}, "Undoing multiple changes at once was successful");
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2, "x": 2, "y": 1}, "Redoing multiple changes at once was successful");
UndoManager.undo();
UndoManager.undo();
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 1}, "Calling undo consecutively several times was successful");
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 1}, "Additional undo calls that would be out of the stack's bounds were successfully ignored");
UndoManager.redo();
UndoManager.redo();
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2, "x": 2, "y": 1}, "Calling redo consecutively several times was successful");
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2, "x": 2, "y": 1}, "Additional redo calls that would be out of the stack's bounds were successfully ignored");
})
test("Undo and redo Collection-changes", function () {
var collection = new Backbone.Collection([{"t": 1}, {"t": 2}, {"t": 3}]),
UndoManager = new Backbone.UndoManager({
track: true,
register: collection
});
collection.add({"t": 4});
UndoManager.undo();
deepEqual(collection.toJSON(), [{"t": 1}, {"t": 2}, {"t": 3}], "Adding a model to the collection was successfully undone");
UndoManager.redo();
deepEqual(collection.toJSON(), [{"t": 1}, {"t": 2}, {"t": 3}, {"t": 4}], "Adding a model to the collection was successfully redone");
collection.remove(collection.first());
UndoManager.undo();
deepEqual(collection.toJSON(), [{"t": 1}, {"t": 2}, {"t": 3}, {"t": 4}], "Removing a model from the collection was successfully undone");
UndoManager.redo();
deepEqual(collection.toJSON(), [{"t": 2}, {"t": 3}, {"t": 4}], "Removing a model from the collection was successfully redone");
collection.reset([{"a": 1}, {"a": 2}, {"a": 3}]);
UndoManager.undo();
deepEqual(collection.toJSON(), [{"t": 2}, {"t": 3}, {"t": 4}], "Resetting the collection was successfully undone");
UndoManager.redo();
deepEqual(collection.toJSON(), [{"a": 1}, {"a": 2}, {"a": 3}], "Resetting the collection was successfully redone");
collection.first().destroy();
UndoManager.undo();
deepEqual(collection.toJSON(), [{"a": 1}, {"a": 2}, {"a": 3}], "Destroying a model in the collection was successfully undone");
UndoManager.redo();
deepEqual(collection.toJSON(), [{"a": 2}, {"a": 3}], "Destroying a model in the collection was successfully redone");
})
test("ObjectRegistry", function () {
var model = new Backbone.Model,
collection = new Backbone.Collection,
nonBackboneObject = {"something":"else"},
UndoManager = new Backbone.UndoManager,
objectRegistry = UndoManager.objectRegistry;
function compare (arr1, arr2) {
return _.all(arr1, function (v) {
return _.contains(arr2, v);
});
}
objectRegistry.register(model);
ok(objectRegistry.isRegistered(model), "The isRegistered method returns true");
objectRegistry.register(collection);
ok(compare(objectRegistry.get(), [model, collection]), "The get method rightfully returns a list of the registered objects");
objectRegistry.register(model);
objectRegistry.register(collection);
equal(objectRegistry.get().length, 2, "Redundant registrations are correctly ignored");
objectRegistry.register(nonBackboneObject);
ok(compare(objectRegistry.get(), [model, collection, nonBackboneObject]), "Non-Backbone objects are also correctly registered");
objectRegistry.unregister(model);
objectRegistry.unregister(collection);
objectRegistry.unregister(nonBackboneObject);
equal(objectRegistry.get().length, 0, "Unregistering objects works properly");
})
test("Merging UndoManagers", 2, function () {
var main = new Backbone.UndoManager({track:true}),
special = new Backbone.UndoManager({track:true}),
model1 = new Backbone.Model({
"t": 1
}),
model2 = new Backbone.Model,
obj = {
object: model1,
before: {"a": 1},
after: {"b": 1}
};
main.id = main.stack.id = "main";
special.id = special.stack.id = "special";
special.changeUndoType("change", {
"on": function () {
return obj;
}
});
special.register(model1);
main.register(model2);
main.merge(special);
model1.set("t", 2); // Here we're triggering a change event
// Now, the stack-length of main must have been changed
deepEqual(main.stack.at(0).toJSON(), obj, "The action data was manipulated by the changed undotype")
model2.set("t", 2); // Here we're checking if the main undoManager can still write on its stack
deepEqual(main.stack.at(1).toJSON().after, {"t": 2}, "The main undomanager can still write on its own stack")
})
test("Clearing all actions", function () {
var model = new Backbone.Model({
"t": 1
});
var UndoManager = new Backbone.UndoManager({
track: true,
register: model
});
model.set("t", 2);
model.set("t", 3);
model.set("t", 4);
UndoManager.clear();
UndoManager.undo();
deepEqual(model.toJSON(), {"t": 4}, "Clearing actions before undoing was successful");
model.set("t", 2);
model.set("t", 3);
model.set("t", 4);
UndoManager.undo();
UndoManager.undo();
UndoManager.clear();
UndoManager.redo();
deepEqual(model.toJSON(), {"t": 2}, "Clearing actions before redoing was successful");
})
test("Undoing all actions", function () {
var model = new Backbone.Model({
"t": 1
});
var UndoManager = new Backbone.UndoManager({
track: true,
register: model
});
model.set("t", 2);
model.set("t", 3);
model.set("t", 4);
UndoManager.undoAll();
deepEqual(model.toJSON(), {"t": 1}, "Calling undoAll was successful");
UndoManager.redoAll();
deepEqual(model.toJSON(), {"t": 4}, "Calling redoAll was successful");
UndoManager.undo();
UndoManager.undoAll();
UndoManager.undo(true);
deepEqual(model.toJSON(), {"t": 1}, "Mixing undoAll with undo doesn't cause any problems");
UndoManager.undoAll(); // back to the stack's beginning
UndoManager.redo();
UndoManager.redoAll();
UndoManager.redo(true);
deepEqual(model.toJSON(), {"t": 4}, "Mixing redoAll with redo doesn't cause any problems");
UndoManager.undoAll();
UndoManager.undoAll();
UndoManager.redoAll();
UndoManager.redoAll();
deepEqual(model.toJSON(), {"t": 4}, "Calling undoAll and redoAll multiple times doesn't cause any problems");
})
/**
* Async tests for magic fusion
*/
var deferQueue = [];
function defer(fn) {
var args = [].slice.call(arguments);
deferQueue.push(fn, args);
}
function flushDeferQueue() {
_.defer(function () {
var fn = deferQueue.shift(),
args = deferQueue.shift();
if (fn) {
fn.apply(null, args);
flushDeferQueue();
}
})
}
asyncTest("Magic Fusion", 4, function () {
var model = new Backbone.Model({
"t": 1
}), collection = new Backbone.Collection([{"a": 3}]),
UndoManager = new Backbone.UndoManager({
track: true,
register: [model, collection]
}), i;
defer(function () {
start();
// Undo / Redo several changes
for (i = 3; i < 10; i++) {
model.set("t", i);
}
UndoManager.undo(true);
equal(model.get("t"), 1, "Undoing all changes that happened on a model at once succeeded");
UndoManager.redo(true);
equal(model.get("t"), 9, "Redoing all changes that happened on a model at once succeeded");
})
defer(function () {
stop();
collection.add([{"a": 4}, {"a": 5}]);
UndoManager.undo(true);
equal(collection.length, 1, "Undoing adding several models to a collection succeeded");
UndoManager.redo(true);
equal(collection.length, 3, "Redoing adding several models to a collection succeeded");
start();
})
flushDeferQueue();
})