// (c) 2011 Michael Aufreiter // Proper is freely distributable under the MIT license. // For all details and documentation: // http://github.com/michael/proper (function(){ // _.Events (borrowed from Backbone.js) // ----------------- // A module that can be mixed in to *any object* in order to provide it with // custom events. You may `bind` or `unbind` a callback function to an event; // `trigger`-ing an event fires all callbacks in succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // _.Events = window.Backbone ? Backbone.Events : { // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. bind : function(ev, callback) { var calls = this._callbacks || (this._callbacks = {}); var list = this._callbacks[ev] || (this._callbacks[ev] = []); list.push(callback); return this; }, // Remove one or many callbacks. If `callback` is null, removes all // callbacks for the event. If `ev` is null, removes all bound callbacks // for all events. unbind : function(ev, callback) { var calls; if (!ev) { this._callbacks = {}; } else if (calls = this._callbacks) { if (!callback) { calls[ev] = []; } else { var list = calls[ev]; if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (callback === list[i]) { list.splice(i, 1); break; } } } } return this; }, // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { var list, calls, i, l; if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, Array.prototype.slice.call(arguments, 1)); } } if (list = calls['all']) { for (i = 0, l = list.length; i < l; i++) { list[i].apply(this, arguments); } } return this; } }; _.stripTags = function(input, allowed) { // Strips HTML and PHP tags from a string // // version: 1009.2513 // discuss at: http://phpjs.org/functions/strip_tags allowed = (((allowed || "") + "") .toLowerCase() .match(/<[a-z][a-z0-9]*>/g) || []) .join(''); // making sure the allowed arg is a string containing only tags in lowercase () var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; return input.replace(commentsAndPhpTags, '').replace(tags, function($0, $1){ return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''; }); }; // Initial Setup // ------------- controlsTpl = ' \
\
Emphasis
\
Strong
\ \
Link
\
Bullets List
\
Numbered List
\
Indent
\
Outdent
\
\
'; var COMMANDS = { "em": "italic", "strong": "bold", "ul": "insertUnorderedList", "ol": "insertOrderedList" // "link": "createLink" // for some reason Firefox can't work with that } // Proper // ----------- this.Proper = function(options) { var activeElement = null, // element that's being edited $controls, self = {}, that = this, pendingChange = false, options = {}, defaultOptions = { // default options multiline: true, markup: true, placeholder: 'Enter Text' }; // Setup temporary hidden DOM Node, for sanitization $('body').append($('
').hide()); var rawContent = $('
'); rawContent.css('position', 'fixed'); rawContent.css('top', '20px'); rawContent.css('left', '20px'); rawContent.css('opacity', '0'); $('body').append(rawContent); // Commands // ----------- function tagActive(element) { var sel = window.getSelection(); var range = sel.getRangeAt(0); return range.startContainer.parentNode.localName === element || range.endContainer.parentNode.localName === element; } // A proper implementation of execCommand function toggleTag(tag) { var sel = window.getSelection(); var range = sel.getRangeAt(0); if (sel+"".length == 0) return; if (tagActive(tag)) { document.execCommand('removeFormat', false, true); } else { var sel = window.getSelection(); var range = sel.getRangeAt(0); document.execCommand('removeFormat', false, true); document.execCommand('insertHTML', false, '<'+tag+'>'+window.getSelection()+''); } } var commands = { execEM: function() { if (!document.queryCommandState('italic', false, true)) document.execCommand('removeFormat', false, true); document.execCommand('italic', false, true); return false; }, execSTRONG: function() { if (!document.queryCommandState('bold', false, true)) document.execCommand('removeFormat', false, true); document.execCommand('bold', false, true); return false; }, execCODE: function() { if (!tagActive('code')) document.execCommand('removeFormat', false, true); toggleTag('code'); return false; }, execUL: function() { document.execCommand('insertUnorderedList', false, true); return false; }, execOL: function() { document.execCommand('insertOrderedList', false, true); return false; }, execINDENT: function() { if (document.queryCommandState('insertOrderedList', false, true) || document.queryCommandState('insertUnorderedList', false, true)) { document.execCommand('indent', false, true); } return false; }, execOUTDENT: function() { if (document.queryCommandState('insertOrderedList', false, true) || document.queryCommandState('insertUnorderedList', false, true)) { document.execCommand('outdent', false, true); } return false; }, execLINK: function() { document.execCommand('createLink', false, prompt('URL:')); return false; }, showHTML: function() { alert($(this.el).html()); } }; // TODO: enable proper sanitizing that allows markup to be pasted too function sanitize() { var rawContent = document.getElementById('proper_raw_content'); $('#proper_content').html($(rawContent).text()); } function updateCommandState() { if (!options.markup) return; $(activeElement).focus(); $controls.find('.command').removeClass('selected'); _.each(COMMANDS, function(command, key) { if (document.queryCommandState(command, false, true)) { $controls.find('.command.'+key).addClass('selected'); } if (tagActive('code')) { $controls.find('.command.code').addClass('selected'); } }); } // Used for placeholders function checkEmpty() { if ($(activeElement).text().trim().length === 0) { $(activeElement).addClass('empty'); if (options.markup) { $(activeElement).html('

« '+options.placeholder+' »

'); } else { $(activeElement).html('« '+options.placeholder+' »'); } } } // Clean up the mess produced by contenteditable function semantify(html) { return html.replace(//g, '') .replace(/<\/i>/g, '') .replace(//g, '') .replace(/<\/b>/g, ''); } function saveSelection() { if (window.getSelection) { sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } } else if (document.selection && document.selection.createRange) { return document.selection.createRange(); } return null; } function restoreSelection(range) { if (range) { if (window.getSelection) { sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } else if (document.selection && range.select) { range.select(); } } } function selectAll() { range = document.createRange(); range.selectNodeContents($(activeElement)[0]); selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } function bindEvents(el) { $(el).unbind('paste'); $(el).unbind('keydown'); $(el).unbind('keyup'); $(el).bind('paste', function() { var selection = saveSelection(); $('#proper_raw_content').focus(); // Immediately sanitize pasted content setTimeout(function() { sanitize(); restoreSelection(selection); $(el).focus(); // Avoid nested paragraph correction resulting from paste var content = $('#proper_content').html().trim(); // For some reason last

gets injected anyway document.execCommand('insertHTML', false, content); $('#proper_raw_content').html(''); }, 1); }); // Prevent multiline $(el).bind('keydown', function(e) { if (!options.multiline && e.keyCode === 13) { e.stopPropagation(); return false; } if (e.keyCode == 8 && $(activeElement).text().trim().length == 0) { e.stopPropagation(); return false; } }); $(el).bind('blur', checkEmpty); $(el).bind('click', updateCommandState); $(el).bind('keyup', function(e) { updateCommandState(); if ($(activeElement).text().trim().length > 0) { $(activeElement).removeClass('empty'); } else { // TODO: problematic when hitting enter on an empty div selectAll(); document.execCommand('delete', false, ""); $(activeElement).addClass('empty'); } // Trigger change events, but consolidate them to 200ms time slices setTimeout(function() { // Skip if there's already a change pending if (!pendingChange) { pendingChange = true; setTimeout(function() { pendingChange = false; self.trigger('changed'); }, 200); } }, 10); return true; }); } // Instance methods // ----------- self.deactivate = function() { $(activeElement).attr('contenteditable', 'false'); $(activeElement).unbind('paste'); $(activeElement).unbind('keydown'); $('.proper-commands').remove(); self.unbind('changed'); }; // Activate editor for a given element self.activate = function(el, opts) { options = {}; _.extend(options, defaultOptions, opts); // Deactivate previously active element self.deactivate(); // Make editable $(el).attr('contenteditable', true); activeElement = el; bindEvents(el); // Setup controls if (options.markup) { $controls = $(controlsTpl); $controls.appendTo($(options.controlsTarget)); } // Keyboard bindings if (options.markup) { $(activeElement).bind('keydown', 'ctrl+shift+e', commands.execEM); $(activeElement).bind('keydown', 'ctrl+shift+s', commands.execSTRONG); $(activeElement).bind('keydown', 'ctrl+shift+c', commands.execCODE); $(activeElement).bind('keydown', 'ctrl+shift+l', commands.execLINK); $(activeElement).bind('keydown', 'ctrl+shift+b', commands.execUL); $(activeElement).bind('keydown', 'ctrl+shift+n', commands.execOL); $(activeElement).bind('keydown', 'tab', commands.execINDENT); $(activeElement).bind('keydown', 'shift+tab', commands.execOUTDENT); } updateCommandState(); if (el.hasClass('empty')) { selectAll(); document.execCommand('delete', false, ""); } $('.proper-commands a.command').click(function(e) { commands['exec'+ $(e.currentTarget).attr('command').toUpperCase()](); updateCommandState(); setTimeout(function() { self.trigger('changed'); }, 10); return false; }); }; // Get current content self.content = function() { if ($(activeElement).hasClass('empty')) return ''; if (options.markup) { return activeElement ? semantify($(activeElement).html()).trim() : ''; } else { if (options.multiline) { return _.stripTags($(activeElement).html().replace(/
/g, '\n') .replace(/<\/div>/g, '')).trim(); } else { return _.stripTags($(activeElement).html()).trim(); } } }; // Expose public API // ----------- _.extend(self, _.Events); return self; }; })(); /* * jQuery Hotkeys Plugin * Copyright 2010, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * * Based upon the plugin by Tzury Bar Yochay: * http://github.com/tzuryby/hotkeys * * Original idea by: * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ */ (function(jQuery){ jQuery.hotkeys = { version: "0.8", specialKeys: { 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" }, shiftNums: { "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", ".": ">", "/": "?", "\\": "|" } }; function keyHandler( handleObj ) { // Only care when a possible input has been specified if ( typeof handleObj.data !== "string" ) { return; } var origHandler = handleObj.handler, keys = handleObj.data.toLowerCase().split(" "); handleObj.handler = function( event ) { // Don't fire in text-accepting inputs that we didn't directly bind to if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || event.target.type === "text") ) { return; } // Keypress represents characters, not special keys var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], character = String.fromCharCode( event.which ).toLowerCase(), key, modif = "", possible = {}; // check combinations (alt|ctrl|shift+anything) if ( event.altKey && special !== "alt" ) { modif += "alt+"; } if ( event.ctrlKey && special !== "ctrl" ) { modif += "ctrl+"; } // TODO: Need to make sure this works consistently across platforms if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { modif += "meta+"; } if ( event.shiftKey && special !== "shift" ) { modif += "shift+"; } if ( special ) { possible[ modif + special ] = true; } else { possible[ modif + character ] = true; possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" if ( modif === "shift+" ) { possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; } } for ( var i = 0, l = keys.length; i < l; i++ ) { if ( possible[ keys[i] ] ) { return origHandler.apply( this, arguments ); } } }; } jQuery.each([ "keydown", "keyup", "keypress" ], function() { jQuery.event.special[ this ] = { add: keyHandler }; }); })( jQuery );