![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/CoreBundle/Assets/js/libraries/froala/ |
/*! * froala_editor v2.4.2 (https://www.froala.com/wysiwyg-editor) * License https://froala.com/wysiwyg-editor/terms/ * Copyright 2014-2017 Froala Labs */ (function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); } else if (typeof module === 'object' && module.exports) { // Node/CommonJS module.exports = function( root, jQuery ) { if ( jQuery === undefined ) { // require('jQuery') returns a factory that requires window to // build a jQuery instance, we normalize how we use modules // that require this pattern but the window provided is a noop // if it's defined (how jquery works) if ( typeof window !== 'undefined' ) { jQuery = require('jquery'); } else { jQuery = require('jquery')(root); } } factory(jQuery); return jQuery; }; } else { // Browser globals factory(jQuery); } }(function ($) { /*jslint browser: true, debug: true, vars: true, devel: true, expr: true, jQuery: true */ // EDITABLE CLASS DEFINITION // ========================= var FE = function (element, options) { this.id = ++$.FE.ID; this.opts = $.extend(true, {}, $.extend({}, FE.DEFAULTS, typeof options == 'object' && options)); var opts_string = JSON.stringify(this.opts); $.FE.OPTS_MAPPING[opts_string] = $.FE.OPTS_MAPPING[opts_string] || this.id; this.sid = $.FE.OPTS_MAPPING[opts_string]; $.FE.SHARED[this.sid] = $.FE.SHARED[this.sid] || {}; this.shared = $.FE.SHARED[this.sid]; this.shared.count = (this.shared.count || 0) + 1; this.$oel = $(element); this.$oel.data('froala.editor', this); this.o_doc = element.ownerDocument; this.o_win = 'defaultView' in this.o_doc ? this.o_doc.defaultView : this.o_doc.parentWindow; var c_scroll = $(this.o_win).scrollTop(); this.$oel.on('froala.doInit', $.proxy(function () { this.$oel.off('froala.doInit'); this.doc = this.$el.get(0).ownerDocument; this.win = 'defaultView' in this.doc ? this.doc.defaultView : this.doc.parentWindow; this.$doc = $(this.doc); this.$win = $(this.win); if (!this.opts.pluginsEnabled) this.opts.pluginsEnabled = Object.keys($.FE.PLUGINS); if (this.opts.initOnClick) { this.load($.FE.MODULES); // https://github.com/froala/wysiwyg-editor/issues/1207. this.$el.on('touchstart.init', function () { $(this).data('touched', true); }); this.$el.on('touchmove.init', function () { $(this).removeData('touched'); }) this.$el.on('mousedown.init touchend.init dragenter.init focus.init', $.proxy(function (e) { if (e.type == 'touchend' && !this.$el.data('touched')) { return true; } if (e.which === 1 || !e.which) { this.$el.off('mousedown.init touchstart.init touchmove.init touchend.init dragenter.init focus.init'); this.load($.FE.MODULES); this.load($.FE.PLUGINS); var target = e.originalEvent && e.originalEvent.originalTarget; if (target && target.tagName == 'IMG') $(target).trigger('mousedown'); if (typeof this.ul == 'undefined') this.destroy(); if (e.type == 'touchend' && this.image && e.originalEvent && e.originalEvent.target && $(e.originalEvent.target).is('img')) { setTimeout($.proxy(function () { this.image.edit($(e.originalEvent.target)); }, this), 100); } this.ready = true; this.events.trigger('initialized'); } }, this)); } else { this.load($.FE.MODULES); this.load($.FE.PLUGINS); $(this.o_win).scrollTop(c_scroll); if (typeof this.ul == 'undefined') this.destroy(); this.ready = true; this.events.trigger('initialized'); } }, this)); this._init(); }; FE.DEFAULTS = { initOnClick: false, pluginsEnabled: null }; FE.MODULES = {}; FE.PLUGINS = {}; FE.VERSION = '2.4.2'; FE.INSTANCES = []; FE.OPTS_MAPPING = {}; FE.SHARED = {}; FE.ID = 0; FE.prototype._init = function () { // Get the tag name of the original element. var tag_name = this.$oel.prop('tagName'); // Initialize on anything else. var initOnDefault = $.proxy(function () { if (tag_name != 'TEXTAREA') { this._original_html = (this._original_html || this.$oel.html()); } this.$box = this.$box || this.$oel; // Turn on iframe if fullPage is on. if (this.opts.fullPage) this.opts.iframe = true; if (!this.opts.iframe) { this.$el = $('<div></div>'); this.el = this.$el.get(0); this.$wp = $('<div></div>').append(this.$el); this.$box.html(this.$wp); this.$oel.trigger('froala.doInit'); } else { this.$iframe = $('<iframe src="about:blank" frameBorder="0">'); this.$wp = $('<div></div>'); this.$box.html(this.$wp); this.$wp.append(this.$iframe); this.$iframe.get(0).contentWindow.document.open(); this.$iframe.get(0).contentWindow.document.write('<!DOCTYPE html>'); this.$iframe.get(0).contentWindow.document.write('<html><head></head><body></body></html>'); this.$iframe.get(0).contentWindow.document.close(); this.$el = this.$iframe.contents().find('body'); this.el = this.$el.get(0); this.$head = this.$iframe.contents().find('head'); this.$html = this.$iframe.contents().find('html'); this.iframe_document = this.$iframe.get(0).contentWindow.document; this.$oel.trigger('froala.doInit'); } }, this); // Initialize on a TEXTAREA. var initOnTextarea = $.proxy(function () { this.$box = $('<div>'); this.$oel.before(this.$box).hide(); this._original_html = this.$oel.val(); // Before submit textarea do a sync. this.$oel.parents('form').on('submit.' + this.id, $.proxy(function () { this.events.trigger('form.submit'); }, this)); this.$oel.parents('form').on('reset.' + this.id, $.proxy(function () { this.events.trigger('form.reset'); }, this)); initOnDefault(); }, this); // Initialize on a Link. var initOnA = $.proxy(function () { this.$el = this.$oel; this.el = this.$el.get(0); this.$el.attr('contenteditable', true).css('outline', 'none').css('display', 'inline-block'); this.opts.multiLine = false; this.opts.toolbarInline = false; this.$oel.trigger('froala.doInit'); }, this) // Initialize on an Image. var initOnImg = $.proxy(function () { this.$el = this.$oel; this.el = this.$el.get(0); this.opts.toolbarInline = false; this.$oel.trigger('froala.doInit'); }, this) var editInPopup = $.proxy(function () { this.$el = this.$oel; this.el = this.$el.get(0); this.opts.toolbarInline = false; this.$oel.on('click.popup', function (e) { e.preventDefault(); }) this.$oel.trigger('froala.doInit'); }, this) // Check on what element it was initialized. if (this.opts.editInPopup) editInPopup(); else if (tag_name == 'TEXTAREA') initOnTextarea(); else if (tag_name == 'A') initOnA(); else if (tag_name == 'IMG') initOnImg(); else if (tag_name == 'BUTTON' || tag_name == 'INPUT') { this.opts.editInPopup = true; this.opts.toolbarInline = false; editInPopup(); } else { initOnDefault(); } } FE.prototype.load = function (module_list) { // Bind modules to the current instance and tear them up. for (var m_name in module_list) { if (module_list.hasOwnProperty(m_name)) { if (this[m_name]) continue; // Do not include plugin. if ($.FE.PLUGINS[m_name] && this.opts.pluginsEnabled.indexOf(m_name) < 0) continue; this[m_name] = new module_list[m_name](this); if (this[m_name]._init) { this[m_name]._init(); if (this.opts.initOnClick && m_name == 'core') { return false; } } } } } // Do destroy. FE.prototype.destroy = function () { this.shared.count--; this.events.$off(); // HTML. var html = this.html.get(); this.events.trigger('destroy', [], true); this.events.trigger('shared.destroy', undefined, true); // Remove shared. if (this.shared.count === 0) { for (var k in this.shared) { if (this.shared.hasOwnProperty(k)) { this.shared[k] == null; $.FE.SHARED[this.sid][k] = null; } } $.FE.SHARED[this.sid] = {}; } this.$oel.parents('form').off('.' + this.id); this.$oel.off('click.popup'); this.$oel.removeData('froala.editor'); this.$oel.off('froalaEditor'); // Destroy editor basic elements. this.core.destroy(html); $.FE.INSTANCES.splice($.FE.INSTANCES.indexOf(this), 1); } // FROALA EDITOR PLUGIN DEFINITION // ========================== $.fn.froalaEditor = function (option) { var arg_list = []; for (var i = 0; i < arguments.length; i++) { arg_list.push(arguments[i]); } if (typeof option == 'string') { var returns = []; this.each(function () { var $this = $(this); var editor = $this.data('froala.editor'); if (!editor) { return console.warn('Editor should be initialized before calling the ' + option + ' method.'); } var context; var nm; // Might do a module call. if (option.indexOf('.') > 0 && editor[option.split('.')[0]]) { if (editor[option.split('.')[0]]) { context = editor[option.split('.')[0]]; } nm = option.split('.')[1]; } else { context = editor; nm = option.split('.')[0] } if (context[nm]) { var returned_value = context[nm].apply(editor, arg_list.slice(1)); if (returned_value === undefined) { returns.push(this); } else if (returns.length === 0) { returns.push(returned_value); } } else { return $.error('Method ' + option + ' does not exist in Froala Editor.'); } }); return (returns.length == 1) ? returns[0] : returns; } else if (typeof option === 'object' || !option) { return this.each(function () { var editor = $(this).data('froala.editor'); if (!editor) { var that = this; new FE(that, option); } }); } } $.fn.froalaEditor.Constructor = FE; $.FroalaEditor = FE; $.FE = FE; $.FE.XS = 0; $.FE.SM = 1; $.FE.MD = 2; $.FE.LG = 3; $.FE.MODULES.helpers = function (editor) { /** * Get the IE version. */ function _ieVersion () { /*global navigator */ var rv = -1; var ua; var re; if (navigator.appName == 'Microsoft Internet Explorer') { ua = navigator.userAgent; re = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})'); if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1); } else if (navigator.appName == 'Netscape') { ua = navigator.userAgent; re = new RegExp('Trident/.*rv:([0-9]{1,}[\\.0-9]{0,})'); if (re.exec(ua) !== null) rv = parseFloat(RegExp.$1); } return rv; } /** * Determine the browser. */ function _browser () { var browser = {}; var ie_version = _ieVersion(); if (ie_version > 0) { browser.msie = true; } else { var ua = navigator.userAgent.toLowerCase(); var match = /(edge)[ \/]([\w.]+)/.exec(ua) || /(chrome)[ \/]([\w.]+)/.exec(ua) || /(webkit)[ \/]([\w.]+)/.exec(ua) || /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || []; var matched = { browser: match[1] || '', version: match[2] || '0' }; if (match[1]) browser[matched.browser] = true; // Chrome is Webkit, but Webkit is also Safari. if (browser.chrome) { browser.webkit = true; } else if (browser.webkit) { browser.safari = true; } } if (browser.msie) browser.version = ie_version; return browser; } function isIOS () { return /(iPad|iPhone|iPod)/g.test(navigator.userAgent) && !isWindowsPhone(); } function isAndroid () { return /(Android)/g.test(navigator.userAgent) && !isWindowsPhone(); } function isBlackberry () { return /(Blackberry)/g.test(navigator.userAgent); } function isWindowsPhone () { return /(Windows Phone)/gi.test(navigator.userAgent); } function isMobile () { return isAndroid() || isIOS() || isBlackberry(); } function requestAnimationFrame () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; } function getPX (val) { return parseInt(val, 10) || 0; } function screenSize () { var $test = $('<div class="fr-visibility-helper"></div>').appendTo('body'); var size = getPX($test.css('margin-left')); $test.remove(); return size; } function isTouch () { return ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; } function isURL (url) { if (!/^(https?:|ftps?:|)\/\//i.test(url)) return false; url = String(url) .replace(/</g, '%3C') .replace(/>/g, '%3E') .replace(/"/g, '%22') .replace(/ /g, '%20'); var test_reg = /(http|ftp|https):\/\/[a-z\u00a1-\uffff0-9{}]+(\.[a-z\u00a1-\uffff0-9{}]*)*([a-z\u00a1-\uffff0-9.,@?^=%&:\/~+#-_{}]*[a-z\u00a1-\uffff0-9@?^=%&\/~+#-_{}])?/gi; return test_reg.test(url); } // Sanitize URL. function sanitizeURL (url) { if (/^(https?:|ftps?:|)\/\//i.test(url)) { if (!isURL(url) && !isURL('http:' + url)) { return ''; } } else { url = encodeURIComponent(url) .replace(/%23/g, '#') .replace(/%2F/g, '/') .replace(/%25/g, '%') .replace(/mailto%3A/gi, 'mailto:') .replace(/file%3A/gi, 'file:') .replace(/sms%3A/gi, 'sms:') .replace(/tel%3A/gi, 'tel:') .replace(/notes%3A/gi, 'notes:') .replace(/data%3Aimage/gi, 'data:image') .replace(/blob%3A/gi, 'blob:') .replace(/webkit-fake-url%3A/gi, 'webkit-fake-url:') .replace(/%3F/g, '?') .replace(/%3D/g, '=') .replace(/%26/g, '&') .replace(/&/g, '&') .replace(/%2C/g, ',') .replace(/%3B/g, ';') .replace(/%2B/g, '+') .replace(/%40/g, '@') .replace(/%5B/g, '[') .replace(/%5D/g, ']') .replace(/%7B/g, '{') .replace(/%7D/g, '}'); } return url; } function isArray (obj) { return obj && !(obj.propertyIsEnumerable('length')) && typeof obj === 'object' && typeof obj.length === 'number'; } /* * Transform RGB color to hex value. */ function RGBToHex (rgb) { function hex(x) { return ('0' + parseInt(x, 10).toString(16)).slice(-2); } try { if (!rgb || rgb === 'transparent') return ''; if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb; rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); return ('#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3])).toUpperCase(); } catch (ex) { return null; } } function HEXtoRGB (hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function (m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? 'rgb(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ')' : ''; } /* * Get block alignment. */ var default_alignment; function getAlignment ($block) { var alignment = ($block.css('text-align') || '').replace(/-(.*)-/g, ''); // Detect rtl. if (['left', 'right', 'justify', 'center'].indexOf(alignment) < 0) { if (!default_alignment) { var $div = $('<div dir="auto" style="text-align: initial; position: fixed; left: -3000px;"><span id="s1">.</span><span id="s2">.</span></div>'); $('body').append($div); var l1 = $div.find('#s1').get(0).getBoundingClientRect().left; var l2 = $div.find('#s2').get(0).getBoundingClientRect().left; $div.remove(); default_alignment = l1 < l2 ? 'left' : 'right'; } alignment = default_alignment; } return alignment; } /** * Check if is mac. */ var is_mac = null; function isMac () { if (is_mac == null) { is_mac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;; } return is_mac; } // https://github.com/lazd/scopedQuerySelectorShim/blob/master/src/scopedQuerySelectorShim.js function _scopeShim () { // A temporary element to query against for elements not currently in the DOM // We'll also use this element to test for :scope support var container = editor.o_doc.createElement('div'); // Check if the browser supports :scope try { // Browser supports :scope, do nothing container.querySelectorAll(':scope *'); } catch (e) { // Match usage of scope var scopeRE = /^\s*:scope/gi; // Overrides function overrideNodeMethod(prototype, methodName) { // Store the old method for use later var oldMethod = prototype[methodName]; // Override the method prototype[methodName] = function(query) { var nodeList, gaveId = false, gaveContainer = false; if (query.match(scopeRE)) { // Remove :scope query = query.replace(scopeRE, ''); if (!this.parentNode) { // Add to temporary container container.appendChild(this); gaveContainer = true; } var parentNode = this.parentNode; if (!this.id) { // Give temporary ID this.id = 'rootedQuerySelector_id_'+(new Date()).getTime(); gaveId = true; } // Find elements against parent node nodeList = oldMethod.call(parentNode, '#'+this.id+' '+query); // Reset the ID if (gaveId) { this.id = ''; } // Remove from temporary container if (gaveContainer) { container.removeChild(this); } return nodeList; } else { // No immediate child selector used return oldMethod.call(this, query); } }; } // Browser doesn't support :scope, add polyfill overrideNodeMethod(Element.prototype, 'querySelector'); overrideNodeMethod(Element.prototype, 'querySelectorAll'); } } function scrollTop () { // Firefox, Chrome, Opera, Safari if (editor.o_win.pageYOffset) return editor.o_win.pageYOffset; // Internet Explorer 6 - standards mode if (editor.o_doc.documentElement && editor.o_doc.documentElement.scrollTop) return editor.o_doc.documentElement.scrollTop; // Internet Explorer 6, 7 and 8 if (editor.o_doc.body.scrollTop) return editor.o_doc.body.scrollTop; return 0; } function scrollLeft () { // Firefox, Chrome, Opera, Safari if (editor.o_win.pageXOffset) return editor.o_win.pageXOffset; // Internet Explorer 6 - standards mode if (editor.o_doc.documentElement && editor.o_doc.documentElement.scrollLeft) return editor.o_doc.documentElement.scrollLeft; // Internet Explorer 6, 7 and 8 if (editor.o_doc.body.scrollLeft) return editor.o_doc.body.scrollLeft; return 0; } /** * Tear up. */ function _init () { editor.browser = _browser(); _scopeShim(); } return { _init: _init, isIOS: isIOS, isMac: isMac, isAndroid: isAndroid, isBlackberry: isBlackberry, isWindowsPhone: isWindowsPhone, isMobile: isMobile, requestAnimationFrame: requestAnimationFrame, getPX: getPX, screenSize: screenSize, isTouch: isTouch, sanitizeURL: sanitizeURL, isArray: isArray, RGBToHex: RGBToHex, HEXtoRGB: HEXtoRGB, isURL: isURL, getAlignment: getAlignment, scrollTop: scrollTop, scrollLeft: scrollLeft } } $.FE.MODULES.events = function (editor) { var _events = {}; var _do_blur; function _assignEvent($el, evs, handler) { $on($el, evs, handler); } function _forPaste () { _assignEvent(editor.$el, 'cut copy paste beforepaste', function (e) { trigger(e.type, [e]); }); } function _forElement() { _assignEvent(editor.$el, 'click mouseup mousedown touchstart touchend dragenter dragover dragleave dragend drop dragstart', function (e) { trigger(e.type, [e]); }); on('mousedown', function () { for (var i = 0; i < $.FE.INSTANCES.length; i++) { if ($.FE.INSTANCES[i] != editor && $.FE.INSTANCES[i].popups && $.FE.INSTANCES[i].popups.areVisible()) { $.FE.INSTANCES[i].$el.find('.fr-marker').remove(); } } }) } function _forKeys () { // Map events. _assignEvent(editor.$el, 'keydown keypress keyup input', function (e) { trigger(e.type, [e]); }); } function _forWindow () { _assignEvent(editor.$win, editor._mousedown, function (e) { trigger('window.mousedown', [e]); enableBlur(); }); _assignEvent(editor.$win, editor._mouseup, function (e) { trigger('window.mouseup', [e]); }); _assignEvent(editor.$win, 'cut copy keydown keyup touchmove touchend', function (e) { trigger('window.' + e.type, [e]); }); } function _forDocument() { _assignEvent(editor.$doc, 'dragend drop', function (e) { trigger('document.' + e.type, [e]); }) } function focus (do_focus) { if (typeof do_focus == 'undefined') do_focus = true; if (!editor.$wp) return false; // Focus the editor window. if (editor.helpers.isIOS()) { editor.$win.get(0).focus(); } // If there is no focus, then force focus. if (!editor.core.hasFocus() && do_focus) { var st = editor.$win.scrollTop(); // Hack to prevent scrolling. if (editor.browser.msie && editor.$box) editor.$box.css('position', 'fixed'); editor.$el.focus(); if (editor.browser.msie && editor.$box) editor.$box.css('position', ''); if (st != editor.$win.scrollTop()) { editor.$win.scrollTop(st); } return false; } // Don't go further if we haven't focused or there are markers. if (!editor.core.hasFocus() || editor.$el.find('.fr-marker').length > 0) { return false; } var info = editor.selection.info(editor.el); if (info.atStart && editor.selection.isCollapsed()) { if (editor.html.defaultTag() != null) { var marker = editor.markers.insert(); if (marker && !editor.node.blockParent(marker)) { $(marker).remove(); var element = editor.$el.find(editor.html.blockTagsQuery()).get(0); if (element) { $(element).prepend($.FE.MARKERS); editor.selection.restore(); } } else if (marker) { $(marker).remove(); } } } } var focused = false; function _forFocus () { _assignEvent(editor.$el, 'focus', function (e) { if (blurActive()) { focus(false); if (focused === false) { trigger(e.type, [e]); } } }); _assignEvent(editor.$el, 'blur', function (e) { if (blurActive() /* && document.activeElement != this */) { if (focused === true) { trigger(e.type, [e]); enableBlur(); } } }); on('focus', function () { focused = true; }); on('blur', function () { focused = false; }); } function _forMouse () { if (editor.helpers.isMobile()) { editor._mousedown = 'touchstart'; editor._mouseup = 'touchend'; editor._move = 'touchmove'; editor._mousemove = 'touchmove'; } else { editor._mousedown = 'mousedown'; editor._mouseup = 'mouseup'; editor._move = ''; editor._mousemove = 'mousemove'; } } function _buttonMouseDown (e) { var $btn = $(e.currentTarget); if (editor.edit.isDisabled() || editor.node.hasClass($btn.get(0), 'fr-disabled')) { e.preventDefault(); return false; } // Not click button. if (e.type === 'mousedown' && e.which !== 1) return true; // Scroll in list. if (!editor.helpers.isMobile()) { e.preventDefault(); } if ((editor.helpers.isAndroid() || editor.helpers.isWindowsPhone()) && $btn.parents('.fr-dropdown-menu').length === 0) { e.preventDefault(); e.stopPropagation(); } // Simulate click. $btn.addClass('fr-selected'); editor.events.trigger('commands.mousedown', [$btn]); } function _buttonMouseUp (e, handler) { var $btn = $(e.currentTarget); if (editor.edit.isDisabled() || editor.node.hasClass($btn.get(0), 'fr-disabled')) { e.preventDefault(); return false; } if (e.type === 'mouseup' && e.which !== 1) return true; if (!editor.node.hasClass($btn.get(0), 'fr-selected')) return true; if (e.type != 'touchmove') { e.stopPropagation(); e.stopImmediatePropagation(); e.preventDefault(); // Simulate click. if (!editor.node.hasClass($btn.get(0), 'fr-selected')) { $('.fr-selected').removeClass('fr-selected'); return false; } $('.fr-selected').removeClass('fr-selected'); if ($btn.data('dragging') || $btn.attr('disabled')) { $btn.removeData('dragging'); return false; } var timeout = $btn.data('timeout'); if (timeout) { clearTimeout(timeout); $btn.removeData('timeout'); } handler.apply(editor, [e]); } else { if (!$btn.data('timeout')) { $btn.data('timeout', setTimeout(function () { $btn.data('dragging', true); }, 100)); } } } function enableBlur () { _do_blur = true; } function disableBlur () { _do_blur = false; } function blurActive () { return _do_blur; } /** * Bind click on an element. */ function bindClick ($element, selector, handler) { $on($element, editor._mousedown, selector, function (e) { if (!editor.edit.isDisabled()) _buttonMouseDown(e); }, true); $on($element, editor._mouseup + ' ' + editor._move, selector, function (e) { if (!editor.edit.isDisabled()) _buttonMouseUp(e, handler); }, true); $on($element, 'mousedown click mouseup', selector, function (e) { if (!editor.edit.isDisabled()) e.stopPropagation(); }, true); on('window.mouseup', function () { if (!editor.edit.isDisabled()) { $element.find(selector).removeClass('fr-selected'); enableBlur(); } }); } /** * Add event. */ function on (name, callback, first) { var names = name.split(' '); if (names.length > 1) { for (var i = 0; i < names.length; i++) { on(names[i], callback, first); } return true; } if (typeof first == 'undefined') first = false; var callbacks; if (name.indexOf('shared.') != 0) { callbacks = (_events[name] = _events[name] || []); } else { callbacks = (editor.shared._events[name] = editor.shared._events[name] || []); } if (first) { callbacks.unshift(callback); } else { callbacks.push(callback); } } var $_events = []; function $on ($el, evs, selector, callback, shared) { if (typeof selector == 'function') { shared = callback; callback = selector; selector = false; } var ary = (!shared ? $_events : editor.shared.$_events); var id = (!shared ? editor.id : editor.sid); if (!selector) { $el.on(evs.split(' ').join('.ed' + id + ' ') + '.ed' + id, callback); } else { $el.on(evs.split(' ').join('.ed' + id + ' ') + '.ed' + id, selector, callback); } if (ary.indexOf($el.get(0)) < 0) ary.push($el.get(0)); } function _$off (evs, id) { for (var i = 0; i < evs.length; i++) { $(evs[i]).off('.ed' + id); } } function $off () { _$off($_events, editor.id); $_events = []; if (editor.shared.count == 0) { _$off(editor.shared.$_events, editor.sid); editor.shared.$_events = null; } } /** * Trigger an event. */ function trigger (name, args, force) { if (!editor.edit.isDisabled() || force) { var callbacks; if (name.indexOf('shared.') != 0) { callbacks = _events[name]; } else { if (editor.shared.count > 0) return false; callbacks = editor.shared._events[name]; } var val; if (callbacks) { for (var i = 0; i < callbacks.length; i++) { val = callbacks[i].apply(editor, args); if (val === false) return false; } } // Trigger event outside. val = editor.$oel.triggerHandler('froalaEditor.' + name, $.merge([editor], (args || []))); if (val === false) return false; return val; } } function chainTrigger (name, param, force) { if (!editor.edit.isDisabled() || force) { var callbacks; if (name.indexOf('shared.') != 0) { callbacks = _events[name]; } else { if (editor.shared.count > 0) return false; callbacks = editor.shared._events[name]; } var resp; if (callbacks) { for (var i = 0; i < callbacks.length; i++) { // Get the callback response. resp = callbacks[i].apply(editor, [param]); // If callback response is defined then assign it to param. if (typeof resp !== 'undefined') param = resp; } } // Trigger event outside. resp = editor.$oel.triggerHandler('froalaEditor.' + name, $.merge([editor], [param])); // If callback response is defined then assign it to param. if (typeof resp !== 'undefined') param = resp; return param; } } /** * Destroy */ function _destroy () { // Clear the events list. for (var k in _events) { if (_events.hasOwnProperty(k)) { delete _events[k]; } } } function _sharedDestroy () { for (var k in editor.shared._events) { if (editor.shared._events.hasOwnProperty(k)) { delete editor.shared._events[k]; } } } /** * Tear up. */ function _init () { editor.shared.$_events = editor.shared.$_events || []; editor.shared._events = {}; _forMouse(); _forElement(); _forWindow(); _forDocument(); _forKeys(); _forFocus(); enableBlur(); _forPaste(); on('destroy', _destroy); on('shared.destroy', _sharedDestroy); } return { _init: _init, on: on, trigger: trigger, bindClick: bindClick, disableBlur: disableBlur, enableBlur: enableBlur, blurActive: blurActive, focus: focus, chainTrigger: chainTrigger, $on: $on, $off: $off } }; $.FE.MODULES.node = function (editor) { function getContents(node) { if (!node || node.tagName == 'IFRAME') return []; return Array.prototype.slice.call(node.childNodes || []); } /** * Determine if the node is a block tag. */ function isBlock (node) { if (!node) return false; if (node.nodeType != Node.ELEMENT_NODE) return false; return $.FE.BLOCK_TAGS.indexOf(node.tagName.toLowerCase()) >= 0; } /** * Check if a DOM element is empty. */ function isEmpty (el, ignore_markers) { if (!el) return true; if (el.querySelector('table')) return false; // Get element contents. var contents = getContents(el); // Check if there is a block tag. if (contents.length == 1 && isBlock(contents[0])) { contents = getContents(contents[0]); } var has_br = false; for (var i = 0; i < contents.length; i++) { var node = contents[i]; if (ignore_markers && editor.node.hasClass(node, 'fr-marker')) continue; if (node.nodeType == Node.TEXT_NODE && node.textContent.length == 0) continue; if (node.tagName != 'BR' && (node.textContent || '').replace(/\u200B/gi, '').replace(/\n/g, '').length > 0) return false; if (has_br) { return false; } else if (node.tagName == 'BR') { has_br = true; } } // Look for void nodes. if (el.querySelectorAll($.FE.VOID_ELEMENTS.join(',')).length - el.querySelectorAll('br').length) return false; // Look for empty allowed tags. if (el.querySelector(editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)')) return false; // Look for block tags. if (el.querySelectorAll($.FE.BLOCK_TAGS.join(',')).length > 1) return false; // Look for do not wrap tags. if (el.querySelector(editor.opts.htmlDoNotWrapTags.join(':not(.fr-marker),') + ':not(.fr-marker)')) return false; return true; } /** * Get the block parent. */ function blockParent (node) { while (node && node.parentNode !== editor.el && !(node.parentNode && editor.node.hasClass(node.parentNode, 'fr-inner'))) { node = node.parentNode; if (isBlock(node)) { return node; } } return null; } /** * Get deepest parent till the element. */ function deepestParent (node, until, simple_enter) { if (typeof until == 'undefined') until = []; if (typeof simple_enter == 'undefined') simple_enter = true; until.push(editor.el); if (until.indexOf(node.parentNode) >= 0 || (node.parentNode && editor.node.hasClass(node.parentNode, 'fr-inner')) || (node.parentNode && $.FE.SIMPLE_ENTER_TAGS.indexOf(node.parentNode.tagName) >= 0 && simple_enter)) { return null; } // 1. Before until. // 2. Parent node doesn't has class fr-inner. // 3. Parent node is not a simple enter tag or quote. // 4. Parent node is not a block tag while (until.indexOf(node.parentNode) < 0 && node.parentNode && !editor.node.hasClass(node.parentNode, 'fr-inner') && ($.FE.SIMPLE_ENTER_TAGS.indexOf(node.parentNode.tagName) < 0 || !simple_enter) && (!(isBlock(node) && isBlock(node.parentNode)) || !simple_enter)) { node = node.parentNode; } return node; } function rawAttributes (node) { var attrs = {}; var atts = node.attributes; if (atts) { for (var i = 0; i < atts.length; i++) { var att = atts[i]; attrs[att.nodeName] = att.value; } } return attrs; } /** * Get attributes for a node as a string. */ function attributes (node) { var str = ''; var atts = rawAttributes(node); var keys = Object.keys(atts).sort(); for (var i = 0; i < keys.length; i++) { var nodeName = keys[i]; var value = atts[nodeName]; // Make sure we don't break any HTML. if (value.indexOf('"') < 0) { str += ' ' + nodeName + '="' + value + '"'; } else { str += ' ' + nodeName + '=\'' + value + '\''; } } return str; } function clearAttributes (node) { var atts = node.attributes; for (var i = 0; i < atts.length; i++) { var att = atts[i]; node.removeAttribute(att.nodeName); } } /** * Open string for a node. */ function openTagString (node) { return '<' + node.tagName.toLowerCase() + attributes(node) + '>'; } /** * Close string for a node. */ function closeTagString (node) { return '</' + node.tagName.toLowerCase() + '>'; } /** * Determine if the node has any left sibling. */ function isFirstSibling (node, ignore_markers) { if (typeof ignore_markers == 'undefined') ignore_markers = true; var sibling = node.previousSibling; while (sibling && ignore_markers && editor.node.hasClass(sibling, 'fr-marker')) { sibling = sibling.previousSibling; } if (!sibling) return true; if (sibling.nodeType == Node.TEXT_NODE && sibling.textContent === '') return isFirstSibling(sibling); return false; } /** * Determine if the node has any right sibling. */ function isLastSibling (node, ignore_markers) { if (typeof ignore_markers == 'undefined') ignore_markers = true; var sibling = node.nextSibling; while (sibling && ignore_markers && editor.node.hasClass(sibling, 'fr-marker')) { sibling = sibling.nextSibling; } if (!sibling) return true; if (sibling.nodeType == Node.TEXT_NODE && sibling.textContent === '') return isLastSibling(sibling); return false; } function isVoid(node) { return node && node.nodeType == Node.ELEMENT_NODE && $.FE.VOID_ELEMENTS.indexOf((node.tagName || '').toLowerCase()) >= 0 } /** * Check if the node is a list. */ function isList (node) { if (!node) return false; return ['UL', 'OL'].indexOf(node.tagName) >= 0; } /** * Check if the node is the editable element. */ function isElement (node) { return node === editor.el; } /** * Check if the node is the editable element. */ function isDeletable (node) { return node && node.nodeType == Node.ELEMENT_NODE && node.getAttribute('class') && (node.getAttribute('class') || '').indexOf('fr-deletable') >= 0; } /** * Check if the node has focus. */ function hasFocus (node) { return node === editor.doc.activeElement && (!editor.doc.hasFocus || editor.doc.hasFocus()) && !!(isElement(node) || node.type || node.href || ~node.tabIndex); } function isEditable (node) { return (!node.getAttribute || node.getAttribute('contenteditable') != 'false') && ['STYLE', 'SCRIPT'].indexOf(node.tagName) < 0; } function hasClass (el, cls) { if (el instanceof $) el = el.get(0); return (el && el.classList && el.classList.contains(cls)); } function filter (callback) { if (editor.browser.msie) { return callback; } else { return { acceptNode: callback } } } return { isBlock: isBlock, isEmpty: isEmpty, blockParent: blockParent, deepestParent: deepestParent, rawAttributes: rawAttributes, attributes: attributes, clearAttributes: clearAttributes, openTagString: openTagString, closeTagString: closeTagString, isFirstSibling: isFirstSibling, isLastSibling: isLastSibling, isList: isList, isElement: isElement, contents: getContents, isVoid: isVoid, hasFocus: hasFocus, isEditable: isEditable, isDeletable: isDeletable, hasClass: hasClass, filter: filter } }; $.FE.INVISIBLE_SPACE = '​'; $.FE.START_MARKER = '<span class="fr-marker" data-id="0" data-type="true" style="display: none; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>'; $.FE.END_MARKER = '<span class="fr-marker" data-id="0" data-type="false" style="display: none; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>'; $.FE.MARKERS = $.FE.START_MARKER + $.FE.END_MARKER; $.FE.MODULES.markers = function (editor) { /** * Build marker element. */ function _build (marker, id) { return $('<span class="fr-marker" data-id="' + id + '" data-type="' + marker + '" style="display: ' + (editor.browser.safari ? 'none' : 'inline-block') + '; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>', editor.doc)[0]; } /** * Place marker. */ function place (range, marker, id) { try { var boundary = range.cloneRange(); boundary.collapse(marker); boundary.insertNode(_build(marker, id)); if (marker === true && range.collapsed) { var mk = editor.$el.find('span.fr-marker[data-type="true"][data-id="' + id + '"]'); var sibling = mk.get(0).nextSibling; while (sibling && sibling.nodeType === Node.TEXT_NODE && sibling.textContent.length === 0) { $(sibling).remove(); sibling = mk.nextSibling; } } if (marker === true && !range.collapsed) { var mk = editor.$el.find('span.fr-marker[data-type="true"][data-id="' + id + '"]').get(0); var sibling = mk.nextSibling; if (sibling && sibling.nodeType === Node.ELEMENT_NODE && editor.node.isBlock(sibling)) { // Place the marker deep inside the block tags. var contents = [sibling]; do { sibling = contents[0]; contents = editor.node.contents(sibling); } while (contents[0] && editor.node.isBlock(contents[0])); $(sibling).prepend($(mk)); } } if (marker === false && !range.collapsed) { var mk = editor.$el.find('span.fr-marker[data-type="false"][data-id="' + id + '"]').get(0); var sibling = mk.previousSibling; if (sibling && sibling.nodeType === Node.ELEMENT_NODE && editor.node.isBlock(sibling)) { // Place the marker deep inside the block tags. var contents = [sibling]; do { sibling = contents[contents.length - 1]; contents = editor.node.contents(sibling); } while (contents[contents.length - 1] && editor.node.isBlock(contents[contents.length - 1])); $(sibling).append($(mk)); } // https://github.com/froala/wysiwyg-editor/issues/705 if (mk.parentNode && ['TD', 'TH'].indexOf(mk.parentNode.tagName) >= 0) { if (mk.parentNode.previousSibling && !mk.previousSibling) { $(mk.parentNode.previousSibling).append(mk); } } } var dom_marker = editor.$el.find('span.fr-marker[data-type="' + marker + '"][data-id="' + id + '"]').get(0); // If image is at the top of the editor in an empty P // and floated to right, the text will be pushed down // when trying to insert an image. if (dom_marker) dom_marker.style.display = 'none'; return dom_marker; } catch (ex) { return null; } } /** * Insert a single marker. */ function insert () { if (!editor.$wp) return null; try { var range = editor.selection.ranges(0); var containter = range.commonAncestorContainer; // Check if selection is inside editor. if (containter != editor.el && editor.$el.find(containter).length == 0) return null; var boundary = range.cloneRange(); var original_range = range.cloneRange(); boundary.collapse(true); var mk = $('<span class="fr-marker" style="display: none; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>', editor.doc)[0]; boundary.insertNode(mk); mk = editor.$el.find('span.fr-marker').get(0); if (mk) { var sibling = mk.nextSibling; while (sibling && sibling.nodeType === Node.TEXT_NODE && sibling.textContent.length === 0) { $(sibling).remove(); sibling = editor.$el.find('span.fr-marker').get(0).nextSibling; } // Keep original selection. editor.selection.clear(); editor.selection.get().addRange(original_range); return mk; } else { return null; } } catch (ex) { console.warn ('MARKER', ex) } } /** * Split HTML at the marker position. */ function split () { if (!editor.selection.isCollapsed()) { editor.selection.remove(); } var marker = editor.$el.find('.fr-marker').get(0); if (marker == null) marker = insert(); if (marker == null) return null; var deep_parent = editor.node.deepestParent(marker); if (!deep_parent) { deep_parent = editor.node.blockParent(marker); if (deep_parent && deep_parent.tagName != 'LI') { deep_parent = null; } } if (deep_parent) { if (editor.node.isBlock(deep_parent) && editor.node.isEmpty(deep_parent)) { $(deep_parent).replaceWith('<span class="fr-marker"></span>'); } else if (editor.cursor.isAtStart(marker, deep_parent)) { $(deep_parent).before('<span class="fr-marker"></span>'); $(marker).remove(); } else if (editor.cursor.isAtEnd(marker, deep_parent)) { $(deep_parent).after('<span class="fr-marker"></span>'); $(marker).remove(); } else { var node = marker; var close_str = ''; var open_str = ''; do { node = node.parentNode; close_str = close_str + editor.node.closeTagString(node); open_str = editor.node.openTagString(node) + open_str; } while (node != deep_parent); $(marker).replaceWith('<span id="fr-break"></span>'); var h = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent); h = h.replace(/<span id="fr-break"><\/span>/g, close_str + '<span class="fr-marker"></span>' + open_str); $(deep_parent).replaceWith(h); } } return editor.$el.find('.fr-marker').get(0) } /** * Insert marker at point from event. * * http://stackoverflow.com/questions/11191136/set-a-selection-range-from-a-to-b-in-absolute-position * https://developer.mozilla.org/en-US/docs/Web/API/this.document.caretPositionFromPoint */ function insertAtPoint (e) { var x = e.clientX; var y = e.clientY; // Clear markers. remove(); var start; var range = null; // Default. if (typeof editor.doc.caretPositionFromPoint != 'undefined') { start = editor.doc.caretPositionFromPoint(x, y); range = editor.doc.createRange(); range.setStart(start.offsetNode, start.offset); range.setEnd(start.offsetNode, start.offset); } // Webkit. else if (typeof editor.doc.caretRangeFromPoint != 'undefined') { start = editor.doc.caretRangeFromPoint(x, y); range = editor.doc.createRange(); range.setStart(start.startContainer, start.startOffset); range.setEnd(start.startContainer, start.startOffset); } // Set ranges. if (range !== null && typeof editor.win.getSelection != 'undefined') { var sel = editor.win.getSelection(); sel.removeAllRanges(); sel.addRange(range); } // MSIE. else if (typeof editor.doc.body.createTextRange != 'undefined') { try { range = editor.doc.body.createTextRange(); range.moveToPoint(x, y); var end_range = range.duplicate(); end_range.moveToPoint(x, y); range.setEndPoint('EndToEnd', end_range); range.select(); } catch (ex) { return false; } } insert(); } /** * Remove markers. */ function remove () { editor.$el.find('.fr-marker').remove(); } return { place: place, insert: insert, split: split, insertAtPoint: insertAtPoint, remove: remove } }; $.FE.MODULES.selection = function (editor) { /** * Get selection text. */ function text () { var text = ''; if (editor.win.getSelection) { text = editor.win.getSelection(); } else if (editor.doc.getSelection) { text = editor.doc.getSelection(); } else if (editor.doc.selection) { text = editor.doc.selection.createRange().text; } return text.toString(); } /** * Get the selection object. */ function get () { var selection = ''; if (editor.win.getSelection) { selection = editor.win.getSelection(); } else if (editor.doc.getSelection) { selection = editor.doc.getSelection(); } else { selection = editor.doc.selection.createRange(); } return selection; } /** * Get the selection ranges or a single range at a specified index. */ function ranges (index) { var sel = get(); var ranges = []; // Get ranges. if (sel && sel.getRangeAt && sel.rangeCount) { var ranges = []; for (var i = 0; i < sel.rangeCount; i++) { ranges.push(sel.getRangeAt(i)); } } else { if (editor.doc.createRange) { ranges = [editor.doc.createRange()]; } else { ranges = []; } } return (typeof index != 'undefined' ? ranges[index] : ranges); } /** * Clear selection. */ function clear () { var sel = get(); try { if (sel.removeAllRanges) { sel.removeAllRanges(); } else if (sel.empty) { // IE? sel.empty(); } else if (sel.clear) { sel.clear(); } } catch (ex) {} } /** * Selection element. */ function element () { var sel = get(); try { if (sel.rangeCount) { var range = ranges(0); var node = range.startContainer; // https://github.com/froala/wysiwyg-editor/issues/1399. if (node.nodeType == Node.TEXT_NODE && range.startOffset == (node.textContent || '').length && node.nextSibling) { node = node.nextSibling; } // Get parrent if node type is not DOM. if (node.nodeType == Node.ELEMENT_NODE) { var node_found = false; // Search for node deeper. if (node.childNodes.length > 0 && node.childNodes[range.startOffset]) { var child = node.childNodes[range.startOffset]; while (child && child.nodeType == Node.TEXT_NODE && child.textContent.length == 0) { child = child.nextSibling; } if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) { node = child; node_found = true; } // Look back maybe me missed something. if (!node_found && node.childNodes.length > 1 && range.startOffset > 0 && node.childNodes[range.startOffset - 1]) { var child = node.childNodes[range.startOffset - 1]; while (child && child.nodeType == Node.TEXT_NODE && child.textContent.length == 0) { child = child.nextSibling; } if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) { node = child; node_found = true; } } } // Selection starts just at the end of the node. else if (!range.collapsed && node.nextSibling && node.nextSibling.nodeType == Node.ELEMENT_NODE) { var child = node.nextSibling; if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) { node = child; node_found = true; } } if (!node_found && node.childNodes.length > 0 && $(node.childNodes[0]).text().replace(/\u200B/g, '') === text().replace(/\u200B/g, '') && ['BR', 'IMG', 'HR'].indexOf(node.childNodes[0].tagName) < 0) { node = node.childNodes[0]; } } while (node.nodeType != Node.ELEMENT_NODE && node.parentNode) { node = node.parentNode; } // Make sure the node is in editor. var p = node; while (p && p.tagName != 'HTML') { if (p == editor.el) { return node; } p = $(p).parent()[0]; } } } catch (ex) { } return editor.el; } /** * Selection element. */ function endElement () { var sel = get(); try { if (sel.rangeCount) { var range = ranges(0); var node = range.endContainer; // Get parrent if node type is not DOM. if (node.nodeType == Node.ELEMENT_NODE) { var node_found = false; // Search for node deeper. if (node.childNodes.length > 0 && node.childNodes[range.endOffset] && $(node.childNodes[range.endOffset]).text() === text()) { node = node.childNodes[range.endOffset]; node_found = true; } // Selection starts just at the end of the node. else if (!range.collapsed && node.previousSibling && node.previousSibling.nodeType == Node.ELEMENT_NODE) { var child = node.previousSibling; if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) { node = child; node_found = true; } } // Browser sees selection at the beginning of the next node. else if (!range.collapsed && node.childNodes.length > 0 && node.childNodes[range.endOffset]) { var child = node.childNodes[range.endOffset].previousSibling; if (child.nodeType == Node.ELEMENT_NODE) { if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) { node = child; node_found = true; } } } if (!node_found && node.childNodes.length > 0 && $(node.childNodes[node.childNodes.length - 1]).text() === text() && ['BR', 'IMG', 'HR'].indexOf(node.childNodes[node.childNodes.length - 1].tagName) < 0) { node = node.childNodes[node.childNodes.length - 1]; } } if (node.nodeType == Node.TEXT_NODE && range.endOffset == 0 && node.previousSibling && node.previousSibling.nodeType == Node.ELEMENT_NODE) { node = node.previousSibling; } while (node.nodeType != Node.ELEMENT_NODE && node.parentNode) { node = node.parentNode; } // Make sure the node is in editor. var p = node; while (p && p.tagName != 'HTML') { if (p == editor.el) { return node; } p = $(p).parent()[0]; } } } catch (ex) { } return editor.el; } /** * Get the ELEMENTS node where the selection starts. * By default TEXT node might be selected. */ function rangeElement(rangeContainer, offset) { var node = rangeContainer; if (node.nodeType == Node.ELEMENT_NODE) { // Search for node deeper. if (node.childNodes.length > 0 && node.childNodes[offset]) { node = node.childNodes[offset]; } } if (node.nodeType == Node.TEXT_NODE) { node = node.parentNode; } return node; } /** * Search for the current selected blocks. */ function blocks () { var blks = []; var sel = get(); // Selection must be inside editor. if (inEditor() && sel.rangeCount) { // Loop through ranges. var rngs = ranges(); for (var i = 0; i < rngs.length; i++) { var range = rngs[i]; // Get start node and end node for range. var start_node = rangeElement(range.startContainer, range.startOffset); var end_node = rangeElement(range.endContainer, range.endOffset); // Add the start node. if (editor.node.isBlock(start_node) && blks.indexOf(start_node) < 0) blks.push(start_node); // Check for the parent node of the start node. var block_parent = editor.node.blockParent(start_node); if (block_parent && blks.indexOf(block_parent) < 0) { blks.push(block_parent); } // Do not add nodes where we've been once. var was_into = []; // Loop until we reach end. var next_node = start_node; while (next_node !== end_node && next_node !== editor.el) { // Get deeper into the current node. if (was_into.indexOf(next_node) < 0 && next_node.children && next_node.children.length) { was_into.push(next_node); next_node = next_node.children[0]; } // Get next sibling. else if (next_node.nextSibling) { next_node = next_node.nextSibling; } // Get parent node. else if (next_node.parentNode) { next_node = next_node.parentNode; was_into.push(next_node); } // Add node to the list. if (editor.node.isBlock(next_node) && was_into.indexOf(next_node) < 0 && blks.indexOf(next_node) < 0) { if (next_node !== end_node || range.endOffset > 0) { blks.push(next_node); } } } // Add the end node. if (editor.node.isBlock(end_node) && blks.indexOf(end_node) < 0 && range.endOffset > 0) blks.push(end_node); // Check for the parent node of the end node. var block_parent = editor.node.blockParent(end_node); if (block_parent && blks.indexOf(block_parent) < 0) { blks.push(block_parent); } } } // Remove blocks that we don't need. for (var i = blks.length - 1; i > 0; i--) { // Nodes that contain another node. Don't do it for LI, but remove them if there is a single child and has format. if ($(blks[i]).find(blks).length && (blks[i].tagName != 'LI' || (blks[i].children.length == 1 && blks.indexOf(blks[i].children[0]) >= 0))) blks.splice(i, 1); } return blks; } /** * Save selection. */ function save () { if (editor.$wp) { editor.markers.remove(); var rgs = ranges(); var new_ranges = []; for (var i = 0; i < rgs.length; i++) { if (rgs[i].startContainer !== editor.doc) { var range = rgs[i]; var collapsed = range.collapsed; var start_m = editor.markers.place(range, true, i); // Start. var end_m = editor.markers.place(range, false, i); // End. // https://github.com/froala/wysiwyg-editor/issues/1398. editor.el.normalize(); if (editor.browser.safari && !collapsed) { var range = editor.doc.createRange(); range.setStartAfter(start_m); range.setEndBefore(end_m); new_ranges.push(range); } } } if (editor.browser.safari && new_ranges.length) { editor.selection.clear(); for (var i = 0; i < new_ranges.length; i++) { editor.selection.get().addRange(new_ranges[i]); } } } } /** * Restore selection. */ function restore () { // Get markers. var markers = editor.el.querySelectorAll('.fr-marker[data-type="true"]'); if (!editor.$wp) { editor.markers.remove(); return false; } // No markers. if (markers.length === 0) { return false; } if (editor.browser.msie || editor.browser.edge) { for (var i = 0; i < markers.length; i++) { markers[i].style.display = 'inline-block'; } } // Focus. if (!editor.core.hasFocus() && !editor.browser.msie && !editor.browser.webkit) { editor.$el.focus(); } clear(); var sel = get(); var parents = []; // Add ranges. for (var i = 0; i < markers.length; i++) { var id = $(markers[i]).data('id'); var start_marker = markers[i]; var range = editor.doc.createRange(); var end_marker = editor.$el.find('.fr-marker[data-type="false"][data-id="' + id + '"]'); if (editor.browser.msie || editor.browser.edge) end_marker.css('display', 'inline-block'); var ghost = null; // Make sure there is an start marker. if (end_marker.length > 0) { end_marker = end_marker[0]; try { // If we have markers one next to each other inside text, then we should normalize text by joining it. var special_case = false; // Clear empty text nodes. var s_node = start_marker.nextSibling; while (s_node && s_node.nodeType == Node.TEXT_NODE && s_node.textContent.length == 0) { var tmp = s_node; s_node = s_node.nextSibling; $(tmp).remove(); } var e_node = end_marker.nextSibling; while (e_node && e_node.nodeType == Node.TEXT_NODE && e_node.textContent.length == 0) { var tmp = e_node; e_node = e_node.nextSibling; $(tmp).remove(); } if (start_marker.nextSibling == end_marker || end_marker.nextSibling == start_marker) { // Decide which is first and which is last between markers. var first_node = (start_marker.nextSibling == end_marker) ? start_marker : end_marker; var last_node = (first_node == start_marker) ? end_marker : start_marker; // Previous node. var prev_node = first_node.previousSibling; while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.length == 0) { var tmp = prev_node; prev_node = prev_node.previousSibling; $(tmp).remove(); } // Normalize text before. if (prev_node && prev_node.nodeType == Node.TEXT_NODE) { while (prev_node && prev_node.previousSibling && prev_node.previousSibling.nodeType == Node.TEXT_NODE) { prev_node.previousSibling.textContent = prev_node.previousSibling.textContent + prev_node.textContent; prev_node = prev_node.previousSibling; $(prev_node.nextSibling).remove(); } } // Next node. var next_node = last_node.nextSibling; while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.length == 0) { var tmp = next_node; next_node = next_node.nextSibling; $(tmp).remove(); } // Normalize text after. if (next_node && next_node.nodeType == Node.TEXT_NODE) { while (next_node && next_node.nextSibling && next_node.nextSibling.nodeType == Node.TEXT_NODE) { next_node.nextSibling.textContent = next_node.textContent + next_node.nextSibling.textContent; next_node = next_node.nextSibling; $(next_node.previousSibling).remove(); } } if (prev_node && (editor.node.isVoid(prev_node) || editor.node.isBlock(prev_node))) prev_node = null; if (next_node && (editor.node.isVoid(next_node) || editor.node.isBlock(next_node))) next_node = null; // Previous node and next node are both text. if (prev_node && next_node && prev_node.nodeType == Node.TEXT_NODE && next_node.nodeType == Node.TEXT_NODE) { // Remove markers. $(start_marker).remove(); $(end_marker).remove(); // Save cursor position. var len = prev_node.textContent.length; prev_node.textContent = prev_node.textContent + next_node.textContent; $(next_node).remove(); // Normalize spaces. editor.spaces.normalize(prev_node); // Restore position. range.setStart(prev_node, len); range.setEnd(prev_node, len); special_case = true; } else if (!prev_node && next_node && next_node.nodeType == Node.TEXT_NODE) { // Remove markers. $(start_marker).remove(); $(end_marker).remove(); // Normalize spaces. editor.spaces.normalize(next_node); ghost = $(editor.doc.createTextNode('\u200B')); $(next_node).before(ghost); // Restore position. range.setStart(next_node, 0); range.setEnd(next_node, 0); special_case = true; } else if (!next_node && prev_node && prev_node.nodeType == Node.TEXT_NODE) { // Remove markers. $(start_marker).remove(); $(end_marker).remove(); // Normalize spaces. editor.spaces.normalize(prev_node); ghost = $(editor.doc.createTextNode('\u200B')); $(prev_node).after(ghost); // Restore position. range.setStart(prev_node, prev_node.textContent.length); range.setEnd(prev_node, prev_node.textContent.length); special_case = true; } } if (!special_case) { var x, y; // DO NOT TOUCH THIS OR IT WILL BREAK!!! if ((editor.browser.chrome || editor.browser.edge) && start_marker.nextSibling == end_marker) { x = _normalizedMarker(end_marker, range, true) || range.setStartAfter(end_marker); y = _normalizedMarker(start_marker, range, false) || range.setEndBefore(start_marker); } else { if (start_marker.previousSibling == end_marker) { start_marker = end_marker; end_marker = start_marker.nextSibling; } // https://github.com/froala/wysiwyg-editor/issues/759 if (!(end_marker.nextSibling && end_marker.nextSibling.tagName === 'BR') && !(!end_marker.nextSibling && editor.node.isBlock(start_marker.previousSibling)) && !(start_marker.previousSibling && start_marker.previousSibling.tagName == 'BR')) { start_marker.style.display = 'inline'; end_marker.style.display = 'inline'; ghost = $(editor.doc.createTextNode('\u200B')); } // https://github.com/froala/wysiwyg-editor/issues/1120 var p_node = start_marker.previousSibling; if (p_node && p_node.style && editor.win.getComputedStyle(p_node).display == 'block' && !editor.opts.enter == $.FE.ENTER_BR) { range.setEndAfter(p_node); range.setStartAfter(p_node); } else { x = _normalizedMarker(start_marker, range, true) || ($(start_marker).before(ghost) && range.setStartBefore(start_marker)); y = _normalizedMarker(end_marker, range, false) || ($(end_marker).after(ghost) && range.setEndAfter(end_marker)); } } if (typeof x == 'function') x(); if (typeof y == 'function') y(); } } catch (ex) { console.warn ('RESTORE RANGE', ex); } } if (ghost) { ghost.remove(); } try { sel.addRange(range); } catch (ex) { console.warn ('ADD RANGE', ex); } } // Remove used markers. editor.markers.remove(); } /** * Normalize marker when restoring selection. */ function _normalizedMarker(marker, range, start) { var prev_node = marker.previousSibling; var next_node = marker.nextSibling; // Prev and next node are both text nodes. if (prev_node && next_node && prev_node.nodeType == Node.TEXT_NODE && next_node.nodeType == Node.TEXT_NODE) { var len = prev_node.textContent.length; if (start) { next_node.textContent = prev_node.textContent + next_node.textContent; $(prev_node).remove(); $(marker).remove(); editor.spaces.normalize(next_node); return function () { range.setStart(next_node, len); } } else { prev_node.textContent = prev_node.textContent + next_node.textContent; $(next_node).remove(); $(marker).remove(); editor.spaces.normalize(prev_node); return function () { range.setEnd(prev_node, len); } } } // Prev node is text node. else if (prev_node && !next_node && prev_node.nodeType == Node.TEXT_NODE) { var len = prev_node.textContent.length; if (start) { editor.spaces.normalize(prev_node); return function () { range.setStart(prev_node, len); } } else { editor.spaces.normalize(prev_node); return function () { range.setEnd(prev_node, len); } } } // Next node is text node. else if (next_node && !prev_node && next_node.nodeType == Node.TEXT_NODE) { if (start) { editor.spaces.normalize(next_node); return function () { range.setStart(next_node, 0); } } else { editor.spaces.normalize(next_node); return function () { range.setEnd(next_node, 0); } } } return false; } /** * Determine if we can do delete. */ function _canDelete () { return true; } /** * Check if selection is collapsed. */ function isCollapsed () { var rgs = ranges(); for (var i = 0; i < rgs.length; i++) { if (!rgs[i].collapsed) return false; } return true; } // From: http://www.coderexception.com/0B1B33z1NyQxUQSJ/contenteditable-div-how-can-i-determine-if-the-cursor-is-at-the-start-or-end-of-the-content function info (el) { var atStart = false; var atEnd = false; var selRange; var testRange; if (editor.win.getSelection) { var sel = editor.win.getSelection(); if (sel.rangeCount) { selRange = sel.getRangeAt(0); testRange = selRange.cloneRange(); testRange.selectNodeContents(el); testRange.setEnd(selRange.startContainer, selRange.startOffset); atStart = (testRange.toString() === ''); testRange.selectNodeContents(el); testRange.setStart(selRange.endContainer, selRange.endOffset); atEnd = (testRange.toString() === ''); } } else if (editor.doc.selection && editor.doc.selection.type != 'Control') { selRange = editor.doc.selection.createRange(); testRange = selRange.duplicate(); testRange.moveToElementText(el); testRange.setEndPoint('EndToStart', selRange); atStart = (testRange.text === ''); testRange.moveToElementText(el); testRange.setEndPoint('StartToEnd', selRange); atEnd = (testRange.text === ''); } return { atStart: atStart, atEnd: atEnd }; } /** * Check if everything is selected inside the editor. */ function isFull () { if (isCollapsed()) return false; // https://github.com/froala/wysiwyg-editor/issues/710 editor.$el.find('td, th, img').prepend('<span class="fr-mk">' + $.FE.INVISIBLE_SPACE + '</span>'); var full = false; var inf = info(editor.el); if (inf.atStart && inf.atEnd) full = true; // https://github.com/froala/wysiwyg-editor/issues/710 editor.$el.find('.fr-mk').remove(); return full; } /** * Remove HTML from inner nodes when we deal with keepFormatOnDelete option. */ function _emptyInnerNodes (node, first) { if (typeof first == 'undefined') first = true; // Remove invisible spaces. var h = $(node).html(); if (h && h.replace(/\u200b/g, '').length != h.length) $(node).html(h.replace(/\u200b/g, '')); // Loop contents. var contents = editor.node.contents(node); for (var j = 0; j < contents.length; j++) { // Remove text nodes. if (contents[j].nodeType != Node.ELEMENT_NODE) { $(contents[j]).remove(); } // Empty inner nodes further. else { // j == 0 determines if the node is the first one and we should keep format. _emptyInnerNodes(contents[j], j == 0); // There are inner nodes, ignore the current one. if (j == 0) first = false; } } // First node is a text node, so replace it with a span. if (node.nodeType == Node.TEXT_NODE) { $(node).replaceWith('<span data-first="true" data-text="true"></span>'); } // Add the first node marker so that we add selection in it later on. else if (first) { $(node).attr('data-first', true); } } /** * Process deleting nodes. */ function _processNodeDelete ($node, should_delete) { var contents = editor.node.contents($node.get(0)); // Node is TD or TH. if (['TD', 'TH'].indexOf($node.get(0).tagName) >= 0 && $node.find('.fr-marker').length == 1 && editor.node.hasClass(contents[0], 'fr-marker')) { $node.attr('data-del-cell', true); } for (var i = 0; i < contents.length; i++) { var node = contents[i]; // We found a marker. if (editor.node.hasClass(node, 'fr-marker')) { should_delete = (should_delete + 1) % 2; } else if (should_delete) { // Check if we have a marker inside it. if ($(node).find('.fr-marker').length > 0) { should_delete = _processNodeDelete($(node), should_delete); } else { // TD, TH or inner, then go further. if (['TD', 'TH'].indexOf(node.tagName) < 0 && !editor.node.hasClass(node, 'fr-inner')) { if (!editor.opts.keepFormatOnDelete || editor.$el.find('[data-first]').length > 0) { $(node).remove(); } else { _emptyInnerNodes(node); } } else if (editor.node.hasClass(node, 'fr-inner')) { if ($(node).find('.fr-inner').length == 0) { $(node).html('<br>'); } else { $(node).find('.fr-inner').filter(function () { return $(this).find('fr-inner').length == 0; }).html('<br>'); } } else { $(node).empty(); $(node).attr('data-del-cell', true); } } } else { if ($(node).find('.fr-marker').length > 0) { should_delete = _processNodeDelete($(node), should_delete); } } } return should_delete; } /** * Determine if selection is inside the editor. */ function inEditor () { try { if (!editor.$wp) return false; var range = ranges(0); var container = range.commonAncestorContainer; while (container && !editor.node.isElement(container)) { container = container.parentNode; } if (editor.node.isElement(container)) return true; return false; } catch (ex) { return false; } } /** * Remove the current selection html. */ function remove () { if (isCollapsed()) return true; save(); // Get the previous sibling normalized. var _prevSibling = function (node) { var prev_node = node.previousSibling; while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 0) { var tmp = prev_node; var prev_node = prev_node.previousSibling; $(tmp).remove(); } return prev_node; } // Get the next sibling normalized. var _nextSibling = function (node) { var next_node = node.nextSibling; while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.textContent.length == 0) { var tmp = next_node; var next_node = next_node.nextSibling; $(tmp).remove(); } return next_node; } // Normalize start markers. var start_markers = editor.$el.find('.fr-marker[data-type="true"]'); for (var i = 0; i < start_markers.length; i++) { var sm = start_markers[i]; while (!_prevSibling(sm) && !editor.node.isBlock(sm.parentNode) && !editor.$el.is(sm.parentNode)) { $(sm.parentNode).before(sm); } } // Normalize end markers. var end_markers = editor.$el.find('.fr-marker[data-type="false"]'); for (var i = 0; i < end_markers.length; i++) { var em = end_markers[i]; while (!_nextSibling(em) && !editor.node.isBlock(em.parentNode) && !editor.$el.is(em.parentNode)) { $(em.parentNode).after(em); } // Last node is empty and has a BR in it. if (em.parentNode && editor.node.isBlock(em.parentNode) && editor.node.isEmpty(em.parentNode) && !editor.$el.is(em.parentNode) && editor.opts.keepFormatOnDelete) { $(em.parentNode).after(em); } } // Check if selection can be deleted. if (_canDelete()) { _processNodeDelete(editor.$el, 0); // Look for selection marker. var $first_node = editor.$el.find('[data-first="true"]'); if ($first_node.length) { // Remove markers. editor.$el.find('.fr-marker').remove(); // Add markers in the node that we marked as the first one. $first_node .append($.FE.INVISIBLE_SPACE + $.FE.MARKERS) .removeAttr('data-first'); // Remove span with data-text if there is one. if ($first_node.attr('data-text')) { $first_node.replaceWith($first_node.html()); } } else { // Remove tables. editor.$el.find('table').filter(function () { var ok = $(this).find('[data-del-cell]').length > 0 && $(this).find('[data-del-cell]').length == $(this).find('td, th').length; return ok; }).remove(); editor.$el.find('[data-del-cell]').removeAttr('data-del-cell'); // Merge contents between markers. var start_markers = editor.$el.find('.fr-marker[data-type="true"]'); for (var i = 0; i < start_markers.length; i++) { // Get start marker. var start_marker = start_markers[i]; // Get the next node after start marker. var next_node = start_marker.nextSibling; // Get the end node. var end_marker = editor.$el.find('.fr-marker[data-type="false"][data-id="' + $(start_marker).data('id') + '"]').get(0); if (end_marker) { // Markers are next to other. if (next_node && next_node == end_marker) { // Do nothing. } else if (start_marker) { // Get the parents of the nodes. var start_parent = editor.node.blockParent(start_marker); var end_parent = editor.node.blockParent(end_marker); // https://github.com/froala/wysiwyg-editor/issues/1233 var list_start = false; var list_end = false; if (start_parent && ['UL', 'OL'].indexOf(start_parent.tagName) >= 0) { start_parent = null; list_start = true; } if (end_parent && ['UL', 'OL'].indexOf(end_parent.tagName) >= 0) { end_parent = null; list_end = true; } // Move end marker next to start marker. $(start_marker).after(end_marker); // We're in the same parent. Moving marker is enough. if (start_parent == end_parent) { } // The block parent of the start marker is the element itself. else if (start_parent == null && !list_start) { var deep_parent = editor.node.deepestParent(start_marker); // There is a parent for the marker. Move the end html to it. if (deep_parent) { $(deep_parent).after($(end_parent).html()); $(end_parent).remove(); } // There is no parent for the marker. else if ($(end_parent).parentsUntil(editor.$el, 'table').length == 0) { $(start_marker).next().after($(end_parent).html()); $(end_parent).remove(); } } // End marker is inside element. We don't merge in table. else if (end_parent == null && !list_end && $(start_parent).parentsUntil(editor.$el, 'table').length == 0) { // Get the node that has a next sibling. var next_node = start_parent; while (!next_node.nextSibling && next_node.parentNode != editor.el) { next_node = next_node.parentNode; } next_node = next_node.nextSibling; // Join HTML inside the start node. while (next_node && next_node.tagName != 'BR') { var tmp_node = next_node.nextSibling; $(start_parent).append(next_node); next_node = tmp_node; } if (next_node && next_node.tagName == 'BR') { $(next_node).remove(); } } // Join end block with start block. else if (start_parent && end_parent && $(start_parent).parentsUntil(editor.$el, 'table').length == 0 && $(end_parent).parentsUntil(editor.$el, 'table').length == 0) { $(start_parent).append($(end_parent).html()); $(end_parent).remove(); } } } else { end_marker = $(start_marker).clone().attr('data-type', false); $(start_marker).after(end_marker); } } } } if (!editor.opts.keepFormatOnDelete) { editor.html.fillEmptyBlocks(); } editor.html.cleanEmptyTags(true); editor.clean.lists(); editor.spaces.normalize(); // https://github.com/froala/wysiwyg-editor/issues/1379 var last_marker = editor.$el.find('.fr-marker:last').get(0); var first_marker = editor.$el.find('.fr-marker:first').get(0); if (!last_marker.nextSibling && first_marker.previousSibling && first_marker.previousSibling.tagName == 'BR' && editor.node.isElement(last_marker.parentNode) && editor.node.isElement(first_marker.parentNode)) { editor.$el.append('<br>'); } restore(); } function setAtStart (node) { if (!node || node.getElementsByClassName('fr-marker').length > 0) return false; var child = node.firstChild; while (child && editor.node.isBlock(child)) { node = child; child = child.firstChild; } node.innerHTML = $.FE.MARKERS + node.innerHTML; } function setAtEnd (node) { if (!node || node.getElementsByClassName('fr-marker').length > 0) return false; var child = node.lastChild; while (child && editor.node.isBlock(child)) { node = child; child = child.lastChild; } node.innerHTML = node.innerHTML + $.FE.MARKERS; } function setBefore (node, use_current_node) { if (typeof use_current_node == 'undefined') use_current_node = true; // Check if there is any previous sibling by skipping the empty text ones. var prev_node = node.previousSibling; while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 0) { prev_node = prev_node.previousSibling; } // There is a previous node. if (prev_node) { // Previous node is block so set the focus at the end of it. if (editor.node.isBlock(prev_node)) { setAtEnd(prev_node); } // Previous node is BR, so place markers before it. else if (prev_node.tagName == 'BR') { $(prev_node).before($.FE.MARKERS); } // Just place marker. else { $(prev_node).after($.FE.MARKERS); } return true; } // Use current node. else if (use_current_node) { // Current node is block, set selection at start. if (editor.node.isBlock(node)) { setAtStart(node); } // Just place markers. else { $(node).before($.FE.MARKERS); } return true; } else { return false; } } function setAfter (node, use_current_node) { if (typeof use_current_node == 'undefined') use_current_node = true; // Check if there is any previous sibling by skipping the empty text ones. var next_node = node.nextSibling; while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.textContent.length == 0) { next_node = next_node.nextSibling; } // There is a next node. if (next_node) { // Next node is block so set the focus at the end of it. if (editor.node.isBlock(next_node)) { setAtStart(next_node); } // Just place marker. else { $(next_node).before($.FE.MARKERS); } return true; } // Use current node. else if (use_current_node) { // Current node is block, set selection at end. if (editor.node.isBlock(node)) { setAtEnd(node); } // Just place markers. else { $(node).after($.FE.MARKERS); } return true; } else { return false; } } return { text: text, get: get, ranges: ranges, clear: clear, element: element, endElement: endElement, save: save, restore: restore, isCollapsed: isCollapsed, isFull: isFull, inEditor: inEditor, remove: remove, blocks: blocks, info: info, setAtEnd: setAtEnd, setAtStart: setAtStart, setBefore: setBefore, setAfter: setAfter, rangeElement: rangeElement } }; // Extend defaults. $.extend($.FE.DEFAULTS, { // Tags that describe head from HEAD http://www.w3schools.com/html/html_head.asp. htmlAllowedTags: ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'queue', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'style', 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'], htmlRemoveTags: ['script', 'style'], htmlAllowedAttrs: ['accept', 'accept-charset', 'accesskey', 'action', 'align', 'allowfullscreen', 'allowtransparency', 'alt', 'async', 'autocomplete', 'autofocus', 'autoplay', 'autosave', 'background', 'bgcolor', 'border', 'charset', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'data', 'data-.*', 'datetime', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable', 'dropzone', 'enctype', 'for', 'form', 'formaction', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang', 'language', 'list', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'mozallowfullscreen', 'multiple', 'name', 'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'reversed', 'rows', 'rowspan', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'span', 'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'summary', 'spellcheck', 'style', 'tabindex', 'target', 'title', 'type', 'translate', 'usemap', 'value', 'valign', 'webkitallowfullscreen', 'width', 'wrap'], htmlAllowComments: true, htmlUntouched: false, fullPage: false // Will also turn iframe on. }); $.FE.HTML5Map = { 'B': 'STRONG', 'I': 'EM', 'STRIKE': 'S' }, $.FE.MODULES.clean = function (editor) { var $iframe, body; var allowedTagsRE, removeTagsRE, allowedAttrsRE; function _removeInvisible (node) { if (node.nodeType == Node.ELEMENT_NODE && node.getAttribute('class') && node.getAttribute('class').indexOf('fr-marker') >= 0) return false; // Get node contents. var contents = editor.node.contents(node); var markers = []; var i; // Loop through contents. for (i = 0; i < contents.length; i++) { // If node is not void. if (contents[i].nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(contents[i])) { // There are invisible spaces. if (contents[i].textContent.replace(/\u200b/g, '').length != contents[i].textContent.length) { // Do remove invisible spaces. _removeInvisible(contents[i]); } } // If node is text node, replace invisible spaces. else if (contents[i].nodeType == Node.TEXT_NODE) { contents[i].textContent = contents[i].textContent.replace(/\u200b/g, '').replace(/&/g, '&'); } } // Reasess contents after cleaning invisible spaces. if (node.nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(node)) { node.normalize(); contents = editor.node.contents(node); markers = node.querySelectorAll('.fr-marker'); // All we have left are markers. if (contents.length - markers.length == 0) { // Make sure contents are all markers. for (i = 0; i < contents.length; i++) { if ((contents[i].getAttribute('class') || '').indexOf('fr-marker') < 0) { return false; } } for (i = 0; i < markers.length; i++) { node.parentNode.insertBefore(markers[i].cloneNode(true), node); } node.parentNode.removeChild(node); return false; } } } function _toHTML (el) { if (el.nodeType == Node.COMMENT_NODE) return '<!--' + el.nodeValue + '-->'; if (el.nodeType == Node.TEXT_NODE) { return el.textContent.replace(/\&/g, '&').replace(/\</g, '<').replace(/\>/g, '>').replace(/\u00A0/g, ' '); } if (el.nodeType != Node.ELEMENT_NODE) return el.outerHTML; if (el.nodeType == Node.ELEMENT_NODE && ['STYLE', 'SCRIPT'].indexOf(el.tagName) >= 0) return el.outerHTML; if (el.nodeType == Node.ELEMENT_NODE && el.tagName == 'svg') { var temp = document.createElement('div'); var node_clone = el.cloneNode(true); temp.appendChild(node_clone); return temp.innerHTML; } if (el.tagName == 'IFRAME') return el.outerHTML; var contents = el.childNodes; if (contents.length === 0) return el.outerHTML; var str = ''; for (var i = 0; i < contents.length; i++) { str += _toHTML(contents[i]); } return editor.node.openTagString(el) + str + editor.node.closeTagString(el); } var scripts = []; function _encode (dirty_html) { // Replace script tag with comments. scripts = []; dirty_html = dirty_html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, function (str) { scripts.push(str); return '[FROALA.EDITOR.SCRIPT ' + (scripts.length - 1) + ']'; }); dirty_html = dirty_html.replace(/<img((?:[\w\W]*?)) src="/g, '<img$1 data-fr-src="'); return dirty_html; } function _decode (dirty_html) { // Replace script comments with the original script. dirty_html = dirty_html.replace(/\[FROALA\.EDITOR\.SCRIPT ([\d]*)\]/gi, function (str, a1) { if (editor.opts.htmlRemoveTags.indexOf('script') >= 0) { return ''; } else { return scripts[parseInt(a1, 10)]; } }); dirty_html = dirty_html.replace(/<img((?:[\w\W]*?)) data-fr-src="/g, '<img$1 src="'); return dirty_html; } function _cleanAttrs (attrs) { var nm; for (nm in attrs) { if (attrs.hasOwnProperty(nm)) { if (!nm.match(allowedAttrsRE)) { delete attrs[nm]; } } } var str = ''; var keys = Object.keys(attrs).sort(); for (var i = 0; i < keys.length; i++) { nm = keys[i]; // Make sure we don't break any HTML. if (attrs[nm].indexOf('"') < 0) { str += ' ' + nm + '="' + attrs[nm] + '"'; } else { str += ' ' + nm + '=\'' + attrs[nm] + '\''; } } return str; } function _rebuild (body_html, head_html, original_html) { if (editor.opts.fullPage) { // Get DOCTYPE. var doctype = editor.html.extractDoctype(original_html); // Get HTML attributes. var html_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'html')); // Get HEAD data. head_html = head_html == null ? editor.html.extractNode(original_html, 'head') || '<title></title>' : head_html; var head_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'head')); // Get BODY attributes. var body_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'body')); return doctype + '<html' + html_attrs + '><head' + head_attrs + '>' + head_html + '</head><body' + body_attrs + '>' + body_html + '</body></html>'; } return body_html; } function _process (html, func) { var $el = $('<div>' + html + '</div>'); var new_html = ''; if ($el) { var els = editor.node.contents($el.get(0)); for (var i = 0; i < els.length; i++) { func(els[i]); } els = editor.node.contents($el.get(0)); for (var i = 0; i < els.length; i++) { new_html += _toHTML(els[i]); } } return new_html; } function exec (html, func, parse_head) { html = _encode(html); var b_html = html; var h_html = null; if (editor.opts.fullPage) { // Get BODY data. var b_html = (editor.html.extractNode(html, 'body') || (html.indexOf('<body') >= 0 ? '' : html)); if (parse_head) { h_html = (editor.html.extractNode(html, 'head') || ''); } } b_html = _process(b_html, func); if (h_html) h_html = _process(h_html, func); var new_html = _rebuild(b_html, h_html, html); return _decode(new_html); } function invisibleSpaces (dirty_html) { if (dirty_html.replace(/\u200b/g, '').length == dirty_html.length) return dirty_html; return editor.clean.exec(dirty_html, _removeInvisible); } function toHTML5 () { var els = editor.el.querySelectorAll(Object.keys($.FE.HTML5Map).join(',')); if (els.length) { var sel_saved = false; if (!editor.el.querySelector('.fr-marker')) { editor.selection.save(); sel_saved = true; } for (var i = 0; i < els.length; i++) { if (editor.node.attributes(els[i]) === '') { $(els[i]).replaceWith('<' + $.FE.HTML5Map[els[i].tagName] + '>' + els[i].innerHTML + '</' + $.FE.HTML5Map[els[i].tagName] + '>'); } } if (sel_saved) { editor.selection.restore(); } } } function _node (node) { // Skip when we're dealing with markers. if (node.tagName == 'SPAN' && (node.getAttribute('class') || '').indexOf('fr-marker') >=0) return false; if (node.tagName == 'PRE') _cleanPre(node); if (node.nodeType == Node.ELEMENT_NODE) { if (node.getAttribute('data-fr-src')) node.setAttribute('data-fr-src', editor.helpers.sanitizeURL(node.getAttribute('data-fr-src'))); if (node.getAttribute('href')) node.setAttribute('href', editor.helpers.sanitizeURL(node.getAttribute('href'))); if (['TABLE', 'TBODY', 'TFOOT', 'TR'].indexOf(node.tagName) >= 0) { node.innerHTML = node.innerHTML.trim(); } } // Remove local images if option they are not allowed. if (!editor.opts.pasteAllowLocalImages && node.nodeType == Node.ELEMENT_NODE && node.tagName == 'IMG' && node.getAttribute('data-fr-src') && node.getAttribute('data-fr-src').indexOf('file://') == 0) { node.parentNode.removeChild(node); return false; } if (node.nodeType == Node.ELEMENT_NODE && $.FE.HTML5Map[node.tagName] && editor.node.attributes(node) === '') { var tg = $.FE.HTML5Map[node.tagName]; var new_node = '<' + tg + '>' + node.innerHTML + '</' + tg + '>'; node.insertAdjacentHTML('beforebegin', new_node); node = node.previousSibling; node.parentNode.removeChild(node.nextSibling); } if (!editor.opts.htmlAllowComments && node.nodeType == Node.COMMENT_NODE) { // Do not remove FROALA.EDITOR comments. if (node.data.indexOf('[FROALA.EDITOR') !== 0) { node.parentNode.removeChild(node); } } // Remove completely tags in denied tags. else if (node.tagName && node.tagName.match(removeTagsRE)) { node.parentNode.removeChild(node); } // Unwrap tags not in allowed tags. else if (node.tagName && !node.tagName.match(allowedTagsRE)) { node.outerHTML = node.innerHTML; } // Check denied attributes. else { var attrs = node.attributes; if (attrs) { for (var i = attrs.length - 1; i >= 0; i--) { var attr = attrs[i]; if (!attr.nodeName.match(allowedAttrsRE)) { node.removeAttribute(attr.nodeName); } } } } } function _run (node) { var contents = editor.node.contents(node); for (var i = 0; i < contents.length; i++) { if (contents[i].nodeType != Node.TEXT_NODE) { _run(contents[i]); } } _node(node); } /** * Clean pre. */ function _cleanPre (pre) { var content = pre.innerHTML; if (content.indexOf('\n') >= 0) { pre.innerHTML = content.replace(/\n/g, '<br>'); } } /** * Clean the html input. */ var scripts = []; function html (dirty_html, denied_tags, denied_attrs, full_page) { if (typeof denied_tags == 'undefined') denied_tags = []; if (typeof denied_attrs == 'undefined') denied_attrs = []; if (typeof full_page == 'undefined') full_page = false; // Strip tabs. dirty_html = dirty_html.replace(/\u0009/g, ''); // Empty spaces after BR always collapse. dirty_html = dirty_html.replace(/<br> */g, '<br>'); // Build the allowed tags array. var allowed_tags = $.merge([], editor.opts.htmlAllowedTags); var i; for (i = 0; i < denied_tags.length; i++) { if (allowed_tags.indexOf(denied_tags[i]) >= 0) { allowed_tags.splice(allowed_tags.indexOf(denied_tags[i]), 1); } } // Build the allowed attrs array. var allowed_attrs = $.merge([], editor.opts.htmlAllowedAttrs); for (i = 0; i < denied_attrs.length; i++) { if (allowed_attrs.indexOf(denied_attrs[i]) >= 0) { allowed_attrs.splice(allowed_attrs.indexOf(denied_attrs[i]), 1); } } // We should allow data-fr. allowed_attrs.push('data-fr-.*'); allowed_attrs.push('fr-.*'); // Generate cleaning RegEx. allowedTagsRE = new RegExp('^' + allowed_tags.join('$|^') + '$', 'gi'); allowedAttrsRE = new RegExp('^' + allowed_attrs.join('$|^') + '$', 'gi'); removeTagsRE = new RegExp('^' + editor.opts.htmlRemoveTags.join('$|^') + '$', 'gi'); dirty_html = exec(dirty_html, _run, true); return dirty_html; } /** * Clean quotes. */ function quotes () { // Join quotes. var sibling_quotes = editor.el.querySelectorAll('blockquote + blockquote'); for (var k = 0; k < sibling_quotes.length; k++) { var quote = sibling_quotes[k]; if (editor.node.attributes(quote) == editor.node.attributes(quote.previousSibling)) { $(quote).prev().append($(quote).html()); $(quote).remove(); } } } function _tablesWrapTHEAD () { var trs = editor.el.querySelectorAll('tr'); // Make sure the TH lives inside thead. for (var i = 0; i < trs.length; i++) { // Search for th inside tr. var children = trs[i].children; var ok = true; for (var j = 0; j < children.length; j++) { if (children[j].tagName != 'TH') { ok = false; break; } } // If there is something else than TH. if (ok == false || children.length == 0) continue; var tr = trs[i]; while (tr && tr.tagName != 'TABLE' && tr.tagName != 'THEAD') { tr = tr.parentNode; } var thead = tr; if (thead.tagName != 'THEAD') { thead = editor.doc.createElement('THEAD'); tr.insertBefore(thead, tr.firstChild); } thead.appendChild(trs[i]); } } function _tablesRemovePFromCell () { // Remove P from TH and TH. var default_tag = editor.html.defaultTag(); if (default_tag) { var nodes = editor.el.querySelectorAll('td > ' + default_tag + ', th > ' + default_tag); for (var i = 0; i < nodes.length; i++) { if (editor.node.attributes(nodes[i]) === '') { $(nodes[i]).replaceWith(nodes[i].innerHTML + '<br>'); } } } } /** * Clean tables. */ function tables () { _tablesWrapTHEAD(); _tablesRemovePFromCell(); } function _listsWrapMissplacedLI () { // Find missplaced list items. var lis = []; var filterListItem = function (li) { return !editor.node.isList(li.parentNode); }; do { if (lis.length) { var li = lis[0]; var ul = editor.doc.createElement('ul'); li.parentNode.insertBefore(ul, li); do { var tmp = li; li = li.nextSibling; ul.appendChild(tmp); } while (li && li.tagName == 'LI'); } lis = []; var li_sel = editor.el.querySelectorAll('li'); for (var i = 0; i < li_sel.length; i++) { if (filterListItem(li_sel[i])) lis.push(li_sel[i]); } } while (lis.length > 0); } function _listsJoinSiblings () { // Join lists. var sibling_lists = editor.el.querySelectorAll('ol + ol, ul + ul'); for (var k = 0; k < sibling_lists.length; k++) { var list = sibling_lists[k]; if (editor.node.isList(list.previousSibling) && editor.node.openTagString(list) == editor.node.openTagString(list.previousSibling)) { var childs = editor.node.contents(list); for (var i = 0; i < childs.length; i++) { list.previousSibling.appendChild(childs[i]); } list.parentNode.removeChild(list); } } } function _listsRemoveEmpty () { // Remove empty lists. var do_remove; var removeEmptyList = function (lst) { if (!lst.querySelector('LI')) { do_remove = true; lst.parentNode.removeChild(lst); } }; do { do_remove = false; // Remove empty li. var empty_lis = editor.el.querySelectorAll('li:empty'); for (var i = 0; i < empty_lis.length; i++) { empty_lis[i].parentNode.removeChild(empty_lis[i]); } // Remove empty ul and ol. var remaining_lists = editor.el.querySelectorAll('ul, ol'); for (var i = 0; i < remaining_lists.length; i++) { removeEmptyList(remaining_lists[i]); } } while (do_remove === true); } function _listsWrapLists () { // Do not allow list directly inside another list. var direct_lists = editor.el.querySelectorAll('ul > ul, ol > ol, ul > ol, ol > ul'); for (var i = 0; i < direct_lists.length; i++) { var list = direct_lists[i]; var prev_li = list.previousSibling; if (prev_li) { if (prev_li.tagName == 'LI') { prev_li.appendChild(list); } else { $(list).wrap('<li></li>'); } } } } function _listsNoTagAfterNested () { // Check if nested lists don't have HTML after them. var nested_lists = editor.el.querySelectorAll('li > ul, li > ol'); for (var i = 0; i < nested_lists.length; i++) { var lst = nested_lists[i]; if (lst.nextSibling) { var node = lst.nextSibling; var $new_li = $('<li>'); $(lst.parentNode).after($new_li); do { var tmp = node; node = node.nextSibling; $new_li.append(tmp); } while (node); } } } function _listsTypeInNested () { // Make sure we can type in nested list. var nested_lists = editor.el.querySelectorAll('li > ul, li > ol'); for (var i = 0; i < nested_lists.length; i++) { var lst = nested_lists[i]; // List is the first in the LI. if (editor.node.isFirstSibling(lst)) { $(lst).before('<br/>'); } // Make sure we don't leave BR before list. else if (lst.previousSibling && lst.previousSibling.tagName == 'BR') { var prev_node = lst.previousSibling.previousSibling; // Skip markers. while (prev_node && editor.node.hasClass(prev_node, 'fr-marker')) { prev_node = prev_node.previousSibling; } // Remove BR only if there is something else than BR. if (prev_node && prev_node.tagName != 'BR') { $(lst.previousSibling).remove(); } } } } function _listsRemoveEmptyLI () { // Remove empty li. var empty_lis = editor.el.querySelectorAll('li:empty'); for (var i = 0; i < empty_lis.length; i++) { $(empty_lis[i]).remove(); } } function _listsFindMissplacedText () { var lists = editor.el.querySelectorAll('ul, ol'); for (var i = 0; i < lists.length; i++) { var contents = editor.node.contents(lists[i]); var $li = null; for (var j = contents.length - 1; j >=0 ; j--) { if (contents[j].tagName != 'LI') { if (!$li) { $li = $('<li>'); $li.insertBefore(contents[j]); } $li.prepend(contents[j]); } else { $li = null; } } } } /** * Clean lists. */ function lists () { _listsWrapMissplacedLI(); _listsJoinSiblings(); _listsRemoveEmpty(); _listsWrapLists(); _listsNoTagAfterNested(); _listsTypeInNested(); _listsFindMissplacedText(); _listsRemoveEmptyLI(); } /** * Initialize */ function _init () { // If fullPage is on allow head and title. if (editor.opts.fullPage) { $.merge(editor.opts.htmlAllowedTags, ['head', 'title', 'style', 'link', 'base', 'body', 'html', 'meta']); } } return { _init: _init, html: html, toHTML5: toHTML5, tables: tables, lists: lists, quotes: quotes, invisibleSpaces: invisibleSpaces, exec: exec } }; $.FE.MODULES.spaces = function (editor) { function _normalizeNode (node, browser_way) { var p_node = node.previousSibling; var n_node = node.nextSibling; var txt = node.textContent; var parent_node = node.parentNode; if (browser_way) { txt = txt.replace(/[\f\n\r\t\v ]{2,}/g, ' '); if ((!n_node || n_node.tagName === 'BR' || editor.node.isBlock(n_node)) && editor.node.isBlock(parent_node)) { txt = txt.replace(/[\f\n\r\t\v ]{1,}$/g, ''); } if ((!p_node || p_node.tagName === 'BR' || editor.node.isBlock(p_node)) && editor.node.isBlock(parent_node)) { txt = txt.replace(/^[\f\n\r\t\v ]{1,}/g, ''); } } // Convert all non breaking to breaking spaces. txt = txt.replace(new RegExp($.FE.UNICODE_NBSP, 'g'), ' '); var new_text = ''; for (var t = 0; t < txt.length; t++) { if (txt.charCodeAt(t) == 32 && (t === 0 || new_text.charCodeAt(t - 1) == 32)) { new_text += $.FE.UNICODE_NBSP; } else { new_text += txt[t]; } } // Ending spaces should be NBSP or spaces before block tags. if (!n_node || editor.node.isBlock(n_node) || (n_node.nodeType == Node.ELEMENT_NODE && editor.win.getComputedStyle(n_node) && editor.win.getComputedStyle(n_node).display == 'block')) { new_text = new_text.replace(/ $/, $.FE.UNICODE_NBSP); } // Previous sibling is not void or block. if (p_node && !editor.node.isVoid(p_node) && !editor.node.isBlock(p_node)) { new_text = new_text.replace(/^\u00A0([^ $])/, ' $1'); // https://github.com/froala/wysiwyg-editor/issues/1355. if (new_text.length === 1 && new_text.charCodeAt(0) === 160 && n_node && !editor.node.isVoid(n_node) && !editor.node.isBlock(n_node)) { new_text = ' '; } } // Convert middle nbsp to spaces. new_text = new_text.replace(/([^ \u00A0])\u00A0([^ \u00A0])/g, '$1 $2'); if (node.textContent != new_text) { node.textContent = new_text; } } function normalize (el, browser_way) { if (typeof el == 'undefined' || !el) el = editor.el; if (typeof browser_way == 'undefined') browser_way = false; // Ignore contenteditable. if (el.getAttribute && el.getAttribute('contenteditable') == 'false') return; if (el.nodeType == Node.TEXT_NODE) { _normalizeNode(el, browser_way) } else if (el.nodeType == Node.ELEMENT_NODE) { var walker = editor.doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, editor.node.filter(function (node) { return node.textContent.match(/([ \u00A0\f\n\r\t\v]{2,})|(^[ \u00A0\f\n\r\t\v]{1,})|([ \u00A0\f\n\r\t\v]{1,}$)/g) != null && !editor.node.hasClass(node.parentNode, 'fr-marker'); }), false); while (walker.nextNode()) { _normalizeNode(walker.currentNode, browser_way); } } } function normalizeAroundCursor () { var nodes = []; var markers = editor.el.querySelectorAll('.fr-marker'); // Get the deep parent node of each marker. for (var i = 0; i < markers.length; i++) { var node = null; var p_node = editor.node.blockParent(markers[i]); if (p_node) { node = p_node; } else { node = markers[i]; } var next_node = node.nextSibling; var prev_node = node.previousSibling; while (next_node && next_node.tagName == 'BR') next_node = next_node.nextSibling; while (prev_node && prev_node.tagName == 'BR') prev_node = prev_node.previousSibling; // Push current node, prev and next one. if (node && nodes.indexOf(node) < 0) nodes.push(node); if (prev_node && nodes.indexOf(prev_node) < 0) nodes.push(prev_node); if (next_node && nodes.indexOf(next_node) < 0) nodes.push(next_node); } for (var j = 0; j < nodes.length; j++) { normalize(nodes[j]); } } return { normalize: normalize, normalizeAroundCursor: normalizeAroundCursor } }; $.FE.UNICODE_NBSP = String.fromCharCode(160); // Void Elements http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements $.FE.VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']; $.FE.BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'ul', 'video']; // Extend defaults. $.extend($.FE.DEFAULTS, { htmlAllowedEmptyTags: ['textarea', 'a', 'iframe', 'object', 'video', 'style', 'script', '.fa', '.fr-emoticon'], htmlDoNotWrapTags: ['script', 'style'], htmlSimpleAmpersand: false, htmlIgnoreCSSProperties: [] }); $.FE.MODULES.html = function (editor) { /** * Determine the default block tag. */ function defaultTag () { if (editor.opts.enter == $.FE.ENTER_P) return 'p'; if (editor.opts.enter == $.FE.ENTER_DIV) return 'div'; if (editor.opts.enter == $.FE.ENTER_BR) return null; } /** * Get the empty blocs. */ function emptyBlocks (around_markers) { var empty_blocks = []; // Block tag elements. var els = []; if (around_markers) { var markers = editor.el.querySelectorAll('.fr-marker'); for (var i = 0; i < markers.length; i++) { var p_node = editor.node.blockParent(markers[i]) || markers[i]; if (p_node) { var next_node = p_node.nextSibling; var prev_node = p_node.previousSibling; // Push current node, prev and next one. if (p_node && els.indexOf(p_node) < 0 && editor.node.isBlock(p_node)) els.push(p_node); if (prev_node && editor.node.isBlock(prev_node) && els.indexOf(prev_node) < 0) els.push(prev_node); if (next_node && editor.node.isBlock(next_node) && els.indexOf(next_node) < 0) els.push(next_node); } } } else { els = editor.el.querySelectorAll(blockTagsQuery()); } var qr = blockTagsQuery(); qr += ',' + $.FE.VOID_ELEMENTS.join(','); qr += ',' + editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)'; // Check if there are empty block tags with markers. for (var i = els.length - 1; i >= 0; i--) { // If the block tag has text content, ignore it. if (els[i].textContent && els[i].textContent.replace(/\u200B|\n/g, '').length > 0) continue; if (els[i].querySelectorAll(qr).length > 0) continue; // We're checking text from here on. var contents = editor.node.contents(els[i]); var found = false; for (var j = 0; j < contents.length; j++) { if (contents[j].nodeType == Node.COMMENT_NODE) continue; // Text node that is not empty. if (contents[j].textContent && contents[j].textContent.replace(/\u200B|\n/g, '').length > 0) { found = true; break; } } // Make sure we don't add TABLE and TD at the same time for instance. if (!found) empty_blocks.push(els[i]); } return empty_blocks; } /** * Create jQuery query for empty block tags. */ function emptyBlockTagsQuery () { return $.FE.BLOCK_TAGS.join(':empty, ') + ':empty'; } /** * Create jQuery query for selecting block tags. */ function blockTagsQuery () { return $.FE.BLOCK_TAGS.join(', '); } /** * Remove empty elements that are not VOID elements. */ function cleanEmptyTags (remove_blocks) { var els = $.merge([], $.FE.VOID_ELEMENTS); els = $.merge(els, editor.opts.htmlAllowedEmptyTags); if (typeof remove_blocks == 'undefined') els = $.merge(els, $.FE.BLOCK_TAGS); var elms; var ok; elms = editor.el.querySelectorAll('*:empty:not(' + els.join('):not(') + '):not(.fr-marker)'); do { ok = false; // Remove those elements that have no attributes. for (var i = 0; i < elms.length; i++) { if (elms[i].attributes.length === 0 || typeof elms[i].getAttribute('href') !== 'undefined') { elms[i].parentNode.removeChild(elms[i]); ok = true; } } elms = editor.el.querySelectorAll('*:empty:not(' + els.join('):not(') + '):not(.fr-marker)'); } while (elms.length && ok); } /** * Wrap the content inside the element passed as argument. */ function _wrapElement(el, temp) { var default_tag = defaultTag(); if (temp) default_tag = 'div'; if (default_tag) { // Rewrite the entire content. var main_doc = editor.doc.createDocumentFragment(); // Anchor. var anchor = null; // If we found anything inside the current anchor. var found = false; var node = el.firstChild; // Loop through contents. while (node) { var next_node = node.nextSibling; // Current node is a block node. // Or it is a do not wrap node and not a fr-marker. if (node.nodeType == Node.ELEMENT_NODE && (editor.node.isBlock(node) || (editor.opts.htmlDoNotWrapTags.indexOf(node.tagName.toLowerCase()) >= 0 && !editor.node.hasClass(node, 'fr-marker')))) { anchor = null; main_doc.appendChild(node); } // Other node types than element and text. else if (node.nodeType != Node.ELEMENT_NODE && node.nodeType != Node.TEXT_NODE) { anchor = null; main_doc.appendChild(node); } // Current node is BR. else if (node.tagName == 'BR') { // There is no anchor. if (anchor == null) { anchor = editor.doc.createElement(default_tag); if (temp) anchor.setAttribute('data-empty', true); anchor.appendChild(node); main_doc.appendChild(anchor); } // There is anchor. Just remove BR. else { // There is nothing else except markers and BR inside the new formed tag. if (found === false) { anchor.appendChild(editor.doc.createElement('br')); anchor.setAttribute('data-empty', true); } } anchor = null; } // Text node or other node type. else { var txt = node.textContent; // Node is not empty. if (!(node.nodeType == Node.TEXT_NODE && txt.replace(/\n/g, '').replace(/(^ *)|( *$)/g, '').length == 0)) { // No anchor. if (anchor == null) { anchor = editor.doc.createElement(default_tag); if (temp) anchor.setAttribute('class', 'fr-temp-div'); main_doc.appendChild(anchor); found = false; } // Add node to anchor. anchor.appendChild(node); // Check if maybe we have a non empty node. if (!found && (!editor.node.hasClass(node, 'fr-marker') && !(node.nodeType == Node.TEXT_NODE && txt.replace(/ /g, '').length === 0))) { found = true; } } // Else skip the node because it's empty. } node = next_node; } el.innerHTML = ''; el.appendChild(main_doc); } } function _wrapElements (els, temp) { for (var i = 0; i < els.length; i++) { _wrapElement(els[i], temp); } } /** * Wrap the direct content inside the default block tag. */ function _wrap (temp, tables, blockquote, inner) { if (!editor.$wp) return false; if (typeof temp == 'undefined') temp = false; if (typeof tables == 'undefined') tables = false; if (typeof blockquote == 'undefined') blockquote = false; if (typeof inner == 'undefined') inner = false; // Wrap element. _wrapElement(editor.el, temp); if (inner) { _wrapElements(editor.el.querySelectorAll('.fr-inner'), temp); } // Wrap table contents. if (tables) { _wrapElements(editor.el.querySelectorAll('td, th'), temp); } // Wrap table contents. if (blockquote) { _wrapElements(editor.el.querySelectorAll('blockquote'), temp); } } /** * Unwrap temporary divs. */ function unwrap () { editor.$el.find('div.fr-temp-div').each(function () { if ($(this).data('empty') || this.parentNode.tagName == 'LI' || (editor.node.isBlock(this.nextSibling) && !$(this.nextSibling).hasClass('fr-temp-div'))) { $(this).replaceWith($(this).html()); } else { $(this).replaceWith($(this).html() + '<br>'); } }); // Remove temp class from other blocks. editor.$el.find('.fr-temp-div').removeClass('fr-temp-div').filter(function () { return $(this).attr('class') == ''; }).removeAttr('class'); } /** * Add BR inside empty elements. */ function fillEmptyBlocks (around_markers) { var blocks = emptyBlocks(around_markers); for (var i = 0; i < blocks.length; i++) { var block = blocks[i]; if (block.getAttribute('contenteditable') != "false" && !block.querySelector(editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)') && !editor.node.isVoid(block)) { if (block.tagName != 'TABLE' && block.tagName != 'TBODY' && block.tagName != 'TR') block.appendChild(editor.doc.createElement('br')); } } // Fix for https://github.com/froala/wysiwyg-editor/issues/1166#issuecomment-204549406. if (editor.browser.msie && editor.opts.enter == $.FE.ENTER_BR) { var contents = editor.node.contents(editor.el); if (contents.length && contents[contents.length - 1].nodeType == Node.TEXT_NODE) { editor.$el.append('<br>'); } } } /** * Get the blocks inside the editable area. */ function blocks () { return editor.$el.get(0).querySelectorAll(blockTagsQuery()); } /** * Clean the blank spaces between the block tags. */ function cleanBlankSpaces (el) { if (typeof el == 'undefined') el = editor.el; if (el && ['SCRIPT', 'STYLE', 'PRE'].indexOf(el.tagName) >= 0) return false; var walker = editor.doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, editor.node.filter(function (node) { return node.textContent.match(/([ \n]{2,})|(^[ \n]{1,})|([ \n]{1,}$)/g) != null; }), false); while (walker.nextNode()) { var node = walker.currentNode; var is_block_or_element = editor.node.isBlock(node.parentNode) || editor.node.isElement(node.parentNode); // Remove middle spaces. // Replace new lines with spaces. // Replace begin/end spaces. var txt = node.textContent .replace(/(?!^)( ){2,}(?!$)/g, ' ') .replace(/\n/g, ' ') .replace(/^[ ]{2,}/g, ' ') .replace(/[ ]{2,}$/g, ' '); if (is_block_or_element) { var p_node = node.previousSibling; var n_node = node.nextSibling; if (p_node && n_node && txt == ' ') { if (editor.node.isBlock(p_node) && editor.node.isBlock(n_node)) { txt = ''; } else { txt = "\n"; } } else { // No previous siblings. if (!p_node) txt = txt.replace(/^ */,''); // No next siblings. if (!n_node) txt = txt.replace(/ *$/,''); } } node.textContent = txt; } } function _isBlock (node) { return node && (editor.node.isBlock(node) || ['STYLE', 'SCRIPT', 'HEAD', 'BR', 'HR'].indexOf(node.tagName) >= 0 || node.nodeType == Node.COMMENT_NODE); } /** * Extract a specific match for a RegEx. */ function _extractMatch (html, re, id) { var reg_exp = new RegExp(re, 'gi'); var matches = reg_exp.exec(html); if (matches) { return matches[id]; } return null; } /** * Create new doctype. */ function _newDoctype (string, doc) { var matches = string.match(/<!DOCTYPE ?([^ ]*) ?([^ ]*) ?"?([^"]*)"? ?"?([^"]*)"?>/i); if (matches) { return doc.implementation.createDocumentType( matches[1], matches[3], matches[4] ) } else { return doc.implementation.createDocumentType('html'); } } /** * Get string doctype of a document. */ function getDoctype (doc) { var node = doc.doctype; var doctype = '<!DOCTYPE html>'; if (node) { doctype = '<!DOCTYPE ' + node.name + (node.publicId ? ' PUBLIC "' + node.publicId + '"' : '') + (!node.publicId && node.systemId ? ' SYSTEM' : '') + (node.systemId ? ' "' + node.systemId + '"' : '') + '>'; } return doctype; } function _processBR (br, store_selection) { var parent_node = br.parentNode; if (parent_node && (editor.node.isBlock(parent_node) || editor.node.isElement(parent_node)) && ['TD', 'TH'].indexOf(parent_node.tagName) < 0) { var prev_node = br.previousSibling; var next_node = br.nextSibling; // Previous node. // Previous node is not BR. // Previoues node is not block tag. // No next node. // Parent node has text. // Previous node has text. if (prev_node && parent_node && prev_node.tagName != 'BR' && !editor.node.isBlock(prev_node) && !next_node && parent_node.textContent.replace(/\u200B/g, '').length > 0 && prev_node.textContent.length > 0 && !editor.node.hasClass(prev_node, 'fr-marker')) { // Fix for https://github.com/froala/wysiwyg-editor/issues/1166#issuecomment-204549406. if (!(editor.el == parent_node && !next_node && editor.opts.enter == $.FE.ENTER_BR && editor.browser.msie)) { if (store_selection) editor.selection.save(); br.parentNode.removeChild(br); if (store_selection) editor.selection.restore(); } } } } function _brsAroundSelection () { var node = editor.selection.element(); var p_node; if (editor.node.isBlock(node)) { p_node = node; } else { p_node = editor.node.blockParent(node); } var els = []; if (p_node) { var next_node = p_node.nextSibling; var prev_node = p_node.previousSibling; // Push current node, prev and next one. if (p_node && els.indexOf(p_node) < 0) els.push(p_node); if (prev_node && editor.node.isBlock(prev_node) && els.indexOf(prev_node) < 0) els.push(prev_node); if (next_node && editor.node.isBlock(next_node) && els.indexOf(next_node) < 0) els.push(next_node); } var brs = []; for (var i = 0; i < els.length; i++) { var c_brs = els[i].querySelectorAll('br'); for (var j = 0; j < c_brs.length; j++) { if (brs.indexOf(c_brs[j]) < 0) brs.push(c_brs[j]); } } if (node.parentNode == editor.el) { var childs = editor.el.children; for (var i = 0; i < childs.length; i++) { if (childs[i].tagName == 'BR') { if (brs.indexOf(childs[i]) < 0) brs.push(childs[i]); } } } return brs; } function cleanBRs (around_selection, store_selection) { // Remove BR from elements that are not empty. var brs; if (around_selection) { brs = _brsAroundSelection(); for (var i = 0; i < brs.length; i++) { _processBR(brs[i], store_selection); } } else { brs = editor.el.getElementsByTagName('br'); for (var i = 0; i < brs.length; i++) { _processBR(brs[i], store_selection); } } } /** * Normalize. */ function _normalize () { if (!editor.opts.htmlUntouched) { // Remove empty tags. cleanEmptyTags(); // Wrap possible text. _wrap(); } // Clean blank spaces. cleanBlankSpaces(); if (!editor.opts.htmlUntouched) { // Normalize spaces. editor.spaces.normalize(null, true); // Add BR tag where it is necessary. editor.html.fillEmptyBlocks(); // Clean quotes. editor.clean.quotes(); // Clean lists. editor.clean.lists(); // Clean tables. editor.clean.tables(); // Convert to HTML5. editor.clean.toHTML5(); // Remove unecessary brs. editor.html.cleanBRs(); } // Restore selection. editor.selection.restore(); // Check if editor is empty and add placeholder. checkIfEmpty(); // Refresh placeholder. editor.placeholder.refresh(); } function checkIfEmpty () { if (editor.core.isEmpty()) { if (defaultTag() != null) { // There is no block tag inside the editor. if (!editor.el.querySelector(blockTagsQuery()) && !editor.el.querySelector(editor.opts.htmlDoNotWrapTags.join(':not(.fr-marker),') + ':not(.fr-marker)')) { if (editor.core.hasFocus()) { editor.$el.html('<' + defaultTag() + '>' + $.FE.MARKERS + '<br/></' + defaultTag() + '>'); editor.selection.restore(); } else { editor.$el.html('<' + defaultTag() + '>' + '<br/></' + defaultTag() + '>'); } } } else { // There is nothing in the editor. if (!editor.el.querySelector('*:not(.fr-marker):not(br)')) { if (editor.core.hasFocus()) { editor.$el.html($.FE.MARKERS + '<br/>'); editor.selection.restore(); } else { if (!editor.$el.is('img')) { editor.$el.html('<br/>'); } } } } } } function extractNode (html, tag) { return _extractMatch(html, '<' + tag +'[^>]*?>([\\w\\W]*)<\/' + tag + '>', 1); } function extractNodeAttrs (html, tag) { var $dv = $('<div ' + (_extractMatch(html, '<' + tag + '([^>]*?)>', 1) || '') + '>'); return editor.node.rawAttributes($dv.get(0)); } function extractDoctype (html) { return _extractMatch(html, '<!DOCTYPE([^>]*?)>', 0) || '<!DOCTYPE html>'; } /** * Set HTML. */ function set (html) { var clean_html = editor.clean.html(html || '', [], [], editor.opts.fullPage); if (!editor.opts.fullPage) { editor.$el.html(clean_html); } else { // Get BODY data. var body_html = (extractNode(clean_html, 'body') || (clean_html.indexOf('<body') >= 0 ? '' : clean_html)); var body_attrs = extractNodeAttrs(clean_html, 'body'); // Get HEAD data. var head_html = extractNode(clean_html, 'head') || '<title></title>'; var head_attrs = extractNodeAttrs(clean_html, 'head'); // Get HTML that might be in <head> other than meta tags. // https://github.com/froala/wysiwyg-editor/issues/1208 var head_bad_html = $('<div>') .append(head_html) .contents().each(function () { if (this.nodeType == Node.COMMENT_NODE || ['BASE', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].indexOf(this.tagName) >= 0) { this.parentNode.removeChild(this); } }).end().html().trim(); // Filter and keep only meta tags in <head>. // https://html.spec.whatwg.org/multipage/dom.html#metadata-content-2 head_html = $('<div>') .append(head_html) .contents().map(function () { if (this.nodeType == Node.COMMENT_NODE) { return '<!--' + this.nodeValue + '-->'; } else if (['BASE', 'LINK', 'META', 'NOSCRIPT', 'SCRIPT', 'STYLE', 'TEMPLATE', 'TITLE'].indexOf(this.tagName) >= 0) { return this.outerHTML; } else { return ''; } }).toArray().join(''); // Get DOCTYPE. var doctype = extractDoctype(clean_html); // Get HTML attributes. var html_attrs = extractNodeAttrs(clean_html, 'html'); editor.$el.html(head_bad_html + '\n' + body_html); editor.node.clearAttributes(editor.el); editor.$el.attr(body_attrs); editor.$el.addClass('fr-view'); editor.$el.attr('spellcheck', editor.opts.spellcheck); editor.$el.attr('dir', editor.opts.direction); editor.$head.html(head_html); editor.node.clearAttributes(editor.$head.get(0)); editor.$head.attr(head_attrs); editor.node.clearAttributes(editor.$html.get(0)); editor.$html.attr(html_attrs); editor.iframe_document.doctype.parentNode.replaceChild( _newDoctype(doctype, editor.iframe_document), editor.iframe_document.doctype ); } // Make sure the content is editable. var disabled = editor.edit.isDisabled(); editor.edit.on(); editor.core.injectStyle(editor.opts.iframeStyle); _normalize(); if (!editor.opts.useClasses) { // Restore orignal attributes if present. editor.$el.find('[fr-original-class]').each (function () { this.setAttribute('class', this.getAttribute('fr-original-class')); this.removeAttribute('fr-original-class'); }); editor.$el.find('[fr-original-style]').each (function () { this.setAttribute('style', this.getAttribute('fr-original-style')); this.removeAttribute('fr-original-style'); }); } if (disabled) editor.edit.off(); editor.events.trigger('html.set'); } function _specifity (selector) { var idRegex = /(#[^\s\+>~\.\[:]+)/g; var attributeRegex = /(\[[^\]]+\])/g; var classRegex = /(\.[^\s\+>~\.\[:]+)/g; var pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi; var pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi; // A regex for other pseudo classes, which don't have brackets var pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g; var elementRegex = /([^\s\+>~\.\[:]+)/g; // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument (function() { var regex = /:not\(([^\)]*)\)/g; if (regex.test(selector)) { selector = selector.replace(regex, ' $1 '); } }()); var s = (selector.match(idRegex) || []).length * 100 + (selector.match(attributeRegex) || []).length * 10 + (selector.match(classRegex) || []).length * 10 + (selector.match(pseudoClassWithBracketsRegex) || []).length * 10 + (selector.match(pseudoClassRegex) || []).length * 10 + (selector.match(pseudoElementRegex) || []).length; // Remove universal selector and separator characters selector = selector.replace(/[\*\s\+>~]/g, ' '); // Remove any stray dots or hashes which aren't attached to words // These may be present if the user is live-editing this selector selector = selector.replace(/[#\.]/g, ' '); s += (selector.match(elementRegex) || []).length; return s; } /** * Do processing on the final html. */ function _processOnGet (el) { editor.events.trigger('html.processGet', [el]); // Remove class attribute when empty. if (el && el.getAttribute && el.getAttribute('class') == '') { el.removeAttribute('class'); } // Look at inner nodes that have no class set. if (el && el.nodeType == Node.ELEMENT_NODE) { var els = el.querySelectorAll('[class=""]'); for (var i = 0; i < els.length; i++) { els[i].removeAttribute('class'); } } } /** * Get HTML. */ function get (keep_markers, keep_classes) { if (!editor.$wp) { return editor.$oel.clone() .removeClass('fr-view') .removeAttr('contenteditable') .get(0).outerHTML; } var html = ''; editor.events.trigger('html.beforeGet'); // Convert STYLE from CSS files to inline style. var updated_elms = []; var elms_info = {}; var i; if (!editor.opts.useClasses && !keep_classes) { var ignoreRegEx = new RegExp('^' + editor.opts.htmlIgnoreCSSProperties.join('$|^') + '$', 'gi') for (i = 0; i < editor.doc.styleSheets.length; i++) { var rules; var head_style = 0; try { rules = editor.doc.styleSheets[i].cssRules; if (editor.doc.styleSheets[i].ownerNode && editor.doc.styleSheets[i].ownerNode.nodeType == 'STYLE') { head_style = 1; } } catch (ex) { } if (rules) { for (var idx = 0, len = rules.length; idx < len; idx++) { if (rules[idx].selectorText) { if (rules[idx].style.cssText.length > 0) { var selector = rules[idx].selectorText.replace(/body |\.fr-view /g, '').replace(/::/g, ':'); var elms; try { elms = editor.el.querySelectorAll(selector); } catch (ex) { elms = []; } for (var j = 0; j < elms.length; j++) { // Save original style. if (!elms[j].getAttribute('fr-original-style') && elms[j].getAttribute('style')) { elms[j].setAttribute('fr-original-style', elms[j].getAttribute('style')); updated_elms.push(elms[j]); } else if (!elms[j].getAttribute('fr-original-style')) { updated_elms.push(elms[j]); } if (!elms_info[elms[j]]) { elms_info[elms[j]] = {}; } // Compute specification. var spec = head_style * 1000 + _specifity(rules[idx].selectorText); // Get CSS text of the rule. var css_text = rules[idx].style.cssText.split(';'); // Get each rule. for (var k = 0; k < css_text.length; k++) { // Rule. var rule = css_text[k].trim().split(':')[0]; // Ignore the CSS rules we don't need. if (rule.match(ignoreRegEx)) continue; if (!elms_info[elms[j]][rule]) { elms_info[elms[j]][rule] = 0; if ((elms[j].getAttribute('fr-original-style') || '').indexOf(rule + ':') >= 0) { elms_info[elms[j]][rule] = 10000; } } // Current spec is higher than the existing one. if (spec >= elms_info[elms[j]][rule]) { elms_info[elms[j]][rule] = spec; if (css_text[k].trim().length) { elms[j].style[rule.trim()] = css_text[k].trim().split(':')[1].trim(); } } } } } } } } } // Save original class. for (i = 0; i < updated_elms.length; i++) { if (updated_elms[i].getAttribute('class')) { updated_elms[i].setAttribute('fr-original-class', updated_elms[i].getAttribute('class')); updated_elms[i].removeAttribute('class'); } // Make sure that we have the inline style first. if ((updated_elms[i].getAttribute('fr-original-style') || '').trim().length > 0) { var original_rules = updated_elms[i].getAttribute('fr-original-style').split(';'); for (var j = 0; j < original_rules.length; j++) { if (original_rules[j].indexOf(':') > 0) { updated_elms[i].style[original_rules[j].split(':')[0].trim()] = original_rules[j].split(':')[1].trim(); } } } } } // If editor is not empty. if (!editor.core.isEmpty()) { if (typeof keep_markers == 'undefined') keep_markers = false; if (!editor.opts.fullPage) { html = editor.$el.html(); } else { html = getDoctype(editor.iframe_document); editor.$el.removeClass('fr-view'); html += '<html' + editor.node.attributes(editor.$html.get(0)) + '>' + editor.$html.html() + '</html>'; editor.$el.addClass('fr-view'); } } else if (editor.opts.fullPage) { html = getDoctype(editor.iframe_document); html += '<html' + editor.node.attributes(editor.$html.get(0)) + '>' + editor.$html.find('head').get(0).outerHTML + '<body></body></html>'; } // Remove unwanted attributes. if (!editor.opts.useClasses && !keep_classes) { for (i = 0; i < updated_elms.length; i++) { if (updated_elms[i].getAttribute('fr-original-class')) { updated_elms[i].setAttribute('class', updated_elms[i].getAttribute('fr-original-class')); updated_elms[i].removeAttribute('fr-original-class'); } if (updated_elms[i].getAttribute('fr-original-style')) { updated_elms[i].setAttribute('style', updated_elms[i].getAttribute('fr-original-style')); updated_elms[i].removeAttribute('fr-original-style'); } else { updated_elms[i].removeAttribute('style'); } } } // Clean helpers. if (editor.opts.fullPage) { html = html.replace(/<style data-fr-style="true">(?:[\w\W]*?)<\/style>/g, ''); html = html.replace(/<link([^>]*)data-fr-style="true"([^>]*)>/g, ''); html = html.replace(/<style(?:[\w\W]*?)class="firebugResetStyles"(?:[\w\W]*?)>(?:[\w\W]*?)<\/style>/g, ''); html = html.replace(/<body((?:[\w\W]*?)) spellcheck="true"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$2>$3</body>'); html = html.replace(/<body((?:[\w\W]*?)) contenteditable="(true|false)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$3>$4</body>'); html = html.replace(/<body((?:[\w\W]*?)) dir="([\w]*)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$3>$4</body>'); html = html.replace(/<body((?:[\w\W]*?))class="([\w\W]*?)(fr-rtl|fr-ltr)([\w\W]*?)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1class="$2$4"$5>$6</body>'); html = html.replace(/<body((?:[\w\W]*?)) class=""((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$2>$3</body>'); } // Ampersand fix. if (editor.opts.htmlSimpleAmpersand) { html = html.replace(/\&/gi, '&'); } editor.events.trigger('html.afterGet'); // Remove markers. if (!keep_markers) { html = html.replace(/<span[^>]*? class\s*=\s*["']?fr-marker["']?[^>]+>\u200b<\/span>/gi, ''); } html = editor.clean.invisibleSpaces(html); html = editor.clean.exec(html, _processOnGet); var new_html = editor.events.chainTrigger('html.get', html); if (typeof new_html == 'string') { html = new_html; } // Deal with pre. html = html.replace(/<pre(?:[\w\W]*?)>(?:[\w\W]*?)<\/pre>/g, function (str) { return str.replace(/<br>/g, '\n'); }); return html; } /** * Get selected HTML. */ function getSelected () { var wrapSelection = function (container, node) { while (node && (node.nodeType == Node.TEXT_NODE || !editor.node.isBlock(node)) && !editor.node.isElement(node)) { if (node && node.nodeType != Node.TEXT_NODE) { $(container).wrapInner(editor.node.openTagString(node) + editor.node.closeTagString(node)); } node = node.parentNode; } if (node && container.innerHTML == node.innerHTML) { container.innerHTML = node.outerHTML; } } var selectionParent = function () { var parent = null; var sel; if (editor.win.getSelection) { sel = editor.win.getSelection(); if (sel && sel.rangeCount) { parent = sel.getRangeAt(0).commonAncestorContainer; if (parent.nodeType != Node.ELEMENT_NODE) { parent = parent.parentNode; } } } else if ((sel = editor.doc.selection) && sel.type != 'Control') { parent = sel.createRange().parentElement(); } if (parent != null && ($.inArray(editor.el, $(parent).parents()) >= 0 || parent == editor.el)) { return parent; } else { return null; } } var html = ''; if (typeof editor.win.getSelection != 'undefined') { // Multiple ranges hack. if (editor.browser.mozilla) { editor.selection.save(); if (editor.$el.find('.fr-marker[data-type="false"]').length > 1) { editor.$el.find('.fr-marker[data-type="false"][data-id="0"]').remove(); editor.$el.find('.fr-marker[data-type="false"]:last').attr('data-id', '0'); editor.$el.find('.fr-marker').not('[data-id="0"]').remove(); } editor.selection.restore(); } var ranges = editor.selection.ranges(); for (var i = 0; i < ranges.length; i++) { var container = document.createElement('div'); container.appendChild(ranges[i].cloneContents()); wrapSelection(container, selectionParent()); // Fix for https://github.com/froala/wysiwyg-editor/issues/1010. if ($(container).find('.fr-element').length > 0) { container = editor.el; } html += container.innerHTML; } } else if (typeof editor.doc.selection != 'undefined') { if (editor.doc.selection.type == 'Text') { html = editor.doc.selection.createRange().htmlText; } } return html; } function _hasBlockTags (html) { var tmp = editor.doc.createElement('div'); tmp.innerHTML = html; return tmp.querySelector(blockTagsQuery()) !== null; } function _setCursorAtEnd (html) { var tmp = editor.doc.createElement('div'); tmp.innerHTML = html; editor.selection.setAtEnd(tmp); return tmp.innerHTML; } function escapeEntities (str) { return str.replace(/</gi, '<') .replace(/>/gi, '>') .replace(/"/gi, '"') .replace(/'/gi, ''') } /** * Insert HTML. */ function insert (dirty_html, clean, do_split) { // There is no selection. if (!editor.selection.isCollapsed()) { editor.selection.remove(); } var clean_html; if (!clean) { clean_html = editor.clean.html(dirty_html); } else { clean_html = dirty_html; } clean_html = clean_html.replace(/\r|\n/g, ' '); if (dirty_html.indexOf('class="fr-marker"') < 0) { clean_html = _setCursorAtEnd(clean_html); } if (editor.core.isEmpty() && !editor.opts.keepFormatOnDelete) { editor.el.innerHTML = clean_html; } else { // Insert a marker. var marker = editor.markers.insert(); if (!marker) { editor.el.innerHTML = editor.el.innerHTML + clean_html; } else { // Check if HTML contains block tags and if so then break the current HTML. var deep_parent; var block_parent = editor.node.blockParent(marker); if ((_hasBlockTags(clean_html) || do_split) && (deep_parent = editor.node.deepestParent(marker) || (block_parent && block_parent.tagName == 'LI'))) { var marker = editor.markers.split(); if (!marker) return false; marker.outerHTML = clean_html; } else { marker.outerHTML = clean_html; } } } _normalize(); editor.events.trigger('html.inserted'); } /** * Clean those tags that have an invisible space inside. */ function cleanWhiteTags (ignore_selection) { var current_el = null; if (typeof ignore_selection == 'undefined') { current_el = editor.selection.element(); } if (editor.opts.keepFormatOnDelete) return false; var current_white = current_el ? (current_el.textContent.match(/\u200B/g) || []).length - current_el.querySelectorAll('.fr-marker').length : 0; var total_white = (editor.el.textContent.match(/\u200B/g) || []).length - editor.el.querySelectorAll('.fr-marker').length ; if (total_white == current_white) return false; var possible_elements; var removed; do { removed = false; possible_elements = editor.el.querySelectorAll('*:not(.fr-marker)'); for (var i = 0; i < possible_elements.length; i++) { var el = possible_elements[i]; if (current_el == el) continue; var text = el.textContent; if (el.children.length === 0 && text.length === 1 && text.charCodeAt(0) == 8203) { $(el).remove(); removed = true; } } } while (removed); } /** * Initialization. */ function _init () { var cleanTags = function () { cleanWhiteTags(); if (editor.placeholder) editor.placeholder.refresh(); } editor.events.on('mouseup', cleanTags); editor.events.on('keydown', cleanTags); editor.events.on('contentChanged', checkIfEmpty); } return { defaultTag: defaultTag, emptyBlocks: emptyBlocks, emptyBlockTagsQuery: emptyBlockTagsQuery, blockTagsQuery: blockTagsQuery, fillEmptyBlocks: fillEmptyBlocks, cleanEmptyTags: cleanEmptyTags, cleanWhiteTags: cleanWhiteTags, cleanBlankSpaces: cleanBlankSpaces, blocks: blocks, getDoctype: getDoctype, set: set, get: get, getSelected: getSelected, insert: insert, wrap: _wrap, unwrap: unwrap, escapeEntities: escapeEntities, checkIfEmpty: checkIfEmpty, extractNode: extractNode, extractNodeAttrs: extractNodeAttrs, extractDoctype: extractDoctype, cleanBRs: cleanBRs, _init: _init } } // Extend defaults. $.extend($.FE.DEFAULTS, { height: null, heightMax: null, heightMin: null, width: null }); $.FE.MODULES.size = function (editor) { function syncIframe () { refresh(); if (editor.opts.height) { editor.$el.css('minHeight', editor.opts.height - editor.helpers.getPX(editor.$el.css('padding-top')) - editor.helpers.getPX(editor.$el.css('padding-bottom'))); } editor.$iframe.height(editor.$el.outerHeight(true)); } function refresh () { if (editor.opts.heightMin) { editor.$el.css('minHeight', editor.opts.heightMin); } else { editor.$el.css('minHeight', ''); } if (editor.opts.heightMax) { editor.$wp.css('maxHeight', editor.opts.heightMax); editor.$wp.css('overflow', 'auto'); } else { editor.$wp.css('maxHeight', ''); editor.$wp.css('overflow', ''); } // Set height. if (editor.opts.height) { editor.$wp.height(editor.opts.height); editor.$el.css('minHeight', editor.opts.height - editor.helpers.getPX(editor.$el.css('padding-top')) - editor.helpers.getPX(editor.$el.css('padding-bottom'))); editor.$wp.css('overflow', 'auto'); } else { editor.$wp.css('height', ''); if (!editor.opts.heightMin) editor.$el.css('minHeight', ''); if (!editor.opts.heightMax) editor.$wp.css('overflow', ''); } if (editor.opts.width) editor.$box.width(editor.opts.width); } function _init () { if (!editor.$wp) return false; refresh(); // Sync iframe height. if (editor.$iframe) { editor.events.on('keyup', syncIframe); editor.events.on('commands.after', syncIframe); editor.events.on('html.set', syncIframe); editor.events.on('init', syncIframe); editor.events.on('initialized', syncIframe); } } return { _init: _init, syncIframe: syncIframe, refresh: refresh } }; // Extend defaults. $.extend($.FE.DEFAULTS, { language: null }); $.FE.LANGUAGE = {}; $.FE.MODULES.language = function (editor) { var lang; /** * Translate. */ function translate (str) { if (lang && lang.translation[str]) { return lang.translation[str]; } else { return str; } } /* Initialize */ function _init () { // Load lang. if ($.FE.LANGUAGE) { lang = $.FE.LANGUAGE[editor.opts.language]; } // Set direction. if (lang && lang.direction) { editor.opts.direction = lang.direction; } } return { _init: _init, translate: translate } }; // Extend defaults. $.extend($.FE.DEFAULTS, { placeholderText: 'Type something' }); $.FE.MODULES.placeholder = function (editor) { /* Show placeholder. */ function show () { if (!editor.$placeholder) _add(); // Determine the placeholder position based on the first element inside editor. var margin_top = 0; var margin_left = 0; var margin_right = 0; var padding_top = 0; var padding_left = 0; var padding_right = 0; var contents = editor.node.contents(editor.el); var alignment = $(editor.selection.element()).css('text-align'); if (contents.length && contents[0].nodeType == Node.ELEMENT_NODE) { var $first_node = $(contents[0]); if (!editor.opts.toolbarInline && editor.ready) { margin_top = editor.helpers.getPX($first_node.css('margin-top')); padding_top = editor.helpers.getPX($first_node.css('padding-top')); margin_left = editor.helpers.getPX($first_node.css('margin-left')); margin_right = editor.helpers.getPX($first_node.css('margin-right')); padding_left = editor.helpers.getPX($first_node.css('padding-left')); padding_right = editor.helpers.getPX($first_node.css('padding-right')); } editor.$placeholder.css('font-size', $first_node.css('font-size')); editor.$placeholder.css('line-height', $first_node.css('line-height')); } else { editor.$placeholder.css('font-size', editor.$el.css('font-size')); editor.$placeholder.css('line-height', editor.$el.css('line-height')); } editor.$wp.addClass('show-placeholder'); editor.$placeholder .css({ marginTop: Math.max(editor.helpers.getPX(editor.$el.css('margin-top')), margin_top), paddingTop: Math.max(editor.helpers.getPX(editor.$el.css('padding-top')), padding_top), paddingLeft: Math.max(editor.helpers.getPX(editor.$el.css('padding-left')), padding_left), marginLeft: Math.max(editor.helpers.getPX(editor.$el.css('margin-left')), margin_left), paddingRight: Math.max(editor.helpers.getPX(editor.$el.css('padding-right')), padding_right), marginRight: Math.max(editor.helpers.getPX(editor.$el.css('margin-right')), margin_right), textAlign: alignment }) .text(editor.language.translate(editor.opts.placeholderText || editor.$oel.attr('placeholder') || '')); editor.$placeholder.html(editor.$placeholder.text().replace(/\n/g, '<br>')); } /* Hide placeholder. */ function hide () { editor.$wp.removeClass('show-placeholder'); } /* Check if placeholder is visible */ function isVisible () { return !editor.$wp ? true : editor.node.hasClass(editor.$wp.get(0), 'show-placeholder'); } /* Refresh placeholder. */ function refresh () { if (!editor.$wp) return false; if (editor.core.isEmpty()) { show(); } else { hide(); } } function _add () { editor.$placeholder = $('<span class="fr-placeholder"></span>'); editor.$wp.append(editor.$placeholder); } /* Initialize. */ function _init () { if (!editor.$wp) return false; editor.events.on('init input keydown keyup contentChanged initialized', refresh); } return { _init: _init, show: show, hide: hide, refresh: refresh, isVisible: isVisible } }; $.FE.MODULES.edit = function (editor) { /** * Disable editing design. */ function disableDesign () { if (editor.browser.mozilla) { try { editor.doc.execCommand('enableObjectResizing', false, 'false'); editor.doc.execCommand('enableInlineTableEditing', false, 'false'); } catch (ex) { } } if (editor.browser.msie) { try { editor.doc.body.addEventListener('mscontrolselect', function (e) { e.preventDefault(); return false; }); } catch (ex) { } } } var disabled = false; /** * Add contneteditable attribute. */ function on () { if (editor.$wp) { editor.$el.attr('contenteditable', true); editor.$el.removeClass('fr-disabled').attr('aria-disabled', false); if (editor.$tb) editor.$tb.removeClass('fr-disabled').attr('aria-disabled', false); disableDesign(); } else if (editor.$el.is('a')) { editor.$el.attr('contenteditable', true); } disabled = false; } /** * Remove contenteditable attribute. */ function off () { if (editor.$wp) { editor.$el.attr('contenteditable', false); editor.$el.addClass('fr-disabled').attr('aria-disabled', true); if (editor.$tb) editor.$tb.addClass('fr-disabled').attr('aria-disabled', true); } else if (editor.$el.is('a')) { editor.$el.attr('contenteditable', false); } disabled = true; } function isDisabled () { return disabled; } return { on: on, off: off, disableDesign: disableDesign, isDisabled: isDisabled } }; // Extend defaults. $.extend($.FE.DEFAULTS, { editorClass: null, typingTimer: 500, iframe: false, requestWithCORS: true, requestWithCredentials: false, requestHeaders: {}, useClasses: true, spellcheck: true, iframeStyle: 'html{margin:0px;height:auto;}body{height:auto;padding:10px;background:transparent;color:#000000;position:relative;z-index: 2;-webkit-user-select:auto;margin:0px;overflow:hidden;min-height:20px;}body:after{content:"";display:block;clear:both;}', iframeStyleFiles: [], direction: 'auto', zIndex: 1, disableRightClick: false, scrollableContainer: 'body', keepFormatOnDelete: false, theme: null }) $.FE.MODULES.core = function(editor) { function injectStyle(style) { if (editor.opts.iframe) { editor.$head.find('style[data-fr-style], link[data-fr-style]').remove(); editor.$head.append('<style data-fr-style="true">' + style + '</style>'); for (var i = 0; i < editor.opts.iframeStyleFiles.length; i++) { var $link = $('<link data-fr-style="true" rel="stylesheet" href="' + editor.opts.iframeStyleFiles[i] + '">'); // Listen to the load event in order to sync iframe. $link.get(0).addEventListener('load', editor.size.syncIframe); // Append to the head. editor.$head.append($link); } } } function _initElementStyle() { if (!editor.opts.iframe) { editor.$el.addClass('fr-element fr-view'); } } /** * Init the editor style. */ function _initStyle() { editor.$box.addClass('fr-box' + (editor.opts.editorClass ? ' ' + editor.opts.editorClass : '')); editor.$wp.addClass('fr-wrapper'); _initElementStyle(); if (editor.opts.iframe) { editor.$iframe.addClass('fr-iframe'); editor.$el.addClass('fr-view'); for (var i = 0; i < editor.o_doc.styleSheets.length; i++) { var rules; try { rules = editor.o_doc.styleSheets[i].cssRules; } catch (ex) { } if (rules) { for (var idx = 0, len = rules.length; idx < len; idx++) { if (rules[idx].selectorText && (rules[idx].selectorText.indexOf('.fr-view') === 0 || rules[idx].selectorText.indexOf('.fr-element') === 0)) { if (rules[idx].style.cssText.length > 0) { if (rules[idx].selectorText.indexOf('.fr-view') === 0) { editor.opts.iframeStyle += rules[idx].selectorText.replace(/\.fr-view/g, 'body') + '{' + rules[idx].style.cssText + '}'; } else { editor.opts.iframeStyle += rules[idx].selectorText.replace(/\.fr-element/g, 'body') + '{' + rules[idx].style.cssText + '}'; } } } } } } } if (editor.opts.direction != 'auto') { editor.$box.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction); } editor.$el.attr('dir', editor.opts.direction); editor.$wp.attr('dir', editor.opts.direction); if (editor.opts.zIndex > 1) { editor.$box.css('z-index', editor.opts.zIndex); } if (editor.opts.theme) { editor.$box.addClass(editor.opts.theme + '-theme'); } } /** * Determine if the editor is empty. */ function isEmpty() { return editor.node.isEmpty(editor.el); } /** * Check if the browser allows drag and init it. */ function _initDrag() { // Drag and drop support. editor.drag_support = { filereader: typeof FileReader != 'undefined', formdata: !! editor.win.FormData, progress: 'upload' in new XMLHttpRequest() }; } /** * Return an XHR object. */ function getXHR(url, method) { var xhr = new XMLHttpRequest(); // Make it async. xhr.open(method, url, true); // Set with credentials. if (editor.opts.requestWithCredentials) { xhr.withCredentials = true; } // Set headers. for (var header in editor.opts.requestHeaders) { if (editor.opts.requestHeaders.hasOwnProperty(header)) { xhr.setRequestHeader(header, editor.opts.requestHeaders[header]); } } return xhr; } function _destroy (html) { if (editor.$oel.get(0).tagName == 'TEXTAREA') { editor.$oel.val(html); } if (editor.$wp) { if (editor.$oel.get(0).tagName == 'TEXTAREA') { editor.$el.html(''); editor.$wp.html(''); editor.$box.replaceWith(editor.$oel); editor.$oel.show(); } else { editor.$wp.replaceWith(html); editor.$el.html(''); editor.$box.removeClass('fr-view fr-ltr fr-box ' + (editor.opts.editorClass || '')); if (editor.opts.theme) { editor.$box.addClass(editor.opts.theme + '-theme'); } } } this.$wp = null; this.$el = null; this.el = null; this.$box = null; } function hasFocus() { if (editor.browser.mozilla && editor.helpers.isMobile()) return editor.selection.inEditor(); return editor.node.hasFocus(editor.el) || editor.$el.find('*:focus').length > 0; } function sameInstance ($obj) { if (!$obj) return false; var inst = $obj.data('instance'); return (inst ? inst.id == editor.id : false); } /** * Tear up. */ function _init () { $.FE.INSTANCES.push(editor); _initDrag(); // Call initialization methods. if (editor.$wp) { _initStyle(); editor.html.set(editor._original_html); // Set spellcheck. editor.$el.attr('spellcheck', editor.opts.spellcheck); // Disable autocomplete. if (editor.helpers.isMobile()) { editor.$el.attr('autocomplete', editor.opts.spellcheck ? 'on' : 'off'); editor.$el.attr('autocorrect', editor.opts.spellcheck ? 'on' : 'off'); editor.$el.attr('autocapitalize', editor.opts.spellcheck ? 'on' : 'off'); } // Disable right click. if (editor.opts.disableRightClick) { editor.events.$on(editor.$el, 'contextmenu', function(e) { if (e.button == 2) { return false; } }); } try { editor.doc.execCommand('styleWithCSS', false, false); } catch (ex) { } } if (editor.$oel.get(0).tagName == 'TEXTAREA') { // Sync on contentChanged. editor.events.on('contentChanged', function() { editor.$oel.val(editor.html.get()); }); // Set HTML on form submit. editor.events.on('form.submit', function() { editor.$oel.val(editor.html.get()); }); editor.events.on('form.reset', function () { editor.html.set(editor._original_html); }) editor.$oel.val(editor.html.get()); } // iOS focus fix. if (editor.helpers.isIOS()) { editor.events.$on(editor.$doc, 'selectionchange', function () { if (!editor.$doc.get(0).hasFocus()) { editor.$win.get(0).focus(); } }); } editor.events.trigger('init'); } return { _init: _init, destroy: _destroy, isEmpty: isEmpty, getXHR: getXHR, injectStyle: injectStyle, hasFocus: hasFocus, sameInstance: sameInstance } } $.FE.MODULES.cursorLists = function (editor) { /** * Find the first li parent. */ function _firstParentLI (node) { var p_node = node; while (p_node.tagName != 'LI') { p_node = p_node.parentNode; } return p_node; } /** * Find the first list parent. */ function _firstParentList (node) { var p_node = node; while (!editor.node.isList(p_node)) { p_node = p_node.parentNode; } return p_node; } /** * Do enter at the beginning of a list item. */ function _startEnter (marker) { var li = _firstParentLI(marker); // Get previous and next siblings. var next_li = li.nextSibling; var prev_li = li.previousSibling; var default_tag = editor.html.defaultTag(); var ul; // We are in a list item at the middle of the list or an list item that is not empty. if (editor.node.isEmpty(li, true) && next_li) { var o_str = ''; var c_str = '' var p_node = marker.parentNode; // Create open / close string. while (!editor.node.isList(p_node) && p_node.parentNode && p_node.parentNode.tagName !== 'LI') { o_str = editor.node.openTagString(p_node) + o_str; c_str = c_str + editor.node.closeTagString(p_node); p_node = p_node.parentNode; } o_str = editor.node.openTagString(p_node) + o_str; c_str = c_str + editor.node.closeTagString(p_node); var str = '' if (p_node.parentNode && p_node.parentNode.tagName == 'LI') { str = c_str + '<li>' + $.FE.MARKERS + '<br>' + o_str; } else { if (default_tag) { str = c_str + '<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>' + o_str; } else { str = c_str + $.FE.MARKERS + '<br>' + o_str; } } $(li).html('<span id="fr-break"></span>'); while (['UL', 'OL'].indexOf(p_node.tagName) < 0 || (p_node.parentNode && p_node.parentNode.tagName === 'LI')) { p_node = p_node.parentNode; } var html = editor.node.openTagString(p_node) + $(p_node).html() + editor.node.closeTagString(p_node); html = html.replace(/<span id="fr-break"><\/span>/g, str); $(p_node).replaceWith(html); editor.$el.find('li:empty').remove(); } else if ((prev_li && next_li) || !editor.node.isEmpty(li, true)) { $(li).before('<li><br></li>'); $(marker).remove(); } // There is no previous list item so transform the current list item to an empty line. else if (!prev_li) { ul = _firstParentList(li); // We are in a nested list so add a new li before it. if (ul.parentNode && ul.parentNode.tagName == 'LI') { if (next_li) { $(ul.parentNode).before('<li>' + $.FE.MARKERS + '<br></li>'); } else { $(ul.parentNode).after('<li>' + $.FE.MARKERS + '<br></li>'); } } // We are in a normal list. Add a new line before. else { if (default_tag) { $(ul).before('<' + default_tag + '>' + $.FE.MARKERS + '<br></' + default_tag + '>'); } else { $(ul).before($.FE.MARKERS + '<br>'); } } // Remove the current li. $(li).remove(); } // There is no next_li item so transform the current list item to an empty line. else { ul = _firstParentList(li); // We are in a nested lists so add a new li after it. if (ul.parentNode && ul.parentNode.tagName == 'LI') { $(ul.parentNode).after('<li>' + $.FE.MARKERS + '<br></li>'); } // We are in a normal list. Add a new line after. else { if (default_tag) { $(ul).after('<' + default_tag + '>' + $.FE.MARKERS + '<br></' + default_tag + '>'); } else { $(ul).after($.FE.MARKERS + '<br>'); } } // Remove the current li. $(li).remove(); } } /** * Enter at the middle of a list. */ function _middleEnter (marker) { var li = _firstParentLI(marker); // Build the closing / opening list item string. var str = ''; var node = marker; var o_str = ''; var c_str = ''; while (node != li) { node = node.parentNode; var cls = (node.tagName == 'A' && editor.cursor.isAtEnd(marker, node)) ? 'fr-to-remove' : ''; o_str = editor.node.openTagString($(node).clone().addClass(cls).get(0)) + o_str; c_str = editor.node.closeTagString(node) + c_str; } // Add markers. str = c_str + str + o_str + $.FE.MARKERS; // Build HTML. $(marker).replaceWith('<span id="fr-break"></span>'); var html = editor.node.openTagString(li) + $(li).html() + editor.node.closeTagString(li); html = html.replace(/<span id="fr-break"><\/span>/g, str); // Replace the current list item. $(li).replaceWith(html); } /** * Enter at the end of a list item. */ function _endEnter (marker) { var li = _firstParentLI(marker); var end_str = $.FE.MARKERS; var start_str = ''; var node = marker; var add_invisible = false; while (node != li) { node = node.parentNode; var cls = (node.tagName == 'A' && editor.cursor.isAtEnd(marker, node)) ? 'fr-to-remove' : ''; if (!add_invisible && node != li && !editor.node.isBlock(node)) { add_invisible = true; start_str = start_str + $.FE.INVISIBLE_SPACE; } start_str = editor.node.openTagString($(node).clone().addClass(cls).get(0)) + start_str; end_str = end_str + editor.node.closeTagString(node); } var str = start_str + end_str; $(marker).remove(); $(li).after(str); } /** * Do backspace on a list item. This method is called only when wer are at the beginning of a LI. */ function _backspace (marker) { var li = _firstParentLI(marker); // Get previous sibling. var prev_li = li.previousSibling; // There is a previous li. if (prev_li) { // Get the li inside a nested list or inner block tags. prev_li = $(prev_li).find(editor.html.blockTagsQuery()).get(-1) || prev_li; // Add markers. $(marker).replaceWith($.FE.MARKERS); // Remove possible BR at the end of the previous list. var contents = editor.node.contents(prev_li); if (contents.length && contents[contents.length - 1].tagName == 'BR') { $(contents[contents.length - 1]).remove(); } // Remove any nodes that might be wrapped. $(li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () { if (this.parentNode == li) { $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>')); } }) // Append the current list item content to the previous one. var node = editor.node.contents(li)[0]; var tmp; while (node && !editor.node.isList(node)) { tmp = node.nextSibling; $(prev_li).append(node); node = tmp; } prev_li = li.previousSibling; while (node) { tmp = node.nextSibling; $(prev_li).append(node); node = tmp; } // Remove the current LI. $(li).remove(); } // No previous li. else { var ul = _firstParentList(li); // Add markers. $(marker).replaceWith($.FE.MARKERS); // Nested lists. if (ul.parentNode && ul.parentNode.tagName == 'LI') { var prev_node = ul.previousSibling; // Previous node is block. if (editor.node.isBlock(prev_node)) { // Remove any nodes that might be wrapped. $(li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () { if (this.parentNode == li) { $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>')); } }); $(prev_node).append($(li).html()); } // Text right in li. else { $(ul).before($(li).html()); } } // Normal lists. Add an empty li instead. else { var default_tag = editor.html.defaultTag(); if (default_tag && $(li).find(editor.html.blockTagsQuery()).length === 0) { $(ul).before('<' + default_tag + '>' + $(li).html() + '</' + default_tag + '>'); } else { $(ul).before($(li).html()); } } // Remove the current li. $(li).remove(); // Remove the ul if it is empty. if ($(ul).find('li').length === 0) $(ul).remove(); } } /** * Delete at the end of list item. */ function _del (marker) { var li = _firstParentLI(marker); var next_li = li.nextSibling; var contents; // There is a next li. if (next_li) { // Remove possible BR at the beginning of the next LI. contents = editor.node.contents(next_li); if (contents.length && contents[0].tagName == 'BR') { $(contents[0]).remove(); } // Unwrap content from the next node. $(next_li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () { if (this.parentNode == next_li) { $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>')); } }); // Append the next LI to the current LI. var last_node = marker; var node = editor.node.contents(next_li)[0]; var tmp; while (node && !editor.node.isList(node)) { tmp = node.nextSibling; $(last_node).after(node); last_node = node; node = tmp; } // Append nested lists. while (node) { tmp = node.nextSibling; $(li).append(node); node = tmp; } // Replace marker with markers. $(marker).replaceWith($.FE.MARKERS); // Remove next li. $(next_li).remove(); } // No next li. else { // Search the next sibling in parents. var next_node = li; while (!next_node.nextSibling && next_node != editor.el) { next_node = next_node.parentNode; } // We're right at the end. if (next_node == editor.el) return false; // Get the next sibling. next_node = next_node.nextSibling; // Next sibling is a block tag. if (editor.node.isBlock(next_node)) { // Check if we can do delete in it. if ($.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0) { // Add markers. $(marker).replaceWith($.FE.MARKERS); // Remove any possible BR at the end of the LI. contents = editor.node.contents(li); if (contents.length && contents[contents.length - 1].tagName == 'BR') { $(contents[contents.length - 1]).remove(); } // Append next node. $(li).append($(next_node).html()); // Remove the next node. $(next_node).remove(); } } // Append everything till the next block tag or BR. else { // Remove any possible BR at the end of the LI. contents = editor.node.contents(li); if (contents.length && contents[contents.length - 1].tagName == 'BR') { $(contents[contents.length - 1]).remove(); } // var next_node = next_li; $(marker).replaceWith($.FE.MARKERS); while (next_node && !editor.node.isBlock(next_node) && next_node.tagName != 'BR') { $(li).append($(next_node)); next_node = next_node.nextSibling; } } } } return { _startEnter: _startEnter, _middleEnter: _middleEnter, _endEnter: _endEnter, _backspace: _backspace, _del: _del } }; // Do not merge with the previous one. $.FE.NO_DELETE_TAGS = ['TH', 'TD', 'TR', 'TABLE', 'FORM']; // Do simple enter. $.FE.SIMPLE_ENTER_TAGS = ['TH', 'TD', 'LI', 'DL', 'DT', 'FORM']; $.FE.MODULES.cursor = function (editor) { /** * Check if node is at the end of a block tag. */ function _atEnd (node) { if (!node) return false; if (editor.node.isBlock(node)) return true; if (node.nextSibling && node.nextSibling.nodeType == Node.TEXT_NODE && node.nextSibling.textContent.replace(/\u200b/g, '').length == 0) { return _atEnd(node.nextSibling); } if (node.nextSibling) return false; return _atEnd(node.parentNode); } /** * Check if node is at the start of a block tag. */ function _atStart (node) { if (!node) return false; if (editor.node.isBlock(node)) return true; if (node.previousSibling && node.previousSibling.nodeType == Node.TEXT_NODE && node.previousSibling.textContent.replace(/\u200b/g, '').length == 0) { return _atStart(node.previousSibling); } if (node.previousSibling) return false; return _atStart(node.parentNode); } /** * Check if node is a the start of the container. */ function _isAtStart (node, container) { if (!node) return false; if (node == editor.$wp.get(0)) return false; if (node.previousSibling && node.previousSibling.nodeType == Node.TEXT_NODE && node.previousSibling.textContent.replace(/\u200b/g, '').length == 0) { return _isAtStart(node.previousSibling, container); } if (node.previousSibling) return false; if (node.parentNode == container) return true; return _isAtStart(node.parentNode, container); } /** * Check if node is a the start of the container. */ function _isAtEnd (node, container) { if (!node) return false; if (node == editor.$wp.get(0)) return false; if (node.nextSibling && node.nextSibling.nodeType == Node.TEXT_NODE && node.nextSibling.textContent.replace(/\u200b/g, '').length == 0) { return _isAtEnd(node.nextSibling, container); } if (node.nextSibling) return false; if (node.parentNode == container) return true; return _isAtEnd(node.parentNode, container); } /** * Check if the node is inside a LI. */ function _inLi (node) { return $(node).parentsUntil(editor.$el, 'LI').length > 0 && $(node).parentsUntil('LI', 'TABLE').length === 0; } /** * Do backspace at the start of a block tag. */ function _startBackspace (marker) { var quote = $(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0; var deep_parent = editor.node.deepestParent(marker, [], !quote); if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') { var m_parent = editor.node.deepestParent(marker, [$(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').get(0)]); if (m_parent && m_parent.previousSibling) { deep_parent = m_parent; } } // Deepest parent is not the main element. if (deep_parent !== null) { var prev_node = deep_parent.previousSibling; var contents; // We are inside a block tag. if (editor.node.isBlock(deep_parent) && editor.node.isEditable(deep_parent)) { // There is a previous node. if (prev_node && $.FE.NO_DELETE_TAGS.indexOf(prev_node.tagName) < 0) { if (editor.node.isDeletable(prev_node)) { $(prev_node).remove(); $(marker).replaceWith($.FE.MARKERS); } else { // Previous node is a block tag. if (editor.node.isEditable(prev_node)) { if (editor.node.isBlock(prev_node)) { if (editor.node.isEmpty(prev_node) && !editor.node.isList(prev_node)) { $(prev_node).remove(); } else { if (editor.node.isList(prev_node)) { prev_node = $(prev_node).find('li:last').get(0); } // Remove last BR. contents = editor.node.contents(prev_node); if (contents.length && contents[contents.length - 1].tagName == 'BR') { $(contents[contents.length - 1]).remove(); } // Prev node is blockquote but the current one isn't. if (prev_node.tagName == 'BLOCKQUOTE' && deep_parent.tagName != 'BLOCKQUOTE') { contents = editor.node.contents(prev_node); while (contents.length && editor.node.isBlock(contents[contents.length - 1])) { prev_node = contents[contents.length - 1]; contents = editor.node.contents(prev_node); } } // Prev node is not blockquote, but the current one is. else if (prev_node.tagName != 'BLOCKQUOTE' && deep_parent.tagName == 'BLOCKQUOTE') { contents = editor.node.contents(deep_parent); while (contents.length && editor.node.isBlock(contents[0])) { deep_parent = contents[0]; contents = editor.node.contents(deep_parent); } } $(marker).replaceWith($.FE.MARKERS); $(prev_node).append(editor.node.isEmpty(deep_parent) ? $.FE.MARKERS : deep_parent.innerHTML); $(deep_parent).remove(); } } else { $(marker).replaceWith($.FE.MARKERS); if (deep_parent.tagName == 'BLOCKQUOTE' && prev_node.nodeType == Node.ELEMENT_NODE) { $(prev_node).remove(); } else { $(prev_node).after(editor.node.isEmpty(deep_parent) ? '' : $(deep_parent).html()); $(deep_parent).remove(); if (prev_node.tagName == 'BR') $(prev_node).remove(); } } } } } } // No block tag. /* jshint ignore:start */ /* jscs:disable */ else { // This should never happen. } /* jshint ignore:end */ /* jscs:enable */ } } /** * Do backspace at the middle of a block tag. */ function _middleBackspace (marker) { var prev_node = marker; // Get the parent node that has a prev sibling. while (!prev_node.previousSibling) { prev_node = prev_node.parentNode; if (editor.node.isElement(prev_node)) return false; } prev_node = prev_node.previousSibling; // Not block tag. var contents; if (!editor.node.isBlock(prev_node) && editor.node.isEditable(prev_node)) { contents = editor.node.contents(prev_node); // Previous node is text. while (prev_node.nodeType != Node.TEXT_NODE && !editor.node.isDeletable(prev_node) && contents.length && editor.node.isEditable(prev_node)) { prev_node = contents[contents.length - 1]; contents = editor.node.contents(prev_node); } if (prev_node.nodeType == Node.TEXT_NODE) { if (editor.helpers.isIOS()) return true; var txt = prev_node.textContent; var len = txt.length - 1; // Tab UNDO. if (editor.opts.tabSpaces && txt.length >= editor.opts.tabSpaces) { var tab_str = txt.substr(txt.length - editor.opts.tabSpaces, txt.length - 1); if (tab_str.replace(/ /g, '').replace(new RegExp($.FE.UNICODE_NBSP, 'g'), '').length == 0) { len = txt.length - editor.opts.tabSpaces; } } prev_node.textContent = txt.substring(0, len); if (prev_node.textContent.length && prev_node.textContent.charCodeAt(prev_node.textContent.length - 1) == 55357) { prev_node.textContent = prev_node.textContent.substr(0, prev_node.textContent.length - 1); } var deleted = (txt.length != prev_node.textContent.length); // Remove node if empty. if (prev_node.textContent.length == 0) { // Here we check to see if we should keep the current formatting. if (deleted && editor.opts.keepFormatOnDelete) { $(prev_node).after($.FE.INVISIBLE_SPACE + $.FE.MARKERS); } else { if (prev_node.parentNode.childNodes.length == 2 && prev_node.parentNode == marker.parentNode && !editor.node.isBlock(prev_node.parentNode) && !editor.node.isElement(prev_node.parentNode)) { $(prev_node.parentNode).after($.FE.MARKERS); $(prev_node.parentNode).remove(); } else { $(prev_node).after($.FE.MARKERS); // https://github.com/froala/wysiwyg-editor/issues/1379. if (editor.node.isElement(prev_node.parentNode) && !marker.nextSibling && prev_node.previousSibling && prev_node.previousSibling.tagName == 'BR') { $(marker).after('<br>'); } prev_node.parentNode.removeChild(prev_node); } } } else { $(prev_node).after($.FE.MARKERS); } } else if (editor.node.isDeletable(prev_node)) { $(prev_node).after($.FE.MARKERS); $(prev_node).remove(); } else { if (editor.events.trigger('node.remove', [$(prev_node)]) !== false) { $(prev_node).after($.FE.MARKERS); $(prev_node).remove(); } } } // Block tag but we are allowed to delete it. else if ($.FE.NO_DELETE_TAGS.indexOf(prev_node.tagName) < 0 && (editor.node.isEditable(prev_node) || editor.node.isDeletable(prev_node))) { if (editor.node.isDeletable(prev_node)) { $(marker).replaceWith($.FE.MARKERS); $(prev_node).remove(); } else if (editor.node.isEmpty(prev_node) && !editor.node.isList(prev_node)) { $(prev_node).remove(); $(marker).replaceWith($.FE.MARKERS); } else { // List correction. if (editor.node.isList(prev_node)) prev_node = $(prev_node).find('li:last').get(0); contents = editor.node.contents(prev_node); if (contents && contents[contents.length - 1].tagName == 'BR') { $(contents[contents.length - 1]).remove(); } contents = editor.node.contents(prev_node); while (contents && editor.node.isBlock(contents[contents.length - 1])) { prev_node = contents[contents.length - 1]; contents = editor.node.contents(prev_node); } $(prev_node).append($.FE.MARKERS); var next_node = marker; while (!next_node.previousSibling) { next_node = next_node.parentNode; } while (next_node && next_node.tagName !== 'BR' && !editor.node.isBlock(next_node)) { var copy_node = next_node; next_node = next_node.nextSibling; $(prev_node).append(copy_node); } // Remove BR. if (next_node && next_node.tagName == 'BR') $(next_node).remove(); $(marker).remove(); } } else { if (marker.nextSibling && marker.nextSibling.tagName == 'BR') { $(marker.nextSibling).remove(); } } } /** * Do backspace. */ function backspace () { var do_default = false; // Add a marker in HTML. var marker = editor.markers.insert(); if (!marker) return true; // Do not allow edit inside contenteditable="false". var p_node = marker.parentNode; while (p_node && !editor.node.isElement(p_node)) { if (p_node.getAttribute('contenteditable') === 'false') { $(marker).replaceWith($.FE.MARKERS); editor.selection.restore(); return false; } else if (p_node.getAttribute('contenteditable') === 'true') { break; } p_node = p_node.parentNode; } editor.el.normalize(); // We should remove invisible space first of all. var prev_node = marker.previousSibling; if (prev_node) { var txt = prev_node.textContent; if (txt && txt.length && txt.charCodeAt(txt.length - 1) == 8203) { if (txt.length == 1) { $(prev_node).remove() } else { prev_node.textContent = prev_node.textContent.substr(0, txt.length - 1); if (prev_node.textContent.length && prev_node.textContent.charCodeAt(prev_node.textContent.length - 1) == 55357) { prev_node.textContent = prev_node.textContent.substr(0, prev_node.textContent.length - 1); } } } } // Delete at end. if (_atEnd(marker)) { do_default = _middleBackspace(marker); } // Delete at start. else if (_atStart(marker)) { if (_inLi(marker) && _isAtStart(marker, $(marker).parents('li:first').get(0))) { editor.cursorLists._backspace(marker); } else { _startBackspace(marker); } } // Delete at middle. else { do_default = _middleBackspace(marker); } $(marker).remove(); _cleanEmptyBlockquotes(); editor.html.fillEmptyBlocks(true); if (!editor.opts.htmlUntouched) { editor.html.cleanEmptyTags(); editor.clean.quotes(); editor.clean.lists(); } editor.spaces.normalizeAroundCursor(); editor.selection.restore(); return do_default; } /** * Delete at the end of a block tag. */ function _endDel (marker) { var quote = $(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0; var deep_parent = editor.node.deepestParent(marker, [], !quote); if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') { var m_parent = editor.node.deepestParent(marker, [$(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').get(0)]); if (m_parent && m_parent.nextSibling) { deep_parent = m_parent; } } // Deepest parent is not the main element. if (deep_parent !== null) { var next_node = deep_parent.nextSibling; var contents; // We are inside a block tag. if (editor.node.isBlock(deep_parent) && (editor.node.isEditable(deep_parent) || editor.node.isDeletable(deep_parent))) { // There is a next node. if (next_node && $.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0) { if (editor.node.isDeletable(next_node)) { $(next_node).remove(); $(marker).replaceWith($.FE.MARKERS); } else { // Next node is a block tag. if (editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) { // Next node is a list. if (editor.node.isList(next_node)) { // Current block tag is empty. if (editor.node.isEmpty(deep_parent, true)) { $(deep_parent).remove(); $(next_node).find('li:first').prepend($.FE.MARKERS); } else { var $li = $(next_node).find('li:first'); if (deep_parent.tagName == 'BLOCKQUOTE') { contents = editor.node.contents(deep_parent); if (contents.length && editor.node.isBlock(contents[contents.length - 1])) { deep_parent = contents[contents.length - 1]; } } // There are no nested lists. if ($li.find('ul, ol').length === 0) { $(marker).replaceWith($.FE.MARKERS); // Remove any nodes that might be wrapped. $li.find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () { if (this.parentNode == $li.get(0)) { $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>')); } }); $(deep_parent).append(editor.node.contents($li.get(0))); $li.remove(); if ($(next_node).find('li').length === 0) $(next_node).remove(); } } } else { // Remove last BR. contents = editor.node.contents(next_node); if (contents.length && contents[0].tagName == 'BR') { $(contents[0]).remove(); } if (next_node.tagName != 'BLOCKQUOTE' && deep_parent.tagName == 'BLOCKQUOTE') { contents = editor.node.contents(deep_parent); while (contents.length && editor.node.isBlock(contents[contents.length - 1])) { deep_parent = contents[contents.length - 1]; contents = editor.node.contents(deep_parent); } } else if (next_node.tagName == 'BLOCKQUOTE' && deep_parent.tagName != 'BLOCKQUOTE') { contents = editor.node.contents(next_node); while (contents.length && editor.node.isBlock(contents[0])) { next_node = contents[0]; contents = editor.node.contents(next_node); } } $(marker).replaceWith($.FE.MARKERS); $(deep_parent).append(next_node.innerHTML); $(next_node).remove(); } } else { $(marker).replaceWith($.FE.MARKERS); // var next_node = next_node.nextSibling; while (next_node && next_node.tagName !== 'BR' && !editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) { var copy_node = next_node; next_node = next_node.nextSibling; $(deep_parent).append(copy_node); } if (next_node && next_node.tagName == 'BR' && editor.node.isEditable(next_node)) { $(next_node).remove(); } } } } } // No block tag. /* jshint ignore:start */ /* jscs:disable */ else { // This should never happen. } /* jshint ignore:end */ /* jscs:enable */ } } /** * Delete at the middle of a block tag. */ function _middleDel (marker) { var next_node = marker; // Get the parent node that has a next sibling. while (!next_node.nextSibling) { next_node = next_node.parentNode; if (editor.node.isElement(next_node)) return false; } next_node = next_node.nextSibling; // Handle the case when the next node is a BR. if (next_node.tagName == 'BR' && editor.node.isEditable(next_node)) { // There is a next sibling. if (next_node.nextSibling) { if (editor.node.isBlock(next_node.nextSibling) && editor.node.isEditable(next_node.nextSibling)) { if ($.FE.NO_DELETE_TAGS.indexOf(next_node.nextSibling.tagName) < 0) { next_node = next_node.nextSibling; $(next_node.previousSibling).remove(); } else { $(next_node).remove(); return; } } } // No next sibling. We should check if BR is at the end. else if (_atEnd(next_node)) { if (_inLi(marker)) { editor.cursorLists._del(marker); } else { var deep_parent = editor.node.deepestParent(next_node); if (deep_parent) { $(next_node).remove(); _endDel(marker); } } return; } } // Not block tag. var contents; if (!editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) { contents = editor.node.contents(next_node); // Next node is text. while (next_node.nodeType != Node.TEXT_NODE && contents.length && !editor.node.isDeletable(next_node) && editor.node.isEditable(next_node)) { next_node = contents[0]; contents = editor.node.contents(next_node); } if (next_node.nodeType == Node.TEXT_NODE) { $(next_node).before($.FE.MARKERS); if (next_node.textContent.length && next_node.textContent.charCodeAt(0) == 55357) { next_node.textContent = next_node.textContent.substring(2, next_node.textContent.length); } else { next_node.textContent = next_node.textContent.substring(1, next_node.textContent.length); } } else if (editor.node.isDeletable(next_node)) { $(next_node).before($.FE.MARKERS); $(next_node).remove(); } else { if (editor.events.trigger('node.remove', [$(next_node)]) !== false) { $(next_node).before($.FE.MARKERS); $(next_node).remove(); } } $(marker).remove(); } // Block tag. else if ($.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0 && (editor.node.isEditable(next_node) || editor.node.isDeletable(next_node))) { if (editor.node.isDeletable(next_node)) { $(marker).replaceWith($.FE.MARKERS); $(next_node).remove(); } else { if (editor.node.isList(next_node)) { // There is a previous sibling. if (marker.previousSibling) { $(next_node).find('li:first').prepend(marker); editor.cursorLists._backspace(marker); } // No previous sibling. else { $(next_node).find('li:first').prepend($.FE.MARKERS); $(marker).remove(); } } else { contents = editor.node.contents(next_node); if (contents && contents[0].tagName == 'BR') { $(contents[0]).remove(); } // Deal with blockquote. if (contents && next_node.tagName == 'BLOCKQUOTE') { var node = contents[0]; $(marker).before($.FE.MARKERS); while (node && node.tagName != 'BR') { var tmp = node; node = node.nextSibling; $(marker).before(tmp); } if (node && node.tagName == 'BR') { $(node).remove(); } } else { $(marker) .after($(next_node).html()) .after($.FE.MARKERS); $(next_node).remove(); } } } } } /** * Delete. */ function del () { var marker = editor.markers.insert(); if (!marker) return false; editor.el.normalize(); // Delete at end. if (_atEnd(marker)) { if (_inLi(marker)) { if ($(marker).parents('li:first').find('ul, ol').length === 0) { editor.cursorLists._del(marker); } else { var $li = $(marker).parents('li:first').find('ul:first, ol:first').find('li:first'); $li = $li.find(editor.html.blockTagsQuery()).get(-1) || $li; $li.prepend(marker); editor.cursorLists._backspace(marker); } } else { _endDel(marker); } } // Delete at start. else if (_atStart(marker)) { _middleDel(marker); } // Delete at middle. else { _middleDel(marker); } $(marker).remove(); _cleanEmptyBlockquotes(); editor.html.fillEmptyBlocks(true); if (!editor.opts.htmlUntouched) { editor.html.cleanEmptyTags(); editor.clean.quotes(); editor.clean.lists(); } editor.spaces.normalizeAroundCursor(); editor.selection.restore(); } function _cleanEmptyBlockquotes () { var blks = editor.el.querySelectorAll('blockquote:empty'); for (var i = 0; i < blks.length; i++) { blks[i].parentNode.removeChild(blks[i]); } } function _cleanNodesToRemove () { editor.$el.find('.fr-to-remove').each (function () { var contents = editor.node.contents(this); for (var i = 0; i < contents.length; i++) { if (contents[i].nodeType == Node.TEXT_NODE) { contents[i].textContent = contents[i].textContent.replace(/\u200B/g, ''); } } $(this).replaceWith(this.innerHTML); }) } /** * Enter at the end of a block tag. */ function _endEnter (marker, shift, quote) { var deep_parent = editor.node.deepestParent(marker, [], !quote); var default_tag; if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') { if (_isAtEnd(marker, deep_parent)) { default_tag = editor.html.defaultTag(); if (default_tag) { $(deep_parent).after('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>'); } else { $(deep_parent).after($.FE.MARKERS + '<br>'); } $(marker).remove(); return false; } else { _middleEnter(marker, shift, quote); return false; } } // We are right in the main element. if (deep_parent == null) { default_tag = editor.html.defaultTag(); if (!default_tag || !editor.node.isElement(marker.parentNode)) { $(marker).replaceWith((!editor.node.isEmpty(marker.parentNode, true) ? '<br/>' : '') + $.FE.MARKERS + '<br/>'); } else { $(marker).replaceWith('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>'); } } // There is a parent. else { // Block tag parent. var c_node = marker; var str = ''; if (!editor.node.isBlock(deep_parent) || shift) { str = '<br/>'; } var c_str = ''; var o_str = ''; default_tag = editor.html.defaultTag(); var open_default_tag = ''; var close_default_tag = ''; if (default_tag && editor.node.isBlock(deep_parent)) { open_default_tag = '<' + default_tag + '>'; close_default_tag = '</' + default_tag + '>'; if (deep_parent.tagName == default_tag.toUpperCase()) { open_default_tag = editor.node.openTagString($(deep_parent).clone().removeAttr('id').get(0)); } } do { c_node = c_node.parentNode; // Shift condition. if (!shift || c_node != deep_parent || (shift && !editor.node.isBlock(deep_parent))) { c_str = c_str + editor.node.closeTagString(c_node); // Open str when there is a block parent. if (c_node == deep_parent && editor.node.isBlock(deep_parent)) { o_str = open_default_tag + o_str; } else { var cls = (c_node.tagName == 'A' && _isAtEnd(marker, c_node)) ? 'fr-to-remove' : ''; o_str = editor.node.openTagString($(c_node).clone().addClass(cls).get(0)) + o_str; } } } while (c_node != deep_parent); // Add BR if deep parent is block tag. str = c_str + str + o_str + ((marker.parentNode == deep_parent && editor.node.isBlock(deep_parent)) ? '' : $.FE.INVISIBLE_SPACE) + $.FE.MARKERS; if (editor.node.isBlock(deep_parent) && !$(deep_parent).find('*:last').is('br')) { $(deep_parent).append('<br/>'); } $(marker).after('<span id="fr-break"></span>'); $(marker).remove(); // Add a BR after to make sure we display the last line. if ((!deep_parent.nextSibling || editor.node.isBlock(deep_parent.nextSibling)) && !editor.node.isBlock(deep_parent)) { $(deep_parent).after('<br>'); } var html; // No shift. if (!shift && editor.node.isBlock(deep_parent)) { html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + close_default_tag; } else { html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent); } html = html.replace(/<span id="fr-break"><\/span>/g, str); $(deep_parent).replaceWith(html); } } /** * Start at the beginning of a block tag. */ function _startEnter (marker, shift, quote) { var deep_parent = editor.node.deepestParent(marker, [], !quote); var default_tag; // https://github.com/froala-labs/froala-editor-js-2/issues/320 if (deep_parent && deep_parent.tagName == 'TABLE') { $(deep_parent).find('td:first, th:first').prepend(marker); return _startEnter(marker, shift, quote); } if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') { if (_isAtStart(marker, deep_parent)) { default_tag = editor.html.defaultTag(); if (default_tag) { $(deep_parent).before('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>'); } else { $(deep_parent).before($.FE.MARKERS + '<br>'); } $(marker).remove(); return false; } else if (_isAtEnd(marker, deep_parent)) { _endEnter(marker, shift, true); } else { _middleEnter(marker, shift, true); } } // We are right in the main element. if (deep_parent == null) { default_tag = editor.html.defaultTag(); if (!default_tag || !editor.node.isElement(marker.parentNode)) { $(marker).replaceWith('<br>' + $.FE.MARKERS); } else { $(marker).replaceWith('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>'); } } else { if (editor.node.isBlock(deep_parent)) { if (shift) { $(marker).remove(); $(deep_parent).prepend('<br>' + $.FE.MARKERS); } else if (editor.node.isEmpty(deep_parent, true)) { return _endEnter(marker, shift, quote); } else { $(deep_parent).before(editor.node.openTagString($(deep_parent).clone().removeAttr('id').get(0)) + '<br>' + editor.node.closeTagString(deep_parent)); } } else { $(deep_parent).before('<br>'); } $(marker).remove(); } } /** * Enter at the middle of a block tag. */ function _middleEnter (marker, shift, quote) { var deep_parent = editor.node.deepestParent(marker, [], !quote); // We are right in the main element. if (deep_parent == null) { // Default tag is not enter. if (editor.html.defaultTag() && marker.parentNode === editor.el) { $(marker).replaceWith('<' + editor.html.defaultTag() + '>' + $.FE.MARKERS + '<br></' + editor.html.defaultTag() + '>'); } else { // Add a BR after to make sure we display the last line. if ((!marker.nextSibling || editor.node.isBlock(marker.nextSibling))) { $(marker).after('<br>'); } $(marker).replaceWith('<br>' + $.FE.MARKERS); } } // There is a parent. else { // Block tag parent. var c_node = marker; var str = ''; if (deep_parent.tagName == 'PRE') shift = true; if (!editor.node.isBlock(deep_parent) || shift) { str = '<br>'; } var c_str = ''; var o_str = ''; do { var tmp = c_node; c_node = c_node.parentNode; // Move marker after node it if is empty and we are in quote. if (deep_parent.tagName == 'BLOCKQUOTE' && editor.node.isEmpty(tmp) && !editor.node.hasClass(tmp, 'fr-marker')) { if ($(tmp).find(marker).length > 0) { $(tmp).after(marker); } } // If not at end or start of element in quote. if (!(deep_parent.tagName == 'BLOCKQUOTE' && (_isAtEnd(marker, c_node) || _isAtStart(marker, c_node)))) { // 1. No shift. // 2. c_node is not deep parent. // 3. Shift and deep parent is not block tag. if (!shift || c_node != deep_parent || (shift && !editor.node.isBlock(deep_parent))) { c_str = c_str + editor.node.closeTagString(c_node); var cls = (c_node.tagName == 'A' && _isAtEnd(marker, c_node)) ? 'fr-to-remove' : ''; o_str = editor.node.openTagString($(c_node).clone().addClass(cls).removeAttr('id').get(0)) + o_str; } } } while (c_node != deep_parent); // We should add an invisible space if: // 1. parent node is not deep parent and block tag. // 2. marker has no next sibling. var add = ( (deep_parent == marker.parentNode && editor.node.isBlock(deep_parent)) || marker.nextSibling ); if (deep_parent.tagName == 'BLOCKQUOTE') { if (marker.previousSibling && editor.node.isBlock(marker.previousSibling) && marker.nextSibling && marker.nextSibling.tagName == 'BR') { $(marker.nextSibling).after(marker); if (marker.nextSibling && marker.nextSibling.tagName == 'BR') { $(marker.nextSibling).remove(); } } var default_tag = editor.html.defaultTag(); str = c_str + str + (default_tag ? '<' + default_tag + '>' : '') + $.FE.MARKERS + '<br>' + (default_tag ? '</' + default_tag + '>' : '') + o_str; } else { str = c_str + str + o_str + (add ? '' : $.FE.INVISIBLE_SPACE) + $.FE.MARKERS; } $(marker).replaceWith('<span id="fr-break"></span>'); var html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent); html = html.replace(/<span id="fr-break"><\/span>/g, str); $(deep_parent).replaceWith(html); } } /** * Do enter. */ function enter (shift) { // Add a marker in HTML. var marker = editor.markers.insert(); if (!marker) return true; editor.el.normalize(); var quote = false; if ($(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0) { shift = false; quote = true; } if ($(marker).parentsUntil(editor.$el, 'TD, TH').length) quote = false; // At the end. if (_atEnd(marker)) { // Enter in list. if (_inLi(marker) && !shift && !quote) { editor.cursorLists._endEnter(marker); } else { _endEnter(marker, shift, quote); } } // At start. else if (_atStart(marker)) { // Enter in list. if (_inLi(marker) && !shift && !quote) { editor.cursorLists._startEnter(marker); } else { _startEnter(marker, shift, quote); } } // At middle. else { // Enter in list. if (_inLi(marker) && !shift && !quote) { editor.cursorLists._middleEnter(marker); } else { _middleEnter(marker, shift, quote); } } _cleanNodesToRemove(); if (!editor.opts.htmlUntouched) { editor.html.fillEmptyBlocks(true); editor.html.cleanEmptyTags(); editor.clean.lists(); } editor.spaces.normalizeAroundCursor(); editor.selection.restore(); } return { enter: enter, backspace: backspace, del: del, isAtEnd: _isAtEnd, isAtStart: _isAtStart } } // Enter possible actions. $.FE.ENTER_P = 0; $.FE.ENTER_DIV = 1; $.FE.ENTER_BR = 2; $.FE.KEYCODE = { BACKSPACE: 8, TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, ESC: 27, SPACE: 32, ARROW_LEFT: 37, ARROW_UP: 38, ARROW_RIGHT: 39, ARROW_DOWN: 40, DELETE: 46, ZERO: 48, ONE: 49, TWO: 50, THREE: 51, FOUR: 52, FIVE: 53, SIX: 54, SEVEN: 55, EIGHT: 56, NINE: 57, FF_SEMICOLON: 59, // Firefox (Gecko) fires this for semicolon instead of 186 FF_EQUALS: 61, // Firefox (Gecko) fires this for equals instead of 187 QUESTION_MARK: 63, // needs localization A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, META: 91, NUM_ZERO: 96, NUM_ONE: 97, NUM_TWO: 98, NUM_THREE: 99, NUM_FOUR: 100, NUM_FIVE: 101, NUM_SIX: 102, NUM_SEVEN: 103, NUM_EIGHT: 104, NUM_NINE: 105, NUM_MULTIPLY: 106, NUM_PLUS: 107, NUM_MINUS: 109, NUM_PERIOD: 110, NUM_DIVISION: 111, F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123, FF_HYPHEN: 173, // Firefox (Gecko) fires this for hyphen instead of 189s SEMICOLON: 186, // needs localization DASH: 189, // needs localization EQUALS: 187, // needs localization COMMA: 188, // needs localization HYPHEN: 189, // needs localization PERIOD: 190, // needs localization SLASH: 191, // needs localization APOSTROPHE: 192, // needs localization TILDE: 192, // needs localization SINGLE_QUOTE: 222, // needs localization OPEN_SQUARE_BRACKET: 219, // needs localization BACKSLASH: 220, // needs localization CLOSE_SQUARE_BRACKET: 221 // needs localization } // Extend defaults. $.extend($.FE.DEFAULTS, { enter: $.FE.ENTER_P, multiLine: true, tabSpaces: 0 }); $.FE.MODULES.keys = function (editor) { var IME = false; /** * ENTER. */ function _enter (e) { if (!editor.opts.multiLine) { e.preventDefault(); e.stopPropagation(); } else if (!editor.helpers.isIOS()) { e.preventDefault(); e.stopPropagation(); if (!editor.selection.isCollapsed()) editor.selection.remove(); editor.cursor.enter(); } } /** * SHIFT ENTER. */ function _shiftEnter (e) { e.preventDefault(); e.stopPropagation(); if (editor.opts.multiLine) { if (!editor.selection.isCollapsed()) editor.selection.remove(); editor.cursor.enter(true); } } /** * BACKSPACE. */ var regular_backspace; function _backspace (e) { // There is no selection. if (editor.selection.isCollapsed()) { if (!editor.cursor.backspace()) { e.preventDefault(); e.stopPropagation(); regular_backspace = false; } } // We have text selected. else { e.preventDefault(); e.stopPropagation(); editor.selection.remove(); editor.html.fillEmptyBlocks(); regular_backspace = false; } editor.placeholder.refresh(); } /** * DELETE */ function _del (e) { e.preventDefault(); e.stopPropagation(); // There is no selection. if (editor.selection.text() === '') { editor.cursor.del(); } // We have text selected. else { editor.selection.remove(); } editor.placeholder.refresh(); } /** * SPACE */ function _space (e) { var el = editor.selection.element(); // Do nothing on mobile. // Browser is Mozilla or we're inside a link tag. if (!editor.helpers.isMobile() && (editor.browser.mozilla || (el && el.tagName == 'A'))) { e.preventDefault(); e.stopPropagation(); if (!editor.selection.isCollapsed()) editor.selection.remove(); var marker = editor.markers.insert(); if (marker) { var prev_node = marker.previousSibling; var next_node = marker.nextSibling; if (!next_node && marker.parentNode && marker.parentNode.tagName == 'A') { marker.parentNode.insertAdjacentHTML('afterend', ' ' + $.FE.MARKERS); marker.parentNode.removeChild(marker); } else { if (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 1 && prev_node.textContent.charCodeAt(0) == 160) { prev_node.textContent = prev_node.textContent + ' '; } else { marker.insertAdjacentHTML('beforebegin', ' ') } marker.outerHTML = $.FE.MARKERS; } editor.selection.restore(); } } } /** * Handle typing in Korean for FF. */ function _input () { // Select is collapsed and we're not using IME. if (editor.browser.mozilla && editor.selection.isCollapsed() && !IME) { var range = editor.selection.ranges(0); var start_container = range.startContainer; var start_offset = range.startOffset; // Start container is text and last char before cursor is space. if (start_container && start_container.nodeType == Node.TEXT_NODE && start_offset <= start_container.textContent.length && start_offset > 0 && start_container.textContent.charCodeAt(start_offset - 1) == 32) { editor.selection.save(); editor.spaces.normalize(); editor.selection.restore(); } } } /** * Cut. */ function _cut() { if (editor.selection.isFull()) { setTimeout(function () { var default_tag = editor.html.defaultTag(); if (default_tag) { editor.$el.html('<' + default_tag + '>' + $.FE.MARKERS + '<br/></' + default_tag + '>'); } else { editor.$el.html($.FE.MARKERS + '<br/>'); } editor.selection.restore(); editor.placeholder.refresh(); editor.button.bulkRefresh(); editor.undo.saveStep(); }, 0); } } /** * Tab. */ function _tab (e) { if (editor.opts.tabSpaces > 0) { if (editor.selection.isCollapsed()) { editor.undo.saveStep(); e.preventDefault(); e.stopPropagation(); var str = ''; for (var i = 0; i < editor.opts.tabSpaces; i++) str += ' '; editor.html.insert(str); editor.placeholder.refresh(); editor.undo.saveStep(); } else { e.preventDefault(); e.stopPropagation(); if (!e.shiftKey) { editor.commands.indent(); } else { editor.commands.outdent(); } } } } /** * Map keyPress actions. */ function _mapKeyPress (e) { IME = false; } /** * If is IME. */ function isIME() { return IME; } /** * Map keyDown actions. */ function _mapKeyDown (e) { editor.events.disableBlur(); regular_backspace = true; var key_code = e.which; if (key_code === 16) return true; // Handle Japanese typing. if (key_code === 229) { IME = true; return true; } else { IME = false; } var char_key = (isCharacter(key_code) && !ctrlKey(e)); var del_key = (key_code == $.FE.KEYCODE.BACKSPACE || key_code == $.FE.KEYCODE.DELETE); // 1. Selection is full. // 2. Del key is hit, editor is empty and there is keepFormatOnDelete. if ((editor.selection.isFull() && !editor.opts.keepFormatOnDelete && !editor.placeholder.isVisible()) || (del_key && editor.placeholder.isVisible() && editor.opts.keepFormatOnDelete)) { if (char_key || del_key) { var default_tag = editor.html.defaultTag(); if (default_tag) { editor.$el.html('<' + default_tag + '>' + $.FE.MARKERS + '<br/></' + default_tag + '>'); } else { editor.$el.html($.FE.MARKERS + '<br/>'); } editor.selection.restore(); if (!isCharacter(key_code)) { e.preventDefault(); return true; } } } // ENTER. if (key_code == $.FE.KEYCODE.ENTER) { if (e.shiftKey) { _shiftEnter(e); } else { _enter(e); } } // Backspace. else if (key_code == $.FE.KEYCODE.BACKSPACE && !ctrlKey(e) && !e.altKey) { if (!editor.placeholder.isVisible()) { _backspace(e); } else { e.preventDefault(); e.stopPropagation(); } } // Delete. else if (key_code == $.FE.KEYCODE.DELETE && !ctrlKey(e) && !e.altKey) { if (!editor.placeholder.isVisible()) { _del(e); } else { e.preventDefault(); e.stopPropagation(); } } else if (key_code == $.FE.KEYCODE.SPACE) { _space(e); } else if (key_code == $.FE.KEYCODE.TAB) { _tab(e); } else if (!ctrlKey(e) && isCharacter(e.which) && !editor.selection.isCollapsed() && !e.ctrlKey) { editor.selection.remove(); } editor.events.enableBlur(); } /** * Remove U200B. */ function _replaceU200B (el) { var walker = editor.doc.createTreeWalker(el, NodeFilter.SHOW_TEXT, editor.node.filter(function (node) { return /\u200B/gi.test(node.textContent); }), false); while (walker.nextNode()) { var node = walker.currentNode; node.textContent = node.textContent.replace(/\u200B/gi, ''); } } function _positionCaret () { if (!editor.$wp) return true; var info; if (!editor.opts.height && !editor.opts.heightMax) { // Make sure we scroll bottom. info = editor.position.getBoundingRect().top; // https://github.com/froala/wysiwyg-editor/issues/834. if (editor.opts.toolbarBottom) info += editor.opts.toolbarStickyOffset; if (editor.helpers.isIOS()) info -= editor.helpers.scrollTop(); if (editor.opts.iframe) { info += editor.$iframe.offset().top; } info += editor.opts.toolbarStickyOffset; if (info > editor.o_win.innerHeight - 20) { $(editor.o_win).scrollTop(info + editor.helpers.scrollTop() - editor.o_win.innerHeight + 20); } // Make sure we scroll top. info = editor.position.getBoundingRect().top; // https://github.com/froala/wysiwyg-editor/issues/834. if (!editor.opts.toolbarBottom) info -= editor.opts.toolbarStickyOffset; if (editor.helpers.isIOS()) info -= editor.helpers.scrollTop(); if (editor.opts.iframe) { info += editor.$iframe.offset().top; } if (info < editor.$tb.height() + 20) { $(editor.o_win).scrollTop(info + editor.helpers.scrollTop() - editor.$tb.height() - 20); } } else { // Make sure we scroll bottom. info = editor.position.getBoundingRect().top; if (editor.helpers.isIOS()) info -= editor.helpers.scrollTop(); if (editor.opts.iframe) { info += editor.$iframe.offset().top; } if (info > editor.$wp.offset().top - editor.helpers.scrollTop() + editor.$wp.height() - 20) { editor.$wp.scrollTop(info + editor.$wp.scrollTop() - (editor.$wp.height() + editor.$wp.offset().top) + editor.helpers.scrollTop() + 20); } } } function _iosENTER () { var el = editor.selection.element(); var block_parent = editor.node.blockParent(el); if (block_parent && block_parent.tagName == 'DIV' && editor.selection.info(block_parent).atStart) { var default_tag = editor.html.defaultTag(); if (block_parent.previousSibling && block_parent.previousSibling.tagName != 'DIV' && default_tag && default_tag != 'div') { editor.selection.save(); $(block_parent).replaceWith('<' + default_tag + '>' + block_parent.innerHTML + '</' + default_tag + '>'); editor.selection.restore(); } } } /** * Map keyUp actions. */ function _mapKeyUp (e) { if (editor.helpers.isAndroid && editor.browser.mozilla) { return true; } // IME IE. if (IME) { IME = false; return false; } if (!editor.selection.isCollapsed()) return true; if (e && (e.which === $.FE.KEYCODE.META || e.which == $.FE.KEYCODE.CTRL)) return true; if (e && isArrow(e.which)) return true; if (e && (e.which == $.FE.KEYCODE.ENTER) && editor.helpers.isIOS()) { _iosENTER(); } if (e && (e.which == $.FE.KEYCODE.ENTER || e.which == $.FE.KEYCODE.BACKSPACE || (e.which >= 37 && e.which <= 40 && !editor.browser.msie))) { if (!(e.which == $.FE.KEYCODE.BACKSPACE && regular_backspace)) _positionCaret(); } editor.html.cleanBRs(true, true); // Remove invisible space where possible. var has_invisible = function (node) { if (!node) return false; var text = node.innerHTML; text = text.replace(/<span[^>]*? class\s*=\s*["']?fr-marker["']?[^>]+>\u200b<\/span>/gi, ''); if (text && /\u200B/.test(text) && text.replace(/\u200B/gi, '').length > 0) return true; return false; } var ios_CJK = function (el) { var CJKRegEx = /[\u3041-\u3096\u30A0-\u30FF\u4E00-\u9FFF\u3130-\u318F\uAC00-\uD7AF]/gi; return !editor.helpers.isIOS() || ((el.textContent || '').match(CJKRegEx) || []).length === 0; } // Get the selection element. var el = editor.selection.element(); if (has_invisible(el) && !editor.node.hasClass(el, 'fr-marker') && el.tagName != 'IFRAME' && ios_CJK(el)) { editor.selection.save(); _replaceU200B(el); editor.selection.restore(); } } // Check if we should consider that CTRL key is pressed. function ctrlKey (e) { if (navigator.userAgent.indexOf('Mac OS X') != -1) { if (e.metaKey && !e.altKey) return true; } else { if (e.ctrlKey && !e.altKey) return true; } return false; } function isArrow (key_code) { if (key_code >= $.FE.KEYCODE.ARROW_LEFT && key_code <= $.FE.KEYCODE.ARROW_DOWN) { return true; } } function isCharacter (key_code) { if (key_code >= $.FE.KEYCODE.ZERO && key_code <= $.FE.KEYCODE.NINE) { return true; } if (key_code >= $.FE.KEYCODE.NUM_ZERO && key_code <= $.FE.KEYCODE.NUM_MULTIPLY) { return true; } if (key_code >= $.FE.KEYCODE.A && key_code <= $.FE.KEYCODE.Z) { return true; } // Safari sends zero key code for non-latin characters. if (editor.browser.webkit && key_code === 0) { return true; } switch (key_code) { case $.FE.KEYCODE.SPACE: case $.FE.KEYCODE.QUESTION_MARK: case $.FE.KEYCODE.NUM_PLUS: case $.FE.KEYCODE.NUM_MINUS: case $.FE.KEYCODE.NUM_PERIOD: case $.FE.KEYCODE.NUM_DIVISION: case $.FE.KEYCODE.SEMICOLON: case $.FE.KEYCODE.FF_SEMICOLON: case $.FE.KEYCODE.DASH: case $.FE.KEYCODE.EQUALS: case $.FE.KEYCODE.FF_EQUALS: case $.FE.KEYCODE.COMMA: case $.FE.KEYCODE.PERIOD: case $.FE.KEYCODE.SLASH: case $.FE.KEYCODE.APOSTROPHE: case $.FE.KEYCODE.SINGLE_QUOTE: case $.FE.KEYCODE.OPEN_SQUARE_BRACKET: case $.FE.KEYCODE.BACKSLASH: case $.FE.KEYCODE.CLOSE_SQUARE_BRACKET: return true; default: return false; } } var _typing_timeout; var _temp_snapshot; function _typingKeyDown (e) { var keycode = e.which; if (ctrlKey(e) || (keycode >= 37 && keycode <= 40) || (!isCharacter(keycode) && keycode != $.FE.KEYCODE.DELETE && keycode != $.FE.KEYCODE.BACKSPACE && keycode != $.FE.KEYCODE.ENTER)) return true; if (!_typing_timeout) { _temp_snapshot = editor.snapshot.get(); } clearTimeout(_typing_timeout); _typing_timeout = setTimeout(function () { _typing_timeout = null; editor.undo.saveStep(); }, Math.max(250, editor.opts.typingTimer)); } function _typingKeyUp (e) { var keycode = e.which; if (ctrlKey(e) || (keycode >= 37 && keycode <= 40)) return true; if (_temp_snapshot && _typing_timeout) { editor.undo.saveStep(_temp_snapshot); _temp_snapshot = null; } } function forceUndo () { if (_typing_timeout) { clearTimeout(_typing_timeout); editor.undo.saveStep(); _temp_snapshot = null; } } /** * Check if key event is part of browser accessibility. */ function isBrowserAction (e) { var keycode = e.which; return ctrlKey(e) || keycode == $.FE.KEYCODE.F5; } /** * Tear up. */ function _init () { editor.events.on('keydown', _typingKeyDown); editor.events.on('input', _input); editor.events.on('keyup input', _typingKeyUp); // Register for handling. editor.events.on('keypress', _mapKeyPress); editor.events.on('keydown', _mapKeyDown); editor.events.on('keyup', _mapKeyUp); editor.events.on('html.inserted', _mapKeyUp); // Handle cut. editor.events.on('cut', _cut); // IME if (!editor.browser.edge && editor.el.msGetInputContext) { try { editor.el.msGetInputContext().addEventListener('MSCandidateWindowShow', function () { IME = true; }) editor.el.msGetInputContext().addEventListener('MSCandidateWindowHide', function () { IME = false; _mapKeyUp(); }) } catch (ex) { } } } return { _init: _init, ctrlKey: ctrlKey, isCharacter: isCharacter, isArrow: isArrow, forceUndo: forceUndo, isIME: isIME, isBrowserAction: isBrowserAction } }; $.FE.MODULES.accessibility = function (editor) { // Flag to tell if mouseenter can blur popup elements with tabindex. This is in case that popup shows over the cursor so mouseenter should not blur immediately. // FireFox issue. var can_blur = true; /* * Focus an element. */ function focusToolbarElement ($el) { // Check if it is empty. if (!$el || !$el.length) { return; } // Add blur event handler on the element that do not reside on a popup. if (!$el.data('blur-event-set') && !$el.parents('.fr-popup').length) { // Set shared event for blur on element because it resides in a popup. editor.events.$on($el, 'blur', function (e) { // Get current instance. var inst = $el.parents('.fr-toolbar, .fr-popup').data('instance') || editor; // Check if we should actually trigger blur. if (inst.events.blurActive()) { inst.events.trigger('blur'); } // Allow blur. inst.events.enableBlur(); }, true); $el.data('blur-event-set', true); } // Get current instance. var inst = $el.parents('.fr-toolbar, .fr-popup').data('instance') || editor; // Do not allow blur on the editor until element focus. inst.events.disableBlur(); $el.focus(); // Store it as the current focused element. editor.shared.$f_el = $el; } /* * Focus first or last toolbar button. */ function focusToolbar ($tb, last) { var position = last ? 'last' : 'first'; var $btn = $tb.find('button:visible:not(.fr-disabled), .fr-group span.fr-command:visible')[position](); if ($btn.length) { focusToolbarElement($btn); return true; } } /* * Focus a popup content element. */ function focusContentElement ($el) { // Save editor selection only if the element we want to focus is input text or textarea. if ($el.is('input, textarea')) { saveSelection(); } editor.events.disableBlur(); $el.focus(); return true; } /* * Focus popup's content. */ function focusContent ($content, backward) { // First input. var $first_input = $content.find('input, textarea, button, select').filter(':visible').not(':disabled').filter(backward ? ':last' : ':first'); if ($first_input.length) { return focusContentElement($first_input); } if (editor.shared.with_kb) { // Active item. var $active_item = $content.find('.fr-active-item:visible:first'); if ($active_item.length) { return focusContentElement($active_item); } // First element with tabindex. var $first_tab_index = $content.find('[tabIndex]:visible:first'); if ($first_tab_index.length) { return focusContentElement($first_tab_index); } } } function saveSelection () { if (editor.$el.find('.fr-marker').length === 0 && editor.core.hasFocus()) { editor.selection.save(); } } function restoreSelection (inst) { // Restore selection. if (inst.$el.find('.fr-marker').length) { inst.events.disableBlur(); inst.selection.restore(); inst.events.enableBlur(); } } /* * Focus popup. */ function focusPopup ($popup) { // Get popup content without fr-buttons toolbar. var $popup_content = $popup.children().not('.fr-buttons'); // Blur popup on mouseenter. if (!$popup_content.data('mouseenter-event-set')) { editor.events.$on($popup_content, 'mouseenter', '[tabIndex]', function (e) { var inst = $popup.data('instance') || editor; // FireFox issue. if (!can_blur) { // Popup showed over the cursor. e.stopPropagation(); e.preventDefault(); return; } var $focused_item = $popup_content.find(':focus:first'); if ($focused_item.length && !$focused_item.is('input, button, textarea')) { inst.events.disableBlur(); $focused_item.blur(); inst.events.disableBlur(); inst.events.focus(); } }); $popup_content.data('mouseenter-event-set', true); } // Focus content if possible, else focus toolbar if the popup is opened with keyboard. if (!focusContent($popup_content) && editor.shared.with_kb) { focusToolbar($popup.find('.fr-buttons')); } } /* * Focus modal. */ function focusModal ($modal) { // Make sure we have focus on editing area. if (!editor.core.hasFocus()) { editor.events.disableBlur(); editor.events.focus(); } // Save selection. editor.accessibility.saveSelection(); editor.events.disableBlur(); // Blur editor and clear selection to enable arrow keys scrolling. editor.$el.blur(); editor.selection.clear(); editor.events.disableBlur(); if (editor.shared.with_kb) { $modal.find('.fr-command[tabIndex]:first').focus(); } else { $modal.find('[tabIndex]:first').focus(); } } /* * Focus popup toolbar or main toolbar. */ function focusToolbars () { // Look for active popup. var $popup = editor.popups.areVisible(); if ($popup) { var $tb = $popup.find('.fr-buttons'); if (!$tb.find('button:focus, .fr-group span:focus').length) { return !focusToolbar($tb); } else { return !focusToolbar($popup.data('instance').$tb) } } // Focus main toolbar if no others were found. return !focusToolbar(editor.$tb); } /* * Get the dropdown button that is active and is focused or is active and its commands are focused. */ function _getActiveFocusedDropdown () { var $activeDropdown = null; // Is active and focused. if (editor.shared.$f_el.is('.fr-dropdown.fr-active')) { $activeDropdown = editor.shared.$f_el; } // Is active and its commands are focused. editor.shared.$f_el is a dropdown command. else if (editor.shared.$f_el.closest('.fr-dropdown-menu').prev().is('.fr-dropdown.fr-active')) { $activeDropdown = editor.shared.$f_el.closest('.fr-dropdown-menu').prev(); } return $activeDropdown; } function _moveHorizontally ($tb, tab_key, forward) { if (editor.shared.$f_el) { var $activeDropdown = _getActiveFocusedDropdown(); // A focused active dropdown button. if ($activeDropdown) { // Unclick. editor.button.click($activeDropdown); editor.shared.$f_el = $activeDropdown; } // Focus the next/previous button. // Get all toobar buttons. var $buttons = $tb.find('button:visible:not(.fr-disabled), .fr-group span.fr-command:visible'); // Get focused button position. var index = $buttons.index(editor.shared.$f_el); // Last or first button reached. if ((index == 0 && !forward) || (index == $buttons.length - 1 && forward)) { var status; // Focus content if last or first toolbar button is reached. if (tab_key) { if ($tb.parent().is('.fr-popup')) { var $popup_content = $tb.parent().children().not('.fr-buttons') status = !focusContent($popup_content, !forward); } if (status === false) { editor.shared.$f_el = null; } } // Arrow used or popup listeners were not active. if (!tab_key || status !== false) { // Focus to the opposite side button of the toolbar. focusToolbar($tb, !forward); } } else { // Focus next or previous button. focusToolbarElement($($buttons.get(index + (forward ? 1 : -1)))); } return false; } } function moveForward ($tb, tab_key) { return _moveHorizontally($tb, tab_key, true); } function moveBackward ($tb, tab_key) { return _moveHorizontally($tb, tab_key); } function _moveVertically (down) { if (editor.shared.$f_el) { // Dropdown button. if (editor.shared.$f_el.is('.fr-dropdown.fr-active')) { // Focus the first/last dropdown command. var $destination; if (down) { $destination = editor.shared.$f_el.next().find('.fr-command:not(.fr-disabled)').first(); } else { $destination = editor.shared.$f_el.next().find('.fr-command:not(.fr-disabled)').last(); } focusToolbarElement($destination); return false; } // Dropdown command. else if (editor.shared.$f_el.is('a.fr-command')) { // Focus the previous/next dropdown command. var $destination; if (down) { $destination = editor.shared.$f_el.closest('li').nextAll(':visible:first').find('.fr-command:not(.fr-disabled)').first(); } else { $destination = editor.shared.$f_el.closest('li').prevAll(':visible:first').find('.fr-command:not(.fr-disabled)').first(); } // Last or first button reached: Focus to the opposite side element of the dropdown. if (!$destination.length) { if (down) { $destination = editor.shared.$f_el.closest('.fr-dropdown-menu').find('.fr-command:not(.fr-disabled)').first(); } else { $destination = editor.shared.$f_el.closest('.fr-dropdown-menu').find('.fr-command:not(.fr-disabled)').last(); } } focusToolbarElement($destination); return false; } } } function moveDown () { // Also enable dropdown opening on arrow down. if (editor.shared.$f_el && editor.shared.$f_el.is('.fr-dropdown:not(.fr-active)')) { return enter(); } else { return _moveVertically(true); } } function moveUp () { return _moveVertically(); } function enter () { if (editor.shared.$f_el) { // Check if the focused element is a dropdown button. if (editor.shared.$f_el.hasClass('fr-dropdown')) { // Do click and focus the first dropdown item. editor.button.click(editor.shared.$f_el); } else if (editor.shared.$f_el.is('button.fr-back')) { if (editor.opts.toolbarInline) { editor.events.disableBlur(); editor.events.focus(); } var $popup = editor.popups.areVisible(editor); // Previous popup will show up so we need to not default focus the popup because back popup button have to be focused. if ($popup) { editor.shared.with_kb = false; } editor.button.click(editor.shared.$f_el); // Focus back popup button. focusPopupButton($popup); } else { editor.events.disableBlur(); editor.button.click(editor.shared.$f_el); if (editor.shared.$f_el.attr('data-popup')) { // Attach button to visible popup. var $visible_popup = editor.popups.areVisible(editor); if ($visible_popup) $visible_popup.data('popup-button', editor.shared.$f_el); } else if (editor.shared.$f_el.attr('data-modal')) { // Attach button to visible modal. var $visible_modal = editor.modals.areVisible(editor); if ($visible_modal) $visible_modal.data('modal-button', editor.shared.$f_el); } editor.shared.$f_el = null; } return false; } } function focusEditor () { if (editor.shared.$f_el) { editor.events.disableBlur(); editor.shared.$f_el.blur(); editor.shared.$f_el = null; } // Trigger custom behavior. if (editor.events.trigger('toolbar.focusEditor') === false) { return; } editor.events.disableBlur(); editor.events.focus(); } function esc ($tb) { if (editor.shared.$f_el) { var $activeDropdown = _getActiveFocusedDropdown(); // Active focused dropdown. if ($activeDropdown) { // Unclick. editor.button.click($activeDropdown); // Focus the unactive dropdown. focusToolbarElement($activeDropdown); } // Toolbar contains a back button. else if ($tb.parent().find('.fr-back:visible').length) { editor.shared.with_kb = false; if (editor.opts.toolbarInline) { // Toolbar inline needs focus in order to show up. editor.events.disableBlur(); editor.events.focus(); } editor.button.exec($tb.parent().find('.fr-back:visible:first')); // Focus back popup button. focusPopupButton($tb.parent()); } // A toolbar that gets opened from the editable area. else if (editor.shared.$f_el.is('button, .fr-group span')) { if ($tb.parent().is('.fr-popup')) { // Restore selection. restoreSelection(editor); editor.shared.$f_el = null; // Trigger custom behaviour. if (editor.events.trigger('toolbar.esc') !== false) { // Default behaviour. // Hide popup. editor.popups.hide($tb.parent()); // Show inline toolbar. if (editor.opts.toolbarInline) editor.toolbar.showInline(null, true); // Focus back popup button. focusPopupButton($tb.parent()); } } else { focusEditor(); } } return false; } } /* * Execute shortcut. */ function exec (e, $tb) { var ctrlKey = navigator.userAgent.indexOf('Mac OS X') != -1 ? e.metaKey : e.ctrlKey; var keycode = e.which; var status = false; // Tab. if (keycode == $.FE.KEYCODE.TAB && !ctrlKey && !e.shiftKey && !e.altKey){ status = moveForward($tb, true); } // Arrow right -> . else if (keycode == $.FE.KEYCODE.ARROW_RIGHT && !ctrlKey && !e.shiftKey && !e.altKey){ status = moveForward($tb); } // Shift + Tab. else if (keycode == $.FE.KEYCODE.TAB && !ctrlKey && e.shiftKey && !e.altKey){ status = moveBackward($tb, true); } // Arrow left <- . else if (keycode == $.FE.KEYCODE.ARROW_LEFT && !ctrlKey && !e.shiftKey && !e.altKey){ status = moveBackward($tb); } // Arrow up. else if (keycode == $.FE.KEYCODE.ARROW_UP && !ctrlKey && !e.shiftKey && !e.altKey){ status = moveUp(); } // Arrow down. else if (keycode == $.FE.KEYCODE.ARROW_DOWN && !ctrlKey && !e.shiftKey && !e.altKey){ status = moveDown(); } // Enter. else if (keycode == $.FE.KEYCODE.ENTER && !ctrlKey && !e.shiftKey && !e.altKey){ status = enter(); } // Esc. else if (keycode == $.FE.KEYCODE.ESC && !ctrlKey && !e.shiftKey && !e.altKey){ status = esc($tb); } // Alt + F10. else if (keycode == $.FE.KEYCODE.F10 && !ctrlKey && !e.shiftKey && e.altKey) { status = focusToolbars(); } // No focused element and no action done. Eg: popup is opened. if (!editor.shared.$f_el && status === undefined) { status = true; } // Check if key event is a browser action. Eg: Ctrl + R. if (!status && editor.keys.isBrowserAction(e)) { status = true; } // Propagate to the next key listeners. if (status) { return true; } else { e.preventDefault(); e.stopPropagation(); return false; } } /* * Register a toolbar to keydown event. */ function registerToolbar ($tb) { if (!$tb || !$tb.length) { return; } // Hitting keydown on toolbar. editor.events.$on($tb, 'keydown', function (e) { // Allow only buttons.fr-command. if (!$(e.target).is('a.fr-command, button.fr-command, .fr-group span.fr-command')) { return true; } // Get the current editor instance for the popup. var inst = $tb.parents('.fr-popup').data('instance') || $tb.data('instance') || editor; // Keyboard used. editor.shared.with_kb = true; var status = inst.accessibility.exec(e, $tb); editor.shared.with_kb = false; return status; }, true); // Unfocus the toolbar on mouseenter. editor.events.$on($tb, 'mouseenter', '[tabIndex]', function (e) { var inst = $tb.parents('.fr-popup').data('instance') || $tb.data('instance') || editor; // FireFox issue. if (!can_blur) { // Popup showed over the cursor. e.stopPropagation(); e.preventDefault(); return; } else { var $hovered_el = $(e.currentTarget); if (inst.shared.$f_el && inst.shared.$f_el.not($hovered_el)) { inst.accessibility.focusEditor(); } } }, true); } /* * Register a popup to a keydown event. */ function registerPopup (id) { var $popup = editor.popups.get(id); var ev = _getPopupEvents(id); // Register popup toolbar. registerToolbar($popup.find('.fr-buttons')); // Clear popup button on mouseenter. editor.events.$on($popup, 'mouseenter', 'tabIndex', ev._tiMouseenter, true); // Keydown handler on every element that has tabIndex. editor.events.$on($popup.children().not('.fr-buttons'), 'keydown', '[tabIndex]', ev._tiKeydown, true); // Restore selection on popups hide for the current active popup. editor.popups.onHide(id, function () { restoreSelection($popup.data('instance') || editor); }) // FireFox issue: Prevent immediate popup bluring. Popup could show up over the cursor. editor.popups.onShow(id, function () { can_blur = false; setTimeout(function () { can_blur = true; }, 0); }); } /* * Get popup events. */ function _getPopupEvents (id) { var $popup = editor.popups.get(id); return { /** * Keydown on an input. */ _tiKeydown: function (e) { var inst = $popup.data('instance') || editor; // See if plugins listeners are active. if (inst.events.trigger('popup.tab', [e]) === false) { return false; } var key_code = e.which; var $focused_item = $popup.find(':focus:first'); // Tabbing. if ($.FE.KEYCODE.TAB == key_code) { e.preventDefault(); // Focus next/previous input. var $popup_content = $popup.children().not('.fr-buttons'); var inputs = $popup_content.find('input, textarea, button, select').filter(':visible').not('.fr-no-touch input, .fr-no-touch textarea, .fr-no-touch button, .fr-no-touch select, :disabled').toArray(); var idx = inputs.indexOf(this) + (e.shiftKey ? -1 : 1); if (0 <= idx && idx < inputs.length) { inst.events.disableBlur(); $(inputs[idx]).focus(); e.stopPropagation(); return false; } // Focus toolbar. var $tb = $popup.find('.fr-buttons'); if ($tb.length && focusToolbar($tb, (e.shiftKey ? true : false))) { e.stopPropagation(); return false; } // Focus content. if (focusContent($popup_content)) { e.stopPropagation(); return false; } } // ENTER. else if ($.FE.KEYCODE.ENTER == key_code) { var $active_button = null; if ($popup.find('.fr-submit:visible').length > 0) { $active_button = $popup.find('.fr-submit:visible:first'); } else if ($popup.find('.fr-dismiss:visible').length) { $active_button = $popup.find('.fr-dismiss:visible:first'); } if ($active_button) { e.preventDefault(); e.stopPropagation(); inst.events.disableBlur(); inst.button.exec($active_button); } } // ESC. else if ($.FE.KEYCODE.ESC == key_code) { e.preventDefault(); e.stopPropagation(); // Restore selection. restoreSelection(inst); if (inst.popups.isVisible(id) && $popup.find('.fr-back:visible').length) { if (inst.opts.toolbarInline) { // Toolbar inline needs focus in order to show up. inst.events.disableBlur(); inst.events.focus(); } inst.button.exec($popup.find('.fr-back:visible:first')); // Focus back popup button. focusPopupButton($popup); } else if (inst.popups.isVisible(id) && $popup.find('.fr-dismiss:visible').length) { inst.button.exec($popup.find('.fr-dismiss:visible:first')); } else { inst.popups.hide(id); if (inst.opts.toolbarInline) inst.toolbar.showInline(null, true); // Focus back popup button. focusPopupButton($popup); } return false; } // Allow space. else if ($.FE.KEYCODE.SPACE == key_code && ($focused_item.is('.fr-submit') || $focused_item.is('.fr-dismiss'))) { e.preventDefault(); e.stopPropagation(); inst.events.disableBlur(); inst.button.exec($focused_item); return true; } // Other KEY. Stop propagation to the window. else { // Check if key event is a browser action. Eg: Ctrl + R. if (inst.keys.isBrowserAction(e)) { e.stopPropagation(); return; } if ($focused_item.is('input[type=text], textarea')) { e.stopPropagation(); return; } if ($.FE.KEYCODE.SPACE == key_code && ($focused_item.is('.fr-link-attr') || $focused_item.is('input[type=file]'))) { e.stopPropagation(); return; } e.stopPropagation(); e.preventDefault(); return false; } }, _tiMouseenter: function (e) { var inst = $popup.data('instance') || editor; _clearPopupButton(inst); } } } /* * Focus the button from which the popup was showed. */ function focusPopupButton ($popup) { var $popup_button = $popup.data('popup-button'); if ($popup_button) { setTimeout(function () { focusToolbarElement($popup_button); $popup.data('popup-button', null); }, 0); } } /* * Focus the button from which the modal was showed. */ function focusModalButton ($modal) { var $modal_button = $modal.data('modal-button'); if ($modal_button) { setTimeout(function () { focusToolbarElement($modal_button); $modal.data('modal-button', null); }, 0); } } function hasFocus () { return editor.shared.$f_el != null; } function _clearPopupButton (inst) { var $visible_popup = editor.popups.areVisible(inst); if ($visible_popup) { $visible_popup.data('popup-button', null); } } function _editorKeydownHandler (e) { var ctrlKey = navigator.userAgent.indexOf('Mac OS X') != -1 ? e.metaKey : e.ctrlKey; var keycode = e.which; // Alt + F10. if (keycode == $.FE.KEYCODE.F10 && !ctrlKey && !e.shiftKey && e.altKey) { // Keyboard used. editor.shared.with_kb = true; // Focus active popup content inside the current editor if possible, else focus an available toolbar. var $visible_popup = editor.popups.areVisible(editor); var focused_content = false; if ($visible_popup) { focused_content = focusContent($visible_popup.children().not('.fr-buttons')); } if (!focused_content) { focusToolbars(); } editor.shared.with_kb = false; e.preventDefault(); e.stopPropagation(); return false; } return true; } /** * Initialize. */ function _init () { // Key down on the editing area. if (editor.$wp) { editor.events.on('keydown', _editorKeydownHandler, true); } else { editor.events.$on(editor.$win, 'keydown', _editorKeydownHandler, true); } // Mousedown on the editing area. editor.events.on('mousedown', function (e) { _clearPopupButton(editor); if (editor.shared.$f_el) { restoreSelection(editor); e.stopPropagation(); editor.events.disableBlur(); editor.shared.$f_el = null; } }, true); // Blur on the editing area. editor.events.on('blur', function (e) { editor.shared.$f_el = null; _clearPopupButton(editor); }, true); } return { _init: _init, registerPopup: registerPopup, registerToolbar: registerToolbar, focusToolbarElement: focusToolbarElement, focusToolbar: focusToolbar, focusContent: focusContent, focusPopup: focusPopup, focusModal: focusModal, focusEditor: focusEditor, focusPopupButton: focusPopupButton, focusModalButton: focusModalButton, hasFocus: hasFocus, exec: exec, saveSelection: saveSelection, restoreSelection: restoreSelection } } $.FE.MODULES.format = function (editor) { /** * Create open tag string. */ function _openTag (tag, attrs) { var str = '<' + tag; for (var key in attrs) { if (attrs.hasOwnProperty(key)) { str += ' ' + key + '="' + attrs[key] + '"'; } } str += '>'; return str; } /** * Create close tag string. */ function _closeTag (tag) { return '</' + tag + '>'; } /** * Create query for the current format. */ function _query (tag, attrs) { var selector = tag; for (var key in attrs) { if (attrs.hasOwnProperty(key)) { if (key == 'id') tag += '#' + attrs[key]; else if (key == 'class') tag += '.' + attrs[key]; else tag += '[' + key + '="' + attrs[key] + '"]'; } } return selector; } /** * Test matching element. */ function _matches (el, selector) { if (!el || el.nodeType != Node.ELEMENT_NODE) return false; return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); } /** * Apply format to the current node till we find a marker. */ function _processNodeFormat (start_node, tag, attrs) { // No start node. if (!start_node) return; // If we are in a block process starting with the first child. if (editor.node.isBlock(start_node)) { _processNodeFormat(start_node.firstChild, tag, attrs); return false; } // Create new element. var $span = $(_openTag(tag, attrs)).insertBefore(start_node); // Start with the next sibling of the current node. var node = start_node; // Search while there is a next node. // Next node is not marker. // Next node does not contain marker. while (node && !$(node).is('.fr-marker') && $(node).find('.fr-marker').length == 0) { var tmp = node; node = node.nextSibling; $span.append(tmp); } // If there is no node left at the right look at parent siblings. if (!node) { var p_node = $span.get(0).parentNode; while (p_node && !p_node.nextSibling && !editor.node.isElement(p_node)) { p_node = p_node.parentNode; } if (p_node) { var sibling = p_node.nextSibling; if (sibling) { // Parent sibling is block then look next. if (!editor.node.isBlock(sibling)) { _processNodeFormat(sibling, tag, attrs); } else { _processNodeFormat(sibling.firstChild, tag, attrs); } } } } // Start processing child nodes if there is a marker. else if ($(node).find('.fr-marker').length) { _processNodeFormat(node.firstChild, tag, attrs); } if ($span.is(':empty')) { $span.remove(); } } /** * Apply tag format. */ function apply (tag, attrs) { if (typeof attrs == 'undefined') attrs = {}; if (attrs.style) { delete attrs.style; } // Selection is collapsed. if (editor.selection.isCollapsed()) { editor.markers.insert(); var $marker = editor.$el.find('.fr-marker'); $marker.replaceWith(_openTag(tag, attrs) + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + _closeTag(tag)); editor.selection.restore(); } // Selection is not collapsed. else { editor.selection.save(); // Check if selection can be deleted. var start_marker = editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling; _processNodeFormat(start_marker, tag, attrs); // Clean inner spans. var inner_spans; do { inner_spans = editor.$el.find(_query(tag, attrs) + ' > ' + _query(tag, attrs)); inner_spans.each (function () { $(this).replaceWith(this.innerHTML); }); } while (inner_spans.length); editor.el.normalize(); // Have markers inside the new tag. var markers = editor.el.querySelectorAll('.fr-marker'); for (var i = 0; i < markers.length; i++) { var $mk = $(markers[i]); if ($mk.data('type') == true) { if (_matches($mk.get(0).nextSibling, _query(tag, attrs))) { $mk.next().prepend($mk); } } else { if (_matches($mk.get(0).previousSibling, _query(tag, attrs))) { $mk.prev().append($mk); } } } editor.selection.restore(); } } /** * Split at current node the parents with tag. */ function _split ($node, tag, attrs, collapsed) { if (!collapsed) { var changed = false; if ($node.data('type') === true) { while (editor.node.isFirstSibling($node.get(0)) && !$node.parent().is(editor.$el)) { $node.parent().before($node); changed = true; } } else if ($node.data('type') === false) { while (editor.node.isLastSibling($node.get(0)) && !$node.parent().is(editor.$el)) { $node.parent().after($node); changed = true; } } if (changed) return true; } // Check if current node has parents which match our tag. if ($node.parents(tag).length || typeof tag == 'undefined') { var close_str = ''; var open_str = ''; var $p_node = $node.parent(); // Do not split when parent is block. if ($p_node.is(editor.$el) || editor.node.isBlock($p_node.get(0))) return false; // Check undefined so that we while ((typeof tag == 'undefined' && !editor.node.isBlock($p_node.parent().get(0))) || (typeof tag != 'undefined' && !_matches($p_node.get(0), _query(tag, attrs)))) { close_str = close_str + editor.node.closeTagString($p_node.get(0)); open_str = editor.node.openTagString($p_node.get(0)) + open_str; $p_node = $p_node.parent(); } // Node STR. var node_str = $node.get(0).outerHTML; // Replace node with marker. $node.replaceWith('<span id="mark"></span>'); // Rebuild the HTML for the node. var p_html = $p_node.html().replace(/<span id="mark"><\/span>/, close_str + editor.node.closeTagString($p_node.get(0)) + open_str + node_str + close_str + editor.node.openTagString($p_node.get(0)) + open_str); $p_node.replaceWith(editor.node.openTagString($p_node.get(0)) + p_html + editor.node.closeTagString($p_node.get(0))); return true; } return false; } /** * Process node remove. */ function _processNodeRemove ($node, should_remove, tag, attrs) { // Get contents. var contents = editor.node.contents($node.get(0)); // Loop contents. for (var i = 0; i < contents.length; i++) { var node = contents[i]; // We found a marker => change should_remove flag. if (editor.node.hasClass(node, 'fr-marker')) { should_remove = (should_remove + 1) % 2; } // We should remove. else if (should_remove) { // Check if we have a marker inside it. if ($(node).find('.fr-marker').length > 0) { should_remove = _processNodeRemove($(node), should_remove, tag, attrs); } // Remove everything starting with the most inner nodes. else { $($(node).find(tag || '*').get().reverse()).each(function() { if (!editor.node.isBlock(this) && !editor.node.isVoid(this)) { $(this).replaceWith(this.innerHTML); } }); // Check inner nodes. if ((typeof tag == 'undefined' && node.nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(node) && !editor.node.isBlock(node)) || _matches(node, _query(tag, attrs))) { $(node).replaceWith(node.innerHTML); } // Remove formatting from block nodes. else if (typeof tag == 'undefined' && node.nodeType == Node.ELEMENT_NODE && editor.node.isBlock(node)) { editor.node.clearAttributes(node); } } } else { // There is a marker. if ($(node).find('.fr-marker').length > 0) { should_remove = _processNodeRemove($(node), should_remove, tag, attrs); } } } return should_remove; } /** * Remove tag. */ function remove (tag, attrs) { if (typeof attrs == 'undefined') attrs = {}; if (attrs.style) { delete attrs.style; } var collapsed = editor.selection.isCollapsed(); editor.selection.save(); // Split at start and end marker. var reassess = true; while (reassess) { reassess = false; var markers = editor.$el.find('.fr-marker'); for (var i = 0; i < markers.length; i++) { if (_split($(markers[i]), tag, attrs, collapsed)) { reassess = true; break; } } } // Remove format between markers. _processNodeRemove(editor.$el, 0, tag, attrs); // Selection is collapsed => add invisible spaces. if (collapsed) { editor.$el.find('.fr-marker').before($.FE.INVISIBLE_SPACE).after($.FE.INVISIBLE_SPACE); } editor.html.cleanEmptyTags(); editor.el.normalize(); editor.selection.restore(); } /** * Toggle format. */ function toggle (tag, attrs) { if (is(tag, attrs)) { remove(tag, attrs); } else { apply(tag, attrs); } } /** * Clean format. */ function _cleanFormat (elem, prop) { var $elem = $(elem); $elem.css(prop, ''); if ($elem.attr('style') === '') { $elem.replaceWith($elem.html()); } } /** * Filter spans with specific property. */ function _filterSpans (elem, prop) { return $(elem).attr('style').indexOf(prop + ':') === 0 || $(elem).attr('style').indexOf(';' + prop + ':') >= 0 || $(elem).attr('style').indexOf('; ' + prop + ':') >= 0; }; /** * Apply inline style. */ function applyStyle (prop, val) { // Selection is collapsed. if (editor.selection.isCollapsed()) { editor.markers.insert(); var $marker = editor.$el.find('.fr-marker'); var $parent = $marker.parent(); // https://github.com/froala/wysiwyg-editor/issues/1084 if (editor.node.openTagString($parent.get(0)) == '<span style="' + prop + ': ' + $parent.css(prop) + ';">') { if (editor.node.isEmpty($parent.get(0))) { $parent.replaceWith('<span style="' + prop + ': ' + val + ';">' + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + '</span>'); } // We should get out of the current span with the same props. else { var x = {}; x[prop] = val; _split($marker, 'span', x, true); $marker = editor.$el.find('.fr-marker'); $marker.replaceWith('<span style="' + prop + ': ' + val + ';">' + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + '</span>'); } } else if (editor.node.isEmpty($parent.get(0)) && $parent.is('span')) { $marker.replaceWith($.FE.MARKERS); $parent.css(prop, val); } else { $marker.replaceWith('<span style="' + prop + ': ' + val + ';">' + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + '</span>'); } editor.selection.restore(); } else { editor.selection.save(); // When removing selection we should make sure we have selection outside of the first/last parent node. // Same applies to applying style because we have to wrap U tags with SPAN so that it keeps the color. var markers = editor.$el.find('.fr-marker'); for (var i = 0; i < markers.length; i++) { var $marker = $(markers[i]); if ($marker.data('type') === true) { while (editor.node.isFirstSibling($marker.get(0)) && !$marker.parent().is(editor.$el)) { $marker.parent().before($marker); } } else { while (editor.node.isLastSibling($marker.get(0)) && !$marker.parent().is(editor.$el)) { $marker.parent().after($marker); } } } // Check if selection can be deleted. var start_marker = editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling; var attrs = { 'class': 'fr-unprocessed' }; if (val) attrs.style = prop + ': ' + val + ';' _processNodeFormat(start_marker, 'span', attrs); editor.$el.find('.fr-marker + .fr-unprocessed').each(function () { $(this).prepend($(this).prev()); }); editor.$el.find('.fr-unprocessed + .fr-marker').each(function () { $(this).prev().append(this); }); while (editor.$el.find('span.fr-unprocessed').length > 0) { var $span = editor.$el.find('span.fr-unprocessed:first').removeClass('fr-unprocessed'); // Look at parent node to see if we can merge with it. $span.parent().get(0).normalize(); if ($span.parent().is('span') && $span.parent().get(0).childNodes.length == 1) { $span.parent().css(prop, val); var $child = $span; $span = $span.parent(); $child.replaceWith($child.html()); } // Replace in reverse order to take care of the inner spans first. var inner_spans = $span.find('span'); for (var i = inner_spans.length - 1; i >= 0; i--) { _cleanFormat(inner_spans[i], prop); } // Look at parents with the same property. var $outer_span = $span.parentsUntil(editor.$el, 'span[style]').filter(function() { return _filterSpans(this, prop); }); if ($outer_span.length) { var c_str = ''; var o_str = ''; var ic_str = ''; var io_str = ''; var c_node = $span.get(0); do { c_node = c_node.parentNode; $(c_node).addClass('fr-split'); c_str = c_str + editor.node.closeTagString(c_node); o_str = editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + o_str; // Inner close and open. if ($outer_span.get(0) != c_node) { ic_str = ic_str + editor.node.closeTagString(c_node); io_str = editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + io_str; } } while ($outer_span.get(0) != c_node); // Build breaking string. var str = c_str + editor.node.openTagString($($outer_span.get(0)).clone().css(prop, val || '').get(0)) + io_str + $span.css(prop, '').get(0).outerHTML + ic_str + '</span>' + o_str; $span.replaceWith('<span id="fr-break"></span>'); var html = $outer_span.get(0).outerHTML; // Replace the outer node. $($outer_span.get(0)).replaceWith(html.replace(/<span id="fr-break"><\/span>/g, str)); } } while (editor.$el.find('.fr-split:empty').length > 0) { editor.$el.find('.fr-split:empty').remove(); } editor.$el.find('.fr-split').removeClass('fr-split'); editor.$el.find('span[style=""]').removeAttr('style'); editor.$el.find('span[class=""]').removeAttr('class'); editor.html.cleanEmptyTags(); $(editor.$el.find('span').get().reverse()).each(function () { if (!this.attributes || this.attributes.length == 0) { $(this).replaceWith(this.innerHTML); } }); editor.el.normalize(); // Join current spans together if they are one next to each other. var just_spans = editor.$el.find('span[style] + span[style]'); for (i = 0; i < just_spans.length; i++) { var $x = $(just_spans[i]); var $p = $(just_spans[i]).prev(); if ($x.get(0).previousSibling == $p.get(0) && editor.node.openTagString($x.get(0)) == editor.node.openTagString($p.get(0))) { $x.prepend($p.html()); $p.remove(); } } editor.el.normalize(); editor.selection.restore(); } } /** * Remove inline style. */ function removeStyle (prop) { applyStyle(prop, null); } /** * Get the current state. */ function is (tag, attrs) { if (typeof attrs == 'undefined') attrs = {}; if (attrs.style) { delete attrs.style; } var range = editor.selection.ranges(0); var el = range.startContainer; if (el.nodeType == Node.ELEMENT_NODE) { // Search for node deeper. if (el.childNodes.length > 0 && el.childNodes[range.startOffset]) { el = el.childNodes[range.startOffset]; } } // Check first childs. var f_child = el; while (f_child && f_child.nodeType == Node.ELEMENT_NODE && !_matches(f_child, _query(tag, attrs))) { f_child = f_child.firstChild; } if (f_child && f_child.nodeType == Node.ELEMENT_NODE && _matches(f_child, _query(tag, attrs))) return true; // Check parents. var p_node = el; if (p_node && p_node.nodeType != Node.ELEMENT_NODE) p_node = p_node.parentNode; while (p_node && p_node.nodeType == Node.ELEMENT_NODE && p_node != editor.el && !_matches(p_node, _query(tag, attrs))) { p_node = p_node.parentNode; } if (p_node && p_node.nodeType == Node.ELEMENT_NODE && p_node != editor.el && _matches(p_node, _query(tag, attrs))) return true; return false; } return { is: is, toggle: toggle, apply: apply, remove: remove, applyStyle: applyStyle, removeStyle: removeStyle } } $.FE.COMMANDS = { bold: { title: 'Bold', toggle: true, refresh: function ($btn) { var format = this.format.is('strong'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, italic: { title: 'Italic', toggle: true, refresh: function ($btn) { var format = this.format.is('em'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, underline: { title: 'Underline', toggle: true, refresh: function ($btn) { var format = this.format.is('u'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, strikeThrough: { title: 'Strikethrough', toggle: true, refresh: function ($btn) { var format = this.format.is('s'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, subscript: { title: 'Subscript', toggle: true, refresh: function ($btn) { var format = this.format.is('sub'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, superscript: { title: 'Superscript', toggle: true, refresh: function ($btn) { var format = this.format.is('sup'); $btn.toggleClass('fr-active', format).attr('aria-pressed', format); } }, outdent: { title: 'Decrease Indent' }, indent: { title: 'Increase Indent' }, undo: { title: 'Undo', undo: false, forcedRefresh: true, disabled: true }, redo: { title: 'Redo', undo: false, forcedRefresh: true, disabled: true }, insertHR: { title: 'Insert Horizontal Line' }, clearFormatting: { title: 'Clear Formatting' }, selectAll: { title: 'Select All', undo: false } }; $.FE.RegisterCommand = function (name, info) { $.FE.COMMANDS[name] = info; } $.FE.MODULES.commands = function (editor) { var mapping = { bold: function () { _execCommand('bold', 'strong'); }, subscript: function () { _execCommand('subscript', 'sub'); }, superscript: function () { _execCommand('superscript', 'sup'); }, italic: function () { _execCommand('italic', 'em'); }, strikeThrough: function () { _execCommand('strikeThrough', 's'); }, underline: function () { _execCommand('underline', 'u'); }, undo: function () { editor.undo.run(); }, redo: function () { editor.undo.redo(); }, indent: function () { _processIndent(1); }, outdent: function () { _processIndent(-1); }, show: function () { if (editor.opts.toolbarInline) { editor.toolbar.showInline(null, true); } }, insertHR: function () { editor.selection.remove(); var empty = ''; if (editor.core.isEmpty()) { empty = '<br>'; if (editor.html.defaultTag()) { empty = '<' + editor.html.defaultTag() + '>' + empty + '</' + editor.html.defaultTag() + '>'; } } editor.html.insert('<hr id="fr-just">' + empty); var $hr = editor.$el.find('hr#fr-just'); $hr.removeAttr('id'); editor.selection.setAfter($hr.get(0), false) || editor.selection.setBefore($hr.get(0), false); editor.selection.restore(); }, clearFormatting: function () { editor.format.remove(); }, selectAll: function () { editor.doc.execCommand('selectAll', false, false); } } /** * Exec command. */ function exec (cmd, params) { // Trigger before command to see if to execute the default callback. if (editor.events.trigger('commands.before', $.merge([cmd], params || [])) !== false) { // Get the callback. var callback = ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].callback) || mapping[cmd]; var focus = true; var accessibilityFocus = false; if ($.FE.COMMANDS[cmd]) { if (typeof $.FE.COMMANDS[cmd].focus != 'undefined') { focus = $.FE.COMMANDS[cmd].focus; } if (typeof $.FE.COMMANDS[cmd].accessibilityFocus != 'undefined') { accessibilityFocus = $.FE.COMMANDS[cmd].accessibilityFocus; } } // Make sure we have focus. if ( (!editor.core.hasFocus() && focus && !editor.popups.areVisible()) || (!editor.core.hasFocus() && accessibilityFocus && editor.accessibility.hasFocus()) ) { // Focus in the editor at any position. editor.events.focus(true); } // Callback. // Save undo step. if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].undo !== false) { if (editor.$el.find('.fr-marker').length) { editor.events.disableBlur(); editor.selection.restore(); } editor.undo.saveStep(); } if (callback) { callback.apply(editor, $.merge([cmd], params || [])); } // Trigger after command. editor.events.trigger('commands.after', $.merge([cmd], params || [])); // Save undo step again. if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].undo !== false) editor.undo.saveStep(); } } /** * Exex default. */ function _execCommand(cmd, tag) { editor.format.toggle(tag); } function _processIndent(indent) { editor.selection.save(); editor.html.wrap(true, true, true, true); editor.selection.restore(); var blocks = editor.selection.blocks(); for (var i = 0; i < blocks.length; i++) { if (blocks[i].tagName != 'LI' && blocks[i].parentNode.tagName != 'LI') { var $block = $(blocks[i]); var prop = (editor.opts.direction == 'rtl' || $block.css('direction') == 'rtl') ? 'margin-right' : 'margin-left'; var margin_left = editor.helpers.getPX($block.css(prop)); $block.css(prop, Math.max(margin_left + indent * 20, 0) || ''); $block.removeClass('fr-temp-div'); } } editor.selection.save(); editor.html.unwrap(); editor.selection.restore(); } function callExec (k) { return function () { exec(k); } } var resp = {}; for (var k in mapping) { if (mapping.hasOwnProperty(k)) { resp[k] = callExec(k); } } function _init () { // Prevent typing in HR. editor.events.on('keydown', function (e) { var el = editor.selection.element(); if (el && el.tagName == 'HR' && !editor.keys.isArrow(e.which)) { e.preventDefault(); return false; } }); editor.events.on('keyup', function (e) { var el = editor.selection.element(); if (el && el.tagName == 'HR') { if (e.which == $.FE.KEYCODE.ARROW_LEFT || e.which == $.FE.KEYCODE.ARROW_UP) { if (el.previousSibling) { if (!editor.node.isBlock(el.previousSibling)) { $(el).before($.FE.MARKERS); } else { editor.selection.setAtEnd(el.previousSibling); } editor.selection.restore(); return false; } } else if (e.which == $.FE.KEYCODE.ARROW_RIGHT || e.which == $.FE.KEYCODE.ARROW_DOWN) { if (el.nextSibling) { if (!editor.node.isBlock(el.nextSibling)) { $(el).after($.FE.MARKERS); } else { editor.selection.setAtStart(el.nextSibling); } editor.selection.restore(); return false; } } } }) // Do not allow mousedown on HR. editor.events.on('mousedown', function (e) { if (e.target && e.target.tagName == 'HR') { e.preventDefault(); e.stopPropagation(); return false; } }); // If somehow focus gets in HR remove it. editor.events.on('mouseup', function (e) { var s_el = editor.selection.element(); var e_el = editor.selection.endElement(); if (s_el == e_el && s_el && s_el.tagName == 'HR') { if (s_el.nextSibling) { if (!editor.node.isBlock(s_el.nextSibling)) { $(s_el).after($.FE.MARKERS); } else { editor.selection.setAtStart(s_el.nextSibling); } } editor.selection.restore(); } }) } return $.extend(resp, { exec: exec, _init: _init }); }; $.FE.MODULES.data=function(a){function b(a){return a}function c(a){if(!a)return a;for(var c="",f=b("charCodeAt"),g=b("fromCharCode"),h=l.indexOf(a[0]),i=1;i<a.length-2;i++){for(var j=d(++h),k=a[f](i),m="";/[0-9-]/.test(a[i+1]);)m+=a[++i];m=parseInt(m,10)||0,k=e(k,j,m),k^=h-1&31,c+=String[g](k)}return c}function d(a){for(var b=a.toString(),c=0,d=0;d<b.length;d++)c+=parseInt(b.charAt(d),10);return c>10?c%9+1:c}function e(a,b,c){for(var d=Math.abs(c);d-- >0;)a-=b;return c<0&&(a+=123),a}function f(a){return!(!a||"none"!=a.css("display"))&&(a.remove(),!0)}function g(){return f(j)||f(k)}function h(){return!!a.$box&&(a.$box.append(n(b(n("kTDD4spmKD1klaMB1C7A5RA1G3RA10YA5qhrjuvnmE1D3FD2bcG-7noHE6B2JB4C3xXA8WF6F-10RG2C3G3B-21zZE3C3H3xCA16NC4DC1f1hOF1MB3B-21whzQH5UA2WB10kc1C2F4D3XC2YD4D1C4F3GF2eJ2lfcD-13HF1IE1TC11TC7WE4TA4d1A2YA6XA4d1A3yCG2qmB-13GF4A1B1KH1HD2fzfbeQC3TD9VE4wd1H2A20A2B-22ujB3nBG2A13jBC10D3C2HD5D1H1KB11uD-16uWF2D4A3F-7C9D-17c1E4D4B3d1D2CA6B2B-13qlwzJF2NC2C-13E-11ND1A3xqUA8UE6bsrrF-7C-22ia1D2CF2H1E2akCD2OE1HH1dlKA6PA5jcyfzB-22cXB4f1C3qvdiC4gjGG2H2gklC3D-16wJC1UG4dgaWE2D5G4g1I2H3B7vkqrxH1H2EC9C3E4gdgzKF1OA1A5PF5C4WWC3VA6XA4e1E3YA2YA5HE4oGH4F2H2IB10D3D2NC5G1B1qWA9PD6PG5fQA13A10XA4C4A3e1H2BA17kC-22cmOB1lmoA2fyhcptwWA3RA8A-13xB-11nf1I3f1B7GB3aD3pavFC10D5gLF2OG1LSB2D9E7fQC1F4F3wpSB5XD3NkklhhaE-11naKA9BnIA6D1F5bQA3A10c1QC6Kjkvitc2B6BE3AF3E2DA6A4JD2IC1jgA-64MB11D6C4==")))),j=a.$box.find("> div:last"),k=j.find("> a"),void("rtl"==a.opts.direction&&j.css("left","auto").css("right",0)))}function i(){var c=a.opts.key||[""];"string"==typeof c&&(c=[c]),a.ul=!0;for(var d=0;d<c.length;d++){var e=n(c[d])||"";if(!(e!==n(b(n("mcVRDoB1BGILD7YFe1BTXBA7B6==")))&&e.indexOf(m,e.length-m.length)<0&&[n("9qqG-7amjlwq=="),n("KA3B3C2A6D1D5H5H1A3=="),n("QzbzvxyB2yA-9m=="),n("naamngiA3dA-16xtE-11C-9B1H-8sc==")].indexOf(m)<0)){a.ul=!1;break}}a.ul===!0&&h(),a.events.on("contentChanged",function(){a.ul===!0&&g()&&h()}),a.events.on("destroy",function(){j&&j.length&&j.remove()},!0)}var j,k,l="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",m=function(){for(var a=0,b=document.domain,c=b.split("."),d="_gd"+(new Date).getTime();a<c.length-1&&document.cookie.indexOf(d+"="+d)==-1;)b=c.slice(-1-++a).join("."),document.cookie=d+"="+d+";domain="+b+";";return document.cookie=d+"=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain="+b+";",(b||"").replace(/(^\.*)|(\.*$)/g,"")}(),n=b(c);return{_init:i}} $.extend($.FE.DEFAULTS, { pastePlain: false, pasteDeniedTags: ['colgroup', 'col'], pasteDeniedAttrs: ['class', 'id', 'style'], pasteAllowLocalImages: false }); $.FE.MODULES.paste = function (editor) { var scroll_position; var clipboard_html; var $paste_div; /** * Handle copy and cut. */ function _handleCopy (e) { $.FE.copied_html = editor.html.getSelected(); $.FE.copied_text = $('<div>').html($.FE.copied_html).text(); if (e.type == 'cut') { editor.undo.saveStep(); setTimeout(function () { editor.selection.save(); editor.html.wrap(); editor.selection.restore(); editor.events.focus(); editor.undo.saveStep(); }, 0); } } /** * Handle pasting. */ var stop_paste = false; function _handlePaste (e) { if (stop_paste) { return false; } if (e.originalEvent) e = e.originalEvent; if (editor.events.trigger('paste.before', [e]) === false) { e.preventDefault(); return false; } scroll_position = editor.$win.scrollTop(); // Read data from clipboard. if (e && e.clipboardData && e.clipboardData.getData) { var types = ''; var clipboard_types = e.clipboardData.types; if (editor.helpers.isArray(clipboard_types)) { for (var i = 0 ; i < clipboard_types.length; i++) { types += clipboard_types[i] + ';'; } } else { types = clipboard_types; } clipboard_html = ''; // HTML. if (/text\/html/.test(types)) { clipboard_html = e.clipboardData.getData('text/html'); } // Safari HTML. else if (/text\/rtf/.test(types) && editor.browser.safari) { clipboard_html = e.clipboardData.getData('text/rtf'); } else if (/text\/plain/.test(types) && !this.browser.mozilla) { clipboard_html = editor.html.escapeEntities(e.clipboardData.getData('text/plain')).replace(/\n/g, '<br>'); } if (clipboard_html !== '') { _processPaste(); if (e.preventDefault) { e.stopPropagation(); e.preventDefault(); } return false; } else { clipboard_html = null; } } // Normal paste. _beforePaste(); } /** * Before starting to paste. */ function _beforePaste () { // Save selection editor.selection.save(); editor.events.disableBlur(); // Set clipboard HTML. clipboard_html = null; // Remove and store the editable content if (!$paste_div) { $paste_div = $('<div contenteditable="true" style="position: fixed; top: 0; left: -9999px; height: 100%; width: 0; word-break: break-all; overflow:hidden; z-index: 9999; line-height: 140%;" tabIndex="-1"></div>'); editor.$box.after($paste_div); editor.events.on('destroy', function () { $paste_div.remove(); }) } else { $paste_div.html(''); } // Focus on the pasted div. $paste_div.focus(); // Process paste soon. editor.win.setTimeout(_processPaste, 1); } /** * Clean HTML that was pasted from Word. */ function _wordClean (html) { // Single item list. html = html.replace( /<p(.*?)class="?'?MsoListParagraph"?'? ([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<ul><li>$3</li></ul>' ); html = html.replace( /<p(.*?)class="?'?NumberedText"?'? ([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<ol><li>$3</li></ol>' ); // List start. html = html.replace( /<p(.*?)class="?'?MsoListParagraphCxSpFirst"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<ul><li$3>$5</li>' ); html = html.replace( /<p(.*?)class="?'?NumberedTextCxSpFirst"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<ol><li$3>$5</li>' ); // List middle. html = html.replace( /<p(.*?)class="?'?MsoListParagraphCxSpMiddle"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<li$3>$5</li>' ); html = html.replace( /<p(.*?)class="?'?NumberedTextCxSpMiddle"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<li$3>$5</li>' ); html = html.replace( /<p(.*?)class="?'?MsoListBullet"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<li$3>$5</li>' ); // List end. html = html.replace( /<p(.*?)class="?'?MsoListParagraphCxSpLast"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<li$3>$5</li></ul>' ); html = html.replace( /<p(.*?)class="?'?NumberedTextCxSpLast"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi, '<li$3>$5</li></ol>' ); // Clean list bullets. html = html.replace(/<span([^<]*?)style="?'?mso-list:Ignore"?'?([\s\S]*?)>([\s\S]*?)<span/gi, '<span><span'); // Webkit clean list bullets. html = html.replace(/<!--\[if \!supportLists\]-->([\s\S]*?)<!--\[endif\]-->/gi, ''); html = html.replace(/<!\[if \!supportLists\]>([\s\S]*?)<!\[endif\]>/gi, ''); // Remove mso classes. html = html.replace(/(\n|\r| class=(")?Mso[a-zA-Z0-9]+(")?)/gi, ' '); // Remove comments. html = html.replace(/<!--[\s\S]*?-->/gi, ''); // Remove tags but keep content. html = html.replace(/<(\/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>/gi, ''); // Remove no needed tags. var word_tags = ['style', 'script', 'applet', 'embed', 'noframes', 'noscript']; for (var i = 0; i < word_tags.length; i++) { var regex = new RegExp('<' + word_tags[i] + '.*?' + word_tags[i] + '(.*?)>', 'gi'); html = html.replace(regex, ''); } // Remove spaces. html = html.replace(/ /gi, ' '); // Keep empty TH and TD. html = html.replace(/<td([^>]*)><\/td>/g, '<td$1><br></td>'); html = html.replace(/<th([^>]*)><\/th>/g, '<th$1><br></th>'); // Remove empty tags. var oldHTML; do { oldHTML = html; html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, ''); } while (html != oldHTML); // Process list indentation. html = html.replace(/<lilevel([^1])([^>]*)>/gi, '<li data-indent="true"$2>'); html = html.replace(/<lilevel1([^>]*)>/gi, '<li$1>'); // Clean HTML. html = editor.clean.html(html, editor.opts.pasteDeniedTags, editor.opts.pasteDeniedAttrs); // Clean empty links. html = html.replace(/<a>(.[^<]+)<\/a>/gi, '$1'); // https://github.com/froala/wysiwyg-editor/issues/1364. html = html.replace(/<br> */g, '<br>'); // Process list indent. var div = editor.o_doc.createElement('div') div.innerHTML = html; var lis = div.querySelectorAll('li[data-indent]'); for (var i = 0; i < lis.length;i ++) { var li = lis[i]; var p_li = li.previousElementSibling; if (p_li && p_li.tagName == 'LI') { var list = p_li.querySelector(':scope > ul, :scope > ol'); if (!list) { list = document.createElement('ul'); p_li.appendChild(list); } list.appendChild(li); } else { li.removeAttribute('data-indent'); } } editor.html.cleanBlankSpaces(div); html = div.innerHTML; return html; } /** * Plain clean. */ function _plainPasteClean (html) { var div = editor.doc.createElement('div'); div.innerHTML = html; var els = div.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6, pre, blockquote'); for (var i = 0; i < els.length; i++) { var el = els[i]; el.outerHTML = '<' + (editor.html.defaultTag() || 'DIV') + '>' + el.innerHTML + '</' + (editor.html.defaultTag() || 'DIV') + '>' } els = div.querySelectorAll('*:not(' + 'p, div, h1, h2, h3, h4, h5, h6, pre, blockquote, ul, ol, li, table, tbody, thead, tr, td, br, img'.split(',').join('):not(') + ')'); for (var i = els.length - 1; i >= 0; i--) { var el = els[i]; el.outerHTML = el.innerHTML; } // Remove comments. var cleanComments = function (node) { var contents = editor.node.contents(node); for (var i = 0; i < contents.length; i++) { if (contents[i].nodeType != Node.TEXT_NODE && contents[i].nodeType != Node.ELEMENT_NODE) { contents[i].parentNode.removeChild(contents[i]); } else { cleanComments(contents[i]); } } }; cleanComments(div); return div.innerHTML; } /** * Process the pasted HTML. */ function _processPaste () { // Save undo snapshot. editor.keys.forceUndo(); var snapshot = editor.snapshot.get(); // Cannot read from clipboard. if (clipboard_html === null) { clipboard_html = $paste_div.get(0).innerHTML; editor.selection.restore(); editor.events.enableBlur(); } // Trigger chain cleanp. var response = editor.events.chainTrigger('paste.beforeCleanup', clipboard_html); if (typeof(response) === 'string') { clipboard_html = response; } // Detect if the clipboard HTML comes from Word before removing the body tag. var is_word = false; if (clipboard_html.match(/(class=\"?Mso|class=\'?Mso|style=\"[^\"]*\bmso\-|style=\'[^\']*\bmso\-|w:WordDocument)/gi)) { is_word = true; } // Keep only body if there is. if (clipboard_html.indexOf('<body') >= 0) { clipboard_html = clipboard_html.replace(/[.\s\S\w\W<>]*<body[^>]*>[\s]*([.\s\S\w\W<>]*)\s]*<\/body>[.\s\S\w\W<>]*/g, '$1'); clipboard_html = clipboard_html.replace(/([^>])\n([^<])/g, '$1 $2'); } // Google Docs paste. var is_gdocs = false; if (clipboard_html.indexOf('id="docs-internal-guid') >= 0) { clipboard_html = clipboard_html.replace(/^.* id="docs-internal-guid[^>]*>(.*)<\/b>.*$/, '$1'); is_gdocs = true; } // Word paste. if (is_word) { // Strip spaces at the beginning. clipboard_html = clipboard_html.replace(/^\n*/g, '').replace(/^ /g, ''); // Firefox paste. if (clipboard_html.indexOf('<colgroup>') === 0) { clipboard_html = '<table>' + clipboard_html + '</table>'; } clipboard_html = _wordClean(clipboard_html); clipboard_html = _removeEmptyTags(clipboard_html); } // Paste. else { // Remove comments. editor.opts.htmlAllowComments = false; clipboard_html = editor.clean.html(clipboard_html, editor.opts.pasteDeniedTags, editor.opts.pasteDeniedAttrs); editor.opts.htmlAllowComments = true; // Remove empty tags. clipboard_html = _removeEmptyTags(clipboard_html); // Do not keep entities that are not HTML compatible. clipboard_html = clipboard_html.replace(/\r|\n|\t/g, ''); // We should use the original text. var tmp_div = editor.doc.createElement('div'); tmp_div.innerHTML = clipboard_html; if ($.FE.copied_text && tmp_div.textContent.replace(/(\u00A0)/gi, ' ').replace(/\r|\n/gi, '') == $.FE.copied_text.replace(/(\u00A0)/gi, ' ').replace(/\r|\n/gi, '')) { clipboard_html = $.FE.copied_html; } // Trail ending and starting spaces. clipboard_html = clipboard_html.replace(/^ */g, '').replace(/ *$/g, ''); } // Do plain paste cleanup. if (editor.opts.pastePlain) { clipboard_html = _plainPasteClean(clipboard_html); } // After paste cleanup event. response = editor.events.chainTrigger('paste.afterCleanup', clipboard_html); if (typeof(response) === 'string') { clipboard_html = response; } // Check if there is anything to clean. if (clipboard_html !== '') { // Normalize spaces. var tmp = editor.o_doc.createElement('div'); tmp.innerHTML = clipboard_html; editor.spaces.normalize(tmp); // Remove all spans. var spans = tmp.getElementsByTagName('span'); for (var i = 0; i < spans.length; i++) { var span = spans[i]; if (span.attributes.length === 0) { span.outerHTML = span.innerHTML; } } // Unwrap lists if they are the only thing in the pasted HTML. var list = tmp.children; if (list.length == 1 && ['OL', 'UL'].indexOf(list[0].tagName) >= 0) { list[0].outerHTML = list[0].innerHTML; } // Remove unecessary new_lines. if (!is_gdocs) { var brs = tmp.getElementsByTagName('br'); for (var i = 0; i < brs.length; i++) { var br = brs[i]; if (editor.node.isBlock(br.previousSibling)) { br.parentNode.removeChild(br); } } } // https://github.com/froala/wysiwyg-editor/issues/1493 if (editor.opts.enter == $.FE.ENTER_BR) { var els = tmp.querySelectorAll('p, div'); for (var i = 0; i < els.length; i++) { var el = els[i]; el.outerHTML = el.innerHTML + (el.nextSibling && !editor.node.isEmpty(el) ? '<br>' : ''); } } else if (editor.opts.enter == $.FE.ENTER_DIV) { var els = tmp.getElementsByTagName('p'); for (var i = 0; i < els.length; i++) { var el = els[i]; el.outerHTML = '<div>' + el.innerHTML + '</div>'; } } clipboard_html = tmp.innerHTML; // Insert HTML. editor.html.insert(clipboard_html, true); } _afterPaste(); editor.undo.saveStep(snapshot); editor.undo.saveStep(); } /** * After pasting. */ function _afterPaste () { editor.events.trigger('paste.after'); } /** * Remove possible empty tags in pasted HTML. */ function _removeEmptyTags (html) { var div = editor.o_doc.createElement('div'); div.innerHTML = html; // Clean empty tags. var empty_tags = div.querySelectorAll('*:empty:not(br):not(img):not(td):not(th)'); while (empty_tags.length) { for (var i = 0; i < empty_tags.length; i++) { empty_tags[i].parentNode.removeChild(empty_tags[i]); } empty_tags = div.querySelectorAll('*:empty:not(br):not(img):not(td):not(th)'); } // Workaround for Nodepad paste. var divs = div.querySelectorAll(':scope > div:not([style]), td > div, th > div, li > div'); while (divs.length) { var dv = divs[divs.length - 1]; if (editor.html.defaultTag() && editor.html.defaultTag() != 'div') { // If we have nested block tags unwrap them. if (dv.querySelector(editor.html.blockTagsQuery())) { dv.outerHTML = dv.innerHTML; } else { dv.outerHTML = '<' + editor.html.defaultTag() + '>' + dv.innerHTML + '</' + editor.html.defaultTag() + '>'; } } else { var els = dv.querySelectorAll('*'); if (!els.length || els[els.length - 1].tagName !== 'BR') { dv.outerHTML = dv.innerHTML + '<br>'; } else { dv.outerHTML = dv.innerHTML; } } divs = div.querySelectorAll(':scope > div:not([style]), td > div, th > div, li > div'); } // Remove divs. divs = div.querySelectorAll('div:not([style])'); while (divs.length) { for (i = 0; i < divs.length; i++) { var el = divs[i]; var text = el.innerHTML.replace(/\u0009/gi, '').trim(); el.outerHTML = text; } divs = div.querySelectorAll('div:not([style])'); } return div.innerHTML; } /** * Initialize. */ function _init () { editor.events.on('copy', _handleCopy); editor.events.on('cut', _handleCopy); editor.events.on('paste', _handlePaste); if (editor.browser.msie && editor.browser.version < 11) { editor.events.on('mouseup', function (e) { if (e.button == 2) { setTimeout(function () { stop_paste = false; }, 50); stop_paste = true; } }, true) editor.events.on('beforepaste', _handlePaste); } } return { _init: _init } }; $.extend($.FE.DEFAULTS, { shortcutsEnabled: ['show', 'bold', 'italic', 'underline', 'strikeThrough', 'indent', 'outdent', 'undo', 'redo'], shortcutsHint: true }); $.FE.SHORTCUTS_MAP = {}; $.FE.RegisterShortcut = function (key, cmd, val, letter, shift, option) { $.FE.SHORTCUTS_MAP[(shift ? '^' : '') + (option ? '@' : '') + key] = { cmd: cmd, val: val, letter: letter, shift: shift, option: option } $.FE.DEFAULTS.shortcutsEnabled.push(cmd); } $.FE.RegisterShortcut($.FE.KEYCODE.E, 'show', null, 'E', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.B, 'bold', null, 'B', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.I, 'italic', null, 'I', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.U, 'underline', null, 'U', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.S, 'strikeThrough', null, 'S', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.CLOSE_SQUARE_BRACKET, 'indent', null, ']', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.OPEN_SQUARE_BRACKET, 'outdent', null, '[', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.Z, 'undo', null, 'Z', false, false); $.FE.RegisterShortcut($.FE.KEYCODE.Z, 'redo', null, 'Z', true, false); $.FE.MODULES.shortcuts = function (editor) { var inverse_map = null; function get (cmd) { if (!editor.opts.shortcutsHint) return null; if (!inverse_map) { inverse_map = {}; for (var key in $.FE.SHORTCUTS_MAP) { if ($.FE.SHORTCUTS_MAP.hasOwnProperty(key) && editor.opts.shortcutsEnabled.indexOf($.FE.SHORTCUTS_MAP[key].cmd) >= 0) { inverse_map[$.FE.SHORTCUTS_MAP[key].cmd + '.' + ($.FE.SHORTCUTS_MAP[key].val || '')] = { shift: $.FE.SHORTCUTS_MAP[key].shift, option: $.FE.SHORTCUTS_MAP[key].option, letter: $.FE.SHORTCUTS_MAP[key].letter } } } } var srct = inverse_map[cmd]; if (!srct) return null; return (editor.helpers.isMac() ? String.fromCharCode(8984) : 'Ctrl+') + (srct.shift ? (editor.helpers.isMac() ? String.fromCharCode(8679) : 'Shift+') : '') + (srct.option ? (editor.helpers.isMac() ? String.fromCharCode(8997) : 'Alt+') : '') + srct.letter; } var active = false; /** * Execute shortcut. */ function exec (e) { if (!editor.core.hasFocus()) return true; var keycode = e.which; var ctrlKey = navigator.userAgent.indexOf('Mac OS X') != -1 ? e.metaKey : e.ctrlKey; if (e.type == 'keyup' && active) { if (keycode != $.FE.KEYCODE.META) { active = false; return false; } } if (e.type == 'keydown') active = false; // Build shortcuts map. var map_key = (e.shiftKey ? '^' : '') + (e.altKey ? '@' : '') + keycode; if (ctrlKey && $.FE.SHORTCUTS_MAP[map_key]) { var cmd = $.FE.SHORTCUTS_MAP[map_key].cmd; // Check if shortcut is enabled. if (cmd && editor.opts.shortcutsEnabled.indexOf(cmd) >= 0) { var val = $.FE.SHORTCUTS_MAP[map_key].val; // Search for button. var $btn; if (cmd && !val) { $btn = editor.$tb.find('.fr-command[data-cmd="' + cmd + '"]'); } else if (cmd && val) { $btn = editor.$tb.find('.fr-command[data-cmd="' + cmd + '"][data-param1="' + val + '"]'); } // Button found. if ($btn.length) { e.preventDefault(); e.stopPropagation(); $btn.parents('.fr-toolbar').data('instance', editor); if (e.type == 'keydown') { editor.button.exec($btn); active = true; } return false; } // Search for command. else if (cmd && editor.commands[cmd]) { e.preventDefault(); e.stopPropagation(); if (e.type == 'keydown') { editor.commands[cmd](); active = true; } return false; } } } } /** * Initialize. */ function _init () { editor.events.on('keydown', exec, true); editor.events.on('keyup', exec, true); } return { _init: _init, get: get } } $.FE.MODULES.snapshot = function (editor) { /** * Get the index of a node inside it's parent. */ function _getNodeIndex (node) { var childNodes = node.parentNode.childNodes; var idx = 0; var prevNode = null; for (var i = 0; i < childNodes.length; i++) { if (prevNode) { // Current node is text and it is empty. var isEmptyText = (childNodes[i].nodeType === Node.TEXT_NODE && childNodes[i].textContent === ''); // Previous node is text, current node is text. var twoTexts = (prevNode.nodeType === Node.TEXT_NODE && childNodes[i].nodeType === Node.TEXT_NODE); if (!isEmptyText && !twoTexts) idx++; } if (childNodes[i] == node) return idx; prevNode = childNodes[i]; } } /** * Determine the location of the node inside the element. */ function _getNodeLocation (node) { var loc = []; if (!node.parentNode) return []; while (!editor.node.isElement(node)) { loc.push(_getNodeIndex(node)); node = node.parentNode; } return loc.reverse(); } /** * Get the range offset inside the node. */ function _getRealNodeOffset (node, offset) { while (node && node.nodeType === Node.TEXT_NODE) { var prevNode = node.previousSibling; if (prevNode && prevNode.nodeType == Node.TEXT_NODE) { offset += prevNode.textContent.length; } node = prevNode; } return offset; } /** * Codify each range. */ function _getRange (range) { return { scLoc: _getNodeLocation(range.startContainer), scOffset: _getRealNodeOffset(range.startContainer, range.startOffset), ecLoc: _getNodeLocation(range.endContainer), ecOffset: _getRealNodeOffset(range.endContainer, range.endOffset) } } /** * Get the current snapshot. */ function get () { var snapshot = {}; editor.events.trigger('snapshot.before'); snapshot.html = (editor.$wp ? editor.$el.html() : editor.$oel.get(0).outerHTML).replace(/ style=""/g, ''); snapshot.ranges = []; if (editor.$wp && editor.selection.inEditor() && editor.core.hasFocus()) { var ranges = editor.selection.ranges(); for (var i = 0; i < ranges.length; i++) { snapshot.ranges.push(_getRange(ranges[i])); } } editor.events.trigger('snapshot.after'); return snapshot; } /** * Determine node by its location in the main element. */ function _getNodeByLocation (loc) { var node = editor.el; for (var i = 0; i < loc.length; i++) { node = node.childNodes[loc[i]]; } return node; } /** * Restore range from snapshot. */ function _restoreRange (sel, range_snapshot) { try { // Get range info. var startNode = _getNodeByLocation(range_snapshot.scLoc); var startOffset = range_snapshot.scOffset; var endNode = _getNodeByLocation(range_snapshot.ecLoc); var endOffset = range_snapshot.ecOffset; // Restore range. var range = editor.doc.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); sel.addRange(range); } catch (ex) { console.warn (ex) } } /** * Restore snapshot. */ function restore (snapshot) { // Restore HTML. if (editor.$el.html() != snapshot.html) editor.$el.html(snapshot.html); // Get selection. var sel = editor.selection.get(); // Make sure to clear current selection. editor.selection.clear(); // Focus. editor.events.focus(true); // Restore Ranges. for (var i = 0; i < snapshot.ranges.length; i++) { _restoreRange(sel, snapshot.ranges[i]); } } /** * Compare two snapshots. */ function equal (s1, s2) { if (s1.html != s2.html) return false; if (editor.core.hasFocus() && JSON.stringify(s1.ranges) != JSON.stringify(s2.ranges)) return false; return true; } return { get: get, restore: restore, equal: equal } }; $.FE.MODULES.undo = function (editor) { /** * Disable the default browser undo. */ function _disableBrowserUndo (e) { var keyCode = e.which; var ctrlKey = editor.keys.ctrlKey(e); // Ctrl Key. if (ctrlKey) { if (keyCode == 90 && e.shiftKey) { e.preventDefault(); } if (keyCode == 90) { e.preventDefault(); } } } function canDo () { if (editor.undo_stack.length === 0 || editor.undo_index <= 1) { return false; } return true; } function canRedo () { if (editor.undo_index == editor.undo_stack.length) { return false; } return true; } var last_html = null; function saveStep (snapshot) { if (!editor.undo_stack || editor.undoing || editor.el.querySelector('.fr-marker')) return false; if (typeof snapshot == 'undefined') { snapshot = editor.snapshot.get(); if (!editor.undo_stack[editor.undo_index - 1] || !editor.snapshot.equal(editor.undo_stack[editor.undo_index - 1], snapshot)) { dropRedo(); editor.undo_stack.push(snapshot); editor.undo_index++; if (snapshot.html != last_html) { editor.events.trigger('contentChanged'); last_html = snapshot.html; } } } else { dropRedo(); if (editor.undo_index > 0) { editor.undo_stack[editor.undo_index - 1] = snapshot; } else { editor.undo_stack.push(snapshot); editor.undo_index++; } } } function dropRedo () { if (!editor.undo_stack || editor.undoing) return false; while (editor.undo_stack.length > editor.undo_index) { editor.undo_stack.pop(); } } function _do () { if (editor.undo_index > 1) { editor.undoing = true; // Get snapshot. var snapshot = editor.undo_stack[--editor.undo_index - 1]; // Clear any existing content changed timers. clearTimeout(editor._content_changed_timer); // Restore snapshot. editor.snapshot.restore(snapshot); last_html = snapshot.html; // Hide popups. editor.popups.hideAll(); // Enable toolbar. editor.toolbar.enable(); // Call content changed. editor.events.trigger('contentChanged'); editor.events.trigger('commands.undo'); editor.undoing = false; } } function _redo () { if (editor.undo_index < editor.undo_stack.length) { editor.undoing = true; // Get snapshot. var snapshot = editor.undo_stack[editor.undo_index++]; // Clear any existing content changed timers. clearTimeout(editor._content_changed_timer) // Restore snapshot. editor.snapshot.restore(snapshot); last_html = snapshot.html; // Hide popups. editor.popups.hideAll(); // Enable toolbar. editor.toolbar.enable(); // Call content changed. editor.events.trigger('contentChanged'); editor.events.trigger('commands.redo'); editor.undoing = false; } } function reset () { editor.undo_index = 0; editor.undo_stack = []; } function _destroy () { editor.undo_stack = []; } /** * Initialize */ function _init () { reset(); editor.events.on('initialized', function () { last_html = (editor.$wp ? editor.$el.html() : editor.$oel.get(0).outerHTML).replace(/ style=""/g, ''); }); editor.events.on('blur', function () { if (!editor.el.querySelector('.fr-dragging')) { editor.undo.saveStep(); } }) editor.events.on('keydown', _disableBrowserUndo); editor.events.on('destroy', _destroy); } return { _init: _init, run: _do, redo: _redo, canDo: canDo, canRedo: canRedo, dropRedo: dropRedo, reset: reset, saveStep: saveStep } }; $.FE.ICON_DEFAULT_TEMPLATE = 'font_awesome'; $.FE.ICON_TEMPLATES = { font_awesome: '<i class="fa fa-[NAME]" aria-hidden="true"></i>', text: '<span style="text-align: center;">[NAME]</span>', image: '<img src=[SRC] alt=[ALT] />', svg: '<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">[PATH]</svg>' } $.FE.ICONS = { bold: {NAME: 'bold'}, italic: {NAME: 'italic'}, underline: {NAME: 'underline'}, strikeThrough: {NAME: 'strikethrough'}, subscript: {NAME: 'subscript'}, superscript: {NAME: 'superscript'}, color: {NAME: 'tint'}, outdent: {NAME: 'outdent'}, indent: {NAME: 'indent'}, undo: {NAME: 'rotate-left'}, redo: {NAME: 'rotate-right'}, insertHR: {NAME: 'minus'}, clearFormatting: {NAME: 'eraser'}, selectAll: {NAME: 'mouse-pointer'} } $.FE.DefineIconTemplate = function (name, options) { $.FE.ICON_TEMPLATES[name] = options; } $.FE.DefineIcon = function (name, options) { $.FE.ICONS[name] = options; } $.FE.MODULES.icon = function (editor) { function create (command) { var icon = null; var info = $.FE.ICONS[command]; if (typeof info != 'undefined') { var template = info.template || $.FE.ICON_DEFAULT_TEMPLATE; if (template && (template = $.FE.ICON_TEMPLATES[template])) { icon = template.replace(/\[([a-zA-Z]*)\]/g, function (str, a1) { return (a1 == 'NAME' ? (info[a1] || command) : info[a1]); }); } } return (icon || command); } function getTemplate (command) { var info = $.FE.ICONS[command]; var template = $.FE.ICON_DEFAULT_TEMPLATE; if (typeof info != 'undefined') { var template = info.template || $.FE.ICON_DEFAULT_TEMPLATE; return template; } return template; } return { create: create, getTemplate: getTemplate } }; // Extend defaults. $.extend($.FE.DEFAULTS, { tooltips: true }); $.FE.MODULES.tooltip = function (editor) { function hide () { // Position fixed for: https://github.com/froala/wysiwyg-editor/issues/1247. if (editor.$tooltip) editor.$tooltip.removeClass('fr-visible').css('left', '-3000px').css('position', 'fixed'); } function to ($el, above) { if (!$el.data('title')) { $el.data('title', $el.attr('title')); } if (!$el.data('title')) return false; if (!editor.$tooltip) _init(); $el.removeAttr('title'); editor.$tooltip.text($el.data('title')); editor.$tooltip.addClass('fr-visible'); var left = $el.offset().left + ($el.outerWidth() - editor.$tooltip.outerWidth()) / 2; // Normalize screen position. if (left < 0) left = 0; if (left + editor.$tooltip.outerWidth() > $(editor.o_win).width()) { left = $(editor.o_win).width() - editor.$tooltip.outerWidth(); } if (typeof above == 'undefined') above = editor.opts.toolbarBottom; var top = !above ? $el.offset().top + $el.outerHeight() : $el.offset().top - editor.$tooltip.height(); editor.$tooltip.css('position', ''); editor.$tooltip.css('left', left); editor.$tooltip.css('top', Math.ceil(top)); if ($(editor.o_doc).find('body').css('position') != 'static') { editor.$tooltip.css('margin-left', - $(editor.o_doc).find('body').offset().left); editor.$tooltip.css('margin-top', - $(editor.o_doc).find('body').offset().top); } else { editor.$tooltip.css('margin-left', ''); editor.$tooltip.css('margin-top', ''); } } function bind ($el, selector, above) { if (editor.opts.tooltips && !editor.helpers.isMobile()) { editor.events.$on($el, 'mouseenter', selector, function (e) { if (!editor.node.hasClass(e.currentTarget, 'fr-disabled') && !editor.edit.isDisabled()) { to($(e.currentTarget), above); } }, true); editor.events.$on($el, 'mouseleave ' + editor._mousedown + ' ' + editor._mouseup, selector, function (e) { hide(); }, true); } } function _init () { if (editor.opts.tooltips && !editor.helpers.isMobile()) { if (!editor.shared.$tooltip) { editor.shared.$tooltip = $('<div class="fr-tooltip"></div>'); editor.$tooltip = editor.shared.$tooltip; if (editor.opts.theme) { editor.$tooltip.addClass(editor.opts.theme + '-theme'); } $(editor.o_doc).find('body').append(editor.$tooltip); } else { editor.$tooltip = editor.shared.$tooltip; } editor.events.on('shared.destroy', function () { editor.$tooltip.html('').removeData().remove(); editor.$tooltip = null; }, true); } } return { hide: hide, to: to, bind: bind } }; $.FE.MODULES.button = function (editor) { var buttons = []; if (editor.opts.toolbarInline || editor.opts.toolbarContainer) { if (!editor.shared.buttons) editor.shared.buttons = []; buttons = editor.shared.buttons; } var popup_buttons = []; if (!editor.shared.popup_buttons) editor.shared.popup_buttons = []; popup_buttons = editor.shared.popup_buttons; /** * Click was made on a dropdown button. */ function _dropdownButtonClick ($btn) { var $dropdown = $btn.next(); var active = editor.node.hasClass($btn.get(0), 'fr-active'); var mobile = editor.helpers.isMobile(); var $active_dropdowns = $('.fr-dropdown.fr-active').not($btn); var inst = $btn.parents('.fr-toolbar, .fr-popup').data('instance') || editor; // Hide keyboard. We need the entire space. if (inst.helpers.isIOS() && !inst.el.querySelector('.fr-marker')) { inst.selection.save(); inst.selection.clear(); inst.selection.restore(); } // Dropdown is not active. if (!active) { // Call refresh on show. var cmd = $btn.data('cmd'); $dropdown.find('.fr-command').removeClass('fr-active').attr('aria-selected', false); if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].refreshOnShow) { $.FE.COMMANDS[cmd].refreshOnShow.apply(inst, [$btn, $dropdown]); } $dropdown.css('left', $btn.offset().left - $btn.parent().offset().left - (editor.opts.direction == 'rtl' ? $dropdown.width() - $btn.outerWidth() : 0)); if (!editor.opts.toolbarBottom) { $dropdown.css('top', $btn.position().top + $btn.outerHeight()); } else { $dropdown.css('bottom', editor.$tb.height() - $btn.position().top); } } // Blink and activate. $btn.addClass('fr-blink').toggleClass('fr-active'); if ($btn.hasClass('fr-active')) { $dropdown.attr('aria-hidden', false); $btn.attr('aria-expanded', true); } else { $dropdown.attr('aria-hidden', true); $btn.attr('aria-expanded', false); } setTimeout (function () { $btn.removeClass('fr-blink'); }, 300); // Check if it exceeds window on the right. if ($dropdown.offset().left + $dropdown.outerWidth() > editor.$sc.offset().left + editor.$sc.outerWidth()) { $dropdown.css('margin-left', -($dropdown.offset().left + $dropdown.outerWidth() - editor.$sc.offset().left - editor.$sc.outerWidth())) } // Hide dropdowns that might be active. $active_dropdowns.removeClass('fr-active').attr('aria-expanded', false).next().attr('aria-hidden', true); $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', ''); if ($btn.parents('.fr-popup').length == 0 && !editor.opts.toolbarInline) { if (editor.node.hasClass($btn.get(0),'fr-active')) { editor.$tb.css('zIndex', (editor.opts.zIndex || 1) + 4); } else { editor.$tb.css('zIndex', ''); } } // Focus the active element or the dropdown button to enable accessibility. var $active_element = $dropdown.find('a.fr-command.fr-active'); if ($active_element.length) { editor.accessibility.focusToolbarElement($active_element); } else { editor.accessibility.focusToolbarElement($btn); } } function exec ($btn) { // Blink. $btn.addClass('fr-blink'); setTimeout (function () { $btn.removeClass('fr-blink'); }, 500); // Get command, value and additional params. var cmd = $btn.data('cmd'); var params = []; while (typeof $btn.data('param' + (params.length + 1)) != 'undefined') { params.push($btn.data('param' + (params.length + 1))); } // Hide dropdowns that might be active including the current one. var $active_dropdowns = $('.fr-dropdown.fr-active'); if ($active_dropdowns.length) { $active_dropdowns.removeClass('fr-active').attr('aria-expanded', false).next().attr('aria-hidden', true); $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', ''); } // Call the command. $btn.parents('.fr-popup, .fr-toolbar').data('instance').commands.exec(cmd, params); } /** * Click was made on a command button. */ function _commandButtonClick ($btn) { exec($btn); } function click ($btn) { var inst = $btn.parents('.fr-popup, .fr-toolbar').data('instance'); if ($btn.parents('.fr-popup').length == 0 && !$btn.data('popup')) { inst.popups.hideAll(); } // Popups are visible, but not in the current instance. if (inst.popups.areVisible() && !inst.popups.areVisible(inst)) { // Hide markers in other instances. for (var i = 0; i < $.FE.INSTANCES.length; i++) { if ($.FE.INSTANCES[i] != inst && $.FE.INSTANCES[i].popups && $.FE.INSTANCES[i].popups.areVisible()) { $.FE.INSTANCES[i].$el.find('.fr-marker').remove(); } } inst.popups.hideAll(); } // Dropdown button. if (editor.node.hasClass($btn.get(0),'fr-dropdown')) { _dropdownButtonClick($btn); } // Regular button. else { _commandButtonClick($btn); if ($.FE.COMMANDS[$btn.data('cmd')] && $.FE.COMMANDS[$btn.data('cmd')].refreshAfterCallback != false) { inst.button.bulkRefresh(); } } } function _click (e) { var $btn = $(e.currentTarget); click($btn); } function hideActiveDropdowns ($el) { var $active_dropdowns = $el.find('.fr-dropdown.fr-active'); if ($active_dropdowns.length) { $active_dropdowns.removeClass('fr-active').attr('aria-expanded', false).next().attr('aria-hidden', true);; $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', ''); } } /** * Click in the dropdown menu. */ function _dropdownMenuClick (e) { e.preventDefault(); e.stopPropagation(); } /** * Click on the dropdown wrapper. */ function _dropdownWrapperClick (e) { e.stopPropagation(); // Prevent blurring. if (!editor.helpers.isMobile()) { return false; } } /** * Bind callbacks for commands. */ function bindCommands ($el, tooltipAbove) { editor.events.bindClick($el, '.fr-command:not(.fr-disabled)', _click); // Click on the dropdown menu. editor.events.$on($el, editor._mousedown + ' ' + editor._mouseup + ' ' + editor._move, '.fr-dropdown-menu', _dropdownMenuClick, true); // Click on the dropdown wrapper. editor.events.$on($el, editor._mousedown + ' ' + editor._mouseup + ' ' + editor._move, '.fr-dropdown-menu .fr-dropdown-wrapper', _dropdownWrapperClick, true); // Hide dropdowns that might be active. var _document = $el.get(0).ownerDocument; var _window = 'defaultView' in _document ? _document.defaultView : _document.parentWindow; var hideDropdowns = function (e) { if (!e || (e.type == editor._mouseup && e.target != $('html').get(0)) || (e.type == 'keydown' && ((editor.keys.isCharacter(e.which) && !editor.keys.ctrlKey(e)) || e.which == $.FE.KEYCODE.ESC))) { hideActiveDropdowns($el); } } editor.events.$on($(_window), editor._mouseup + ' resize keydown', hideDropdowns, true); if (editor.opts.iframe) { editor.events.$on(editor.$win, editor._mouseup, hideDropdowns, true); } // Add refresh. if (editor.node.hasClass($el.get(0), 'fr-popup')) { $.merge(popup_buttons, $el.find('.fr-btn').toArray()); } else { $.merge(buttons, $el.find('.fr-btn').toArray()); } // Assing tooltips to buttons. editor.tooltip.bind($el, '.fr-btn, .fr-title', tooltipAbove); } /** * Create the content for dropdown. */ function _content (command, info) { var c = ''; if (info.html) { if (typeof info.html == 'function') { c += info.html.call(editor); } else { c += info.html; } } else { var options = info.options; if (typeof options == 'function') options = options(); c += '<ul class="fr-dropdown-list" role="presentation">'; for (var val in options) { if (options.hasOwnProperty(val)) { var shortcut = editor.shortcuts.get(command + '.' + val); if (shortcut) { shortcut = '<span class="fr-shortcut">' + shortcut + '</span>'; } else { shortcut = ''; } c += '<li role="presentation"><a class="fr-command" tabIndex="-1" role="option" data-cmd="' + command + '" data-param1="' + val + '" title="' + options[val] + '">' + editor.language.translate(options[val]) + '</a></li>'; } } c += '</ul>'; } return c; } /** * Create button. */ function _build (command, info, visible) { if (editor.helpers.isMobile() && info.showOnMobile === false) return ''; var display_selection = info.displaySelection; if (typeof display_selection == 'function') { display_selection = display_selection(editor); } var icon; if (display_selection) { var default_selection = (typeof info.defaultSelection == 'function' ? info.defaultSelection(editor) : info.defaultSelection); icon = '<span style="width:' + (info.displaySelectionWidth || 100) + 'px">' + (default_selection || editor.language.translate(info.title)) + '</span>'; } else { icon = editor.icon.create(info.icon || command); // Used instead of aria-label. The advantage is that it also display text when the css is disabled. icon += '<span class="fr-sr-only">' + (editor.language.translate(info.title) || '') + '</span>'; } var popup = info.popup ? ' data-popup="true"' : ''; var modal = info.modal ? ' data-modal="true"' : ''; var shortcut = editor.shortcuts.get(command + '.'); if (shortcut) { shortcut = ' (' + shortcut + ')'; } else { shortcut = ''; } var button_id = command + '-' + editor.id; var btn = '<button id="' + button_id + '"type="button" tabIndex="-1" role="button"' + (info.toggle ? ' aria-pressed="false"' : '') + (info.type == 'dropdown' ? ' aria-controls="drop" aria-expanded="false" aria-haspopup="true"' : '') + (info.disabled ? ' aria-disabled="true"' : '') + ' title="' + (editor.language.translate(info.title) || '') + shortcut + '" class="fr-command fr-btn' + (info.type == 'dropdown' ? ' fr-dropdown' : '') + (' fr-btn-' + editor.icon.getTemplate(info.icon)) + (info.displaySelection ? ' fr-selection' : '') + (info.back ? ' fr-back' : '') + (info.disabled ? ' fr-disabled' : '') + (!visible ? ' fr-hidden' : '') + '" data-cmd="' + command + '"' + popup + modal + '>' + icon + '</button>'; if (info.type == 'dropdown') { // Build dropdown. var dropdown = '<div class="fr-dropdown-menu" role="listbox" aria-labelledby="' + button_id + '" aria-hidden="true"><div class="fr-dropdown-wrapper" role="presentation"><div class="fr-dropdown-content" role="presentation">'; dropdown += _content(command, info); dropdown += '</div></div></div>'; btn += dropdown; } return btn; } function buildList (buttons, visible_buttons) { var str = ''; for (var i = 0; i < buttons.length; i++) { var cmd_name = buttons[i]; var cmd_info = $.FE.COMMANDS[cmd_name]; if (cmd_info && typeof cmd_info.plugin !== 'undefined' && editor.opts.pluginsEnabled.indexOf(cmd_info.plugin) < 0) continue; if (cmd_info) { var visible = typeof visible_buttons != 'undefined' ? visible_buttons.indexOf(cmd_name) >= 0 : true; str += _build(cmd_name, cmd_info, visible); } else if (cmd_name == '|') { str += '<div class="fr-separator fr-vs" role="separator" aria-orientation="vertical"></div>'; } else if (cmd_name == '-') { str += '<div class="fr-separator fr-hs" role="separator" aria-orientation="horizontal"></div>'; } } return str; } function refresh ($btn) { var inst = $btn.parents('.fr-popup, .fr-toolbar').data('instance') || editor; var cmd = $btn.data('cmd'); var $dropdown; if (!editor.node.hasClass($btn.get(0),'fr-dropdown')) { $btn.removeClass('fr-active'); if ($btn.attr('aria-pressed')) $btn.attr('aria-pressed', false); } else { $dropdown = $btn.next(); } if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].refresh) { $.FE.COMMANDS[cmd].refresh.apply(inst, [$btn, $dropdown]); } else if (editor.refresh[cmd]) { inst.refresh[cmd]($btn, $dropdown); } } function _bulkRefresh (btns) { var inst = editor.$tb ? (editor.$tb.data('instance') || editor) : editor; // Check the refresh event. if (editor.events.trigger('buttons.refresh') == false) return true; setTimeout(function () { var focused = (inst.selection.inEditor() && inst.core.hasFocus()); for (var i = 0; i < btns.length; i++) { var $btn = $(btns[i]); var cmd = $btn.data('cmd'); if ($btn.parents('.fr-popup').length == 0) { if (focused || ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].forcedRefresh)) { inst.button.refresh($btn); } else { if (!editor.node.hasClass($btn.get(0),'fr-dropdown')) { $btn.removeClass('fr-active'); if ($btn.attr('aria-pressed')) $btn.attr('aria-pressed', false); } } } else if ($btn.parents('.fr-popup').is(':visible')) { inst.button.refresh($btn); } } }, 0); } /** * Do buttons refresh. */ function bulkRefresh () { _bulkRefresh(buttons); _bulkRefresh(popup_buttons); } function _destroy () { buttons = []; popup_buttons = []; } /** * Initialize. */ function _init () { // Assign refresh and do refresh. if (editor.opts.toolbarInline) { editor.events.on('toolbar.show', bulkRefresh); } else { editor.events.on('mouseup', bulkRefresh); editor.events.on('keyup', bulkRefresh); editor.events.on('blur', bulkRefresh); editor.events.on('focus', bulkRefresh); editor.events.on('contentChanged', bulkRefresh); } editor.events.on('shared.destroy', _destroy); } return { _init: _init, buildList: buildList, bindCommands: bindCommands, refresh: refresh, bulkRefresh: bulkRefresh, exec: exec, click: click, hideActiveDropdowns: hideActiveDropdowns } }; $.FE.MODULES.modals = function (editor) { if (!editor.shared.modals) editor.shared.modals = {}; var modals = editor.shared.modals; var $overlay; /** * Get the modal with the specific id. */ function get(id) { return modals[id]; } /* * Get modal html */ function _modalHTML (head, body) { // Modal wrapper. var html = '<div tabIndex="-1" class="fr-modal' + (editor.opts.theme ? ' ' + editor.opts.theme + '-theme' : '') + '"><div class="fr-modal-wrapper">'; // Modal title. var close_button = '<i title="' + editor.language.translate('Cancel') + '" class="ri-close-line fr-modal-close"></i>'; html += '<div class="fr-modal-head">' + head + close_button + '</div>'; // Body. html += '<div tabIndex="-1" class="fr-modal-body">' + body + '</div>'; // End Modal. html += '</div></div>'; return $(html); } /* * Create modal. */ function create (id, head, body) { // Build modal overlay. if (!editor.shared.$overlay) { editor.shared.$overlay = $('<div class="fr-overlay">').appendTo('body'); } $overlay = editor.shared.$overlay; if (editor.opts.theme) { $overlay.addClass(editor.opts.theme + '-theme'); } // Build modal. if (!modals[id]) { var $modal = _modalHTML(head, body); modals[id] = { $modal: $modal, $head: $modal.find('.fr-modal-head'), $body: $modal.find('.fr-modal-body') }; // Desktop or mobile device. if (!editor.helpers.isMobile()) { $modal.addClass('fr-desktop'); } // Append modal to body. $modal.appendTo('body'); // Click on close button. editor.events.bindClick($modal, 'i.fr-modal-close', function () { hide(id); }); modals[id].$body.css('margin-top', modals[id].$head.outerHeight()); // Keydown handler. editor.events.$on($modal, 'keydown', function (e) { var keycode = e.which; // Esc. if (keycode == $.FE.KEYCODE.ESC){ hide(id); editor.accessibility.focusModalButton($modal); return false; } else if (!$(e.currentTarget).is('input[type=text], textarea') && keycode != $.FE.KEYCODE.ARROW_UP && keycode != $.FE.KEYCODE.ARROW_DOWN && !editor.keys.isBrowserAction(e)){ e.preventDefault(); e.stopPropagation(); return false; } else { return true; } }, true); hide(id); } return modals[id]; } /* * Destroy modals. */ function destroy () { // Destroy all modals. for (var i in modals) { var modalHash = modals[i]; modalHash && modalHash.$modal && modalHash.$modal.removeData().remove(); } $overlay && $overlay.removeData().remove(); modals = {}; } /* * Show modal. */ function show (id) { if (!modals[id]) { return; } var $modal = modals[id].$modal; // Set the current instance for the modal. $modal.data('instance', editor); // Show modal. $modal.show(); $overlay.show(); // Prevent scrolling in page. editor.$doc.find('body').addClass('prevent-scroll'); // Mobile device if (editor.helpers.isMobile()) { editor.$doc.find('body').addClass('fr-mobile'); } $modal.addClass('fr-active'); editor.accessibility.focusModal($modal); } /* * Hide modal. */ function hide (id) { if (!modals[id]) { return; } var $modal = modals[id].$modal; var inst = $modal.data('instance') || editor inst.events.enableBlur(); $modal.hide(); $overlay.hide(); inst.$doc.find('body').removeClass('prevent-scroll fr-mobile'); $modal.removeClass('fr-active'); // Restore selection. editor.accessibility.restoreSelection(inst); } /** * Resize modal according to its body or editor heights. */ function resize (id) { if (!modals[id]) { return; } var modalHash = modals[id]; var $modal = modalHash.$modal; var $body = modalHash.$body; var height = editor.$win.height(); // The wrapper object. var $wrapper = $modal.find('.fr-modal-wrapper'); // Calculate max allowed height. var allWrapperHeight = $wrapper.outerHeight(true); var exteriorBodyHeight = $wrapper.height() - ($body.outerHeight(true) - $body.height()); var maxHeight = height - allWrapperHeight + exteriorBodyHeight; // Get body content height. var body_content_height = $body.get(0).scrollHeight; // Calculate the new height. var newHeight = 'auto'; if (body_content_height > maxHeight) { newHeight = maxHeight; } $body.height(newHeight); } /** * Find visible modal. */ function isVisible (id) { var $modal; // By id. if (typeof id === 'string') { if (!modals[id]) { return; } $modal = modals[id].$modal } // By modal object. else { $modal = id; } return ($modal && editor.node.hasClass($modal, 'fr-active') && editor.core.sameInstance($modal)) || false; } /** * Check if there is any modal visible. */ function areVisible (new_instance) { for (var id in modals) { if (modals.hasOwnProperty(id)) { if (isVisible(id) && (typeof new_instance == 'undefined' || modals[id].$modal.data('instance') == new_instance)) return modals[id].$modal; } } return false; } /** * Initialization. */ function _init () { editor.events.on('shared.destroy', destroy, true); } return { _init: _init, get: get, create: create, show: show, hide: hide, resize: resize, isVisible: isVisible, areVisible: areVisible } }; $.FE.POPUP_TEMPLATES = { 'text.edit': '[_EDIT_]' }; $.FE.RegisterTemplate = function (name, template) { $.FE.POPUP_TEMPLATES[name] = template; } $.FE.MODULES.popups = function (editor) { if (!editor.shared.popups) editor.shared.popups = {}; var popups = editor.shared.popups; function setContainer(id, $container) { if (!$container.is(':visible')) $container = editor.$sc; if (!$container.is(popups[id].data('container'))) { popups[id].data('container', $container); $container.append(popups[id]); } } /** * Show popup at a specific position. */ function show (id, left, top, obj_height) { // Restore selection on show if it is there. if (areVisible() && editor.$el.find('.fr-marker').length > 0) { editor.events.disableBlur(); editor.selection.restore(); } else { // We must have focus into editor because we may want to save selection. editor.events.disableBlur(); editor.events.focus(); editor.events.enableBlur(); } hideAll([id]); if (!popups[id]) return false; // Hide active dropdowns. var $active_dropdowns = $('.fr-dropdown.fr-active'); $active_dropdowns.removeClass('fr-active').attr('aria-expanded', false).parent('.fr-toolbar').css('zIndex', ''); $active_dropdowns.next().attr('aria-hidden', true); // Set the current instance for the popup. popups[id].data('instance', editor); if (editor.$tb) editor.$tb.data('instance', editor); var width = popups[id].outerWidth(); var height = popups[id].outerHeight(); var is_visible = isVisible(id); popups[id].addClass('fr-active').removeClass('fr-hidden').find('input, textarea').removeAttr('disabled'); var $container = popups[id].data('container'); // Inline mode when container is toolbar. if (editor.opts.toolbarInline && $container && editor.$tb && $container.get(0) == editor.$tb.get(0)) { setContainer(id, editor.$sc); top = editor.$tb.offset().top - editor.helpers.getPX(editor.$tb.css('margin-top')); left = editor.$tb.offset().left + editor.$tb.outerWidth() / 2 + (parseFloat(editor.$tb.find('.fr-arrow').css('margin-left')) || 0) + editor.$tb.find('.fr-arrow').outerWidth() / 2; if (editor.node.hasClass(editor.$tb.get(0), 'fr-above') && top) { top += editor.$tb.outerHeight(); } obj_height = 0; } // Apply iframe correction. $container = popups[id].data('container'); if (editor.opts.iframe && !obj_height && !is_visible) { if (left) left -= editor.$iframe.offset().left; if (top) top -= editor.$iframe.offset().top; } // If container is toolbar then increase zindex. if ($container.is(editor.$tb)) { editor.$tb.css('zIndex', (editor.opts.zIndex || 1) + 4); } else { popups[id].css('zIndex', (editor.opts.zIndex || 1) + 4); } // Apply left correction. if (left) left = left - width / 2; // Toolbar at the bottom and container is toolbar. if (editor.opts.toolbarBottom && $container && editor.$tb && $container.get(0) == editor.$tb.get(0)) { popups[id].addClass('fr-above'); if (top) top = top - popups[id].outerHeight(); } // Position editor. popups[id].removeClass('fr-active'); editor.position.at(left, top, popups[id], obj_height || 0); popups[id].addClass('fr-active'); if (!is_visible) { editor.accessibility.focusPopup(popups[id]); } if (editor.opts.toolbarInline) editor.toolbar.hide(); editor.events.trigger('popups.show.' + id); // https://github.com/froala/wysiwyg-editor/issues/1248 _events(id)._repositionPopup(); _unmarkExit(); } function onShow (id, callback) { editor.events.on('popups.show.' + id, callback); } /** * Find visible popup. */ function isVisible (id) { return (popups[id] && editor.node.hasClass(popups[id], 'fr-active') && editor.core.sameInstance(popups[id])) || false; } /** * Check if there is any popup visible. */ function areVisible (new_instance) { for (var id in popups) { if (popups.hasOwnProperty(id)) { if (isVisible(id) && (typeof new_instance == 'undefined' || popups[id].data('instance') == new_instance)) return popups[id]; } } return false; } /** * Hide popup. */ function hide (id) { var $popup = null; if (typeof id !== 'string') { $popup = id; } else { $popup = popups[id]; } if ($popup && editor.node.hasClass($popup, 'fr-active')) { $popup.removeClass('fr-active fr-above'); editor.events.trigger('popups.hide.' + id); // Reset toolbar zIndex. if (editor.$tb) { if (editor.opts.zIndex > 1) { editor.$tb.css('zIndex', editor.opts.zIndex + 1); } else { editor.$tb.css('zIndex', ''); } } editor.events.disableBlur(); $popup.find('input, textarea, button').filter(':focus').blur(); $popup.find('input, textarea').attr('disabled', 'disabled'); } } /** * Assign an event for hiding. */ function onHide (id, callback) { editor.events.on('popups.hide.' + id, callback); } /** * Get the popup with the specific id. */ function get (id) { var $popup = popups[id]; if ($popup && !$popup.data('inst' + editor.id)) { var ev = _events(id); _bindInstanceEvents(ev, id); } return $popup; } function onRefresh (id, callback) { editor.events.on('popups.refresh.' + id, callback); } /** * Refresh content inside the popup. */ function refresh (id) { editor.events.trigger('popups.refresh.' + id); var btns = popups[id].find('.fr-command'); for (var i = 0; i < btns.length; i++) { var $btn = $(btns[i]); if ($btn.parents('.fr-dropdown-menu').length == 0) { editor.button.refresh($btn); } } } /** * Hide all popups. */ function hideAll (except) { if (typeof except == 'undefined') except = []; for (var id in popups) { if (popups.hasOwnProperty(id)) { if (except.indexOf(id) < 0) { hide(id); } } } } editor.shared.exit_flag = false; function _markExit () { editor.shared.exit_flag = true; } function _unmarkExit () { editor.shared.exit_flag = false; } function _canExit () { return editor.shared.exit_flag; } function _buildTemplate (id, template) { // Load template. var html = $.FE.POPUP_TEMPLATES[id]; if (typeof html == 'function') html = html.apply(editor); for (var nm in template) { if (template.hasOwnProperty(nm)) { html = html.replace('[_' + nm.toUpperCase() + '_]', template[nm]); } } return html; } function _build (id, template) { var html = _buildTemplate(id, template); var $popup = $('<div class="fr-popup' + (editor.helpers.isMobile() ? ' fr-mobile' : ' fr-desktop') + (editor.opts.toolbarInline ? ' fr-inline' : '') + '"><span class="fr-arrow"></span>' + html + '</div>'); if (editor.opts.theme) { $popup.addClass(editor.opts.theme + '-theme'); } if (editor.opts.zIndex > 1) { editor.$tb.css('z-index', editor.opts.zIndex + 2); } if (editor.opts.direction != 'auto') { $popup.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction); } $popup.find('input, textarea').attr('dir', editor.opts.direction).attr('disabled', 'disabled'); var $container = $('body'); $container.append($popup); $popup.data('container', $container); popups[id] = $popup; // Bind commands from the popup. editor.button.bindCommands($popup, false); return $popup; } function _events (id) { var $popup = popups[id]; return { /** * Resize window. */ _windowResize: function () { var inst = $popup.data('instance') || editor; if (!inst.helpers.isMobile() && $popup.is(':visible')) { inst.events.disableBlur(); inst.popups.hide(id); inst.events.enableBlur(); } }, /** * Focus on an input. */ _inputFocus: function (e) { var inst = $popup.data('instance') || editor; var $target = $(e.currentTarget); if ($target.is('input:file')) { $target.closest('.fr-layer').addClass('fr-input-focus'); } e.preventDefault(); e.stopPropagation(); // IE workaround. setTimeout(function () { inst.events.enableBlur(); }, 0); // Reposition scroll on mobile to the original one. if (inst.helpers.isMobile()) { var t = $(inst.o_win).scrollTop(); setTimeout(function () { $(inst.o_win).scrollTop(t); }, 0); } }, /** * Blur on an input. */ _inputBlur: function (e) { var inst = $popup.data('instance') || editor; var $target = $(e.currentTarget); if ($target.is('input:file')) { $target.closest('.fr-layer').removeClass('fr-input-focus'); } // Do not do blur on window change. if (document.activeElement != this && $(this).is(':visible')) { if (inst.events.blurActive()) { inst.events.trigger('blur'); } inst.events.enableBlur(); } }, /** * Editor keydown. */ _editorKeydown: function (e) { var inst = $popup.data('instance') || editor; // ESC. if (!inst.keys.ctrlKey(e) && e.which != $.FE.KEYCODE.ALT && e.which != $.FE.KEYCODE.ESC) { if (isVisible(id) && $popup.find('.fr-back:visible').length) { inst.button.exec($popup.find('.fr-back:visible:first')) } else { // Don't hide if alt alone is pressed to allow Alt + F10 shortcut for accessibility. if (e.which != $.FE.KEYCODE.ALT) { inst.popups.hide(id); } } } }, /** * Handling hitting the popup elements with the mouse. */ _preventFocus: function (e) { var inst = $popup.data('instance') || editor; // Hide popup's active dropdowns on mouseup. if (e.type == 'mouseup') { editor.button.hideActiveDropdowns($popup); } // Get the original target. var originalTarget = e.originalEvent ? (e.originalEvent.target || e.originalEvent.originalTarget) : null; // Do not disable blur on mouseup because it is the last event in the chain. if (e.type != 'mouseup' && !$(originalTarget).is(':focus')) inst.events.disableBlur(); // Define the input selector. var input_selector = 'input, textarea, button, select, label, .fr-command'; // Click was not made inside an input. if (originalTarget && !$(originalTarget).is(input_selector) && $(originalTarget).parents(input_selector).length === 0) { e.stopPropagation(); return false; } // Click was made on another input inside popup. Prevent propagation of the event. else if (originalTarget && $(originalTarget).is(input_selector)) { e.stopPropagation(); } _unmarkExit(); }, /** * Mouseup inside the editor. */ _editorMouseup: function (e) { // Check if popup is visible and we can exit. if ($popup.is(':visible') && _canExit()) { // If we have an input focused, then disable blur. if ($popup.find('input:focus, textarea:focus, button:focus, select:focus').filter(':visible').length > 0) { editor.events.disableBlur(); } } }, /** * Mouseup on window. */ _windowMouseup: function (e) { if (!editor.core.sameInstance($popup)) return true; var inst = $popup.data('instance') || editor; if ($popup.is(':visible') && _canExit()) { e.stopPropagation(); inst.markers.remove(); inst.popups.hide(id); _unmarkExit(); } }, /** * Keydown on window. */ _windowKeydown: function (e) { if (!editor.core.sameInstance($popup)) return true; var inst = $popup.data('instance') || editor; var key_code = e.which; // ESC. if ($.FE.KEYCODE.ESC == key_code) { if (inst.popups.isVisible(id) && inst.opts.toolbarInline) { e.stopPropagation(); if (inst.popups.isVisible(id)) { if ($popup.find('.fr-back:visible').length) { inst.button.exec($popup.find('.fr-back:visible:first')); // Focus back popup button. inst.accessibility.focusPopupButton($popup); } else if ($popup.find('.fr-dismiss:visible').length) { inst.button.exec($popup.find('.fr-dismiss:visible:first')); } else { inst.popups.hide(id); inst.toolbar.showInline(null, true); // Focus back popup button. inst.accessibility.FocusPopupButton($popup); } } return false; } else { if (inst.popups.isVisible(id)) { if ($popup.find('.fr-back:visible').length) { inst.button.exec($popup.find('.fr-back:visible:first')); // Focus back popup button. inst.accessibility.focusPopupButton($popup); } else if ($popup.find('.fr-dismiss:visible').length) { inst.button.exec($popup.find('.fr-dismiss:visible:first')); } else { inst.popups.hide(id); // Focus back popup button. inst.accessibility.focusPopupButton($popup); } return false; } } } }, /** * Placeholder effect. */ _doPlaceholder: function (e) { var $label = $(this).next(); if ($label.length == 0 && $(this).attr('placeholder')) { $(this).after('<label for="' + $(this).attr('id') + '">' + $(this).attr('placeholder') + '</label>'); } $(this).toggleClass('fr-not-empty', $(this).val() != ''); }, /** * Reposition popup. */ _repositionPopup: function (e) { // No height set or toolbar inline. if (!(editor.opts.height || editor.opts.heightMax) || editor.opts.toolbarInline) return true; if (editor.$wp && isVisible(id) && $popup.parent().get(0) == editor.$sc.get(0)) { // Popup top - wrapper top. var p_top = $popup.offset().top - editor.$wp.offset().top; // Wrapper height. var w_height = editor.$wp.outerHeight(); if (editor.node.hasClass($popup.get(0), 'fr-above')) p_top += $popup.outerHeight(); // 1. Popup top > w_height. // 2. Popup top + popup height < 0. if (p_top > w_height || p_top < 0) { $popup.addClass('fr-hidden'); } else { $popup.removeClass('fr-hidden'); } } } } } function _bindInstanceEvents (ev, id) { // Editor mouseup. editor.events.on('mouseup', ev._editorMouseup, true); if (editor.$wp) editor.events.on('keydown', ev._editorKeydown); // Hide all popups on blur. editor.events.on('blur', function (e) { if (areVisible()) editor.markers.remove(); hideAll(); }); // Update the position of the popup. if (editor.$wp && !editor.helpers.isMobile()) { editor.events.$on(editor.$wp, 'scroll.popup' + id, ev._repositionPopup); } editor.events.on('window.mouseup', ev._windowMouseup, true); editor.events.on('window.keydown', ev._windowKeydown, true); popups[id].data('inst' + editor.id, true); editor.events.on('destroy', function () { if (editor.core.sameInstance(popups[id])) { popups[id].removeClass('fr-active').appendTo('body'); } }, true) } /** * Create a popup. */ function create (id, template) { var $popup = _build(id, template); // Build events. var ev = _events(id); // Events binded here should be assigned in every instace. _bindInstanceEvents(ev, id); // Input Focus / Blur / Keydown. editor.events.$on($popup, 'mousedown mouseup touchstart touchend touch', '*', ev._preventFocus, true); editor.events.$on($popup, 'focus', 'input, textarea, button, select', ev._inputFocus, true); editor.events.$on($popup, 'blur', 'input, textarea, button, select', ev._inputBlur, true); // Register popup to handle keyboard accessibility. editor.accessibility.registerPopup(id); // Placeholder. editor.events.$on($popup, 'keydown keyup change input', 'input, textarea', ev._doPlaceholder, true); // Toggle checkbox. if (editor.helpers.isIOS()) { editor.events.$on($popup, 'touchend', 'label', function () { $('#' + $(this).attr('for')).prop('checked', function (i, val) { return !val; }) }, true); } // Window mouseup. editor.events.$on($(editor.o_win), 'resize', ev._windowResize, true); return $popup; } /** * Destroy. */ function _destroy () { for (var id in popups) { if (popups.hasOwnProperty(id)) { var $popup = popups[id]; $popup.html('').removeData().remove(); popups[id] = null; } } popups = []; } /** * Initialization. */ function _init () { editor.events.on('shared.destroy', _destroy, true); editor.events.on('window.mousedown', _markExit); editor.events.on('window.touchmove', _unmarkExit); editor.events.on('mousedown', function (e) { if (areVisible()) { e.stopPropagation(); // Remove markers. editor.$el.find('.fr-marker').remove(); // Prepare for exit. _markExit(); // Disable blur. editor.events.disableBlur(); } }) } return { _init: _init, create: create, get: get, show: show, hide: hide, onHide: onHide, hideAll: hideAll, setContainer: setContainer, refresh: refresh, onRefresh: onRefresh, onShow: onShow, isVisible: isVisible, areVisible: areVisible } }; $.FE.MODULES.position = function (editor) { /** * Get bounding rect around selection. * */ function getBoundingRect () { var range = editor.selection.ranges(0); var boundingRect = range.getBoundingClientRect(); if ((boundingRect.top == 0 && boundingRect.left == 0 && boundingRect.width == 0) || boundingRect.height == 0) { var remove = false; if (editor.$el.find('.fr-marker').length == 0) { editor.selection.save(); remove = true; } var $marker = editor.$el.find('.fr-marker:first'); $marker.css('display', 'inline'); $marker.css('line-height', ''); var offset = $marker.offset(); var height = $marker.outerHeight(); $marker.css('display', 'none'); $marker.css('line-height', 0); boundingRect = {} boundingRect.left = offset.left; boundingRect.width = 0; boundingRect.height = height; boundingRect.top = offset.top - (editor.helpers.isMobile() ? 0 : editor.helpers.scrollTop()); boundingRect.right = 1; boundingRect.bottom = 1; boundingRect.ok = true; if (remove) editor.selection.restore(); } return boundingRect; } /** * Normalize top positioning. */ function _topNormalized ($el, top, obj_height) { var height = $el.get(0).offsetHeight; if (!editor.helpers.isMobile() && editor.$tb && $el.parent().get(0) != editor.$tb.get(0)) { // 1. Parent offset + toolbar top + toolbar height > scrollableContainer height. // 2. Selection doesn't go above the screen. var p_height = $el.get(0).parentNode.clientHeight - 20 - (editor.opts.toolbarBottom ? editor.$tb.get(0).offsetHeight : 0); var p_offset = $el.parent().offset().top; var new_top = top - height - (obj_height || 0); if ($el.parent().get(0) == editor.$sc.get(0)) p_offset = p_offset - $el.parent().position().top; var s_height = editor.$sc.get(0).scrollHeight; if (p_offset + top + height > editor.$sc.offset().top + s_height && $el.parent().offset().top + new_top > 0) { top = new_top; $el.addClass('fr-above'); } else { $el.removeClass('fr-above'); } } return top; } /** * Normalize left position. */ function _leftNormalized ($el, left) { var width = $el.get(0).offsetWidth; // Normalize right. if (left + width > editor.$sc.get(0).clientWidth - 10) { left = editor.$sc.get(0).clientWidth - width - 10; } // Normalize left. if (left < 0) { left = 10; } return left; } /** * Place editor below selection. */ function forSelection ($el) { var selection_rect = getBoundingRect(); $el.css({ top: 0, left: 0 }); var top = selection_rect.top + selection_rect.height; var left = selection_rect.left + selection_rect.width / 2 - $el.get(0).offsetWidth / 2 + editor.helpers.scrollLeft(); if (!editor.opts.iframe) { top += editor.helpers.scrollTop(); } at(left, top, $el, selection_rect.height); } /** * Position element at the specified position. */ function at (left, top, $el, obj_height) { var $container = $el.data('container'); if ($container && ($container.get(0).tagName !== 'BODY' || $container.css('position') != 'static')) { if (left) left -= $container.offset().left; if (top) top -= $container.offset().top; if ($container.get(0).tagName != 'BODY') { if (left) left += $container.get(0).scrollLeft; if (top) top += $container.get(0).scrollTop; } else if ($container.css('position') == 'absolute') { if (left) left += $container.position().left; if (top) top += $container.position().top; } } // Apply iframe correction. if (editor.opts.iframe && $container && editor.$tb && $container.get(0) != editor.$tb.get(0)) { if (left) left += editor.$iframe.offset().left; if (top) top += editor.$iframe.offset().top; } var new_left = _leftNormalized($el, left); if (left) { // Set the new left. $el.css('left', new_left); // Normalize arrow. var $arrow = $el.data('fr-arrow'); if (!$arrow) { $arrow = $el.find('.fr-arrow'); $el.data('fr-arrow', $arrow) } if (!$arrow.data('margin-left')) $arrow.data('margin-left', editor.helpers.getPX($arrow.css('margin-left'))); $arrow.css('margin-left', left - new_left + $arrow.data('margin-left')); } if (top) { $el.css('top', _topNormalized($el, top, obj_height)); } } /** * Special case for update sticky on iOS. */ function _updateIOSSticky (el) { var $el = $(el); var is_on = $el.is('.fr-sticky-on'); var prev_top = $el.data('sticky-top'); var scheduled_top = $el.data('sticky-scheduled'); // Create a dummy div that we show then sticky is on. if (typeof prev_top == 'undefined') { $el.data('sticky-top', 0); var $dummy = $('<div class="fr-sticky-dummy" style="height: ' + $el.outerHeight() + 'px;"></div>'); editor.$box.prepend($dummy); } else { editor.$box.find('.fr-sticky-dummy').css('height', $el.outerHeight()); } // Position sticky doesn't work when the keyboard is on the screen. if (editor.core.hasFocus() || editor.$tb.find('input:visible:focus').length > 0) { // Get the current scroll. var x_scroll = editor.helpers.scrollTop(); // Get the current top. // We make sure that we keep it within the editable box. var x_top = Math.min(Math.max(x_scroll - editor.$tb.parent().offset().top, 0), editor.$tb.parent().outerHeight() - $el.outerHeight()); // Not the same top and different than the already scheduled. if (x_top != prev_top && x_top != scheduled_top) { // Clear any too soon change to avoid flickering. clearTimeout($el.data('sticky-timeout')); // Store the current scheduled top. $el.data('sticky-scheduled', x_top); // Hide the toolbar for a rich experience. if ($el.outerHeight() < x_scroll - editor.$tb.parent().offset().top) { $el.addClass('fr-opacity-0'); } // Set the timeout for changing top. // Based on the test 100ms seems to be the best timeout. $el.data('sticky-timeout', setTimeout(function () { // Get the current top. var c_scroll = editor.helpers.scrollTop(); var c_top = Math.min(Math.max(c_scroll - editor.$tb.parent().offset().top, 0), editor.$tb.parent().outerHeight() - $el.outerHeight()); if (c_top > 0 && editor.$tb.parent().get(0).tagName == 'BODY') c_top += editor.$tb.parent().position().top; // Don't update if it is not different than the prev top. if (c_top != prev_top) { $el.css('top', Math.max(c_top, 0)); $el.data('sticky-top', c_top); $el.data('sticky-scheduled', c_top); } // Show toolbar. $el.removeClass('fr-opacity-0'); }, 100)); } // Turn on sticky mode. if (!is_on) { $el.css('top', '0'); $el.width(editor.$tb.parent().width()); $el.addClass('fr-sticky-on'); editor.$box.addClass('fr-sticky-box'); } } // Turn off sticky mode. else { clearTimeout($(el).css('sticky-timeout')); $el.css('top', '0'); $el.css('position', ''); $el.width(''); $el.data('sticky-top', 0); $el.removeClass('fr-sticky-on'); editor.$box.removeClass('fr-sticky-box'); } } /** * Update sticky location for browsers that don't support sticky. * https://github.com/filamentgroup/fixed-sticky * * The MIT License (MIT) * * Copyright (c) 2013 Filament Group */ function _updateSticky (el) { if( !el.offsetWidth ) { return; } var el_top; var el_bottom; var $el = $(el); var height = $el.outerHeight(); var position = $el.data('sticky-position'); // Viewport height. var viewport_height = $(editor.opts.scrollableContainer == 'body' ? editor.o_win : editor.opts.scrollableContainer).outerHeight(); var scrollable_top = 0; var scrollable_bottom = 0; if (editor.opts.scrollableContainer !== 'body') { scrollable_top = editor.$sc.offset().top; scrollable_bottom = $(editor.o_win).outerHeight() - scrollable_top - viewport_height; } var offset_top = editor.opts.scrollableContainer == 'body' ? editor.helpers.scrollTop() : scrollable_top; var is_on = $el.is('.fr-sticky-on'); // Decide parent. if (!$el.data('sticky-parent')) { $el.data('sticky-parent', $el.parent()); } var $parent = $el.data('sticky-parent'); var parent_top = $parent.offset().top; var parent_height = $parent.outerHeight(); if (!$el.data('sticky-offset')) { $el.data('sticky-offset', true); $el.after('<div class="fr-sticky-dummy" style="height: ' + height + 'px;"></div>'); } // Detect position placement. if (!position) { // Some browsers require fixed/absolute to report accurate top/left values. var skip_setting_fixed = $el.css('top') !== 'auto' || $el.css('bottom') !== 'auto'; // Set to position fixed for a split of second. if(!skip_setting_fixed) { $el.css('position', 'fixed'); } // Find position. position = { top: editor.node.hasClass($el.get(0), 'fr-top'), bottom: editor.node.hasClass($el.get(0), 'fr-bottom') }; // Remove position fixed. if(!skip_setting_fixed) { $el.css('position', ''); } // Store position. $el.data('sticky-position', position); $el.data('top', editor.node.hasClass($el.get(0), 'fr-top') ? $el.css('top') : 'auto'); $el.data('bottom', editor.node.hasClass($el.get(0), 'fr-bottom') ? $el.css('bottom') : 'auto'); } // Detect if is OK to fix at the top. var isFixedToTop = function () { // 1. Top condition. // 2. Bottom condition. return parent_top < offset_top + el_top && parent_top + parent_height - height >= offset_top + el_top; } // Detect if it is OK to fix at the bottom. var isFixedToBottom = function () { return parent_top + height < offset_top + viewport_height - el_bottom && parent_top + parent_height > offset_top + viewport_height - el_bottom ; } el_top = editor.helpers.getPX($el.data('top')); el_bottom = editor.helpers.getPX($el.data('bottom')); var at_top = (position.top && isFixedToTop()); var at_bottom = (position.bottom && isFixedToBottom()); // Should be fixed. if (at_top || at_bottom) { $el.css('width', $parent.width() + 'px'); if (!is_on) { $el.addClass('fr-sticky-on') $el.removeClass('fr-sticky-off'); if ($el.css('top')) { if ($el.data('top') != 'auto') { $el.css('top', editor.helpers.getPX($el.data('top')) + scrollable_top); } else { $el.data('top', 'auto'); } } if ($el.css('bottom')) { if ($el.data('bottom') != 'auto') { $el.css('bottom', editor.helpers.getPX($el.data('bottom')) + scrollable_bottom); } else { $el.css('bottom', 'auto'); } } } } // Shouldn't be fixed. else { if (!editor.node.hasClass($el.get(0), 'fr-sticky-off')) { // Reset. $el.width(''); $el.removeClass('fr-sticky-on'); $el.addClass('fr-sticky-off'); if ($el.css('top') && $el.data('top') != 'auto' && position.top) { $el.css('top', 0); } if ($el.css('bottom') && $el.data('bottom') != 'auto' && position.bottom) { $el.css('bottom', 0); } } } } /** * Test if browser supports sticky. * https://github.com/filamentgroup/fixed-sticky * * The MIT License (MIT) * * Copyright (c) 2013 Filament Group */ function _testSticky () { var el = document.createElement('test'); var mStyle = el.style; mStyle.cssText = 'position:' + [ '-webkit-', '-moz-', '-ms-', '-o-', '' ].join('sticky; position:') + ' sticky;'; return mStyle['position'].indexOf('sticky') !== -1 && !editor.helpers.isIOS() && !editor.helpers.isAndroid() && !editor.browser.chrome; } /** * Initialize sticky position. */ function _initSticky () { if (!_testSticky()) { editor._stickyElements = []; // iOS special case. if (editor.helpers.isIOS()) { // Use an animation frame to make sure we're always OK with the updates. var animate = function () { editor.helpers.requestAnimationFrame()(animate); for (var i = 0; i < editor._stickyElements.length; i++) { _updateIOSSticky(editor._stickyElements[i]); } }; animate(); // Hide toolbar on touchmove. This is very useful on iOS versions < 8. editor.events.$on($(editor.o_win), 'scroll', function () { if (editor.core.hasFocus()) { for (var i = 0; i < editor._stickyElements.length; i++) { var $el = $(editor._stickyElements[i]); var $parent = $el.parent(); var c_scroll = editor.helpers.scrollTop(); if ($el.outerHeight() < c_scroll - $parent.offset().top) { $el.addClass('fr-opacity-0'); $el.data('sticky-top', -1); $el.data('sticky-scheduled', -1); } } } }, true); } // Default case. Do the updates on scroll. else { editor.events.$on($(editor.opts.scrollableContainer == 'body' ? editor.o_win : editor.opts.scrollableContainer), 'scroll', refresh, true); editor.events.$on($(editor.o_win), 'resize', refresh, true); editor.events.on('initialized', refresh); editor.events.on('focus', refresh); editor.events.$on($(editor.o_win), 'resize', 'textarea', refresh, true); } } editor.events.on('destroy', function (e) { editor._stickyElements = []; }); } function refresh () { if (editor._stickyElements) { for (var i = 0; i < editor._stickyElements.length; i++) { _updateSticky(editor._stickyElements[i]); } } } /** * Mark element as sticky. */ function addSticky ($el) { $el.addClass('fr-sticky'); if (editor.helpers.isIOS()) $el.addClass('fr-sticky-ios'); if (!_testSticky()) { editor._stickyElements.push($el.get(0)); } } function _init () { _initSticky(); } return { _init: _init, forSelection: forSelection, addSticky: addSticky, refresh: refresh, at: at, getBoundingRect: getBoundingRect } }; $.FE.MODULES.refresh = function (editor) { function undo ($btn) { _setDisabled($btn, !editor.undo.canDo()) } function redo ($btn) { _setDisabled($btn, !editor.undo.canRedo()); } function indent ($btn) { if (editor.node.hasClass($btn.get(0), 'fr-no-refresh')) return false; var blocks = editor.selection.blocks(); for (var i = 0; i < blocks.length; i++) { var p_node = blocks[i].previousSibling; while (p_node && p_node.nodeType == Node.TEXT_NODE && p_node.textContent.length === 0) { p_node = p_node.previousSibling; } if (blocks[i].tagName == 'LI' && !p_node) { _setDisabled($btn, true); } else { _setDisabled($btn, false); return true; } } } function outdent ($btn) { if (editor.node.hasClass($btn.get(0), 'fr-no-refresh')) return false; var blocks = editor.selection.blocks(); for (var i = 0; i < blocks.length; i++) { var prop = (editor.opts.direction == 'rtl' || $(blocks[i]).css('direction') == 'rtl') ? 'margin-right' : 'margin-left'; if (blocks[i].tagName == 'LI' || blocks[i].parentNode.tagName == 'LI') { _setDisabled($btn, false); return true; } if (editor.helpers.getPX($(blocks[i]).css(prop)) > 0) { _setDisabled($btn, false); return true; } } _setDisabled($btn, true); } /** * Disable/enable buton. */ function _setDisabled ($btn, disabled) { $btn.toggleClass('fr-disabled', disabled).attr('aria-disabled', disabled); } return { undo: undo, redo: redo, outdent: outdent, indent: indent } }; $.extend($.FE.DEFAULTS, { editInPopup: false }); $.FE.MODULES.textEdit = function (editor) { function _initPopup () { // Image buttons. var txt = '<div id="fr-text-edit-' + editor.id + '" class="fr-layer fr-text-edit-layer"><div class="fr-input-line"><input type="text" placeholder="' + editor.language.translate('Text') + '" tabIndex="1"></div><div class="fr-action-buttons"><button type="button" class="fr-command fr-submit" data-cmd="updateText" tabIndex="2">' + editor.language.translate('Update') + '</button></div></div>' var template = { edit: txt }; var $popup = editor.popups.create('text.edit', template); } function _showPopup () { var $popup = editor.popups.get('text.edit'); var text; if (editor.$el.prop('tagName') === 'INPUT') { text = editor.$el.attr('placeholder'); } else { text = editor.$el.text(); } $popup.find('input').val(text).trigger('change'); editor.popups.setContainer('text.edit', $('body')); editor.popups.show('text.edit', editor.$el.offset().left + editor.$el.outerWidth() / 2, editor.$el.offset().top + editor.$el.outerHeight(), editor.$el.outerHeight()); } function _initEvents () { // Show edit popup. editor.events.$on(editor.$el, editor._mouseup, function (e) { setTimeout (function () { _showPopup(); }, 10); }) } function update () { var $popup = editor.popups.get('text.edit'); var new_text = $popup.find('input').val(); if (new_text.length == 0) new_text = editor.opts.placeholderText; if (editor.$el.prop('tagName') === 'INPUT') { editor.$el.attr('placeholder', new_text); } else { editor.$el.text(new_text); } editor.events.trigger('contentChanged'); editor.popups.hide('text.edit'); } /** * Initialize. */ function _init () { if (editor.opts.editInPopup) { _initPopup(); _initEvents(); } } return { _init: _init, update: update } }; $.FE.RegisterCommand('updateText', { focus: false, undo: false, callback: function () { this.textEdit.update(); } }) // Extend defaults. $.extend($.FE.DEFAULTS, { toolbarBottom: false, toolbarButtons: ['fullscreen', 'print', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', 'fontFamily', 'fontSize', '|', 'specialCharacters', 'color', 'emoticons', 'inlineStyle', 'paragraphStyle', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', '-', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting', 'selectAll', 'html', 'applyFormat', 'removeFormat', 'help'], toolbarButtonsXS: ['bold', 'italic', 'fontFamily', 'fontSize', '|', 'undo', 'redo'], toolbarButtonsSM: ['bold', 'italic', 'underline', '|', 'fontFamily', 'fontSize', 'insertLink', 'insertImage', 'table', '|', 'undo', 'redo'], toolbarButtonsMD: ['fullscreen', 'bold', 'italic', 'underline', 'fontFamily', 'fontSize', 'color', 'paragraphStyle', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', '-', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting'], toolbarContainer: null, toolbarInline: false, toolbarSticky: true, toolbarStickyOffset: 0, toolbarVisibleWithoutSelection: false }); $.FE.MODULES.toolbar = function (editor) { var _document, _window; // Create a button map for each screen size. var _buttons_map = []; _buttons_map[$.FE.XS] = editor.opts.toolbarButtonsXS || editor.opts.toolbarButtons; _buttons_map[$.FE.SM] = editor.opts.toolbarButtonsSM || editor.opts.toolbarButtons; _buttons_map[$.FE.MD] = editor.opts.toolbarButtonsMD || editor.opts.toolbarButtons; _buttons_map[$.FE.LG] = editor.opts.toolbarButtons; function _addOtherButtons (buttons, toolbarButtons) { for (var i = 0; i < toolbarButtons.length; i++) { if (toolbarButtons[i] != '-' && toolbarButtons[i] != '|' && buttons.indexOf(toolbarButtons[i]) < 0) { buttons.push(toolbarButtons[i]); } } } /** * Add buttons to the toolbar. */ function _addButtons () { var _buttons = $.merge([], _screenButtons()); _addOtherButtons(_buttons, editor.opts.toolbarButtonsXS || []); _addOtherButtons(_buttons, editor.opts.toolbarButtonsSM || []); _addOtherButtons(_buttons, editor.opts.toolbarButtonsMD || []); _addOtherButtons(_buttons, editor.opts.toolbarButtons); for (var i = _buttons.length - 1; i >= 0; i--) { if (_buttons[i] != '-' && _buttons[i] != '|' && _buttons.indexOf(_buttons[i]) < i) { _buttons.splice(i, 1); } } var buttons_list = editor.button.buildList(_buttons, _screenButtons()); editor.$tb.append(buttons_list); editor.button.bindCommands(editor.$tb); } /** * The buttons that should be visible on the current screen size. */ function _screenButtons () { var screen_size = editor.helpers.screenSize(); return _buttons_map[screen_size]; } function _showScreenButtons () { var c_buttons = _screenButtons(); // Remove separator from toolbar. editor.$tb.find('.fr-separator').remove(); // Hide all buttons. editor.$tb.find('> .fr-command').addClass('fr-hidden'); // Reorder buttons. for (var i = 0; i < c_buttons.length; i++) { if (c_buttons[i] == '|' || c_buttons[i] == '-') { editor.$tb.append(editor.button.buildList([c_buttons[i]])); } else { var $btn = editor.$tb.find('> .fr-command[data-cmd="' + c_buttons[i] + '"]'); var $dropdown = null; if (editor.node.hasClass($btn.next().get(0), 'fr-dropdown-menu')) $dropdown = $btn.next(); $btn.removeClass('fr-hidden').appendTo(editor.$tb); if ($dropdown) $dropdown.appendTo(editor.$tb); } } } /** * Set the buttons visibility based on screen size. */ function _setVisibility () { editor.events.$on($(editor.o_win), 'resize', _showScreenButtons); editor.events.$on($(editor.o_win), 'orientationchange', _showScreenButtons); } function showInline (e, force) { setTimeout(function () { if (e && e.which == $.FE.KEYCODE.ESC) { // Nothing. } else if (editor.selection.inEditor() && editor.core.hasFocus() && !editor.popups.areVisible()) { if (editor.opts.toolbarVisibleWithoutSelection || (!editor.selection.isCollapsed() && !editor.keys.isIME()) || force) { editor.$tb.data('instance', editor); // Check if we should actually show the toolbar. if (editor.events.trigger('toolbar.show', [e]) == false) return false; editor.$tb.show(); if (!editor.opts.toolbarContainer) { editor.position.forSelection(editor.$tb); } if (editor.opts.zIndex > 1) { editor.$tb.css('z-index', editor.opts.zIndex + 1); } else { editor.$tb.css('z-index', null); } } } }, 0); } function hide (e) { // Prevent hiding when dropdown is active and we scoll in it. // https://github.com/froala/wysiwyg-editor/issues/1290 var $active_dropdowns = $('.fr-dropdown.fr-active'); if ($active_dropdowns.next().find(editor.o_doc.activeElement).length) return true; // Check if we should actually hide the toolbar. if (editor.events.trigger('toolbar.hide') !== false) { editor.$tb.hide(); } } function show () { // Check if we should actually hide the toolbar. if (editor.events.trigger('toolbar.show') == false) return false; editor.$tb.show(); } var tm = null; function _showInlineWithTimeout (e) { clearTimeout(tm); if (!e || e.which != $.FE.KEYCODE.ESC) { tm = setTimeout(showInline, editor.opts.typingTimer); } } /** * Set the events for show / hide toolbar. */ function _initInlineBehavior () { // Window mousedown. editor.events.on('window.mousedown', hide); // Element keydown. editor.events.on('keydown', hide); // Element blur. editor.events.on('blur', hide); // Window mousedown. editor.events.on('window.mouseup', showInline); var t = null; if (editor.helpers.isMobile()) { if (!editor.helpers.isIOS()) { editor.events.on('window.touchend', showInline); if (editor.browser.mozilla) { setInterval(showInline, 200); } } } else { editor.events.on('window.keyup', _showInlineWithTimeout); } // Hide editor on ESC. editor.events.on('keydown', function (e) { if (e && e.which == $.FE.KEYCODE.ESC) { hide(); } }); // Enable accessibility shortcut. editor.events.on('keydown', function (e) { if (e.which == $.FE.KEYCODE.ALT) { e.stopPropagation(); return false; } }, true); editor.events.$on(editor.$wp, 'scroll.toolbar', showInline); editor.events.on('commands.after', showInline); if (editor.helpers.isMobile()) { editor.events.$on(editor.$doc, 'selectionchange', _showInlineWithTimeout); editor.events.$on(editor.$doc, 'orientationchange', showInline); } } function _initPositioning () { // Toolbar is inline. if (editor.opts.toolbarInline) { // Mobile should handle this as regular. editor.$sc.append(editor.$tb); // Add toolbar to body. editor.$tb.data('container', editor.$sc); // Add inline class. editor.$tb.addClass('fr-inline'); // Add arrow. editor.$tb.prepend('<span class="fr-arrow"></span>') // Init mouse behavior. _initInlineBehavior(); editor.opts.toolbarBottom = false; } // Toolbar is normal. else { // Won't work on iOS. if (editor.opts.toolbarBottom && !editor.helpers.isIOS()) { editor.$box.append(editor.$tb); editor.$tb.addClass('fr-bottom'); editor.$box.addClass('fr-bottom'); } else { editor.opts.toolbarBottom = false; editor.$box.prepend(editor.$tb); editor.$tb.addClass('fr-top'); editor.$box.addClass('fr-top'); } editor.$tb.addClass('fr-basic'); if (editor.opts.toolbarSticky) { if (editor.opts.toolbarStickyOffset) { if (editor.opts.toolbarBottom) { editor.$tb.css('bottom', editor.opts.toolbarStickyOffset); } else { editor.$tb.css('top', editor.opts.toolbarStickyOffset); } } editor.position.addSticky(editor.$tb); } } } /** * Destroy. */ function _sharedDestroy () { editor.$tb.html('').removeData().remove(); editor.$tb = null; } function _destroy () { editor.$box.removeClass('fr-top fr-bottom fr-inline fr-basic'); editor.$box.find('.fr-sticky-dummy').remove(); } function _setDefaults () { if (editor.opts.theme) { editor.$tb.addClass(editor.opts.theme + '-theme'); } if (editor.opts.zIndex > 1) { editor.$tb.css('z-index', editor.opts.zIndex + 1); } // Set direction. if (editor.opts.direction != 'auto') { editor.$tb.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction); } // Mark toolbar for desktop / mobile. if (!editor.helpers.isMobile()) { editor.$tb.addClass('fr-desktop'); } else { editor.$tb.addClass('fr-mobile'); } // Set the toolbar specific position inline / normal. if (!editor.opts.toolbarContainer) { _initPositioning(); } else { if (editor.opts.toolbarInline) { _initInlineBehavior(); hide(); } if (editor.opts.toolbarBottom) editor.$tb.addClass('fr-bottom'); else editor.$tb.addClass('fr-top'); } // Set documetn and window for toolbar. _document = editor.$tb.get(0).ownerDocument; _window = 'defaultView' in _document ? _document.defaultView : _document.parentWindow; // Add buttons to the toolbar. // Set their visibility for different screens. // Asses commands to the butttons. _addButtons(); _setVisibility(); editor.accessibility.registerToolbar(editor.$tb); // Make sure we don't trigger blur. editor.events.$on(editor.$tb, editor._mousedown + ' ' + editor._mouseup, function (e) { var originalTarget = e.originalEvent ? (e.originalEvent.target || e.originalEvent.originalTarget) : null; if (originalTarget && originalTarget.tagName != 'INPUT' && !editor.edit.isDisabled()) { e.stopPropagation(); e.preventDefault(); return false; } }, true); } /** * Initialize */ var tb_exists = false; function _init () { editor.$sc = $(editor.opts.scrollableContainer); if (!editor.$wp) return false; // Container for toolbar. if (editor.opts.toolbarContainer) { // Shared toolbar. if (!editor.shared.$tb) { editor.shared.$tb = $('<div class="fr-toolbar"></div>'); editor.$tb = editor.shared.$tb; $(editor.opts.toolbarContainer).append(editor.$tb); _setDefaults(); editor.$tb.data('instance', editor); } else { editor.$tb = editor.shared.$tb; if (editor.opts.toolbarInline) _initInlineBehavior(); } if (editor.opts.toolbarInline) { // Update box. editor.$box.addClass('fr-inline'); } else { editor.$box.addClass('fr-basic'); } // On focus set the current instance. editor.events.on('focus', function () { editor.$tb.data('instance', editor); }, true); editor.opts.toolbarInline = false; } else { // Inline toolbar. if (editor.opts.toolbarInline) { // Update box. editor.$box.addClass('fr-inline'); // Check for shared toolbar. if (!editor.shared.$tb) { editor.shared.$tb = $('<div class="fr-toolbar"></div>'); editor.$tb = editor.shared.$tb; _setDefaults(); } else { editor.$tb = editor.shared.$tb; // Init mouse behavior. _initInlineBehavior(); } } else { editor.$box.addClass('fr-basic'); editor.$tb = $('<div class="fr-toolbar"></div>'); _setDefaults(); editor.$tb.data('instance', editor); } } // Destroy. editor.events.on('destroy', _destroy, true); editor.events.on(!editor.opts.toolbarInline && !editor.opts.toolbarContainer ? 'destroy' : 'shared.destroy', _sharedDestroy, true); } var disabled = false; function disable () { if (!disabled && editor.$tb) { editor.$tb.find('> .fr-command').addClass('fr-disabled fr-no-refresh').attr('aria-disabled', true); disabled = true; } } function enable () { if (disabled && editor.$tb) { editor.$tb.find('> .fr-command').removeClass('fr-disabled fr-no-refresh').attr('aria-disabled', false); disabled = false; } editor.button.bulkRefresh(); } return { _init: _init, hide: hide, show: show, showInline: showInline, disable: disable, enable: enable } }; }));