diff --git a/Gruntfile.js b/Gruntfile.js index 2f3a7d2..90697d9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -90,7 +90,7 @@ module.exports = function(grunt) { } }, all: ["tests/all.html"], - single: ["tests/index.html"] + single: ["tests/single.html"] }, copy: { qunit: { diff --git a/src/editor/core/Arte.js b/src/editor/core/Arte.js index b5aac5d..edac782 100644 --- a/src/editor/core/Arte.js +++ b/src/editor/core/Arte.js @@ -11,9 +11,12 @@ (function($) { $.Arte = $.Arte || {}; $.fn.Arte = function(options, args) { - var result = []; rangy.init(); - this.each(function() { + + if (!this.length) { + return this; + } + return this.map(function() { var $this = $(this); var editor = $this.data("Arte"); if (options && typeof(options) === "string") { @@ -28,7 +31,7 @@ } var returnValue = editor[methodName].call(editor, args); - result.push(returnValue); + return returnValue; } else { // If $this is not a rich text editor, construct the editor if (!editor) { @@ -37,9 +40,8 @@ editor = new $.Arte.TextArea(options); $this.data("Arte", editor); } - result.push(editor); + return editor; } }); - return $(result); }; })(jQuery); diff --git a/src/new/arte-commands.js b/src/new/arte-commands.js new file mode 100644 index 0000000..252e948 --- /dev/null +++ b/src/new/arte-commands.js @@ -0,0 +1,167 @@ +jQuery(function($) { + var commands = { + bold: function() { + var weight = this.element.css("fontWeight"); + + if (weight === "bold" || weight > 500) { + this.element.css("fontWeight", 400); + } else { + this.element.css("fontWeight", 700); + } + }, + italic: function() { + var style = this.element.css("fontStyle"); + + if (style === "italic") { + this.element.css("fontStyle", "normal"); + } else { + this.element.css("fontStyle", "italic"); + } + }, + underline: function() { + var decoration = this.element.css("textDecoration"); + + if (decoration === "underline") { + this.element.css("textDecoration", "none"); + } else { + this.element.css("textDecoration", "underline"); + } + }, + fontSize: function(value) { + if (value) { + return this.element.css("fontSize", value); + } else { + return this.element.css("fontSize"); + } + }, + fontFamily: function(value) { + if (value) { + return this.element.css("fontFamily", value); + } else { + return this.element.css("fontFamily"); + } + }, + color: function(value) { + if (value) { + return this.element.css("color", value); + } else { + return this.element.css("color"); + } + }, + backgroundColor: function(value) { + if (value) { + return this.element.css("backgroundColor", value); + } else { + return this.element.css("backgroundColor"); + } + }, + textAlign: function(value) { + if (value) { + return this.element.css("textAlign", value); + } else { + return this.element.css("textAlign"); + } + }, + unorderedList: function() { + if (this.__type === "plainText") { + throw new Error("Can't create lists on plain text editors"); + } + }, + orderedList: function() { + if (this.__type === "plainText") { + throw new Error("Can't create lists on plain text editors"); + } + }, + blockquote: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h1: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h2: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h3: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h4: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h5: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + h6: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + subscript: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + }, + superscript: function() { + if (this.__type === "plainText") { + throw new Error("Can't create elements on plain text editor"); + } + } + }; + + $.each([ + "bold", "italic", "underline", + "fontSize", "fontFamily", + "color", "backgroundColor", + "unorderedList", "orderedList", + "textAlign" + ], function() { + var command = this; + Arte.prototype[command] = function(options) { + return this.exec(command, options); + }; + }); + + $.each([ + "blockquote", + "h1", "h2", "h3", "h4", "h5", "h6", + "subscript", "superscript", + ], function() { + var command = this; + Arte.prototype[command] = function() { + return this.exec(command); + }; + }); + + $.extend(Arte.prototype, { + exec: function(command, options) { + var result; + + this.triggerEvent({ + type: "arte-beforecommand", + options: options, + command: command + }); + + result = commands[command].call(this, options); + + this.triggerEvent({ + type: "arte-command", + options: options, + command: command + }); + + return result; + } + }); +}); diff --git a/src/new/arte-configuration.js b/src/new/arte-configuration.js new file mode 100644 index 0000000..e69de29 diff --git a/src/new/arte-core.js b/src/new/arte-core.js new file mode 100644 index 0000000..5c2eab2 --- /dev/null +++ b/src/new/arte-core.js @@ -0,0 +1,156 @@ +/** + * @fileoverview Rich text editor + * Usage: + * 1) var arte = Arte(element); + * Converts the first matched element into a rich text editor using default options and returns an existing instance + * @returns {an Arte object} + * 2) var arte = Arte(element, { options }); + * Converts the first matched elements into a rich text editor using the options supplied and returns an existing instance + * @returns {an Arte object} + * + */ + +jQuery(function($) { + function Arte(element, options) { + if (!(this instanceof Arte)) { + return new Arte(element, options); + } + + this.container = $(element).first(); + this.options = $.extend({ + editorType: "plainText", + styles: { + "min-height": "200px", + "height": "inherit" + }, + classes: [], + value: "Please enter text ..." + // TODO: set all default values here + }, options); + + if (!this.container.length) { + throw "Arte requires a DOM element"; + } + + // NEW: replace element reference to the new made element + // after it's initialized + this.element = this.__initialize(); + + this.__initEvents(); + + this.__type = this.options.editorType; + + return this; + } + + // Export Arte + window.Arte = Arte; + + Arte.prototype = { + __initialize: function() { + var newElement; + + if (this.options.editorType === "richText") { + newElement = $("
", { + class: "arte-richtext", + contenteditable: true + }) + .html(this.options.value); + } else { + newElement = $("", { + class: "arte-plaintext", + style: { + height: "100%", + width: "100%", + padding: 0, + border: 0 + } + }) + .val(this.options.value); + } + + newElement + // Apply default style + .css(this.options.styles) + // add custom classes + .addClass(this.options.classes.join(" ")) + // insert newElement to the element container + .appendTo(this.container); + + return newElement; + }, + + /* Methods */ + + /** + * Get or Set content of the element + * @params {string} value string to set content of element + * @returns {string} returns 'innerHTML' of the contentEditable element + * if in rich text mode or 'value' of the element if in plaintext mode + */ + value: function() { + if (this.__type === "plainText") { + return $.fn.val.apply(this.element, arguments); + } else { + return $.fn.html.apply(this.element, arguments); + } + }, + + /** + * Gets outerHtml of the element + * @returns {string} returns 'outerHTML' of the element + */ + outerValue: function() { + return this.element.get(0).outerHTML; + }, + + /** + * Calls a focus event on the contentEditable element, moves the cursor + * to the end of that element, and fires an onselectionchange event + */ + focus: function() { + var elem = this.element[0]; + var range; + var sel; + + this.element.trigger("focus"); + + // Moves the cursor to the end of the element + if (elem.selectionStart >= 0) { + // Works only on textarea elements + elem.selectionStart = elem.selectionEnd = elem.value.length; + } else if (typeof elem.createTextRange !== "undefined") { + // Fallback for textarea elements without support to selectionStart + range = elem.createTextRange(); + range.collapse(false); + range.select(); + } else if (this.__type === "richText" && document.createRange && window.getSelection) { + // Works only on contenteditable + range = document.createRange(); + sel = window.getSelection(); + + while (elem.lastChild) { + elem = elem.lastChild; + } + range.setStart(elem, elem.nodeValue.length); + range.collapse(true); + + sel.removeAllRanges(); + sel.addRange(range); + } + + return this; + }, + + destroy: function(options) { + this.triggerEvent("arte-destroy"); + + if (options && options.removeContent) { + this.element.empty(); + } else { + this.element.remove(); + } + } + }; + +}); diff --git a/src/new/arte-events.js b/src/new/arte-events.js new file mode 100644 index 0000000..4decd5d --- /dev/null +++ b/src/new/arte-events.js @@ -0,0 +1,58 @@ +jQuery(function($) { + $.extend(Arte.prototype, { + /** + * Triggers the event passed in on the contentEditable element with data provided + * @params {string} name - name of the event you want to trigger + * @params {object} data - extra parameters to pass into the event handler + */ + triggerEvent: function() { + $.fn.trigger.apply(this.element, arguments); + return this; + }, + + on: function() { + $.fn.on.apply(this.element, arguments); + }, + + off: function() { + $.fn.off.apply(this.element, arguments); + }, + + __initEvents: function() { + var arte = this; + + this.on("keydown keyup keypress focus", function(ev) { + arte.triggerEvent({ + type: "arte-" + ev.type, + originalEvent: ev + }); + ev.stopPropagation(); + }); + + this.on("blur", function(ev) { + arte.triggerEvent("arte-selectionchange"); + arte.triggerEvent({ + type: "arte-blur", + originalEvent: ev + }); + + ev.stopPropagation(); + }); + + this.on("mouseup", function(ev) { + arte.triggerEvent("arte-selectionchange"); + arte.triggerEvent({ + type: "arte-mouseup", + originalEvent: ev + }); + }); + + arte.on("mousedown click paste", function(ev) { + arte.triggerEvent({ + type: "arte-" + ev.type, + originalEvent: ev + }); + }); + } + }); +}); diff --git a/tests/all.html b/tests/all.html index a29f9b3..4fd1e7d 100644 --- a/tests/all.html +++ b/tests/all.html @@ -12,9 +12,12 @@ diff --git a/tests/new/commands-plaintext.js b/tests/new/commands-plaintext.js new file mode 100644 index 0000000..62e58ad --- /dev/null +++ b/tests/new/commands-plaintext.js @@ -0,0 +1,209 @@ +QUnit.module("commands - plaintext", { + beforeEach: function() { + this.element = $("#test-element"); + this.arte = new Arte(this.element); + } +}); + +QUnit.test("bold", function(t) { + var arte = this.arte; + + arte.element.css("fontWeight", 400); + + arte.bold(); + + // Crossbrowser support + var validBold = [ + "bold", "700" + ]; + t.ok(validBold.indexOf(arte.element.css("fontWeight")) >= 0); + + arte.bold(); + t.equal(arte.element.css("fontWeight"), "400"); +}); + +QUnit.test("italic", function(t) { + var arte = this.arte; + + arte.element.css("fontStyle", "normal"); + + arte.italic(); + t.strictEqual(arte.element.css("fontStyle"), "italic"); + + arte.italic(); + t.strictEqual(arte.element.css("fontStyle"), "normal"); +}); + +QUnit.test("underline", function(t) { + var arte = this.arte; + + arte.element.css("textDecoration", "none"); + + arte.underline(); + t.strictEqual(arte.element.css("textDecoration"), "underline"); + + arte.underline(); + t.strictEqual(arte.element.css("textDecoration"), "none"); +}); + +QUnit.test("fontSize", function(t) { + var arte = this.arte; + + arte.element.css("fontSize", "10px"); + + t.equal(arte.fontSize(), "10px"); + + arte.fontSize(20); + t.equal(arte.element.css("fontSize"), "20px"); + + arte.fontSize(15); + t.equal(arte.element.css("fontSize"), "15px"); + + t.equal(arte.fontSize(), "15px"); +}); + +QUnit.test("fontFamily", function(t) { + var arte = this.arte; + + arte.element.css("fontFamily", "Verdana"); + + t.strictEqual(arte.fontFamily(), "Verdana"); + + arte.fontFamily("Arial"); + t.strictEqual(arte.element.css("fontFamily"), "Arial"); +}); + +QUnit.test("color", function(t) { + var arte = this.arte; + + arte.element.css("color", "#000"); + t.strictEqual(arte.color(), "rgb(0, 0, 0)"); + + arte.color("#fff"); + t.strictEqual(arte.element.css("color"), "rgb(255, 255, 255)"); +}); + +QUnit.test("backgroundColor", function(t) { + var arte = this.arte; + + arte.element.css("backgroundColor", "black"); + t.strictEqual(arte.backgroundColor(), "rgb(0, 0, 0)"); + + arte.backgroundColor("#fff"); + t.strictEqual(arte.element.css("backgroundColor"), "rgb(255, 255, 255)"); +}); + +QUnit.test("unorderedList", function(t) { + var arte = this.arte; + + t.throws( + function() { + arte.unorderedList(); + }, + /Can't create lists on plain text editors/, + "Throws and error after trying to create lists on plaintext editors" + ); +}); + +QUnit.test("orderedList", function(t) { + var arte = this.arte; + + t.throws( + function() { + arte.orderedList(); + }, + /Can't create lists on plain text editors/, + "Throws and error after trying to create lists on plaintext editors" + ); +}); + +QUnit.test("textAlign", function(t) { + var arte = this.arte; + + arte.element.css("textAlign", "center"); + t.strictEqual(arte.textAlign(), "center"); + + arte.textAlign("left"); + t.strictEqual(arte.element.css("textAlign"), "left"); +}); + +QUnit.test("blockquote", function(t) { + t.throws( + function() { + this.arte.blockquote(); + }, + "arte.blockquote() throws an error on plain text editors" + ); +}); + +QUnit.test("h1", function(t) { + t.throws( + function() { + this.arte.h1(); + }, + "arte.h1() throws an error on plain text editors" + ); +}); + +QUnit.test("h2", function(t) { + t.throws( + function() { + this.arte.h2(); + }, + "arte.h2() throws an error on plain text editors" + ); +}); + +QUnit.test("h3", function(t) { + t.throws( + function() { + this.arte.h3(); + }, + "arte.h3() throws an error on plain text editors" + ); +}); + +QUnit.test("h4", function(t) { + t.throws( + function() { + this.arte.h4(); + }, + "arte.h4() throws an error on plain text editors" + ); +}); + +QUnit.test("h5", function(t) { + t.throws( + function() { + this.arte.h5(); + }, + "arte.h5() throws an error on plain text editors" + ); +}); + +QUnit.test("h6", function(t) { + t.throws( + function() { + this.arte.h6(); + }, + "arte.h6() throws an error on plain text editors" + ); +}); + +QUnit.test("superscript", function(t) { + t.throws( + function() { + this.arte.superscript(); + }, + "arte.superscript() throws an error on plain text editors" + ); +}); + +QUnit.test("subscript", function(t) { + t.throws( + function() { + this.arte.subscript(); + }, + "arte.subscript() throws an error on plain text editors" + ); +}); diff --git a/tests/new/commands-richtext.js b/tests/new/commands-richtext.js new file mode 100644 index 0000000..9ee115b --- /dev/null +++ b/tests/new/commands-richtext.js @@ -0,0 +1,149 @@ +QUnit.module("commands - richText", { + beforeEach: function() { + this.element = $("#test-element"); + this.arte = new Arte(this.element, { + editorType: "richText" + }); + } +}); + +// Commands that return tags +var tagCommands = { + bold: "b", + italic: "i", + underline: "u", + blockquote: "blockquote", + h1: "h1", + h2: "h2", + h3: "h3", + h4: "h4", + h5: "h5", + h6: "h6" + }; + +var spanCommands = { + fontSize: ["10px", "20px"], + fontFamily: ["Verdana", "monospace"], + color: ["rgb(0, 0, 0)", "rgb(255, 255, 255)"], + backgroundColor: ["rgb(255, 255, 255)", "rgb(0, 0, 0)"], + textAlign: ["center", "right"] + }; + +$.each(tagCommands, function(command, tag) { + QUnit.test(command + " - no selection", function(t) { + var arte = this.arte; + + t.expect(3); + + arte.value("foo"); + + // TODO: set selection cursor on the last character. + arte[command](); + t.strictEqual( + arte.value(), + "foo<" + tag + ">" + tag + ">", + "creates " + command + " tags" + ); + + // TODO: assert cursor is insidefoo
", + editorType: "richText" + }); + + // catchesand
foo
foo
foo
foo