From 59e240e478dfc1edcb0ff2fa0deccb235ae9db07 Mon Sep 17 00:00:00 2001
From: gdub22
Date: Fri, 27 Jun 2014 11:00:52 -0700
Subject: [PATCH] initial commit
---
.gitignore | 19 +
.jshintrc | 98 ++
.travis.yml | 5 +
LICENSE.md | 21 +
README.md | 22 +
demo/content-kit-compiler.js | 569 +++++++++
demo/content-kit-demo.js | 93 ++
demo/demo.css | 141 +++
demo/index.html | 73 ++
dist/content-kit-editor.js | 812 +++++++++++++
gulpfile.js | 86 ++
package.json | 31 +
src/css/editor.css | 331 +++++
src/js/commands.js | 177 +++
src/js/constants.js | 31 +
src/js/editor.js | 225 ++++
src/js/index.js | 5 +
src/js/prompt.js | 62 +
src/js/toolbar-button.js | 47 +
src/js/toolbar.js | 134 +++
src/js/utils.js | 110 ++
tests/index.html | 19 +
tests/lib/qunit-1.13.0.css | 245 ++++
tests/lib/qunit-1.13.0.js | 2210 ++++++++++++++++++++++++++++++++++
tests/tests.js | 62 +
25 files changed, 5628 insertions(+)
create mode 100644 .gitignore
create mode 100644 .jshintrc
create mode 100644 .travis.yml
create mode 100644 LICENSE.md
create mode 100644 README.md
create mode 100755 demo/content-kit-compiler.js
create mode 100644 demo/content-kit-demo.js
create mode 100644 demo/demo.css
create mode 100644 demo/index.html
create mode 100644 dist/content-kit-editor.js
create mode 100644 gulpfile.js
create mode 100644 package.json
create mode 100644 src/css/editor.css
create mode 100644 src/js/commands.js
create mode 100644 src/js/constants.js
create mode 100644 src/js/editor.js
create mode 100644 src/js/index.js
create mode 100644 src/js/prompt.js
create mode 100644 src/js/toolbar-button.js
create mode 100644 src/js/toolbar.js
create mode 100644 src/js/utils.js
create mode 100644 tests/index.html
create mode 100644 tests/lib/qunit-1.13.0.css
create mode 100644 tests/lib/qunit-1.13.0.js
create mode 100644 tests/tests.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..037de96ec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+lib-cov
+lcov.info
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+
+pids
+logs
+results
+build
+.grunt
+
+node_modules
+
+.DS_Store
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 000000000..97daaa015
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,98 @@
+{
+ // --------------------------------------------------------------------
+ // JSHint Configuration
+ // --------------------------------------------------------------------
+ //
+ // http://www.jshint.com/
+ // Modifed from: https://gist.github.com/haschek/2595796
+ //
+ // * set all enforcing options to true
+ // * set all relaxing options to false
+ // * set all JSLint legacy options to false
+ //
+
+ // == Enforcing Options ===============================================
+ //
+ // These options tell JSHint to be more strict towards your code. Use
+ // them if you want to allow only a safe subset of JavaScript, very
+ // useful when your codebase is shared with a big number of developers
+ // with different skill levels.
+
+ "curly" : true, // Require {} for every new block or scope.
+ "eqeqeq" : true, // Require triple equals i.e. `===`.
+ "forin" : true, // Require `for in` loops with `hasOwnPrototype`.
+ "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
+ "latedef" : true, // Prohibit variable use before definition.
+ "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
+ "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
+ "noempty" : true, // Prohibit use of empty blocks.
+ "nonew" : true, // Prohibit use of constructors for side-effects.
+ "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions.
+ //"undef" : true, // Require all non-global variables be declared before they are used.
+ //"unused" : true, // Warn when variables are created but not used.
+ "trailing" : true, // Prohibit trailing whitespaces.
+ "es3" : true, // Prohibit trailing commas for old IE
+
+ // == Relaxing Options ================================================
+ //
+ // These options allow you to suppress certain types of warnings. Use
+ // them only if you are absolutely positive that you know what you are
+ // doing.
+
+ "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.).
+ "plusplus" : false, // Prohibit use of `++` & `--`.
+ "strict" : false, // Require `use strict` pragma in every file.
+ "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
+ "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
+ "debug" : false, // Allow debugger statements e.g. browser breakpoints.
+ "eqnull" : false, // Tolerate use of `== null`.
+ "es5" : false, // Allow EcmaScript 5 syntax.
+ "esnext" : false, // Allow ES.next specific features such as `const` and `let`.
+ "evil" : false, // Tolerate use of `eval`.
+ "expr" : false, // Tolerate `ExpressionStatement` as Programs.
+ "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside.
+ "globalstrict" : false, // Allow global "use strict" (also enables 'strict').
+ "iterator" : false, // Allow usage of __iterator__ property.
+ "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block.
+ "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
+ "laxcomma" : false, // Suppress warnings about comma-first coding style.
+ "loopfunc" : false, // Allow functions to be defined within loops.
+ "multistr" : false, // Tolerate multi-line strings.
+ "onecase" : false, // Tolerate switches with just one case.
+ "proto" : false, // Tolerate __proto__ property. This property is deprecated.
+ "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
+ "scripturl" : false, // Tolerate script-targeted URLs.
+ "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only.
+ "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
+ "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
+ "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
+ "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function.
+
+ // == Environments ====================================================
+ //
+ // These options pre-define global variables that are exposed by
+ // popular JavaScript libraries and runtime environments—such as
+ // browser or node.js.
+
+ "browser" : true, // Standard browser globals e.g. `window`, `document`.
+ "devel" : false, // Allow development statements e.g. `console.log();`.
+ "jquery" : false, // Enable globals exposed by jQuery JavaScript library.
+ "node" : false, // Enable globals available when code is running inside of the NodeJS runtime environment.
+
+ // == JSLint Legacy ===================================================
+ //
+ // These options are legacy from JSLint. Aside from bug fixes they will
+ // not be improved in any way and might be removed at any point.
+
+ "nomen" : false, // Prohibit use of initial or trailing underbars in names.
+ "onevar" : false, // Allow only one `var` statement per function.
+ "passfail" : false, // Stop on first error.
+ "white" : false, // Check against strict whitespace and indentation rules.
+
+ // == Global Variables ====================================================
+ //
+ // These options pre-define global variables specific
+ // to your app that hang off `window`.
+
+ "predef": []
+}
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..6b7f05d7b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - "0.10"
+notifications:
+ email: false
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 000000000..c5fb4f626
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+### The MIT License (MIT)
+
+Copyright (c) 2014 Garth Poitras
+
+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.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..2ed361f0d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# ContentKit-Editor [](https://travis-ci.org/ContentKit/content-kit-editor)
+
+A modern, minimalist WYSIWYG editor.
+
+---
+
+## About ContentKit
+
+ContentKit is a suite of tools used to create, parse, and render user generated content. ContentKit's core centers around parsing content into its own simple JSON format. By storing a set of simple data, you are no longer bound to the originally generated HTML. By separating the data from the presentation, you can render the same content in various different formats, layouts, and on various different platforms.
+
+#### Use-case example:
+You are developing a blogging platform. You allow authors to create posts using a ContentKit's WYSIWYG editor. ContentKit parses the post into its JSON format. Readers then visit the blog on the web and ContentKit renders the post's JSON data back to HTML for display on the page. Next, you decide to build mobile apps for your blogging platform. ContentKit renders the content natively on iOS and Android. Later, your power users want to use Markdown or HTML code to write their blogs posts. ContentKit can parse it while also cleaning up tags you may not want to allow. Later, you redesign the blog. Your original content is intact and is easily able to be rendered into your ambitious new layout.
+
+#### Current Tools:
+- An HTML parser/renderer to transform content to and from JSON/HTML [(ContentKit-Compiler)](https://github.com/ContentKit/content-kit-compiler)
+- A simple, modern WYSIWYG editor to generate content on the web [(ContentKit-Editor)](https://github.com/ContentKit/content-kit-editor)
+
+#### Future Tools:
+- A HTML renderer for Ruby, to pre-render HTML server-side
+- An iOS renderer to display content natively using `UILabel`/`NSAttributedString` [(reference)](https://developer.apple.com/library/mac/documentation/cocoa/reference/foundation/classes/NSAttributedString_Class/Reference/Reference.html)
+- An Android renderer for to display content natively using `TextField`/`Spannable` [(reference)](http://developer.android.com/reference/android/text/Spannable.html)
+- A Markdown parser/renderer to transform content to and from JSON/Markdown
diff --git a/demo/content-kit-compiler.js b/demo/content-kit-compiler.js
new file mode 100755
index 000000000..25a461119
--- /dev/null
+++ b/demo/content-kit-compiler.js
@@ -0,0 +1,569 @@
+/*!
+ * @overview ContentKit-Compiler: Parses HTML to ContentKit's JSON schema and renders back to HTML.
+ * @version 0.1.0
+ * @author Garth Poitras (http://garthpoitras.com/)
+ * @license MIT
+ * Last modified: Jul 10, 2014
+ */
+
+(function(exports, document, undefined) {
+
+'use strict';
+
+/**
+ * @namespace ContentKit
+ */
+var ContentKit = exports.ContentKit || {};
+exports.ContentKit = ContentKit;
+
+/**
+ * @class Type
+ * @private
+ * @constructor
+ * Base class that contains info about an allowed node type (type id, tag, etc).
+ * Only to be subclassed (BlockType, MarkupType)
+ */
+function Type(options, meta) {
+ if (options) {
+ this.id = options.id === undefined ? meta.autoId++ : options.id;
+ meta.idLookup[this.id] = this;
+ this.name = options.name || options.tag;
+ if (options.tag) {
+ this.tag = options.tag;
+ this.selfClosing = /^(br|img|hr|meta|link|embed)$/i.test(this.tag);
+ meta.tagLookup[this.tag] = this;
+ }
+ }
+}
+
+/**
+ * Type static meta properties
+ */
+function TypeMeta() {
+ this.autoId = 1; // Auto-increment id counter
+ this.idLookup = {}; // Hash cache for finding by id
+ this.tagLookup = {}; // Hash cache for finding by tag
+}
+
+/**
+ * Returns type info for a given Node
+ */
+Type.findByNode = function(node) {
+ return this.meta.tagLookup[node.tagName.toLowerCase()];
+};
+
+/**
+ * Returns type info for a given id
+ */
+Type.findById = function(id) {
+ return this.meta.idLookup[id];
+};
+
+/**
+ * @class BlockType
+ * @private
+ * @constructor
+ * @extends Type
+ */
+function BlockType(options) {
+ Type.call(this, options, BlockType.meta);
+}
+BlockType.meta = new TypeMeta();
+inherit(BlockType, Type);
+
+/**
+ * Default supported block node type dictionary
+ */
+var DefaultBlockTypes = {
+ TEXT : new BlockType({ tag: 'p', name: 'text' }),
+ HEADING : new BlockType({ tag: 'h2', name: 'heading' }),
+ SUBHEADING : new BlockType({ tag: 'h3', name: 'subheading' }),
+ IMAGE : new BlockType({ tag: 'img', name: 'image' }),
+ QUOTE : new BlockType({ tag: 'blockquote', name: 'quote' }),
+ LIST : new BlockType({ tag: 'ul', name: 'list' }),
+ ORDERED_LIST : new BlockType({ tag: 'ol', name: 'ordered list' }),
+ EMBED : new BlockType({ name: 'embed' }),
+ GROUP : new BlockType({ name: 'group' })
+};
+
+/**
+ * @class MarkupType
+ * @private
+ * @constructor
+ * @extends Type
+ */
+function MarkupType(options) {
+ Type.call(this, options, MarkupType.meta);
+}
+MarkupType.meta = new TypeMeta();
+inherit(MarkupType, Type);
+
+/**
+ * Default supported markup type dictionary
+ */
+var DefaultMarkupTypes = {
+ BOLD : new MarkupType({ tag: 'b', name: 'bold' }),
+ ITALIC : new MarkupType({ tag: 'i', name: 'italic' }),
+ UNDERLINE : new MarkupType({ tag: 'u', name: 'underline' }),
+ LINK : new MarkupType({ tag: 'a', name: 'link' }),
+ BREAK : new MarkupType({ tag: 'br', name: 'break' }),
+ LIST_ITEM : new MarkupType({ tag: 'li', name: 'list item' }),
+ SUBSCRIPT : new MarkupType({ tag: 'sub', name: 'subscript' }),
+ SUPERSCRIPT : new MarkupType({ tag: 'sup', name: 'superscript' })
+};
+
+/**
+ * Converts an array-like object (i.e. NodeList) to Array
+ */
+function toArray(obj) {
+ var array = [],
+ i = obj.length >>> 0; // cast to Uint32
+ while (i--) {
+ array[i] = obj[i];
+ }
+ return array;
+}
+
+/**
+ * Computes the sum of values in an array
+ */
+function sumArray(array) {
+ var sum = 0, i, num;
+ for (i in array) { // 'for in' best for sparse arrays
+ sum += array[i];
+ }
+ return sum;
+}
+
+/**
+ * A document instance separate from the page's document. (if browser supports it)
+ * Prevents images, scripts, and styles from executing while parsing nodes.
+ */
+var doc = (function() {
+ var implementation = document.implementation,
+ createHTMLDocument = implementation.createHTMLDocument;
+ if (createHTMLDocument) {
+ return createHTMLDocument.call(implementation, '');
+ }
+ return document;
+})();
+
+/**
+ * A reusable DOM Node for parsing html content.
+ */
+var parserNode = doc.createElement('div');
+
+/**
+ * Returns plain-text of a `Node`
+ */
+function textOfNode(node) {
+ var text = node.textContent || node.innerText;
+ return text ? sanitizeWhitespace(text) : '';
+}
+
+/**
+ * Replaces a `Node` with it with its children
+ */
+function unwrapNode(node) {
+ var children = toArray(node.childNodes),
+ len = children.length,
+ parent = node.parentNode, i;
+ for (i = 0; i < len; i++) {
+ parent.insertBefore(children[i], node);
+ }
+}
+
+/**
+ * Extracts attributes of a `Node` to a hash of key/value pairs
+ */
+function attributesForNode(node /*,blacklist*/) {
+ var attrs = node.attributes,
+ len = attrs && attrs.length,
+ i, attr, name, hash;
+ for (i = 0; i < len; i++) {
+ attr = attrs[i];
+ name = attr.name;
+ if (attr.specified) {
+ //if (blacklist && name in blacklist)) { continue; }
+ hash = hash || {};
+ hash[name] = attr.value;
+ }
+ }
+ return hash;
+}
+
+/**
+ * Merges set of properties on a object
+ * Useful for constructor defaults/options
+ */
+function merge(object, defaults, updates) {
+ updates = updates || {};
+ for(var o in defaults) {
+ if (defaults.hasOwnProperty(o)) {
+ object[o] = updates[o] || defaults[o];
+ }
+ }
+}
+
+/**
+ * Prototype inheritance helper
+ */
+function inherit(Sub, Super) {
+ for (var key in Super) {
+ if (Super.hasOwnProperty(key)) {
+ Sub[key] = Super[key];
+ }
+ }
+ Sub.prototype = new Super();
+ Sub.constructor = Sub;
+}
+
+var RegExpTrim = /^\s+|\s+$/g,
+ RegExpTrimLeft = /^\s+/,
+ RegExpWSChars = /(\r\n|\n|\r|\t|\u00A0)/gm,
+ RegExpMultiWS = / +/g;
+
+/**
+ * String.prototype.trim polyfill
+ * Removes whitespace at beginning and end of string
+ */
+function trim(string) {
+ return string ? string.replace(RegExpTrim, '') : '';
+}
+
+/**
+ * String.prototype.trimLeft polyfill
+ * Removes whitespace at beginning of string
+ */
+function trimLeft(string) {
+ return string ? string.replace(RegExpTrimLeft, '') : '';
+}
+
+/**
+ * Cleans line breaks, tabs, non-breaking spaces, then multiple occuring whitespaces.
+ */
+function sanitizeWhitespace(string) {
+ return string.replace(RegExpWSChars, '').replace(RegExpMultiWS, ' ');
+}
+
+/**
+ * Injects a string into another string at the index specified
+ */
+function injectIntoString(string, injection, index) {
+ return string.substr(0, index) + injection + string.substr(index);
+}
+
+/**
+ * @class Compiler
+ * @constructor
+ * @param options
+ */
+function Compiler(options) {
+ var defaults = {
+ parser : new HTMLParser(),
+ renderer : new HTMLRenderer(),
+ blockTypes : DefaultBlockTypes,
+ markupTypes : DefaultMarkupTypes
+ };
+ merge(this, defaults, options);
+}
+
+/**
+ * @method parse
+ * @param input
+ * @return Object
+ */
+Compiler.prototype.parse = function(input) {
+ return this.parser.parse(input);
+};
+
+/**
+ * @method render
+ * @param data
+ * @return Object
+ */
+Compiler.prototype.render = function(data) {
+ return this.renderer.render(data);
+};
+
+ContentKit.Compiler = Compiler;
+
+/**
+ * @class HTMLParser
+ * @constructor
+ */
+function HTMLParser(options) {
+ var defaults = {
+ includeTypeNames : false
+ };
+ merge(this, defaults, options);
+}
+
+/**
+ * @method parse
+ * @param html String of HTML content
+ * @return Array Parsed JSON content array
+ */
+HTMLParser.prototype.parse = function(html) {
+ parserNode.innerHTML = sanitizeWhitespace(html);
+
+ var children = toArray(parserNode.childNodes),
+ len = children.length,
+ blocks = [],
+ i, currentNode, block, text;
+
+ for (i = 0; i < len; i++) {
+ currentNode = children[i];
+ // All top level nodes *should be* `Element` nodes and supported block types.
+ // We'll handle some cases if it isn't so we don't lose any content when parsing.
+ // Parser assumes sane input (such as from the ContentKit Editor) and is not intended to be a full html sanitizer.
+ if (currentNode.nodeType === 1) {
+ block = parseBlock(currentNode, this.includeTypeNames);
+ if (block) {
+ blocks.push(block);
+ } else {
+ handleNonBlockElementAtRoot(currentNode, blocks);
+ }
+ } else if (currentNode.nodeType === 3) {
+ text = textOfNode(currentNode);
+ if (trim(text)) {
+ block = getLastBlockOrCreate(blocks);
+ block.value += text;
+ }
+ }
+ }
+
+ return blocks;
+};
+
+ContentKit.HTMLParser = HTMLParser;
+
+
+/**
+ * Parses a single block type node into json
+ */
+function parseBlock(node, includeTypeNames) {
+ var meta = BlockType.findByNode(node), parsed, attributes;
+ if (meta) {
+ parsed = { type : meta.id };
+ if (includeTypeNames && meta.name) {
+ parsed.type_name = meta.name;
+ }
+ parsed.value = trim(textOfNode(node));
+ attributes = attributesForNode(node);
+ if (attributes) {
+ parsed.attributes = attributes;
+ }
+ parsed.markup = parseBlockMarkup(node, includeTypeNames);
+ return parsed;
+ }
+}
+
+/**
+ * Parses all of the markup in a block type node
+ */
+function parseBlockMarkup(node, includeTypeNames) {
+ var processedText = '',
+ markups = [],
+ index = 0,
+ currentNode, markup;
+
+ while (node.hasChildNodes()) {
+ currentNode = node.firstChild;
+ if (currentNode.nodeType === 1) {
+ markup = parseElementMarkup(currentNode, processedText.length, includeTypeNames);
+ if (markup) {
+ markups.push(markup);
+ }
+ // unwrap the element so we can process any children
+ if (currentNode.hasChildNodes()) {
+ unwrapNode(currentNode);
+ }
+ } else if (currentNode.nodeType === 3) {
+ var text = sanitizeWhitespace(currentNode.nodeValue);
+ if (index === 0) { text = trimLeft(text); }
+ if (text) { processedText += text; }
+ }
+
+ // node has been processed, remove it
+ currentNode.parentNode.removeChild(currentNode);
+ index++;
+ }
+
+ return markups;
+}
+
+/**
+ * Parses markup of a single html element node
+ */
+function parseElementMarkup(node, startIndex, includeTypeNames) {
+ var meta = MarkupType.findByNode(node),
+ selfClosing, endIndex, markup, attributes;
+
+ if (meta) {
+ selfClosing = meta.selfClosing;
+ if (!selfClosing && !node.hasChildNodes()) { return; } // check for empty nodes
+
+ endIndex = startIndex + (selfClosing ? 0 : textOfNode(node).length);
+ if (endIndex > startIndex || (selfClosing && endIndex === startIndex)) { // check for empty nodes
+ markup = { type : meta.id };
+ if (includeTypeNames && meta.name) {
+ markup.type_name = meta.name;
+ }
+ markup.start = startIndex;
+ markup.end = endIndex;
+ attributes = attributesForNode(node);
+ if (attributes) {
+ markup.attributes = attributes;
+ }
+ return markup;
+ }
+ }
+}
+
+/**
+ * Helper to retain stray elements at the root of the html that aren't blocks
+ */
+function handleNonBlockElementAtRoot(elementNode, blocks) {
+ var block = getLastBlockOrCreate(blocks),
+ markup = parseElementMarkup(elementNode, block.value.length);
+ if (markup) {
+ block.markup = block.markup || [];
+ block.markup.push(markup);
+ }
+ block.value += textOfNode(elementNode);
+}
+
+/**
+ * Gets the last block in the set or creates and return a default block if none exist yet.
+ */
+function getLastBlockOrCreate(blocks) {
+ var block;
+ if (blocks.length) {
+ block = blocks[blocks.length - 1];
+ } else {
+ block = parseBlock(doc.createElement('p'));
+ blocks.push(block);
+ }
+ return block;
+}
+
+/**
+ * @class HTMLRenderer
+ * @constructor
+ */
+function HTMLRenderer(options) {
+ var defaults = {
+ typeRenderers : {}
+ };
+ merge(this, defaults, options);
+}
+
+/**
+ * @method render
+ * @param data
+ * @return String html
+ */
+HTMLRenderer.prototype.render = function(data) {
+ var html = '',
+ len = data && data.length,
+ i, block, typeRenderer, blockHtml;
+
+ for (i = 0; i < len; i++) {
+ block = data[i];
+ typeRenderer = this.typeRenderers[block.type] || renderBlock;
+ blockHtml = typeRenderer(block);
+ if (blockHtml) { html += blockHtml; }
+ }
+ return html;
+};
+
+/**
+ * @method willRenderType
+ * @param type type id
+ * @param renderer the rendering function that returns a string of html
+ * Registers custom rendering for a type
+ */
+HTMLRenderer.prototype.willRenderType = function(type, renderer) {
+ this.typeRenderers[type] = renderer;
+};
+
+ContentKit.HTMLRenderer = HTMLRenderer;
+
+
+/**
+ * Builds an opening html tag. i.e. ''
+ */
+function createOpeningTag(tagName, attributes, selfClosing /*,blacklist*/) {
+ var tag = '<' + tagName;
+ for (var attr in attributes) {
+ if (attributes.hasOwnProperty(attr)) {
+ //if (blacklist && attr in blacklist) { continue; }
+ tag += ' ' + attr + '="' + attributes[attr] + '"';
+ }
+ }
+ if (selfClosing) { tag += '/'; }
+ tag += '>';
+ return tag;
+}
+
+/**
+ * Builds a closing html tag. i.e. '
'
+ */
+function createCloseTag(tagName) {
+ return '' + tagName + '>';
+}
+
+/**
+ * Renders a block's json into a HTML string.
+ */
+function renderBlock(block) {
+ var blockMeta = BlockType.findById(block.type),
+ html = '', tagName, selfClosing;
+
+ if (blockMeta) {
+ tagName = blockMeta.tag;
+ selfClosing = blockMeta.selfClosing;
+ html += createOpeningTag(tagName, block.attributes, selfClosing);
+ if (!selfClosing) {
+ html += renderMarkup(block.value, block.markup);
+ html += createCloseTag(tagName);
+ }
+ }
+ return html;
+}
+
+/**
+ * Renders markup json into a HTML string.
+ */
+function renderMarkup(text, markups) {
+ var parsedTagsIndexes = [],
+ len = markups && markups.length, i;
+
+ for (i = 0; i < len; i++) {
+ var markup = markups[i],
+ markupMeta = MarkupType.findById(markup.type),
+ tagName = markupMeta.tag,
+ selfClosing = markupMeta.selfClosing,
+ start = markup.start,
+ end = markup.end,
+ openTag = createOpeningTag(tagName, markup.attributes, selfClosing),
+ parsedTagLengthAtIndex = parsedTagsIndexes[start] || 0,
+ parsedTagLengthBeforeIndex = sumArray(parsedTagsIndexes.slice(0, start + 1));
+
+ text = injectIntoString(text, openTag, start + parsedTagLengthBeforeIndex);
+ parsedTagsIndexes[start] = parsedTagLengthAtIndex + openTag.length;
+
+ if (!selfClosing) {
+ var closeTag = createCloseTag(tagName);
+ parsedTagLengthAtIndex = parsedTagsIndexes[end] || 0;
+ parsedTagLengthBeforeIndex = sumArray(parsedTagsIndexes.slice(0, end));
+ text = injectIntoString(text, closeTag, end + parsedTagLengthBeforeIndex);
+ parsedTagsIndexes[end] = parsedTagLengthAtIndex + closeTag.length;
+ }
+ }
+
+ return text;
+}
+
+}(this, document));
diff --git a/demo/content-kit-demo.js b/demo/content-kit-demo.js
new file mode 100644
index 000000000..a25e0fb5c
--- /dev/null
+++ b/demo/content-kit-demo.js
@@ -0,0 +1,93 @@
+(function(exports, document, undefined) {
+
+'use strict';
+
+var ContentKit = exports.ContentKit || {};
+exports.ContentKit = ContentKit;
+
+ContentKit.Demo = {
+ toggleCode: function(e, button, editor) {
+ var codeUI = document.getElementById('code-panes'),
+ editorUI = editor.element;
+
+ if(codeUI.style.display === '') {
+ var codePaneJSON = document.getElementById('code-json'),
+ codePaneHTML = document.getElementById('code-html'),
+ json = editor.parse(),
+ html = new ContentKit.HTMLRenderer().render(json); //editor.element.innerHTML;
+
+ codePaneJSON.innerHTML = this.syntaxHighlight(json);
+ codePaneHTML.textContent = this.formatXML(html);
+
+ window.getSelection().removeAllRanges();
+
+ codeUI.style.display = 'block';
+ editorUI.style.display = 'none';
+ button.textContent = 'Show Editor';
+ } else {
+ codeUI.style.display = '';
+ editorUI.style.display = 'block';
+ button.textContent = 'Show Code';
+ }
+ },
+
+ formatXML: function(xml) {
+ // https://gist.github.com/sente/1083506
+ xml = xml.replace(/(>)(<)(\/*)/g, '$1\r\n$2$3');
+ var formatted = '';
+ var pad = 0;
+ var nodes = xml.split('\r\n');
+ var nodeLen = nodes.length;
+ var node, indent, padding, i, j;
+ for(i = 0; i < nodeLen; i++) {
+ node = nodes[i];
+ if (node.match( /.+<\/\w[^>]*>$/ )) {
+ indent = 0;
+ } else if (node.match( /^<\/\w/ )) {
+ if (pad != 0) {
+ pad -= 1;
+ }
+ } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
+ indent = 1;
+ } else {
+ indent = 0;
+ }
+
+ padding = '';
+ for (j = 0; j < pad; j++) {
+ padding += ' ';
+ }
+
+ formatted += padding + node + '\r\n';
+ pad += indent;
+ }
+
+ return formatted;
+ },
+
+ syntaxHighlight: function(json) {
+ // http://stackoverflow.com/a/7220510/189440
+ if (typeof json !== 'string') {
+ json = JSON.stringify(json, undefined, 2);
+ }
+ json = json.replace(/&/g, '&').replace(//g, '>');
+ return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+ var cls = 'json-number';
+ if (/^"/.test(match)) {
+ if (/:$/.test(match)) {
+ cls = 'json-key';
+ } else {
+ cls = 'json-string';
+ }
+ } else if (/true|false/.test(match)) {
+ cls = 'json-boolean';
+ } else if (/null/.test(match)) {
+ cls = 'json-null';
+ }
+ return '' + match + ' ';
+ });
+ }
+
+};
+
+}(this, document));
diff --git a/demo/demo.css b/demo/demo.css
new file mode 100644
index 000000000..b8f48cfac
--- /dev/null
+++ b/demo/demo.css
@@ -0,0 +1,141 @@
+*,
+*:before,
+*:after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Helvetica Neue", Helvetica, sans-serif;
+ color: #333;
+ margin: 0 1.45em;
+ padding: 0;
+}
+@media only screen and (max-width: 767px) {
+ body {
+ font-size: 0.88em;
+ }
+}
+
+.wrapper {
+ max-width: 50em;
+ margin: 6.7em auto 1em;
+}
+
+header, footer {
+ z-index: 10;
+ background-color: rgba(247,247,248,0.92);
+ position: fixed;
+ left:0; right:0;
+ height: 3.125em;
+}
+header {
+ border-bottom: 1px solid #c4c4c4;
+ top: 0;
+}
+footer {
+ border-top: 1px solid #c4c4c4;
+ bottom: 0;
+}
+
+.demo-title {
+ text-align: center;
+ font-size: 1.4em;
+ letter-spacing: -0.035em;
+ padding: 0;
+ margin: 0 auto;
+ width: 50%;
+ line-height: 2.2em;
+}
+
+.mode-buttons {
+ position: absolute;
+ top: 0;
+ right: 0.5em;
+ line-height: 2.9em;
+}
+.mode-buttons button {
+ background-color: transparent;
+ border: 1px solid #007aff;
+ color: #007aff;
+ border-radius: 5px;
+ padding: 0.5em 1.4em;
+ min-width: 8.7em;
+ font-size: 0.75em;
+ cursor: pointer;
+ transition: background-color 0.15s;
+}
+.mode-buttons button:hover {
+ background-color: rgba(0,122,255,0.15);
+}
+.mode-buttons button:active {
+ background-color: #007aff;
+ color: #FFF;
+ transition: none;
+}
+.mode-buttons button:focus {
+ outline: none;
+ cursor: pointer;
+}
+
+#code-panes {
+ display: none;
+}
+.code-pane {
+ position: absolute;
+ top: 3.125em;
+ bottom: 0;
+ right: 0;
+ width: 50%;
+ border-left: 1px dotted #c0c5ce;
+ background-color: #2b303b;
+ overflow: hidden;
+}
+.code-pane:first-child {
+ left: 0;
+ right: auto;
+ border-left: none;
+}
+.code-pane code {
+ white-space: pre-wrap;
+ font-family: Consolas, Menlo, Courier, monospace;
+ font-size: 0.8em;
+ line-height: 1.4em;
+ background-color: transparent;
+ color: #c0c5ce;
+ padding: 1.5em 1em 1em;
+ overflow: auto;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+}
+.code-pane label {
+ font-size: 0.8em;
+ color: #c0c5ce;
+ background: rgba(30,40,48,0.92);
+ padding: 0.5em 0.75em;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ right: 0;
+ border-radius: 0 0 0 3px;
+ border-left: 1px solid rgba(192,197,206,0.25);
+ border-bottom: 1px solid rgba(192,197,206,0.25);
+}
+
+.json-key {
+ color: #b48ead;
+}
+.json-number {
+ color: #8fa1b3;
+}
+.json-string {
+ color: #c0c5ce;
+}
+.json-boolean {
+ color: #bf616a;
+}
+.json-null {
+ color: #bf616a;
+}
diff --git a/demo/index.html b/demo/index.html
new file mode 100644
index 000000000..03e71890c
--- /dev/null
+++ b/demo/index.html
@@ -0,0 +1,73 @@
+
+
+
+
+ ContentKit Editor
+
+
+
+
+
+
+ ContentKit Editor
+
+ Show Code
+
+
+
+
+
+
A modern, minimalist text editor allowing you to write in a distraction free environment. Simply select any text you would like to format and a toolbar will appear where you can toggle options such as bold and italic , or create a link .
+
Create headings by pressing "H1" on the toolbar
+
Pressing "H2" will create a subheading, like this one.
+
Create block quotes by selecting any text and pressing the "quote" button. Press it again to toggle back to a standard paragraph.
+
To create a list , start typing a dash followed by a space ("- " ) on a new line and a list will be automatically created.
+
To create an ordered list , start typing a one followed by a period and a space ("1. " ) and the list will be automatically created.
+
+
Tips & Tricks:
+
+ Pressing enter creates a new paragraph
+ To create a soft line break, hold shift while pressing enter
+ Close the formatting toolbar by clicking anywhere, or press ESC
+ Clicking an active format button, will remove that format
+ Double click a word to select it
+ You only have to select a portion of a paragraph if you want to change it to a heading, subheading, or quote
+ Press enter twice to exit a list
+
+
+
Keyboard shortcuts:
+
+ bold : (cmd+B)
+ italic : (cmd+I)
+ undo: (cmd+z)
+ redo: (cmd+y)
+ select all text: (cmd+a)
+ select letters: (hold shift + arrow keys)
+ close toolbar: (ESC)
+
+
+
+
+
+
+ ContentKit JSON
+
+
+
+ HTML
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dist/content-kit-editor.js b/dist/content-kit-editor.js
new file mode 100644
index 000000000..856d72fd5
--- /dev/null
+++ b/dist/content-kit-editor.js
@@ -0,0 +1,812 @@
+/*!
+ * @overview ContentKit-Editor: A modern, minimalist WYSIWYG editor.
+ * @version 0.1.0
+ * @author Garth Poitras (http://garthpoitras.com/)
+ * @license MIT
+ * Last modified: Jul 10, 2014
+ */
+
+(function(exports, document) {
+
+'use strict';
+
+/**
+ * @namespace ContentKit
+ */
+var ContentKit = exports.ContentKit || {};
+exports.ContentKit = ContentKit;
+
+var Keycodes = {
+ ENTER : 13,
+ ESC : 27
+};
+
+var Regex = {
+ NEWLINE : /[\r\n]/g,
+ HTTP_PROTOCOL : /^https?:\/\//i,
+ HEADING_TAG : /^(h1|h2|h3|h4|h5|h6)$/i,
+ UL_START : /^[-*]\s/,
+ OL_START : /^1\.\s/
+};
+
+var SelectionDirection = {
+ LEFT_TO_RIGHT : 0,
+ RIGHT_TO_LEFT : 1,
+ SAME_NODE : 2
+};
+
+var Tags = {
+ LINK : 'a',
+ PARAGRAPH : 'p',
+ HEADING : 'h2',
+ SUBHEADING : 'h3',
+ QUOTE : 'blockquote',
+ LIST : 'ul',
+ ORDERED_LIST : 'ol',
+ LIST_ITEM : 'li'
+};
+
+var RootTags = [ Tags.PARAGRAPH, Tags.HEADING, Tags.SUBHEADING, Tags.QUOTE, Tags.LIST, Tags.ORDERED_LIST, 'div'];
+
+function getNodeTagName(node) {
+ return node.tagName && node.tagName.toLowerCase() || null;
+}
+
+function getDirectionOfSelection(selection) {
+ var position = selection.anchorNode.compareDocumentPosition(selection.focusNode);
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
+ return SelectionDirection.LEFT_TO_RIGHT;
+ } else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
+ return SelectionDirection.RIGHT_TO_LEFT;
+ }
+ return SelectionDirection.SAME_NODE;
+}
+
+function getCurrentSelectionNode() {
+ var selection = window.getSelection();
+ var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.anchorNode : selection.focusNode;
+ return node && (node.nodeType === 3 ? node.parentNode : node);
+}
+
+function getCurrentSelectionRootNode() {
+ var node = getCurrentSelectionNode(),
+ tag = getNodeTagName(node);
+ while (tag && RootTags.indexOf(tag) === -1) {
+ node = node.parentNode;
+ tag = getNodeTagName(node);
+ }
+ return node;
+}
+
+function getCurrentSelectionTag() {
+ return getNodeTagName(getCurrentSelectionNode());
+}
+
+function getCurrentSelectionRootTag() {
+ return getNodeTagName(getCurrentSelectionRootNode());
+}
+
+function getElementOffset(element) {
+ var offset = { left: 0, top: 0 };
+ var elementStyle = window.getComputedStyle(element);
+
+ if (elementStyle.position === 'relative') {
+ offset.left = parseInt(elementStyle['margin-left'], 10);
+ offset.top = parseInt(elementStyle['margin-top'], 10);
+ }
+ return offset;
+}
+
+function createDiv(className) {
+ var div = document.createElement('div');
+ if (className) {
+ div.className = className;
+ }
+ return div;
+}
+
+function extend(object, updates) {
+ updates = updates || {};
+ for(var o in updates) {
+ if (updates.hasOwnProperty(o)) {
+ object[o] = updates[o];
+ }
+ }
+ return object;
+}
+
+function applyConstructorProperties(instance, props) {
+ for(var p in props) {
+ if (props.hasOwnProperty(p)) {
+ instance[p] = props[p];
+ }
+ }
+}
+
+function inherits(Subclass, Superclass) {
+ Subclass._super = Superclass;
+ Subclass.prototype = Object.create(Superclass.prototype, {
+ constructor: {
+ value: Subclass,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+}
+
+function moveCursorToBeginningOfSelection(selection) {
+ var range = document.createRange(),
+ node = selection.anchorNode;
+ range.setStart(node, 0);
+ range.setEnd(node, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function restoreRange(range) {
+ var selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function selectNode(node) {
+ var range = document.createRange(),
+ selection = window.getSelection();
+ range.setStart(node, 0);
+ range.setEnd(node, node.length);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+var Prompt = (function() {
+
+ var container = document.body;
+ var hiliter = createDiv('ck-editor-hilite');
+
+ function Prompt(options) {
+ if (options) {
+ var prompt = this;
+ var element = document.createElement('input');
+ this.placeholder = options.placeholder;
+ this.command = options.command;
+ this.element = element;
+ element.type = 'text';
+ element.placeholder = this.placeholder;
+ element.addEventListener('mouseup', function(e) { e.stopPropagation(); }); // prevents closing prompt when clicking input
+ element.addEventListener('keyup', function(e) {
+ var entry = this.value;
+ if(entry && !e.shiftKey && e.which === Keycodes.ENTER) {
+ restoreRange(prompt.range);
+ prompt.command.exec(entry);
+ if (prompt.onComplete) { prompt.onComplete(); }
+ }
+ });
+ }
+ }
+
+ Prompt.prototype = {
+ display: function(callback) {
+ this.range = window.getSelection().getRangeAt(0); // save the selection range
+ hiliteRange(this.range);
+ this.clear();
+ var element = this.element;
+ setTimeout(function(){ element.focus(); }); // defer focus (disrupts mouseup events)
+ if (callback) { this.onComplete = callback; }
+ },
+ dismiss: function() {
+ this.clear();
+ unhiliteRange();
+ },
+ clear: function() {
+ this.element.value = null;
+ }
+ };
+
+ function hiliteRange(range) {
+ var rangeBounds = range.getBoundingClientRect();
+ var hiliterStyle = hiliter.style;
+ var offset = getElementOffset(container);
+
+ hiliterStyle.width = rangeBounds.width + 'px';
+ hiliterStyle.height = rangeBounds.height + 'px';
+ hiliterStyle.left = rangeBounds.left - offset.left + 'px';
+ hiliterStyle.top = rangeBounds.top + window.pageYOffset - offset.top + 'px';
+ container.appendChild(hiliter);
+ }
+
+ function unhiliteRange() {
+ container.removeChild(hiliter);
+ }
+
+ return Prompt;
+}());
+
+function Command(options) {
+ if(options) {
+ var name = options.name;
+ var prompt = options.prompt;
+ this.name = name;
+ this.tag = options.tag;
+ this.action = options.action || name;
+ this.removeAction = options.removeAction || options.action;
+ this.button = options.button || name;
+ if (prompt) { this.prompt = prompt; }
+ }
+}
+Command.prototype.exec = function(value) {
+ document.execCommand(this.action, false, value || null);
+};
+Command.prototype.unexec = function(value) {
+ document.execCommand(this.removeAction, false, value || null);
+};
+
+function BoldCommand() {
+ Command.call(this, {
+ name: 'bold',
+ tag: 'b',
+ button: ' '
+ });
+}
+inherits(BoldCommand, Command);
+BoldCommand.prototype.exec = function() {
+ // Don't allow executing bold command on heading tags
+ if (!Regex.HEADING_TAG.test(getCurrentSelectionRootTag())) {
+ BoldCommand._super.prototype.exec.call(this);
+ }
+};
+
+function ItalicCommand() {
+ Command.call(this, {
+ name: 'italic',
+ tag: 'i',
+ button: ' '
+ });
+}
+inherits(ItalicCommand, Command);
+
+function LinkCommand() {
+ Command.call(this, {
+ name: 'link',
+ tag: Tags.LINK,
+ action: 'createLink',
+ removeAction: 'unlink',
+ button: ' ',
+ prompt: new Prompt({
+ command: this,
+ placeholder: 'Enter a url, press return...'
+ })
+ });
+}
+inherits(LinkCommand, Command);
+LinkCommand.prototype.exec = function(url) {
+ if(this.tag === getCurrentSelectionTag()) {
+ this.unexec();
+ } else {
+ if (!Regex.HTTP_PROTOCOL.test(url)) {
+ url = 'http://' + url;
+ }
+ LinkCommand._super.prototype.exec.call(this, url);
+ }
+};
+
+function FormatBlockCommand(options) {
+ options.action = 'formatBlock';
+ Command.call(this, options);
+}
+inherits(FormatBlockCommand, Command);
+FormatBlockCommand.prototype.exec = function() {
+ var tag = this.tag;
+ // Brackets neccessary for certain browsers
+ var value = '<' + tag + '>';
+ // Allow block commands to be toggled back to a paragraph
+ if(tag === getCurrentSelectionRootTag()) {
+ value = Tags.PARAGRAPH;
+ } else {
+ // Flattens the selection before applying the block format.
+ // Otherwise, undesirable nested blocks can occur.
+ var root = getCurrentSelectionRootNode();
+ var flatNode = document.createTextNode(root.textContent);
+ root.parentNode.insertBefore(flatNode, root);
+ root.parentNode.removeChild(root);
+ selectNode(flatNode);
+ }
+
+ FormatBlockCommand._super.prototype.exec.call(this, value);
+};
+
+function QuoteCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'quote',
+ tag: Tags.QUOTE,
+ button: ' '
+ });
+}
+inherits(QuoteCommand, FormatBlockCommand);
+
+function HeadingCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'heading',
+ tag: Tags.HEADING,
+ button: ' 1'
+ });
+}
+inherits(HeadingCommand, FormatBlockCommand);
+
+function SubheadingCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'subheading',
+ tag: Tags.SUBHEADING,
+ button: ' 2'
+ });
+}
+inherits(SubheadingCommand, FormatBlockCommand);
+
+function ListCommand(options) {
+ Command.call(this, options);
+}
+inherits(ListCommand, Command);
+ListCommand.prototype.exec = function() {
+ ListCommand._super.prototype.exec.call(this);
+
+ // After creation, lists need to be unwrapped from the default formatter P tag
+ var listNode = getCurrentSelectionRootNode();
+ var wrapperNode = listNode.parentNode;
+ if (wrapperNode.firstChild === listNode) {
+ var editorNode = wrapperNode.parentNode;
+ editorNode.insertBefore(listNode, wrapperNode);
+ editorNode.removeChild(wrapperNode);
+ selectNode(listNode);
+ }
+};
+
+function UnorderedListCommand() {
+ ListCommand.call(this, {
+ name: 'list',
+ tag: Tags.LIST,
+ action: 'insertUnorderedList',
+ button: ' '
+ });
+}
+inherits(UnorderedListCommand, ListCommand);
+
+function OrderedListCommand() {
+ ListCommand.call(this, {
+ name: 'ordered list',
+ tag: Tags.ORDERED_LIST,
+ action: 'insertOrderedList',
+ button: ' '
+ });
+}
+inherits(OrderedListCommand, ListCommand);
+
+Command.all = [
+ new BoldCommand(),
+ new ItalicCommand(),
+ new LinkCommand(),
+ new QuoteCommand(),
+ new HeadingCommand(),
+ new SubheadingCommand()
+];
+
+Command.index = (function() {
+ var index = {},
+ commands = Command.all,
+ len = commands.length, i, command;
+ for(i = 0; i < len; i++) {
+ command = commands[i];
+ index[command.name] = command;
+ }
+ return index;
+})();
+
+ContentKit.Editor = (function() {
+
+ // Default `Editor` options
+ var defaults = {
+ defaultFormatter: Tags.PARAGRAPH,
+ placeholder: 'Write here...',
+ spellcheck: true,
+ autofocus: true,
+ commands: Command.all
+ };
+
+ var editorClassName = 'ck-editor',
+ editorClassNameRegExp = new RegExp(editorClassName);
+
+ /**
+ * Publically expose this class which sets up indiviual `Editor` classes
+ * depending if user passes string selector, Node, or NodeList
+ */
+ function EditorFactory(element, options) {
+ var editors = [],
+ elements, elementsLen, i;
+
+ if (typeof element === 'string') {
+ elements = document.querySelectorAll(element);
+ } else if (element && element.length) {
+ elements = element;
+ } else if (element) {
+ elements = [element];
+ }
+
+ if (elements) {
+ options = extend(defaults, options);
+ elementsLen = elements.length;
+ for (i = 0; i < elementsLen; i++) {
+ editors.push(new Editor(elements[i], options));
+ }
+ }
+
+ return editors.length > 1 ? editors : editors[0];
+ }
+
+ /**
+ * @class Editor
+ * An individual Editor
+ * @param element `Element` node
+ * @param options hash of options
+ */
+ function Editor(element, options) {
+ applyConstructorProperties(this, options);
+
+ if (element) {
+ var className = element.className;
+ var dataset = element.dataset;
+
+ if (!editorClassNameRegExp.test(className)) {
+ className += (className ? ' ' : '') + editorClassName;
+ }
+ element.className = className;
+
+ if (!dataset.placeholder) {
+ dataset.placeholder = this.placeholder;
+ }
+
+ if(!this.spellcheck) {
+ element.spellcheck = false;
+ }
+
+ this.element = element;
+ this.toolbar = new Toolbar({ commands: this.commands });
+
+ bindTextSelectionEvents(this);
+ bindTypingEvents(this);
+ bindPasteEvents(this);
+
+ this.enable();
+ if(this.autofocus) {
+ element.focus();
+ }
+ }
+ }
+
+ Editor.prototype = {
+ enable: function() {
+ var editor = this,
+ element = editor.element;
+ if(element && !editor.enabled) {
+ element.setAttribute('contentEditable', true);
+ editor.enabled = true;
+ }
+ },
+ disable: function() {
+ var editor = this,
+ element = editor.element;
+ if(element && editor.enabled) {
+ element.removeAttribute('contentEditable');
+ editor.enabled = false;
+ }
+ },
+ parse: function() {
+ var editor = this;
+ if (!editor.parser) {
+ if (!ContentKit.HTMLParser) {
+ throw new Error('Include the ContentKit compiler for parsing');
+ }
+ editor.parser = new ContentKit.HTMLParser();
+ }
+ return editor.parser.parse(editor.element.innerHTML);
+ }
+ };
+
+ function bindTextSelectionEvents(editor) {
+ // Mouse text selection
+ document.addEventListener('mouseup', function(e) {
+ setTimeout(function(){ handleTextSelection(e, editor); });
+ });
+
+ // Keyboard text selection
+ editor.element.addEventListener('keyup', function(e) {
+ handleTextSelection(e, editor);
+ });
+ }
+
+ function bindTypingEvents(editor) {
+ var editorEl = editor.element;
+
+ // Breaks out of blockquotes when pressing enter.
+ editorEl.addEventListener('keyup', function(e) {
+ if(!e.shiftKey && e.which === Keycodes.ENTER) {
+ if(Tags.QUOTE === getCurrentSelectionRootTag()) {
+ document.execCommand('formatBlock', false, editor.defaultFormatter);
+ e.stopPropagation();
+ }
+ }
+ });
+
+ // Creates unordered list when block starts with '- ', or ordered if starts with '1. '
+ editorEl.addEventListener('keyup', function(e) {
+ var selectedText = window.getSelection().anchorNode.textContent,
+ selection, selectionNode, command, replaceRegex;
+
+ if (Tags.LIST_ITEM !== getCurrentSelectionTag()) {
+ if (Regex.UL_START.test(selectedText)) {
+ command = new UnorderedListCommand();
+ replaceRegex = Regex.UL_START;
+ } else if (Regex.OL_START.test(selectedText)) {
+ command = new OrderedListCommand();
+ replaceRegex = Regex.OL_START;
+ }
+
+ if (command) {
+ command.exec();
+ selection = window.getSelection();
+ selectionNode = selection.anchorNode;
+ selectionNode.textContent = selectedText.replace(replaceRegex, '');
+ moveCursorToBeginningOfSelection(selection);
+ e.stopPropagation();
+ }
+ }
+ });
+
+ // Assure there is always a paragraph and not divs
+ editorEl.addEventListener('keyup', function() {
+ var node = getCurrentSelectionRootNode();
+ // TODO: support root or other block element
+ if(getNodeTagName(node) === 'div' && node.innerHTML !== '') {
+ document.execCommand('formatBlock', false, editor.defaultFormatter);
+ }
+ });
+ }
+
+ function handleTextSelection(e, editor) {
+ var selection = window.getSelection();
+ if (!selection.isCollapsed && selectionIsInElement(editor.element, selection)) {
+ editor.toolbar.updateForSelection(selection);
+ } else {
+ editor.toolbar.hide();
+ }
+ }
+
+ function selectionIsInElement(element, selection) {
+ var node = selection.focusNode,
+ parentNode = node.parentNode;
+ while(parentNode) {
+ if (parentNode === element) {
+ return true;
+ }
+ parentNode = parentNode.parentNode;
+ }
+ return false;
+ }
+
+ function bindPasteEvents(editor) {
+ editor.element.addEventListener('paste', function(e) {
+ var data = e.clipboardData, plainText;
+ e.preventDefault();
+ if(data && data.getData) {
+ plainText = data.getData('text/plain');
+ var formattedContent = plainTextToBlocks(plainText, editor.defaultFormatter);
+ document.execCommand('insertHTML', false, formattedContent);
+ }
+ });
+ }
+
+ function plainTextToBlocks(plainText, blockTag) {
+ var blocks = plainText.split(Regex.NEWLINE),
+ len = blocks.length,
+ block, openTag, closeTag, content, i;
+ if(len < 2) {
+ return plainText;
+ } else {
+ content = '';
+ openTag = '<' + blockTag + '>';
+ closeTag = '' + blockTag + '>';
+ for(i=0; i -1) {
+ button.setActive();
+ } else {
+ button.setInactive();
+ }
+ }
+ }
+
+ function tagsInSelection(selection) {
+ var node = selection.focusNode.parentNode,
+ tags = [];
+
+ if (!selection.isCollapsed) {
+ while(node) {
+ // Stop traversing up dom when hitting an editor element
+ if (node.contentEditable === 'true') { break; }
+ if (node.tagName) {
+ tags.push(node.tagName.toLowerCase());
+ }
+ node = node.parentNode;
+ }
+ }
+ return tags;
+ }
+
+ return Toolbar;
+}());
+
+var ToolbarButton = (function() {
+
+ var buttonClassName = 'ck-toolbar-btn';
+
+ function ToolbarButton(options) {
+ var toolbar = options.toolbar,
+ command = options.command,
+ prompt = command.prompt,
+ element = document.createElement('button'),
+ button = this;
+
+ if(typeof command === 'string') {
+ command = Command.index[command];
+ }
+
+ element.title = command.name;
+ element.className = buttonClassName;
+ element.innerHTML = command.button;
+ element.addEventListener('click', function() {
+ if (!button.isActive && prompt) {
+ toolbar.displayPrompt(prompt);
+ } else {
+ command.exec();
+ }
+ });
+ this.element = element;
+ this.command = command;
+ this.isActive = false;
+ }
+
+ ToolbarButton.prototype = {
+ setActive: function() {
+ if (!this.isActive) {
+ this.element.className = buttonClassName + ' active';
+ this.isActive = true;
+ }
+ },
+ setInactive: function() {
+ if (this.isActive) {
+ this.element.className = buttonClassName;
+ this.isActive = false;
+ }
+ }
+ };
+
+ return ToolbarButton;
+}());
+
+}(this, document));
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 000000000..be22a1af5
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,86 @@
+var gulp = require('gulp');
+var jshint = require('gulp-jshint');
+var qunit = require('gulp-qunit');
+var concat = require('gulp-concat');
+var uglify = require('gulp-uglify');
+var rename = require('gulp-rename');
+var header = require('gulp-header');
+var footer = require('gulp-footer');
+var util = require('gulp-util');
+var open = require('gulp-open');
+
+var pkg = require('./package.json');
+
+var src = [
+ './src/js/index.js',
+ './src/js/constants.js',
+ './src/js/utils.js',
+ './src/js/prompt.js',
+ './src/js/commands.js',
+ './src/js/editor.js',
+ './src/js/toolbar.js',
+ './src/js/toolbar-button.js'
+];
+
+var distName = 'content-kit-editor.js';
+var distDest = './dist/';
+var distPath = distDest + distName;
+
+var testRunner = './tests/index.html';
+
+var banner = ['/*!',
+ ' * @overview <%= pkg.name %>: <%= pkg.description %>',
+ ' * @version <%= pkg.version %>',
+ ' * @author <%= pkg.author %>',
+ ' * @license <%= pkg.license %>',
+ ' * Last modified: ' + util.date('mmm d, yyyy'),
+ ' */',
+ ''].join('\n');
+
+var iifeHeader = ['',
+ '(function(exports, document) {',
+ '',
+ '\'use strict\';',
+ '',
+ ''].join('\n');
+var iifeFooter = ['',
+ '}(this, document));',
+ ''].join('\n');
+
+gulp.task('lint', function() {
+ gulp.src(src)
+ .pipe(jshint('.jshintrc'))
+ .pipe(jshint.reporter('default'));
+});
+
+gulp.task('lint-built', function() {
+ return gulp.src(distPath)
+ .pipe(jshint('.jshintrc'))
+ .pipe(jshint.reporter('default'));
+});
+
+gulp.task('build', function() {
+ gulp.src(src)
+ .pipe(concat(distName))
+ .pipe(header(iifeHeader))
+ .pipe(header(banner, { pkg : pkg } ))
+ .pipe(footer(iifeFooter))
+ .pipe(gulp.dest(distDest));
+});
+
+gulp.task('test', function() {
+ return gulp.src(testRunner)
+ .pipe(qunit());
+});
+
+gulp.task('test-browser', function(){
+ return gulp.src(testRunner)
+ .pipe(open('<% file.path %>'));
+});
+
+gulp.task('watch', function() {
+ gulp.watch(src, ['build']);
+});
+
+
+gulp.task('default', ['lint', 'build', 'lint-built', 'test']);
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..830c21fb3
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "ContentKit-Editor",
+ "version": "0.1.0",
+ "description": "A modern, minimalist WYSIWYG editor.",
+ "repository": "https://github.com/ContentKit/content-kit-editor",
+ "main": "dist/content-kit-editor.js",
+ "scripts": {
+ "test": "gulp test"
+ },
+ "keywords": [
+ "html",
+ "json",
+ "wysiwyg",
+ "editor",
+ "contenteditable"
+ ],
+ "author": "Garth Poitras (http://garthpoitras.com/)",
+ "license": "MIT",
+ "devDependencies": {
+ "gulp": "^3.8.1",
+ "gulp-concat": "~2.1.7",
+ "gulp-footer": "~1.0.3",
+ "gulp-header": "~1.0.2",
+ "gulp-jshint": "~1.3.4",
+ "gulp-open": "^0.2.8",
+ "gulp-qunit": "^0.3.3",
+ "gulp-rename": "~0.2.2",
+ "gulp-uglify": "~0.2.0",
+ "gulp-util": "~2.2.12"
+ }
+}
diff --git a/src/css/editor.css b/src/css/editor.css
new file mode 100644
index 000000000..9b72b7946
--- /dev/null
+++ b/src/css/editor.css
@@ -0,0 +1,331 @@
+.ck-editor {
+ font-family: Georgia, serif;
+ margin: 1em 0;
+ color: #333;
+ /*
+ Chrome bug adds inline styles when backspacing to join 2 blocks.
+ Fix: Apply font styles to parent element, or use % for font-size, line-height.
+ http://stackoverflow.com/questions/15015019/prevent-chrome-from-wrapping-contents-of-joined-p-with-a-span
+ */
+ font-size: 120%;
+ line-height: 150%;
+}
+.ck-editor:focus {
+ outline: none;
+}
+.ck-editor:empty:before {
+ content: attr(data-placeholder);
+ color: #bbb;
+}
+.ck-editor a {
+ color: #40b855;
+}
+.ck-editor blockquote {
+ border-left: 4px solid #4CD964;
+ margin: 0 0 0 -1.2em;
+ padding-left: 1.05em;
+ color: #a0a0a0;
+}
+
+.ck-toolbar {
+ text-align: center;
+ position: absolute;
+ z-index: 1;
+ padding-bottom: 0.5em; /* space for arrows */
+}
+
+.ck-toolbar-buttons,
+.ck-toolbar-prompt {
+ background: -moz-linear-gradient(top, rgba(74,74,74,0.97) 0%, rgba(43,43,43,1) 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(74,74,74,0.97)), color-stop(100%,rgba(43,43,43,1)));
+ background: -webkit-linear-gradient(top, rgba(74,74,74,0.97) 0%,rgba(43,43,43,1) 100%);
+ background: -o-linear-gradient(top, rgba(74,74,74,0.97) 0%,rgba(43,43,43,1) 100%);
+ background: -ms-linear-gradient(top, rgba(74,74,74,0.97) 0%,rgba(43,43,43,1) 100%);
+ background: linear-gradient(to bottom, rgba(74,74,74,0.97) 0%,rgba(43,43,43,1) 100%);
+ box-shadow: 0 1px 3px -1px rgba(0,0,0,0.8), inset 0 2px 0 rgba(255,255,255,0.12), inset 0 1px 0 rgba(0,0,0,0.65);
+ border-radius: 5px;
+ position: relative;
+ -webkit-animation: toolbarPop 0.5s linear both;
+ animation: toolbarPop 0.5s linear both;
+}
+/* toolbar arrows */
+.ck-toolbar-buttons:after,
+.ck-toolbar-prompt:after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ width: 0;
+ height: 0;
+ border-left: 0.5em solid transparent;
+ border-right: 0.5em solid transparent;
+ border-top: 0.5em solid rgba(43,43,43,1);
+ bottom: -0.5em;
+ margin-left: -0.5em;
+}
+
+.ck-toolbar-btn {
+ background-color: transparent;
+ border: none;
+ outline: none;
+ color: #FFF;
+ font-size: 18px;
+ padding: 0;
+ margin: 0;
+ width: 48px;
+ height: 44px;
+ line-height: 44px;
+ cursor: pointer;
+ transition: background-color 0.15s;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+}
+.ck-toolbar-btn:hover {
+ background-color: rgba(43,43,43,0.5);
+}
+.ck-toolbar-btn:active {
+ background-color: rgba(43,43,43,0.75);
+}
+.ck-toolbar-btn:active,
+.ck-toolbar-btn.active {
+ color: #4CD964;
+}
+.ck-toolbar-btn:first-child {
+ border-radius: 5px 0 0 5px;
+}
+.ck-toolbar-btn:last-child {
+ border-radius: 0 5px 5px 0;
+}
+
+.ck-toolbar-prompt {
+ display: none;
+}
+.ck-toolbar-prompt input {
+ background: none;
+ border: none;
+ color: #f5f5f5;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 15px;
+ padding: 0 16px;
+ width: 288px;
+ height: 44px;
+}
+.ck-toolbar-prompt input:focus {
+ outline: none;
+}
+.ck-toolbar-prompt ::-webkit-input-placeholder {
+ background-color: #a2a2a2;
+ background-image: -webkit-gradient(linear,left top,right top,color-stop(0, #a2a2a2),color-stop(0.4, #a2a2a2),color-stop(0.5, white),color-stop(0.6, #a2a2a2),color-stop(1, #a2a2a2));
+ background-repeat: no-repeat;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ -webkit-animation: textGlimmer 4s infinite;
+}
+
+.ck-editor-hilite {
+ position: absolute;
+ z-index: -1;
+ background-color: rgba(76,217,100,0.05);
+ border-bottom: 2px dotted #4CD964;
+ -webkit-animation: hiliteAppear 0.2s;
+ animation: hiliteAppear 0.2s;
+}
+
+/* icons */
+@font-face {
+ font-family: 'ck-icons';
+ src: url('fonts/ck-icons.eot');
+}
+@font-face {
+ font-family: 'ck-icons';
+ src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjCJIAAAC8AAAAYGNtYXDUA9LlAAABHAAAAHRnYXNwAAAAEAAAAZAAAAAIZ2x5ZqGnie0AAAGYAAAL4GhlYWQAvnP3AAANeAAAADZoaGVhA+IB7QAADbAAAAAkaG10eA9MABsAAA3UAAAAMGxvY2EOPgu2AAAOBAAAABptYXhwABUAtgAADiAAAAAgbmFtZdfxxl4AAA5AAAABQnBvc3QAAwAAAAAPhAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADx3AHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEAGAAAAAUABAAAwAEAAEAIPAz8MHwy/EO8Sfx3P/9//8AAAAAACDwMvDB8MrxDvEn8dz//f//AAH/4w/SD0UPPQ77DuMOLwADAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAMAAAAAAZIBtwBPAG4AjQAAPwE2NzY3Njc2NzY3NDc0PQEQJyYnJicmJyYjJiMnMjc2MzIzMjMyFxYXFhcWFxYVFAcGBwYHBgcGBxYXFhUUBwYHBgcGBwYjIicmIyIHBiMTFBcWFRQHFBUUFxYzMjc2NzY3NjU0JyYnJicmIyIHExQXFhcWMzI1NCcmJyYnJicmIyYjIgcUBxQVFBUUFQABBBQUCgICAQEBAQEHAQUFCAcHBggHAQEcRUUlBw0NBhQTExISDQ0ICAUFBgYMDAkJDywdHQoKEBEXFxgYGgwaGQwfOTkJmAECAQEMExcSEQ4OBwgJCA4PEBETDhcCAQEDFRNrDAcKCgoJDg0LChEUCAEAGwEDBAQEBAQFBgQEBgcDEwEYDQICAgEBAQEBGAMDBAQICAwMEhIWDg0MCAkICAUEBwocHSodFxYPDgoKBAQBAQQDAZYPHB0PCA8PBw0HAgQDCQkRERgUDw8ICAQEA/6+Cw0NBglfIRMMCQkEBQIDAQMPHh8OAxERCgAAAAABAAAAAAElAbcATgAAPwEyNzY3Njc0NzY3Nj0BJicmJyYjNxYXFhcWMzI3Njc2NwYHBgcGBwYHBhUGBwYVBgcGBxQHBgcGBwYXFRYXBgciByIjIicmIyIjIgcGIwAFAhUWCggEERIPDwcJCAwLBQUKGRgSEhENDw4UFQgCBAgVFAsCAgIBAgEIERIFAwMDAwICAQUwAQQDBgYDCRARCCcUDhobCAEYBgYFChMCUFFKSwoHBAIBAQEeAQEBAQEBAQEBAQsPAgYFBAYGBwUFCAgEKk5NGAMODgwLDA0EBQEIDBABAwMCAwAAAwAFAAUB1wHXACgAVACAAAATND8BNhc2HwEWFRQHFzYXNh8BFhUUDwEGByYvASY1NDcnBgcmLwEmNRcUHwEWNxY3IicmJyYnJicmNTQ3NjcWMxYXFhcWFxY3NjU0LwEmByYPAQYVFxQfARY3Fj8BNjU0LwEmJwYHFBcWFxYXFhcWFRQHBicGJyInJicmJyYnBhUFGCoYIiIYOxgZGRgjIhg8GBgqGCIjGDsXGRkZIyIYOxg2CDwICwwJAQUEAgEDAwEBCAgMBAQDBAQBAgQFAQkIOwgLCwkqCMkIOwgMCwgqCAg7CAwMCAUFAQICAwEBCAgLBQMEAwQCAgQEAQoBWyIYKRkBARk7GCIkFxoaAQEZOxkhIxcrFgEBFzwXIyIaGBgBARc8FyMBCgk6CQEBCgUDAwEFAgUCBgoJBwEBAgIDAQIEBgEHDQsJOgkBAQkoCQvKCgk7CAEBCCkJCgwHPQcBAQgCBAUBAgMFAgUDDQcJAQECBAICAQUEAgoLAAAABgAAABICAAGlABAAIQAyAEcAXABxAAA3NDc2MzIXFhUUBwYjIicmNTU0NzYzMhcWFRQHBiMiJyY1NTQ3NjMyFxYVFAcGIyInJjUTNTQ3NjMhMhcWHQEUBwYjISInJjU9ATQ3NjMhMhcWHQEUBwYjISInJjU9ATQ3NjMhMhcWHQEUBwYjISInJjUAEBAXFxAQEBAXFxAQEBAXFxAQEBAXFxAQEBAXFxAQEBAXFxAQkgMDAwFcBAIDAwIE/qQDAwMDAwMBXAQCAwMCBP6kAwMDAwMDAVwEAgMDAgT+pAMDA0kXEBAQEBcXEBAQEBeSFxAQEBAXFhAQEBAWkxcQEBAQFxcQEBAQF/7ANwMDAwMDAzcEAwICAwSSNwQCAwMCBDcEAgMDAgSSNwQDAgIDBDcDAwMDAwMAAAAABgAE/9sCAAHZACgATgBfAHQAiQCeAAA3NDc2NzY3Njc2NTQnJgcmByc2NzY3FhcWFRQHBgcGBwYVFzUXFQcmNRc3Fhc2NzY1NAcnNjc2NzY3FSYHIgcVBzUXFQcWFxYVFAcGByYnEz8BFTcVJzU3NDU0PQEnBgcTNTQ3NjcFNhcWHQEUBwYnBSYnJjU9ATQ3NhclFhcWHQEUBwYHJQYnJjU9ATQ3NjcFNhcWHQEUBwYnBSYnJjUEBwcJCgkJBwcEBAgNChgHDQ4RFA8OCgkMDAoKJB5nAgERDhAIBgceCAIHBwYFBQQKCQUeXxsPCAkQDxcfEwUnHh9gHwECDHQDAgQBXAQCAwMCBP6kAwMDAwIEAVwEAgMDAgT+pAMDAwMDAwFcBAIDAwIE/qQDAwOiDwsNBwgFBwYHBggDBAEBERINCQgBAQsNExALDAYIBgkGARIBLQEMBLMYCwEBAwUHEwMRAgoJBwUGAQEBAQ4BLQEYIgMLCg8WDg0BARIByCMBdQEdARsBCxgWDQIBBgn+jjgDAwIBAQEEAgQ2BQIDAQEBAQQDkzYFAQQBAQECAwM4AwMCAQEBBAEFkTgDBAEBAQEDAgU2BAIEAQEBAgQCAAACAAAAJQHbAbcALABZAAATNTQ3NjsBMhcWHQEUBwYHBgcGKwEiJyY9ATQ3NjsBMjc2PQE0JyYrASInJjUhNTQ3NjsBMhcWHQEUBwYHBgcGKwEiJyY9ATQ3NjsBMjc2PQE0JyYrASInJjUAEBAXbhYQEAsMExQbGx4SCAUFBQUIEh4WFQgIC0AXEBABABAQF24WEBALDBMUGxseEggFBQUFCBIeFhUICAtAFxAQARJuFxAQEBAXyR4bGxQTDAsFBQgkCAUGFRYeCQsICBAQF24XEBAQEBfJHhsbFBMMCwUFCCQIBQYVFh4JCwgIEBAXAAAIAAAAAAHbAdsAEgAvAEIAVQByAIUAmACrAAA3NDc2OwEyFxYVFAcGKwEiJyY1NzQ/ATYzMh8BFhcHJyYjIg8BBhUUHwEHJi8BJjUTND8BNjMyFxYVFA8BBiMiJyY1FzU0NzYzMhcWHQEUBwYjIicmNT8BFxYzFj8BNjU0LwE3Fh8BFhUUDwEGIyIvASYnNzU0NzYzMhcWHQEUBwYjIicmNRc0PwE2MzIXFhUUDwEGIyInJjUXNDc2OwEyFxYVFAcGKwEiJyY1AAMCBFwEAgMDAgRcBAIDBRgqGCIiGGAGBkVOCAsLCSoICE8FCgZgGCACSQMEBAIDA0kDAwQDAnYDAwQEAgMDAgQEAwMaRE4IDAsIKggITgUKBmAYGCoYIiMYXwYGeQIDBAQCAwMCBAQDAi0DSQMEAwMDA0kDAwQDAxMCAwRbBAMCAgMEWwQDAqUEAgMDAgQEAwMDAwS2IxcqGBhgBgoFTggIKQgMCwhORQYGYBkh/tMDA0kDAwIEBANJAgIDBCVcBAIDAwIEXAQCAwMCBIQFTwcBCCoICwsIT0QGBmAYIiIYKhcYYAYK6lsEAwICAwRbBAMCAgMEEgMDSQMDAwMEA0kDAwMELgQCAwMCBAQDAgIDBAAAAAEAEgAAAe4BtwCzAAATNDc2MzIXFjMyNzYzMhcWFRQHBgciBwYHBh0BFBUWOwEyNzQ1NzQnJiMiJyY1NDc2MzIXFjMyNzYzMhcWFRQHBiMiByIHBhURFBcWFxYzMhcWFRQHBiMiJyYjIgcGIyInJjU0NzY3Mjc2NzY1JzQ1JisBIgcUHQEUFxYXFjMyFxYVFAcGIyInJiMiBwYjIicmNTQ3NjcyNzY3NjUnNTA3NDUmNTQnJicmJyYnJiMmIyInJjUSAwQGDhoaDgwYGAwHBAMFBQYGCAgECgQIxwcEAQoFDAwHBwMEBw0ZGQ0MGBkMBwQEBQUHBgkIBAoKBAkJBgcFBQMEBg0ZGgwNGRkNBwMEBQUGBggJBAoBBArBCwQKBQkJBwgFBgQDBw0bGg0NGBgMBwMEBAUGBgcIBAoBAQEBAQEBAgICBQgJBgcFBQGjBwcGAQEBAQYHBwkEAwEBAQMGJ1wGAwEBAwZcJwYEBAMLBwcGAQEBAQYHBwkEBAEDByf+8iIGAwEBBAQJBwcGAQEBAQYGBwkEBAECAQMGInAGAwEBAwZqKQYDAQEEBAkHBwYBAQEBBgYHCAUEAQIBAwciEOkHBwMEBwgEBQYFBAMCAwEEBAkAAAABAAAAAQAATjBma18PPPUACwIAAAAAAM/PmaIAAAAAz8+ZogAA/9sCAAHbAAAACAACAAAAAAAAAAEAAAHg/+AAAAIAAAAAAAIAAAEAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAQAAAAGTAAABJQAAAdwABQIAAAACAAAEAdwAAAHcAAACAAASAAAAAAAKABQAHgDmAVoCGgK2A54EFgUEBfAAAAABAAAADAC0AAgAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEAEAAAAAEAAAAAAAIADgBOAAEAAAAAAAMAEAAmAAEAAAAAAAQAEABcAAEAAAAAAAUAFgAQAAEAAAAAAAYACAA2AAEAAAAAAAoAKABsAAMAAQQJAAEAEAAAAAMAAQQJAAIADgBOAAMAAQQJAAMAEAAmAAMAAQQJAAQAEABcAAMAAQQJAAUAFgAQAAMAAQQJAAYAEAA+AAMAAQQJAAoAKABsAGMAawAtAGkAYwBvAG4AcwBWAGUAcgBzAGkAbwBuACAAMQAuADAAYwBrAC0AaQBjAG8AbgBzY2staWNvbnMAYwBrAC0AaQBjAG8AbgBzAFIAZQBnAHUAbABhAHIAYwBrAC0AaQBjAG8AbgBzAEcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'),
+ url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AABHUAAoAAAAAEYwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAADgoAAA4KNgNGZE9TLzIAAA8AAAAAYAAAAGAIIwiSY21hcAAAD2AAAAB0AAAAdNQD0uVnYXNwAAAP1AAAAAgAAAAIAAAAEGhlYWQAAA/cAAAANgAAADYAvnP3aGhlYQAAEBQAAAAkAAAAJAPiAe1obXR4AAAQOAAAADAAAAAwD0wAG21heHAAABBoAAAABgAAAAYADFAAbmFtZQAAEHAAAAFCAAABQtfxxl5wb3N0AAARtAAAACAAAAAgAAMAAAEABAQAAQEBCWNrLWljb25zAAECAAEAOvgcAvgbA/gYBB4KABlT/4uLHgoAGVP/i4sMB4tm+JT4dAUdAAAAow8dAAAAqBEdAAAACR0AAA4BEgANAQEJERMVGB0iJywxNjtAY2staWNvbnNjay1pY29uc3UwdTF1MjB1RjAzMnVGMDMzdUYwQzF1RjBDQXVGMENCdUYxMEV1RjEyN3VGMURDAAACAYkACgAMAgABAAQABwAKAA0ByAK/BFoFzAe8COQLCg09/JQO/JQO/JQO+5QO+wGLixWMpgWNjJSMmI2YjpWNko6MjY2OjI6MjoyOi46Mj4uOjI6LjYuPjI+LkIuOi40Ii54Fi/dPie2Gk4uMiY2HjIiMhoyGjIaMh4yGi4eLhoyGi4eMiIuKiwiKowWdi6yMuY25ja6MpIuQi5GLlIuUi5GLj4uYi5iJmImYiJeHl4aXhZWFlIOTg5OBkH8IkH+Ofot8i4KJgYiDiIOHhIeGh4WFhoOFg4aEhoWIhYiDh4GHqISjfp95n3iUc4tvCIt4iHqEe4V8gn+AgX+BfoN8hXyEe4d7iHuIe4p5i4OLfot6jHuLfoyDi3eLbYpliAhliXWKhYsI9yz4KhWLgYx9i3iMd4x9i4GLhouDioGLgYuEi4aLgouEjIeTipWKmIuai5mMl46WjZaPlJEIlJGSlJCWkJaOmYubi5iIl4WVhpWDkoKRgZGBj4CNgI5/jH6Lgot/inuJCI371hWLhIuDjIKMgoyFjYeZhZiImIvSi6+ri8qLoYecg5iGk4WShZGEkYWPhI6FjoOOgowIgo2DjISLhIyCi4CLfouBioaJi4GLfIp3i3eLe4uCi4mLhYt/i4CLgouECA77b4uMFZCjBYyLk42Zj5qPlY6Sj5CRj5WOmIuMkaaWwZfBlr+VvJW9kKeLkgiLkgWGjoaNhYyFjIWLg4yEi4WMiIsIkKkFkoqWi5yKm4qZipeLl4uXipaLlIuUjJWLlIuXjJiMmYyUi5GMioOJg4iBhYqCiH2HCH6IgIiEiIqHiYeKh4qGioeLiIqIioeKhYuGioeLiIVvg2OAV39YhGmHe4uJioWJggiJgomCiYOJhIqDiYOKgouGi4gIi4YFjoqdiKuGioOKgYiBiYuIi4eKh4uIi4mLhYuDjICNgI2DjIWLcYt3i36Lgot9inqKCHmJgIqFiwgOZ5D37xWLopOfm5oItbUFm5uek6KLooueg5t7CMZPBZt8k3eLdIt0gnd7ewikcgWbm5+Uoouii56Dm3sIx08Fm3uTeIt0i3SDeHt7CGFhBXt7d4R1i3SLd5N7mwhQxgV8m4Oei6KLopOfnJwIcqQFenp4g3OLdIt4k3ubCFDGBXubg5+LoQjBixWLhI6EkIYIx1AFkIWRiZOLk4uSjpGRiouJjYiOiI6JjYqMioyKjYmOiY2KjYqOi42KjouOi5KOkpCQCJGQkY6Ti46LjYuOio2KjYqOiY2KjYmMioyKjYmOiI6IjYmMi5GQjpKLk4uTiJKGkAhQxgWGkISOhIuDi4WIhYYIYWIFhoWIhYuDCPdd+10Vi4OOhZCGCMZPBZCGkomTi5KLko2QkAi1tQWQkI6Si5KLk4iRhpAIUMcFhZCFjoOLg4uEiIaFi4qNiY6IjoiNiYyKjIqNiYyJjYiMiYyJjIiLiYuIi4OIhYaFCIaGhIiEi4iLiIyJi4iMiYyJjYiNiYyKjIqMiY2IjoiOiY2LjISFiISLgwgOi9QVi5qQmJaWlpaYkJqLmouYhpaAlYCRfot8i3yFfoGAgIF+hXyLfIt+kYCVgJaGmIuaCIv3JhWLm5CYlpWWlpiQmouai5iGloCVgZF+i3uLfIV+gYGAgH6GfIt8i36QgJaAlYaYi5oIi/cnFYuakJiWlpaVmJGai5qLmIWWgZWAkX6LfIt7hX+BgICAfoZ8i3yLfpCAloCWhpeLmwj3JvvUFYvCBYuNjI2NjY2NjYyNiwj38IsFjYuNio2JjYmMiYuJCItUBYuIiomJiYmJiYuJiwj78IsFiYuJi4mNiY2KjYuOCIv3JhWLwgWLjYyNjY2NjY2MjYsI9/CLBY2LjYqNiY2JjImLiQiLVAWLiYqIiYqJiYmKiYsI+/CLBYmLiYyJjYmMio6LjQiL9yYVi8IFi46MjY2NjYyNjI2LCPfwiwWNi42KjYqNiYyJi4gIi1QFi4mKiYmJiYmJiomLCPvwiwWJi4mMiY2JjYqNi40IDo/3NhWLlI6Uj5OPk5GSkZCRkJKPkY+Rj5GQj4+Qj42Pi5CLj4mPiY6IjYeMhouDi4OGhIAIc5wFkJSRk5SQlJGWjpaLmIuXh5WDlIOQgIt+i4GIgoSDhYSEhIOHg4eEhoSGhYWHhouHCK+Li5ypi4tdJIsFipKKkIuPCIz7SBWcpAWUg5WHlouRi4+Mj46QjY2Qi5CLl4GQd4oIg5sFjY2Oj4+RkJKPkI+Pjo+Pjo6PCIuLBYiLhouFi4WLhoqIiwiLfG2Li7fqi4tycGoFlYiTh5CEkYSOgouCi3uGf4CDgYJ+hnyLdot7kn6XCJD4WxWyr6mLi/sIqouLbyuLi6eqiwWLk4uXi5qLm4uWi5MIi46KiwWKiIaGg4QI9wj8BRWLwgWLjYyNjY2NjY2MjYsI9/CLBY2LjYqNiY2JjImLiQiLVAWLiIqJiYmJiYmLiYsI+/CLBYmLiYuJjYmNio2LjgiL9yYVi8IFi46MjY2MjY2NjI2LCPfwiwWNi42KjYmNiYyJi4kIi1QFi4mKiImKiYmJiomLCPvwiwWJi4mMiY2JjIqOi40Ii/cmFYvCBYuOjI2NjY2MjYyNiwj38IsFjYuNio2KjYmMiYuICItUBYuJiomJiYmJiYqJiwj78IsFiYuJjImNiY2KjYuNCA5ni/emFYv3AgWLmpCYlpaWlpiQmosI9wKLBZqLmIaVgJaAkH6LfAiL+10Fi3eIeIN5g3mBfH59fX58gXmDeYN4iHeLCHmLBYaLh4yHj4ePio+LkAiLrwWLkIyQj46Pj4+NkIsInYsFn4udkpmZmZmSnYufCIuUBYuTiZGFkIaRhI2EiwhLiwV8i36RgJWAloaYi5oI95SLFYv3AgWLmpCYlpaWlpiQmosI9wKLBZqLmIaVgJaAkH6LfAiL+10Fi3eIeIN5g3mBfH59fX58gXmDeYN4iHeLCHmLBYaLh4yHj4ePio+LkAiLrwWLkIyQj46Pj4+NkIsInYsFn4udkpmZmZmSnYufCIuUBYuTiZGFkIaRhI2EiwhLiwV8i36RgJWAloaYi5oIDmeL9zkVi42MjY2NjI2NjI6LCOeLBY2LjYqNiY2JjImLiYuIiomJiYmJiYqJiwgviwWIi4mMio2JjYqNi44IkPdKFYuik5+bmgi1tQWbm56Toouii56Dm3sI6ysFj4ePho+ECEaGPdkFhpCEjoSLg4uFiIWGCGFiBYaFiIWLg4uEjoSQhgjaPYZGBYSPho+Hjwgr6wV7nIOei6EIq/vBFYuNi42NjQjU1AWNjY2MjouNi46KjImNioyIi4mLiIqJiYkIQkIFiYmJi4mLiIuJi4mNiY2LjYuOCPcKZhWL5wWLjYyNjY2NjY2MjouNi42KjYmNiYyJi4kIiy8Fi4iKiYmKiYmJiomLiIuJjImNiYyKjYuOCKX3GBXPkNk8BZCGkomTi5KKko6QkAi1tQWQkI6Si5KLk4iRhpAIPdqQzwWSh5CHj4cI6ysFm3uTd4t1i3SDeHt7CGFhBXt7d4R1i3SLd5N7mwgs6wWHj4eQh5II9w33fhWL5gWLjoyNjI2NjY2LjouOi42LjImNiYyJi4gIizAFi4iKiYmJioqJioiLiIuJjImMio2KjYuOCLh5FYuNjI2NjQjU1AWNjY2MjouNi42KjYmNiYyJi4mLiIqJiYkIQkIFiYmJiomLiIuJjImNiY2KjYuOCJ5dFYuOjI2MjI2NjYyOiwjmiwWOi42KjYmNiouJi4iLiIuJiYmJiomKiIsIMIsFiIuJjImMio2KjYuOCA6d+DcVi5CMj42QjY+PjY+LlIuYi52KnYqYi5SLk4uXi5uMm4yXi5OLkIuOiY6HjYaMh4uGCIuFioeHiIiJh4mHi4eLhouGioaLh4mIiYSHiHyLcQiLLwWLh4uIi4mOi4+KkIsI91uLBZCLj4yNi4uNi46LjwiM5wWLpYeahY+HjoaMg4uDi4WNho2GjYmQi5KLkIyPjZCOj46NkIuUi5eLnIqcipeLlIsIk4uXi5uMnIyXi5OLkIuPiY2HjYaNh4uGi4WJh4iIh4mHiYeLh4uGi4WKhouHioiJCISGiHyLcQiL+6IFi3WOfZKHjomPipGKkYuQio+LkIuPiY6Jj4iMh4uFi4aKhomHiYeHiYeLgot/i3qMCHqMfouDi4KLf4t6inqKf4uCi4eLh42Jj4iPio+LkIuRjI+Pjo6Oj4yPjI+LkIyQjAiRjI+Mjo2Sj46Yi6IIivcEBYuPi46LjYmLhoyEiwj7VYsFhIuGioiLi4mLiIuHCIshBYtwjnuSh46JkIqRipGLkYqPi5CLj4qPiI+IjYeLhYuGioaIh4mHiImGi4KLfot5jAh6jH2Lg4uCi3+Le4p7in+Lg4uGi4iNiY+Ij4qPi5CLkYyPjo6Ojo+Mj4yPi4+MkIwIkIyPjI6NkpCOmIuiCIqbi/d9BYuLi46Mj4uPi4+LjYqOi4+Lj4uQi4+KjouPio6Kj4uPiY6KjYqNiY2KjIiNhoyGiwiFjIaLh4uGi4eMiI6HjoqPi5EIDviUFPiUFYsMCgAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA8dwB4P/g/+AB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABABgAAAAFAAQAAMABAABACDwM/DB8MvxDvEn8dz//f//AAAAAAAg8DLwwfDK8Q7xJ/Hc//3//wAB/+MP0g9FDz0O+w7jDi8AAwABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAB//8ADwABAAAAAQAAg0l/n18PPPUACwIAAAAAAM/PmaIAAAAAz8+ZogAA/9sCAAHbAAAACAACAAAAAAAAAAEAAAHg/+AAAAIAAAAAAAIAAAEAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAQAAAAGTAAABJQAAAdwABQIAAAACAAAEAdwAAAHcAAACAAASAABQAAAMAAAAAAAOAK4AAQAAAAAAAQAQAAAAAQAAAAAAAgAOAE4AAQAAAAAAAwAQACYAAQAAAAAABAAQAFwAAQAAAAAABQAWABAAAQAAAAAABgAIADYAAQAAAAAACgAoAGwAAwABBAkAAQAQAAAAAwABBAkAAgAOAE4AAwABBAkAAwAQACYAAwABBAkABAAQAFwAAwABBAkABQAWABAAAwABBAkABgAQAD4AAwABBAkACgAoAGwAYwBrAC0AaQBjAG8AbgBzAFYAZQByAHMAaQBvAG4AIAAxAC4AMABjAGsALQBpAGMAbwBuAHNjay1pY29ucwBjAGsALQBpAGMAbwBuAHMAUgBlAGcAdQBsAGEAcgBjAGsALQBpAGMAbwBuAHMARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+.ck-icon-bold,
+.ck-icon-italic,
+.ck-icon-link,
+.ck-icon-list,
+.ck-icon-list-ordered,
+.ck-icon-quote,
+.ck-icon-unlink,
+.ck-icon-heading {
+ font-family: 'ck-icons';
+ speak: none;
+ font-style: normal;
+ font-weight: normal;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+.ck-icon-bold:before {
+ content: "\f032";
+}
+.ck-icon-italic:before {
+ content: "\f033";
+}
+.ck-icon-link:before {
+ content: "\f0c1";
+}
+.ck-icon-list:before {
+ content: "\f0ca";
+}
+.ck-icon-list-ordered:before {
+ content: "\f0cb";
+}
+.ck-icon-quote:before {
+ content: "\f10e";
+}
+.ck-icon-unlink:before {
+ content: "\f127";
+}
+.ck-icon-heading:before {
+ content: "\f1dc";
+}
+
+/* animations */
+@-webkit-keyframes hiliteAppear {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+@keyframes hiliteAppear {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+@-webkit-keyframes textGlimmer {
+ 0% { background-position: -288px 0; }
+ 100% { background-position: 288px 0; }
+}
+
+/* Generated with Bounce.js. Edit at http://goo.gl/kRKkQd */
+@-webkit-keyframes toolbarPop {
+ 0% { -webkit-transform: matrix3d(0.7, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 10, 0, 1); transform: matrix3d(0.7, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 10, 0, 1); }
+ 1.666667% { -webkit-transform: matrix3d(0.76047, 0, 0, 0, 0, 0.60078, 0, 0, 0, 0, 1, 0, 0, 9.59374, 0, 1); transform: matrix3d(0.76047, 0, 0, 0, 0, 0.60078, 0, 0, 0, 0, 1, 0, 0, 9.59374, 0, 1); }
+ 3.333333% { -webkit-transform: matrix3d(0.81739, 0, 0, 0, 0, 0.69565, 0, 0, 0, 0, 1, 0, 0, 8.46888, 0, 1); transform: matrix3d(0.81739, 0, 0, 0, 0, 0.69565, 0, 0, 0, 0, 1, 0, 0, 8.46888, 0, 1); }
+ 5% { -webkit-transform: matrix3d(0.86799, 0, 0, 0, 0, 0.77999, 0, 0, 0, 0, 1, 0, 0, 6.86422, 0, 1); transform: matrix3d(0.86799, 0, 0, 0, 0, 0.77999, 0, 0, 0, 0, 1, 0, 0, 6.86422, 0, 1); }
+ 6.666667% { -webkit-transform: matrix3d(0.91088, 0, 0, 0, 0, 0.85146, 0, 0, 0, 0, 1, 0, 0, 5.05894, 0, 1); transform: matrix3d(0.91088, 0, 0, 0, 0, 0.85146, 0, 0, 0, 0, 1, 0, 0, 5.05894, 0, 1); }
+ 8.333333% { -webkit-transform: matrix3d(0.94564, 0, 0, 0, 0, 0.9094, 0, 0, 0, 0, 1, 0, 0, 3.29569, 0, 1); transform: matrix3d(0.94564, 0, 0, 0, 0, 0.9094, 0, 0, 0, 0, 1, 0, 0, 3.29569, 0, 1); }
+ 10% { -webkit-transform: matrix3d(0.97258, 0, 0, 0, 0, 0.95429, 0, 0, 0, 0, 1, 0, 0, 1.74471, 0, 1); transform: matrix3d(0.97258, 0, 0, 0, 0, 0.95429, 0, 0, 0, 0, 1, 0, 0, 1.74471, 0, 1); }
+ 11.666667% { -webkit-transform: matrix3d(0.99243, 0, 0, 0, 0, 0.98738, 0, 0, 0, 0, 1, 0, 0, 0.49844, 0, 1); transform: matrix3d(0.99243, 0, 0, 0, 0, 0.98738, 0, 0, 0, 0, 1, 0, 0, 0.49844, 0, 1); }
+ 13.333333% { -webkit-transform: matrix3d(1.00618, 0, 0, 0, 0, 1.0103, 0, 0, 0, 0, 1, 0, 0, -0.41631, 0, 1); transform: matrix3d(1.00618, 0, 0, 0, 0, 1.0103, 0, 0, 0, 0, 1, 0, 0, -0.41631, 0, 1); }
+ 15% { -webkit-transform: matrix3d(1.01492, 0, 0, 0, 0, 1.02486, 0, 0, 0, 0, 1, 0, 0, -1.01911, 0, 1); transform: matrix3d(1.01492, 0, 0, 0, 0, 1.02486, 0, 0, 0, 0, 1, 0, 0, -1.01911, 0, 1); }
+ 16.666667% { -webkit-transform: matrix3d(1.0197, 0, 0, 0, 0, 1.03283, 0, 0, 0, 0, 1, 0, 0, -1.35648, 0, 1); transform: matrix3d(1.0197, 0, 0, 0, 0, 1.03283, 0, 0, 0, 0, 1, 0, 0, -1.35648, 0, 1); }
+ 18.333333% { -webkit-transform: matrix3d(1.02152, 0, 0, 0, 0, 1.03587, 0, 0, 0, 0, 1, 0, 0, -1.48616, 0, 1); transform: matrix3d(1.02152, 0, 0, 0, 0, 1.03587, 0, 0, 0, 0, 1, 0, 0, -1.48616, 0, 1); }
+ 20% { -webkit-transform: matrix3d(1.02124, 0, 0, 0, 0, 1.0354, 0, 0, 0, 0, 1, 0, 0, -1.46607, 0, 1); transform: matrix3d(1.02124, 0, 0, 0, 0, 1.0354, 0, 0, 0, 0, 1, 0, 0, -1.46607, 0, 1); }
+ 21.666667% { -webkit-transform: matrix3d(1.01958, 0, 0, 0, 0, 1.03263, 0, 0, 0, 0, 1, 0, 0, -1.34772, 0, 1); transform: matrix3d(1.01958, 0, 0, 0, 0, 1.03263, 0, 0, 0, 0, 1, 0, 0, -1.34772, 0, 1); }
+ 23.333333% { -webkit-transform: matrix3d(1.01711, 0, 0, 0, 0, 1.02852, 0, 0, 0, 0, 1, 0, 0, -1.17322, 0, 1); transform: matrix3d(1.01711, 0, 0, 0, 0, 1.02852, 0, 0, 0, 0, 1, 0, 0, -1.17322, 0, 1); }
+ 25% { -webkit-transform: matrix3d(1.01428, 0, 0, 0, 0, 1.0238, 0, 0, 0, 0, 1, 0, 0, -0.97458, 0, 1); transform: matrix3d(1.01428, 0, 0, 0, 0, 1.0238, 0, 0, 0, 0, 1, 0, 0, -0.97458, 0, 1); }
+ 26.666667% { -webkit-transform: matrix3d(1.0114, 0, 0, 0, 0, 1.019, 0, 0, 0, 0, 1, 0, 0, -0.7745, 0, 1); transform: matrix3d(1.0114, 0, 0, 0, 0, 1.019, 0, 0, 0, 0, 1, 0, 0, -0.7745, 0, 1); }
+ 28.333333% { -webkit-transform: matrix3d(1.00869, 0, 0, 0, 0, 1.01449, 0, 0, 0, 0, 1, 0, 0, -0.58784, 0, 1); transform: matrix3d(1.00869, 0, 0, 0, 0, 1.01449, 0, 0, 0, 0, 1, 0, 0, -0.58784, 0, 1); }
+ 30% { -webkit-transform: matrix3d(1.00628, 0, 0, 0, 0, 1.01047, 0, 0, 0, 0, 1, 0, 0, -0.42325, 0, 1); transform: matrix3d(1.00628, 0, 0, 0, 0, 1.01047, 0, 0, 0, 0, 1, 0, 0, -0.42325, 0, 1); }
+ 31.666667% { -webkit-transform: matrix3d(1.00424, 0, 0, 0, 0, 1.00707, 0, 0, 0, 0, 1, 0, 0, -0.28479, 0, 1); transform: matrix3d(1.00424, 0, 0, 0, 0, 1.00707, 0, 0, 0, 0, 1, 0, 0, -0.28479, 0, 1); }
+ 33.333333% { -webkit-transform: matrix3d(1.00259, 0, 0, 0, 0, 1.00431, 0, 0, 0, 0, 1, 0, 0, -0.17323, 0, 1); transform: matrix3d(1.00259, 0, 0, 0, 0, 1.00431, 0, 0, 0, 0, 1, 0, 0, -0.17323, 0, 1); }
+ 35% { -webkit-transform: matrix3d(1.00131, 0, 0, 0, 0, 1.00218, 0, 0, 0, 0, 1, 0, 0, -0.08721, 0, 1); transform: matrix3d(1.00131, 0, 0, 0, 0, 1.00218, 0, 0, 0, 0, 1, 0, 0, -0.08721, 0, 1); }
+ 36.666667% { -webkit-transform: matrix3d(1.00036, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, -0.02404, 0, 1); transform: matrix3d(1.00036, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, -0.02404, 0, 1); }
+ 38.333333% { -webkit-transform: matrix3d(0.99971, 0, 0, 0, 0, 0.99951, 0, 0, 0, 0, 1, 0, 0, 0.0196, 0, 1); transform: matrix3d(0.99971, 0, 0, 0, 0, 0.99951, 0, 0, 0, 0, 1, 0, 0, 0.0196, 0, 1); }
+ 40% { -webkit-transform: matrix3d(0.99929, 0, 0, 0, 0, 0.99882, 0, 0, 0, 0, 1, 0, 0, 0.04727, 0, 1); transform: matrix3d(0.99929, 0, 0, 0, 0, 0.99882, 0, 0, 0, 0, 1, 0, 0, 0.04727, 0, 1); }
+ 41.666667% { -webkit-transform: matrix3d(0.99906, 0, 0, 0, 0, 0.99844, 0, 0, 0, 0, 1, 0, 0, 0.06241, 0, 1); transform: matrix3d(0.99906, 0, 0, 0, 0, 0.99844, 0, 0, 0, 0, 1, 0, 0, 0.06241, 0, 1); }
+ 43.333333% { -webkit-transform: matrix3d(0.99898, 0, 0, 0, 0, 0.99829, 0, 0, 0, 0, 1, 0, 0, 0.06817, 0, 1); transform: matrix3d(0.99898, 0, 0, 0, 0, 0.99829, 0, 0, 0, 0, 1, 0, 0, 0.06817, 0, 1); }
+ 45% { -webkit-transform: matrix3d(0.99899, 0, 0, 0, 0, 0.99832, 0, 0, 0, 0, 1, 0, 0, 0.06728, 0, 1); transform: matrix3d(0.99899, 0, 0, 0, 0, 0.99832, 0, 0, 0, 0, 1, 0, 0, 0.06728, 0, 1); }
+ 46.666667% { -webkit-transform: matrix3d(0.99907, 0, 0, 0, 0, 0.99845, 0, 0, 0, 0, 1, 0, 0, 0.06202, 0, 1); transform: matrix3d(0.99907, 0, 0, 0, 0, 0.99845, 0, 0, 0, 0, 1, 0, 0, 0.06202, 0, 1); }
+ 48.333333% { -webkit-transform: matrix3d(0.99919, 0, 0, 0, 0, 0.99864, 0, 0, 0, 0, 1, 0, 0, 0.05422, 0, 1); transform: matrix3d(0.99919, 0, 0, 0, 0, 0.99864, 0, 0, 0, 0, 1, 0, 0, 0.05422, 0, 1); }
+ 50% { -webkit-transform: matrix3d(0.99932, 0, 0, 0, 0, 0.99887, 0, 0, 0, 0, 1, 0, 0, 0.04526, 0, 1); transform: matrix3d(0.99932, 0, 0, 0, 0, 0.99887, 0, 0, 0, 0, 1, 0, 0, 0.04526, 0, 1); }
+ 51.666667% { -webkit-transform: matrix3d(0.99946, 0, 0, 0, 0, 0.9991, 0, 0, 0, 0, 1, 0, 0, 0.03614, 0, 1); transform: matrix3d(0.99946, 0, 0, 0, 0, 0.9991, 0, 0, 0, 0, 1, 0, 0, 0.03614, 0, 1); }
+ 53.333333% { -webkit-transform: matrix3d(0.99959, 0, 0, 0, 0, 0.99931, 0, 0, 0, 0, 1, 0, 0, 0.02756, 0, 1); transform: matrix3d(0.99959, 0, 0, 0, 0, 0.99931, 0, 0, 0, 0, 1, 0, 0, 0.02756, 0, 1); }
+ 55% { -webkit-transform: matrix3d(0.9997, 0, 0, 0, 0, 0.9995, 0, 0, 0, 0, 1, 0, 0, 0.01993, 0, 1); transform: matrix3d(0.9997, 0, 0, 0, 0, 0.9995, 0, 0, 0, 0, 1, 0, 0, 0.01993, 0, 1); }
+ 56.666667% { -webkit-transform: matrix3d(0.9998, 0, 0, 0, 0, 0.99966, 0, 0, 0, 0, 1, 0, 0, 0.01346, 0, 1); transform: matrix3d(0.9998, 0, 0, 0, 0, 0.99966, 0, 0, 0, 0, 1, 0, 0, 0.01346, 0, 1); }
+ 58.333333% { -webkit-transform: matrix3d(0.99988, 0, 0, 0, 0, 0.99979, 0, 0, 0, 0, 1, 0, 0, 0.00821, 0, 1); transform: matrix3d(0.99988, 0, 0, 0, 0, 0.99979, 0, 0, 0, 0, 1, 0, 0, 0.00821, 0, 1); }
+ 60% { -webkit-transform: matrix3d(0.99994, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0.00414, 0, 1); transform: matrix3d(0.99994, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0.00414, 0, 1); }
+ 61.666667% { -webkit-transform: matrix3d(0.99998, 0, 0, 0, 0, 0.99997, 0, 0, 0, 0, 1, 0, 0, 0.00114, 0, 1); transform: matrix3d(0.99998, 0, 0, 0, 0, 0.99997, 0, 0, 0, 0, 1, 0, 0, 0.00114, 0, 1); }
+ 63.333333% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00093, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00093, 0, 1); }
+ 65% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00225, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00225, 0, 1); }
+ 66.666667% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00298, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00298, 0, 1); }
+ 68.333333% { -webkit-transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00325, 0, 1); transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00325, 0, 1); }
+ 70% { -webkit-transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00321, 0, 1); transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00321, 0, 1); }
+ 71.666667% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00296, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00296, 0, 1); }
+ 73.333333% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00258, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00258, 0, 1); }
+ 75% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00005, 0, 0, 0, 0, 1, 0, 0, -0.00216, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00005, 0, 0, 0, 0, 1, 0, 0, -0.00216, 0, 1); }
+ 76.666667% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00004, 0, 0, 0, 0, 1, 0, 0, -0.00172, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00004, 0, 0, 0, 0, 1, 0, 0, -0.00172, 0, 1); }
+ 78.333333% { -webkit-transform: matrix3d(1.00002, 0, 0, 0, 0, 1.00003, 0, 0, 0, 0, 1, 0, 0, -0.00131, 0, 1); transform: matrix3d(1.00002, 0, 0, 0, 0, 1.00003, 0, 0, 0, 0, 1, 0, 0, -0.00131, 0, 1); }
+ 80% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00095, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00095, 0, 1); }
+ 81.666667% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00064, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00064, 0, 1); }
+ 83.333333% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00001, 0, 0, 0, 0, 1, 0, 0, -0.00039, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00001, 0, 0, 0, 0, 1, 0, 0, -0.00039, 0, 1); }
+ 85% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.0002, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.0002, 0, 1); }
+ 86.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.00005, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.00005, 0, 1); }
+ 88.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00004, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00004, 0, 1); }
+ 90% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00011, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00011, 0, 1); }
+ 91.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); }
+ 93.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); }
+ 95% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); }
+ 96.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); }
+ 98.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00012, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00012, 0, 1); }
+ 100% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
+}
+
+@keyframes toolbarPop {
+ 0% { -webkit-transform: matrix3d(0.7, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 10, 0, 1); transform: matrix3d(0.7, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, 0, 10, 0, 1); }
+ 1.666667% { -webkit-transform: matrix3d(0.76047, 0, 0, 0, 0, 0.60078, 0, 0, 0, 0, 1, 0, 0, 9.59374, 0, 1); transform: matrix3d(0.76047, 0, 0, 0, 0, 0.60078, 0, 0, 0, 0, 1, 0, 0, 9.59374, 0, 1); }
+ 3.333333% { -webkit-transform: matrix3d(0.81739, 0, 0, 0, 0, 0.69565, 0, 0, 0, 0, 1, 0, 0, 8.46888, 0, 1); transform: matrix3d(0.81739, 0, 0, 0, 0, 0.69565, 0, 0, 0, 0, 1, 0, 0, 8.46888, 0, 1); }
+ 5% { -webkit-transform: matrix3d(0.86799, 0, 0, 0, 0, 0.77999, 0, 0, 0, 0, 1, 0, 0, 6.86422, 0, 1); transform: matrix3d(0.86799, 0, 0, 0, 0, 0.77999, 0, 0, 0, 0, 1, 0, 0, 6.86422, 0, 1); }
+ 6.666667% { -webkit-transform: matrix3d(0.91088, 0, 0, 0, 0, 0.85146, 0, 0, 0, 0, 1, 0, 0, 5.05894, 0, 1); transform: matrix3d(0.91088, 0, 0, 0, 0, 0.85146, 0, 0, 0, 0, 1, 0, 0, 5.05894, 0, 1); }
+ 8.333333% { -webkit-transform: matrix3d(0.94564, 0, 0, 0, 0, 0.9094, 0, 0, 0, 0, 1, 0, 0, 3.29569, 0, 1); transform: matrix3d(0.94564, 0, 0, 0, 0, 0.9094, 0, 0, 0, 0, 1, 0, 0, 3.29569, 0, 1); }
+ 10% { -webkit-transform: matrix3d(0.97258, 0, 0, 0, 0, 0.95429, 0, 0, 0, 0, 1, 0, 0, 1.74471, 0, 1); transform: matrix3d(0.97258, 0, 0, 0, 0, 0.95429, 0, 0, 0, 0, 1, 0, 0, 1.74471, 0, 1); }
+ 11.666667% { -webkit-transform: matrix3d(0.99243, 0, 0, 0, 0, 0.98738, 0, 0, 0, 0, 1, 0, 0, 0.49844, 0, 1); transform: matrix3d(0.99243, 0, 0, 0, 0, 0.98738, 0, 0, 0, 0, 1, 0, 0, 0.49844, 0, 1); }
+ 13.333333% { -webkit-transform: matrix3d(1.00618, 0, 0, 0, 0, 1.0103, 0, 0, 0, 0, 1, 0, 0, -0.41631, 0, 1); transform: matrix3d(1.00618, 0, 0, 0, 0, 1.0103, 0, 0, 0, 0, 1, 0, 0, -0.41631, 0, 1); }
+ 15% { -webkit-transform: matrix3d(1.01492, 0, 0, 0, 0, 1.02486, 0, 0, 0, 0, 1, 0, 0, -1.01911, 0, 1); transform: matrix3d(1.01492, 0, 0, 0, 0, 1.02486, 0, 0, 0, 0, 1, 0, 0, -1.01911, 0, 1); }
+ 16.666667% { -webkit-transform: matrix3d(1.0197, 0, 0, 0, 0, 1.03283, 0, 0, 0, 0, 1, 0, 0, -1.35648, 0, 1); transform: matrix3d(1.0197, 0, 0, 0, 0, 1.03283, 0, 0, 0, 0, 1, 0, 0, -1.35648, 0, 1); }
+ 18.333333% { -webkit-transform: matrix3d(1.02152, 0, 0, 0, 0, 1.03587, 0, 0, 0, 0, 1, 0, 0, -1.48616, 0, 1); transform: matrix3d(1.02152, 0, 0, 0, 0, 1.03587, 0, 0, 0, 0, 1, 0, 0, -1.48616, 0, 1); }
+ 20% { -webkit-transform: matrix3d(1.02124, 0, 0, 0, 0, 1.0354, 0, 0, 0, 0, 1, 0, 0, -1.46607, 0, 1); transform: matrix3d(1.02124, 0, 0, 0, 0, 1.0354, 0, 0, 0, 0, 1, 0, 0, -1.46607, 0, 1); }
+ 21.666667% { -webkit-transform: matrix3d(1.01958, 0, 0, 0, 0, 1.03263, 0, 0, 0, 0, 1, 0, 0, -1.34772, 0, 1); transform: matrix3d(1.01958, 0, 0, 0, 0, 1.03263, 0, 0, 0, 0, 1, 0, 0, -1.34772, 0, 1); }
+ 23.333333% { -webkit-transform: matrix3d(1.01711, 0, 0, 0, 0, 1.02852, 0, 0, 0, 0, 1, 0, 0, -1.17322, 0, 1); transform: matrix3d(1.01711, 0, 0, 0, 0, 1.02852, 0, 0, 0, 0, 1, 0, 0, -1.17322, 0, 1); }
+ 25% { -webkit-transform: matrix3d(1.01428, 0, 0, 0, 0, 1.0238, 0, 0, 0, 0, 1, 0, 0, -0.97458, 0, 1); transform: matrix3d(1.01428, 0, 0, 0, 0, 1.0238, 0, 0, 0, 0, 1, 0, 0, -0.97458, 0, 1); }
+ 26.666667% { -webkit-transform: matrix3d(1.0114, 0, 0, 0, 0, 1.019, 0, 0, 0, 0, 1, 0, 0, -0.7745, 0, 1); transform: matrix3d(1.0114, 0, 0, 0, 0, 1.019, 0, 0, 0, 0, 1, 0, 0, -0.7745, 0, 1); }
+ 28.333333% { -webkit-transform: matrix3d(1.00869, 0, 0, 0, 0, 1.01449, 0, 0, 0, 0, 1, 0, 0, -0.58784, 0, 1); transform: matrix3d(1.00869, 0, 0, 0, 0, 1.01449, 0, 0, 0, 0, 1, 0, 0, -0.58784, 0, 1); }
+ 30% { -webkit-transform: matrix3d(1.00628, 0, 0, 0, 0, 1.01047, 0, 0, 0, 0, 1, 0, 0, -0.42325, 0, 1); transform: matrix3d(1.00628, 0, 0, 0, 0, 1.01047, 0, 0, 0, 0, 1, 0, 0, -0.42325, 0, 1); }
+ 31.666667% { -webkit-transform: matrix3d(1.00424, 0, 0, 0, 0, 1.00707, 0, 0, 0, 0, 1, 0, 0, -0.28479, 0, 1); transform: matrix3d(1.00424, 0, 0, 0, 0, 1.00707, 0, 0, 0, 0, 1, 0, 0, -0.28479, 0, 1); }
+ 33.333333% { -webkit-transform: matrix3d(1.00259, 0, 0, 0, 0, 1.00431, 0, 0, 0, 0, 1, 0, 0, -0.17323, 0, 1); transform: matrix3d(1.00259, 0, 0, 0, 0, 1.00431, 0, 0, 0, 0, 1, 0, 0, -0.17323, 0, 1); }
+ 35% { -webkit-transform: matrix3d(1.00131, 0, 0, 0, 0, 1.00218, 0, 0, 0, 0, 1, 0, 0, -0.08721, 0, 1); transform: matrix3d(1.00131, 0, 0, 0, 0, 1.00218, 0, 0, 0, 0, 1, 0, 0, -0.08721, 0, 1); }
+ 36.666667% { -webkit-transform: matrix3d(1.00036, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, -0.02404, 0, 1); transform: matrix3d(1.00036, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, -0.02404, 0, 1); }
+ 38.333333% { -webkit-transform: matrix3d(0.99971, 0, 0, 0, 0, 0.99951, 0, 0, 0, 0, 1, 0, 0, 0.0196, 0, 1); transform: matrix3d(0.99971, 0, 0, 0, 0, 0.99951, 0, 0, 0, 0, 1, 0, 0, 0.0196, 0, 1); }
+ 40% { -webkit-transform: matrix3d(0.99929, 0, 0, 0, 0, 0.99882, 0, 0, 0, 0, 1, 0, 0, 0.04727, 0, 1); transform: matrix3d(0.99929, 0, 0, 0, 0, 0.99882, 0, 0, 0, 0, 1, 0, 0, 0.04727, 0, 1); }
+ 41.666667% { -webkit-transform: matrix3d(0.99906, 0, 0, 0, 0, 0.99844, 0, 0, 0, 0, 1, 0, 0, 0.06241, 0, 1); transform: matrix3d(0.99906, 0, 0, 0, 0, 0.99844, 0, 0, 0, 0, 1, 0, 0, 0.06241, 0, 1); }
+ 43.333333% { -webkit-transform: matrix3d(0.99898, 0, 0, 0, 0, 0.99829, 0, 0, 0, 0, 1, 0, 0, 0.06817, 0, 1); transform: matrix3d(0.99898, 0, 0, 0, 0, 0.99829, 0, 0, 0, 0, 1, 0, 0, 0.06817, 0, 1); }
+ 45% { -webkit-transform: matrix3d(0.99899, 0, 0, 0, 0, 0.99832, 0, 0, 0, 0, 1, 0, 0, 0.06728, 0, 1); transform: matrix3d(0.99899, 0, 0, 0, 0, 0.99832, 0, 0, 0, 0, 1, 0, 0, 0.06728, 0, 1); }
+ 46.666667% { -webkit-transform: matrix3d(0.99907, 0, 0, 0, 0, 0.99845, 0, 0, 0, 0, 1, 0, 0, 0.06202, 0, 1); transform: matrix3d(0.99907, 0, 0, 0, 0, 0.99845, 0, 0, 0, 0, 1, 0, 0, 0.06202, 0, 1); }
+ 48.333333% { -webkit-transform: matrix3d(0.99919, 0, 0, 0, 0, 0.99864, 0, 0, 0, 0, 1, 0, 0, 0.05422, 0, 1); transform: matrix3d(0.99919, 0, 0, 0, 0, 0.99864, 0, 0, 0, 0, 1, 0, 0, 0.05422, 0, 1); }
+ 50% { -webkit-transform: matrix3d(0.99932, 0, 0, 0, 0, 0.99887, 0, 0, 0, 0, 1, 0, 0, 0.04526, 0, 1); transform: matrix3d(0.99932, 0, 0, 0, 0, 0.99887, 0, 0, 0, 0, 1, 0, 0, 0.04526, 0, 1); }
+ 51.666667% { -webkit-transform: matrix3d(0.99946, 0, 0, 0, 0, 0.9991, 0, 0, 0, 0, 1, 0, 0, 0.03614, 0, 1); transform: matrix3d(0.99946, 0, 0, 0, 0, 0.9991, 0, 0, 0, 0, 1, 0, 0, 0.03614, 0, 1); }
+ 53.333333% { -webkit-transform: matrix3d(0.99959, 0, 0, 0, 0, 0.99931, 0, 0, 0, 0, 1, 0, 0, 0.02756, 0, 1); transform: matrix3d(0.99959, 0, 0, 0, 0, 0.99931, 0, 0, 0, 0, 1, 0, 0, 0.02756, 0, 1); }
+ 55% { -webkit-transform: matrix3d(0.9997, 0, 0, 0, 0, 0.9995, 0, 0, 0, 0, 1, 0, 0, 0.01993, 0, 1); transform: matrix3d(0.9997, 0, 0, 0, 0, 0.9995, 0, 0, 0, 0, 1, 0, 0, 0.01993, 0, 1); }
+ 56.666667% { -webkit-transform: matrix3d(0.9998, 0, 0, 0, 0, 0.99966, 0, 0, 0, 0, 1, 0, 0, 0.01346, 0, 1); transform: matrix3d(0.9998, 0, 0, 0, 0, 0.99966, 0, 0, 0, 0, 1, 0, 0, 0.01346, 0, 1); }
+ 58.333333% { -webkit-transform: matrix3d(0.99988, 0, 0, 0, 0, 0.99979, 0, 0, 0, 0, 1, 0, 0, 0.00821, 0, 1); transform: matrix3d(0.99988, 0, 0, 0, 0, 0.99979, 0, 0, 0, 0, 1, 0, 0, 0.00821, 0, 1); }
+ 60% { -webkit-transform: matrix3d(0.99994, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0.00414, 0, 1); transform: matrix3d(0.99994, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0.00414, 0, 1); }
+ 61.666667% { -webkit-transform: matrix3d(0.99998, 0, 0, 0, 0, 0.99997, 0, 0, 0, 0, 1, 0, 0, 0.00114, 0, 1); transform: matrix3d(0.99998, 0, 0, 0, 0, 0.99997, 0, 0, 0, 0, 1, 0, 0, 0.00114, 0, 1); }
+ 63.333333% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00093, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00093, 0, 1); }
+ 65% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00225, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00225, 0, 1); }
+ 66.666667% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00298, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00298, 0, 1); }
+ 68.333333% { -webkit-transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00325, 0, 1); transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00325, 0, 1); }
+ 70% { -webkit-transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00321, 0, 1); transform: matrix3d(1.00005, 0, 0, 0, 0, 1.00008, 0, 0, 0, 0, 1, 0, 0, -0.00321, 0, 1); }
+ 71.666667% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00296, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00007, 0, 0, 0, 0, 1, 0, 0, -0.00296, 0, 1); }
+ 73.333333% { -webkit-transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00258, 0, 1); transform: matrix3d(1.00004, 0, 0, 0, 0, 1.00006, 0, 0, 0, 0, 1, 0, 0, -0.00258, 0, 1); }
+ 75% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00005, 0, 0, 0, 0, 1, 0, 0, -0.00216, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00005, 0, 0, 0, 0, 1, 0, 0, -0.00216, 0, 1); }
+ 76.666667% { -webkit-transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00004, 0, 0, 0, 0, 1, 0, 0, -0.00172, 0, 1); transform: matrix3d(1.00003, 0, 0, 0, 0, 1.00004, 0, 0, 0, 0, 1, 0, 0, -0.00172, 0, 1); }
+ 78.333333% { -webkit-transform: matrix3d(1.00002, 0, 0, 0, 0, 1.00003, 0, 0, 0, 0, 1, 0, 0, -0.00131, 0, 1); transform: matrix3d(1.00002, 0, 0, 0, 0, 1.00003, 0, 0, 0, 0, 1, 0, 0, -0.00131, 0, 1); }
+ 80% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00095, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00095, 0, 1); }
+ 81.666667% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00064, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00002, 0, 0, 0, 0, 1, 0, 0, -0.00064, 0, 1); }
+ 83.333333% { -webkit-transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00001, 0, 0, 0, 0, 1, 0, 0, -0.00039, 0, 1); transform: matrix3d(1.00001, 0, 0, 0, 0, 1.00001, 0, 0, 0, 0, 1, 0, 0, -0.00039, 0, 1); }
+ 85% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.0002, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.0002, 0, 1); }
+ 86.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.00005, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, -0.00005, 0, 1); }
+ 88.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00004, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00004, 0, 1); }
+ 90% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00011, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00011, 0, 1); }
+ 91.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); }
+ 93.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); }
+ 95% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00015, 0, 1); }
+ 96.666667% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00014, 0, 1); }
+ 98.333333% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00012, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0.00012, 0, 1); }
+ 100% { -webkit-transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
+}
diff --git a/src/js/commands.js b/src/js/commands.js
new file mode 100644
index 000000000..d7a20c3aa
--- /dev/null
+++ b/src/js/commands.js
@@ -0,0 +1,177 @@
+function Command(options) {
+ if(options) {
+ var name = options.name;
+ var prompt = options.prompt;
+ this.name = name;
+ this.tag = options.tag;
+ this.action = options.action || name;
+ this.removeAction = options.removeAction || options.action;
+ this.button = options.button || name;
+ if (prompt) { this.prompt = prompt; }
+ }
+}
+Command.prototype.exec = function(value) {
+ document.execCommand(this.action, false, value || null);
+};
+Command.prototype.unexec = function(value) {
+ document.execCommand(this.removeAction, false, value || null);
+};
+
+function BoldCommand() {
+ Command.call(this, {
+ name: 'bold',
+ tag: 'b',
+ button: ' '
+ });
+}
+inherits(BoldCommand, Command);
+BoldCommand.prototype.exec = function() {
+ // Don't allow executing bold command on heading tags
+ if (!Regex.HEADING_TAG.test(getCurrentSelectionRootTag())) {
+ BoldCommand._super.prototype.exec.call(this);
+ }
+};
+
+function ItalicCommand() {
+ Command.call(this, {
+ name: 'italic',
+ tag: 'i',
+ button: ' '
+ });
+}
+inherits(ItalicCommand, Command);
+
+function LinkCommand() {
+ Command.call(this, {
+ name: 'link',
+ tag: Tags.LINK,
+ action: 'createLink',
+ removeAction: 'unlink',
+ button: ' ',
+ prompt: new Prompt({
+ command: this,
+ placeholder: 'Enter a url, press return...'
+ })
+ });
+}
+inherits(LinkCommand, Command);
+LinkCommand.prototype.exec = function(url) {
+ if(this.tag === getCurrentSelectionTag()) {
+ this.unexec();
+ } else {
+ if (!Regex.HTTP_PROTOCOL.test(url)) {
+ url = 'http://' + url;
+ }
+ LinkCommand._super.prototype.exec.call(this, url);
+ }
+};
+
+function FormatBlockCommand(options) {
+ options.action = 'formatBlock';
+ Command.call(this, options);
+}
+inherits(FormatBlockCommand, Command);
+FormatBlockCommand.prototype.exec = function() {
+ var tag = this.tag;
+ // Brackets neccessary for certain browsers
+ var value = '<' + tag + '>';
+ // Allow block commands to be toggled back to a paragraph
+ if(tag === getCurrentSelectionRootTag()) {
+ value = Tags.PARAGRAPH;
+ } else {
+ // Flattens the selection before applying the block format.
+ // Otherwise, undesirable nested blocks can occur.
+ var root = getCurrentSelectionRootNode();
+ var flatNode = document.createTextNode(root.textContent);
+ root.parentNode.insertBefore(flatNode, root);
+ root.parentNode.removeChild(root);
+ selectNode(flatNode);
+ }
+
+ FormatBlockCommand._super.prototype.exec.call(this, value);
+};
+
+function QuoteCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'quote',
+ tag: Tags.QUOTE,
+ button: ' '
+ });
+}
+inherits(QuoteCommand, FormatBlockCommand);
+
+function HeadingCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'heading',
+ tag: Tags.HEADING,
+ button: ' 1'
+ });
+}
+inherits(HeadingCommand, FormatBlockCommand);
+
+function SubheadingCommand() {
+ FormatBlockCommand.call(this, {
+ name: 'subheading',
+ tag: Tags.SUBHEADING,
+ button: ' 2'
+ });
+}
+inherits(SubheadingCommand, FormatBlockCommand);
+
+function ListCommand(options) {
+ Command.call(this, options);
+}
+inherits(ListCommand, Command);
+ListCommand.prototype.exec = function() {
+ ListCommand._super.prototype.exec.call(this);
+
+ // After creation, lists need to be unwrapped from the default formatter P tag
+ var listNode = getCurrentSelectionRootNode();
+ var wrapperNode = listNode.parentNode;
+ if (wrapperNode.firstChild === listNode) {
+ var editorNode = wrapperNode.parentNode;
+ editorNode.insertBefore(listNode, wrapperNode);
+ editorNode.removeChild(wrapperNode);
+ selectNode(listNode);
+ }
+};
+
+function UnorderedListCommand() {
+ ListCommand.call(this, {
+ name: 'list',
+ tag: Tags.LIST,
+ action: 'insertUnorderedList',
+ button: ' '
+ });
+}
+inherits(UnorderedListCommand, ListCommand);
+
+function OrderedListCommand() {
+ ListCommand.call(this, {
+ name: 'ordered list',
+ tag: Tags.ORDERED_LIST,
+ action: 'insertOrderedList',
+ button: ' '
+ });
+}
+inherits(OrderedListCommand, ListCommand);
+
+Command.all = [
+ new BoldCommand(),
+ new ItalicCommand(),
+ new LinkCommand(),
+ new QuoteCommand(),
+ new HeadingCommand(),
+ new SubheadingCommand()
+];
+
+Command.index = (function() {
+ var index = {},
+ commands = Command.all,
+ len = commands.length, i, command;
+ for(i = 0; i < len; i++) {
+ command = commands[i];
+ index[command.name] = command;
+ }
+ return index;
+})();
diff --git a/src/js/constants.js b/src/js/constants.js
new file mode 100644
index 000000000..8cdbd0109
--- /dev/null
+++ b/src/js/constants.js
@@ -0,0 +1,31 @@
+var Keycodes = {
+ ENTER : 13,
+ ESC : 27
+};
+
+var Regex = {
+ NEWLINE : /[\r\n]/g,
+ HTTP_PROTOCOL : /^https?:\/\//i,
+ HEADING_TAG : /^(h1|h2|h3|h4|h5|h6)$/i,
+ UL_START : /^[-*]\s/,
+ OL_START : /^1\.\s/
+};
+
+var SelectionDirection = {
+ LEFT_TO_RIGHT : 0,
+ RIGHT_TO_LEFT : 1,
+ SAME_NODE : 2
+};
+
+var Tags = {
+ LINK : 'a',
+ PARAGRAPH : 'p',
+ HEADING : 'h2',
+ SUBHEADING : 'h3',
+ QUOTE : 'blockquote',
+ LIST : 'ul',
+ ORDERED_LIST : 'ol',
+ LIST_ITEM : 'li'
+};
+
+var RootTags = [ Tags.PARAGRAPH, Tags.HEADING, Tags.SUBHEADING, Tags.QUOTE, Tags.LIST, Tags.ORDERED_LIST, 'div'];
diff --git a/src/js/editor.js b/src/js/editor.js
new file mode 100644
index 000000000..c79b44703
--- /dev/null
+++ b/src/js/editor.js
@@ -0,0 +1,225 @@
+ContentKit.Editor = (function() {
+
+ // Default `Editor` options
+ var defaults = {
+ defaultFormatter: Tags.PARAGRAPH,
+ placeholder: 'Write here...',
+ spellcheck: true,
+ autofocus: true,
+ commands: Command.all
+ };
+
+ var editorClassName = 'ck-editor',
+ editorClassNameRegExp = new RegExp(editorClassName);
+
+ /**
+ * Publically expose this class which sets up indiviual `Editor` classes
+ * depending if user passes string selector, Node, or NodeList
+ */
+ function EditorFactory(element, options) {
+ var editors = [],
+ elements, elementsLen, i;
+
+ if (typeof element === 'string') {
+ elements = document.querySelectorAll(element);
+ } else if (element && element.length) {
+ elements = element;
+ } else if (element) {
+ elements = [element];
+ }
+
+ if (elements) {
+ options = extend(defaults, options);
+ elementsLen = elements.length;
+ for (i = 0; i < elementsLen; i++) {
+ editors.push(new Editor(elements[i], options));
+ }
+ }
+
+ return editors.length > 1 ? editors : editors[0];
+ }
+
+ /**
+ * @class Editor
+ * An individual Editor
+ * @param element `Element` node
+ * @param options hash of options
+ */
+ function Editor(element, options) {
+ applyConstructorProperties(this, options);
+
+ if (element) {
+ var className = element.className;
+ var dataset = element.dataset;
+
+ if (!editorClassNameRegExp.test(className)) {
+ className += (className ? ' ' : '') + editorClassName;
+ }
+ element.className = className;
+
+ if (!dataset.placeholder) {
+ dataset.placeholder = this.placeholder;
+ }
+
+ if(!this.spellcheck) {
+ element.spellcheck = false;
+ }
+
+ this.element = element;
+ this.toolbar = new Toolbar({ commands: this.commands });
+
+ bindTextSelectionEvents(this);
+ bindTypingEvents(this);
+ bindPasteEvents(this);
+
+ this.enable();
+ if(this.autofocus) {
+ element.focus();
+ }
+ }
+ }
+
+ Editor.prototype = {
+ enable: function() {
+ var editor = this,
+ element = editor.element;
+ if(element && !editor.enabled) {
+ element.setAttribute('contentEditable', true);
+ editor.enabled = true;
+ }
+ },
+ disable: function() {
+ var editor = this,
+ element = editor.element;
+ if(element && editor.enabled) {
+ element.removeAttribute('contentEditable');
+ editor.enabled = false;
+ }
+ },
+ parse: function() {
+ var editor = this;
+ if (!editor.parser) {
+ if (!ContentKit.HTMLParser) {
+ throw new Error('Include the ContentKit compiler for parsing');
+ }
+ editor.parser = new ContentKit.HTMLParser();
+ }
+ return editor.parser.parse(editor.element.innerHTML);
+ }
+ };
+
+ function bindTextSelectionEvents(editor) {
+ // Mouse text selection
+ document.addEventListener('mouseup', function(e) {
+ setTimeout(function(){ handleTextSelection(e, editor); });
+ });
+
+ // Keyboard text selection
+ editor.element.addEventListener('keyup', function(e) {
+ handleTextSelection(e, editor);
+ });
+ }
+
+ function bindTypingEvents(editor) {
+ var editorEl = editor.element;
+
+ // Breaks out of blockquotes when pressing enter.
+ editorEl.addEventListener('keyup', function(e) {
+ if(!e.shiftKey && e.which === Keycodes.ENTER) {
+ if(Tags.QUOTE === getCurrentSelectionRootTag()) {
+ document.execCommand('formatBlock', false, editor.defaultFormatter);
+ e.stopPropagation();
+ }
+ }
+ });
+
+ // Creates unordered list when block starts with '- ', or ordered if starts with '1. '
+ editorEl.addEventListener('keyup', function(e) {
+ var selectedText = window.getSelection().anchorNode.textContent,
+ selection, selectionNode, command, replaceRegex;
+
+ if (Tags.LIST_ITEM !== getCurrentSelectionTag()) {
+ if (Regex.UL_START.test(selectedText)) {
+ command = new UnorderedListCommand();
+ replaceRegex = Regex.UL_START;
+ } else if (Regex.OL_START.test(selectedText)) {
+ command = new OrderedListCommand();
+ replaceRegex = Regex.OL_START;
+ }
+
+ if (command) {
+ command.exec();
+ selection = window.getSelection();
+ selectionNode = selection.anchorNode;
+ selectionNode.textContent = selectedText.replace(replaceRegex, '');
+ moveCursorToBeginningOfSelection(selection);
+ e.stopPropagation();
+ }
+ }
+ });
+
+ // Assure there is always a paragraph and not divs
+ editorEl.addEventListener('keyup', function() {
+ var node = getCurrentSelectionRootNode();
+ // TODO: support root or other block element
+ if(getNodeTagName(node) === 'div' && node.innerHTML !== '') {
+ document.execCommand('formatBlock', false, editor.defaultFormatter);
+ }
+ });
+ }
+
+ function handleTextSelection(e, editor) {
+ var selection = window.getSelection();
+ if (!selection.isCollapsed && selectionIsInElement(editor.element, selection)) {
+ editor.toolbar.updateForSelection(selection);
+ } else {
+ editor.toolbar.hide();
+ }
+ }
+
+ function selectionIsInElement(element, selection) {
+ var node = selection.focusNode,
+ parentNode = node.parentNode;
+ while(parentNode) {
+ if (parentNode === element) {
+ return true;
+ }
+ parentNode = parentNode.parentNode;
+ }
+ return false;
+ }
+
+ function bindPasteEvents(editor) {
+ editor.element.addEventListener('paste', function(e) {
+ var data = e.clipboardData, plainText;
+ e.preventDefault();
+ if(data && data.getData) {
+ plainText = data.getData('text/plain');
+ var formattedContent = plainTextToBlocks(plainText, editor.defaultFormatter);
+ document.execCommand('insertHTML', false, formattedContent);
+ }
+ });
+ }
+
+ function plainTextToBlocks(plainText, blockTag) {
+ var blocks = plainText.split(Regex.NEWLINE),
+ len = blocks.length,
+ block, openTag, closeTag, content, i;
+ if(len < 2) {
+ return plainText;
+ } else {
+ content = '';
+ openTag = '<' + blockTag + '>';
+ closeTag = '' + blockTag + '>';
+ for(i=0; i -1) {
+ button.setActive();
+ } else {
+ button.setInactive();
+ }
+ }
+ }
+
+ function tagsInSelection(selection) {
+ var node = selection.focusNode.parentNode,
+ tags = [];
+
+ if (!selection.isCollapsed) {
+ while(node) {
+ // Stop traversing up dom when hitting an editor element
+ if (node.contentEditable === 'true') { break; }
+ if (node.tagName) {
+ tags.push(node.tagName.toLowerCase());
+ }
+ node = node.parentNode;
+ }
+ }
+ return tags;
+ }
+
+ return Toolbar;
+}());
diff --git a/src/js/utils.js b/src/js/utils.js
new file mode 100644
index 000000000..8efe77085
--- /dev/null
+++ b/src/js/utils.js
@@ -0,0 +1,110 @@
+function getNodeTagName(node) {
+ return node.tagName && node.tagName.toLowerCase() || null;
+}
+
+function getDirectionOfSelection(selection) {
+ var position = selection.anchorNode.compareDocumentPosition(selection.focusNode);
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
+ return SelectionDirection.LEFT_TO_RIGHT;
+ } else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
+ return SelectionDirection.RIGHT_TO_LEFT;
+ }
+ return SelectionDirection.SAME_NODE;
+}
+
+function getCurrentSelectionNode() {
+ var selection = window.getSelection();
+ var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.anchorNode : selection.focusNode;
+ return node && (node.nodeType === 3 ? node.parentNode : node);
+}
+
+function getCurrentSelectionRootNode() {
+ var node = getCurrentSelectionNode(),
+ tag = getNodeTagName(node);
+ while (tag && RootTags.indexOf(tag) === -1) {
+ node = node.parentNode;
+ tag = getNodeTagName(node);
+ }
+ return node;
+}
+
+function getCurrentSelectionTag() {
+ return getNodeTagName(getCurrentSelectionNode());
+}
+
+function getCurrentSelectionRootTag() {
+ return getNodeTagName(getCurrentSelectionRootNode());
+}
+
+function getElementOffset(element) {
+ var offset = { left: 0, top: 0 };
+ var elementStyle = window.getComputedStyle(element);
+
+ if (elementStyle.position === 'relative') {
+ offset.left = parseInt(elementStyle['margin-left'], 10);
+ offset.top = parseInt(elementStyle['margin-top'], 10);
+ }
+ return offset;
+}
+
+function createDiv(className) {
+ var div = document.createElement('div');
+ if (className) {
+ div.className = className;
+ }
+ return div;
+}
+
+function extend(object, updates) {
+ updates = updates || {};
+ for(var o in updates) {
+ if (updates.hasOwnProperty(o)) {
+ object[o] = updates[o];
+ }
+ }
+ return object;
+}
+
+function applyConstructorProperties(instance, props) {
+ for(var p in props) {
+ if (props.hasOwnProperty(p)) {
+ instance[p] = props[p];
+ }
+ }
+}
+
+function inherits(Subclass, Superclass) {
+ Subclass._super = Superclass;
+ Subclass.prototype = Object.create(Superclass.prototype, {
+ constructor: {
+ value: Subclass,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+}
+
+function moveCursorToBeginningOfSelection(selection) {
+ var range = document.createRange(),
+ node = selection.anchorNode;
+ range.setStart(node, 0);
+ range.setEnd(node, 0);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function restoreRange(range) {
+ var selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function selectNode(node) {
+ var range = document.createRange(),
+ selection = window.getSelection();
+ range.setStart(node, 0);
+ range.setEnd(node, node.length);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
diff --git a/tests/index.html b/tests/index.html
new file mode 100644
index 000000000..3513b8af5
--- /dev/null
+++ b/tests/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ QUnit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/lib/qunit-1.13.0.css b/tests/lib/qunit-1.13.0.css
new file mode 100644
index 000000000..26a85d24b
--- /dev/null
+++ b/tests/lib/qunit-1.13.0.css
@@ -0,0 +1,245 @@
+/*!
+ * QUnit 1.13.0
+ * http://qunitjs.com/
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2014-01-04T17:09Z
+ */
+
+/** Font Family and Sizes */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+}
+
+#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-tests { font-size: smaller; }
+
+
+/** Resets */
+
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+ margin: 0;
+ padding: 0;
+}
+
+
+/** Header */
+
+#qunit-header {
+ padding: 0.5em 0 0.5em 1em;
+
+ color: #8699a4;
+ background-color: #0d3349;
+
+ font-size: 1.5em;
+ line-height: 1em;
+ font-weight: normal;
+
+ border-radius: 5px 5px 0 0;
+ -moz-border-radius: 5px 5px 0 0;
+ -webkit-border-top-right-radius: 5px;
+ -webkit-border-top-left-radius: 5px;
+}
+
+#qunit-header a {
+ text-decoration: none;
+ color: #c2ccd1;
+}
+
+#qunit-header a:hover,
+#qunit-header a:focus {
+ color: #fff;
+}
+
+#qunit-testrunner-toolbar label {
+ display: inline-block;
+ padding: 0 .5em 0 .1em;
+}
+
+#qunit-banner {
+ height: 5px;
+}
+
+#qunit-testrunner-toolbar {
+ padding: 0.5em 0 0.5em 2em;
+ color: #5E740B;
+ background-color: #eee;
+ overflow: hidden;
+}
+
+#qunit-userAgent {
+ padding: 0.5em 0 0.5em 2.5em;
+ background-color: #2b81af;
+ color: #fff;
+ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+
+#qunit-modulefilter-container {
+ float: right;
+}
+
+/** Tests: Pass/Fail */
+
+#qunit-tests {
+ list-style-position: inside;
+}
+
+#qunit-tests li {
+ padding: 0.4em 0.5em 0.4em 2.5em;
+ border-bottom: 1px solid #fff;
+ list-style-position: inside;
+}
+
+#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
+ display: none;
+}
+
+#qunit-tests li strong {
+ cursor: pointer;
+}
+
+#qunit-tests li a {
+ padding: 0.5em;
+ color: #c2ccd1;
+ text-decoration: none;
+}
+#qunit-tests li a:hover,
+#qunit-tests li a:focus {
+ color: #000;
+}
+
+#qunit-tests li .runtime {
+ float: right;
+ font-size: smaller;
+}
+
+.qunit-assert-list {
+ margin-top: 0.5em;
+ padding: 0.5em;
+
+ background-color: #fff;
+
+ border-radius: 5px;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+}
+
+.qunit-collapsed {
+ display: none;
+}
+
+#qunit-tests table {
+ border-collapse: collapse;
+ margin-top: .2em;
+}
+
+#qunit-tests th {
+ text-align: right;
+ vertical-align: top;
+ padding: 0 .5em 0 0;
+}
+
+#qunit-tests td {
+ vertical-align: top;
+}
+
+#qunit-tests pre {
+ margin: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+#qunit-tests del {
+ background-color: #e0f2be;
+ color: #374e0c;
+ text-decoration: none;
+}
+
+#qunit-tests ins {
+ background-color: #ffcaca;
+ color: #500;
+ text-decoration: none;
+}
+
+/*** Test Counts */
+
+#qunit-tests b.counts { color: black; }
+#qunit-tests b.passed { color: #5E740B; }
+#qunit-tests b.failed { color: #710909; }
+
+#qunit-tests li li {
+ padding: 5px;
+ background-color: #fff;
+ border-bottom: none;
+ list-style-position: inside;
+}
+
+/*** Passing Styles */
+
+#qunit-tests li li.pass {
+ color: #3c510c;
+ background-color: #fff;
+ border-left: 10px solid #C6E746;
+}
+
+#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
+#qunit-tests .pass .test-name { color: #366097; }
+
+#qunit-tests .pass .test-actual,
+#qunit-tests .pass .test-expected { color: #999999; }
+
+#qunit-banner.qunit-pass { background-color: #C6E746; }
+
+/*** Failing Styles */
+
+#qunit-tests li li.fail {
+ color: #710909;
+ background-color: #fff;
+ border-left: 10px solid #EE5757;
+ white-space: pre;
+}
+
+#qunit-tests > li:last-child {
+ border-radius: 0 0 5px 5px;
+ -moz-border-radius: 0 0 5px 5px;
+ -webkit-border-bottom-right-radius: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+}
+
+#qunit-tests .fail { color: #000000; background-color: #EE5757; }
+#qunit-tests .fail .test-name,
+#qunit-tests .fail .module-name { color: #000000; }
+
+#qunit-tests .fail .test-actual { color: #EE5757; }
+#qunit-tests .fail .test-expected { color: green; }
+
+#qunit-banner.qunit-fail { background-color: #EE5757; }
+
+
+/** Result */
+
+#qunit-testresult {
+ padding: 0.5em 0.5em 0.5em 2.5em;
+
+ color: #2b81af;
+ background-color: #D2E0E6;
+
+ border-bottom: 1px solid white;
+}
+#qunit-testresult .module-name {
+ font-weight: bold;
+}
+
+/** Fixture */
+
+#qunit-fixture {
+ position: absolute;
+ top: -10000px;
+ left: -10000px;
+ width: 1000px;
+ height: 1000px;
+}
diff --git a/tests/lib/qunit-1.13.0.js b/tests/lib/qunit-1.13.0.js
new file mode 100644
index 000000000..a2fb2e884
--- /dev/null
+++ b/tests/lib/qunit-1.13.0.js
@@ -0,0 +1,2210 @@
+/*!
+ * QUnit 1.13.0
+ * http://qunitjs.com/
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2014-01-04T17:09Z
+ */
+
+(function( window ) {
+
+var QUnit,
+ assert,
+ config,
+ onErrorFnPrev,
+ testId = 0,
+ fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ // Keep a local reference to Date (GH-283)
+ Date = window.Date,
+ setTimeout = window.setTimeout,
+ defined = {
+ document: typeof window.document !== "undefined",
+ setTimeout: typeof window.setTimeout !== "undefined",
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch( e ) {
+ return false;
+ }
+ }())
+ },
+ /**
+ * Provides a normalized error string, correcting an issue
+ * with IE 7 (and prior) where Error.prototype.toString is
+ * not properly implemented
+ *
+ * Based on http://es5.github.com/#x15.11.4.4
+ *
+ * @param {String|Error} error
+ * @return {String} error message
+ */
+ errorString = function( error ) {
+ var name, message,
+ errorString = error.toString();
+ if ( errorString.substring( 0, 7 ) === "[object" ) {
+ name = error.name ? error.name.toString() : "Error";
+ message = error.message ? error.message.toString() : "";
+ if ( name && message ) {
+ return name + ": " + message;
+ } else if ( name ) {
+ return name;
+ } else if ( message ) {
+ return message;
+ } else {
+ return "Error";
+ }
+ } else {
+ return errorString;
+ }
+ },
+ /**
+ * Makes a clone of an object using only Array or Object as base,
+ * and copies over the own enumerable properties.
+ *
+ * @param {Object} obj
+ * @return {Object} New object with only the own properties (recursively).
+ */
+ objectValues = function( obj ) {
+ // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
+ /*jshint newcap: false */
+ var key, val,
+ vals = QUnit.is( "array", obj ) ? [] : {};
+ for ( key in obj ) {
+ if ( hasOwn.call( obj, key ) ) {
+ val = obj[key];
+ vals[key] = val === Object(val) ? objectValues(val) : val;
+ }
+ }
+ return vals;
+ };
+
+
+// Root QUnit object.
+// `QUnit` initialized at top of scope
+QUnit = {
+
+ // call on start of module test to prepend name to all tests
+ module: function( name, testEnvironment ) {
+ config.currentModule = name;
+ config.currentModuleTestEnvironment = testEnvironment;
+ config.modules[name] = true;
+ },
+
+ asyncTest: function( testName, expected, callback ) {
+ if ( arguments.length === 2 ) {
+ callback = expected;
+ expected = null;
+ }
+
+ QUnit.test( testName, expected, callback, true );
+ },
+
+ test: function( testName, expected, callback, async ) {
+ var test,
+ nameHtml = "" + escapeText( testName ) + " ";
+
+ if ( arguments.length === 2 ) {
+ callback = expected;
+ expected = null;
+ }
+
+ if ( config.currentModule ) {
+ nameHtml = "" + escapeText( config.currentModule ) + " : " + nameHtml;
+ }
+
+ test = new Test({
+ nameHtml: nameHtml,
+ testName: testName,
+ expected: expected,
+ async: async,
+ callback: callback,
+ module: config.currentModule,
+ moduleTestEnvironment: config.currentModuleTestEnvironment,
+ stack: sourceFromStacktrace( 2 )
+ });
+
+ if ( !validTest( test ) ) {
+ return;
+ }
+
+ test.queue();
+ },
+
+ // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
+ expect: function( asserts ) {
+ if (arguments.length === 1) {
+ config.current.expected = asserts;
+ } else {
+ return config.current.expected;
+ }
+ },
+
+ start: function( count ) {
+ // QUnit hasn't been initialized yet.
+ // Note: RequireJS (et al) may delay onLoad
+ if ( config.semaphore === undefined ) {
+ QUnit.begin(function() {
+ // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
+ setTimeout(function() {
+ QUnit.start( count );
+ });
+ });
+ return;
+ }
+
+ config.semaphore -= count || 1;
+ // don't start until equal number of stop-calls
+ if ( config.semaphore > 0 ) {
+ return;
+ }
+ // ignore if start is called more often then stop
+ if ( config.semaphore < 0 ) {
+ config.semaphore = 0;
+ QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
+ return;
+ }
+ // A slight delay, to avoid any current callbacks
+ if ( defined.setTimeout ) {
+ setTimeout(function() {
+ if ( config.semaphore > 0 ) {
+ return;
+ }
+ if ( config.timeout ) {
+ clearTimeout( config.timeout );
+ }
+
+ config.blocking = false;
+ process( true );
+ }, 13);
+ } else {
+ config.blocking = false;
+ process( true );
+ }
+ },
+
+ stop: function( count ) {
+ config.semaphore += count || 1;
+ config.blocking = true;
+
+ if ( config.testTimeout && defined.setTimeout ) {
+ clearTimeout( config.timeout );
+ config.timeout = setTimeout(function() {
+ QUnit.ok( false, "Test timed out" );
+ config.semaphore = 1;
+ QUnit.start();
+ }, config.testTimeout );
+ }
+ }
+};
+
+// We use the prototype to distinguish between properties that should
+// be exposed as globals (and in exports) and those that shouldn't
+(function() {
+ function F() {}
+ F.prototype = QUnit;
+ QUnit = new F();
+ // Make F QUnit's constructor so that we can add to the prototype later
+ QUnit.constructor = F;
+}());
+
+/**
+ * Config object: Maintain internal state
+ * Later exposed as QUnit.config
+ * `config` initialized at top of scope
+ */
+config = {
+ // The queue of tests to run
+ queue: [],
+
+ // block until document ready
+ blocking: true,
+
+ // when enabled, show only failing tests
+ // gets persisted through sessionStorage and can be changed in UI via checkbox
+ hidepassed: false,
+
+ // by default, run previously failed tests first
+ // very useful in combination with "Hide passed tests" checked
+ reorder: true,
+
+ // by default, modify document.title when suite is done
+ altertitle: true,
+
+ // when enabled, all tests must call expect()
+ requireExpects: false,
+
+ // add checkboxes that are persisted in the query-string
+ // when enabled, the id is set to `true` as a `QUnit.config` property
+ urlConfig: [
+ {
+ id: "noglobals",
+ label: "Check for Globals",
+ tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
+ },
+ {
+ id: "notrycatch",
+ label: "No try-catch",
+ tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
+ }
+ ],
+
+ // Set of all modules.
+ modules: {},
+
+ // logging callback queues
+ begin: [],
+ done: [],
+ log: [],
+ testStart: [],
+ testDone: [],
+ moduleStart: [],
+ moduleDone: []
+};
+
+// Initialize more QUnit.config and QUnit.urlParams
+(function() {
+ var i,
+ location = window.location || { search: "", protocol: "file:" },
+ params = location.search.slice( 1 ).split( "&" ),
+ length = params.length,
+ urlParams = {},
+ current;
+
+ if ( params[ 0 ] ) {
+ for ( i = 0; i < length; i++ ) {
+ current = params[ i ].split( "=" );
+ current[ 0 ] = decodeURIComponent( current[ 0 ] );
+ // allow just a key to turn on a flag, e.g., test.html?noglobals
+ current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+ urlParams[ current[ 0 ] ] = current[ 1 ];
+ }
+ }
+
+ QUnit.urlParams = urlParams;
+
+ // String search anywhere in moduleName+testName
+ config.filter = urlParams.filter;
+
+ // Exact match of the module name
+ config.module = urlParams.module;
+
+ config.testNumber = parseInt( urlParams.testNumber, 10 ) || null;
+
+ // Figure out if we're running the tests from a server or not
+ QUnit.isLocal = location.protocol === "file:";
+}());
+
+extend( QUnit, {
+
+ config: config,
+
+ // Initialize the configuration options
+ init: function() {
+ extend( config, {
+ stats: { all: 0, bad: 0 },
+ moduleStats: { all: 0, bad: 0 },
+ started: +new Date(),
+ updateRate: 1000,
+ blocking: false,
+ autostart: true,
+ autorun: false,
+ filter: "",
+ queue: [],
+ semaphore: 1
+ });
+
+ var tests, banner, result,
+ qunit = id( "qunit" );
+
+ if ( qunit ) {
+ qunit.innerHTML =
+ "" +
+ " " +
+ "
" +
+ " " +
+ " ";
+ }
+
+ tests = id( "qunit-tests" );
+ banner = id( "qunit-banner" );
+ result = id( "qunit-testresult" );
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ }
+
+ if ( banner ) {
+ banner.className = "";
+ }
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running... ";
+ }
+ },
+
+ // Resets the test setup. Useful for tests that modify the DOM.
+ /*
+ DEPRECATED: Use multiple tests instead of resetting inside a test.
+ Use testStart or testDone for custom cleanup.
+ This method will throw an error in 2.0, and will be removed in 2.1
+ */
+ reset: function() {
+ var fixture = id( "qunit-fixture" );
+ if ( fixture ) {
+ fixture.innerHTML = config.fixture;
+ }
+ },
+
+ // Safe object type checking
+ is: function( type, obj ) {
+ return QUnit.objectType( obj ) === type;
+ },
+
+ objectType: function( obj ) {
+ if ( typeof obj === "undefined" ) {
+ return "undefined";
+ }
+
+ // Consider: typeof null === object
+ if ( obj === null ) {
+ return "null";
+ }
+
+ var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
+ type = match && match[1] || "";
+
+ switch ( type ) {
+ case "Number":
+ if ( isNaN(obj) ) {
+ return "nan";
+ }
+ return "number";
+ case "String":
+ case "Boolean":
+ case "Array":
+ case "Date":
+ case "RegExp":
+ case "Function":
+ return type.toLowerCase();
+ }
+ if ( typeof obj === "object" ) {
+ return "object";
+ }
+ return undefined;
+ },
+
+ push: function( result, actual, expected, message ) {
+ if ( !config.current ) {
+ throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
+ }
+
+ var output, source,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: result,
+ message: message,
+ actual: actual,
+ expected: expected
+ };
+
+ message = escapeText( message ) || ( result ? "okay" : "failed" );
+ message = "" + message + " ";
+ output = message;
+
+ if ( !result ) {
+ expected = escapeText( QUnit.jsDump.parse(expected) );
+ actual = escapeText( QUnit.jsDump.parse(actual) );
+ output += "Expected: " + expected + " ";
+
+ if ( actual !== expected ) {
+ output += "Result: " + actual + " ";
+ output += "Diff: " + QUnit.diff( expected, actual ) + " ";
+ }
+
+ source = sourceFromStacktrace();
+
+ if ( source ) {
+ details.source = source;
+ output += "Source: " + escapeText( source ) + " ";
+ }
+
+ output += "
";
+ }
+
+ runLoggingCallbacks( "log", QUnit, details );
+
+ config.current.assertions.push({
+ result: !!result,
+ message: output
+ });
+ },
+
+ pushFailure: function( message, source, actual ) {
+ if ( !config.current ) {
+ throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
+ }
+
+ var output,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: false,
+ message: message
+ };
+
+ message = escapeText( message ) || "error";
+ message = "" + message + " ";
+ output = message;
+
+ output += "";
+
+ if ( actual ) {
+ output += "Result: " + escapeText( actual ) + " ";
+ }
+
+ if ( source ) {
+ details.source = source;
+ output += "Source: " + escapeText( source ) + " ";
+ }
+
+ output += "
";
+
+ runLoggingCallbacks( "log", QUnit, details );
+
+ config.current.assertions.push({
+ result: false,
+ message: output
+ });
+ },
+
+ url: function( params ) {
+ params = extend( extend( {}, QUnit.urlParams ), params );
+ var key,
+ querystring = "?";
+
+ for ( key in params ) {
+ if ( hasOwn.call( params, key ) ) {
+ querystring += encodeURIComponent( key ) + "=" +
+ encodeURIComponent( params[ key ] ) + "&";
+ }
+ }
+ return window.location.protocol + "//" + window.location.host +
+ window.location.pathname + querystring.slice( 0, -1 );
+ },
+
+ extend: extend,
+ id: id,
+ addEvent: addEvent,
+ addClass: addClass,
+ hasClass: hasClass,
+ removeClass: removeClass
+ // load, equiv, jsDump, diff: Attached later
+});
+
+/**
+ * @deprecated: Created for backwards compatibility with test runner that set the hook function
+ * into QUnit.{hook}, instead of invoking it and passing the hook function.
+ * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
+ * Doing this allows us to tell if the following methods have been overwritten on the actual
+ * QUnit object.
+ */
+extend( QUnit.constructor.prototype, {
+
+ // Logging callbacks; all receive a single argument with the listed properties
+ // run test/logs.html for any related changes
+ begin: registerLoggingCallback( "begin" ),
+
+ // done: { failed, passed, total, runtime }
+ done: registerLoggingCallback( "done" ),
+
+ // log: { result, actual, expected, message }
+ log: registerLoggingCallback( "log" ),
+
+ // testStart: { name }
+ testStart: registerLoggingCallback( "testStart" ),
+
+ // testDone: { name, failed, passed, total, runtime }
+ testDone: registerLoggingCallback( "testDone" ),
+
+ // moduleStart: { name }
+ moduleStart: registerLoggingCallback( "moduleStart" ),
+
+ // moduleDone: { name, failed, passed, total }
+ moduleDone: registerLoggingCallback( "moduleDone" )
+});
+
+if ( !defined.document || document.readyState === "complete" ) {
+ config.autorun = true;
+}
+
+QUnit.load = function() {
+ runLoggingCallbacks( "begin", QUnit, {} );
+
+ // Initialize the config, saving the execution queue
+ var banner, filter, i, label, len, main, ol, toolbar, userAgent, val,
+ urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter,
+ numModules = 0,
+ moduleNames = [],
+ moduleFilterHtml = "",
+ urlConfigHtml = "",
+ oldconfig = extend( {}, config );
+
+ QUnit.init();
+ extend(config, oldconfig);
+
+ config.blocking = false;
+
+ len = config.urlConfig.length;
+
+ for ( i = 0; i < len; i++ ) {
+ val = config.urlConfig[i];
+ if ( typeof val === "string" ) {
+ val = {
+ id: val,
+ label: val,
+ tooltip: "[no tooltip available]"
+ };
+ }
+ config[ val.id ] = QUnit.urlParams[ val.id ];
+ urlConfigHtml += "" + val.label + " ";
+ }
+ for ( i in config.modules ) {
+ if ( config.modules.hasOwnProperty( i ) ) {
+ moduleNames.push(i);
+ }
+ }
+ numModules = moduleNames.length;
+ moduleNames.sort( function( a, b ) {
+ return a.localeCompare( b );
+ });
+ moduleFilterHtml += "Module: < All Modules > ";
+
+
+ for ( i = 0; i < numModules; i++) {
+ moduleFilterHtml += "" + escapeText(moduleNames[i]) + " ";
+ }
+ moduleFilterHtml += " ";
+
+ // `userAgent` initialized at top of scope
+ userAgent = id( "qunit-userAgent" );
+ if ( userAgent ) {
+ userAgent.innerHTML = navigator.userAgent;
+ }
+
+ // `banner` initialized at top of scope
+ banner = id( "qunit-header" );
+ if ( banner ) {
+ banner.innerHTML = "" + banner.innerHTML + " ";
+ }
+
+ // `toolbar` initialized at top of scope
+ toolbar = id( "qunit-testrunner-toolbar" );
+ if ( toolbar ) {
+ // `filter` initialized at top of scope
+ filter = document.createElement( "input" );
+ filter.type = "checkbox";
+ filter.id = "qunit-filter-pass";
+
+ addEvent( filter, "click", function() {
+ var tmp,
+ ol = id( "qunit-tests" );
+
+ if ( filter.checked ) {
+ ol.className = ol.className + " hidepass";
+ } else {
+ tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
+ ol.className = tmp.replace( / hidepass /, " " );
+ }
+ if ( defined.sessionStorage ) {
+ if (filter.checked) {
+ sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
+ } else {
+ sessionStorage.removeItem( "qunit-filter-passed-tests" );
+ }
+ }
+ });
+
+ if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
+ filter.checked = true;
+ // `ol` initialized at top of scope
+ ol = id( "qunit-tests" );
+ ol.className = ol.className + " hidepass";
+ }
+ toolbar.appendChild( filter );
+
+ // `label` initialized at top of scope
+ label = document.createElement( "label" );
+ label.setAttribute( "for", "qunit-filter-pass" );
+ label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." );
+ label.innerHTML = "Hide passed tests";
+ toolbar.appendChild( label );
+
+ urlConfigCheckboxesContainer = document.createElement("span");
+ urlConfigCheckboxesContainer.innerHTML = urlConfigHtml;
+ urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input");
+ // For oldIE support:
+ // * Add handlers to the individual elements instead of the container
+ // * Use "click" instead of "change"
+ // * Fallback from event.target to event.srcElement
+ addEvents( urlConfigCheckboxes, "click", function( event ) {
+ var params = {},
+ target = event.target || event.srcElement;
+ params[ target.name ] = target.checked ? true : undefined;
+ window.location = QUnit.url( params );
+ });
+ toolbar.appendChild( urlConfigCheckboxesContainer );
+
+ if (numModules > 1) {
+ moduleFilter = document.createElement( "span" );
+ moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
+ moduleFilter.innerHTML = moduleFilterHtml;
+ addEvent( moduleFilter.lastChild, "change", function() {
+ var selectBox = moduleFilter.getElementsByTagName("select")[0],
+ selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
+
+ window.location = QUnit.url({
+ module: ( selectedModule === "" ) ? undefined : selectedModule,
+ // Remove any existing filters
+ filter: undefined,
+ testNumber: undefined
+ });
+ });
+ toolbar.appendChild(moduleFilter);
+ }
+ }
+
+ // `main` initialized at top of scope
+ main = id( "qunit-fixture" );
+ if ( main ) {
+ config.fixture = main.innerHTML;
+ }
+
+ if ( config.autostart ) {
+ QUnit.start();
+ }
+};
+
+if ( defined.document ) {
+ addEvent( window, "load", QUnit.load );
+}
+
+// `onErrorFnPrev` initialized at top of scope
+// Preserve other handlers
+onErrorFnPrev = window.onerror;
+
+// Cover uncaught exceptions
+// Returning true will suppress the default browser handler,
+// returning false will let it run.
+window.onerror = function ( error, filePath, linerNr ) {
+ var ret = false;
+ if ( onErrorFnPrev ) {
+ ret = onErrorFnPrev( error, filePath, linerNr );
+ }
+
+ // Treat return value as window.onerror itself does,
+ // Only do our handling if not suppressed.
+ if ( ret !== true ) {
+ if ( QUnit.config.current ) {
+ if ( QUnit.config.current.ignoreGlobalErrors ) {
+ return true;
+ }
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ } else {
+ QUnit.test( "global failure", extend( function() {
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ }, { validTest: validTest } ) );
+ }
+ return false;
+ }
+
+ return ret;
+};
+
+function done() {
+ config.autorun = true;
+
+ // Log the last module results
+ if ( config.previousModule ) {
+ runLoggingCallbacks( "moduleDone", QUnit, {
+ name: config.previousModule,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all
+ });
+ }
+ delete config.previousModule;
+
+ var i, key,
+ banner = id( "qunit-banner" ),
+ tests = id( "qunit-tests" ),
+ runtime = +new Date() - config.started,
+ passed = config.stats.all - config.stats.bad,
+ html = [
+ "Tests completed in ",
+ runtime,
+ " milliseconds. ",
+ "",
+ passed,
+ " assertions of ",
+ config.stats.all,
+ " passed, ",
+ config.stats.bad,
+ " failed."
+ ].join( "" );
+
+ if ( banner ) {
+ banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
+ }
+
+ if ( tests ) {
+ id( "qunit-testresult" ).innerHTML = html;
+ }
+
+ if ( config.altertitle && defined.document && document.title ) {
+ // show ✖ for good, ✔ for bad suite result in title
+ // use escape sequences in case file gets loaded with non-utf-8-charset
+ document.title = [
+ ( config.stats.bad ? "\u2716" : "\u2714" ),
+ document.title.replace( /^[\u2714\u2716] /i, "" )
+ ].join( " " );
+ }
+
+ // clear own sessionStorage items if all tests passed
+ if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
+ // `key` & `i` initialized at top of scope
+ for ( i = 0; i < sessionStorage.length; i++ ) {
+ key = sessionStorage.key( i++ );
+ if ( key.indexOf( "qunit-test-" ) === 0 ) {
+ sessionStorage.removeItem( key );
+ }
+ }
+ }
+
+ // scroll back to top to show results
+ if ( window.scrollTo ) {
+ window.scrollTo(0, 0);
+ }
+
+ runLoggingCallbacks( "done", QUnit, {
+ failed: config.stats.bad,
+ passed: passed,
+ total: config.stats.all,
+ runtime: runtime
+ });
+}
+
+/** @return Boolean: true if this test should be ran */
+function validTest( test ) {
+ var include,
+ filter = config.filter && config.filter.toLowerCase(),
+ module = config.module && config.module.toLowerCase(),
+ fullName = (test.module + ": " + test.testName).toLowerCase();
+
+ // Internally-generated tests are always valid
+ if ( test.callback && test.callback.validTest === validTest ) {
+ delete test.callback.validTest;
+ return true;
+ }
+
+ if ( config.testNumber ) {
+ return test.testNumber === config.testNumber;
+ }
+
+ if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
+ return false;
+ }
+
+ if ( !filter ) {
+ return true;
+ }
+
+ include = filter.charAt( 0 ) !== "!";
+ if ( !include ) {
+ filter = filter.slice( 1 );
+ }
+
+ // If the filter matches, we need to honour include
+ if ( fullName.indexOf( filter ) !== -1 ) {
+ return include;
+ }
+
+ // Otherwise, do the opposite
+ return !include;
+}
+
+// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
+// Later Safari and IE10 are supposed to support error.stack as well
+// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
+function extractStacktrace( e, offset ) {
+ offset = offset === undefined ? 3 : offset;
+
+ var stack, include, i;
+
+ if ( e.stacktrace ) {
+ // Opera
+ return e.stacktrace.split( "\n" )[ offset + 3 ];
+ } else if ( e.stack ) {
+ // Firefox, Chrome
+ stack = e.stack.split( "\n" );
+ if (/^error$/i.test( stack[0] ) ) {
+ stack.shift();
+ }
+ if ( fileName ) {
+ include = [];
+ for ( i = offset; i < stack.length; i++ ) {
+ if ( stack[ i ].indexOf( fileName ) !== -1 ) {
+ break;
+ }
+ include.push( stack[ i ] );
+ }
+ if ( include.length ) {
+ return include.join( "\n" );
+ }
+ }
+ return stack[ offset ];
+ } else if ( e.sourceURL ) {
+ // Safari, PhantomJS
+ // hopefully one day Safari provides actual stacktraces
+ // exclude useless self-reference for generated Error objects
+ if ( /qunit.js$/.test( e.sourceURL ) ) {
+ return;
+ }
+ // for actual exceptions, this is useful
+ return e.sourceURL + ":" + e.line;
+ }
+}
+function sourceFromStacktrace( offset ) {
+ try {
+ throw new Error();
+ } catch ( e ) {
+ return extractStacktrace( e, offset );
+ }
+}
+
+/**
+ * Escape text for attribute or text content.
+ */
+function escapeText( s ) {
+ if ( !s ) {
+ return "";
+ }
+ s = s + "";
+ // Both single quotes and double quotes (for attributes)
+ return s.replace( /['"<>&]/g, function( s ) {
+ switch( s ) {
+ case "'":
+ return "'";
+ case "\"":
+ return """;
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case "&":
+ return "&";
+ }
+ });
+}
+
+function synchronize( callback, last ) {
+ config.queue.push( callback );
+
+ if ( config.autorun && !config.blocking ) {
+ process( last );
+ }
+}
+
+function process( last ) {
+ function next() {
+ process( last );
+ }
+ var start = new Date().getTime();
+ config.depth = config.depth ? config.depth + 1 : 1;
+
+ while ( config.queue.length && !config.blocking ) {
+ if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
+ config.queue.shift()();
+ } else {
+ setTimeout( next, 13 );
+ break;
+ }
+ }
+ config.depth--;
+ if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
+ done();
+ }
+}
+
+function saveGlobal() {
+ config.pollution = [];
+
+ if ( config.noglobals ) {
+ for ( var key in window ) {
+ if ( hasOwn.call( window, key ) ) {
+ // in Opera sometimes DOM element ids show up here, ignore them
+ if ( /^qunit-test-output/.test( key ) ) {
+ continue;
+ }
+ config.pollution.push( key );
+ }
+ }
+ }
+}
+
+function checkPollution() {
+ var newGlobals,
+ deletedGlobals,
+ old = config.pollution;
+
+ saveGlobal();
+
+ newGlobals = diff( config.pollution, old );
+ if ( newGlobals.length > 0 ) {
+ QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
+ }
+
+ deletedGlobals = diff( old, config.pollution );
+ if ( deletedGlobals.length > 0 ) {
+ QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
+ }
+}
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+ var i, j,
+ result = a.slice();
+
+ for ( i = 0; i < result.length; i++ ) {
+ for ( j = 0; j < b.length; j++ ) {
+ if ( result[i] === b[j] ) {
+ result.splice( i, 1 );
+ i--;
+ break;
+ }
+ }
+ }
+ return result;
+}
+
+function extend( a, b ) {
+ for ( var prop in b ) {
+ if ( hasOwn.call( b, prop ) ) {
+ // Avoid "Member not found" error in IE8 caused by messing with window.constructor
+ if ( !( prop === "constructor" && a === window ) ) {
+ if ( b[ prop ] === undefined ) {
+ delete a[ prop ];
+ } else {
+ a[ prop ] = b[ prop ];
+ }
+ }
+ }
+ }
+
+ return a;
+}
+
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvent( elem, type, fn ) {
+ if ( elem.addEventListener ) {
+
+ // Standards-based browsers
+ elem.addEventListener( type, fn, false );
+ } else if ( elem.attachEvent ) {
+
+ // support: IE <9
+ elem.attachEvent( "on" + type, fn );
+ } else {
+
+ // Caller must ensure support for event listeners is present
+ throw new Error( "addEvent() was called in a context without event listener support" );
+ }
+}
+
+/**
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvents( elems, type, fn ) {
+ var i = elems.length;
+ while ( i-- ) {
+ addEvent( elems[i], type, fn );
+ }
+}
+
+function hasClass( elem, name ) {
+ return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += (elem.className ? " " : "") + name;
+ }
+}
+
+function removeClass( elem, name ) {
+ var set = " " + elem.className + " ";
+ // Class name may appear multiple times
+ while ( set.indexOf(" " + name + " ") > -1 ) {
+ set = set.replace(" " + name + " " , " ");
+ }
+ // If possible, trim it for prettiness, but not necessarily
+ elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, "");
+}
+
+function id( name ) {
+ return defined.document && document.getElementById && document.getElementById( name );
+}
+
+function registerLoggingCallback( key ) {
+ return function( callback ) {
+ config[key].push( callback );
+ };
+}
+
+// Supports deprecated method of completely overwriting logging callbacks
+function runLoggingCallbacks( key, scope, args ) {
+ var i, callbacks;
+ if ( QUnit.hasOwnProperty( key ) ) {
+ QUnit[ key ].call(scope, args );
+ } else {
+ callbacks = config[ key ];
+ for ( i = 0; i < callbacks.length; i++ ) {
+ callbacks[ i ].call( scope, args );
+ }
+ }
+}
+
+// from jquery.js
+function inArray( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+function Test( settings ) {
+ extend( this, settings );
+ this.assertions = [];
+ this.testNumber = ++Test.count;
+}
+
+Test.count = 0;
+
+Test.prototype = {
+ init: function() {
+ var a, b, li,
+ tests = id( "qunit-tests" );
+
+ if ( tests ) {
+ b = document.createElement( "strong" );
+ b.innerHTML = this.nameHtml;
+
+ // `a` initialized at top of scope
+ a = document.createElement( "a" );
+ a.innerHTML = "Rerun";
+ a.href = QUnit.url({ testNumber: this.testNumber });
+
+ li = document.createElement( "li" );
+ li.appendChild( b );
+ li.appendChild( a );
+ li.className = "running";
+ li.id = this.id = "qunit-test-output" + testId++;
+
+ tests.appendChild( li );
+ }
+ },
+ setup: function() {
+ if (
+ // Emit moduleStart when we're switching from one module to another
+ this.module !== config.previousModule ||
+ // They could be equal (both undefined) but if the previousModule property doesn't
+ // yet exist it means this is the first test in a suite that isn't wrapped in a
+ // module, in which case we'll just emit a moduleStart event for 'undefined'.
+ // Without this, reporters can get testStart before moduleStart which is a problem.
+ !hasOwn.call( config, "previousModule" )
+ ) {
+ if ( hasOwn.call( config, "previousModule" ) ) {
+ runLoggingCallbacks( "moduleDone", QUnit, {
+ name: config.previousModule,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all
+ });
+ }
+ config.previousModule = this.module;
+ config.moduleStats = { all: 0, bad: 0 };
+ runLoggingCallbacks( "moduleStart", QUnit, {
+ name: this.module
+ });
+ }
+
+ config.current = this;
+
+ this.testEnvironment = extend({
+ setup: function() {},
+ teardown: function() {}
+ }, this.moduleTestEnvironment );
+
+ this.started = +new Date();
+ runLoggingCallbacks( "testStart", QUnit, {
+ name: this.testName,
+ module: this.module
+ });
+
+ /*jshint camelcase:false */
+
+
+ /**
+ * Expose the current test environment.
+ *
+ * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
+ */
+ QUnit.current_testEnvironment = this.testEnvironment;
+
+ /*jshint camelcase:true */
+
+ if ( !config.pollution ) {
+ saveGlobal();
+ }
+ if ( config.notrycatch ) {
+ this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
+ return;
+ }
+ try {
+ this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
+ } catch( e ) {
+ QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
+ }
+ },
+ run: function() {
+ config.current = this;
+
+ var running = id( "qunit-testresult" );
+
+ if ( running ) {
+ running.innerHTML = "Running: " + this.nameHtml;
+ }
+
+ if ( this.async ) {
+ QUnit.stop();
+ }
+
+ this.callbackStarted = +new Date();
+
+ if ( config.notrycatch ) {
+ this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ return;
+ }
+
+ try {
+ this.callback.call( this.testEnvironment, QUnit.assert );
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ } catch( e ) {
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+
+ QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Restart the tests if they're blocking
+ if ( config.blocking ) {
+ QUnit.start();
+ }
+ }
+ },
+ teardown: function() {
+ config.current = this;
+ if ( config.notrycatch ) {
+ if ( typeof this.callbackRuntime === "undefined" ) {
+ this.callbackRuntime = +new Date() - this.callbackStarted;
+ }
+ this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
+ return;
+ } else {
+ try {
+ this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
+ } catch( e ) {
+ QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
+ }
+ }
+ checkPollution();
+ },
+ finish: function() {
+ config.current = this;
+ if ( config.requireExpects && this.expected === null ) {
+ QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
+ } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
+ QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
+ } else if ( this.expected === null && !this.assertions.length ) {
+ QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
+ }
+
+ var i, assertion, a, b, time, li, ol,
+ test = this,
+ good = 0,
+ bad = 0,
+ tests = id( "qunit-tests" );
+
+ this.runtime = +new Date() - this.started;
+ config.stats.all += this.assertions.length;
+ config.moduleStats.all += this.assertions.length;
+
+ if ( tests ) {
+ ol = document.createElement( "ol" );
+ ol.className = "qunit-assert-list";
+
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ assertion = this.assertions[i];
+
+ li = document.createElement( "li" );
+ li.className = assertion.result ? "pass" : "fail";
+ li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
+ ol.appendChild( li );
+
+ if ( assertion.result ) {
+ good++;
+ } else {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+
+ // store result when possible
+ if ( QUnit.config.reorder && defined.sessionStorage ) {
+ if ( bad ) {
+ sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
+ } else {
+ sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
+ }
+ }
+
+ if ( bad === 0 ) {
+ addClass( ol, "qunit-collapsed" );
+ }
+
+ // `b` initialized at top of scope
+ b = document.createElement( "strong" );
+ b.innerHTML = this.nameHtml + " (" + bad + " , " + good + " , " + this.assertions.length + ") ";
+
+ addEvent(b, "click", function() {
+ var next = b.parentNode.lastChild,
+ collapsed = hasClass( next, "qunit-collapsed" );
+ ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
+ });
+
+ addEvent(b, "dblclick", function( e ) {
+ var target = e && e.target ? e.target : window.event.srcElement;
+ if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
+ target = target.parentNode;
+ }
+ if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
+ window.location = QUnit.url({ testNumber: test.testNumber });
+ }
+ });
+
+ // `time` initialized at top of scope
+ time = document.createElement( "span" );
+ time.className = "runtime";
+ time.innerHTML = this.runtime + " ms";
+
+ // `li` initialized at top of scope
+ li = id( this.id );
+ li.className = bad ? "fail" : "pass";
+ li.removeChild( li.firstChild );
+ a = li.firstChild;
+ li.appendChild( b );
+ li.appendChild( a );
+ li.appendChild( time );
+ li.appendChild( ol );
+
+ } else {
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ if ( !this.assertions[i].result ) {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+ }
+
+ runLoggingCallbacks( "testDone", QUnit, {
+ name: this.testName,
+ module: this.module,
+ failed: bad,
+ passed: this.assertions.length - bad,
+ total: this.assertions.length,
+ runtime: this.runtime,
+ // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
+ duration: this.runtime,
+ });
+
+ QUnit.reset();
+
+ config.current = undefined;
+ },
+
+ queue: function() {
+ var bad,
+ test = this;
+
+ synchronize(function() {
+ test.init();
+ });
+ function run() {
+ // each of these can by async
+ synchronize(function() {
+ test.setup();
+ });
+ synchronize(function() {
+ test.run();
+ });
+ synchronize(function() {
+ test.teardown();
+ });
+ synchronize(function() {
+ test.finish();
+ });
+ }
+
+ // `bad` initialized at top of scope
+ // defer when previous test run passed, if storage is available
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
+
+ if ( bad ) {
+ run();
+ } else {
+ synchronize( run, true );
+ }
+ }
+};
+
+// `assert` initialized at top of scope
+// Assert helpers
+// All of these must either call QUnit.push() or manually do:
+// - runLoggingCallbacks( "log", .. );
+// - config.current.assertions.push({ .. });
+assert = QUnit.assert = {
+ /**
+ * Asserts rough true-ish result.
+ * @name ok
+ * @function
+ * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+ */
+ ok: function( result, msg ) {
+ if ( !config.current ) {
+ throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
+ }
+ result = !!result;
+ msg = msg || ( result ? "okay" : "failed" );
+
+ var source,
+ details = {
+ module: config.current.module,
+ name: config.current.testName,
+ result: result,
+ message: msg
+ };
+
+ msg = "" + escapeText( msg ) + " ";
+
+ if ( !result ) {
+ source = sourceFromStacktrace( 2 );
+ if ( source ) {
+ details.source = source;
+ msg += "Source: " +
+ escapeText( source ) +
+ "
";
+ }
+ }
+ runLoggingCallbacks( "log", QUnit, details );
+ config.current.assertions.push({
+ result: result,
+ message: msg
+ });
+ },
+
+ /**
+ * Assert that the first two arguments are equal, with an optional message.
+ * Prints out both actual and expected values.
+ * @name equal
+ * @function
+ * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
+ */
+ equal: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ QUnit.push( expected == actual, actual, expected, message );
+ },
+
+ /**
+ * @name notEqual
+ * @function
+ */
+ notEqual: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ QUnit.push( expected != actual, actual, expected, message );
+ },
+
+ /**
+ * @name propEqual
+ * @function
+ */
+ propEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name notPropEqual
+ * @function
+ */
+ notPropEqual: function( actual, expected, message ) {
+ actual = objectValues(actual);
+ expected = objectValues(expected);
+ QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name deepEqual
+ * @function
+ */
+ deepEqual: function( actual, expected, message ) {
+ QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name notDeepEqual
+ * @function
+ */
+ notDeepEqual: function( actual, expected, message ) {
+ QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
+ },
+
+ /**
+ * @name strictEqual
+ * @function
+ */
+ strictEqual: function( actual, expected, message ) {
+ QUnit.push( expected === actual, actual, expected, message );
+ },
+
+ /**
+ * @name notStrictEqual
+ * @function
+ */
+ notStrictEqual: function( actual, expected, message ) {
+ QUnit.push( expected !== actual, actual, expected, message );
+ },
+
+ "throws": function( block, expected, message ) {
+ var actual,
+ expectedOutput = expected,
+ ok = false;
+
+ // 'expected' is optional
+ if ( typeof expected === "string" ) {
+ message = expected;
+ expected = null;
+ }
+
+ config.current.ignoreGlobalErrors = true;
+ try {
+ block.call( config.current.testEnvironment );
+ } catch (e) {
+ actual = e;
+ }
+ config.current.ignoreGlobalErrors = false;
+
+ if ( actual ) {
+ // we don't want to validate thrown error
+ if ( !expected ) {
+ ok = true;
+ expectedOutput = null;
+ // expected is a regexp
+ } else if ( QUnit.objectType( expected ) === "regexp" ) {
+ ok = expected.test( errorString( actual ) );
+ // expected is a constructor
+ } else if ( actual instanceof expected ) {
+ ok = true;
+ // expected is a validation function which returns true is validation passed
+ } else if ( expected.call( {}, actual ) === true ) {
+ expectedOutput = null;
+ ok = true;
+ }
+
+ QUnit.push( ok, actual, expectedOutput, message );
+ } else {
+ QUnit.pushFailure( message, null, "No exception was thrown." );
+ }
+ }
+};
+
+/**
+ * @deprecated since 1.8.0
+ * Kept assertion helpers in root for backwards compatibility.
+ */
+extend( QUnit.constructor.prototype, assert );
+
+/**
+ * @deprecated since 1.9.0
+ * Kept to avoid TypeErrors for undefined methods.
+ */
+QUnit.constructor.prototype.raises = function() {
+ QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" );
+};
+
+/**
+ * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
+ * Kept to avoid TypeErrors for undefined methods.
+ */
+QUnit.constructor.prototype.equals = function() {
+ QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
+};
+QUnit.constructor.prototype.same = function() {
+ QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
+};
+
+// Test for equality any JavaScript type.
+// Author: Philippe Rathé
+QUnit.equiv = (function() {
+
+ // Call the o related callback with the given arguments.
+ function bindCallbacks( o, callbacks, args ) {
+ var prop = QUnit.objectType( o );
+ if ( prop ) {
+ if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
+ return callbacks[ prop ].apply( callbacks, args );
+ } else {
+ return callbacks[ prop ]; // or undefined
+ }
+ }
+ }
+
+ // the real equiv function
+ var innerEquiv,
+ // stack to decide between skip/abort functions
+ callers = [],
+ // stack to avoiding loops from circular referencing
+ parents = [],
+ parentsB = [],
+
+ getProto = Object.getPrototypeOf || function ( obj ) {
+ /*jshint camelcase:false */
+ return obj.__proto__;
+ },
+ callbacks = (function () {
+
+ // for string, boolean, number and null
+ function useStrictEquality( b, a ) {
+ /*jshint eqeqeq:false */
+ if ( b instanceof a.constructor || a instanceof b.constructor ) {
+ // to catch short annotation VS 'new' annotation of a
+ // declaration
+ // e.g. var i = 1;
+ // var j = new Number(1);
+ return a == b;
+ } else {
+ return a === b;
+ }
+ }
+
+ return {
+ "string": useStrictEquality,
+ "boolean": useStrictEquality,
+ "number": useStrictEquality,
+ "null": useStrictEquality,
+ "undefined": useStrictEquality,
+
+ "nan": function( b ) {
+ return isNaN( b );
+ },
+
+ "date": function( b, a ) {
+ return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
+ },
+
+ "regexp": function( b, a ) {
+ return QUnit.objectType( b ) === "regexp" &&
+ // the regex itself
+ a.source === b.source &&
+ // and its modifiers
+ a.global === b.global &&
+ // (gmi) ...
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline &&
+ a.sticky === b.sticky;
+ },
+
+ // - skip when the property is a method of an instance (OOP)
+ // - abort otherwise,
+ // initial === would have catch identical references anyway
+ "function": function() {
+ var caller = callers[callers.length - 1];
+ return caller !== Object && typeof caller !== "undefined";
+ },
+
+ "array": function( b, a ) {
+ var i, j, len, loop, aCircular, bCircular;
+
+ // b could be an object literal here
+ if ( QUnit.objectType( b ) !== "array" ) {
+ return false;
+ }
+
+ len = a.length;
+ if ( len !== b.length ) {
+ // safe and faster
+ return false;
+ }
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+ for ( i = 0; i < len; i++ ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[j] === a[i];
+ bCircular = parentsB[j] === b[i];
+ if ( aCircular || bCircular ) {
+ if ( a[i] === b[i] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ }
+ if ( !loop && !innerEquiv(a[i], b[i]) ) {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ parents.pop();
+ parentsB.pop();
+ return true;
+ },
+
+ "object": function( b, a ) {
+ /*jshint forin:false */
+ var i, j, loop, aCircular, bCircular,
+ // Default to true
+ eq = true,
+ aProperties = [],
+ bProperties = [];
+
+ // comparing constructors is more strict than using
+ // instanceof
+ if ( a.constructor !== b.constructor ) {
+ // Allow objects with no prototype to be equivalent to
+ // objects with Object as their constructor.
+ if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
+ ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
+ return false;
+ }
+ }
+
+ // stack constructor before traversing properties
+ callers.push( a.constructor );
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+
+ // be strict: don't ensure hasOwnProperty and go deep
+ for ( i in a ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[j] === a[i];
+ bCircular = parentsB[j] === b[i];
+ if ( aCircular || bCircular ) {
+ if ( a[i] === b[i] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ eq = false;
+ break;
+ }
+ }
+ }
+ aProperties.push(i);
+ if ( !loop && !innerEquiv(a[i], b[i]) ) {
+ eq = false;
+ break;
+ }
+ }
+
+ parents.pop();
+ parentsB.pop();
+ callers.pop(); // unstack, we are done
+
+ for ( i in b ) {
+ bProperties.push( i ); // collect b's properties
+ }
+
+ // Ensures identical properties name
+ return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
+ }
+ };
+ }());
+
+ innerEquiv = function() { // can take multiple arguments
+ var args = [].slice.apply( arguments );
+ if ( args.length < 2 ) {
+ return true; // end transition
+ }
+
+ return (function( a, b ) {
+ if ( a === b ) {
+ return true; // catch the most you can
+ } else if ( a === null || b === null || typeof a === "undefined" ||
+ typeof b === "undefined" ||
+ QUnit.objectType(a) !== QUnit.objectType(b) ) {
+ return false; // don't lose time with error prone cases
+ } else {
+ return bindCallbacks(a, callbacks, [ b, a ]);
+ }
+
+ // apply transition with (1..n) arguments
+ }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) );
+ };
+
+ return innerEquiv;
+}());
+
+/**
+ * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
+ * http://flesler.blogspot.com Licensed under BSD
+ * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
+ *
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+QUnit.jsDump = (function() {
+ function quote( str ) {
+ return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+ }
+ function literal( o ) {
+ return o + "";
+ }
+ function join( pre, arr, post ) {
+ var s = jsDump.separator(),
+ base = jsDump.indent(),
+ inner = jsDump.indent(1);
+ if ( arr.join ) {
+ arr = arr.join( "," + s + inner );
+ }
+ if ( !arr ) {
+ return pre + post;
+ }
+ return [ pre, inner + arr, base + post ].join(s);
+ }
+ function array( arr, stack ) {
+ var i = arr.length, ret = new Array(i);
+ this.up();
+ while ( i-- ) {
+ ret[i] = this.parse( arr[i] , undefined , stack);
+ }
+ this.down();
+ return join( "[", ret, "]" );
+ }
+
+ var reName = /^function (\w+)/,
+ jsDump = {
+ // type is used mostly internally, you can fix a (custom)type in advance
+ parse: function( obj, type, stack ) {
+ stack = stack || [ ];
+ var inStack, res,
+ parser = this.parsers[ type || this.typeOf(obj) ];
+
+ type = typeof parser;
+ inStack = inArray( obj, stack );
+
+ if ( inStack !== -1 ) {
+ return "recursion(" + (inStack - stack.length) + ")";
+ }
+ if ( type === "function" ) {
+ stack.push( obj );
+ res = parser.call( this, obj, stack );
+ stack.pop();
+ return res;
+ }
+ return ( type === "string" ) ? parser : this.parsers.error;
+ },
+ typeOf: function( obj ) {
+ var type;
+ if ( obj === null ) {
+ type = "null";
+ } else if ( typeof obj === "undefined" ) {
+ type = "undefined";
+ } else if ( QUnit.is( "regexp", obj) ) {
+ type = "regexp";
+ } else if ( QUnit.is( "date", obj) ) {
+ type = "date";
+ } else if ( QUnit.is( "function", obj) ) {
+ type = "function";
+ } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
+ type = "window";
+ } else if ( obj.nodeType === 9 ) {
+ type = "document";
+ } else if ( obj.nodeType ) {
+ type = "node";
+ } else if (
+ // native arrays
+ toString.call( obj ) === "[object Array]" ||
+ // NodeList objects
+ ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
+ ) {
+ type = "array";
+ } else if ( obj.constructor === Error.prototype.constructor ) {
+ type = "error";
+ } else {
+ type = typeof obj;
+ }
+ return type;
+ },
+ separator: function() {
+ return this.multiline ? this.HTML ? " " : "\n" : this.HTML ? " " : " ";
+ },
+ // extra can be a number, shortcut for increasing-calling-decreasing
+ indent: function( extra ) {
+ if ( !this.multiline ) {
+ return "";
+ }
+ var chr = this.indentChar;
+ if ( this.HTML ) {
+ chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
+ }
+ return new Array( this.depth + ( extra || 0 ) ).join(chr);
+ },
+ up: function( a ) {
+ this.depth += a || 1;
+ },
+ down: function( a ) {
+ this.depth -= a || 1;
+ },
+ setParser: function( name, parser ) {
+ this.parsers[name] = parser;
+ },
+ // The next 3 are exposed so you can use them
+ quote: quote,
+ literal: literal,
+ join: join,
+ //
+ depth: 1,
+ // This is the list of parsers, to modify them, use jsDump.setParser
+ parsers: {
+ window: "[Window]",
+ document: "[Document]",
+ error: function(error) {
+ return "Error(\"" + error.message + "\")";
+ },
+ unknown: "[Unknown]",
+ "null": "null",
+ "undefined": "undefined",
+ "function": function( fn ) {
+ var ret = "function",
+ // functions never have name in IE
+ name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
+
+ if ( name ) {
+ ret += " " + name;
+ }
+ ret += "( ";
+
+ ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
+ return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
+ },
+ array: array,
+ nodelist: array,
+ "arguments": array,
+ object: function( map, stack ) {
+ /*jshint forin:false */
+ var ret = [ ], keys, key, val, i;
+ QUnit.jsDump.up();
+ keys = [];
+ for ( key in map ) {
+ keys.push( key );
+ }
+ keys.sort();
+ for ( i = 0; i < keys.length; i++ ) {
+ key = keys[ i ];
+ val = map[ key ];
+ ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
+ }
+ QUnit.jsDump.down();
+ return join( "{", ret, "}" );
+ },
+ node: function( node ) {
+ var len, i, val,
+ open = QUnit.jsDump.HTML ? "<" : "<",
+ close = QUnit.jsDump.HTML ? ">" : ">",
+ tag = node.nodeName.toLowerCase(),
+ ret = open + tag,
+ attrs = node.attributes;
+
+ if ( attrs ) {
+ for ( i = 0, len = attrs.length; i < len; i++ ) {
+ val = attrs[i].nodeValue;
+ // IE6 includes all attributes in .attributes, even ones not explicitly set.
+ // Those have values like undefined, null, 0, false, "" or "inherit".
+ if ( val && val !== "inherit" ) {
+ ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
+ }
+ }
+ }
+ ret += close;
+
+ // Show content of TextNode or CDATASection
+ if ( node.nodeType === 3 || node.nodeType === 4 ) {
+ ret += node.nodeValue;
+ }
+
+ return ret + open + "/" + tag + close;
+ },
+ // function calls it internally, it's the arguments part of the function
+ functionArgs: function( fn ) {
+ var args,
+ l = fn.length;
+
+ if ( !l ) {
+ return "";
+ }
+
+ args = new Array(l);
+ while ( l-- ) {
+ // 97 is 'a'
+ args[l] = String.fromCharCode(97+l);
+ }
+ return " " + args.join( ", " ) + " ";
+ },
+ // object calls it internally, the key part of an item in a map
+ key: quote,
+ // function calls it internally, it's the content of the function
+ functionCode: "[code]",
+ // node calls it internally, it's an html attribute value
+ attribute: quote,
+ string: quote,
+ date: quote,
+ regexp: literal,
+ number: literal,
+ "boolean": literal
+ },
+ // if true, entities are escaped ( <, >, \t, space and \n )
+ HTML: false,
+ // indentation unit
+ indentChar: " ",
+ // if true, items in a collection, are separated by a \n, else just a space.
+ multiline: true
+ };
+
+ return jsDump;
+}());
+
+/*
+ * Javascript Diff Algorithm
+ * By John Resig (http://ejohn.org/)
+ * Modified by Chu Alan "sprite"
+ *
+ * Released under the MIT license.
+ *
+ * More Info:
+ * http://ejohn.org/projects/javascript-diff-algorithm/
+ *
+ * Usage: QUnit.diff(expected, actual)
+ *
+ * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over"
+ */
+QUnit.diff = (function() {
+ /*jshint eqeqeq:false, eqnull:true */
+ function diff( o, n ) {
+ var i,
+ ns = {},
+ os = {};
+
+ for ( i = 0; i < n.length; i++ ) {
+ if ( !hasOwn.call( ns, n[i] ) ) {
+ ns[ n[i] ] = {
+ rows: [],
+ o: null
+ };
+ }
+ ns[ n[i] ].rows.push( i );
+ }
+
+ for ( i = 0; i < o.length; i++ ) {
+ if ( !hasOwn.call( os, o[i] ) ) {
+ os[ o[i] ] = {
+ rows: [],
+ n: null
+ };
+ }
+ os[ o[i] ].rows.push( i );
+ }
+
+ for ( i in ns ) {
+ if ( hasOwn.call( ns, i ) ) {
+ if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
+ n[ ns[i].rows[0] ] = {
+ text: n[ ns[i].rows[0] ],
+ row: os[i].rows[0]
+ };
+ o[ os[i].rows[0] ] = {
+ text: o[ os[i].rows[0] ],
+ row: ns[i].rows[0]
+ };
+ }
+ }
+ }
+
+ for ( i = 0; i < n.length - 1; i++ ) {
+ if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
+ n[ i + 1 ] == o[ n[i].row + 1 ] ) {
+
+ n[ i + 1 ] = {
+ text: n[ i + 1 ],
+ row: n[i].row + 1
+ };
+ o[ n[i].row + 1 ] = {
+ text: o[ n[i].row + 1 ],
+ row: i + 1
+ };
+ }
+ }
+
+ for ( i = n.length - 1; i > 0; i-- ) {
+ if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
+ n[ i - 1 ] == o[ n[i].row - 1 ]) {
+
+ n[ i - 1 ] = {
+ text: n[ i - 1 ],
+ row: n[i].row - 1
+ };
+ o[ n[i].row - 1 ] = {
+ text: o[ n[i].row - 1 ],
+ row: i - 1
+ };
+ }
+ }
+
+ return {
+ o: o,
+ n: n
+ };
+ }
+
+ return function( o, n ) {
+ o = o.replace( /\s+$/, "" );
+ n = n.replace( /\s+$/, "" );
+
+ var i, pre,
+ str = "",
+ out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
+ oSpace = o.match(/\s+/g),
+ nSpace = n.match(/\s+/g);
+
+ if ( oSpace == null ) {
+ oSpace = [ " " ];
+ }
+ else {
+ oSpace.push( " " );
+ }
+
+ if ( nSpace == null ) {
+ nSpace = [ " " ];
+ }
+ else {
+ nSpace.push( " " );
+ }
+
+ if ( out.n.length === 0 ) {
+ for ( i = 0; i < out.o.length; i++ ) {
+ str += "" + out.o[i] + oSpace[i] + "";
+ }
+ }
+ else {
+ if ( out.n[0].text == null ) {
+ for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
+ str += "" + out.o[n] + oSpace[n] + "";
+ }
+ }
+
+ for ( i = 0; i < out.n.length; i++ ) {
+ if (out.n[i].text == null) {
+ str += "" + out.n[i] + nSpace[i] + " ";
+ }
+ else {
+ // `pre` initialized at top of scope
+ pre = "";
+
+ for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
+ pre += "" + out.o[n] + oSpace[n] + "";
+ }
+ str += " " + out.n[i].text + nSpace[i] + pre;
+ }
+ }
+ }
+
+ return str;
+ };
+}());
+
+// For browser, export only select globals
+if ( typeof window !== "undefined" ) {
+ extend( window, QUnit.constructor.prototype );
+ window.QUnit = QUnit;
+}
+
+// For CommonJS environments, export everything
+if ( typeof module !== "undefined" && module.exports ) {
+ module.exports = QUnit;
+}
+
+
+// Get a reference to the global object, like window in browsers
+}( (function() {
+ return this;
+})() ));
diff --git a/tests/tests.js b/tests/tests.js
new file mode 100644
index 000000000..dfc5fcb0e
--- /dev/null
+++ b/tests/tests.js
@@ -0,0 +1,62 @@
+var fixture = document.getElementById('qunit-fixture');
+var editorElement = document.createElement('div');
+editorElement.id = 'editor1';
+editorElement.className = 'editor';
+
+QUnit.module('Editor', {
+ setup: function() {
+ fixture.appendChild(editorElement);
+ },
+ teardown: function() {
+ fixture.removeChild(editorElement);
+ }
+});
+
+test('can create an editor', function() {
+ var editor = new ContentKit.Editor();
+ ok(editor);
+ ok(editor instanceof ContentKit.Editor);
+});
+
+test('can create an editor via dom node reference', function() {
+ var editor = new ContentKit.Editor(editorElement);
+ equal(editor.element, editorElement);
+});
+
+test('can create an editor via dom node reference from getElementById', function() {
+ var editor = new ContentKit.Editor(document.getElementById('editor1'));
+ equal(editor.element, editorElement);
+});
+
+test('can create an editor via id selector', function() {
+ var editor = new ContentKit.Editor('#editor1');
+ equal(editor.element, editorElement);
+});
+
+test('can create an editor via class selector', function() {
+ var editor = new ContentKit.Editor('.editor');
+ equal(editor.element, editorElement);
+});
+
+test('can recreate an editor on the same element', function() {
+ var editor = new ContentKit.Editor('#editor1');
+ ok(editor.element === editorElement);
+
+ editor = new ContentKit.Editor('.editor');
+ equal(editor.element, editorElement);
+ equal(editor.element.className, 'editor ck-editor');
+});
+
+test('creating an editor doesn\'t trash existing class names', function() {
+ editorElement.className = 'some-class';
+
+ var editor = new ContentKit.Editor('.some-class');
+ equal(editor.element.className, 'some-class ck-editor');
+});
+
+test('creating an editor without a class name adds appropriate class', function() {
+ editorElement.className = '';
+
+ var editor = new ContentKit.Editor(document.getElementById('editor1'));
+ equal(editor.element.className, 'ck-editor');
+});