diff --git a/bootstrap-wysiwyg.js b/bootstrap-wysiwyg.js index 69f64a7..c554441 100644 --- a/bootstrap-wysiwyg.js +++ b/bootstrap-wysiwyg.js @@ -23,39 +23,193 @@ selectedRange, options, toolbarBtnSelector, - updateToolbar = function () { + commandCache = {}, + updateCommandCache = function () { + var key; + for (key in commandCache) { + var commandValue = document.queryCommandValue(key); + var commandState = document.queryCommandState(key); + + if (commandState) { + commandCache[key] = commandState; + } else if (commandValue.length > 0 && commandValue !== 'false') { + commandCache[key] = commandValue; + } else { + commandCache[key] = false; + } + } + }, + restoreCommandCache = function() { + var key; + for (key in commandCache) { + var val = commandCache[key]; + if (typeof(val) === 'boolean') { + if (val !== document.queryCommandState(key)) { + document.execCommand(key, 0, null); + } + } else if (val !== document.queryCommandValue(key)) { + document.execCommand(key, 0, val); + } + } + }, + namespaceEvents = function(events) { + if (events.toString() != '[object Array]') { + events = events.split(' '); + } + + events.push(''); // Add empty element for easy join() support. + + if (options.eventNamespace != null && options.eventNamespace.length > 0) + { + return events.join('.' + options.eventNamespace + ' '); + } else { + return events.join(' '); + } + + }, + throttle = function(handler, threshold, scope) { + var last, deferTimer; + return function() { + var context = scope || this; + var now = new Date().getTime(); + var args = arguments; + + if (last && now < last + threshold) { + // wait for it... + clearTimeout(deferTimer); + deferTimer = setTimeout(function() { + last = now; + handler.apply(context, args); + }, threshold); + + } else { + last = now; + handler.apply(context, args); + } + }; + }, + updateToolbar = throttle(function () { if (options.activeToolbarClass) { $(options.toolbarSelector).find(toolbarBtnSelector).each(function () { var command = $(this).data(options.commandRole); + var commandNoArgs; // Temporarily store index, replace with command. + + if ((commandNoArgs = command.indexOf(' ')) >= 0) { + commandNoArgs = command.slice(0, commandNoArgs); + } else { + commandNoArgs = command; + } if (document.queryCommandState(command)) { $(this).addClass(options.activeToolbarClass); + } else if (commandNoArgs + ' ' + document.queryCommandValue(commandNoArgs) === command) { + $(this).addClass(options.activeToolbarClass); } else { $(this).removeClass(options.activeToolbarClass); } }); + if (options.setRealFontSize !== null) { + options.setRealFontSize(selectedRange); + } } - }, + }, 500), execCommand = function (commandWithArgs, valueArg) { var commandArr = commandWithArgs.split(' '), command = commandArr.shift(), args = commandArr.join(' ') + (valueArg || ''); + if (command.indexOf('justify') === 0) { + ['left', 'center', 'right'].forEach(function(val) { commandCache['justify' + val] = false; }); + } document.execCommand(command, 0, args); + if (args.length > 0) { + commandCache[command] = document.queryCommandValue(command); + } else { + commandCache[command] = document.queryCommandState(command); + } + restoreCommandCache(); updateToolbar(); }, + mapHotKeyToCommand = function (event, hotKeys) { + var hotKey = ''; + + if (event.ctrlKey) { + hotKey += 'ctrl+'; + } else if (event.metaKey) { + hotKey += 'meta+'; + } + + if (event.shiftKey) { + hotKey += 'shift+'; + } + + var keyCode = event.keyCode; + if (keyCode >= 20) { // Real characters + // Reduce UTF chars. + if (96 <= event.keyCode && event.keyCode <= 105) { + keyCode -= 48; + } + hotKey += String.fromCharCode(keyCode).toLowerCase(); + + } else if (event.keyCode === 9) { // TAB character + hotKey += 'tab'; + } + + return hotKeys[hotKey]; + + }, + editorActive = function () { + return (editor.attr('contenteditable') && editor.is(':visible')); + }, bindHotkeys = function (hotKeys) { - $.each(hotKeys, function (hotkey, command) { - editor.keydown(hotkey, function (e) { - if (editor.attr('contenteditable') && editor.is(':visible')) { - e.preventDefault(); - e.stopPropagation(); - execCommand(command); + /* + * Break up the hotkeys into multiple values. For example + * {'ctrl+b meta+b': 'bold'} -> {'ctrl+b': 'bold'},{'meta+b': 'bold'}. + * Also ensures everything is stored in lower case. + */ + for (var hotKey in hotKeys) { + var split; + if ((split = hotKey.split(' ')).length > 1) { + for (var i = 0; i < split.length; i++) { + hotKeys[split[i].toLowerCase()] = hotKeys[hotKey].toLowerCase(); } - }).keyup(hotkey, function (e) { - if (editor.attr('contenteditable') && editor.is(':visible')) { - e.preventDefault(); - e.stopPropagation(); + delete hotKeys[hotKey]; + } else { + hotKeys[hotKey.toLowerCase()] = hotKeys[hotKey].toLowerCase(); + } + } + + var eventNamespace = options.eventNamespace + '-' + 'bindKeys'; + + editor.on(namespaceEvents('focus'), function(e) { + setTimeout(function() { + restoreCommandCache(); + }, 0); + }); + + editor.on(namespaceEvents('keydown'), hotKeys, function (e) { + var command = ''; + + // Ensure we have a command to execute for this hotkey, before blocking anything. + if (editorActive() && (command = mapHotKeyToCommand(e, hotKeys)) != null) { + e.preventDefault(); + e.stopPropagation(); + + // Execute hotkey if the callback tells us it's enabled. + if ( + typeof options.hotKeyEnabledCallback === 'function' && + options.hotKeyEnabledCallback(command) + ) { + execCommand(command); } - }); + } + }); + + // Install a single keyup handler to block the keyup events matching a hotKey. + editor.on(namespaceEvents('keyup'), hotKeys, function (e) { + var command = ''; + if (editorActive() && (command = mapHotKeyToCommand(e, hotKeys)) != null) { + e.preventDefault(); + e.stopPropagation(); + } }); }, getCurrentRange = function () { @@ -65,6 +219,7 @@ } }, saveSelection = function () { + updateCommandCache(); selectedRange = getCurrentRange(); }, restoreSelection = function () { @@ -87,8 +242,8 @@ $.when(readFileIntoDataUrl(fileInfo)).done(function (dataUrl) { execCommand('insertimage', dataUrl); }).fail(function (e) { - options.fileUploadError("file-reader", e); - }); + options.fileUploadError("file-reader", e); + }); } else { options.fileUploadError("unsupported-file-type", fileInfo.type); } @@ -106,12 +261,13 @@ toolbar.find(toolbarBtnSelector).click(function () { restoreSelection(); editor.focus(); + restoreCommandCache(); execCommand($(this).data(options.commandRole)); saveSelection(); }); toolbar.find('[data-toggle=dropdown]').click(restoreSelection); - toolbar.find('input[type=text][data-' + options.commandRole + ']').on('webkitspeechchange change', function () { + toolbar.find('input[type=text][data-' + options.commandRole + ']').on(namespaceEvents('webkitspeechchange change'), function () { var newValue = this.value; /* ugly but prevents fake double-calls due to selection restoration */ this.value = ''; restoreSelection(); @@ -120,30 +276,30 @@ execCommand($(this).data(options.commandRole), newValue); } saveSelection(); - }).on('focus', function () { - var input = $(this); - if (!input.data(options.selectionMarker)) { - markSelection(input, options.selectionColor); - input.focus(); - } - }).on('blur', function () { - var input = $(this); - if (input.data(options.selectionMarker)) { - markSelection(input, false); - } - }); - toolbar.find('input[type=file][data-' + options.commandRole + ']').change(function () { + }).on(namespaceEvents('focus'), function () { + var input = $(this); + if (!input.data(options.selectionMarker)) { + markSelection(input, options.selectionColor); + input.focus(); + } + }).on(namespaceEvents('blur'), function () { + var input = $(this); + if (input.data(options.selectionMarker)) { + markSelection(input, false); + } + }); + toolbar.find('input[type=file][data-' + options.commandRole + ']').on(namespaceEvents('change'), (function () { restoreSelection(); if (this.type === 'file' && this.files && this.files.length > 0) { insertFiles(this.files); } saveSelection(); this.value = ''; - }); + })); }, initFileDrops = function () { - editor.on('dragenter dragover', false) - .on('drop', function (e) { + editor.on(namespaceEvents('dragenter dragover'), false) + .on(namespaceEvents('drop'), function (e) { var dataTransfer = e.originalEvent.dataTransfer; e.stopPropagation(); e.preventDefault(); @@ -153,18 +309,26 @@ }); }; options = $.extend({}, $.fn.wysiwyg.defaults, userOptions); - toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + ']'; + toolbarBtnSelector = 'a[data-' + options.commandRole + '],button[data-' + options.commandRole + '],input[type=button][data-' + options.commandRole + '],div[data-' + options.commandRole + ']'; bindHotkeys(options.hotKeys); if (options.dragAndDropImages) { initFileDrops(); } bindToolbar($(options.toolbarSelector), options); editor.attr('contenteditable', true) - .on('mouseup keyup mouseout', function () { - saveSelection(); - updateToolbar(); + .on(namespaceEvents('mouseup keyup mouseout'), function () { + setTimeout(function() { + saveSelection(); + updateToolbar(); + }, 0); }); - $(window).bind('touchend', function (e) { + + $(toolbarBtnSelector).each(function () { + var btnAttr = this.getAttribute('data-' + options.commandRole); + commandCache[btnAttr.split(' ')[0]] = false; + }); + + $(window).bind(namespaceEvents('touchend'), function (e) { var isInside = (editor.is(e.target) || editor.has(e.target).length > 0), currentRange = getCurrentRange(), clear = currentRange && (currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset); @@ -195,6 +359,8 @@ selectionMarker: 'edit-focus-marker', selectionColor: 'darkgrey', dragAndDropImages: true, + eventNamespace: 'bootstrap-wysiwyg', + hotKeyEnabledCallback: function(command) { return true; }, fileUploadError: function (reason, detail) { console.log("File upload error", reason, detail); } }; }(window.jQuery));