').css({
+ position: "absolute",
+ top: 0, left: 0,
+ width: docSize.width,
+ height: docSize.height,
+ zIndex: 0x7FFFFFFF,
+ opacity: 0.0001,
+ cursor: cursor
+ }).appendTo(doc.body);
+
+ $(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop);
+
+ settings.start(e);
+ };
+
+ drag = function(e) {
+ updateWithTouchData(e);
+
+ if (e.button !== downButton) {
+ return stop(e);
+ }
+
+ e.deltaX = e.screenX - startX;
+ e.deltaY = e.screenY - startY;
+
+ e.preventDefault();
+ settings.drag(e);
+ };
+
+ stop = function(e) {
+ updateWithTouchData(e);
+
+ $(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop);
+
+ $eventOverlay.remove();
+
+ if (settings.stop) {
+ settings.stop(e);
+ }
+ };
+
+ /**
+ * Destroys the drag/drop helper instance.
+ *
+ * @method destroy
+ */
+ this.destroy = function() {
+ $(getHandleElm()).off();
+ };
+
+ $(getHandleElm()).on('mousedown touchstart', start);
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Scrollable.js
+
+/**
+ * Scrollable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This mixin makes controls scrollable using custom scrollbars.
+ *
+ * @-x-less Scrollable.less
+ * @mixin tinymce.ui.Scrollable
+ */
+define("tinymce/ui/Scrollable", [
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DragHelper"
+], function($, DragHelper) {
+ "use strict";
+
+ return {
+ init: function() {
+ var self = this;
+ self.on('repaint', self.renderScroll);
+ },
+
+ renderScroll: function() {
+ var self = this, margin = 2;
+
+ function repaintScroll() {
+ var hasScrollH, hasScrollV, bodyElm;
+
+ function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) {
+ var containerElm, scrollBarElm, scrollThumbElm;
+ var containerSize, scrollSize, ratio, rect;
+ var posNameLower, sizeNameLower;
+
+ scrollBarElm = self.getEl('scroll' + axisName);
+ if (scrollBarElm) {
+ posNameLower = posName.toLowerCase();
+ sizeNameLower = sizeName.toLowerCase();
+
+ $(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1);
+
+ if (!hasScroll) {
+ $(scrollBarElm).css('display', 'none');
+ return;
+ }
+
+ $(scrollBarElm).css('display', 'block');
+ containerElm = self.getEl('body');
+ scrollThumbElm = self.getEl('scroll' + axisName + "t");
+ containerSize = containerElm["client" + sizeName] - (margin * 2);
+ containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0;
+ scrollSize = containerElm["scroll" + sizeName];
+ ratio = containerSize / scrollSize;
+
+ rect = {};
+ rect[posNameLower] = containerElm["offset" + posName] + margin;
+ rect[sizeNameLower] = containerSize;
+ $(scrollBarElm).css(rect);
+
+ rect = {};
+ rect[posNameLower] = containerElm["scroll" + posName] * ratio;
+ rect[sizeNameLower] = containerSize * ratio;
+ $(scrollThumbElm).css(rect);
+ }
+ }
+
+ bodyElm = self.getEl('body');
+ hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth;
+ hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight;
+
+ repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height");
+ repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width");
+ }
+
+ function addScroll() {
+ function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) {
+ var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix;
+
+ $(self.getEl()).append(
+ '
'
+ );
+
+ self.draghelper = new DragHelper(axisId + 't', {
+ start: function() {
+ scrollStart = self.getEl('body')["scroll" + posName];
+ $('#' + axisId).addClass(prefix + 'active');
+ },
+
+ drag: function(e) {
+ var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect();
+
+ hasScrollH = layoutRect.contentW > layoutRect.innerW;
+ hasScrollV = layoutRect.contentH > layoutRect.innerH;
+ containerSize = self.getEl('body')["client" + sizeName] - (margin * 2);
+ containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0;
+
+ ratio = containerSize / self.getEl('body')["scroll" + sizeName];
+ self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio);
+ },
+
+ stop: function() {
+ $('#' + axisId).removeClass(prefix + 'active');
+ }
+ });
+ }
+
+ self.classes.add('scroll');
+
+ addScrollAxis("v", "Top", "Height", "Y", "Width");
+ addScrollAxis("h", "Left", "Width", "X", "Height");
+ }
+
+ if (self.settings.autoScroll) {
+ if (!self._hasScroll) {
+ self._hasScroll = true;
+ addScroll();
+
+ self.on('wheel', function(e) {
+ var bodyEl = self.getEl('body');
+
+ bodyEl.scrollLeft += (e.deltaX || 0) * 10;
+ bodyEl.scrollTop += e.deltaY * 10;
+
+ repaintScroll();
+ });
+
+ $(self.getEl('body')).on("scroll", repaintScroll);
+ }
+
+ repaintScroll();
+ }
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Panel.js
+
+/**
+ * Panel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new panel.
+ *
+ * @-x-less Panel.less
+ * @class tinymce.ui.Panel
+ * @extends tinymce.ui.Container
+ * @mixes tinymce.ui.Scrollable
+ */
+define("tinymce/ui/Panel", [
+ "tinymce/ui/Container",
+ "tinymce/ui/Scrollable"
+], function(Container, Scrollable) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ layout: 'fit',
+ containerCls: 'panel'
+ },
+
+ Mixins: [Scrollable],
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, innerHtml = self.settings.html;
+
+ self.preRender();
+ layout.preRender(self);
+
+ if (typeof innerHtml == "undefined") {
+ innerHtml = (
+ '
' +
+ layout.renderHtml(self) +
+ '
'
+ );
+ } else {
+ if (typeof innerHtml == 'function') {
+ innerHtml = innerHtml.call(self);
+ }
+
+ self._hasBody = false;
+ }
+
+ return (
+ '
' +
+ (self._preBodyHtml || '') +
+ innerHtml +
+ '
'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Movable.js
+
+/**
+ * Movable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Movable mixin. Makes controls movable absolute and relative to other elements.
+ *
+ * @mixin tinymce.ui.Movable
+ */
+define("tinymce/ui/Movable", [
+ "tinymce/ui/DomUtils"
+], function(DomUtils) {
+ "use strict";
+
+ function calculateRelativePosition(ctrl, targetElm, rel) {
+ var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
+
+ viewport = DomUtils.getViewPort();
+
+ // Get pos of target
+ pos = DomUtils.getPos(targetElm);
+ x = pos.x;
+ y = pos.y;
+
+ if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') {
+ x -= viewport.x;
+ y -= viewport.y;
+ }
+
+ // Get size of self
+ ctrlElm = ctrl.getEl();
+ size = DomUtils.getSize(ctrlElm);
+ selfW = size.width;
+ selfH = size.height;
+
+ // Get size of target
+ size = DomUtils.getSize(targetElm);
+ targetW = size.width;
+ targetH = size.height;
+
+ // Parse align string
+ rel = (rel || '').split('');
+
+ // Target corners
+ if (rel[0] === 'b') {
+ y += targetH;
+ }
+
+ if (rel[1] === 'r') {
+ x += targetW;
+ }
+
+ if (rel[0] === 'c') {
+ y += Math.round(targetH / 2);
+ }
+
+ if (rel[1] === 'c') {
+ x += Math.round(targetW / 2);
+ }
+
+ // Self corners
+ if (rel[3] === 'b') {
+ y -= selfH;
+ }
+
+ if (rel[4] === 'r') {
+ x -= selfW;
+ }
+
+ if (rel[3] === 'c') {
+ y -= Math.round(selfH / 2);
+ }
+
+ if (rel[4] === 'c') {
+ x -= Math.round(selfW / 2);
+ }
+
+ return {
+ x: x,
+ y: y,
+ w: selfW,
+ h: selfH
+ };
+ }
+
+ return {
+ /**
+ * Tests various positions to get the most suitable one.
+ *
+ * @method testMoveRel
+ * @param {DOMElement} elm Element to position against.
+ * @param {Array} rels Array with relative positions.
+ * @return {String} Best suitable relative position.
+ */
+ testMoveRel: function(elm, rels) {
+ var viewPortRect = DomUtils.getViewPort();
+
+ for (var i = 0; i < rels.length; i++) {
+ var pos = calculateRelativePosition(this, elm, rels[i]);
+
+ if (this.state.get('fixed')) {
+ if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) {
+ return rels[i];
+ }
+ } else {
+ if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x &&
+ pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) {
+ return rels[i];
+ }
+ }
+ }
+
+ return rels[0];
+ },
+
+ /**
+ * Move relative to the specified element.
+ *
+ * @method moveRel
+ * @param {Element} elm Element to move relative to.
+ * @param {String} rel Relative mode. For example: br-tl.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveRel: function(elm, rel) {
+ if (typeof rel != 'string') {
+ rel = this.testMoveRel(elm, rel);
+ }
+
+ var pos = calculateRelativePosition(this, elm, rel);
+ return this.moveTo(pos.x, pos.y);
+ },
+
+ /**
+ * Move by a relative x, y values.
+ *
+ * @method moveBy
+ * @param {Number} dx Relative x position.
+ * @param {Number} dy Relative y position.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveBy: function(dx, dy) {
+ var self = this, rect = self.layoutRect();
+
+ self.moveTo(rect.x + dx, rect.y + dy);
+
+ return self;
+ },
+
+ /**
+ * Move to absolute position.
+ *
+ * @method moveTo
+ * @param {Number} x Absolute x position.
+ * @param {Number} y Absolute y position.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ moveTo: function(x, y) {
+ var self = this;
+
+ // TODO: Move this to some global class
+ function constrain(value, max, size) {
+ if (value < 0) {
+ return 0;
+ }
+
+ if (value + size > max) {
+ value = max - size;
+ return value < 0 ? 0 : value;
+ }
+
+ return value;
+ }
+
+ if (self.settings.constrainToViewport) {
+ var viewPortRect = DomUtils.getViewPort(window);
+ var layoutRect = self.layoutRect();
+
+ x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
+ y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
+ }
+
+ if (self.state.get('rendered')) {
+ self.layoutRect({x: x, y: y}).repaint();
+ } else {
+ self.settings.x = x;
+ self.settings.y = y;
+ }
+
+ self.fire('move', {x: x, y: y});
+
+ return self;
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Resizable.js
+
+/**
+ * Resizable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Resizable mixin. Enables controls to be resized.
+ *
+ * @mixin tinymce.ui.Resizable
+ */
+define("tinymce/ui/Resizable", [
+ "tinymce/ui/DomUtils"
+], function(DomUtils) {
+ "use strict";
+
+ return {
+ /**
+ * Resizes the control to contents.
+ *
+ * @method resizeToContent
+ */
+ resizeToContent: function() {
+ this._layoutRect.autoResize = true;
+ this._lastRect = null;
+ this.reflow();
+ },
+
+ /**
+ * Resizes the control to a specific width/height.
+ *
+ * @method resizeTo
+ * @param {Number} w Control width.
+ * @param {Number} h Control height.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ resizeTo: function(w, h) {
+ // TODO: Fix hack
+ if (w <= 1 || h <= 1) {
+ var rect = DomUtils.getWindowSize();
+
+ w = w <= 1 ? w * rect.w : w;
+ h = h <= 1 ? h * rect.h : h;
+ }
+
+ this._layoutRect.autoResize = false;
+ return this.layoutRect({minW: w, minH: h, w: w, h: h}).reflow();
+ },
+
+ /**
+ * Resizes the control to a specific relative width/height.
+ *
+ * @method resizeBy
+ * @param {Number} dw Relative control width.
+ * @param {Number} dh Relative control height.
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ resizeBy: function(dw, dh) {
+ var self = this, rect = self.layoutRect();
+
+ return self.resizeTo(rect.w + dw, rect.h + dh);
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/ui/FloatPanel.js
+
+/**
+ * FloatPanel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a floating panel.
+ *
+ * @-x-less FloatPanel.less
+ * @class tinymce.ui.FloatPanel
+ * @extends tinymce.ui.Panel
+ * @mixes tinymce.ui.Movable
+ * @mixes tinymce.ui.Resizable
+ */
+define("tinymce/ui/FloatPanel", [
+ "tinymce/ui/Panel",
+ "tinymce/ui/Movable",
+ "tinymce/ui/Resizable",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/util/Delay"
+], function(Panel, Movable, Resizable, DomUtils, $, Delay) {
+ "use strict";
+
+ var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = [];
+ var zOrder = [], hasModal;
+
+ function isChildOf(ctrl, parent) {
+ while (ctrl) {
+ if (ctrl == parent) {
+ return true;
+ }
+
+ ctrl = ctrl.parent();
+ }
+ }
+
+ function skipOrHidePanels(e) {
+ // Hide any float panel when a click/focus out is out side that float panel and the
+ // float panels direct parent for example a click on a menu button
+ var i = visiblePanels.length;
+
+ while (i--) {
+ var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target);
+
+ if (panel.settings.autohide) {
+ if (clickCtrl) {
+ if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
+ continue;
+ }
+ }
+
+ e = panel.fire('autohide', {target: e.target});
+ if (!e.isDefaultPrevented()) {
+ panel.hide();
+ }
+ }
+ }
+ }
+
+ function bindDocumentClickHandler() {
+
+ if (!documentClickHandler) {
+ documentClickHandler = function(e) {
+ // Gecko fires click event and in the wrong order on Mac so lets normalize
+ if (e.button == 2) {
+ return;
+ }
+
+ skipOrHidePanels(e);
+ };
+
+ $(document).on('click touchstart', documentClickHandler);
+ }
+ }
+
+ function bindDocumentScrollHandler() {
+ if (!documentScrollHandler) {
+ documentScrollHandler = function() {
+ var i;
+
+ i = visiblePanels.length;
+ while (i--) {
+ repositionPanel(visiblePanels[i]);
+ }
+ };
+
+ $(window).on('scroll', documentScrollHandler);
+ }
+ }
+
+ function bindWindowResizeHandler() {
+ if (!windowResizeHandler) {
+ var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight;
+
+ windowResizeHandler = function() {
+ // Workaround for #7065 IE 7 fires resize events event though the window wasn't resized
+ if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) {
+ clientWidth = docElm.clientWidth;
+ clientHeight = docElm.clientHeight;
+ FloatPanel.hideAll();
+ }
+ };
+
+ $(window).on('resize', windowResizeHandler);
+ }
+ }
+
+ /**
+ * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
+ * also reposition all child panels of the current panel.
+ */
+ function repositionPanel(panel) {
+ var scrollY = DomUtils.getViewPort().y;
+
+ function toggleFixedChildPanels(fixed, deltaY) {
+ var parent;
+
+ for (var i = 0; i < visiblePanels.length; i++) {
+ if (visiblePanels[i] != panel) {
+ parent = visiblePanels[i].parent();
+
+ while (parent && (parent = parent.parent())) {
+ if (parent == panel) {
+ visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
+ }
+ }
+ }
+ }
+ }
+
+ if (panel.settings.autofix) {
+ if (!panel.state.get('fixed')) {
+ panel._autoFixY = panel.layoutRect().y;
+
+ if (panel._autoFixY < scrollY) {
+ panel.fixed(true).layoutRect({y: 0}).repaint();
+ toggleFixedChildPanels(true, scrollY - panel._autoFixY);
+ }
+ } else {
+ if (panel._autoFixY > scrollY) {
+ panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint();
+ toggleFixedChildPanels(false, panel._autoFixY - scrollY);
+ }
+ }
+ }
+ }
+
+ function addRemove(add, ctrl) {
+ var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
+
+ if (add) {
+ zOrder.push(ctrl);
+ } else {
+ i = zOrder.length;
+
+ while (i--) {
+ if (zOrder[i] === ctrl) {
+ zOrder.splice(i, 1);
+ }
+ }
+ }
+
+ if (zOrder.length) {
+ for (i = 0; i < zOrder.length; i++) {
+ if (zOrder[i].modal) {
+ zIndex++;
+ topModal = zOrder[i];
+ }
+
+ zOrder[i].getEl().style.zIndex = zIndex;
+ zOrder[i].zIndex = zIndex;
+ zIndex++;
+ }
+ }
+
+ var modalBlockEl = $('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0];
+
+ if (topModal) {
+ $(modalBlockEl).css('z-index', topModal.zIndex - 1);
+ } else if (modalBlockEl) {
+ modalBlockEl.parentNode.removeChild(modalBlockEl);
+ hasModal = false;
+ }
+
+ FloatPanel.currentZIndex = zIndex;
+ }
+
+ var FloatPanel = Panel.extend({
+ Mixins: [Movable, Resizable],
+
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} autohide Automatically hide the panel.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self._eventsRoot = self;
+
+ self.classes.add('floatpanel');
+
+ // Hide floatpanes on click out side the root button
+ if (settings.autohide) {
+ bindDocumentClickHandler();
+ bindWindowResizeHandler();
+ visiblePanels.push(self);
+ }
+
+ if (settings.autofix) {
+ bindDocumentScrollHandler();
+
+ self.on('move', function() {
+ repositionPanel(this);
+ });
+ }
+
+ self.on('postrender show', function(e) {
+ if (e.control == self) {
+ var $modalBlockEl, prefix = self.classPrefix;
+
+ if (self.modal && !hasModal) {
+ $modalBlockEl = $('#' + prefix + 'modal-block', self.getContainerElm());
+ if (!$modalBlockEl[0]) {
+ $modalBlockEl = $(
+ '
'
+ ).appendTo(self.getContainerElm());
+ }
+
+ Delay.setTimeout(function() {
+ $modalBlockEl.addClass(prefix + 'in');
+ $(self.getEl()).addClass(prefix + 'in');
+ });
+
+ hasModal = true;
+ }
+
+ addRemove(true, self);
+ }
+ });
+
+ self.on('show', function() {
+ self.parents().each(function(ctrl) {
+ if (ctrl.state.get('fixed')) {
+ self.fixed(true);
+ return false;
+ }
+ });
+ });
+
+ if (settings.popover) {
+ self._preBodyHtml = '
';
+ self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start');
+ }
+
+ self.aria('label', settings.ariaLabel);
+ self.aria('labelledby', self._id);
+ self.aria('describedby', self.describedBy || self._id + '-none');
+ },
+
+ fixed: function(state) {
+ var self = this;
+
+ if (self.state.get('fixed') != state) {
+ if (self.state.get('rendered')) {
+ var viewport = DomUtils.getViewPort();
+
+ if (state) {
+ self.layoutRect().y -= viewport.y;
+ } else {
+ self.layoutRect().y += viewport.y;
+ }
+ }
+
+ self.classes.toggle('fixed', state);
+ self.state.set('fixed', state);
+ }
+
+ return self;
+ },
+
+ /**
+ * Shows the current float panel.
+ *
+ * @method show
+ * @return {tinymce.ui.FloatPanel} Current floatpanel instance.
+ */
+ show: function() {
+ var self = this, i, state = self._super();
+
+ i = visiblePanels.length;
+ while (i--) {
+ if (visiblePanels[i] === self) {
+ break;
+ }
+ }
+
+ if (i === -1) {
+ visiblePanels.push(self);
+ }
+
+ return state;
+ },
+
+ /**
+ * Hides the current float panel.
+ *
+ * @method hide
+ * @return {tinymce.ui.FloatPanel} Current floatpanel instance.
+ */
+ hide: function() {
+ removeVisiblePanel(this);
+ addRemove(false, this);
+
+ return this._super();
+ },
+
+ /**
+ * Hide all visible float panels with he autohide setting enabled. This is for
+ * manually hiding floating menus or panels.
+ *
+ * @method hideAll
+ */
+ hideAll: function() {
+ FloatPanel.hideAll();
+ },
+
+ /**
+ * Closes the float panel. This will remove the float panel from page and fire the close event.
+ *
+ * @method close
+ */
+ close: function() {
+ var self = this;
+
+ if (!self.fire('close').isDefaultPrevented()) {
+ self.remove();
+ addRemove(false, self);
+ }
+
+ return self;
+ },
+
+ /**
+ * Removes the float panel from page.
+ *
+ * @method remove
+ */
+ remove: function() {
+ removeVisiblePanel(this);
+ this._super();
+ },
+
+ postRender: function() {
+ var self = this;
+
+ if (self.settings.bodyRole) {
+ this.getEl('body').setAttribute('role', self.settings.bodyRole);
+ }
+
+ return self._super();
+ }
+ });
+
+ /**
+ * Hide all visible float panels with he autohide setting enabled. This is for
+ * manually hiding floating menus or panels.
+ *
+ * @static
+ * @method hideAll
+ */
+ FloatPanel.hideAll = function() {
+ var i = visiblePanels.length;
+
+ while (i--) {
+ var panel = visiblePanels[i];
+
+ if (panel && panel.settings.autohide) {
+ panel.hide();
+ visiblePanels.splice(i, 1);
+ }
+ }
+ };
+
+ function removeVisiblePanel(panel) {
+ var i;
+
+ i = visiblePanels.length;
+ while (i--) {
+ if (visiblePanels[i] === panel) {
+ visiblePanels.splice(i, 1);
+ }
+ }
+
+ i = zOrder.length;
+ while (i--) {
+ if (zOrder[i] === panel) {
+ zOrder.splice(i, 1);
+ }
+ }
+ }
+
+ return FloatPanel;
+});
+
+// Included from: js/tinymce/classes/ui/Window.js
+
+/**
+ * Window.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new window.
+ *
+ * @-x-less Window.less
+ * @class tinymce.ui.Window
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/Window", [
+ "tinymce/ui/FloatPanel",
+ "tinymce/ui/Panel",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/BoxUtils",
+ "tinymce/Env",
+ "tinymce/util/Delay"
+], function(FloatPanel, Panel, DomUtils, $, DragHelper, BoxUtils, Env, Delay) {
+ "use strict";
+
+ var windows = [], oldMetaValue = '';
+
+ function toggleFullScreenState(state) {
+ var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0',
+ viewport = $("meta[name=viewport]")[0],
+ contentValue;
+
+ if (Env.overrideViewPort === false) {
+ return;
+ }
+
+ if (!viewport) {
+ viewport = document.createElement('meta');
+ viewport.setAttribute('name', 'viewport');
+ document.getElementsByTagName('head')[0].appendChild(viewport);
+ }
+
+ contentValue = viewport.getAttribute('content');
+ if (contentValue && typeof oldMetaValue != 'undefined') {
+ oldMetaValue = contentValue;
+ }
+
+ viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue);
+ }
+
+ function toggleBodyFullScreenClasses(classPrefix) {
+ for (var i = 0; i < windows.length; i++) {
+ if (windows[i]._fullscreen) {
+ return;
+ }
+ }
+
+ $([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen');
+ }
+
+ function handleWindowResize() {
+ if (!Env.desktop) {
+ var lastSize = {
+ w: window.innerWidth,
+ h: window.innerHeight
+ };
+
+ Delay.setInterval(function() {
+ var w = window.innerWidth,
+ h = window.innerHeight;
+
+ if (lastSize.w != w || lastSize.h != h) {
+ lastSize = {
+ w: w,
+ h: h
+ };
+
+ $(window).trigger('resize');
+ }
+ }, 100);
+ }
+
+ function reposition() {
+ var i, rect = DomUtils.getWindowSize(), layoutRect;
+
+ for (i = 0; i < windows.length; i++) {
+ layoutRect = windows[i].layoutRect();
+
+ windows[i].moveTo(
+ windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2),
+ windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2)
+ );
+ }
+ }
+
+ $(window).on('resize', reposition);
+ }
+
+ var Window = FloatPanel.extend({
+ modal: true,
+
+ Defaults: {
+ border: 1,
+ layout: 'flex',
+ containerCls: 'panel',
+ role: 'dialog',
+ callbacks: {
+ submit: function() {
+ this.fire('submit', {data: this.toJSON()});
+ },
+
+ close: function() {
+ this.close();
+ }
+ }
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (self.isRtl()) {
+ self.classes.add('rtl');
+ }
+
+ self.classes.add('window');
+ self.bodyClasses.add('window-body');
+ self.state.set('fixed', true);
+
+ // Create statusbar
+ if (settings.buttons) {
+ self.statusbar = new Panel({
+ layout: 'flex',
+ border: '1 0 0 0',
+ spacing: 3,
+ padding: 10,
+ align: 'center',
+ pack: self.isRtl() ? 'start' : 'end',
+ defaults: {
+ type: 'button'
+ },
+ items: settings.buttons
+ });
+
+ self.statusbar.classes.add('foot');
+ self.statusbar.parent(self);
+ }
+
+ self.on('click', function(e) {
+ var closeClass = self.classPrefix + 'close';
+
+ if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) {
+ self.close();
+ }
+ });
+
+ self.on('cancel', function() {
+ self.close();
+ });
+
+ self.aria('describedby', self.describedBy || self._id + '-none');
+ self.aria('label', settings.title);
+ self._fullscreen = false;
+ },
+
+ /**
+ * Recalculates the positions of the controls in the current container.
+ * This is invoked by the reflow method and shouldn't be called directly.
+ *
+ * @method recalc
+ */
+ recalc: function() {
+ var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc;
+
+ if (self._fullscreen) {
+ self.layoutRect(DomUtils.getWindowSize());
+ self.layoutRect().contentH = self.layoutRect().innerH;
+ }
+
+ self._super();
+
+ layoutRect = self.layoutRect();
+
+ // Resize window based on title width
+ if (self.settings.title && !self._fullscreen) {
+ width = layoutRect.headerW;
+ if (width > layoutRect.w) {
+ x = layoutRect.x - Math.max(0, width / 2);
+ self.layoutRect({w: width, x: x});
+ needsRecalc = true;
+ }
+ }
+
+ // Resize window based on statusbar width
+ if (statusbar) {
+ statusbar.layoutRect({w: self.layoutRect().innerW}).recalc();
+
+ width = statusbar.layoutRect().minW + layoutRect.deltaW;
+ if (width > layoutRect.w) {
+ x = layoutRect.x - Math.max(0, width - layoutRect.w);
+ self.layoutRect({w: width, x: x});
+ needsRecalc = true;
+ }
+ }
+
+ // Recalc body and disable auto resize
+ if (needsRecalc) {
+ self.recalc();
+ }
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, layoutRect = self._super(), deltaH = 0, headEl;
+
+ // Reserve vertical space for title
+ if (self.settings.title && !self._fullscreen) {
+ headEl = self.getEl('head');
+
+ var size = DomUtils.getSize(headEl);
+
+ layoutRect.headerW = size.width;
+ layoutRect.headerH = size.height;
+
+ deltaH += layoutRect.headerH;
+ }
+
+ // Reserve vertical space for statusbar
+ if (self.statusbar) {
+ deltaH += self.statusbar.layoutRect().h;
+ }
+
+ layoutRect.deltaH += deltaH;
+ layoutRect.minH += deltaH;
+ //layoutRect.innerH -= deltaH;
+ layoutRect.h += deltaH;
+
+ var rect = DomUtils.getWindowSize();
+
+ layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2);
+ layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2);
+
+ return layoutRect;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix;
+ var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html;
+
+ self.preRender();
+ layout.preRender(self);
+
+ if (settings.title) {
+ headerHtml = (
+ '
' +
+ '
' + self.encode(settings.title) + '
' +
+ '
' +
+ '
' +
+ '
'
+ );
+ }
+
+ if (settings.url) {
+ html = '
';
+ }
+
+ if (typeof html == "undefined") {
+ html = layout.renderHtml(self);
+ }
+
+ if (self.statusbar) {
+ footerHtml = self.statusbar.renderHtml();
+ }
+
+ return (
+ '
' +
+ '
' +
+ headerHtml +
+ '
' +
+ html +
+ '
' +
+ footerHtml +
+ '
' +
+ '
'
+ );
+ },
+
+ /**
+ * Switches the window fullscreen mode.
+ *
+ * @method fullscreen
+ * @param {Boolean} state True/false state.
+ * @return {tinymce.ui.Window} Current window instance.
+ */
+ fullscreen: function(state) {
+ var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect;
+
+ if (state != self._fullscreen) {
+ $(window).on('resize', function() {
+ var time;
+
+ if (self._fullscreen) {
+ // Time the layout time if it's to slow use a timeout to not hog the CPU
+ if (!slowRendering) {
+ time = new Date().getTime();
+
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+
+ if ((new Date().getTime()) - time > 50) {
+ slowRendering = true;
+ }
+ } else {
+ if (!self._timer) {
+ self._timer = Delay.setTimeout(function() {
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+
+ self._timer = 0;
+ }, 50);
+ }
+ }
+ }
+ });
+
+ layoutRect = self.layoutRect();
+ self._fullscreen = state;
+
+ if (!state) {
+ self.borderBox = BoxUtils.parseBox(self.settings.border);
+ self.getEl('head').style.display = '';
+ layoutRect.deltaH += layoutRect.headerH;
+ $([documentElement, document.body]).removeClass(prefix + 'fullscreen');
+ self.classes.remove('fullscreen');
+ self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h);
+ } else {
+ self._initial = {x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h};
+
+ self.borderBox = BoxUtils.parseBox('0');
+ self.getEl('head').style.display = 'none';
+ layoutRect.deltaH -= layoutRect.headerH + 2;
+ $([documentElement, document.body]).addClass(prefix + 'fullscreen');
+ self.classes.add('fullscreen');
+
+ var rect = DomUtils.getWindowSize();
+ self.moveTo(0, 0).resizeTo(rect.w, rect.h);
+ }
+ }
+
+ return self.reflow();
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, startPos;
+
+ setTimeout(function() {
+ self.classes.add('in');
+ self.fire('open');
+ }, 0);
+
+ self._super();
+
+ if (self.statusbar) {
+ self.statusbar.postRender();
+ }
+
+ self.focus();
+
+ this.dragHelper = new DragHelper(self._id + '-dragh', {
+ start: function() {
+ startPos = {
+ x: self.layoutRect().x,
+ y: self.layoutRect().y
+ };
+ },
+
+ drag: function(e) {
+ self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY);
+ }
+ });
+
+ self.on('submit', function(e) {
+ if (!e.isDefaultPrevented()) {
+ self.close();
+ }
+ });
+
+ windows.push(self);
+ toggleFullScreenState(true);
+ },
+
+ /**
+ * Fires a submit event with the serialized form.
+ *
+ * @method submit
+ * @return {Object} Event arguments object.
+ */
+ submit: function() {
+ return this.fire('submit', {data: this.toJSON()});
+ },
+
+ /**
+ * Removes the current control from DOM and from UI collections.
+ *
+ * @method remove
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ remove: function() {
+ var self = this, i;
+
+ self.dragHelper.destroy();
+ self._super();
+
+ if (self.statusbar) {
+ this.statusbar.remove();
+ }
+
+ i = windows.length;
+ while (i--) {
+ if (windows[i] === self) {
+ windows.splice(i, 1);
+ }
+ }
+
+ toggleFullScreenState(windows.length > 0);
+ toggleBodyFullScreenClasses(self.classPrefix);
+ },
+
+ /**
+ * Returns the contentWindow object of the iframe if it exists.
+ *
+ * @method getContentWindow
+ * @return {Window} window object or null.
+ */
+ getContentWindow: function() {
+ var ifr = this.getEl().getElementsByTagName('iframe')[0];
+ return ifr ? ifr.contentWindow : null;
+ }
+ });
+
+ handleWindowResize();
+
+ return Window;
+});
+
+// Included from: js/tinymce/classes/ui/MessageBox.js
+
+/**
+ * MessageBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to create MessageBoxes like alerts/confirms etc.
+ *
+ * @class tinymce.ui.MessageBox
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/MessageBox", [
+ "tinymce/ui/Window"
+], function(Window) {
+ "use strict";
+
+ var MessageBox = Window.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ settings = {
+ border: 1,
+ padding: 20,
+ layout: 'flex',
+ pack: "center",
+ align: "center",
+ containerCls: 'panel',
+ autoScroll: true,
+ buttons: {type: "button", text: "Ok", action: "ok"},
+ items: {
+ type: "label",
+ multiline: true,
+ maxWidth: 500,
+ maxHeight: 200
+ }
+ };
+
+ this._super(settings);
+ },
+
+ Statics: {
+ /**
+ * Ok buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} OK
+ */
+ OK: 1,
+
+ /**
+ * Ok/cancel buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} OK_CANCEL
+ */
+ OK_CANCEL: 2,
+
+ /**
+ * yes/no buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} YES_NO
+ */
+ YES_NO: 3,
+
+ /**
+ * yes/no/cancel buttons constant.
+ *
+ * @static
+ * @final
+ * @field {Number} YES_NO_CANCEL
+ */
+ YES_NO_CANCEL: 4,
+
+ /**
+ * Constructs a new message box and renders it to the body element.
+ *
+ * @static
+ * @method msgBox
+ * @param {Object} settings Name/value object with settings.
+ */
+ msgBox: function(settings) {
+ var buttons, callback = settings.callback || function() {};
+
+ function createButton(text, status, primary) {
+ return {
+ type: "button",
+ text: text,
+ subtype: primary ? 'primary' : '',
+ onClick: function(e) {
+ e.control.parents()[1].close();
+ callback(status);
+ }
+ };
+ }
+
+ switch (settings.buttons) {
+ case MessageBox.OK_CANCEL:
+ buttons = [
+ createButton('Ok', true, true),
+ createButton('Cancel', false)
+ ];
+ break;
+
+ case MessageBox.YES_NO:
+ case MessageBox.YES_NO_CANCEL:
+ buttons = [
+ createButton('Yes', 1, true),
+ createButton('No', 0)
+ ];
+
+ if (settings.buttons == MessageBox.YES_NO_CANCEL) {
+ buttons.push(createButton('Cancel', -1));
+ }
+ break;
+
+ default:
+ buttons = [
+ createButton('Ok', true, true)
+ ];
+ break;
+ }
+
+ return new Window({
+ padding: 20,
+ x: settings.x,
+ y: settings.y,
+ minWidth: 300,
+ minHeight: 100,
+ layout: "flex",
+ pack: "center",
+ align: "center",
+ buttons: buttons,
+ title: settings.title,
+ role: 'alertdialog',
+ items: {
+ type: "label",
+ multiline: true,
+ maxWidth: 500,
+ maxHeight: 200,
+ text: settings.text
+ },
+ onPostRender: function() {
+ this.aria('describedby', this.items()[0]._id);
+ },
+ onClose: settings.onClose,
+ onCancel: function() {
+ callback(false);
+ }
+ }).renderTo(document.body).reflow();
+ },
+
+ /**
+ * Creates a new alert dialog.
+ *
+ * @method alert
+ * @param {Object} settings Settings for the alert dialog.
+ * @param {function} [callback] Callback to execute when the user makes a choice.
+ */
+ alert: function(settings, callback) {
+ if (typeof settings == "string") {
+ settings = {text: settings};
+ }
+
+ settings.callback = callback;
+ return MessageBox.msgBox(settings);
+ },
+
+ /**
+ * Creates a new confirm dialog.
+ *
+ * @method confirm
+ * @param {Object} settings Settings for the confirm dialog.
+ * @param {function} [callback] Callback to execute when the user makes a choice.
+ */
+ confirm: function(settings, callback) {
+ if (typeof settings == "string") {
+ settings = {text: settings};
+ }
+
+ settings.callback = callback;
+ settings.buttons = MessageBox.OK_CANCEL;
+
+ return MessageBox.msgBox(settings);
+ }
+ }
+ });
+
+ return MessageBox;
+});
+
+// Included from: js/tinymce/classes/WindowManager.js
+
+/**
+ * WindowManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
+ *
+ * @class tinymce.WindowManager
+ * @example
+ * // Opens a new dialog with the file.htm file and the size 320x240
+ * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
+ * tinymce.activeEditor.windowManager.open({
+ * url: 'file.htm',
+ * width: 320,
+ * height: 240
+ * }, {
+ * custom_param: 1
+ * });
+ *
+ * // Displays an alert box using the active editors window manager instance
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ *
+ * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
+ * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
+ * if (s)
+ * tinymce.activeEditor.windowManager.alert("Ok");
+ * else
+ * tinymce.activeEditor.windowManager.alert("Cancel");
+ * });
+ */
+define("tinymce/WindowManager", [
+ "tinymce/ui/Window",
+ "tinymce/ui/MessageBox"
+], function(Window, MessageBox) {
+ return function(editor) {
+ var self = this, windows = [];
+
+ function getTopMostWindow() {
+ if (windows.length) {
+ return windows[windows.length - 1];
+ }
+ }
+
+ function fireOpenEvent(win) {
+ editor.fire('OpenWindow', {
+ win: win
+ });
+ }
+
+ function fireCloseEvent(win) {
+ editor.fire('CloseWindow', {
+ win: win
+ });
+ }
+
+ self.windows = windows;
+
+ editor.on('remove', function() {
+ var i = windows.length;
+
+ while (i--) {
+ windows[i].close();
+ }
+ });
+
+ /**
+ * Opens a new window.
+ *
+ * @method open
+ * @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
+ * @param {Object} params Options like title, file, width, height etc.
+ * @option {String} title Window title.
+ * @option {String} file URL of the file to open in the window.
+ * @option {Number} width Width in pixels.
+ * @option {Number} height Height in pixels.
+ * @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
+ * larger than the popup size specified).
+ */
+ self.open = function(args, params) {
+ var win;
+
+ editor.editorManager.setActive(editor);
+
+ args.title = args.title || ' ';
+
+ // Handle URL
+ args.url = args.url || args.file; // Legacy
+ if (args.url) {
+ args.width = parseInt(args.width || 320, 10);
+ args.height = parseInt(args.height || 240, 10);
+ }
+
+ // Handle body
+ if (args.body) {
+ args.items = {
+ defaults: args.defaults,
+ type: args.bodyType || 'form',
+ items: args.body,
+ data: args.data,
+ callbacks: args.commands
+ };
+ }
+
+ if (!args.url && !args.buttons) {
+ args.buttons = [
+ {text: 'Ok', subtype: 'primary', onclick: function() {
+ win.find('form')[0].submit();
+ }},
+
+ {text: 'Cancel', onclick: function() {
+ win.close();
+ }}
+ ];
+ }
+
+ win = new Window(args);
+ windows.push(win);
+
+ win.on('close', function() {
+ var i = windows.length;
+
+ while (i--) {
+ if (windows[i] === win) {
+ windows.splice(i, 1);
+ }
+ }
+
+ if (!windows.length) {
+ editor.focus();
+ }
+
+ fireCloseEvent(win);
+ });
+
+ // Handle data
+ if (args.data) {
+ win.on('postRender', function() {
+ this.find('*').each(function(ctrl) {
+ var name = ctrl.name();
+
+ if (name in args.data) {
+ ctrl.value(args.data[name]);
+ }
+ });
+ });
+ }
+
+ // store args and parameters
+ win.features = args || {};
+ win.params = params || {};
+
+ // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
+ if (windows.length === 1) {
+ editor.nodeChanged();
+ }
+
+ win = win.renderTo().reflow();
+
+ fireOpenEvent(win);
+
+ return win;
+ };
+
+ /**
+ * Creates a alert dialog. Please don't use the blocking behavior of this
+ * native version use the callback method instead then it can be extended.
+ *
+ * @method alert
+ * @param {String} message Text to display in the new alert dialog.
+ * @param {function} callback Callback function to be executed after the user has selected ok.
+ * @param {Object} scope Optional scope to execute the callback in.
+ * @example
+ * // Displays an alert box using the active editors window manager instance
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ */
+ self.alert = function(message, callback, scope) {
+ var win;
+
+ win = MessageBox.alert(message, function() {
+ if (callback) {
+ callback.call(scope || this);
+ } else {
+ editor.focus();
+ }
+ });
+
+ win.on('close', function() {
+ fireCloseEvent(win);
+ });
+
+ fireOpenEvent(win);
+ };
+
+ /**
+ * Creates a confirm dialog. Please don't use the blocking behavior of this
+ * native version use the callback method instead then it can be extended.
+ *
+ * @method confirm
+ * @param {String} message Text to display in the new confirm dialog.
+ * @param {function} callback Callback function to be executed after the user has selected ok or cancel.
+ * @param {Object} scope Optional scope to execute the callback in.
+ * @example
+ * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
+ * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
+ * if (s)
+ * tinymce.activeEditor.windowManager.alert("Ok");
+ * else
+ * tinymce.activeEditor.windowManager.alert("Cancel");
+ * });
+ */
+ self.confirm = function(message, callback, scope) {
+ var win;
+
+ win = MessageBox.confirm(message, function(state) {
+ callback.call(scope || this, state);
+ });
+
+ win.on('close', function() {
+ fireCloseEvent(win);
+ });
+
+ fireOpenEvent(win);
+ };
+
+ /**
+ * Closes the top most window.
+ *
+ * @method close
+ */
+ self.close = function() {
+ if (getTopMostWindow()) {
+ getTopMostWindow().close();
+ }
+ };
+
+ /**
+ * Returns the params of the last window open call. This can be used in iframe based
+ * dialog to get params passed from the tinymce plugin.
+ *
+ * @example
+ * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
+ *
+ * @method getParams
+ * @return {Object} Name/value object with parameters passed from windowManager.open call.
+ */
+ self.getParams = function() {
+ return getTopMostWindow() ? getTopMostWindow().params : null;
+ };
+
+ /**
+ * Sets the params of the last opened window.
+ *
+ * @method setParams
+ * @param {Object} params Params object to set for the last opened window.
+ */
+ self.setParams = function(params) {
+ if (getTopMostWindow()) {
+ getTopMostWindow().params = params;
+ }
+ };
+
+ /**
+ * Returns the currently opened window objects.
+ *
+ * @method getWindows
+ * @return {Array} Array of the currently opened windows.
+ */
+ self.getWindows = function() {
+ return windows;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Tooltip.js
+
+/**
+ * Tooltip.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a tooltip instance.
+ *
+ * @-x-less ToolTip.less
+ * @class tinymce.ui.ToolTip
+ * @extends tinymce.ui.Control
+ * @mixes tinymce.ui.Movable
+ */
+define("tinymce/ui/Tooltip", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Movable"
+], function(Control, Movable) {
+ return Control.extend({
+ Mixins: [Movable],
+
+ Defaults: {
+ classes: 'widget tooltip tooltip-n'
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ return (
+ '
' +
+ '
' +
+ '
' + self.encode(self.state.get('text')) + '
' +
+ '
'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl().lastChild.innerHTML = self.encode(e.value);
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+
+ style.left = rect.x + 'px';
+ style.top = rect.y + 'px';
+ style.zIndex = 0xFFFF + 0xFFFF;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Widget.js
+
+/**
+ * Widget.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Widget base class a widget is a control that has a tooltip and some basic states.
+ *
+ * @class tinymce.ui.Widget
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/Widget", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Tooltip"
+], function(Control, Tooltip) {
+ "use strict";
+
+ var tooltip;
+
+ var Widget = Control.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} tooltip Tooltip text to display when hovering.
+ * @setting {Boolean} autofocus True if the control should be focused when rendered.
+ * @setting {String} text Text to display inside widget.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ settings = self.settings;
+ self.canFocus = true;
+
+ if (settings.tooltip && Widget.tooltips !== false) {
+ self.on('mouseenter', function(e) {
+ var tooltip = self.tooltip().moveTo(-0xFFFF);
+
+ if (e.control == self) {
+ var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
+
+ tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
+ tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
+ tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
+
+ tooltip.moveRel(self.getEl(), rel);
+ } else {
+ tooltip.hide();
+ }
+ });
+
+ self.on('mouseleave mousedown click', function() {
+ self.tooltip().hide();
+ });
+ }
+
+ self.aria('label', settings.ariaLabel || settings.tooltip);
+ },
+
+ /**
+ * Returns the current tooltip instance.
+ *
+ * @method tooltip
+ * @return {tinymce.ui.Tooltip} Tooltip instance.
+ */
+ tooltip: function() {
+ if (!tooltip) {
+ tooltip = new Tooltip({type: 'tooltip'});
+ tooltip.renderTo();
+ }
+
+ return tooltip;
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, settings = self.settings;
+
+ self._super();
+
+ if (!self.parent() && (settings.width || settings.height)) {
+ self.initLayoutRect();
+ self.repaint();
+ }
+
+ if (settings.autofocus) {
+ self.focus();
+ }
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function disable(state) {
+ self.aria('disabled', state);
+ self.classes.toggle('disabled', state);
+ }
+
+ function active(state) {
+ self.aria('pressed', state);
+ self.classes.toggle('active', state);
+ }
+
+ self.state.on('change:disabled', function(e) {
+ disable(e.value);
+ });
+
+ self.state.on('change:active', function(e) {
+ active(e.value);
+ });
+
+ if (self.state.get('disabled')) {
+ disable(true);
+ }
+
+ if (self.state.get('active')) {
+ active(true);
+ }
+
+ return self._super();
+ },
+
+ /**
+ * Removes the current control from DOM and from UI collections.
+ *
+ * @method remove
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ remove: function() {
+ this._super();
+
+ if (tooltip) {
+ tooltip.remove();
+ tooltip = null;
+ }
+ }
+ });
+
+ return Widget;
+});
+
+// Included from: js/tinymce/classes/ui/Progress.js
+
+/**
+ * Progress.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Progress control.
+ *
+ * @-x-less Progress.less
+ * @class tinymce.ui.Progress
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/Progress", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ value: 0
+ },
+
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('progress');
+
+ if (!self.settings.filter) {
+ self.settings.filter = function(value) {
+ return Math.round(value);
+ };
+ }
+ },
+
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = this.classPrefix;
+
+ return (
+ '
'
+ );
+ },
+
+ postRender: function() {
+ var self = this;
+
+ self._super();
+ self.value(self.settings.value);
+
+ return self;
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function setValue(value) {
+ value = self.settings.filter(value);
+ self.getEl().lastChild.innerHTML = value + '%';
+ self.getEl().firstChild.firstChild.style.width = value + '%';
+ }
+
+ self.state.on('change:value', function(e) {
+ setValue(e.value);
+ });
+
+ setValue(self.state.get('value'));
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Notification.js
+
+/**
+ * Notification.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a notification instance.
+ *
+ * @-x-less Notification.less
+ * @class tinymce.ui.Notification
+ * @extends tinymce.ui.Container
+ * @mixes tinymce.ui.Movable
+ */
+define("tinymce/ui/Notification", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Movable",
+ "tinymce/ui/Progress",
+ "tinymce/util/Delay"
+], function(Control, Movable, Progress, Delay) {
+ return Control.extend({
+ Mixins: [Movable],
+
+ Defaults: {
+ classes: 'widget notification'
+ },
+
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (settings.text) {
+ self.text(settings.text);
+ }
+
+ if (settings.icon) {
+ self.icon = settings.icon;
+ }
+
+ if (settings.color) {
+ self.color = settings.color;
+ }
+
+ if (settings.type) {
+ self.classes.add('notification-' + settings.type);
+ }
+
+ if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) {
+ self.closeButton = false;
+ } else {
+ self.classes.add('has-close');
+ self.closeButton = true;
+ }
+
+ if (settings.progressBar) {
+ self.progressBar = new Progress();
+ }
+
+ self.on('click', function(e) {
+ if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
+ self.close();
+ }
+ });
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = '';
+
+ if (self.icon) {
+ icon = '
';
+ }
+
+ if (self.color) {
+ notificationStyle = ' style="background-color: ' + self.color + '"';
+ }
+
+ if (self.closeButton) {
+ closeButton = '
';
+ }
+
+ if (self.progressBar) {
+ progressBar = self.progressBar.renderHtml();
+ }
+
+ return (
+ '
' +
+ icon +
+ '
' + self.state.get('text') + '
' +
+ progressBar +
+ closeButton +
+ '
'
+ );
+ },
+
+ postRender: function() {
+ var self = this;
+
+ Delay.setTimeout(function() {
+ self.$el.addClass(self.classPrefix + 'in');
+ });
+
+ return self._super();
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl().childNodes[1].innerHTML = e.value;
+ });
+ if (self.progressBar) {
+ self.progressBar.bindStates();
+ }
+ return self._super();
+ },
+
+ close: function() {
+ var self = this;
+
+ if (!self.fire('close').isDefaultPrevented()) {
+ self.remove();
+ }
+
+ return self;
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+
+ style.left = rect.x + 'px';
+ style.top = rect.y + 'px';
+ style.zIndex = 0xFFFF + 0xFFFF;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/NotificationManager.js
+
+/**
+ * NotificationManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the creation of TinyMCE's notifications.
+ *
+ * @class tinymce.notificationManager
+ * @example
+ * // Opens a new notification of type "error" with text "An error occurred."
+ * tinymce.activeEditor.notificationManager.open({
+ * text: 'An error occurred.',
+ * type: 'error'
+ * });
+ */
+define("tinymce/NotificationManager", [
+ "tinymce/ui/Notification",
+ "tinymce/util/Delay"
+], function(Notification, Delay) {
+ return function(editor) {
+ var self = this, notifications = [];
+
+ function getLastNotification() {
+ if (notifications.length) {
+ return notifications[notifications.length - 1];
+ }
+ }
+
+ self.notifications = notifications;
+
+ function resizeWindowEvent() {
+ Delay.requestAnimationFrame(function() {
+ prePositionNotifications();
+ positionNotifications();
+ });
+ }
+
+ // Since the viewport will change based on the present notifications, we need to move them all to the
+ // top left of the viewport to give an accurate size measurement so we can position them later.
+ function prePositionNotifications() {
+ for (var i = 0; i < notifications.length; i++) {
+ notifications[i].moveTo(0, 0);
+ }
+ }
+
+ function positionNotifications() {
+ if (notifications.length > 0) {
+ var firstItem = notifications.slice(0, 1)[0];
+ var container = editor.inline ? editor.getElement() : editor.getContentAreaContainer();
+ firstItem.moveRel(container, 'tc-tc');
+ if (notifications.length > 1) {
+ for (var i = 1; i < notifications.length; i++) {
+ notifications[i].moveRel(notifications[i - 1].getEl(), 'bc-tc');
+ }
+ }
+ }
+ }
+
+ editor.on('remove', function() {
+ var i = notifications.length;
+
+ while (i--) {
+ notifications[i].close();
+ }
+ });
+
+ editor.on('ResizeEditor', positionNotifications);
+ editor.on('ResizeWindow', resizeWindowEvent);
+
+ /**
+ * Opens a new notification.
+ *
+ * @method open
+ * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
+ */
+ self.open = function(args) {
+ var notif;
+
+ editor.editorManager.setActive(editor);
+
+ notif = new Notification(args);
+ notifications.push(notif);
+
+ //If we have a timeout value
+ if (args.timeout > 0) {
+ notif.timer = setTimeout(function() {
+ notif.close();
+ }, args.timeout);
+ }
+
+ notif.on('close', function() {
+ var i = notifications.length;
+
+ if (notif.timer) {
+ editor.getWin().clearTimeout(notif.timer);
+ }
+
+ while (i--) {
+ if (notifications[i] === notif) {
+ notifications.splice(i, 1);
+ }
+ }
+
+ positionNotifications();
+ });
+
+ notif.renderTo();
+
+ positionNotifications();
+
+ return notif;
+ };
+
+ /**
+ * Closes the top most notification.
+ *
+ * @method close
+ */
+ self.close = function() {
+ if (getLastNotification()) {
+ getLastNotification().close();
+ }
+ };
+
+ /**
+ * Returns the currently opened notification objects.
+ *
+ * @method getNotifications
+ * @return {Array} Array of the currently opened notifications.
+ */
+ self.getNotifications = function() {
+ return notifications;
+ };
+
+ editor.on('SkinLoaded', function() {
+ var serviceMessage = editor.settings.service_message;
+
+ if (serviceMessage) {
+ editor.notificationManager.open({
+ text: serviceMessage,
+ type: 'warning',
+ timeout: 0,
+ icon: ''
+ });
+ }
+ });
+
+ //self.positionNotifications = positionNotifications;
+ };
+});
+
+// Included from: js/tinymce/classes/dom/NodePath.js
+
+/**
+ * NodePath.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles paths of nodes within an element.
+ *
+ * @private
+ * @class tinymce.dom.NodePath
+ */
+define("tinymce/dom/NodePath", [
+ "tinymce/dom/DOMUtils"
+], function(DOMUtils) {
+ function create(rootNode, targetNode, normalized) {
+ var path = [];
+
+ for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) {
+ path.push(DOMUtils.nodeIndex(targetNode, normalized));
+ }
+
+ return path;
+ }
+
+ function resolve(rootNode, path) {
+ var i, node, children;
+
+ for (node = rootNode, i = path.length - 1; i >= 0; i--) {
+ children = node.childNodes;
+
+ if (path[i] > children.length - 1) {
+ return null;
+ }
+
+ node = children[path[i]];
+ }
+
+ return node;
+ }
+
+ return {
+ create: create,
+ resolve: resolve
+ };
+});
+
+// Included from: js/tinymce/classes/util/Quirks.js
+
+/**
+ * Quirks.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ *
+ * @ignore-file
+ */
+
+/**
+ * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes.
+ *
+ * @private
+ * @class tinymce.util.Quirks
+ */
+define("tinymce/util/Quirks", [
+ "tinymce/util/VK",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/TreeWalker",
+ "tinymce/dom/NodePath",
+ "tinymce/html/Node",
+ "tinymce/html/Entities",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Delay",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretWalker"
+], function(VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) {
+ return function(editor) {
+ var each = Tools.each, $ = editor.$;
+ var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
+ settings = editor.settings, parser = editor.parser, serializer = editor.serializer;
+ var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit;
+ var mceInternalUrlPrefix = 'data:text/mce-internal,';
+ var mceInternalDataType = isIE ? 'Text' : 'URL';
+
+ /**
+ * Executes a command with a specific state this can be to enable/disable browser editing features.
+ */
+ function setEditorCommandState(cmd, state) {
+ try {
+ editor.getDoc().execCommand(cmd, false, state);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ /**
+ * Returns current IE document mode.
+ */
+ function getDocumentMode() {
+ var documentMode = editor.getDoc().documentMode;
+
+ return documentMode ? documentMode : 6;
+ }
+
+ /**
+ * Returns true/false if the event is prevented or not.
+ *
+ * @private
+ * @param {Event} e Event object.
+ * @return {Boolean} true/false if the event is prevented or not.
+ */
+ function isDefaultPrevented(e) {
+ return e.isDefaultPrevented();
+ }
+
+ /**
+ * Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url.
+ * This is to workaround the inability to set custom contentType on IE and Safari.
+ * The editor's selected content is encoded into this url so drag and drop between editors will work.
+ *
+ * @private
+ * @param {DragEvent} e Event object
+ */
+ function setMceInternalContent(e) {
+ var selectionHtml, internalContent;
+
+ if (e.dataTransfer) {
+ if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') {
+ selection.select(e.target);
+ }
+
+ selectionHtml = editor.selection.getContent();
+
+ // Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text
+ if (selectionHtml.length > 0) {
+ internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml);
+ e.dataTransfer.setData(mceInternalDataType, internalContent);
+ }
+ }
+ }
+
+ /**
+ * Gets content of special data:text/mce-internal url on the event's dataTransfer object.
+ * This is to workaround the inability to set custom contentType on IE and Safari.
+ * The editor's selected content is encoded into this url so drag and drop between editors will work.
+ *
+ * @private
+ * @param {DragEvent} e Event object
+ * @returns {String} mce-internal content
+ */
+ function getMceInternalContent(e) {
+ var internalContent;
+
+ if (e.dataTransfer) {
+ internalContent = e.dataTransfer.getData(mceInternalDataType);
+
+ if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) {
+ internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(',');
+
+ return {
+ id: unescape(internalContent[0]),
+ html: unescape(internalContent[1])
+ };
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Inserts contents using the paste clipboard command if it's available if it isn't it will fallback
+ * to the core command.
+ *
+ * @private
+ * @param {String} content Content to insert at selection.
+ */
+ function insertClipboardContents(content) {
+ if (editor.queryCommandSupported('mceInsertClipboardContent')) {
+ editor.execCommand('mceInsertClipboardContent', false, {content: content});
+ } else {
+ editor.execCommand('mceInsertContent', false, content);
+ }
+ }
+
+ /**
+ * Fixes a WebKit bug when deleting contents using backspace or delete key.
+ * WebKit will produce a span element if you delete across two block elements.
+ *
+ * Example:
+ *
a
|b
+ *
+ * Will produce this on backspace:
+ *
ab
+ *
+ * This fixes the backspace to produce:
+ * a|b
+ *
+ * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
+ *
+ * This fixes the following delete scenarios:
+ * 1. Delete by pressing backspace key.
+ * 2. Delete by pressing delete key.
+ * 3. Delete by pressing backspace key with ctrl/cmd (Word delete).
+ * 4. Delete by pressing delete key with ctrl/cmd (Word delete).
+ * 5. Delete by drag/dropping contents inside the editor.
+ * 6. Delete by using Cut Ctrl+X/Cmd+X.
+ * 7. Delete by selecting contents and writing a character.
+ *
+ * This code is a ugly hack since writing full custom delete logic for just this bug
+ * fix seemed like a huge task. I hope we can remove this before the year 2030.
+ */
+ function cleanupStylesWhenDeleting() {
+ var doc = editor.getDoc(), dom = editor.dom, selection = editor.selection;
+ var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng;
+
+ // Add mini polyfill for older WebKits
+ // TODO: Remove this when old Safari versions gets updated
+ if (!MutationObserver) {
+ olderWebKit = true;
+
+ MutationObserver = function() {
+ var records = [], target;
+
+ function nodeInsert(e) {
+ var target = e.relatedNode || e.target;
+ records.push({target: target, addedNodes: [target]});
+ }
+
+ function attrModified(e) {
+ var target = e.relatedNode || e.target;
+ records.push({target: target, attributeName: e.attrName});
+ }
+
+ this.observe = function(node) {
+ target = node;
+ target.addEventListener('DOMSubtreeModified', nodeInsert, false);
+ target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
+ target.addEventListener('DOMNodeInserted', nodeInsert, false);
+ target.addEventListener('DOMAttrModified', attrModified, false);
+ };
+
+ this.disconnect = function() {
+ target.removeEventListener('DOMSubtreeModified', nodeInsert, false);
+ target.removeEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
+ target.removeEventListener('DOMNodeInserted', nodeInsert, false);
+ target.removeEventListener('DOMAttrModified', attrModified, false);
+ };
+
+ this.takeRecords = function() {
+ return records;
+ };
+ };
+ }
+
+ function isTrailingBr(node) {
+ var blockElements = dom.schema.getBlockElements(), rootNode = editor.getBody();
+
+ if (node.nodeName != 'BR') {
+ return false;
+ }
+
+ for (; node != rootNode && !blockElements[node.nodeName]; node = node.parentNode) {
+ if (node.nextSibling) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function isSiblingsIgnoreWhiteSpace(node1, node2) {
+ var node;
+
+ for (node = node1.nextSibling; node && node != node2; node = node.nextSibling) {
+ if (node.nodeType == 3 && $.trim(node.data).length === 0) {
+ continue;
+ }
+
+ if (node !== node2) {
+ return false;
+ }
+ }
+
+ return node === node2;
+ }
+
+ function findCaretNode(node, forward, startNode) {
+ var walker, current, nonEmptyElements;
+
+ nonEmptyElements = dom.schema.getNonEmptyElements();
+
+ walker = new TreeWalker(startNode || node, node);
+
+ while ((current = walker[forward ? 'next' : 'prev']())) {
+ if (nonEmptyElements[current.nodeName] && !isTrailingBr(current)) {
+ return current;
+ }
+
+ if (current.nodeType == 3 && current.data.length > 0) {
+ return current;
+ }
+ }
+ }
+
+ function deleteRangeBetweenTextBlocks(rng) {
+ var startBlock, endBlock, caretNodeBefore, caretNodeAfter, textBlockElements;
+
+ if (rng.collapsed) {
+ return;
+ }
+
+ startBlock = dom.getParent(RangeUtils.getNode(rng.startContainer, rng.startOffset), dom.isBlock);
+ endBlock = dom.getParent(RangeUtils.getNode(rng.endContainer, rng.endOffset), dom.isBlock);
+ textBlockElements = editor.schema.getTextBlockElements();
+
+ if (startBlock == endBlock) {
+ return;
+ }
+
+ if (!textBlockElements[startBlock.nodeName] || !textBlockElements[endBlock.nodeName]) {
+ return;
+ }
+
+ if (dom.getContentEditable(startBlock) === "false" || dom.getContentEditable(endBlock) === "false") {
+ return;
+ }
+
+ rng.deleteContents();
+
+ caretNodeBefore = findCaretNode(startBlock, false);
+ caretNodeAfter = findCaretNode(endBlock, true);
+
+ if (!dom.isEmpty(endBlock)) {
+ $(startBlock).append(endBlock.childNodes);
+ }
+
+ $(endBlock).remove();
+
+ if (caretNodeBefore) {
+ if (caretNodeBefore.nodeType == 1) {
+ if (caretNodeBefore.nodeName == "BR") {
+ rng.setStartBefore(caretNodeBefore);
+ rng.setEndBefore(caretNodeBefore);
+ } else {
+ rng.setStartAfter(caretNodeBefore);
+ rng.setEndAfter(caretNodeBefore);
+ }
+ } else {
+ rng.setStart(caretNodeBefore, caretNodeBefore.data.length);
+ rng.setEnd(caretNodeBefore, caretNodeBefore.data.length);
+ }
+ } else if (caretNodeAfter) {
+ if (caretNodeAfter.nodeType == 1) {
+ rng.setStartBefore(caretNodeAfter);
+ rng.setEndBefore(caretNodeAfter);
+ } else {
+ rng.setStart(caretNodeAfter, 0);
+ rng.setEnd(caretNodeAfter, 0);
+ }
+ }
+
+ selection.setRng(rng);
+
+ return true;
+ }
+
+ function expandBetweenBlocks(rng, isForward) {
+ var caretNode, targetCaretNode, textBlock, targetTextBlock, container, offset;
+
+ if (!rng.collapsed) {
+ return rng;
+ }
+
+ container = rng.startContainer;
+ offset = rng.startOffset;
+
+ if (container.nodeType == 3) {
+ if (isForward) {
+ if (offset < container.data.length) {
+ return rng;
+ }
+ } else {
+ if (offset > 0) {
+ return rng;
+ }
+ }
+ }
+
+ caretNode = RangeUtils.getNode(rng.startContainer, rng.startOffset);
+ textBlock = dom.getParent(caretNode, dom.isBlock);
+ targetCaretNode = findCaretNode(editor.getBody(), isForward, caretNode);
+ targetTextBlock = dom.getParent(targetCaretNode, dom.isBlock);
+
+ if (!caretNode || !targetCaretNode) {
+ return rng;
+ }
+
+ if (targetTextBlock && textBlock != targetTextBlock) {
+ if (!isForward) {
+ if (!isSiblingsIgnoreWhiteSpace(targetTextBlock, textBlock)) {
+ return rng;
+ }
+
+ if (targetCaretNode.nodeType == 1) {
+ if (targetCaretNode.nodeName == "BR") {
+ rng.setStartBefore(targetCaretNode);
+ } else {
+ rng.setStartAfter(targetCaretNode);
+ }
+ } else {
+ rng.setStart(targetCaretNode, targetCaretNode.data.length);
+ }
+
+ if (caretNode.nodeType == 1) {
+ rng.setEnd(caretNode, 0);
+ } else {
+ rng.setEndBefore(caretNode);
+ }
+ } else {
+ if (!isSiblingsIgnoreWhiteSpace(textBlock, targetTextBlock)) {
+ return rng;
+ }
+
+ if (caretNode.nodeType == 1) {
+ if (caretNode.nodeName == "BR") {
+ rng.setStartBefore(caretNode);
+ } else {
+ rng.setStartAfter(caretNode);
+ }
+ } else {
+ rng.setStart(caretNode, caretNode.data.length);
+ }
+
+ if (targetCaretNode.nodeType == 1) {
+ rng.setEnd(targetCaretNode, 0);
+ } else {
+ rng.setEndBefore(targetCaretNode);
+ }
+ }
+ }
+
+ return rng;
+ }
+
+ function handleTextBlockMergeDelete(isForward) {
+ var rng = selection.getRng();
+
+ rng = expandBetweenBlocks(rng, isForward);
+
+ if (deleteRangeBetweenTextBlocks(rng)) {
+ return true;
+ }
+ }
+
+ /**
+ * This retains the formatting if the last character is to be deleted.
+ *
+ * Backspace on this:
a|
would become
|
in WebKit.
+ * With this patch:
|
+ */
+ function handleLastBlockCharacterDelete(isForward, rng) {
+ var path, blockElm, newBlockElm, clonedBlockElm, sibling,
+ container, offset, br, currentFormatNodes;
+
+ function cloneTextBlockWithFormats(blockElm, node) {
+ currentFormatNodes = $(node).parents().filter(function(idx, node) {
+ return !!editor.schema.getTextInlineElements()[node.nodeName];
+ });
+
+ newBlockElm = blockElm.cloneNode(false);
+
+ currentFormatNodes = Tools.map(currentFormatNodes, function(formatNode) {
+ formatNode = formatNode.cloneNode(false);
+
+ if (newBlockElm.hasChildNodes()) {
+ formatNode.appendChild(newBlockElm.firstChild);
+ newBlockElm.appendChild(formatNode);
+ } else {
+ newBlockElm.appendChild(formatNode);
+ }
+
+ newBlockElm.appendChild(formatNode);
+
+ return formatNode;
+ });
+
+ if (currentFormatNodes.length) {
+ br = dom.create('br');
+ currentFormatNodes[0].appendChild(br);
+ dom.replace(newBlockElm, blockElm);
+
+ rng.setStartBefore(br);
+ rng.setEndBefore(br);
+ editor.selection.setRng(rng);
+
+ return br;
+ }
+
+ return null;
+ }
+
+ function isTextBlock(node) {
+ return node && editor.schema.getTextBlockElements()[node.tagName];
+ }
+
+ if (!rng.collapsed) {
+ return;
+ }
+
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ blockElm = dom.getParent(container, dom.isBlock);
+ if (!isTextBlock(blockElm)) {
+ return;
+ }
+
+ if (container.nodeType == 1) {
+ container = container.childNodes[offset];
+ if (container && container.tagName != 'BR') {
+ return;
+ }
+
+ if (isForward) {
+ sibling = blockElm.nextSibling;
+ } else {
+ sibling = blockElm.previousSibling;
+ }
+
+ if (dom.isEmpty(blockElm) && isTextBlock(sibling) && dom.isEmpty(sibling)) {
+ if (cloneTextBlockWithFormats(blockElm, container)) {
+ dom.remove(sibling);
+ return true;
+ }
+ }
+ } else if (container.nodeType == 3) {
+ path = NodePath.create(blockElm, container);
+ clonedBlockElm = blockElm.cloneNode(true);
+ container = NodePath.resolve(clonedBlockElm, path);
+
+ if (isForward) {
+ if (offset >= container.data.length) {
+ return;
+ }
+
+ container.deleteData(offset, 1);
+ } else {
+ if (offset <= 0) {
+ return;
+ }
+
+ container.deleteData(offset - 1, 1);
+ }
+
+ if (dom.isEmpty(clonedBlockElm)) {
+ return cloneTextBlockWithFormats(blockElm, container);
+ }
+ }
+ }
+
+ function customDelete(isForward) {
+ var mutationObserver, rng, caretElement;
+
+ if (handleTextBlockMergeDelete(isForward)) {
+ return;
+ }
+
+ Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) {
+ // Mark existing spans
+ if (elm.tagName == 'SPAN') {
+ elm.setAttribute('mce-data-marked', 1);
+ }
+
+ // Make sure all elements has a data-mce-style attribute
+ if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) {
+ editor.dom.setAttrib(elm, 'style', editor.dom.getAttrib(elm, 'style'));
+ }
+ });
+
+ // Observe added nodes and style attribute changes
+ mutationObserver = new MutationObserver(function() {});
+ mutationObserver.observe(editor.getDoc(), {
+ childList: true,
+ attributes: true,
+ subtree: true,
+ attributeFilter: ['style']
+ });
+
+ editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null);
+
+ rng = editor.selection.getRng();
+ caretElement = rng.startContainer.parentNode;
+
+ Tools.each(mutationObserver.takeRecords(), function(record) {
+ if (!dom.isChildOf(record.target, editor.getBody())) {
+ return;
+ }
+
+ // Restore style attribute to previous value
+ if (record.attributeName == "style") {
+ var oldValue = record.target.getAttribute('data-mce-style');
+
+ if (oldValue) {
+ record.target.setAttribute("style", oldValue);
+ } else {
+ record.target.removeAttribute("style");
+ }
+ }
+
+ // Remove all spans that aren't marked and retain selection
+ Tools.each(record.addedNodes, function(node) {
+ if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) {
+ var offset, container;
+
+ if (node == caretElement) {
+ offset = rng.startOffset;
+ container = node.firstChild;
+ }
+
+ dom.remove(node, true);
+
+ if (container) {
+ rng.setStart(container, offset);
+ rng.setEnd(container, offset);
+ editor.selection.setRng(rng);
+ }
+ }
+ });
+ });
+
+ mutationObserver.disconnect();
+
+ // Remove any left over marks
+ Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) {
+ span.removeAttribute('mce-data-marked');
+ });
+ }
+
+ editor.on('keydown', function(e) {
+ var isForward = e.keyCode == DELETE, isMetaOrCtrl = e.ctrlKey || e.metaKey;
+
+ if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) {
+ var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset;
+
+ // Shift+Delete is cut
+ if (isForward && e.shiftKey) {
+ return;
+ }
+
+ if (handleLastBlockCharacterDelete(isForward, rng)) {
+ e.preventDefault();
+ return;
+ }
+
+ // Ignore non meta delete in the where there is text before/after the caret
+ if (!isMetaOrCtrl && rng.collapsed && container.nodeType == 3) {
+ if (isForward ? offset < container.data.length : offset > 0) {
+ return;
+ }
+ }
+
+ e.preventDefault();
+
+ if (isMetaOrCtrl) {
+ editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", e.metaKey ? "lineboundary" : "word");
+ }
+
+ customDelete(isForward);
+ }
+ });
+
+ // Handle case where text is deleted by typing over
+ editor.on('keypress', function(e) {
+ if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
+ var rng, currentFormatNodes, fragmentNode, blockParent, caretNode, charText;
+
+ rng = editor.selection.getRng();
+ charText = String.fromCharCode(e.charCode);
+ e.preventDefault();
+
+ // Keep track of current format nodes
+ currentFormatNodes = $(rng.startContainer).parents().filter(function(idx, node) {
+ return !!editor.schema.getTextInlineElements()[node.nodeName];
+ });
+
+ customDelete(true);
+
+ // Check if the browser removed them
+ currentFormatNodes = currentFormatNodes.filter(function(idx, node) {
+ return !$.contains(editor.getBody(), node);
+ });
+
+ // Then re-add them
+ if (currentFormatNodes.length) {
+ fragmentNode = dom.createFragment();
+
+ currentFormatNodes.each(function(idx, formatNode) {
+ formatNode = formatNode.cloneNode(false);
+
+ if (fragmentNode.hasChildNodes()) {
+ formatNode.appendChild(fragmentNode.firstChild);
+ fragmentNode.appendChild(formatNode);
+ } else {
+ caretNode = formatNode;
+ fragmentNode.appendChild(formatNode);
+ }
+
+ fragmentNode.appendChild(formatNode);
+ });
+
+ caretNode.appendChild(editor.getDoc().createTextNode(charText));
+
+ // Prevent edge case where older WebKit would add an extra BR element
+ blockParent = dom.getParent(rng.startContainer, dom.isBlock);
+ if (dom.isEmpty(blockParent)) {
+ $(blockParent).empty().append(fragmentNode);
+ } else {
+ rng.insertNode(fragmentNode);
+ }
+
+ rng.setStart(caretNode.firstChild, 1);
+ rng.setEnd(caretNode.firstChild, 1);
+ editor.selection.setRng(rng);
+ } else {
+ editor.selection.setContent(charText);
+ }
+ }
+ });
+
+ editor.addCommand('Delete', function() {
+ customDelete();
+ });
+
+ editor.addCommand('ForwardDelete', function() {
+ customDelete(true);
+ });
+
+ // Older WebKits doesn't properly handle the clipboard so we can't add the rest
+ if (olderWebKit) {
+ return;
+ }
+
+ editor.on('dragstart', function(e) {
+ dragStartRng = selection.getRng();
+ setMceInternalContent(e);
+ });
+
+ editor.on('drop', function(e) {
+ if (!isDefaultPrevented(e)) {
+ var internalContent = getMceInternalContent(e);
+
+ if (internalContent) {
+ e.preventDefault();
+
+ // Safari has a weird issue where drag/dropping images sometimes
+ // produces a green plus icon. When this happens the caretRangeFromPoint
+ // will return "null" even though the x, y coordinate is correct.
+ // But if we detach the insert from the drop event we will get a proper range
+ Delay.setEditorTimeout(editor, function() {
+ var pointRng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, doc);
+
+ if (dragStartRng) {
+ selection.setRng(dragStartRng);
+ dragStartRng = null;
+ }
+
+ customDelete();
+ selection.setRng(pointRng);
+ insertClipboardContents(internalContent.html);
+ });
+ }
+ }
+ });
+
+ editor.on('cut', function(e) {
+ if (!isDefaultPrevented(e) && e.clipboardData && !editor.selection.isCollapsed()) {
+ e.preventDefault();
+ e.clipboardData.clearData();
+ e.clipboardData.setData('text/html', editor.selection.getContent());
+ e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'}));
+
+ // Needed delay for https://code.google.com/p/chromium/issues/detail?id=363288#c3
+ // Nested delete/forwardDelete not allowed on execCommand("cut")
+ // This is ugly but not sure how to work around it otherwise
+ Delay.setEditorTimeout(editor, function() {
+ customDelete(true);
+ });
+ }
+ });
+ }
+
+ /**
+ * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
+ *
+ * For example:
+ *
|
+ *
+ * Or:
+ *
|
+ *
+ * Or:
+ * [
]
+ */
+ function emptyEditorWhenDeleting() {
+ function serializeRng(rng) {
+ var body = dom.create("body");
+ var contents = rng.cloneContents();
+ body.appendChild(contents);
+ return selection.serializer.serialize(body, {format: 'html'});
+ }
+
+ function allContentsSelected(rng) {
+ if (!rng.setStart) {
+ if (rng.item) {
+ return false;
+ }
+
+ var bodyRng = rng.duplicate();
+ bodyRng.moveToElementText(editor.getBody());
+ return RangeUtils.compareRanges(rng, bodyRng);
+ }
+
+ var selection = serializeRng(rng);
+
+ var allRng = dom.createRng();
+ allRng.selectNode(editor.getBody());
+
+ var allSelection = serializeRng(allRng);
+ return selection === allSelection;
+ }
+
+ editor.on('keydown', function(e) {
+ var keyCode = e.keyCode, isCollapsed, body;
+
+ // Empty the editor if it's needed for example backspace at
|
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
+ isCollapsed = editor.selection.isCollapsed();
+ body = editor.getBody();
+
+ // Selection is collapsed but the editor isn't empty
+ if (isCollapsed && !dom.isEmpty(body)) {
+ return;
+ }
+
+ // Selection isn't collapsed but not all the contents is selected
+ if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
+ return;
+ }
+
+ // Manually empty the editor
+ e.preventDefault();
+ editor.setContent('');
+
+ if (body.firstChild && dom.isBlock(body.firstChild)) {
+ editor.selection.setCursorLocation(body.firstChild, 0);
+ } else {
+ editor.selection.setCursorLocation(body, 0);
+ }
+
+ editor.nodeChanged();
+ }
+ });
+ }
+
+ /**
+ * WebKit doesn't select all the nodes in the body when you press Ctrl+A.
+ * IE selects more than the contents [
a
] instead of
[a]
see bug #6438
+ * This selects the whole body so that backspace/delete logic will delete everything
+ */
+ function selectAll() {
+ editor.shortcuts.add('meta+a', null, 'SelectAll');
+ }
+
+ /**
+ * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
+ * The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
+ *
+ * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
+ * you enter a character into the editor.
+ *
+ * It also happens when the first focus in made to the body.
+ *
+ * See: https://bugs.webkit.org/show_bug.cgi?id=83566
+ */
+ function inputMethodFocus() {
+ if (!editor.settings.content_editable) {
+ // Case 1 IME doesn't initialize if you focus the document
+ // Disabled since it was interferring with the cE=false logic
+ // Also coultn't reproduce the issue on Safari 9
+ /*dom.bind(editor.getDoc(), 'focusin', function() {
+ selection.setRng(selection.getRng());
+ });*/
+
+ // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
+ // Needs to be both down/up due to weird rendering bug on Chrome Windows
+ dom.bind(editor.getDoc(), 'mousedown mouseup', function(e) {
+ var rng;
+
+ if (e.target == editor.getDoc().documentElement) {
+ rng = selection.getRng();
+ editor.getBody().focus();
+
+ if (e.type == 'mousedown') {
+ if (CaretContainer.isCaretContainer(rng.startContainer)) {
+ return;
+ }
+
+ // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret
+ selection.placeCaretAt(e.clientX, e.clientY);
+ } else {
+ selection.setRng(rng);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
+ * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
+ * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
+ * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
+ * browsers.
+ *
+ * It also fixes a bug on Firefox where it's impossible to delete HR elements.
+ */
+ function removeHrOnBackspace() {
+ editor.on('keydown', function(e) {
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
+ // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow
+ if (!editor.getBody().getElementsByTagName('hr').length) {
+ return;
+ }
+
+ if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
+ var node = selection.getNode();
+ var previousSibling = node.previousSibling;
+
+ if (node.nodeName == 'HR') {
+ dom.remove(node);
+ e.preventDefault();
+ return;
+ }
+
+ if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
+ dom.remove(previousSibling);
+ e.preventDefault();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Firefox 3.x has an issue where the body element won't get proper focus if you click out
+ * side it's rectangle.
+ */
+ function focusBody() {
+ // Fix for a focus bug in FF 3.x where the body element
+ // wouldn't get proper focus if the user clicked on the HTML element
+ if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
+ editor.on('mousedown', function(e) {
+ if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
+ var body = editor.getBody();
+
+ // Blur the body it's focused but not correctly focused
+ body.blur();
+
+ // Refocus the body after a little while
+ Delay.setEditorTimeout(editor, function() {
+ body.focus();
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * WebKit has a bug where it isn't possible to select image, hr or anchor elements
+ * by clicking on them so we need to fake that.
+ */
+ function selectControlElements() {
+ editor.on('click', function(e) {
+ var target = e.target;
+
+ // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
+ // WebKit can't even do simple things like selecting an image
+ // Needs to be the setBaseAndExtend or it will fail to select floated images
+ if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") {
+ e.preventDefault();
+ selection.getSel().setBaseAndExtent(target, 0, target, 1);
+ editor.nodeChanged();
+ }
+
+ if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) {
+ e.preventDefault();
+ selection.select(target);
+ }
+ });
+ }
+
+ /**
+ * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
+ *
+ * Fixes do backspace/delete on this:
+ *
bla[ck
r]ed
+ *
+ * Would become:
+ *
bla|ed
+ *
+ * Instead of:
+ *
bla|ed
+ */
+ function removeStylesWhenDeletingAcrossBlockElements() {
+ function getAttributeApplyFunction() {
+ var template = dom.getAttribs(selection.getStart().cloneNode(false));
+
+ return function() {
+ var target = selection.getStart();
+
+ if (target !== editor.getBody()) {
+ dom.setAttrib(target, "style", null);
+
+ each(template, function(attr) {
+ target.setAttributeNode(attr.cloneNode(true));
+ });
+ }
+ };
+ }
+
+ function isSelectionAcrossElements() {
+ return !selection.isCollapsed() &&
+ dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
+ }
+
+ editor.on('keypress', function(e) {
+ var applyAttributes;
+
+ if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
+ applyAttributes = getAttributeApplyFunction();
+ editor.getDoc().execCommand('delete', false, null);
+ applyAttributes();
+ e.preventDefault();
+ return false;
+ }
+ });
+
+ dom.bind(editor.getDoc(), 'cut', function(e) {
+ var applyAttributes;
+
+ if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
+ applyAttributes = getAttributeApplyFunction();
+
+ Delay.setEditorTimeout(editor, function() {
+ applyAttributes();
+ });
+ }
+ });
+ }
+
+ /**
+ * Screen readers on IE needs to have the role application set on the body.
+ */
+ function ensureBodyHasRoleApplication() {
+ document.body.setAttribute("role", "application");
+ }
+
+ /**
+ * Backspacing into a table behaves differently depending upon browser type.
+ * Therefore, disable Backspace when cursor immediately follows a table.
+ */
+ function disableBackspaceIntoATable() {
+ editor.on('keydown', function(e) {
+ if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
+ if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
+ var previousSibling = selection.getNode().previousSibling;
+ if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
+ e.preventDefault();
+ return false;
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this
+ * logic adds a \n before the BR so that it will get rendered.
+ */
+ function addNewLinesBeforeBrInPre() {
+ // IE8+ rendering mode does the right thing with BR in PRE
+ if (getDocumentMode() > 7) {
+ return;
+ }
+
+ // Enable display: none in area and add a specific class that hides all BR elements in PRE to
+ // avoid the caret from getting stuck at the BR elements while pressing the right arrow key
+ setEditorCommandState('RespectVisibilityInDesign', true);
+ editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');
+ dom.addClass(editor.getBody(), 'mceHideBrInPre');
+
+ // Adds a \n before all BR elements in PRE to get them visual
+ parser.addNodeFilter('pre', function(nodes) {
+ var i = nodes.length, brNodes, j, brElm, sibling;
+
+ while (i--) {
+ brNodes = nodes[i].getAll('br');
+ j = brNodes.length;
+ while (j--) {
+ brElm = brNodes[j];
+
+ // Add \n before BR in PRE elements on older IE:s so the new lines get rendered
+ sibling = brElm.prev;
+ if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') {
+ sibling.value += '\n';
+ } else {
+ brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n';
+ }
+ }
+ }
+ });
+
+ // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible
+ serializer.addNodeFilter('pre', function(nodes) {
+ var i = nodes.length, brNodes, j, brElm, sibling;
+
+ while (i--) {
+ brNodes = nodes[i].getAll('br');
+ j = brNodes.length;
+ while (j--) {
+ brElm = brNodes[j];
+ sibling = brElm.prev;
+ if (sibling && sibling.type == 3) {
+ sibling.value = sibling.value.replace(/\r?\n$/, '');
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Moves style width/height to attribute width/height when the user resizes an image on IE.
+ */
+ function removePreSerializedStylesWhenSelectingControls() {
+ dom.bind(editor.getBody(), 'mouseup', function() {
+ var value, node = selection.getNode();
+
+ // Moved styles to attributes on IMG eements
+ if (node.nodeName == 'IMG') {
+ // Convert style width to width attribute
+ if ((value = dom.getStyle(node, 'width'))) {
+ dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, ''));
+ dom.setStyle(node, 'width', '');
+ }
+
+ // Convert style height to height attribute
+ if ((value = dom.getStyle(node, 'height'))) {
+ dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, ''));
+ dom.setStyle(node, 'height', '');
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes a blockquote when backspace is pressed at the beginning of it.
+ *
+ * For example:
+ *
|x
+ *
+ * Becomes:
+ *
|x
+ */
+ function removeBlockQuoteOnBackSpace() {
+ // Add block quote deletion handler
+ editor.on('keydown', function(e) {
+ var rng, container, offset, root, parent;
+
+ if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
+ return;
+ }
+
+ rng = selection.getRng();
+ container = rng.startContainer;
+ offset = rng.startOffset;
+ root = dom.getRoot();
+ parent = container;
+
+ if (!rng.collapsed || offset !== 0) {
+ return;
+ }
+
+ while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
+ parent = parent.parentNode;
+ }
+
+ // Is the cursor at the beginning of a blockquote?
+ if (parent.tagName === 'BLOCKQUOTE') {
+ // Remove the blockquote
+ editor.formatter.toggle('blockquote', null, parent);
+
+ // Move the caret to the beginning of container
+ rng = dom.createRng();
+ rng.setStart(container, 0);
+ rng.setEnd(container, 0);
+ selection.setRng(rng);
+ }
+ });
+ }
+
+ /**
+ * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
+ */
+ function setGeckoEditingOptions() {
+ function setOpts() {
+ refreshContentEditable();
+
+ setEditorCommandState("StyleWithCSS", false);
+ setEditorCommandState("enableInlineTableEditing", false);
+
+ if (!settings.object_resizing) {
+ setEditorCommandState("enableObjectResizing", false);
+ }
+ }
+
+ if (!settings.readonly) {
+ editor.on('BeforeExecCommand MouseDown', setOpts);
+ }
+ }
+
+ /**
+ * Fixes a gecko link bug, when a link is placed at the end of block elements there is
+ * no way to move the caret behind the link. This fix adds a bogus br element after the link.
+ *
+ * For example this:
+ *
x
+ *
+ * Becomes this:
+ *
x
+ */
+ function addBrAfterLastLinks() {
+ function fixLinks() {
+ each(dom.select('a'), function(node) {
+ var parentNode = node.parentNode, root = dom.getRoot();
+
+ if (parentNode.lastChild === node) {
+ while (parentNode && !dom.isBlock(parentNode)) {
+ if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
+ return;
+ }
+
+ parentNode = parentNode.parentNode;
+ }
+
+ dom.add(parentNode, 'br', {'data-mce-bogus': 1});
+ }
+ });
+ }
+
+ editor.on('SetContent ExecCommand', function(e) {
+ if (e.type == "setcontent" || e.command === 'mceInsertLink') {
+ fixLinks();
+ }
+ });
+ }
+
+ /**
+ * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
+ * default we want to change that behavior.
+ */
+ function setDefaultBlockType() {
+ if (settings.forced_root_block) {
+ editor.on('init', function() {
+ setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
+ });
+ }
+ }
+
+ /**
+ * Deletes the selected image on IE instead of navigating to previous page.
+ */
+ function deleteControlItemOnBackSpace() {
+ editor.on('keydown', function(e) {
+ var rng;
+
+ if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
+ rng = editor.getDoc().selection.createRange();
+ if (rng && rng.item) {
+ e.preventDefault();
+ editor.undoManager.beforeChange();
+ dom.remove(rng.item(0));
+ editor.undoManager.add();
+ }
+ }
+ });
+ }
+
+ /**
+ * IE10 doesn't properly render block elements with the right height until you add contents to them.
+ * This fixes that by adding a padding-right to all empty text block elements.
+ * See: https://connect.microsoft.com/IE/feedback/details/743881
+ */
+ function renderEmptyBlocksFix() {
+ var emptyBlocksCSS;
+
+ // IE10+
+ if (getDocumentMode() >= 10) {
+ emptyBlocksCSS = '';
+ each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
+ emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
+ });
+
+ editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
+ }
+ }
+
+ /**
+ * Old IE versions can't retain contents within noscript elements so this logic will store the contents
+ * as a attribute and the insert that value as it's raw text when the DOM is serialized.
+ */
+ function keepNoScriptContents() {
+ if (getDocumentMode() < 9) {
+ parser.addNodeFilter('noscript', function(nodes) {
+ var i = nodes.length, node, textNode;
+
+ while (i--) {
+ node = nodes[i];
+ textNode = node.firstChild;
+
+ if (textNode) {
+ node.attr('data-mce-innertext', textNode.value);
+ }
+ }
+ });
+
+ serializer.addNodeFilter('noscript', function(nodes) {
+ var i = nodes.length, node, textNode, value;
+
+ while (i--) {
+ node = nodes[i];
+ textNode = nodes[i].firstChild;
+
+ if (textNode) {
+ textNode.value = Entities.decode(textNode.value);
+ } else {
+ // Old IE can't retain noscript value so an attribute is used to store it
+ value = node.attributes.map['data-mce-innertext'];
+ if (value) {
+ node.attr('data-mce-innertext', null);
+ textNode = new Node('#text', 3);
+ textNode.value = value;
+ textNode.raw = true;
+ node.append(textNode);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode.
+ */
+ function fixCaretSelectionOfDocumentElementOnIe() {
+ var doc = dom.doc, body = doc.body, started, startRng, htmlElm;
+
+ // Return range from point or null if it failed
+ function rngFromPoint(x, y) {
+ var rng = body.createTextRange();
+
+ try {
+ rng.moveToPoint(x, y);
+ } catch (ex) {
+ // IE sometimes throws and exception, so lets just ignore it
+ rng = null;
+ }
+
+ return rng;
+ }
+
+ // Fires while the selection is changing
+ function selectionChange(e) {
+ var pointRng;
+
+ // Check if the button is down or not
+ if (e.button) {
+ // Create range from mouse position
+ pointRng = rngFromPoint(e.x, e.y);
+
+ if (pointRng) {
+ // Check if pointRange is before/after selection then change the endPoint
+ if (pointRng.compareEndPoints('StartToStart', startRng) > 0) {
+ pointRng.setEndPoint('StartToStart', startRng);
+ } else {
+ pointRng.setEndPoint('EndToEnd', startRng);
+ }
+
+ pointRng.select();
+ }
+ } else {
+ endSelection();
+ }
+ }
+
+ // Removes listeners
+ function endSelection() {
+ var rng = doc.selection.createRange();
+
+ // If the range is collapsed then use the last start range
+ if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
+ startRng.select();
+ }
+
+ dom.unbind(doc, 'mouseup', endSelection);
+ dom.unbind(doc, 'mousemove', selectionChange);
+ startRng = started = 0;
+ }
+
+ // Make HTML element unselectable since we are going to handle selection by hand
+ doc.documentElement.unselectable = true;
+
+ // Detect when user selects outside BODY
+ dom.bind(doc, 'mousedown contextmenu', function(e) {
+ if (e.target.nodeName === 'HTML') {
+ if (started) {
+ endSelection();
+ }
+
+ // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
+ htmlElm = doc.documentElement;
+ if (htmlElm.scrollHeight > htmlElm.clientHeight) {
+ return;
+ }
+
+ started = 1;
+ // Setup start position
+ startRng = rngFromPoint(e.x, e.y);
+ if (startRng) {
+ // Listen for selection change events
+ dom.bind(doc, 'mouseup', endSelection);
+ dom.bind(doc, 'mousemove', selectionChange);
+
+ dom.getRoot().focus();
+ startRng.select();
+ }
+ }
+ });
+ }
+
+ /**
+ * Fixes selection issues where the caret can be placed between two inline elements like
a|
b
+ * this fix will lean the caret right into the closest inline element.
+ */
+ function normalizeSelection() {
+ // Normalize selection for example
a|a becomes
a|a except for Ctrl+A since it selects everything
+ editor.on('keyup focusin mouseup', function(e) {
+ if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
+ selection.normalize();
+ }
+ }, true);
+ }
+
+ /**
+ * Forces Gecko to render a broken image icon if it fails to load an image.
+ */
+ function showBrokenImageIcon() {
+ editor.contentStyles.push(
+ 'img:-moz-broken {' +
+ '-moz-force-broken-image-icon:1;' +
+ 'min-width:24px;' +
+ 'min-height:24px' +
+ '}'
+ );
+ }
+
+ /**
+ * iOS has a bug where it's impossible to type if the document has a touchstart event
+ * bound and the user touches the document while having the on screen keyboard visible.
+ *
+ * The touch event moves the focus to the parent document while having the caret inside the iframe
+ * this fix moves the focus back into the iframe document.
+ */
+ function restoreFocusOnKeyDown() {
+ if (!editor.inline) {
+ editor.on('keydown', function() {
+ if (document.activeElement == document.body) {
+ editor.getWin().focus();
+ }
+ });
+ }
+ }
+
+ /**
+ * IE 11 has an annoying issue where you can't move focus into the editor
+ * by clicking on the white area HTML element. We used to be able to to fix this with
+ * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
+ * object it's not possible anymore. So we need to hack in a ungly CSS to force the
+ * body to be at least 150px. If the user clicks the HTML element out side this 150px region
+ * we simply move the focus into the first paragraph. Not ideal since you loose the
+ * positioning of the caret but goot enough for most cases.
+ */
+ function bodyHeight() {
+ if (!editor.inline) {
+ editor.contentStyles.push('body {min-height: 150px}');
+ editor.on('click', function(e) {
+ var rng;
+
+ if (e.target.nodeName == 'HTML') {
+ // Edge seems to only need focus if we set the range
+ // the caret will become invisible and moved out of the iframe!!
+ if (Env.ie > 11) {
+ editor.getBody().focus();
+ return;
+ }
+
+ // Need to store away non collapsed ranges since the focus call will mess that up see #7382
+ rng = editor.selection.getRng();
+ editor.getBody().focus();
+ editor.selection.setRng(rng);
+ editor.selection.normalize();
+ editor.nodeChanged();
+ }
+ });
+ }
+ }
+
+ /**
+ * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
+ * You might then loose all your work so we need to block that behavior and replace it with our own.
+ */
+ function blockCmdArrowNavigation() {
+ if (Env.mac) {
+ editor.on('keydown', function(e) {
+ if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) {
+ e.preventDefault();
+ editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary');
+ }
+ });
+ }
+ }
+
+ /**
+ * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
+ */
+ function disableAutoUrlDetect() {
+ setEditorCommandState("AutoUrlDetect", false);
+ }
+
+ /**
+ * iOS 7.1 introduced two new bugs:
+ * 1) It's possible to open links within a contentEditable area by clicking on them.
+ * 2) If you hold down the finger it will display the link/image touch callout menu.
+ */
+ function tapLinksAndImages() {
+ editor.on('click', function(e) {
+ var elm = e.target;
+
+ do {
+ if (elm.tagName === 'A') {
+ e.preventDefault();
+ return;
+ }
+ } while ((elm = elm.parentNode));
+ });
+
+ editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
+ }
+
+ /**
+ * iOS Safari and possible other browsers have a bug where it won't fire
+ * a click event when a contentEditable is focused. This function fakes click events
+ * by using touchstart/touchend and measuring the time and distance travelled.
+ */
+ /*
+ function touchClickEvent() {
+ editor.on('touchstart', function(e) {
+ var elm, time, startTouch, changedTouches;
+
+ elm = e.target;
+ time = new Date().getTime();
+ changedTouches = e.changedTouches;
+
+ if (!changedTouches || changedTouches.length > 1) {
+ return;
+ }
+
+ startTouch = changedTouches[0];
+
+ editor.once('touchend', function(e) {
+ var endTouch = e.changedTouches[0], args;
+
+ if (new Date().getTime() - time > 500) {
+ return;
+ }
+
+ if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) {
+ return;
+ }
+
+ if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) {
+ return;
+ }
+
+ args = {
+ target: elm
+ };
+
+ each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) {
+ args[key] = endTouch[key];
+ });
+
+ args = editor.fire('click', args);
+
+ if (!args.isDefaultPrevented()) {
+ // iOS WebKit can't place the caret properly once
+ // you bind touch events so we need to do this manually
+ // TODO: Expand to the closest word? Touble tap still works.
+ editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY);
+ editor.nodeChanged();
+ }
+ });
+ });
+ }
+ */
+
+ /**
+ * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
+ * For example this:
+ */
+ function blockFormSubmitInsideEditor() {
+ editor.on('init', function() {
+ editor.dom.bind(editor.getBody(), 'submit', function(e) {
+ e.preventDefault();
+ });
+ });
+ }
+
+ /**
+ * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
+ *
+ * Scenario:
+ * 1) Create a table 2x2.
+ * 2) Select and copy cells A2-B2.
+ * 3) Paste and it will add BR element to table cell.
+ */
+ function removeAppleInterchangeBrs() {
+ parser.addNodeFilter('br', function(nodes) {
+ var i = nodes.length;
+
+ while (i--) {
+ if (nodes[i].attr('class') == 'Apple-interchange-newline') {
+ nodes[i].remove();
+ }
+ }
+ });
+ }
+
+ /**
+ * IE cannot set custom contentType's on drag events, and also does not properly drag/drop between
+ * editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors.
+ */
+ function ieInternalDragAndDrop() {
+ editor.on('dragstart', function(e) {
+ setMceInternalContent(e);
+ });
+
+ editor.on('drop', function(e) {
+ if (!isDefaultPrevented(e)) {
+ var internalContent = getMceInternalContent(e);
+
+ if (internalContent && internalContent.id != editor.id) {
+ e.preventDefault();
+
+ var rng = RangeUtils.getCaretRangeFromPoint(e.x, e.y, editor.getDoc());
+ selection.setRng(rng);
+ insertClipboardContents(internalContent.html);
+ }
+ }
+ });
+ }
+
+ function refreshContentEditable() {
+ var body, parent;
+
+ // Check if the editor was hidden and the re-initialize contentEditable mode by removing and adding the body again
+ if (isHidden()) {
+ body = editor.getBody();
+ parent = body.parentNode;
+
+ parent.removeChild(body);
+ parent.appendChild(body);
+
+ body.focus();
+ }
+ }
+
+ function isHidden() {
+ var sel;
+
+ if (!isGecko) {
+ return 0;
+ }
+
+ // Weird, wheres that cursor selection?
+ sel = editor.selection.getSel();
+ return (!sel || !sel.rangeCount || sel.rangeCount === 0);
+ }
+
+ /**
+ * Properly empties the editor if all contents is selected and deleted this to
+ * prevent empty paragraphs from being produced at beginning/end of contents.
+ */
+ function emptyEditorOnDeleteEverything() {
+ function isEverythingSelected(editor) {
+ var caretWalker = new CaretWalker(editor.getBody());
+ var rng = editor.selection.getRng();
+ var startCaretPos = CaretPosition.fromRangeStart(rng);
+ var endCaretPos = CaretPosition.fromRangeEnd(rng);
+
+ return !editor.selection.isCollapsed() && !caretWalker.prev(startCaretPos) && !caretWalker.next(endCaretPos);
+ }
+
+ // Type over case delete and insert this won't cover typeover with a IME but at least it covers the common case
+ editor.on('keypress', function (e) {
+ if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) {
+ if (isEverythingSelected(editor)) {
+ e.preventDefault();
+ editor.setContent(String.fromCharCode(e.charCode));
+ editor.selection.select(editor.getBody(), true);
+ editor.selection.collapse(false);
+ editor.nodeChanged();
+ }
+ }
+ });
+
+ editor.on('keydown', function (e) {
+ var keyCode = e.keyCode;
+
+ if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
+ if (isEverythingSelected(editor)) {
+ e.preventDefault();
+ editor.setContent('');
+ editor.nodeChanged();
+ }
+ }
+ });
+ }
+
+ // All browsers
+ removeBlockQuoteOnBackSpace();
+ emptyEditorWhenDeleting();
+
+ // Windows phone will return a range like [body, 0] on mousedown so
+ // it will always normalize to the wrong location
+ if (!Env.windowsPhone) {
+ normalizeSelection();
+ }
+
+ // WebKit
+ if (isWebKit) {
+ emptyEditorOnDeleteEverything();
+ cleanupStylesWhenDeleting();
+ inputMethodFocus();
+ selectControlElements();
+ setDefaultBlockType();
+ blockFormSubmitInsideEditor();
+ disableBackspaceIntoATable();
+ removeAppleInterchangeBrs();
+
+ //touchClickEvent();
+
+ // iOS
+ if (Env.iOS) {
+ restoreFocusOnKeyDown();
+ bodyHeight();
+ tapLinksAndImages();
+ } else {
+ selectAll();
+ }
+ }
+
+ // IE
+ if (isIE && Env.ie < 11) {
+ removeHrOnBackspace();
+ ensureBodyHasRoleApplication();
+ addNewLinesBeforeBrInPre();
+ removePreSerializedStylesWhenSelectingControls();
+ deleteControlItemOnBackSpace();
+ renderEmptyBlocksFix();
+ keepNoScriptContents();
+ fixCaretSelectionOfDocumentElementOnIe();
+ }
+
+ if (Env.ie >= 11) {
+ bodyHeight();
+ disableBackspaceIntoATable();
+ }
+
+ if (Env.ie) {
+ selectAll();
+ disableAutoUrlDetect();
+ ieInternalDragAndDrop();
+ }
+
+ // Gecko
+ if (isGecko) {
+ emptyEditorOnDeleteEverything();
+ removeHrOnBackspace();
+ focusBody();
+ removeStylesWhenDeletingAcrossBlockElements();
+ setGeckoEditingOptions();
+ addBrAfterLastLinks();
+ showBrokenImageIcon();
+ blockCmdArrowNavigation();
+ disableBackspaceIntoATable();
+ }
+
+ return {
+ refreshContentEditable: refreshContentEditable,
+ isHidden: isHidden
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/EditorObservable.js
+
+/**
+ * EditorObservable.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This mixin contains the event logic for the tinymce.Editor class.
+ *
+ * @mixin tinymce.EditorObservable
+ * @extends tinymce.util.Observable
+ */
+define("tinymce/EditorObservable", [
+ "tinymce/util/Observable",
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Tools"
+], function(Observable, DOMUtils, Tools) {
+ var DOM = DOMUtils.DOM, customEventRootDelegates;
+
+ /**
+ * Returns the event target so for the specified event. Some events fire
+ * only on document, some fire on documentElement etc. This also handles the
+ * custom event root setting where it returns that element instead of the body.
+ *
+ * @private
+ * @param {tinymce.Editor} editor Editor instance to get event target from.
+ * @param {String} eventName Name of the event for example "click".
+ * @return {Element/Document} HTML Element or document target to bind on.
+ */
+ function getEventTarget(editor, eventName) {
+ if (eventName == 'selectionchange') {
+ return editor.getDoc();
+ }
+
+ // Need to bind mousedown/mouseup etc to document not body in iframe mode
+ // Since the user might click on the HTML element not the BODY
+ if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
+ return editor.getDoc().documentElement;
+ }
+
+ // Bind to event root instead of body if it's defined
+ if (editor.settings.event_root) {
+ if (!editor.eventRoot) {
+ editor.eventRoot = DOM.select(editor.settings.event_root)[0];
+ }
+
+ return editor.eventRoot;
+ }
+
+ return editor.getBody();
+ }
+
+ /**
+ * Binds a event delegate for the specified name this delegate will fire
+ * the event to the editor dispatcher.
+ *
+ * @private
+ * @param {tinymce.Editor} editor Editor instance to get event target from.
+ * @param {String} eventName Name of the event for example "click".
+ */
+ function bindEventDelegate(editor, eventName) {
+ var eventRootElm = getEventTarget(editor, eventName), delegate;
+
+ function isListening(editor) {
+ return !editor.hidden && !editor.readonly;
+ }
+
+ if (!editor.delegates) {
+ editor.delegates = {};
+ }
+
+ if (editor.delegates[eventName]) {
+ return;
+ }
+
+ if (editor.settings.event_root) {
+ if (!customEventRootDelegates) {
+ customEventRootDelegates = {};
+ editor.editorManager.on('removeEditor', function() {
+ var name;
+
+ if (!editor.editorManager.activeEditor) {
+ if (customEventRootDelegates) {
+ for (name in customEventRootDelegates) {
+ editor.dom.unbind(getEventTarget(editor, name));
+ }
+
+ customEventRootDelegates = null;
+ }
+ }
+ });
+ }
+
+ if (customEventRootDelegates[eventName]) {
+ return;
+ }
+
+ delegate = function(e) {
+ var target = e.target, editors = editor.editorManager.editors, i = editors.length;
+
+ while (i--) {
+ var body = editors[i].getBody();
+
+ if (body === target || DOM.isChildOf(target, body)) {
+ if (isListening(editors[i])) {
+ editors[i].fire(eventName, e);
+ }
+ }
+ }
+ };
+
+ customEventRootDelegates[eventName] = delegate;
+ DOM.bind(eventRootElm, eventName, delegate);
+ } else {
+ delegate = function(e) {
+ if (isListening(editor)) {
+ editor.fire(eventName, e);
+ }
+ };
+
+ DOM.bind(eventRootElm, eventName, delegate);
+ editor.delegates[eventName] = delegate;
+ }
+ }
+
+ var EditorObservable = {
+ /**
+ * Bind any pending event delegates. This gets executed after the target body/document is created.
+ *
+ * @private
+ */
+ bindPendingEventDelegates: function() {
+ var self = this;
+
+ Tools.each(self._pendingNativeEvents, function(name) {
+ bindEventDelegate(self, name);
+ });
+ },
+
+ /**
+ * Toggles a native event on/off this is called by the EventDispatcher when
+ * the first native event handler is added and when the last native event handler is removed.
+ *
+ * @private
+ */
+ toggleNativeEvent: function(name, state) {
+ var self = this;
+
+ // Never bind focus/blur since the FocusManager fakes those
+ if (name == "focus" || name == "blur") {
+ return;
+ }
+
+ if (state) {
+ if (self.initialized) {
+ bindEventDelegate(self, name);
+ } else {
+ if (!self._pendingNativeEvents) {
+ self._pendingNativeEvents = [name];
+ } else {
+ self._pendingNativeEvents.push(name);
+ }
+ }
+ } else if (self.initialized) {
+ self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
+ delete self.delegates[name];
+ }
+ },
+
+ /**
+ * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
+ *
+ * @private
+ */
+ unbindAllNativeEvents: function() {
+ var self = this, name;
+
+ if (self.delegates) {
+ for (name in self.delegates) {
+ self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
+ }
+
+ delete self.delegates;
+ }
+
+ if (!self.inline) {
+ self.getBody().onload = null;
+ self.dom.unbind(self.getWin());
+ self.dom.unbind(self.getDoc());
+ }
+
+ self.dom.unbind(self.getBody());
+ self.dom.unbind(self.getContainer());
+ }
+ };
+
+ EditorObservable = Tools.extend({}, Observable, EditorObservable);
+
+ return EditorObservable;
+});
+
+// Included from: js/tinymce/classes/Mode.js
+
+/**
+ * Mode.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Mode switcher logic.
+ *
+ * @private
+ * @class tinymce.Mode
+ */
+define("tinymce/Mode", [], function() {
+ function setEditorCommandState(editor, cmd, state) {
+ try {
+ editor.getDoc().execCommand(cmd, false, state);
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ function clickBlocker(editor) {
+ var target, handler;
+
+ target = editor.getBody();
+
+ handler = function(e) {
+ if (editor.dom.getParents(e.target, 'a').length > 0) {
+ e.preventDefault();
+ }
+ };
+
+ editor.dom.bind(target, 'click', handler);
+
+ return {
+ unbind: function() {
+ editor.dom.unbind(target, 'click', handler);
+ }
+ };
+ }
+
+ function toggleReadOnly(editor, state) {
+ if (editor._clickBlocker) {
+ editor._clickBlocker.unbind();
+ editor._clickBlocker = null;
+ }
+
+ if (state) {
+ editor._clickBlocker = clickBlocker(editor);
+ editor.selection.controlSelection.hideResizeRect();
+ editor.readonly = true;
+ editor.getBody().contentEditable = false;
+ } else {
+ editor.readonly = false;
+ editor.getBody().contentEditable = true;
+ setEditorCommandState(editor, "StyleWithCSS", false);
+ setEditorCommandState(editor, "enableInlineTableEditing", false);
+ setEditorCommandState(editor, "enableObjectResizing", false);
+ editor.focus();
+ editor.nodeChanged();
+ }
+ }
+
+ function setMode(editor, mode) {
+ var currentMode = editor.readonly ? 'readonly' : 'design';
+
+ if (mode == currentMode) {
+ return;
+ }
+
+ if (editor.initialized) {
+ toggleReadOnly(editor, mode == 'readonly');
+ } else {
+ editor.on('init', function() {
+ toggleReadOnly(editor, mode == 'readonly');
+ });
+ }
+
+ // Event is NOT preventable
+ editor.fire('SwitchMode', {mode: mode});
+ }
+
+ return {
+ setMode: setMode
+ };
+});
+
+// Included from: js/tinymce/classes/Shortcuts.js
+
+/**
+ * Shortcuts.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Contains all logic for handling of keyboard shortcuts.
+ *
+ * @class tinymce.Shortcuts
+ * @example
+ * editor.shortcuts.add('ctrl+a', function() {});
+ * editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC
+ * editor.shortcuts.add('ctrl+alt+a', function() {});
+ * editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
+ */
+define("tinymce/Shortcuts", [
+ "tinymce/util/Tools",
+ "tinymce/Env"
+], function(Tools, Env) {
+ var each = Tools.each, explode = Tools.explode;
+
+ var keyCodeLookup = {
+ "f9": 120,
+ "f10": 121,
+ "f11": 122
+ };
+
+ var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
+
+ return function(editor) {
+ var self = this, shortcuts = {}, pendingPatterns = [];
+
+ function parseShortcut(pattern) {
+ var id, key, shortcut = {};
+
+ // Parse modifiers and keys ctrl+alt+b for example
+ each(explode(pattern, '+'), function(value) {
+ if (value in modifierNames) {
+ shortcut[value] = true;
+ } else {
+ // Allow numeric keycodes like ctrl+219 for ctrl+[
+ if (/^[0-9]{2,}$/.test(value)) {
+ shortcut.keyCode = parseInt(value, 10);
+ } else {
+ shortcut.charCode = value.charCodeAt(0);
+ shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
+ }
+ }
+ });
+
+ // Generate unique id for modifier combination and set default state for unused modifiers
+ id = [shortcut.keyCode];
+ for (key in modifierNames) {
+ if (shortcut[key]) {
+ id.push(key);
+ } else {
+ shortcut[key] = false;
+ }
+ }
+ shortcut.id = id.join(',');
+
+ // Handle special access modifier differently depending on Mac/Win
+ if (shortcut.access) {
+ shortcut.alt = true;
+
+ if (Env.mac) {
+ shortcut.ctrl = true;
+ } else {
+ shortcut.shift = true;
+ }
+ }
+
+ // Handle special meta modifier differently depending on Mac/Win
+ if (shortcut.meta) {
+ if (Env.mac) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ shortcut.meta = false;
+ }
+ }
+
+ return shortcut;
+ }
+
+ function createShortcut(pattern, desc, cmdFunc, scope) {
+ var shortcuts;
+
+ shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
+ shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
+ func: cmdFunc,
+ scope: scope || editor
+ });
+
+ return Tools.extend(shortcuts[0], {
+ desc: editor.translate(desc),
+ subpatterns: shortcuts.slice(1)
+ });
+ }
+
+ function hasModifier(e) {
+ return e.altKey || e.ctrlKey || e.metaKey;
+ }
+
+ function isFunctionKey(e) {
+ return e.keyCode >= 112 && e.keyCode <= 123;
+ }
+
+ function matchShortcut(e, shortcut) {
+ if (!shortcut) {
+ return false;
+ }
+
+ if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
+ return false;
+ }
+
+ if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
+ return false;
+ }
+
+ if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
+ e.preventDefault();
+ return true;
+ }
+
+ return false;
+ }
+
+ function executeShortcutAction(shortcut) {
+ return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
+ }
+
+ editor.on('keyup keypress keydown', function(e) {
+ if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) {
+ each(shortcuts, function(shortcut) {
+ if (matchShortcut(e, shortcut)) {
+ pendingPatterns = shortcut.subpatterns.slice(0);
+
+ if (e.type == "keydown") {
+ executeShortcutAction(shortcut);
+ }
+
+ return true;
+ }
+ });
+
+ if (matchShortcut(e, pendingPatterns[0])) {
+ if (pendingPatterns.length === 1) {
+ if (e.type == "keydown") {
+ executeShortcutAction(pendingPatterns[0]);
+ }
+ }
+
+ pendingPatterns.shift();
+ }
+ }
+ });
+
+ /**
+ * Adds a keyboard shortcut for some command or function.
+ *
+ * @method addShortcut
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @param {String} desc Text description for the command.
+ * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @return {Boolean} true/false state if the shortcut was added or not.
+ */
+ self.add = function(pattern, desc, cmdFunc, scope) {
+ var cmd;
+
+ cmd = cmdFunc;
+
+ if (typeof cmdFunc === 'string') {
+ cmdFunc = function() {
+ editor.execCommand(cmd, false, null);
+ };
+ } else if (Tools.isArray(cmd)) {
+ cmdFunc = function() {
+ editor.execCommand(cmd[0], cmd[1], cmd[2]);
+ };
+ }
+
+ each(explode(Tools.trim(pattern.toLowerCase())), function(pattern) {
+ var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
+ shortcuts[shortcut.id] = shortcut;
+ });
+
+ return true;
+ };
+
+ /**
+ * Remove a keyboard shortcut by pattern.
+ *
+ * @method remove
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @return {Boolean} true/false state if the shortcut was removed or not.
+ */
+ self.remove = function(pattern) {
+ var shortcut = createShortcut(pattern);
+
+ if (shortcuts[shortcut.id]) {
+ delete shortcuts[shortcut.id];
+ return true;
+ }
+
+ return false;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/Uploader.js
+
+/**
+ * Uploader.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Upload blobs or blob infos to the specified URL or handler.
+ *
+ * @private
+ * @class tinymce.file.Uploader
+ * @example
+ * var uploader = new Uploader({
+ * url: '/upload.php',
+ * basePath: '/base/path',
+ * credentials: true,
+ * handler: function(data, success, failure) {
+ * ...
+ * }
+ * });
+ *
+ * uploader.upload(blobInfos).then(function(result) {
+ * ...
+ * });
+ */
+define("tinymce/file/Uploader", [
+ "tinymce/util/Promise",
+ "tinymce/util/Tools",
+ "tinymce/util/Fun"
+], function(Promise, Tools, Fun) {
+ return function(uploadStatus, settings) {
+ var pendingPromises = {};
+
+ function fileName(blobInfo) {
+ var ext, extensions;
+
+ extensions = {
+ 'image/jpeg': 'jpg',
+ 'image/jpg': 'jpg',
+ 'image/gif': 'gif',
+ 'image/png': 'png'
+ };
+
+ ext = extensions[blobInfo.blob().type.toLowerCase()] || 'dat';
+
+ return blobInfo.id() + '.' + ext;
+ }
+
+ function pathJoin(path1, path2) {
+ if (path1) {
+ return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
+ }
+
+ return path2;
+ }
+
+ function blobInfoToData(blobInfo) {
+ return {
+ id: blobInfo.id,
+ blob: blobInfo.blob,
+ base64: blobInfo.base64,
+ filename: Fun.constant(fileName(blobInfo))
+ };
+ }
+
+ function defaultHandler(blobInfo, success, failure, progress) {
+ var xhr, formData;
+
+ xhr = new XMLHttpRequest();
+ xhr.open('POST', settings.url);
+ xhr.withCredentials = settings.credentials;
+
+ xhr.upload.onprogress = function(e) {
+ progress(e.loaded / e.total * 100);
+ };
+
+ xhr.onerror = function() {
+ failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status);
+ };
+
+ xhr.onload = function() {
+ var json;
+
+ if (xhr.status != 200) {
+ failure("HTTP Error: " + xhr.status);
+ return;
+ }
+
+ json = JSON.parse(xhr.responseText);
+
+ if (!json || typeof json.location != "string") {
+ failure("Invalid JSON: " + xhr.responseText);
+ return;
+ }
+
+ success(pathJoin(settings.basePath, json.location));
+ };
+
+ formData = new FormData();
+ formData.append('file', blobInfo.blob(), fileName(blobInfo));
+
+ xhr.send(formData);
+ }
+
+ function noUpload() {
+ return new Promise(function(resolve) {
+ resolve([]);
+ });
+ }
+
+ function handlerSuccess(blobInfo, url) {
+ return {
+ url: url,
+ blobInfo: blobInfo,
+ status: true
+ };
+ }
+
+ function handlerFailure(blobInfo, error) {
+ return {
+ url: '',
+ blobInfo: blobInfo,
+ status: false,
+ error: error
+ };
+ }
+
+ function resolvePending(blobUri, result) {
+ Tools.each(pendingPromises[blobUri], function(resolve) {
+ resolve(result);
+ });
+
+ delete pendingPromises[blobUri];
+ }
+
+ function uploadBlobInfo(blobInfo, handler, openNotification) {
+ uploadStatus.markPending(blobInfo.blobUri());
+
+ return new Promise(function(resolve) {
+ var notification, progress;
+
+ var noop = function() {
+ };
+
+ try {
+ var closeNotification = function() {
+ if (notification) {
+ notification.close();
+ progress = noop; // Once it's closed it's closed
+ }
+ };
+
+ var success = function(url) {
+ closeNotification();
+ uploadStatus.markUploaded(blobInfo.blobUri(), url);
+ resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
+ resolve(handlerSuccess(blobInfo, url));
+ };
+
+ var failure = function() {
+ closeNotification();
+ uploadStatus.removeFailed(blobInfo.blobUri());
+ resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, failure));
+ resolve(handlerFailure(blobInfo, failure));
+ };
+
+ progress = function(percent) {
+ if (percent < 0 || percent > 100) {
+ return;
+ }
+
+ if (!notification) {
+ notification = openNotification();
+ }
+
+ notification.progressBar.value(percent);
+ };
+
+ handler(blobInfoToData(blobInfo), success, failure, progress);
+ } catch (ex) {
+ resolve(handlerFailure(blobInfo, ex.message));
+ }
+ });
+ }
+
+ function isDefaultHandler(handler) {
+ return handler === defaultHandler;
+ }
+
+ function pendingUploadBlobInfo(blobInfo) {
+ var blobUri = blobInfo.blobUri();
+
+ return new Promise(function(resolve) {
+ pendingPromises[blobUri] = pendingPromises[blobUri] || [];
+ pendingPromises[blobUri].push(resolve);
+ });
+ }
+
+ function uploadBlobs(blobInfos, openNotification) {
+ blobInfos = Tools.grep(blobInfos, function(blobInfo) {
+ return !uploadStatus.isUploaded(blobInfo.blobUri());
+ });
+
+ return Promise.all(Tools.map(blobInfos, function(blobInfo) {
+ return uploadStatus.isPending(blobInfo.blobUri()) ?
+ pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification);
+ }));
+ }
+
+ function upload(blobInfos, openNotification) {
+ return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
+ }
+
+ settings = Tools.extend({
+ credentials: false,
+ // We are adding a notify argument to this (at the moment, until it doesn't work)
+ handler: defaultHandler
+ }, settings);
+
+ return {
+ upload: upload
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/Conversions.js
+
+/**
+ * Conversions.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Converts blob/uris back and forth.
+ *
+ * @private
+ * @class tinymce.file.Conversions
+ */
+define("tinymce/file/Conversions", [
+ "tinymce/util/Promise"
+], function(Promise) {
+ function blobUriToBlob(url) {
+ return new Promise(function(resolve) {
+ var xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url, true);
+ xhr.responseType = 'blob';
+
+ xhr.onload = function() {
+ if (this.status == 200) {
+ resolve(this.response);
+ }
+ };
+
+ xhr.send();
+ });
+ }
+
+ function parseDataUri(uri) {
+ var type, matches;
+
+ uri = decodeURIComponent(uri).split(',');
+
+ matches = /data:([^;]+)/.exec(uri[0]);
+ if (matches) {
+ type = matches[1];
+ }
+
+ return {
+ type: type,
+ data: uri[1]
+ };
+ }
+
+ function dataUriToBlob(uri) {
+ return new Promise(function(resolve) {
+ var str, arr, i;
+
+ uri = parseDataUri(uri);
+
+ // Might throw error if data isn't proper base64
+ try {
+ str = atob(uri.data);
+ } catch (e) {
+ resolve(new Blob([]));
+ return;
+ }
+
+ arr = new Uint8Array(str.length);
+
+ for (i = 0; i < arr.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+
+ resolve(new Blob([arr], {type: uri.type}));
+ });
+ }
+
+ function uriToBlob(url) {
+ if (url.indexOf('blob:') === 0) {
+ return blobUriToBlob(url);
+ }
+
+ if (url.indexOf('data:') === 0) {
+ return dataUriToBlob(url);
+ }
+
+ return null;
+ }
+
+ function blobToDataUri(blob) {
+ return new Promise(function(resolve) {
+ var reader = new FileReader();
+
+ reader.onloadend = function() {
+ resolve(reader.result);
+ };
+
+ reader.readAsDataURL(blob);
+ });
+ }
+
+ return {
+ uriToBlob: uriToBlob,
+ blobToDataUri: blobToDataUri,
+ parseDataUri: parseDataUri
+ };
+});
+
+// Included from: js/tinymce/classes/file/ImageScanner.js
+
+/**
+ * ImageScanner.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
+ *
+ * @private
+ * @class tinymce.file.ImageScanner
+ */
+define("tinymce/file/ImageScanner", [
+ "tinymce/util/Promise",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun",
+ "tinymce/file/Conversions",
+ "tinymce/Env"
+], function(Promise, Arr, Fun, Conversions, Env) {
+ var count = 0;
+
+ return function(uploadStatus, blobCache) {
+ var cachedPromises = {};
+
+ function findAll(elm, predicate) {
+ var images, promises;
+
+ function imageToBlobInfo(img, resolve) {
+ var base64, blobInfo;
+
+ if (img.src.indexOf('blob:') === 0) {
+ blobInfo = blobCache.getByUri(img.src);
+
+ if (blobInfo) {
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ }
+
+ return;
+ }
+
+ base64 = Conversions.parseDataUri(img.src).data;
+ blobInfo = blobCache.findFirst(function(cachedBlobInfo) {
+ return cachedBlobInfo.base64() === base64;
+ });
+
+ if (blobInfo) {
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ } else {
+ Conversions.uriToBlob(img.src).then(function(blob) {
+ var blobInfoId = 'blobid' + (count++),
+ blobInfo = blobCache.create(blobInfoId, blob, base64);
+
+ blobCache.add(blobInfo);
+
+ resolve({
+ image: img,
+ blobInfo: blobInfo
+ });
+ });
+ }
+ }
+
+ if (!predicate) {
+ predicate = Fun.constant(true);
+ }
+
+ images = Arr.filter(elm.getElementsByTagName('img'), function(img) {
+ var src = img.src;
+
+ if (!Env.fileApi) {
+ return false;
+ }
+
+ if (img.hasAttribute('data-mce-bogus')) {
+ return false;
+ }
+
+ if (img.hasAttribute('data-mce-placeholder')) {
+ return false;
+ }
+
+ if (!src || src == Env.transparentSrc) {
+ return false;
+ }
+
+ if (src.indexOf('blob:') === 0) {
+ return !uploadStatus.isUploaded(src);
+ }
+
+ if (src.indexOf('data:') === 0) {
+ return predicate(img);
+ }
+
+ return false;
+ });
+
+ promises = Arr.map(images, function(img) {
+ var newPromise;
+
+ if (cachedPromises[img.src]) {
+ // Since the cached promise will return the cached image
+ // We need to wrap it and resolve with the actual image
+ return new Promise(function(resolve) {
+ cachedPromises[img.src].then(function(imageInfo) {
+ resolve({
+ image: img,
+ blobInfo: imageInfo.blobInfo
+ });
+ });
+ });
+ }
+
+ newPromise = new Promise(function(resolve) {
+ imageToBlobInfo(img, resolve);
+ }).then(function(result) {
+ delete cachedPromises[result.image.src];
+ return result;
+ })['catch'](function(error) {
+ delete cachedPromises[img.src];
+ return error;
+ });
+
+ cachedPromises[img.src] = newPromise;
+
+ return newPromise;
+ });
+
+ return Promise.all(promises);
+ }
+
+ return {
+ findAll: findAll
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/BlobCache.js
+
+/**
+ * BlobCache.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Hold blob info objects where a blob has extra internal information.
+ *
+ * @private
+ * @class tinymce.file.BlobCache
+ */
+define("tinymce/file/BlobCache", [
+ "tinymce/util/Arr",
+ "tinymce/util/Fun"
+], function(Arr, Fun) {
+ return function() {
+ var cache = [], constant = Fun.constant;
+
+ function create(id, blob, base64) {
+ return {
+ id: constant(id),
+ blob: constant(blob),
+ base64: constant(base64),
+ blobUri: constant(URL.createObjectURL(blob))
+ };
+ }
+
+ function add(blobInfo) {
+ if (!get(blobInfo.id())) {
+ cache.push(blobInfo);
+ }
+ }
+
+ function get(id) {
+ return findFirst(function(cachedBlobInfo) {
+ return cachedBlobInfo.id() === id;
+ });
+ }
+
+ function findFirst(predicate) {
+ return Arr.filter(cache, predicate)[0];
+ }
+
+ function getByUri(blobUri) {
+ return findFirst(function(blobInfo) {
+ return blobInfo.blobUri() == blobUri;
+ });
+ }
+
+ function removeByUri(blobUri) {
+ cache = Arr.filter(cache, function(blobInfo) {
+ if (blobInfo.blobUri() === blobUri) {
+ URL.revokeObjectURL(blobInfo.blobUri());
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ function destroy() {
+ Arr.each(cache, function(cachedBlobInfo) {
+ URL.revokeObjectURL(cachedBlobInfo.blobUri());
+ });
+
+ cache = [];
+ }
+
+ return {
+ create: create,
+ add: add,
+ get: get,
+ getByUri: getByUri,
+ findFirst: findFirst,
+ removeByUri: removeByUri,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/file/UploadStatus.js
+
+/**
+ * UploadStatus.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
+ *
+ * @private
+ * @class tinymce.file.UploadStatus
+ */
+define("tinymce/file/UploadStatus", [
+], function() {
+ return function() {
+ var PENDING = 1, UPLOADED = 2;
+ var blobUriStatuses = {};
+
+ function createStatus(status, resultUri) {
+ return {
+ status: status,
+ resultUri: resultUri
+ };
+ }
+
+ function hasBlobUri(blobUri) {
+ return blobUri in blobUriStatuses;
+ }
+
+ function getResultUri(blobUri) {
+ var result = blobUriStatuses[blobUri];
+
+ return result ? result.resultUri : null;
+ }
+
+ function isPending(blobUri) {
+ return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
+ }
+
+ function isUploaded(blobUri) {
+ return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
+ }
+
+ function markPending(blobUri) {
+ blobUriStatuses[blobUri] = createStatus(PENDING, null);
+ }
+
+ function markUploaded(blobUri, resultUri) {
+ blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
+ }
+
+ function removeFailed(blobUri) {
+ delete blobUriStatuses[blobUri];
+ }
+
+ function destroy() {
+ blobUriStatuses = {};
+ }
+
+ return {
+ hasBlobUri: hasBlobUri,
+ getResultUri: getResultUri,
+ isPending: isPending,
+ isUploaded: isUploaded,
+ markPending: markPending,
+ markUploaded: markUploaded,
+ removeFailed: removeFailed,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/EditorUpload.js
+
+/**
+ * EditorUpload.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles image uploads, updates undo stack and patches over various internal functions.
+ *
+ * @private
+ * @class tinymce.EditorUpload
+ */
+define("tinymce/EditorUpload", [
+ "tinymce/util/Arr",
+ "tinymce/file/Uploader",
+ "tinymce/file/ImageScanner",
+ "tinymce/file/BlobCache",
+ "tinymce/file/UploadStatus"
+], function(Arr, Uploader, ImageScanner, BlobCache, UploadStatus) {
+ return function(editor) {
+ var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings;
+ var uploadStatus = new UploadStatus();
+
+ function aliveGuard(callback) {
+ return function(result) {
+ if (editor.selection) {
+ return callback(result);
+ }
+
+ return [];
+ };
+ }
+
+ // Replaces strings without regexps to avoid FF regexp to big issue
+ function replaceString(content, search, replace) {
+ var index = 0;
+
+ do {
+ index = content.indexOf(search, index);
+
+ if (index !== -1) {
+ content = content.substring(0, index) + replace + content.substr(index + search.length);
+ index += replace.length - search.length + 1;
+ }
+ } while (index !== -1);
+
+ return content;
+ }
+
+ function replaceImageUrl(content, targetUrl, replacementUrl) {
+ content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
+ content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
+
+ return content;
+ }
+
+ function replaceUrlInUndoStack(targetUrl, replacementUrl) {
+ Arr.each(editor.undoManager.data, function(level) {
+ level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
+ });
+ }
+
+ function openNotification() {
+ return editor.notificationManager.open({
+ text: editor.translate('Image uploading...'),
+ type: 'info',
+ timeout: -1,
+ progressBar: true
+ });
+ }
+
+ function replaceImageUri(image, resultUri) {
+ blobCache.removeByUri(image.src);
+ replaceUrlInUndoStack(image.src, resultUri);
+
+ editor.$(image).attr({
+ src: resultUri,
+ 'data-mce-src': editor.convertURL(resultUri, 'src')
+ });
+ }
+
+ function uploadImages(callback) {
+ if (!uploader) {
+ uploader = new Uploader(uploadStatus, {
+ url: settings.images_upload_url,
+ basePath: settings.images_upload_base_path,
+ credentials: settings.images_upload_credentials,
+ handler: settings.images_upload_handler
+ });
+ }
+
+ return scanForImages().then(aliveGuard(function(imageInfos) {
+ var blobInfos;
+
+ blobInfos = Arr.map(imageInfos, function(imageInfo) {
+ return imageInfo.blobInfo;
+ });
+
+ return uploader.upload(blobInfos, openNotification).then(aliveGuard(function(result) {
+ result = Arr.map(result, function(uploadInfo, index) {
+ var image = imageInfos[index].image;
+
+ if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) {
+ replaceImageUri(image, uploadInfo.url);
+ }
+
+ return {
+ element: image,
+ status: uploadInfo.status
+ };
+ });
+
+ if (callback) {
+ callback(result);
+ }
+
+ return result;
+ }));
+ }));
+ }
+
+ function uploadImagesAuto(callback) {
+ if (settings.automatic_uploads !== false) {
+ return uploadImages(callback);
+ }
+ }
+
+ function isValidDataUriImage(imgElm) {
+ return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
+ }
+
+ function scanForImages() {
+ if (!imageScanner) {
+ imageScanner = new ImageScanner(uploadStatus, blobCache);
+ }
+
+ return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function(result) {
+ Arr.each(result, function(resultItem) {
+ replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
+ resultItem.image.src = resultItem.blobInfo.blobUri();
+ resultItem.image.removeAttribute('data-mce-src');
+ });
+
+ return result;
+ }));
+ }
+
+ function destroy() {
+ blobCache.destroy();
+ uploadStatus.destroy();
+ imageScanner = uploader = null;
+ }
+
+ function replaceBlobUris(content) {
+ return content.replace(/src="(blob:[^"]+)"/g, function(match, blobUri) {
+ var resultUri = uploadStatus.getResultUri(blobUri);
+
+ if (resultUri) {
+ return 'src="' + resultUri + '"';
+ }
+
+ var blobInfo = blobCache.getByUri(blobUri);
+
+ if (!blobInfo) {
+ blobInfo = Arr.reduce(editor.editorManager.editors, function(result, editor) {
+ return result || editor.editorUpload.blobCache.getByUri(blobUri);
+ }, null);
+ }
+
+ if (blobInfo) {
+ return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"';
+ }
+
+ return match;
+ });
+ }
+
+ editor.on('setContent', function() {
+ if (editor.settings.automatic_uploads !== false) {
+ uploadImagesAuto();
+ } else {
+ scanForImages();
+ }
+ });
+
+ editor.on('RawSaveContent', function(e) {
+ e.content = replaceBlobUris(e.content);
+ });
+
+ editor.on('getContent', function(e) {
+ if (e.source_view || e.format == 'raw') {
+ return;
+ }
+
+ e.content = replaceBlobUris(e.content);
+ });
+
+ editor.on('PostRender', function() {
+ editor.parser.addNodeFilter('img', function(images) {
+ Arr.each(images, function(img) {
+ var src = img.attr('src');
+
+ if (blobCache.getByUri(src)) {
+ return;
+ }
+
+ var resultUri = uploadStatus.getResultUri(src);
+ if (resultUri) {
+ img.attr('src', resultUri);
+ }
+ });
+ });
+ });
+
+ return {
+ blobCache: blobCache,
+ uploadImages: uploadImages,
+ uploadImagesAuto: uploadImagesAuto,
+ scanForImages: scanForImages,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/caret/FakeCaret.js
+
+/**
+ * FakeCaret.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic for rendering a fake visual caret.
+ *
+ * @private
+ * @class tinymce.caret.FakeCaret
+ */
+define("tinymce/caret/FakeCaret", [
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretPosition",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/RangeUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/geom/ClientRect",
+ "tinymce/util/Delay"
+], function(CaretContainer, CaretPosition, NodeType, RangeUtils, $, ClientRect, Delay) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse;
+
+ return function(rootNode, isBlock) {
+ var cursorInterval, $lastVisualCaret, caretContainerNode;
+
+ function getAbsoluteClientRect(node, before) {
+ var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before),
+ docElm, scrollX, scrollY, margin, rootRect;
+
+ if (rootNode.tagName == 'BODY') {
+ docElm = rootNode.ownerDocument.documentElement;
+ scrollX = rootNode.scrollLeft || docElm.scrollLeft;
+ scrollY = rootNode.scrollTop || docElm.scrollTop;
+ } else {
+ rootRect = rootNode.getBoundingClientRect();
+ scrollX = rootNode.scrollLeft - rootRect.left;
+ scrollY = rootNode.scrollTop - rootRect.top;
+ }
+
+ clientRect.left += scrollX;
+ clientRect.right += scrollX;
+ clientRect.top += scrollY;
+ clientRect.bottom += scrollY;
+ clientRect.width = 1;
+
+ margin = node.offsetWidth - node.clientWidth;
+
+ if (margin > 0) {
+ if (before) {
+ margin *= -1;
+ }
+
+ clientRect.left += margin;
+ clientRect.right += margin;
+ }
+
+ return clientRect;
+ }
+
+ function trimInlineCaretContainers() {
+ var contentEditableFalseNodes, node, sibling, i, data;
+
+ contentEditableFalseNodes = $('*[contentEditable=false]', rootNode);
+ for (i = 0; i < contentEditableFalseNodes.length; i++) {
+ node = contentEditableFalseNodes[i];
+
+ sibling = node.previousSibling;
+ if (CaretContainer.endsWithCaretContainer(sibling)) {
+ data = sibling.data;
+
+ if (data.length == 1) {
+ sibling.parentNode.removeChild(sibling);
+ } else {
+ sibling.deleteData(data.length - 1, 1);
+ }
+ }
+
+ sibling = node.nextSibling;
+ if (CaretContainer.startsWithCaretContainer(sibling)) {
+ data = sibling.data;
+
+ if (data.length == 1) {
+ sibling.parentNode.removeChild(sibling);
+ } else {
+ sibling.deleteData(0, 1);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function show(before, node) {
+ var clientRect, rng, container;
+
+ hide();
+
+ if (isBlock(node)) {
+ caretContainerNode = CaretContainer.insertBlock('p', node, before);
+ clientRect = getAbsoluteClientRect(node, before);
+ $(caretContainerNode).css('top', clientRect.top);
+
+ $lastVisualCaret = $('
').css(clientRect).appendTo(rootNode);
+
+ if (before) {
+ $lastVisualCaret.addClass('mce-visual-caret-before');
+ }
+
+ startBlink();
+
+ rng = node.ownerDocument.createRange();
+ container = caretContainerNode.firstChild;
+ rng.setStart(container, 0);
+ rng.setEnd(container, 1);
+ } else {
+ caretContainerNode = CaretContainer.insertInline(node, before);
+ rng = node.ownerDocument.createRange();
+
+ if (isContentEditableFalse(caretContainerNode.nextSibling)) {
+ rng.setStart(caretContainerNode, 0);
+ rng.setEnd(caretContainerNode, 0);
+ } else {
+ rng.setStart(caretContainerNode, 1);
+ rng.setEnd(caretContainerNode, 1);
+ }
+
+ return rng;
+ }
+
+ return rng;
+ }
+
+ function hide() {
+ trimInlineCaretContainers();
+
+ if (caretContainerNode) {
+ CaretContainer.remove(caretContainerNode);
+ caretContainerNode = null;
+ }
+
+ if ($lastVisualCaret) {
+ $lastVisualCaret.remove();
+ $lastVisualCaret = null;
+ }
+
+ clearInterval(cursorInterval);
+ }
+
+ function startBlink() {
+ cursorInterval = Delay.setInterval(function() {
+ $('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden');
+ }, 500);
+ }
+
+ function destroy() {
+ Delay.clearInterval(cursorInterval);
+ }
+
+ function getCss() {
+ return (
+ '.mce-visual-caret {' +
+ 'position: absolute;' +
+ 'background-color: black;' +
+ 'background-color: currentcolor;' +
+ '}' +
+ '.mce-visual-caret-hidden {' +
+ 'display: none;' +
+ '}' +
+ '*[data-mce-caret] {' +
+ 'position: absolute;' +
+ 'left: -1000px;' +
+ 'right: auto;' +
+ 'top: 0;' +
+ 'margin: 0;' +
+ 'padding: 0;' +
+ '}'
+ );
+ }
+
+ return {
+ show: show,
+ hide: hide,
+ getCss: getCss,
+ destroy: destroy
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/dom/Dimensions.js
+
+/**
+ * Dimensions.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module measures nodes and returns client rects. The client rects has an
+ * extra node property.
+ *
+ * @private
+ * @class tinymce.dom.Dimensions
+ */
+define("tinymce/dom/Dimensions", [
+ "tinymce/util/Arr",
+ "tinymce/dom/NodeType",
+ "tinymce/geom/ClientRect"
+], function(Arr, NodeType, ClientRect) {
+
+ function getClientRects(node) {
+ function toArrayWithNode(clientRects) {
+ return Arr.map(clientRects, function(clientRect) {
+ clientRect = ClientRect.clone(clientRect);
+ clientRect.node = node;
+
+ return clientRect;
+ });
+ }
+
+ if (Arr.isArray(node)) {
+ return Arr.reduce(node, function(result, node) {
+ return result.concat(getClientRects(node));
+ }, []);
+ }
+
+ if (NodeType.isElement(node)) {
+ return toArrayWithNode(node.getClientRects());
+ }
+
+ if (NodeType.isText(node)) {
+ var rng = node.ownerDocument.createRange();
+
+ rng.setStart(node, 0);
+ rng.setEnd(node, node.data.length);
+
+ return toArrayWithNode(rng.getClientRects());
+ }
+ }
+
+ return {
+ /**
+ * Returns the client rects for a specific node.
+ *
+ * @method getClientRects
+ * @param {Array/DOMNode} node Node or array of nodes to get client rects on.
+ * @param {Array} Array of client rects with a extra node property.
+ */
+ getClientRects: getClientRects
+ };
+});
+
+// Included from: js/tinymce/classes/caret/LineWalker.js
+
+/**
+ * LineWalker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module lets you walk the document line by line
+ * returing nodes and client rects for each line.
+ *
+ * @private
+ * @class tinymce.caret.LineWalker
+ */
+define("tinymce/caret/LineWalker", [
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/dom/Dimensions",
+ "tinymce/caret/CaretCandidate",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition",
+ "tinymce/geom/ClientRect"
+], function(Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) {
+ var curry = Fun.curry;
+
+ function findUntil(direction, rootNode, predicateFn, node) {
+ while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
+ if (predicateFn(node)) {
+ return;
+ }
+ }
+ }
+
+ function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) {
+ var line = 0, node, result = [], targetClientRect;
+
+ function add(node) {
+ var i, clientRect, clientRects;
+
+ clientRects = Dimensions.getClientRects(node);
+ if (direction == -1) {
+ clientRects = clientRects.reverse();
+ }
+
+ for (i = 0; i < clientRects.length; i++) {
+ clientRect = clientRects[i];
+ if (isBeflowFn(clientRect, targetClientRect)) {
+ continue;
+ }
+
+ if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) {
+ line++;
+ }
+
+ clientRect.line = line;
+
+ if (predicateFn(clientRect)) {
+ return true;
+ }
+
+ result.push(clientRect);
+ }
+ }
+
+ targetClientRect = Arr.last(caretPosition.getClientRects());
+ if (!targetClientRect) {
+ return result;
+ }
+
+ node = caretPosition.getNode();
+ add(node);
+ findUntil(direction, rootNode, add, node);
+
+ return result;
+ }
+
+ function aboveLineNumber(lineNumber, clientRect) {
+ return clientRect.line > lineNumber;
+ }
+
+ function isLine(lineNumber, clientRect) {
+ return clientRect.line === lineNumber;
+ }
+
+ var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow);
+ var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove);
+
+ function positionsUntil(direction, rootNode, predicateFn, node) {
+ var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn,
+ caretPosition, result = [], line = 0, clientRect, targetClientRect;
+
+ function getClientRect(caretPosition) {
+ if (direction == 1) {
+ return Arr.last(caretPosition.getClientRects());
+ }
+
+ return Arr.last(caretPosition.getClientRects());
+ }
+
+ if (direction == 1) {
+ walkFn = caretWalker.next;
+ isBelowFn = ClientRect.isBelow;
+ isAboveFn = ClientRect.isAbove;
+ caretPosition = CaretPosition.after(node);
+ } else {
+ walkFn = caretWalker.prev;
+ isBelowFn = ClientRect.isAbove;
+ isAboveFn = ClientRect.isBelow;
+ caretPosition = CaretPosition.before(node);
+ }
+
+ targetClientRect = getClientRect(caretPosition);
+
+ do {
+ if (!caretPosition.isVisible()) {
+ continue;
+ }
+
+ clientRect = getClientRect(caretPosition);
+
+ if (isAboveFn(clientRect, targetClientRect)) {
+ continue;
+ }
+
+ if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) {
+ line++;
+ }
+
+ clientRect = ClientRect.clone(clientRect);
+ clientRect.position = caretPosition;
+ clientRect.line = line;
+
+ if (predicateFn(clientRect)) {
+ return result;
+ }
+
+ result.push(clientRect);
+ } while ((caretPosition = walkFn(caretPosition)));
+
+ return result;
+ }
+
+ return {
+ upUntil: upUntil,
+ downUntil: downUntil,
+
+ /**
+ * Find client rects with line and caret position until the predicate returns true.
+ *
+ * @method positionsUntil
+ * @param {Number} direction Direction forward/backward 1/-1.
+ * @param {DOMNode} rootNode Root node to walk within.
+ * @param {function} predicateFn Gets the client rect as it's input.
+ * @param {DOMNode} node Node to start walking from.
+ * @return {Array} Array of client rects with line and position properties.
+ */
+ positionsUntil: positionsUntil,
+
+ isAboveLine: curry(aboveLineNumber),
+ isLine: curry(isLine)
+ };
+});
+
+// Included from: js/tinymce/classes/caret/LineUtils.js
+
+/**
+ * LineUtils.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Utility functions for working with lines.
+ *
+ * @private
+ * @class tinymce.caret.LineUtils
+ */
+define("tinymce/caret/LineUtils", [
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/Dimensions",
+ "tinymce/geom/ClientRect",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/CaretCandidate"
+], function(Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse,
+ findNode = CaretUtils.findNode,
+ curry = Fun.curry;
+
+ function distanceToRectLeft(clientRect, clientX) {
+ return Math.abs(clientRect.left - clientX);
+ }
+
+ function distanceToRectRight(clientRect, clientX) {
+ return Math.abs(clientRect.right - clientX);
+ }
+
+ function findClosestClientRect(clientRects, clientX) {
+ function isInside(clientX, clientRect) {
+ return clientX >= clientRect.left && clientX <= clientRect.right;
+ }
+
+ return Arr.reduce(clientRects, function(oldClientRect, clientRect) {
+ var oldDistance, newDistance;
+
+ oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX));
+ newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX));
+
+ if (isInside(clientX, clientRect)) {
+ return clientRect;
+ }
+
+ if (isInside(clientX, oldClientRect)) {
+ return oldClientRect;
+ }
+
+ // cE=false has higher priority
+ if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) {
+ return clientRect;
+ }
+
+ if (newDistance < oldDistance) {
+ return clientRect;
+ }
+
+ return oldClientRect;
+ });
+ }
+
+ function walkUntil(direction, rootNode, predicateFn, node) {
+ while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) {
+ if (predicateFn(node)) {
+ return;
+ }
+ }
+ }
+
+ function findLineNodeRects(rootNode, targetNodeRect) {
+ var clientRects = [];
+
+ function collect(checkPosFn, node) {
+ var lineRects;
+
+ lineRects = Arr.filter(Dimensions.getClientRects(node), function(clientRect) {
+ return !checkPosFn(clientRect, targetNodeRect);
+ });
+
+ clientRects = clientRects.concat(lineRects);
+
+ return lineRects.length === 0;
+ }
+
+ clientRects.push(targetNodeRect);
+ walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node);
+ walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node);
+
+ return clientRects;
+ }
+
+ function getContentEditableFalseChildren(rootNode) {
+ return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse);
+ }
+
+ function caretInfo(clientRect, clientX) {
+ return {
+ node: clientRect.node,
+ before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX)
+ };
+ }
+
+ function closestCaret(rootNode, clientX, clientY) {
+ var contentEditableFalseNodeRects, closestNodeRect;
+
+ contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode));
+ contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function(clientRect) {
+ return clientY >= clientRect.top && clientY <= clientRect.bottom;
+ });
+
+ closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX);
+ if (closestNodeRect) {
+ closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX);
+ if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) {
+ return caretInfo(closestNodeRect, clientX);
+ }
+ }
+
+ return null;
+ }
+
+ return {
+ findClosestClientRect: findClosestClientRect,
+ findLineNodeRects: findLineNodeRects,
+ closestCaret: closestCaret
+ };
+});
+
+// Included from: js/tinymce/classes/dom/MousePosition.js
+
+/**
+ * MousePosition.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module calculates an absolute coordinate inside the editor body for both local and global mouse events.
+ *
+ * @private
+ * @class tinymce.dom.MousePosition
+ */
+define("tinymce/dom/MousePosition", [
+], function() {
+ var getAbsolutePosition = function (elm) {
+ var doc, docElem, win, clientRect;
+
+ clientRect = elm.getBoundingClientRect();
+ doc = elm.ownerDocument;
+ docElem = doc.documentElement;
+ win = doc.defaultView;
+
+ return {
+ top: clientRect.top + win.pageYOffset - docElem.clientTop,
+ left: clientRect.left + win.pageXOffset - docElem.clientLeft
+ };
+ };
+
+ var getBodyPosition = function (editor) {
+ return editor.inline ? getAbsolutePosition(editor.getBody()) : {left: 0, top: 0};
+ };
+
+ var getScrollPosition = function (editor) {
+ var body = editor.getBody();
+ return editor.inline ? {left: body.scrollLeft, top: body.scrollTop} : {left: 0, top: 0};
+ };
+
+ var getBodyScroll = function (editor) {
+ var body = editor.getBody(), docElm = editor.getDoc().documentElement;
+ var inlineScroll = {left: body.scrollLeft, top: body.scrollTop};
+ var iframeScroll = {left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop};
+
+ return editor.inline ? inlineScroll : iframeScroll;
+ };
+
+ var getMousePosition = function (editor, event) {
+ if (event.target.ownerDocument !== editor.getDoc()) {
+ var iframePosition = getAbsolutePosition(editor.getContentAreaContainer());
+ var scrollPosition = getBodyScroll(editor);
+
+ return {
+ left: event.pageX - iframePosition.left + scrollPosition.left,
+ top: event.pageY - iframePosition.top + scrollPosition.top
+ };
+ }
+
+ return {
+ left: event.pageX,
+ top: event.pageY
+ };
+ };
+
+ var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) {
+ return {
+ pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left,
+ pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top
+ };
+ };
+
+ var calc = function (editor, event) {
+ return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event));
+ };
+
+ return {
+ calc: calc
+ };
+});
+
+// Included from: js/tinymce/classes/DragDropOverrides.js
+
+/**
+ * DragDropOverrides.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic overriding the drag/drop logic of the editor.
+ *
+ * @private
+ * @class tinymce.DragDropOverrides
+ */
+define("tinymce/DragDropOverrides", [
+ "tinymce/dom/NodeType",
+ "tinymce/util/Arr",
+ "tinymce/util/Fun",
+ "tinymce/util/Delay",
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/MousePosition"
+], function(
+ NodeType, Arr, Fun, Delay, DOMUtils, MousePosition
+) {
+ var isContentEditableFalse = NodeType.isContentEditableFalse,
+ isContentEditableTrue = NodeType.isContentEditableTrue;
+
+ var isDraggable = function (elm) {
+ return isContentEditableFalse(elm);
+ };
+
+ var isValidDropTarget = function (editor, targetElement, dragElement) {
+ if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
+ return false;
+ }
+
+ if (isContentEditableFalse(targetElement)) {
+ return false;
+ }
+
+ return true;
+ };
+
+ var cloneElement = function (elm) {
+ var cloneElm = elm.cloneNode(true);
+ cloneElm.removeAttribute('data-mce-selected');
+ return cloneElm;
+ };
+
+ var createGhost = function (editor, elm, width, height) {
+ var clonedElm = elm.cloneNode(true);
+
+ editor.dom.setStyles(clonedElm, {width: width, height: height});
+ editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);
+
+ var ghostElm = editor.dom.create('div', {
+ 'class': 'mce-drag-container',
+ 'data-mce-bogus': 'all',
+ unselectable: 'on',
+ contenteditable: 'false'
+ });
+
+ editor.dom.setStyles(ghostElm, {
+ position: 'absolute',
+ opacity: 0.5,
+ overflow: 'hidden',
+ border: 0,
+ padding: 0,
+ margin: 0,
+ width: width,
+ height: height
+ });
+
+ editor.dom.setStyles(clonedElm, {
+ margin: 0,
+ boxSizing: 'border-box'
+ });
+
+ ghostElm.appendChild(clonedElm);
+
+ return ghostElm;
+ };
+
+ var appendGhostToBody = function (ghostElm, bodyElm) {
+ if (ghostElm.parentNode !== bodyElm) {
+ bodyElm.appendChild(ghostElm);
+ }
+ };
+
+ var moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
+ var overflowX = 0, overflowY = 0;
+
+ ghostElm.style.left = position.pageX + 'px';
+ ghostElm.style.top = position.pageY + 'px';
+
+ if (position.pageX + width > maxX) {
+ overflowX = (position.pageX + width) - maxX;
+ }
+
+ if (position.pageY + height > maxY) {
+ overflowY = (position.pageY + height) - maxY;
+ }
+
+ ghostElm.style.width = (width - overflowX) + 'px';
+ ghostElm.style.height = (height - overflowY) + 'px';
+ };
+
+ var removeElement = function (elm) {
+ if (elm && elm.parentNode) {
+ elm.parentNode.removeChild(elm);
+ }
+ };
+
+ var isLeftMouseButtonPressed = function (e) {
+ return e.button === 0;
+ };
+
+ var hasDraggableElement = function (state) {
+ return state.element;
+ };
+
+ var applyRelPos = function (state, position) {
+ return {
+ pageX: position.pageX - state.relX,
+ pageY: position.pageY + 5
+ };
+ };
+
+ var start = function (state, editor) {
+ return function (e) {
+ if (isLeftMouseButtonPressed(e)) {
+ var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));
+
+ if (isDraggable(ceElm)) {
+ var elmPos = editor.dom.getPos(ceElm);
+ var bodyElm = editor.getBody();
+ var docElm = editor.getDoc().documentElement;
+
+ state.element = ceElm;
+ state.screenX = e.screenX;
+ state.screenY = e.screenY;
+ state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2;
+ state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2;
+ state.relX = e.pageX - elmPos.x;
+ state.relY = e.pageY - elmPos.y;
+ state.width = ceElm.offsetWidth;
+ state.height = ceElm.offsetHeight;
+ state.ghost = createGhost(editor, ceElm, state.width, state.height);
+ }
+ }
+ };
+ };
+
+ var move = function (state, editor) {
+ // Reduces laggy drag behavior on Gecko
+ var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
+ editor._selectionOverrides.hideFakeCaret();
+ editor.selection.placeCaretAt(clientX, clientY);
+ }, 0);
+
+ return function (e) {
+ var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));
+
+ if (hasDraggableElement(state) && !state.dragging && movement > 10) {
+ var args = editor.fire('dragstart', {target: state.element});
+ if (args.isDefaultPrevented()) {
+ return;
+ }
+
+ state.dragging = true;
+ editor.focus();
+ }
+
+ if (state.dragging) {
+ var targetPos = applyRelPos(state, MousePosition.calc(editor, e));
+
+ appendGhostToBody(state.ghost, editor.getBody());
+ moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY);
+
+ throttledPlaceCaretAt(e.clientX, e.clientY);
+ }
+ };
+ };
+
+ var drop = function (state, editor) {
+ return function (e) {
+ if (state.dragging) {
+ if (isValidDropTarget(editor, editor.selection.getNode(), state.element)) {
+ var targetClone = cloneElement(state.element);
+
+ var args = editor.fire('drop', {
+ targetClone: targetClone,
+ clientX: e.clientX,
+ clientY: e.clientY
+ });
+
+ if (!args.isDefaultPrevented()) {
+ targetClone = args.targetClone;
+
+ editor.undoManager.transact(function() {
+ removeElement(state.element);
+ editor.insertContent(editor.dom.getOuterHTML(targetClone));
+ editor._selectionOverrides.hideFakeCaret();
+ });
+ }
+ }
+ }
+
+ removeDragState(state);
+ };
+ };
+
+ var stop = function (state, editor) {
+ return function () {
+ removeDragState(state);
+ if (state.dragging) {
+ editor.fire('dragend');
+ }
+ };
+ };
+
+ var removeDragState = function (state) {
+ state.dragging = false;
+ state.element = null;
+ removeElement(state.ghost);
+ };
+
+ var bindFakeDragEvents = function (editor) {
+ var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument;
+
+ pageDom = DOMUtils.DOM;
+ rootDocument = document;
+ dragStartHandler = start(state, editor);
+ dragHandler = move(state, editor);
+ dropHandler = drop(state, editor);
+ dragEndHandler = stop(state, editor);
+
+ editor.on('mousedown', dragStartHandler);
+ editor.on('mousemove', dragHandler);
+ editor.on('mouseup', dropHandler);
+
+ pageDom.bind(rootDocument, 'mousemove', dragHandler);
+ pageDom.bind(rootDocument, 'mouseup', dragEndHandler);
+
+ editor.on('remove', function () {
+ pageDom.unbind(rootDocument, 'mousemove', dragHandler);
+ pageDom.unbind(rootDocument, 'mouseup', dragEndHandler);
+ });
+ };
+
+ var blockIeDrop = function (editor) {
+ editor.on('drop', function(e) {
+ // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead
+ var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;
+
+ if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
+ e.preventDefault();
+ }
+ });
+ };
+
+ var init = function (editor) {
+ bindFakeDragEvents(editor);
+ blockIeDrop(editor);
+ };
+
+ return {
+ init: init
+ };
+});
+
+// Included from: js/tinymce/classes/SelectionOverrides.js
+
+/**
+ * SelectionOverrides.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This module contains logic overriding the selection with keyboard/mouse
+ * around contentEditable=false regions.
+ *
+ * @example
+ * // Disable the default cE=false selection
+ * tinymce.activeEditor.on('ShowCaret BeforeObjectSelected', function(e) {
+ * e.preventDefault();
+ * });
+ *
+ * @private
+ * @class tinymce.SelectionOverrides
+ */
+define("tinymce/SelectionOverrides", [
+ "tinymce/Env",
+ "tinymce/caret/CaretWalker",
+ "tinymce/caret/CaretPosition",
+ "tinymce/caret/CaretContainer",
+ "tinymce/caret/CaretUtils",
+ "tinymce/caret/FakeCaret",
+ "tinymce/caret/LineWalker",
+ "tinymce/caret/LineUtils",
+ "tinymce/dom/NodeType",
+ "tinymce/dom/RangeUtils",
+ "tinymce/geom/ClientRect",
+ "tinymce/util/VK",
+ "tinymce/util/Fun",
+ "tinymce/util/Arr",
+ "tinymce/util/Delay",
+ "tinymce/DragDropOverrides",
+ "tinymce/text/Zwsp"
+], function(
+ Env, CaretWalker, CaretPosition, CaretContainer, CaretUtils, FakeCaret, LineWalker,
+ LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides, Zwsp
+) {
+ var curry = Fun.curry,
+ isContentEditableTrue = NodeType.isContentEditableTrue,
+ isContentEditableFalse = NodeType.isContentEditableFalse,
+ isElement = NodeType.isElement,
+ isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse,
+ isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse,
+ getSelectedNode = RangeUtils.getSelectedNode;
+
+ function getVisualCaretPosition(walkFn, caretPosition) {
+ while ((caretPosition = walkFn(caretPosition))) {
+ if (caretPosition.isVisible()) {
+ return caretPosition;
+ }
+ }
+
+ return caretPosition;
+ }
+
+ function SelectionOverrides(editor) {
+ var rootNode = editor.getBody(), caretWalker = new CaretWalker(rootNode);
+ var getNextVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.next);
+ var getPrevVisualCaretPosition = curry(getVisualCaretPosition, caretWalker.prev),
+ fakeCaret = new FakeCaret(editor.getBody(), isBlock),
+ realSelectionId = 'sel-' + editor.dom.uniqueId(),
+ selectedContentEditableNode, $ = editor.$;
+
+ function getRealSelectionElement() {
+ var container = editor.dom.get(realSelectionId);
+ return container ? container.getElementsByTagName('*')[0] : container;
+ }
+
+ function isBlock(node) {
+ return editor.dom.isBlock(node);
+ }
+
+ function setRange(range) {
+ //console.log('setRange', range);
+ if (range) {
+ editor.selection.setRng(range);
+ }
+ }
+
+ function getRange() {
+ return editor.selection.getRng();
+ }
+
+ function scrollIntoView(node, alignToTop) {
+ editor.selection.scrollIntoView(node, alignToTop);
+ }
+
+ function showCaret(direction, node, before) {
+ var e;
+
+ e = editor.fire('ShowCaret', {
+ target: node,
+ direction: direction,
+ before: before
+ });
+
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ scrollIntoView(node, direction === -1);
+
+ return fakeCaret.show(before, node);
+ }
+
+ function selectNode(node) {
+ var e;
+
+ fakeCaret.hide();
+
+ e = editor.fire('BeforeObjectSelected', {target: node});
+ if (e.isDefaultPrevented()) {
+ return null;
+ }
+
+ return getNodeRange(node);
+ }
+
+ function getNodeRange(node) {
+ var rng = node.ownerDocument.createRange();
+
+ rng.selectNode(node);
+
+ return rng;
+ }
+
+ function isMoveInsideSameBlock(fromCaretPosition, toCaretPosition) {
+ var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition);
+
+ // Handle bogus BR
abc|
+ if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) {
+ return true;
+ }
+
+ return inSameBlock;
+ }
+
+ function getNormalizedRangeEndPoint(direction, range) {
+ range = CaretUtils.normalizeRange(direction, rootNode, range);
+
+ if (direction == -1) {
+ return CaretPosition.fromRangeStart(range);
+ }
+
+ return CaretPosition.fromRangeEnd(range);
+ }
+
+ function isRangeInCaretContainerBlock(range) {
+ return CaretContainer.isCaretContainerBlock(range.startContainer);
+ }
+
+ function moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
+ var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;
+
+ if (!range.collapsed) {
+ node = getSelectedNode(range);
+ if (isContentEditableFalse(node)) {
+ return showCaret(direction, node, direction == -1);
+ }
+ }
+
+ rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+
+ if (isBeforeContentEditableFalseFn(caretPosition)) {
+ return selectNode(caretPosition.getNode(direction == -1));
+ }
+
+ caretPosition = getNextPosFn(caretPosition);
+ if (!caretPosition) {
+ if (rangeIsInContainerBlock) {
+ return range;
+ }
+
+ return null;
+ }
+
+ if (isBeforeContentEditableFalseFn(caretPosition)) {
+ return showCaret(direction, caretPosition.getNode(direction == -1), direction == 1);
+ }
+
+ // Peek ahead for handling of ab|c
-> abc|
+ peekCaretPosition = getNextPosFn(caretPosition);
+ if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
+ if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
+ return showCaret(direction, peekCaretPosition.getNode(direction == -1), direction == 1);
+ }
+ }
+
+ if (rangeIsInContainerBlock) {
+ return renderRangeCaret(caretPosition.toRange());
+ }
+
+ return null;
+ }
+
+ function moveToCeFalseVertically(direction, walkerFn, range) {
+ var caretPosition, linePositions, nextLinePositions,
+ closestNextLineRect, caretClientRect, clientX,
+ dist1, dist2, contentEditableFalseNode;
+
+ contentEditableFalseNode = getSelectedNode(range);
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+ linePositions = walkerFn(rootNode, LineWalker.isAboveLine(1), caretPosition);
+ nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
+ caretClientRect = Arr.last(caretPosition.getClientRects());
+
+ if (isBeforeContentEditableFalse(caretPosition)) {
+ contentEditableFalseNode = caretPosition.getNode();
+ }
+
+ if (isAfterContentEditableFalse(caretPosition)) {
+ contentEditableFalseNode = caretPosition.getNode(true);
+ }
+
+ if (!caretClientRect) {
+ return null;
+ }
+
+ clientX = caretClientRect.left;
+
+ closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
+ if (closestNextLineRect) {
+ if (isContentEditableFalse(closestNextLineRect.node)) {
+ dist1 = Math.abs(clientX - closestNextLineRect.left);
+ dist2 = Math.abs(clientX - closestNextLineRect.right);
+
+ return showCaret(direction, closestNextLineRect.node, dist1 < dist2);
+ }
+ }
+
+ if (contentEditableFalseNode) {
+ var caretPositions = LineWalker.positionsUntil(direction, rootNode, LineWalker.isAboveLine(1), contentEditableFalseNode);
+
+ closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
+ if (closestNextLineRect) {
+ return renderRangeCaret(closestNextLineRect.position.toRange());
+ }
+
+ closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
+ if (closestNextLineRect) {
+ return renderRangeCaret(closestNextLineRect.position.toRange());
+ }
+ }
+ }
+
+ function exitPreBlock(direction, range) {
+ var pre, caretPos, newBlock;
+
+ function createTextBlock() {
+ var textBlock = editor.dom.create(editor.settings.forced_root_block);
+
+ if (!Env.ie || Env.ie >= 11) {
+ textBlock.innerHTML = '
';
+ }
+
+ return textBlock;
+ }
+
+ if (range.collapsed && editor.settings.forced_root_block) {
+ pre = editor.dom.getParent(range.startContainer, 'PRE');
+ if (!pre) {
+ return;
+ }
+
+ if (direction == 1) {
+ caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
+ } else {
+ caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
+ }
+
+ if (!caretPos) {
+ newBlock = createTextBlock();
+
+ if (direction == 1) {
+ editor.$(pre).after(newBlock);
+ } else {
+ editor.$(pre).before(newBlock);
+ }
+
+ editor.selection.select(newBlock, true);
+ editor.selection.collapse();
+ }
+ }
+ }
+
+ function moveH(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
+ var newRange;
+
+ newRange = moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ newRange = exitPreBlock(direction, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ return null;
+ }
+
+ function moveV(direction, walkerFn, range) {
+ var newRange;
+
+ newRange = moveToCeFalseVertically(direction, walkerFn, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ newRange = exitPreBlock(direction, range);
+ if (newRange) {
+ return newRange;
+ }
+
+ return null;
+ }
+
+ function getBlockCaretContainer() {
+ return $('*[data-mce-caret]')[0];
+ }
+
+ function showBlockCaretContainer(blockCaretContainer) {
+ blockCaretContainer = $(blockCaretContainer);
+
+ if (blockCaretContainer.attr('data-mce-caret')) {
+ fakeCaret.hide();
+ blockCaretContainer.removeAttr('data-mce-caret');
+ blockCaretContainer.removeAttr('data-mce-bogus');
+ blockCaretContainer.removeAttr('style');
+
+ // Removes control rect on IE
+ setRange(getRange());
+ scrollIntoView(blockCaretContainer[0]);
+ }
+ }
+
+ function renderCaretAtRange(range) {
+ var caretPosition, ceRoot;
+
+ range = CaretUtils.normalizeRange(1, rootNode, range);
+ caretPosition = CaretPosition.fromRangeStart(range);
+
+ if (isContentEditableFalse(caretPosition.getNode())) {
+ return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
+ }
+
+ if (isContentEditableFalse(caretPosition.getNode(true))) {
+ return showCaret(1, caretPosition.getNode(true), false);
+ }
+
+ // TODO: Should render caret before/after depending on where you click on the page forces after now
+ ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue));
+ if (isContentEditableFalse(ceRoot)) {
+ return showCaret(1, ceRoot, false);
+ }
+
+ fakeCaret.hide();
+
+ return null;
+ }
+
+ function renderRangeCaret(range) {
+ var caretRange;
+
+ if (!range || !range.collapsed) {
+ return range;
+ }
+
+ caretRange = renderCaretAtRange(range);
+ if (caretRange) {
+ return caretRange;
+ }
+
+ return range;
+ }
+
+ function deleteContentEditableNode(node) {
+ var nextCaretPosition, prevCaretPosition, prevCeFalseElm, nextElement;
+
+ if (!isContentEditableFalse(node)) {
+ return null;
+ }
+
+ if (isContentEditableFalse(node.previousSibling)) {
+ prevCeFalseElm = node.previousSibling;
+ }
+
+ prevCaretPosition = getPrevVisualCaretPosition(CaretPosition.before(node));
+ if (!prevCaretPosition) {
+ nextCaretPosition = getNextVisualCaretPosition(CaretPosition.after(node));
+ }
+
+ if (nextCaretPosition && isElement(nextCaretPosition.getNode())) {
+ nextElement = nextCaretPosition.getNode();
+ }
+
+ CaretContainer.remove(node.previousSibling);
+ CaretContainer.remove(node.nextSibling);
+ editor.dom.remove(node);
+ clearContentEditableSelection();
+
+ if (editor.dom.isEmpty(editor.getBody())) {
+ editor.setContent('');
+ editor.focus();
+ return;
+ }
+
+ if (prevCeFalseElm) {
+ return CaretPosition.after(prevCeFalseElm).toRange();
+ }
+
+ if (nextElement) {
+ return CaretPosition.before(nextElement).toRange();
+ }
+
+ if (prevCaretPosition) {
+ return prevCaretPosition.toRange();
+ }
+
+ if (nextCaretPosition) {
+ return nextCaretPosition.toRange();
+ }
+
+ return null;
+ }
+
+ function isTextBlock(node) {
+ var textBlocks = editor.schema.getTextBlockElements();
+ return node.nodeName in textBlocks;
+ }
+
+ function isEmpty(elm) {
+ return editor.dom.isEmpty(elm);
+ }
+
+ function mergeTextBlocks(direction, fromCaretPosition, toCaretPosition) {
+ var dom = editor.dom, fromBlock, toBlock, node, ceTarget;
+
+ fromBlock = dom.getParent(fromCaretPosition.getNode(), dom.isBlock);
+ toBlock = dom.getParent(toCaretPosition.getNode(), dom.isBlock);
+
+ if (direction === -1) {
+ ceTarget = toCaretPosition.getNode(true);
+ if (isAfterContentEditableFalse(toCaretPosition) && isBlock(ceTarget)) {
+ if (isTextBlock(fromBlock)) {
+ if (isEmpty(fromBlock)) {
+ dom.remove(fromBlock);
+ }
+
+ return CaretPosition.after(ceTarget).toRange();
+ }
+
+ return deleteContentEditableNode(toCaretPosition.getNode(true));
+ }
+ } else {
+ ceTarget = fromCaretPosition.getNode();
+ if (isBeforeContentEditableFalse(fromCaretPosition) && isBlock(ceTarget)) {
+ if (isTextBlock(toBlock)) {
+ if (isEmpty(toBlock)) {
+ dom.remove(toBlock);
+ }
+
+ return CaretPosition.before(ceTarget).toRange();
+ }
+
+ return deleteContentEditableNode(fromCaretPosition.getNode());
+ }
+ }
+
+ // Verify that both blocks are text blocks
+ if (fromBlock === toBlock || !isTextBlock(fromBlock) || !isTextBlock(toBlock)) {
+ return null;
+ }
+
+ while ((node = fromBlock.firstChild)) {
+ toBlock.appendChild(node);
+ }
+
+ editor.dom.remove(fromBlock);
+
+ return toCaretPosition.toRange();
+ }
+
+ function backspaceDelete(direction, beforeFn, afterFn, range) {
+ var node, caretPosition, peekCaretPosition, newCaretPosition;
+
+ if (!range.collapsed) {
+ node = getSelectedNode(range);
+ if (isContentEditableFalse(node)) {
+ return renderRangeCaret(deleteContentEditableNode(node));
+ }
+ }
+
+ caretPosition = getNormalizedRangeEndPoint(direction, range);
+
+ if (afterFn(caretPosition) && CaretContainer.isCaretContainerBlock(range.startContainer)) {
+ newCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
+ return newCaretPosition ? renderRangeCaret(newCaretPosition.toRange()) : range;
+ }
+
+ if (beforeFn(caretPosition)) {
+ return renderRangeCaret(deleteContentEditableNode(caretPosition.getNode(direction == -1)));
+ }
+
+ peekCaretPosition = direction == -1 ? caretWalker.prev(caretPosition) : caretWalker.next(caretPosition);
+ if (beforeFn(peekCaretPosition)) {
+ if (direction === -1) {
+ return mergeTextBlocks(direction, caretPosition, peekCaretPosition);
+ }
+
+ return mergeTextBlocks(direction, peekCaretPosition, caretPosition);
+ }
+ }
+
+ function registerEvents() {
+ var right = curry(moveH, 1, getNextVisualCaretPosition, isBeforeContentEditableFalse);
+ var left = curry(moveH, -1, getPrevVisualCaretPosition, isAfterContentEditableFalse);
+ var deleteForward = curry(backspaceDelete, 1, isBeforeContentEditableFalse, isAfterContentEditableFalse);
+ var backspace = curry(backspaceDelete, -1, isAfterContentEditableFalse, isBeforeContentEditableFalse);
+ var up = curry(moveV, -1, LineWalker.upUntil);
+ var down = curry(moveV, 1, LineWalker.downUntil);
+
+ function override(evt, moveFn) {
+ var range = moveFn(getRange());
+
+ if (range && !evt.isDefaultPrevented()) {
+ evt.preventDefault();
+ setRange(range);
+ }
+ }
+
+ function getContentEditableRoot(node) {
+ var root = editor.getBody();
+
+ while (node && node != root) {
+ if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
+ return node;
+ }
+
+ node = node.parentNode;
+ }
+
+ return null;
+ }
+
+ function isXYWithinRange(clientX, clientY, range) {
+ if (range.collapsed) {
+ return false;
+ }
+
+ return Arr.reduce(range.getClientRects(), function(state, rect) {
+ return state || ClientRect.containsXY(rect, clientX, clientY);
+ }, false);
+ }
+
+ // Some browsers (Chrome) lets you place the caret after a cE=false
+ // Make sure we render the caret container in this case
+ editor.on('mouseup', function() {
+ var range = getRange();
+
+ if (range.collapsed) {
+ setRange(renderCaretAtRange(range));
+ }
+ });
+
+ editor.on('click', function(e) {
+ var contentEditableRoot;
+
+ // Prevent clicks on links in a cE=false element
+ contentEditableRoot = getContentEditableRoot(e.target);
+ if (contentEditableRoot) {
+ if (isContentEditableFalse(contentEditableRoot)) {
+ e.preventDefault();
+ editor.focus();
+ }
+ }
+ });
+
+ function handleTouchSelect(editor) {
+ var moved = false;
+
+ editor.on('touchstart', function () {
+ moved = false;
+ });
+
+ editor.on('touchmove', function () {
+ moved = true;
+ });
+
+ editor.on('touchend', function (e) {
+ var contentEditableRoot = getContentEditableRoot(e.target);
+
+ if (isContentEditableFalse(contentEditableRoot)) {
+ if (!moved) {
+ e.preventDefault();
+ setContentEditableSelection(selectNode(contentEditableRoot));
+ }
+ } else {
+ clearContentEditableSelection();
+ }
+ });
+ }
+
+ var hasNormalCaretPosition = function (elm) {
+ var caretWalker = new CaretWalker(elm);
+
+ if (!elm.firstChild) {
+ return false;
+ }
+
+ var startPos = CaretPosition.before(elm.firstChild);
+ var newPos = caretWalker.next(startPos);
+
+ return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
+ };
+
+ var isInSameBlock = function (node1, node2) {
+ var block1 = editor.dom.getParent(node1, editor.dom.isBlock);
+ var block2 = editor.dom.getParent(node2, editor.dom.isBlock);
+ return block1 === block2;
+ };
+
+ // Checks if the target node is in a block and if that block has a caret position better than the
+ // suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if
+ // they are adjacent on the vertical axis
+ var hasBetterMouseTarget = function (targetNode, caretNode) {
+ var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
+ var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);
+
+ return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
+ };
+
+ handleTouchSelect(editor);
+
+ editor.on('mousedown', function(e) {
+ var contentEditableRoot;
+
+ contentEditableRoot = getContentEditableRoot(e.target);
+ if (contentEditableRoot) {
+ if (isContentEditableFalse(contentEditableRoot)) {
+ e.preventDefault();
+ setContentEditableSelection(selectNode(contentEditableRoot));
+ } else {
+ clearContentEditableSelection();
+
+ if (!isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) {
+ editor.selection.placeCaretAt(e.clientX, e.clientY);
+ }
+ }
+ } else {
+ clearContentEditableSelection();
+ fakeCaret.hide();
+
+ var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
+ if (caretInfo) {
+ if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
+ e.preventDefault();
+ editor.getBody().focus();
+ setRange(showCaret(1, caretInfo.node, caretInfo.before));
+ }
+ }
+ }
+ });
+
+ editor.on('keydown', function(e) {
+ if (VK.modifierPressed(e)) {
+ return;
+ }
+
+ switch (e.keyCode) {
+ case VK.RIGHT:
+ override(e, right);
+ break;
+
+ case VK.DOWN:
+ override(e, down);
+ break;
+
+ case VK.LEFT:
+ override(e, left);
+ break;
+
+ case VK.UP:
+ override(e, up);
+ break;
+
+ case VK.DELETE:
+ override(e, deleteForward);
+ break;
+
+ case VK.BACKSPACE:
+ override(e, backspace);
+ break;
+
+ default:
+ if (isContentEditableFalse(editor.selection.getNode())) {
+ e.preventDefault();
+ }
+ break;
+ }
+ });
+
+ function paddEmptyContentEditableArea() {
+ var br, ceRoot = getContentEditableRoot(editor.selection.getNode());
+
+ if (isContentEditableTrue(ceRoot) && isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) {
+ br = editor.dom.create('br', {"data-mce-bogus": "1"});
+ editor.$(ceRoot).empty().append(br);
+ editor.selection.setRng(CaretPosition.before(br).toRange());
+ }
+ }
+
+ function handleBlockContainer(e) {
+ var blockCaretContainer = getBlockCaretContainer();
+
+ if (!blockCaretContainer) {
+ return;
+ }
+
+ if (e.type == 'compositionstart') {
+ e.preventDefault();
+ e.stopPropagation();
+ showBlockCaretContainer(blockCaretContainer);
+ return;
+ }
+
+ if (blockCaretContainer.innerHTML != ' ') {
+ showBlockCaretContainer(blockCaretContainer);
+ }
+ }
+
+ function handleEmptyBackspaceDelete(e) {
+ var prevent;
+
+ switch (e.keyCode) {
+ case VK.DELETE:
+ prevent = paddEmptyContentEditableArea();
+ break;
+
+ case VK.BACKSPACE:
+ prevent = paddEmptyContentEditableArea();
+ break;
+ }
+
+ if (prevent) {
+ e.preventDefault();
+ }
+ }
+
+ // Must be added to "top" since undoManager needs to be executed after
+ editor.on('keyup compositionstart', function(e) {
+ handleBlockContainer(e);
+ handleEmptyBackspaceDelete(e);
+ }, true);
+
+ editor.on('cut', function() {
+ var node = editor.selection.getNode();
+
+ if (isContentEditableFalse(node)) {
+ Delay.setEditorTimeout(editor, function() {
+ setRange(renderRangeCaret(deleteContentEditableNode(node)));
+ });
+ }
+ });
+
+ editor.on('getSelectionRange', function(e) {
+ var rng = e.range;
+
+ if (selectedContentEditableNode) {
+ if (!selectedContentEditableNode.parentNode) {
+ selectedContentEditableNode = null;
+ return;
+ }
+
+ rng = rng.cloneRange();
+ rng.selectNode(selectedContentEditableNode);
+ e.range = rng;
+ }
+ });
+
+ editor.on('setSelectionRange', function(e) {
+ var rng;
+
+ rng = setContentEditableSelection(e.range);
+ if (rng) {
+ e.range = rng;
+ }
+ });
+
+ editor.on('focus', function() {
+ // Make sure we have a proper fake caret on focus
+ Delay.setEditorTimeout(editor, function() {
+ editor.selection.setRng(renderRangeCaret(editor.selection.getRng()));
+ }, 0);
+ });
+
+ editor.on('copy', function (e) {
+ var clipboardData = e.clipboardData;
+
+ // Make sure we get proper html/text for the fake cE=false selection
+ // Doesn't work at all on Edge since it doesn't have proper clipboardData support
+ if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) {
+ var realSelectionElement = getRealSelectionElement();
+ if (realSelectionElement) {
+ e.preventDefault();
+ clipboardData.clearData();
+ clipboardData.setData('text/html', realSelectionElement.outerHTML);
+ clipboardData.setData('text/plain', realSelectionElement.outerText);
+ }
+ }
+ });
+
+ DragDropOverrides.init(editor);
+ }
+
+ function addCss() {
+ var styles = editor.contentStyles, rootClass = '.mce-content-body';
+
+ styles.push(fakeCaret.getCss());
+ styles.push(
+ rootClass + ' .mce-offscreen-selection {' +
+ 'position: absolute;' +
+ 'left: -9999999999px;' +
+ '}' +
+ rootClass + ' *[contentEditable=false] {' +
+ 'cursor: default;' +
+ '}' +
+ rootClass + ' *[contentEditable=true] {' +
+ 'cursor: text;' +
+ '}'
+ );
+ }
+
+ function isRangeInCaretContainer(rng) {
+ return CaretContainer.isCaretContainer(rng.startContainer) || CaretContainer.isCaretContainer(rng.endContainer);
+ }
+
+ function setContentEditableSelection(range) {
+ var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel,
+ startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone;
+
+ if (!range) {
+ clearContentEditableSelection();
+ return null;
+ }
+
+ if (range.collapsed) {
+ clearContentEditableSelection();
+
+ if (!isRangeInCaretContainer(range)) {
+ caretPosition = getNormalizedRangeEndPoint(1, range);
+
+ if (isContentEditableFalse(caretPosition.getNode())) {
+ return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
+ }
+
+ if (isContentEditableFalse(caretPosition.getNode(true))) {
+ return showCaret(1, caretPosition.getNode(true), false);
+ }
+ }
+
+ return null;
+ }
+
+ startContainer = range.startContainer;
+ startOffset = range.startOffset;
+ endOffset = range.endOffset;
+
+ // Normalizes [] to []
+ if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) {
+ startContainer = startContainer.parentNode;
+ startOffset = dom.nodeIndex(startContainer);
+ startContainer = startContainer.parentNode;
+ }
+
+ if (startContainer.nodeType != 1) {
+ clearContentEditableSelection();
+ return null;
+ }
+
+ if (endOffset == startOffset + 1) {
+ node = startContainer.childNodes[startOffset];
+ }
+
+ if (!isContentEditableFalse(node)) {
+ clearContentEditableSelection();
+ return null;
+ }
+
+ targetClone = origTargetClone = node.cloneNode(true);
+ e = editor.fire('ObjectSelected', {target: node, targetClone: targetClone});
+ if (e.isDefaultPrevented()) {
+ clearContentEditableSelection();
+ return null;
+ }
+
+ targetClone = e.targetClone;
+ $realSelectionContainer = $('#' + realSelectionId);
+ if ($realSelectionContainer.length === 0) {
+ $realSelectionContainer = $(
+ ''
+ ).attr('id', realSelectionId);
+
+ $realSelectionContainer.appendTo(editor.getBody());
+ }
+
+ range = editor.dom.createRng();
+
+ // WHY is IE making things so hard! Copy on x produces: x
+ if (targetClone === origTargetClone && Env.ie) {
+ $realSelectionContainer.empty().append(Zwsp.ZWSP).append(targetClone).append(Zwsp.ZWSP);
+ range.setStart($realSelectionContainer[0].firstChild, 0);
+ range.setEnd($realSelectionContainer[0].lastChild, 1);
+ } else {
+ $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0');
+ range.setStart($realSelectionContainer[0].firstChild, 1);
+ range.setEnd($realSelectionContainer[0].lastChild, 0);
+ }
+
+ $realSelectionContainer.css({
+ top: dom.getPos(node, editor.getBody()).y
+ });
+
+ $realSelectionContainer[0].focus();
+ sel = editor.selection.getSel();
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ editor.$('*[data-mce-selected]').removeAttr('data-mce-selected');
+ node.setAttribute('data-mce-selected', 1);
+ selectedContentEditableNode = node;
+
+ return range;
+ }
+
+ function clearContentEditableSelection() {
+ if (selectedContentEditableNode) {
+ selectedContentEditableNode.removeAttribute('data-mce-selected');
+ editor.$('#' + realSelectionId).remove();
+ selectedContentEditableNode = null;
+ }
+ }
+
+ function destroy() {
+ fakeCaret.destroy();
+ selectedContentEditableNode = null;
+ }
+
+ function hideFakeCaret() {
+ fakeCaret.hide();
+ }
+
+ if (Env.ceFalse) {
+ registerEvents();
+ addCss();
+ }
+
+ return {
+ showBlockCaretContainer: showBlockCaretContainer,
+ hideFakeCaret: hideFakeCaret,
+ destroy: destroy
+ };
+ }
+
+ return SelectionOverrides;
+});
+
+// Included from: js/tinymce/classes/util/Uuid.js
+
+/**
+ * Uuid.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Generates unique ids.
+ *
+ * @class tinymce.util.Uuid
+ * @private
+ */
+define("tinymce/util/Uuid", [
+], function() {
+ var count = 0;
+
+ var seed = function () {
+ var rnd = function () {
+ return Math.round(Math.random() * 0xFFFFFFFF).toString(36);
+ };
+
+ var now = new Date().getTime();
+ return 's' + now.toString(36) + rnd() + rnd() + rnd();
+ };
+
+ var uuid = function (prefix) {
+ return prefix + (count++) + seed();
+ };
+
+ return {
+ uuid: uuid
+ };
+});
+
+// Included from: js/tinymce/classes/Editor.js
+
+/**
+ * Editor.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint scripturl:true */
+
+/**
+ * Include the base event class documentation.
+ *
+ * @include ../../../tools/docs/tinymce.Event.js
+ */
+
+/**
+ * This class contains the core logic for a TinyMCE editor.
+ *
+ * @class tinymce.Editor
+ * @mixes tinymce.util.Observable
+ * @example
+ * // Add a class to all paragraphs in the editor.
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
+ *
+ * // Gets the current editors selection as text
+ * tinymce.activeEditor.selection.getContent({format: 'text'});
+ *
+ * // Creates a new editor instance
+ * var ed = new tinymce.Editor('textareaid', {
+ * some_setting: 1
+ * }, tinymce.EditorManager);
+ *
+ * // Select each item the user clicks on
+ * ed.on('click', function(e) {
+ * ed.selection.select(e.target);
+ * });
+ *
+ * ed.render();
+ */
+define("tinymce/Editor", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/DomQuery",
+ "tinymce/AddOnManager",
+ "tinymce/NodeChange",
+ "tinymce/html/Node",
+ "tinymce/dom/Serializer",
+ "tinymce/html/Serializer",
+ "tinymce/dom/Selection",
+ "tinymce/Formatter",
+ "tinymce/UndoManager",
+ "tinymce/EnterKey",
+ "tinymce/ForceBlocks",
+ "tinymce/EditorCommands",
+ "tinymce/util/URI",
+ "tinymce/dom/ScriptLoader",
+ "tinymce/dom/EventUtils",
+ "tinymce/WindowManager",
+ "tinymce/NotificationManager",
+ "tinymce/html/Schema",
+ "tinymce/html/DomParser",
+ "tinymce/util/Quirks",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Delay",
+ "tinymce/EditorObservable",
+ "tinymce/Mode",
+ "tinymce/Shortcuts",
+ "tinymce/EditorUpload",
+ "tinymce/SelectionOverrides",
+ "tinymce/util/Uuid"
+], function(
+ DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer,
+ Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands,
+ URI, ScriptLoader, EventUtils, WindowManager, NotificationManager,
+ Schema, DomParser, Quirks, Env, Tools, Delay, EditorObservable, Mode, Shortcuts, EditorUpload,
+ SelectionOverrides, Uuid
+) {
+ // Shorten these names
+ var DOM = DOMUtils.DOM, ThemeManager = AddOnManager.ThemeManager, PluginManager = AddOnManager.PluginManager;
+ var extend = Tools.extend, each = Tools.each, explode = Tools.explode;
+ var inArray = Tools.inArray, trim = Tools.trim, resolve = Tools.resolve;
+ var Event = EventUtils.Event;
+ var isGecko = Env.gecko, ie = Env.ie;
+
+ /**
+ * Include documentation for all the events.
+ *
+ * @include ../../../tools/docs/tinymce.Editor.js
+ */
+
+ /**
+ * Constructs a editor instance by id.
+ *
+ * @constructor
+ * @method Editor
+ * @param {String} id Unique id for the editor.
+ * @param {Object} settings Settings for the editor.
+ * @param {tinymce.EditorManager} editorManager EditorManager instance.
+ */
+ function Editor(id, settings, editorManager) {
+ var self = this, documentBaseUrl, baseUri, defaultSettings;
+
+ documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
+ baseUri = editorManager.baseURI;
+ defaultSettings = editorManager.defaultSettings;
+
+ /**
+ * Name/value collection with editor settings.
+ *
+ * @property settings
+ * @type Object
+ * @example
+ * // Get the value of the theme setting
+ * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
+ */
+ settings = extend({
+ id: id,
+ theme: 'modern',
+ delta_width: 0,
+ delta_height: 0,
+ popup_css: '',
+ plugins: '',
+ document_base_url: documentBaseUrl,
+ add_form_submit_trigger: true,
+ submit_patch: true,
+ add_unload_trigger: true,
+ convert_urls: true,
+ relative_urls: true,
+ remove_script_host: true,
+ object_resizing: true,
+ doctype: '',
+ visual: true,
+ font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',
+
+ // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
+ font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
+ forced_root_block: 'p',
+ hidden_input: true,
+ padd_empty_editor: true,
+ render_ui: true,
+ indentation: '30px',
+ inline_styles: true,
+ convert_fonts_to_spans: true,
+ indent: 'simple',
+ indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
+ 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
+ indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
+ 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
+ validate: true,
+ entity_encoding: 'named',
+ url_converter: self.convertURL,
+ url_converter_scope: self,
+ ie7_compat: true
+ }, defaultSettings, settings);
+
+ // Merge external_plugins
+ if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) {
+ settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins);
+ }
+
+ self.settings = settings;
+ AddOnManager.language = settings.language || 'en';
+ AddOnManager.languageLoad = settings.language_load;
+ AddOnManager.baseURL = editorManager.baseURL;
+
+ /**
+ * Editor instance id, normally the same as the div/textarea that was replaced.
+ *
+ * @property id
+ * @type String
+ */
+ self.id = settings.id = id;
+
+ /**
+ * State to force the editor to return false on a isDirty call.
+ *
+ * @property isNotDirty
+ * @type Boolean
+ * @deprecated Use editor.setDirty instead.
+ */
+ self.setDirty(false);
+
+ /**
+ * Name/Value object containing plugin instances.
+ *
+ * @property plugins
+ * @type Object
+ * @example
+ * // Execute a method inside a plugin directly
+ * tinymce.activeEditor.plugins.someplugin.someMethod();
+ */
+ self.plugins = {};
+
+ /**
+ * URI object to document configured for the TinyMCE instance.
+ *
+ * @property documentBaseURI
+ * @type tinymce.util.URI
+ * @example
+ * // Get relative URL from the location of document_base_url
+ * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
+ *
+ * // Get absolute URL from the location of document_base_url
+ * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
+ */
+ self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, {
+ base_uri: baseUri
+ });
+
+ /**
+ * URI object to current document that holds the TinyMCE editor instance.
+ *
+ * @property baseURI
+ * @type tinymce.util.URI
+ * @example
+ * // Get relative URL from the location of the API
+ * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
+ *
+ * // Get absolute URL from the location of the API
+ * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
+ */
+ self.baseURI = baseUri;
+
+ /**
+ * Array with CSS files to load into the iframe.
+ *
+ * @property contentCSS
+ * @type Array
+ */
+ self.contentCSS = [];
+
+ /**
+ * Array of CSS styles to add to head of document when the editor loads.
+ *
+ * @property contentStyles
+ * @type Array
+ */
+ self.contentStyles = [];
+
+ // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
+ self.shortcuts = new Shortcuts(self);
+ self.loadedCSS = {};
+ self.editorCommands = new EditorCommands(self);
+
+ if (settings.target) {
+ self.targetElm = settings.target;
+ }
+
+ self.suffix = editorManager.suffix;
+ self.editorManager = editorManager;
+ self.inline = settings.inline;
+ self.settings.content_editable = self.inline;
+
+ if (settings.cache_suffix) {
+ Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
+ }
+
+ if (settings.override_viewport === false) {
+ Env.overrideViewPort = false;
+ }
+
+ // Call setup
+ editorManager.fire('SetupEditor', self);
+ self.execCallback('setup', self);
+
+ /**
+ * Dom query instance with default scope to the editor document and default element is the body of the editor.
+ *
+ * @property $
+ * @type tinymce.dom.DomQuery
+ * @example
+ * tinymce.activeEditor.$('p').css('color', 'red');
+ * tinymce.activeEditor.$().append('new
');
+ */
+ self.$ = DomQuery.overrideDefaults(function() {
+ return {
+ context: self.inline ? self.getBody() : self.getDoc(),
+ element: self.getBody()
+ };
+ });
+ }
+
+ Editor.prototype = {
+ /**
+ * Renders the editor/adds it to the page.
+ *
+ * @method render
+ */
+ render: function() {
+ var self = this, settings = self.settings, id = self.id, suffix = self.suffix;
+
+ function readyHandler() {
+ DOM.unbind(window, 'ready', readyHandler);
+ self.render();
+ }
+
+ // Page is not loaded yet, wait for it
+ if (!Event.domLoaded) {
+ DOM.bind(window, 'ready', readyHandler);
+ return;
+ }
+
+ // Element not found, then skip initialization
+ if (!self.getElement()) {
+ return;
+ }
+
+ // No editable support old iOS versions etc
+ if (!Env.contentEditable) {
+ return;
+ }
+
+ // Hide target element early to prevent content flashing
+ if (!settings.inline) {
+ self.orgVisibility = self.getElement().style.visibility;
+ self.getElement().style.visibility = 'hidden';
+ } else {
+ self.inline = true;
+ }
+
+ var form = self.getElement().form || DOM.getParent(id, 'form');
+ if (form) {
+ self.formElement = form;
+
+ // Add hidden input for non input elements inside form elements
+ if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(self.getElement().nodeName)) {
+ DOM.insertAfter(DOM.create('input', {type: 'hidden', name: id}), id);
+ self.hasHiddenInput = true;
+ }
+
+ // Pass submit/reset from form to editor instance
+ self.formEventDelegate = function(e) {
+ self.fire(e.type, e);
+ };
+
+ DOM.bind(form, 'submit reset', self.formEventDelegate);
+
+ // Reset contents in editor when the form is reset
+ self.on('reset', function() {
+ self.setContent(self.startContent, {format: 'raw'});
+ });
+
+ // Check page uses id="submit" or name="submit" for it's submit button
+ if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
+ form._mceOldSubmit = form.submit;
+ form.submit = function() {
+ self.editorManager.triggerSave();
+ self.setDirty(false);
+
+ return form._mceOldSubmit(form);
+ };
+ }
+ }
+
+ /**
+ * Window manager reference, use this to open new windows and dialogs.
+ *
+ * @property windowManager
+ * @type tinymce.WindowManager
+ * @example
+ * // Shows an alert message
+ * tinymce.activeEditor.windowManager.alert('Hello world!');
+ *
+ * // Opens a new dialog with the file.htm file and the size 320x240
+ * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
+ * tinymce.activeEditor.windowManager.open({
+ * url: 'file.htm',
+ * width: 320,
+ * height: 240
+ * }, {
+ * custom_param: 1
+ * });
+ */
+ self.windowManager = new WindowManager(self);
+
+ /**
+ * Notification manager reference, use this to open new windows and dialogs.
+ *
+ * @property notificationManager
+ * @type tinymce.NotificationManager
+ * @example
+ * // Shows a notification info message.
+ * tinymce.activeEditor.notificationManager.open({text: 'Hello world!', type: 'info'});
+ */
+ self.notificationManager = new NotificationManager(self);
+
+ if (settings.encoding == 'xml') {
+ self.on('GetContent', function(e) {
+ if (e.save) {
+ e.content = DOM.encode(e.content);
+ }
+ });
+ }
+
+ if (settings.add_form_submit_trigger) {
+ self.on('submit', function() {
+ if (self.initialized) {
+ self.save();
+ }
+ });
+ }
+
+ if (settings.add_unload_trigger) {
+ self._beforeUnload = function() {
+ if (self.initialized && !self.destroyed && !self.isHidden()) {
+ self.save({format: 'raw', no_events: true, set_dirty: false});
+ }
+ };
+
+ self.editorManager.on('BeforeUnload', self._beforeUnload);
+ }
+
+ // Load scripts
+ function loadScripts() {
+ var scriptLoader = ScriptLoader.ScriptLoader;
+
+ if (settings.language && settings.language != 'en' && !settings.language_url) {
+ settings.language_url = self.editorManager.baseURL + '/langs/' + settings.language + '.js';
+ }
+
+ if (settings.language_url) {
+ scriptLoader.add(settings.language_url);
+ }
+
+ if (settings.theme && typeof settings.theme != "function" &&
+ settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) {
+ var themeUrl = settings.theme_url;
+
+ if (themeUrl) {
+ themeUrl = self.documentBaseURI.toAbsolute(themeUrl);
+ } else {
+ themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js';
+ }
+
+ ThemeManager.load(settings.theme, themeUrl);
+ }
+
+ if (Tools.isArray(settings.plugins)) {
+ settings.plugins = settings.plugins.join(' ');
+ }
+
+ each(settings.external_plugins, function(url, name) {
+ PluginManager.load(name, url);
+ settings.plugins += ' ' + name;
+ });
+
+ each(settings.plugins.split(/[ ,]/), function(plugin) {
+ plugin = trim(plugin);
+
+ if (plugin && !PluginManager.urls[plugin]) {
+ if (plugin.charAt(0) == '-') {
+ plugin = plugin.substr(1, plugin.length);
+
+ var dependencies = PluginManager.dependencies(plugin);
+
+ each(dependencies, function(dep) {
+ var defaultSettings = {
+ prefix: 'plugins/',
+ resource: dep,
+ suffix: '/plugin' + suffix + '.js'
+ };
+
+ dep = PluginManager.createUrl(defaultSettings, dep);
+ PluginManager.load(dep.resource, dep);
+ });
+ } else {
+ PluginManager.load(plugin, {
+ prefix: 'plugins/',
+ resource: plugin,
+ suffix: '/plugin' + suffix + '.js'
+ });
+ }
+ }
+ });
+
+ scriptLoader.loadQueue(function() {
+ if (!self.removed) {
+ self.init();
+ }
+ });
+ }
+
+ self.editorManager.add(self);
+ loadScripts();
+ },
+
+ /**
+ * Initializes the editor this will be called automatically when
+ * all plugins/themes and language packs are loaded by the rendered method.
+ * This method will setup the iframe and create the theme and plugin instances.
+ *
+ * @method init
+ */
+ init: function() {
+ var self = this, settings = self.settings, elm = self.getElement();
+ var w, h, minHeight, n, o, Theme, url, bodyId, bodyClass, re, i, initializedPlugins = [];
+
+ self.rtl = settings.rtl_ui || self.editorManager.i18n.rtl;
+ self.editorManager.i18n.setCode(settings.language);
+ settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', self.getLang('aria.rich_text_area'));
+
+ self.fire('ScriptsLoaded');
+
+ /**
+ * Reference to the theme instance that was used to generate the UI.
+ *
+ * @property theme
+ * @type tinymce.Theme
+ * @example
+ * // Executes a method on the theme directly
+ * tinymce.activeEditor.theme.someMethod();
+ */
+ if (settings.theme) {
+ if (typeof settings.theme != "function") {
+ settings.theme = settings.theme.replace(/-/, '');
+ Theme = ThemeManager.get(settings.theme);
+ self.theme = new Theme(self, ThemeManager.urls[settings.theme]);
+
+ if (self.theme.init) {
+ self.theme.init(self, ThemeManager.urls[settings.theme] || self.documentBaseUrl.replace(/\/$/, ''), self.$);
+ }
+ } else {
+ self.theme = settings.theme;
+ }
+ }
+
+ function initPlugin(plugin) {
+ var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
+
+ pluginUrl = PluginManager.urls[plugin] || self.documentBaseUrl.replace(/\/$/, '');
+ plugin = trim(plugin);
+ if (Plugin && inArray(initializedPlugins, plugin) === -1) {
+ each(PluginManager.dependencies(plugin), function(dep) {
+ initPlugin(dep);
+ });
+
+ if (self.plugins[plugin]) {
+ return;
+ }
+
+ pluginInstance = new Plugin(self, pluginUrl, self.$);
+
+ self.plugins[plugin] = pluginInstance;
+
+ if (pluginInstance.init) {
+ pluginInstance.init(self, pluginUrl);
+ initializedPlugins.push(plugin);
+ }
+ }
+ }
+
+ // Create all plugins
+ each(settings.plugins.replace(/\-/g, '').split(/[ ,]/), initPlugin);
+
+ // Measure box
+ if (settings.render_ui && self.theme) {
+ self.orgDisplay = elm.style.display;
+
+ if (typeof settings.theme != "function") {
+ w = settings.width || elm.style.width || elm.offsetWidth;
+ h = settings.height || elm.style.height || elm.offsetHeight;
+ minHeight = settings.min_height || 100;
+ re = /^[0-9\.]+(|px)$/i;
+
+ if (re.test('' + w)) {
+ w = Math.max(parseInt(w, 10), 100);
+ }
+
+ if (re.test('' + h)) {
+ h = Math.max(parseInt(h, 10), minHeight);
+ }
+
+ // Render UI
+ o = self.theme.renderUI({
+ targetNode: elm,
+ width: w,
+ height: h,
+ deltaWidth: settings.delta_width,
+ deltaHeight: settings.delta_height
+ });
+
+ // Resize editor
+ if (!settings.content_editable) {
+ h = (o.iframeHeight || h) + (typeof h == 'number' ? (o.deltaHeight || 0) : '');
+ if (h < minHeight) {
+ h = minHeight;
+ }
+ }
+ } else {
+ o = settings.theme(self, elm);
+
+ // Convert element type to id:s
+ if (o.editorContainer.nodeType) {
+ o.editorContainer = o.editorContainer.id = o.editorContainer.id || self.id + "_parent";
+ }
+
+ // Convert element type to id:s
+ if (o.iframeContainer.nodeType) {
+ o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || self.id + "_iframecontainer";
+ }
+
+ // Use specified iframe height or the targets offsetHeight
+ h = o.iframeHeight || elm.offsetHeight;
+ }
+
+ self.editorContainer = o.editorContainer;
+ }
+
+ // Load specified content CSS last
+ if (settings.content_css) {
+ each(explode(settings.content_css), function(u) {
+ self.contentCSS.push(self.documentBaseURI.toAbsolute(u));
+ });
+ }
+
+ // Load specified content CSS last
+ if (settings.content_style) {
+ self.contentStyles.push(settings.content_style);
+ }
+
+ // Content editable mode ends here
+ if (settings.content_editable) {
+ elm = n = o = null; // Fix IE leak
+ return self.initContentBody();
+ }
+
+ self.iframeHTML = settings.doctype + '';
+
+ // We only need to override paths if we have to
+ // IE has a bug where it remove site absolute urls to relative ones if this is specified
+ if (settings.document_base_url != self.documentBaseUrl) {
+ self.iframeHTML += '';
+ }
+
+ // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
+ if (!Env.caretAfter && settings.ie7_compat) {
+ self.iframeHTML += '';
+ }
+
+ self.iframeHTML += '';
+
+ // Load the CSS by injecting them into the HTML this will reduce "flicker"
+ // However we can't do that on Chrome since # will scroll to the editor for some odd reason see #2427
+ if (!/#$/.test(document.location.href)) {
+ for (i = 0; i < self.contentCSS.length; i++) {
+ var cssUrl = self.contentCSS[i];
+ self.iframeHTML += (
+ ''
+ );
+ self.loadedCSS[cssUrl] = true;
+ }
+ }
+
+ bodyId = settings.body_id || 'tinymce';
+ if (bodyId.indexOf('=') != -1) {
+ bodyId = self.getParam('body_id', '', 'hash');
+ bodyId = bodyId[self.id] || bodyId;
+ }
+
+ bodyClass = settings.body_class || '';
+ if (bodyClass.indexOf('=') != -1) {
+ bodyClass = self.getParam('body_class', '', 'hash');
+ bodyClass = bodyClass[self.id] || '';
+ }
+
+ if (settings.content_security_policy) {
+ self.iframeHTML += '';
+ }
+
+ self.iframeHTML += '
';
+
+ /*eslint no-script-url:0 */
+ var domainRelaxUrl = 'javascript:(function(){' +
+ 'document.open();document.domain="' + document.domain + '";' +
+ 'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' +
+ 'document.close();ed.initContentBody(true);})()';
+
+ // Domain relaxing is required since the user has messed around with document.domain
+ if (document.domain != location.hostname) {
+ // Edge seems to be able to handle domain relaxing
+ if (Env.ie && Env.ie < 12) {
+ url = domainRelaxUrl;
+ }
+ }
+
+ // Create iframe
+ // TODO: ACC add the appropriate description on this.
+ var ifr = DOM.create('iframe', {
+ id: self.id + "_ifr",
+ //src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
+ frameBorder: '0',
+ allowTransparency: "true",
+ title: self.editorManager.translate(
+ "Rich Text Area. Press ALT-F9 for menu. " +
+ "Press ALT-F10 for toolbar. Press ALT-0 for help"
+ ),
+ style: {
+ width: '100%',
+ height: h,
+ display: 'block' // Important for Gecko to render the iframe correctly
+ }
+ });
+
+ ifr.onload = function() {
+ ifr.onload = null;
+ self.fire("load");
+ };
+
+ DOM.setAttrib(ifr, "src", url || 'javascript:""');
+
+ self.contentAreaContainer = o.iframeContainer;
+ self.iframeElement = ifr;
+
+ n = DOM.add(o.iframeContainer, ifr);
+
+ // Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
+ // Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
+ if (ie) {
+ try {
+ self.getDoc();
+ } catch (e) {
+ n.src = url = domainRelaxUrl;
+ }
+ }
+
+ if (o.editorContainer) {
+ DOM.get(o.editorContainer).style.display = self.orgDisplay;
+ self.hidden = DOM.isHidden(o.editorContainer);
+ }
+
+ self.getElement().style.display = 'none';
+ DOM.setAttrib(self.id, 'aria-hidden', true);
+
+ if (!url) {
+ self.initContentBody();
+ }
+
+ elm = n = o = null; // Cleanup
+ },
+
+ /**
+ * This method get called by the init method once the iframe is loaded.
+ * It will fill the iframe with contents, sets up DOM and selection objects for the iframe.
+ *
+ * @method initContentBody
+ * @private
+ */
+ initContentBody: function(skipWrite) {
+ var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText;
+
+ // Restore visibility on target element
+ if (!settings.inline) {
+ self.getElement().style.visibility = self.orgVisibility;
+ }
+
+ // Setup iframe body
+ if (!skipWrite && !settings.content_editable) {
+ doc.open();
+ doc.write(self.iframeHTML);
+ doc.close();
+ }
+
+ if (settings.content_editable) {
+ self.on('remove', function() {
+ var bodyEl = this.getBody();
+
+ DOM.removeClass(bodyEl, 'mce-content-body');
+ DOM.removeClass(bodyEl, 'mce-edit-focus');
+ DOM.setAttrib(bodyEl, 'contentEditable', null);
+ });
+
+ DOM.addClass(targetElm, 'mce-content-body');
+ self.contentDocument = doc = settings.content_document || document;
+ self.contentWindow = settings.content_window || window;
+ self.bodyElement = targetElm;
+
+ // Prevent leak in IE
+ settings.content_document = settings.content_window = null;
+
+ // TODO: Fix this
+ settings.root_name = targetElm.nodeName.toLowerCase();
+ }
+
+ // It will not steal focus while setting contentEditable
+ body = self.getBody();
+ body.disabled = true;
+ self.readonly = settings.readonly;
+
+ if (!self.readonly) {
+ if (self.inline && DOM.getStyle(body, 'position', true) == 'static') {
+ body.style.position = 'relative';
+ }
+
+ body.contentEditable = self.getParam('content_editable_state', true);
+ }
+
+ body.disabled = false;
+
+ self.editorUpload = new EditorUpload(self);
+
+ /**
+ * Schema instance, enables you to validate elements and its children.
+ *
+ * @property schema
+ * @type tinymce.html.Schema
+ */
+ self.schema = new Schema(settings);
+
+ /**
+ * DOM instance for the editor.
+ *
+ * @property dom
+ * @type tinymce.dom.DOMUtils
+ * @example
+ * // Adds a class to all paragraphs within the editor
+ * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
+ */
+ self.dom = new DOMUtils(doc, {
+ keep_values: true,
+ url_converter: self.convertURL,
+ url_converter_scope: self,
+ hex_colors: settings.force_hex_style_colors,
+ class_filter: settings.class_filter,
+ update_styles: true,
+ root_element: self.inline ? self.getBody() : null,
+ collect: settings.content_editable,
+ schema: self.schema,
+ onSetAttrib: function(e) {
+ self.fire('SetAttrib', e);
+ }
+ });
+
+ /**
+ * HTML parser will be used when contents is inserted into the editor.
+ *
+ * @property parser
+ * @type tinymce.html.DomParser
+ */
+ self.parser = new DomParser(settings, self.schema);
+
+ // Convert src and href into data-mce-src, data-mce-href and data-mce-style
+ self.parser.addAttributeFilter('src,href,style,tabindex', function(nodes, name) {
+ var i = nodes.length, node, dom = self.dom, value, internalName;
+
+ while (i--) {
+ node = nodes[i];
+ value = node.attr(name);
+ internalName = 'data-mce-' + name;
+
+ // Add internal attribute if we need to we don't on a refresh of the document
+ if (!node.attributes.map[internalName]) {
+ // Don't duplicate these since they won't get modified by any browser
+ if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) {
+ continue;
+ }
+
+ if (name === "style") {
+ value = dom.serializeStyle(dom.parseStyle(value), node.name);
+
+ if (!value.length) {
+ value = null;
+ }
+
+ node.attr(internalName, value);
+ node.attr(name, value);
+ } else if (name === "tabindex") {
+ node.attr(internalName, value);
+ node.attr(name, null);
+ } else {
+ node.attr(internalName, self.convertURL(value, name, node.name));
+ }
+ }
+ }
+ });
+
+ // Keep scripts from executing
+ self.parser.addNodeFilter('script', function(nodes) {
+ var i = nodes.length, node, type;
+
+ while (i--) {
+ node = nodes[i];
+ type = node.attr('type') || 'no/type';
+ if (type.indexOf('mce-') !== 0) {
+ node.attr('type', 'mce-' + type);
+ }
+ }
+ });
+
+ self.parser.addNodeFilter('#cdata', function(nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ node.type = 8;
+ node.name = '#comment';
+ node.value = '[CDATA[' + node.value + ']]';
+ }
+ });
+
+ self.parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function(nodes) {
+ var i = nodes.length, node, nonEmptyElements = self.schema.getNonEmptyElements();
+
+ while (i--) {
+ node = nodes[i];
+
+ if (node.isEmpty(nonEmptyElements)) {
+ node.append(new Node('br', 1)).shortEnded = true;
+ }
+ }
+ });
+
+ /**
+ * DOM serializer for the editor. Will be used when contents is extracted from the editor.
+ *
+ * @property serializer
+ * @type tinymce.dom.Serializer
+ * @example
+ * // Serializes the first paragraph in the editor into a string
+ * tinymce.activeEditor.serializer.serialize(tinymce.activeEditor.dom.select('p')[0]);
+ */
+ self.serializer = new DomSerializer(settings, self);
+
+ /**
+ * Selection instance for the editor.
+ *
+ * @property selection
+ * @type tinymce.dom.Selection
+ * @example
+ * // Sets some contents to the current selection in the editor
+ * tinymce.activeEditor.selection.setContent('Some contents');
+ *
+ * // Gets the current selection
+ * alert(tinymce.activeEditor.selection.getContent());
+ *
+ * // Selects the first paragraph found
+ * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
+ */
+ self.selection = new Selection(self.dom, self.getWin(), self.serializer, self);
+
+ /**
+ * Formatter instance.
+ *
+ * @property formatter
+ * @type tinymce.Formatter
+ */
+ self.formatter = new Formatter(self);
+
+ /**
+ * Undo manager instance, responsible for handling undo levels.
+ *
+ * @property undoManager
+ * @type tinymce.UndoManager
+ * @example
+ * // Undoes the last modification to the editor
+ * tinymce.activeEditor.undoManager.undo();
+ */
+ self.undoManager = new UndoManager(self);
+
+ self.forceBlocks = new ForceBlocks(self);
+ self.enterKey = new EnterKey(self);
+ self._nodeChangeDispatcher = new NodeChange(self);
+ self._selectionOverrides = new SelectionOverrides(self);
+
+ self.fire('PreInit');
+
+ if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
+ doc.body.spellcheck = false; // Gecko
+ DOM.setAttrib(body, "spellcheck", "false");
+ }
+
+ self.quirks = new Quirks(self);
+ self.fire('PostRender');
+
+ if (settings.directionality) {
+ body.dir = settings.directionality;
+ }
+
+ if (settings.nowrap) {
+ body.style.whiteSpace = "nowrap";
+ }
+
+ if (settings.protect) {
+ self.on('BeforeSetContent', function(e) {
+ each(settings.protect, function(pattern) {
+ e.content = e.content.replace(pattern, function(str) {
+ return '';
+ });
+ });
+ });
+ }
+
+ self.on('SetContent', function() {
+ self.addVisual(self.getBody());
+ });
+
+ // Remove empty contents
+ if (settings.padd_empty_editor) {
+ self.on('PostProcess', function(e) {
+ e.content = e.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
[\r\n]*)$/, '');
+ });
+ }
+
+ self.load({initial: true, format: 'html'});
+ self.startContent = self.getContent({format: 'raw'});
+
+ /**
+ * Is set to true after the editor instance has been initialized
+ *
+ * @property initialized
+ * @type Boolean
+ * @example
+ * function isEditorInitialized(editor) {
+ * return editor && editor.initialized;
+ * }
+ */
+ self.initialized = true;
+ self.bindPendingEventDelegates();
+
+ self.fire('init');
+ self.focus(true);
+ self.nodeChanged({initial: true});
+ self.execCallback('init_instance_callback', self);
+
+ self.on('compositionstart compositionend', function(e) {
+ self.composing = e.type === 'compositionstart';
+ });
+
+ // Add editor specific CSS styles
+ if (self.contentStyles.length > 0) {
+ contentCssText = '';
+
+ each(self.contentStyles, function(style) {
+ contentCssText += style + "\r\n";
+ });
+
+ self.dom.addStyle(contentCssText);
+ }
+
+ // Load specified content CSS last
+ each(self.contentCSS, function(cssUrl) {
+ if (!self.loadedCSS[cssUrl]) {
+ self.dom.loadCSS(cssUrl);
+ self.loadedCSS[cssUrl] = true;
+ }
+ });
+
+ // Handle auto focus
+ if (settings.auto_focus) {
+ Delay.setEditorTimeout(self, function() {
+ var editor;
+
+ if (settings.auto_focus === true) {
+ editor = self;
+ } else {
+ editor = self.editorManager.get(settings.auto_focus);
+ }
+
+ if (!editor.destroyed) {
+ editor.focus();
+ }
+ }, 100);
+ }
+
+ // Clean up references for IE
+ targetElm = doc = body = null;
+ },
+
+ /**
+ * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
+ * it will also place DOM focus inside the editor.
+ *
+ * @method focus
+ * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
+ */
+ focus: function(skipFocus) {
+ var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
+ var controlElm, doc = self.getDoc(), body = self.getBody(), contentEditableHost;
+
+ function getContentEditableHost(node) {
+ return self.dom.getParent(node, function(node) {
+ return self.dom.getContentEditable(node) === "true";
+ });
+ }
+
+ if (!skipFocus) {
+ // Get selected control element
+ rng = selection.getRng();
+ if (rng.item) {
+ controlElm = rng.item(0);
+ }
+
+ self.quirks.refreshContentEditable();
+
+ // Move focus to contentEditable=true child if needed
+ contentEditableHost = getContentEditableHost(selection.getNode());
+ if (self.$.contains(body, contentEditableHost)) {
+ contentEditableHost.focus();
+ selection.normalize();
+ self.editorManager.setActive(self);
+ return;
+ }
+
+ // Focus the window iframe
+ if (!contentEditable) {
+ // WebKit needs this call to fire focusin event properly see #5948
+ // But Opera pre Blink engine will produce an empty selection so skip Opera
+ if (!Env.opera) {
+ self.getBody().focus();
+ }
+
+ self.getWin().focus();
+ }
+
+ // Focus the body as well since it's contentEditable
+ if (isGecko || contentEditable) {
+ // Check for setActive since it doesn't scroll to the element
+ if (body.setActive) {
+ // IE 11 sometimes throws "Invalid function" then fallback to focus
+ try {
+ body.setActive();
+ } catch (ex) {
+ body.focus();
+ }
+ } else {
+ body.focus();
+ }
+
+ if (contentEditable) {
+ selection.normalize();
+ }
+ }
+
+ // Restore selected control element
+ // This is needed when for example an image is selected within a
+ // layer a call to focus will then remove the control selection
+ if (controlElm && controlElm.ownerDocument == doc) {
+ rng = doc.body.createControlRange();
+ rng.addElement(controlElm);
+ rng.select();
+ }
+ }
+
+ self.editorManager.setActive(self);
+ },
+
+ /**
+ * Executes a legacy callback. This method is useful to call old 2.x option callbacks.
+ * There new event model is a better way to add callback so this method might be removed in the future.
+ *
+ * @method execCallback
+ * @param {String} name Name of the callback to execute.
+ * @return {Object} Return value passed from callback function.
+ */
+ execCallback: function(name) {
+ var self = this, callback = self.settings[name], scope;
+
+ if (!callback) {
+ return;
+ }
+
+ // Look through lookup
+ if (self.callbackLookup && (scope = self.callbackLookup[name])) {
+ callback = scope.func;
+ scope = scope.scope;
+ }
+
+ if (typeof callback === 'string') {
+ scope = callback.replace(/\.\w+$/, '');
+ scope = scope ? resolve(scope) : 0;
+ callback = resolve(callback);
+ self.callbackLookup = self.callbackLookup || {};
+ self.callbackLookup[name] = {func: callback, scope: scope};
+ }
+
+ return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1));
+ },
+
+ /**
+ * Translates the specified string by replacing variables with language pack items it will also check if there is
+ * a key matching the input.
+ *
+ * @method translate
+ * @param {String} text String to translate by the language pack data.
+ * @return {String} Translated string.
+ */
+ translate: function(text) {
+ var lang = this.settings.language || 'en', i18n = this.editorManager.i18n;
+
+ if (!text) {
+ return '';
+ }
+
+ text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function(a, b) {
+ return i18n.data[lang + '.' + b] || '{#' + b + '}';
+ });
+
+ return this.editorManager.translate(text);
+ },
+
+ /**
+ * Returns a language pack item by name/key.
+ *
+ * @method getLang
+ * @param {String} name Name/key to get from the language pack.
+ * @param {String} defaultVal Optional default value to retrieve.
+ */
+ getLang: function(name, defaultVal) {
+ return (
+ this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] ||
+ (defaultVal !== undefined ? defaultVal : '{#' + name + '}')
+ );
+ },
+
+ /**
+ * Returns a configuration parameter by name.
+ *
+ * @method getParam
+ * @param {String} name Configruation parameter to retrieve.
+ * @param {String} defaultVal Optional default value to return.
+ * @param {String} type Optional type parameter.
+ * @return {String} Configuration parameter value or default value.
+ * @example
+ * // Returns a specific config value from the currently active editor
+ * var someval = tinymce.activeEditor.getParam('myvalue');
+ *
+ * // Returns a specific config value from a specific editor instance by id
+ * var someval2 = tinymce.get('my_editor').getParam('myvalue');
+ */
+ getParam: function(name, defaultVal, type) {
+ var value = name in this.settings ? this.settings[name] : defaultVal, output;
+
+ if (type === 'hash') {
+ output = {};
+
+ if (typeof value === 'string') {
+ each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function(value) {
+ value = value.split('=');
+
+ if (value.length > 1) {
+ output[trim(value[0])] = trim(value[1]);
+ } else {
+ output[trim(value[0])] = trim(value);
+ }
+ });
+ } else {
+ output = value;
+ }
+
+ return output;
+ }
+
+ return value;
+ },
+
+ /**
+ * Dispatches out a onNodeChange event to all observers. This method should be called when you
+ * need to update the UI states or element path etc.
+ *
+ * @method nodeChanged
+ * @param {Object} args Optional args to pass to NodeChange event handlers.
+ */
+ nodeChanged: function(args) {
+ this._nodeChangeDispatcher.nodeChanged(args);
+ },
+
+ /**
+ * Adds a button that later gets created by the theme in the editors toolbars.
+ *
+ * @method addButton
+ * @param {String} name Button name to add.
+ * @param {Object} settings Settings object with title, cmd etc.
+ * @example
+ * // Adds a custom button to the editor that inserts contents when clicked
+ * tinymce.init({
+ * ...
+ *
+ * toolbar: 'example'
+ *
+ * setup: function(ed) {
+ * ed.addButton('example', {
+ * title: 'My title',
+ * image: '../js/tinymce/plugins/example/img/example.gif',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addButton: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ if (!settings.text && !settings.icon) {
+ settings.icon = name;
+ }
+
+ self.buttons = self.buttons || {};
+ settings.tooltip = settings.tooltip || settings.title;
+ self.buttons[name] = settings;
+ },
+
+ /**
+ * Adds a menu item to be used in the menus of the theme. There might be multiple instances
+ * of this menu item for example it might be used in the main menus of the theme but also in
+ * the context menu so make sure that it's self contained and supports multiple instances.
+ *
+ * @method addMenuItem
+ * @param {String} name Menu item name to add.
+ * @param {Object} settings Settings object with title, cmd etc.
+ * @example
+ * // Adds a custom menu item to the editor that inserts contents when clicked
+ * // The context option allows you to add the menu item to an existing default menu
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * ed.addMenuItem('example', {
+ * text: 'My menu item',
+ * context: 'tools',
+ * onclick: function() {
+ * ed.insertContent('Hello world!!');
+ * }
+ * });
+ * }
+ * });
+ */
+ addMenuItem: function(name, settings) {
+ var self = this;
+
+ if (settings.cmd) {
+ settings.onclick = function() {
+ self.execCommand(settings.cmd);
+ };
+ }
+
+ self.menuItems = self.menuItems || {};
+ self.menuItems[name] = settings;
+ },
+
+ /**
+ * Adds a contextual toolbar to be rendered when the selector matches.
+ *
+ * @method addContextToolbar
+ * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates.
+ * @param {String/Array} items String or array with items to add to the context toolbar.
+ */
+ addContextToolbar: function(predicate, items) {
+ var self = this, selector;
+
+ self.contextToolbars = self.contextToolbars || [];
+
+ // Convert selector to predicate
+ if (typeof predicate == "string") {
+ selector = predicate;
+ predicate = function(elm) {
+ return self.dom.is(elm, selector);
+ };
+ }
+
+ self.contextToolbars.push({
+ id: Uuid.uuid('mcet'),
+ predicate: predicate,
+ items: items
+ });
+ },
+
+ /**
+ * Adds a custom command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with execCommand.
+ *
+ * @method addCommand
+ * @param {String} name Command name to add/override.
+ * @param {addCommandCallback} callback Function to execute when the command occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ * @example
+ * // Adds a custom command that later can be executed using execCommand
+ * tinymce.init({
+ * ...
+ *
+ * setup: function(ed) {
+ * // Register example command
+ * ed.addCommand('mycommand', function(ui, v) {
+ * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'}));
+ * });
+ * }
+ * });
+ */
+ addCommand: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a command is executed.
+ *
+ * @callback addCommandCallback
+ * @param {Boolean} ui Display UI state true/false.
+ * @param {Object} value Optional value for command.
+ * @return {Boolean} True/false state if the command was handled or not.
+ */
+ this.editorCommands.addCommand(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query state command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandState function.
+ *
+ * @method addQueryStateHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryStateHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandState is executed.
+ *
+ * @callback addQueryStateHandlerCallback
+ * @return {Boolean} True/false state if the command is enabled or not like is it bold.
+ */
+ this.editorCommands.addQueryStateHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a custom query value command to the editor, you can also override existing commands with this method.
+ * The command that you add can be executed with queryCommandValue function.
+ *
+ * @method addQueryValueHandler
+ * @param {String} name Command name to add/override.
+ * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs.
+ * @param {Object} scope Optional scope to execute the function in.
+ */
+ addQueryValueHandler: function(name, callback, scope) {
+ /**
+ * Callback function that gets called when a queryCommandValue is executed.
+ *
+ * @callback addQueryValueHandlerCallback
+ * @return {Object} Value of the command or undefined.
+ */
+ this.editorCommands.addQueryValueHandler(name, callback, scope);
+ },
+
+ /**
+ * Adds a keyboard shortcut for some command or function.
+ *
+ * @method addShortcut
+ * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
+ * @param {String} desc Text description for the command.
+ * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
+ * @param {Object} sc Optional scope to execute the function in.
+ * @return {Boolean} true/false state if the shortcut was added or not.
+ */
+ addShortcut: function(pattern, desc, cmdFunc, scope) {
+ this.shortcuts.add(pattern, desc, cmdFunc, scope);
+ },
+
+ /**
+ * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or
+ * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org.
+ * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these
+ * return true it will handle the command as a internal browser command.
+ *
+ * @method execCommand
+ * @param {String} cmd Command name to execute, for example mceLink or Bold.
+ * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not.
+ * @param {mixed} value Optional command value, this can be anything.
+ * @param {Object} args Optional arguments object.
+ */
+ execCommand: function(cmd, ui, value, args) {
+ return this.editorCommands.execCommand(cmd, ui, value, args);
+ },
+
+ /**
+ * Returns a command specific state, for example if bold is enabled or not.
+ *
+ * @method queryCommandState
+ * @param {string} cmd Command to query state from.
+ * @return {Boolean} Command specific state, for example if bold is enabled or not.
+ */
+ queryCommandState: function(cmd) {
+ return this.editorCommands.queryCommandState(cmd);
+ },
+
+ /**
+ * Returns a command specific value, for example the current font size.
+ *
+ * @method queryCommandValue
+ * @param {string} cmd Command to query value from.
+ * @return {Object} Command specific value, for example the current font size.
+ */
+ queryCommandValue: function(cmd) {
+ return this.editorCommands.queryCommandValue(cmd);
+ },
+
+ /**
+ * Returns true/false if the command is supported or not.
+ *
+ * @method queryCommandSupported
+ * @param {String} cmd Command that we check support for.
+ * @return {Boolean} true/false if the command is supported or not.
+ */
+ queryCommandSupported: function(cmd) {
+ return this.editorCommands.queryCommandSupported(cmd);
+ },
+
+ /**
+ * Shows the editor and hides any textarea/div that the editor is supposed to replace.
+ *
+ * @method show
+ */
+ show: function() {
+ var self = this;
+
+ if (self.hidden) {
+ self.hidden = false;
+
+ if (self.inline) {
+ self.getBody().contentEditable = true;
+ } else {
+ DOM.show(self.getContainer());
+ DOM.hide(self.id);
+ }
+
+ self.load();
+ self.fire('show');
+ }
+ },
+
+ /**
+ * Hides the editor and shows any textarea/div that the editor is supposed to replace.
+ *
+ * @method hide
+ */
+ hide: function() {
+ var self = this, doc = self.getDoc();
+
+ if (!self.hidden) {
+ // Fixed bug where IE has a blinking cursor left from the editor
+ if (ie && doc && !self.inline) {
+ doc.execCommand('SelectAll');
+ }
+
+ // We must save before we hide so Safari doesn't crash
+ self.save();
+
+ if (self.inline) {
+ self.getBody().contentEditable = false;
+
+ // Make sure the editor gets blurred
+ if (self == self.editorManager.focusedEditor) {
+ self.editorManager.focusedEditor = null;
+ }
+ } else {
+ DOM.hide(self.getContainer());
+ DOM.setStyle(self.id, 'display', self.orgDisplay);
+ }
+
+ self.hidden = true;
+ self.fire('hide');
+ }
+ },
+
+ /**
+ * Returns true/false if the editor is hidden or not.
+ *
+ * @method isHidden
+ * @return {Boolean} True/false if the editor is hidden or not.
+ */
+ isHidden: function() {
+ return !!this.hidden;
+ },
+
+ /**
+ * Sets the progress state, this will display a throbber/progess for the editor.
+ * This is ideal for asynchronous operations like an AJAX save call.
+ *
+ * @method setProgressState
+ * @param {Boolean} state Boolean state if the progress should be shown or hidden.
+ * @param {Number} time Optional time to wait before the progress gets shown.
+ * @return {Boolean} Same as the input state.
+ * @example
+ * // Show progress for the active editor
+ * tinymce.activeEditor.setProgressState(true);
+ *
+ * // Hide progress for the active editor
+ * tinymce.activeEditor.setProgressState(false);
+ *
+ * // Show progress after 3 seconds
+ * tinymce.activeEditor.setProgressState(true, 3000);
+ */
+ setProgressState: function(state, time) {
+ this.fire('ProgressState', {state: state, time: time});
+ },
+
+ /**
+ * Loads contents from the textarea or div element that got converted into an editor instance.
+ * This method will move the contents from that textarea or div into the editor by using setContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method load
+ * @param {Object} args Optional content object, this gets passed around through the whole load process.
+ * @return {String} HTML string that got set into the editor.
+ */
+ load: function(args) {
+ var self = this, elm = self.getElement(), html;
+
+ if (elm) {
+ args = args || {};
+ args.load = true;
+
+ html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args);
+ args.element = elm;
+
+ if (!args.no_events) {
+ self.fire('LoadContent', args);
+ }
+
+ args.element = elm = null;
+
+ return html;
+ }
+ },
+
+ /**
+ * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance.
+ * This method will move the HTML contents from the editor into that textarea or div by getContent
+ * so all events etc that method has will get dispatched as well.
+ *
+ * @method save
+ * @param {Object} args Optional content object, this gets passed around through the whole save process.
+ * @return {String} HTML string that got set into the textarea/div.
+ */
+ save: function(args) {
+ var self = this, elm = self.getElement(), html, form;
+
+ if (!elm || !self.initialized) {
+ return;
+ }
+
+ args = args || {};
+ args.save = true;
+
+ args.element = elm;
+ html = args.content = self.getContent(args);
+
+ if (!args.no_events) {
+ self.fire('SaveContent', args);
+ }
+
+ // Always run this internal event
+ if (args.format == 'raw') {
+ self.fire('RawSaveContent', args);
+ }
+
+ html = args.content;
+
+ if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) {
+ // Update DIV element when not in inline mode
+ if (!self.inline) {
+ elm.innerHTML = html;
+ }
+
+ // Update hidden form element
+ if ((form = DOM.getParent(self.id, 'form'))) {
+ each(form.elements, function(elm) {
+ if (elm.name == self.id) {
+ elm.value = html;
+ return false;
+ }
+ });
+ }
+ } else {
+ elm.value = html;
+ }
+
+ args.element = elm = null;
+
+ if (args.set_dirty !== false) {
+ self.setDirty(false);
+ }
+
+ return html;
+ },
+
+ /**
+ * Sets the specified content to the editor instance, this will cleanup the content before it gets set using
+ * the different cleanup rules options.
+ *
+ * @method setContent
+ * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well.
+ * @param {Object} args Optional content object, this gets passed around through the whole set process.
+ * @return {String} HTML string that got set into the editor.
+ * @example
+ * // Sets the HTML contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('some html');
+ *
+ * // Sets the raw contents of the activeEditor editor
+ * tinymce.activeEditor.setContent('some html', {format: 'raw'});
+ *
+ * // Sets the content of a specific editor (my_editor in this example)
+ * tinymce.get('my_editor').setContent(data);
+ *
+ * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added
+ * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'});
+ */
+ setContent: function(content, args) {
+ var self = this, body = self.getBody(), forcedRootBlockName, padd;
+
+ // Setup args object
+ args = args || {};
+ args.format = args.format || 'html';
+ args.set = true;
+ args.content = content;
+
+ // Do preprocessing
+ if (!args.no_events) {
+ self.fire('BeforeSetContent', args);
+ }
+
+ content = args.content;
+
+ // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content
+ // It will also be impossible to place the caret in the editor unless there is a BR element present
+ if (content.length === 0 || /^\s+$/.test(content)) {
+ padd = ie && ie < 11 ? '' : '
';
+
+ // Todo: There is a lot more root elements that need special padding
+ // so separate this and add all of them at some point.
+ if (body.nodeName == 'TABLE') {
+ content = '
' + padd + ' |
';
+ } else if (/^(UL|OL)$/.test(body.nodeName)) {
+ content = '' + padd + '';
+ }
+
+ forcedRootBlockName = self.settings.forced_root_block;
+
+ // Check if forcedRootBlock is configured and that the block is a valid child of the body
+ if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
+ // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
+ content = padd;
+ content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
+ } else if (!ie && !content) {
+ // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
+ content = '
';
+ }
+
+ self.dom.setHTML(body, content);
+
+ self.fire('SetContent', args);
+ } else {
+ // Parse and serialize the html
+ if (args.format !== 'raw') {
+ content = new Serializer({
+ validate: self.validate
+ }, self.schema).serialize(
+ self.parser.parse(content, {isRootContent: true})
+ );
+ }
+
+ // Set the new cleaned contents to the editor
+ args.content = trim(content);
+ self.dom.setHTML(body, args.content);
+
+ // Do post processing
+ if (!args.no_events) {
+ self.fire('SetContent', args);
+ }
+
+ // Don't normalize selection if the focused element isn't the body in
+ // content editable mode since it will steal focus otherwise
+ /*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
+ self.selection.normalize();
+ }*/
+ }
+
+ return args.content;
+ },
+
+ /**
+ * Gets the content from the editor instance, this will cleanup the content before it gets returned using
+ * the different cleanup rules options.
+ *
+ * @method getContent
+ * @param {Object} args Optional content object, this gets passed around through the whole get process.
+ * @return {String} Cleaned content string, normally HTML contents.
+ * @example
+ * // Get the HTML contents of the currently active editor
+ * console.debug(tinymce.activeEditor.getContent());
+ *
+ * // Get the raw contents of the currently active editor
+ * tinymce.activeEditor.getContent({format: 'raw'});
+ *
+ * // Get content of a specific editor:
+ * tinymce.get('content id').getContent()
+ */
+ getContent: function(args) {
+ var self = this, content, body = self.getBody();
+
+ // Setup args object
+ args = args || {};
+ args.format = args.format || 'html';
+ args.get = true;
+ args.getInner = true;
+
+ // Do preprocessing
+ if (!args.no_events) {
+ self.fire('BeforeGetContent', args);
+ }
+
+ // Get raw contents or by default the cleaned contents
+ if (args.format == 'raw') {
+ content = self.serializer.getTrimmedContent();
+ } else if (args.format == 'text') {
+ content = body.innerText || body.textContent;
+ } else {
+ content = self.serializer.serialize(body, args);
+ }
+
+ // Trim whitespace in beginning/end of HTML
+ if (args.format != 'text') {
+ args.content = trim(content);
+ } else {
+ args.content = content;
+ }
+
+ // Do post processing
+ if (!args.no_events) {
+ self.fire('GetContent', args);
+ }
+
+ return args.content;
+ },
+
+ /**
+ * Inserts content at caret position.
+ *
+ * @method insertContent
+ * @param {String} content Content to insert.
+ * @param {Object} args Optional args to pass to insert call.
+ */
+ insertContent: function(content, args) {
+ if (args) {
+ content = extend({content: content}, args);
+ }
+
+ this.execCommand('mceInsertContent', false, content);
+ },
+
+ /**
+ * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
+ *
+ * The dirty state is automatically set to true if you do modifications to the content in other
+ * words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set
+ * to false if you call editor.save().
+ *
+ * @method isDirty
+ * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
+ * @example
+ * if (tinymce.activeEditor.isDirty())
+ * alert("You must save your contents.");
+ */
+ isDirty: function() {
+ return !this.isNotDirty;
+ },
+
+ /**
+ * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
+ * by invoking this method.
+ *
+ * @method setDirty
+ * @param {Boolean} state True/false if the editor is considered dirty.
+ * @example
+ * function ajaxSave() {
+ * var editor = tinymce.get('elm1');
+ *
+ * // Save contents using some XHR call
+ * alert(editor.getContent());
+ *
+ * editor.setDirty(false); // Force not dirty state
+ * }
+ */
+ setDirty: function(state) {
+ var oldState = !this.isNotDirty;
+
+ this.isNotDirty = !state;
+
+ if (state && state != oldState) {
+ this.fire('dirty');
+ }
+ },
+
+ /**
+ * Sets the editor mode. Mode can be for example "design", "code" or "readonly".
+ *
+ * @method setMode
+ * @param {String} mode Mode to set the editor in.
+ */
+ setMode: function(mode) {
+ Mode.setMode(this, mode);
+ },
+
+ /**
+ * Returns the editors container element. The container element wrappes in
+ * all the elements added to the page for the editor. Such as UI, iframe etc.
+ *
+ * @method getContainer
+ * @return {Element} HTML DOM element for the editor container.
+ */
+ getContainer: function() {
+ var self = this;
+
+ if (!self.container) {
+ self.container = DOM.get(self.editorContainer || self.id + '_parent');
+ }
+
+ return self.container;
+ },
+
+ /**
+ * Returns the editors content area container element. The this element is the one who
+ * holds the iframe or the editable element.
+ *
+ * @method getContentAreaContainer
+ * @return {Element} HTML DOM element for the editor area container.
+ */
+ getContentAreaContainer: function() {
+ return this.contentAreaContainer;
+ },
+
+ /**
+ * Returns the target element/textarea that got replaced with a TinyMCE editor instance.
+ *
+ * @method getElement
+ * @return {Element} HTML DOM element for the replaced element.
+ */
+ getElement: function() {
+ if (!this.targetElm) {
+ this.targetElm = DOM.get(this.id);
+ }
+
+ return this.targetElm;
+ },
+
+ /**
+ * Returns the iframes window object.
+ *
+ * @method getWin
+ * @return {Window} Iframe DOM window object.
+ */
+ getWin: function() {
+ var self = this, elm;
+
+ if (!self.contentWindow) {
+ elm = self.iframeElement;
+
+ if (elm) {
+ self.contentWindow = elm.contentWindow;
+ }
+ }
+
+ return self.contentWindow;
+ },
+
+ /**
+ * Returns the iframes document object.
+ *
+ * @method getDoc
+ * @return {Document} Iframe DOM document object.
+ */
+ getDoc: function() {
+ var self = this, win;
+
+ if (!self.contentDocument) {
+ win = self.getWin();
+
+ if (win) {
+ self.contentDocument = win.document;
+ }
+ }
+
+ return self.contentDocument;
+ },
+
+ /**
+ * Returns the root element of the editable area.
+ * For a non-inline iframe-based editor, returns the iframe's body element.
+ *
+ * @method getBody
+ * @return {Element} The root element of the editable area.
+ */
+ getBody: function() {
+ var doc = this.getDoc();
+ return this.bodyElement || (doc ? doc.body : null);
+ },
+
+ /**
+ * URL converter function this gets executed each time a user adds an img, a or
+ * any other element that has a URL in it. This will be called both by the DOM and HTML
+ * manipulation functions.
+ *
+ * @method convertURL
+ * @param {string} url URL to convert.
+ * @param {string} name Attribute name src, href etc.
+ * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
+ * @return {string} Converted URL string.
+ */
+ convertURL: function(url, name, elm) {
+ var self = this, settings = self.settings;
+
+ // Use callback instead
+ if (settings.urlconverter_callback) {
+ return self.execCallback('urlconverter_callback', url, elm, true, name);
+ }
+
+ // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
+ if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
+ return url;
+ }
+
+ // Convert to relative
+ if (settings.relative_urls) {
+ return self.documentBaseURI.toRelative(url);
+ }
+
+ // Convert to absolute
+ url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
+
+ return url;
+ },
+
+ /**
+ * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
+ *
+ * @method addVisual
+ * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
+ */
+ addVisual: function(elm) {
+ var self = this, settings = self.settings, dom = self.dom, cls;
+
+ elm = elm || self.getBody();
+
+ if (self.hasVisual === undefined) {
+ self.hasVisual = settings.visual;
+ }
+
+ each(dom.select('table,a', elm), function(elm) {
+ var value;
+
+ switch (elm.nodeName) {
+ case 'TABLE':
+ cls = settings.visual_table_class || 'mce-item-table';
+ value = dom.getAttrib(elm, 'border');
+
+ if ((!value || value == '0') && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
+ }
+
+ return;
+
+ case 'A':
+ if (!dom.getAttrib(elm, 'href', false)) {
+ value = dom.getAttrib(elm, 'name') || elm.id;
+ cls = settings.visual_anchor_class || 'mce-item-anchor';
+
+ if (value && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
+ }
+ }
+
+ return;
+ }
+ });
+
+ self.fire('VisualAid', {element: elm, hasVisual: self.hasVisual});
+ },
+
+ /**
+ * Removes the editor from the dom and tinymce collection.
+ *
+ * @method remove
+ */
+ remove: function() {
+ var self = this;
+
+ if (!self.removed) {
+ self.save();
+ self.removed = 1;
+ self.unbindAllNativeEvents();
+
+ // Remove any hidden input
+ if (self.hasHiddenInput) {
+ DOM.remove(self.getElement().nextSibling);
+ }
+
+ if (!self.inline) {
+ // IE 9 has a bug where the selection stops working if you place the
+ // caret inside the editor then remove the iframe
+ if (ie && ie < 10) {
+ self.getDoc().execCommand('SelectAll', false, null);
+ }
+
+ DOM.setStyle(self.id, 'display', self.orgDisplay);
+ self.getBody().onload = null; // Prevent #6816
+ }
+
+ self.fire('remove');
+
+ self.editorManager.remove(self);
+ DOM.remove(self.getContainer());
+ self._selectionOverrides.destroy();
+ self.editorUpload.destroy();
+ self.destroy();
+ }
+ },
+
+ /**
+ * Destroys the editor instance by removing all events, element references or other resources
+ * that could leak memory. This method will be called automatically when the page is unloaded
+ * but you can also call it directly if you know what you are doing.
+ *
+ * @method destroy
+ * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
+ */
+ destroy: function(automatic) {
+ var self = this, form;
+
+ // One time is enough
+ if (self.destroyed) {
+ return;
+ }
+
+ // If user manually calls destroy and not remove
+ // Users seems to have logic that calls destroy instead of remove
+ if (!automatic && !self.removed) {
+ self.remove();
+ return;
+ }
+
+ if (!automatic) {
+ self.editorManager.off('beforeunload', self._beforeUnload);
+
+ // Manual destroy
+ if (self.theme && self.theme.destroy) {
+ self.theme.destroy();
+ }
+
+ // Destroy controls, selection and dom
+ self.selection.destroy();
+ self.dom.destroy();
+ }
+
+ form = self.formElement;
+ if (form) {
+ if (form._mceOldSubmit) {
+ form.submit = form._mceOldSubmit;
+ form._mceOldSubmit = null;
+ }
+
+ DOM.unbind(form, 'submit reset', self.formEventDelegate);
+ }
+
+ self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
+ self.bodyElement = self.contentDocument = self.contentWindow = null;
+ self.iframeElement = self.targetElm = null;
+
+ if (self.selection) {
+ self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
+ }
+
+ self.destroyed = 1;
+ },
+
+ /**
+ * Uploads all data uri/blob uri images in the editor contents to server.
+ *
+ * @method uploadImages
+ * @param {function} callback Optional callback with images and status for each image.
+ * @return {tinymce.util.Promise} Promise instance.
+ */
+ uploadImages: function(callback) {
+ return this.editorUpload.uploadImages(callback);
+ },
+
+ // Internal functions
+
+ _scanForImages: function() {
+ return this.editorUpload.scanForImages();
+ }
+ };
+
+ extend(Editor.prototype, EditorObservable);
+
+ return Editor;
+});
+
+// Included from: js/tinymce/classes/util/I18n.js
+
+/**
+ * I18n.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * I18n class that handles translation of TinyMCE UI.
+ * Uses po style with csharp style parameters.
+ *
+ * @class tinymce.util.I18n
+ */
+define("tinymce/util/I18n", [], function() {
+ "use strict";
+
+ var data = {}, code = "en";
+
+ return {
+ /**
+ * Sets the current language code.
+ *
+ * @method setCode
+ * @param {String} newCode Current language code.
+ */
+ setCode: function(newCode) {
+ if (newCode) {
+ code = newCode;
+ this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
+ }
+ },
+
+ /**
+ * Returns the current language code.
+ *
+ * @method getCode
+ * @return {String} Current language code.
+ */
+ getCode: function() {
+ return code;
+ },
+
+ /**
+ * Property gets set to true if a RTL language pack was loaded.
+ *
+ * @property rtl
+ * @type Boolean
+ */
+ rtl: false,
+
+ /**
+ * Adds translations for a specific language code.
+ *
+ * @method add
+ * @param {String} code Language code like sv_SE.
+ * @param {Array} items Name/value array with English en_US to sv_SE.
+ */
+ add: function(code, items) {
+ var langData = data[code];
+
+ if (!langData) {
+ data[code] = langData = {};
+ }
+
+ for (var name in items) {
+ langData[name] = items[name];
+ }
+
+ this.setCode(code);
+ },
+
+ /**
+ * Translates the specified text.
+ *
+ * It has a few formats:
+ * I18n.translate("Text");
+ * I18n.translate(["Text {0}/{1}", 0, 1]);
+ * I18n.translate({raw: "Raw string"});
+ *
+ * @method translate
+ * @param {String/Object/Array} text Text to translate.
+ * @return {String} String that got translated.
+ */
+ translate: function(text) {
+ var langData;
+
+ langData = data[code];
+ if (!langData) {
+ langData = {};
+ }
+
+ if (typeof text == "undefined") {
+ return text;
+ }
+
+ if (typeof text != "string" && text.raw) {
+ return text.raw;
+ }
+
+ if (text.push) {
+ var values = text.slice(1);
+
+ text = (langData[text[0]] || text[0]).replace(/\{([0-9]+)\}/g, function(match1, match2) {
+ return values[match2];
+ });
+ }
+
+ return (langData[text] || text).replace(/{context:\w+}$/, '');
+ },
+
+ data: data
+ };
+});
+
+// Included from: js/tinymce/classes/FocusManager.js
+
+/**
+ * FocusManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class manages the focus/blur state of the editor. This class is needed since some
+ * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
+ *
+ * This class will fire two events focus and blur on the editor instances that got affected.
+ * It will also handle the restore of selection when the focus is lost and returned.
+ *
+ * @class tinymce.FocusManager
+ */
+define("tinymce/FocusManager", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/Delay",
+ "tinymce/Env"
+], function(DOMUtils, Delay, Env) {
+ var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
+
+ /**
+ * Constructs a new focus manager instance.
+ *
+ * @constructor FocusManager
+ * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
+ */
+ function FocusManager(editorManager) {
+ function getActiveElement() {
+ try {
+ return document.activeElement;
+ } catch (ex) {
+ // IE sometimes fails to get the activeElement when resizing table
+ // TODO: Investigate this
+ return document.body;
+ }
+ }
+
+ // We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
+ // TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
+ function createBookmark(dom, rng) {
+ if (rng && rng.startContainer) {
+ // Verify that the range is within the root of the editor
+ if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
+ return;
+ }
+
+ return {
+ startContainer: rng.startContainer,
+ startOffset: rng.startOffset,
+ endContainer: rng.endContainer,
+ endOffset: rng.endOffset
+ };
+ }
+
+ return rng;
+ }
+
+ function bookmarkToRng(editor, bookmark) {
+ var rng;
+
+ if (bookmark.startContainer) {
+ rng = editor.getDoc().createRange();
+ rng.setStart(bookmark.startContainer, bookmark.startOffset);
+ rng.setEnd(bookmark.endContainer, bookmark.endOffset);
+ } else {
+ rng = bookmark;
+ }
+
+ return rng;
+ }
+
+ function isUIElement(elm) {
+ return !!DOM.getParent(elm, FocusManager.isEditorUIElement);
+ }
+
+ function registerEvents(e) {
+ var editor = e.editor;
+
+ editor.on('init', function() {
+ // Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
+ if (editor.inline || Env.ie) {
+ // Use the onbeforedeactivate event when available since it works better see #7023
+ if ("onbeforedeactivate" in document && Env.ie < 9) {
+ editor.dom.bind(editor.getBody(), 'beforedeactivate', function(e) {
+ if (e.target != editor.getBody()) {
+ return;
+ }
+
+ try {
+ editor.lastRng = editor.selection.getRng();
+ } catch (ex) {
+ // IE throws "Unexcpected call to method or property access" some times so lets ignore it
+ }
+ });
+ } else {
+ // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
+ editor.on('nodechange mouseup keyup', function(e) {
+ var node = getActiveElement();
+
+ // Only act on manual nodechanges
+ if (e.type == 'nodechange' && e.selectionChange) {
+ return;
+ }
+
+ // IE 11 reports active element as iframe not body of iframe
+ if (node && node.id == editor.id + '_ifr') {
+ node = editor.getBody();
+ }
+
+ if (editor.dom.isChildOf(node, editor.getBody())) {
+ editor.lastRng = editor.selection.getRng();
+ }
+ });
+ }
+
+ // Handles the issue with WebKit not retaining selection within inline document
+ // If the user releases the mouse out side the body since a mouse up event wont occur on the body
+ if (Env.webkit && !selectionChangeHandler) {
+ selectionChangeHandler = function() {
+ var activeEditor = editorManager.activeEditor;
+
+ if (activeEditor && activeEditor.selection) {
+ var rng = activeEditor.selection.getRng();
+
+ // Store when it's non collapsed
+ if (rng && !rng.collapsed) {
+ editor.lastRng = rng;
+ }
+ }
+ };
+
+ DOM.bind(document, 'selectionchange', selectionChangeHandler);
+ }
+ }
+ });
+
+ editor.on('setcontent', function() {
+ editor.lastRng = null;
+ });
+
+ // Remove last selection bookmark on mousedown see #6305
+ editor.on('mousedown', function() {
+ editor.selection.lastFocusBookmark = null;
+ });
+
+ editor.on('focusin', function() {
+ var focusedEditor = editorManager.focusedEditor, lastRng;
+
+ if (editor.selection.lastFocusBookmark) {
+ lastRng = bookmarkToRng(editor, editor.selection.lastFocusBookmark);
+ editor.selection.lastFocusBookmark = null;
+ editor.selection.setRng(lastRng);
+ }
+
+ if (focusedEditor != editor) {
+ if (focusedEditor) {
+ focusedEditor.fire('blur', {focusedEditor: editor});
+ }
+
+ editorManager.setActive(editor);
+ editorManager.focusedEditor = editor;
+ editor.fire('focus', {blurredEditor: focusedEditor});
+ editor.focus(true);
+ }
+
+ editor.lastRng = null;
+ });
+
+ editor.on('focusout', function() {
+ Delay.setEditorTimeout(editor, function() {
+ var focusedEditor = editorManager.focusedEditor;
+
+ // Still the same editor the blur was outside any editor UI
+ if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
+ editor.fire('blur', {focusedEditor: null});
+ editorManager.focusedEditor = null;
+
+ // Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
+ if (editor.selection) {
+ editor.selection.lastFocusBookmark = null;
+ }
+ }
+ });
+ });
+
+ // Check if focus is moved to an element outside the active editor by checking if the target node
+ // isn't within the body of the activeEditor nor a UI element such as a dialog child control
+ if (!documentFocusInHandler) {
+ documentFocusInHandler = function(e) {
+ var activeEditor = editorManager.activeEditor, target;
+
+ target = e.target;
+
+ if (activeEditor && target.ownerDocument == document) {
+ // Check to make sure we have a valid selection don't update the bookmark if it's
+ // a focusin to the body of the editor see #7025
+ if (activeEditor.selection && target != activeEditor.getBody()) {
+ activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
+ }
+
+ // Fire a blur event if the element isn't a UI element
+ if (target != document.body && !isUIElement(target) && editorManager.focusedEditor == activeEditor) {
+ activeEditor.fire('blur', {focusedEditor: null});
+ editorManager.focusedEditor = null;
+ }
+ }
+ };
+
+ DOM.bind(document, 'focusin', documentFocusInHandler);
+ }
+
+ // Handle edge case when user starts the selection inside the editor and releases
+ // the mouse outside the editor producing a new selection. This weird workaround is needed since
+ // Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
+ if (editor.inline && !documentMouseUpHandler) {
+ documentMouseUpHandler = function(e) {
+ var activeEditor = editorManager.activeEditor, dom = activeEditor.dom;
+
+ if (activeEditor.inline && dom && !dom.isChildOf(e.target, activeEditor.getBody())) {
+ var rng = activeEditor.selection.getRng();
+
+ if (!rng.collapsed) {
+ activeEditor.lastRng = rng;
+ }
+ }
+ };
+
+ DOM.bind(document, 'mouseup', documentMouseUpHandler);
+ }
+ }
+
+ function unregisterDocumentEvents(e) {
+ if (editorManager.focusedEditor == e.editor) {
+ editorManager.focusedEditor = null;
+ }
+
+ if (!editorManager.activeEditor) {
+ DOM.unbind(document, 'selectionchange', selectionChangeHandler);
+ DOM.unbind(document, 'focusin', documentFocusInHandler);
+ DOM.unbind(document, 'mouseup', documentMouseUpHandler);
+ selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
+ }
+ }
+
+ editorManager.on('AddEditor', registerEvents);
+ editorManager.on('RemoveEditor', unregisterDocumentEvents);
+ }
+
+ /**
+ * Returns true if the specified element is part of the UI for example an button or text input.
+ *
+ * @method isEditorUIElement
+ * @param {Element} elm Element to check if it's part of the UI or not.
+ * @return {Boolean} True/false state if the element is part of the UI or not.
+ */
+ FocusManager.isEditorUIElement = function(elm) {
+ // Needs to be converted to string since svg can have focus: #6776
+ return elm.className.toString().indexOf('mce-') !== -1;
+ };
+
+ return FocusManager;
+});
+
+// Included from: js/tinymce/classes/EditorManager.js
+
+/**
+ * EditorManager.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class used as a factory for manager for tinymce.Editor instances.
+ *
+ * @example
+ * tinymce.EditorManager.init({});
+ *
+ * @class tinymce.EditorManager
+ * @mixes tinymce.util.Observable
+ * @static
+ */
+define("tinymce/EditorManager", [
+ "tinymce/Editor",
+ "tinymce/dom/DomQuery",
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/URI",
+ "tinymce/Env",
+ "tinymce/util/Tools",
+ "tinymce/util/Promise",
+ "tinymce/util/Observable",
+ "tinymce/util/I18n",
+ "tinymce/FocusManager"
+], function(Editor, $, DOMUtils, URI, Env, Tools, Promise, Observable, I18n, FocusManager) {
+ var DOM = DOMUtils.DOM;
+ var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
+ var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
+
+ function globalEventDelegate(e) {
+ each(EditorManager.editors, function(editor) {
+ if (e.type === 'scroll') {
+ editor.fire('ScrollWindow', e);
+ } else {
+ editor.fire('ResizeWindow', e);
+ }
+ });
+ }
+
+ function toggleGlobalEvents(editors, state) {
+ if (state !== boundGlobalEvents) {
+ if (state) {
+ $(window).on('resize scroll', globalEventDelegate);
+ } else {
+ $(window).off('resize scroll', globalEventDelegate);
+ }
+
+ boundGlobalEvents = state;
+ }
+ }
+
+ function removeEditorFromList(editor) {
+ var editors = EditorManager.editors, removedFromList;
+
+ delete editors[editor.id];
+
+ for (var i = 0; i < editors.length; i++) {
+ if (editors[i] == editor) {
+ editors.splice(i, 1);
+ removedFromList = true;
+ break;
+ }
+ }
+
+ // Select another editor since the active one was removed
+ if (EditorManager.activeEditor == editor) {
+ EditorManager.activeEditor = editors[0];
+ }
+
+ // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
+ if (EditorManager.focusedEditor == editor) {
+ EditorManager.focusedEditor = null;
+ }
+
+ return removedFromList;
+ }
+
+ function purgeDestroyedEditor(editor) {
+ // User has manually destroyed the editor lets clean up the mess
+ if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
+ removeEditorFromList(editor);
+ editor.unbindAllNativeEvents();
+ editor.destroy(true);
+ editor.removed = true;
+ editor = null;
+ }
+
+ return editor;
+ }
+
+ EditorManager = {
+ /**
+ * Dom query instance.
+ *
+ * @property $
+ * @type tinymce.dom.DomQuery
+ */
+ $: $,
+
+ /**
+ * Major version of TinyMCE build.
+ *
+ * @property majorVersion
+ * @type String
+ */
+ majorVersion: '4',
+
+ /**
+ * Minor version of TinyMCE build.
+ *
+ * @property minorVersion
+ * @type String
+ */
+ minorVersion: '4.3',
+
+ /**
+ * Release date of TinyMCE build.
+ *
+ * @property releaseDate
+ * @type String
+ */
+ releaseDate: '2016-09-01',
+
+ /**
+ * Collection of editor instances.
+ *
+ * @property editors
+ * @type Object
+ * @example
+ * for (edId in tinymce.editors)
+ * tinymce.editors[edId].save();
+ */
+ editors: [],
+
+ /**
+ * Collection of language pack data.
+ *
+ * @property i18n
+ * @type Object
+ */
+ i18n: I18n,
+
+ /**
+ * Currently active editor instance.
+ *
+ * @property activeEditor
+ * @type tinymce.Editor
+ * @example
+ * tinyMCE.activeEditor.selection.getContent();
+ * tinymce.EditorManager.activeEditor.selection.getContent();
+ */
+ activeEditor: null,
+
+ setup: function() {
+ var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
+
+ // Get base URL for the current document
+ documentBaseURL = URI.getDocumentBaseUrl(document.location);
+
+ // Check if the URL is a document based format like: http://site/dir/file and file:///
+ // leave other formats like applewebdata://... intact
+ if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
+ documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
+
+ if (!/[\/\\]$/.test(documentBaseURL)) {
+ documentBaseURL += '/';
+ }
+ }
+
+ // If tinymce is defined and has a base use that or use the old tinyMCEPreInit
+ preInit = window.tinymce || window.tinyMCEPreInit;
+ if (preInit) {
+ baseURL = preInit.base || preInit.baseURL;
+ suffix = preInit.suffix;
+ } else {
+ // Get base where the tinymce script is located
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; i++) {
+ src = scripts[i].src;
+
+ // Script types supported:
+ // tinymce.js tinymce.min.js tinymce.dev.js
+ // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
+ // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
+ var srcScript = src.substring(src.lastIndexOf('/'));
+ if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
+ if (srcScript.indexOf('.min') != -1) {
+ suffix = '.min';
+ }
+
+ baseURL = src.substring(0, src.lastIndexOf('/'));
+ break;
+ }
+ }
+
+ // We didn't find any baseURL by looking at the script elements
+ // Try to use the document.currentScript as a fallback
+ if (!baseURL && document.currentScript) {
+ src = document.currentScript.src;
+
+ if (src.indexOf('.min') != -1) {
+ suffix = '.min';
+ }
+
+ baseURL = src.substring(0, src.lastIndexOf('/'));
+ }
+ }
+
+ /**
+ * Base URL where the root directory if TinyMCE is located.
+ *
+ * @property baseURL
+ * @type String
+ */
+ self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
+
+ /**
+ * Document base URL where the current document is located.
+ *
+ * @property documentBaseURL
+ * @type String
+ */
+ self.documentBaseURL = documentBaseURL;
+
+ /**
+ * Absolute baseURI for the installation path of TinyMCE.
+ *
+ * @property baseURI
+ * @type tinymce.util.URI
+ */
+ self.baseURI = new URI(self.baseURL);
+
+ /**
+ * Current suffix to add to each plugin/theme that gets loaded for example ".min".
+ *
+ * @property suffix
+ * @type String
+ */
+ self.suffix = suffix;
+
+ self.focusManager = new FocusManager(self);
+ },
+
+ /**
+ * Overrides the default settings for editor instances.
+ *
+ * @method overrideDefaults
+ * @param {Object} defaultSettings Defaults settings object.
+ */
+ overrideDefaults: function(defaultSettings) {
+ var baseUrl, suffix;
+
+ baseUrl = defaultSettings.base_url;
+ if (baseUrl) {
+ this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
+ this.baseURI = new URI(this.baseURL);
+ }
+
+ suffix = defaultSettings.suffix;
+ if (defaultSettings.suffix) {
+ this.suffix = suffix;
+ }
+
+ this.defaultSettings = defaultSettings;
+ },
+
+ /**
+ * Initializes a set of editors. This method will create editors based on various settings.
+ *
+ * @method init
+ * @param {Object} settings Settings object to be passed to each editor instance.
+ * @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
+ * @example
+ * // Initializes a editor using the longer method
+ * tinymce.EditorManager.init({
+ * some_settings : 'some value'
+ * });
+ *
+ * // Initializes a editor instance using the shorter version and with a promise
+ * tinymce.init({
+ * some_settings : 'some value'
+ * }).then(function(editors) {
+ * ...
+ * });
+ */
+ init: function(settings) {
+ var self = this, result, invalidInlineTargets;
+
+ invalidInlineTargets = Tools.makeMap(
+ 'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
+ 'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
+ ' '
+ );
+
+ function isInvalidInlineTarget(settings, elm) {
+ return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
+ }
+
+ function report(msg, elm) {
+ // Log in a non test environment
+ if (window.console && !window.test) {
+ window.console.log(msg, elm);
+ }
+ }
+
+ function createId(elm) {
+ var id = elm.id;
+
+ // Use element id, or unique name or generate a unique id
+ if (!id) {
+ id = elm.name;
+
+ if (id && !DOM.get(id)) {
+ id = elm.name;
+ } else {
+ // Generate unique name
+ id = DOM.uniqueId();
+ }
+
+ elm.setAttribute('id', id);
+ }
+
+ return id;
+ }
+
+ function execCallback(name) {
+ var callback = settings[name];
+
+ if (!callback) {
+ return;
+ }
+
+ return callback.apply(self, Array.prototype.slice.call(arguments, 2));
+ }
+
+ function hasClass(elm, className) {
+ return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
+ }
+
+ function findTargets(settings) {
+ var l, targets = [];
+
+ if (settings.types) {
+ each(settings.types, function(type) {
+ targets = targets.concat(DOM.select(type.selector));
+ });
+
+ return targets;
+ } else if (settings.selector) {
+ return DOM.select(settings.selector);
+ } else if (settings.target) {
+ return [settings.target];
+ }
+
+ // Fallback to old setting
+ switch (settings.mode) {
+ case "exact":
+ l = settings.elements || '';
+
+ if (l.length > 0) {
+ each(explode(l), function(id) {
+ var elm;
+
+ if ((elm = DOM.get(id))) {
+ targets.push(elm);
+ } else {
+ each(document.forms, function(f) {
+ each(f.elements, function(e) {
+ if (e.name === id) {
+ id = 'mce_editor_' + instanceCounter++;
+ DOM.setAttrib(e, 'id', id);
+ targets.push(e);
+ }
+ });
+ });
+ }
+ });
+ }
+ break;
+
+ case "textareas":
+ case "specific_textareas":
+ each(DOM.select('textarea'), function(elm) {
+ if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
+ return;
+ }
+
+ if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
+ targets.push(elm);
+ }
+ });
+ break;
+ }
+
+ return targets;
+ }
+
+ var provideResults = function(editors) {
+ result = editors;
+ };
+
+ function initEditors() {
+ var initCount = 0, editors = [], targets;
+
+ function createEditor(id, settings, targetElm) {
+ var editor = new Editor(id, settings, self);
+
+ editors.push(editor);
+
+ editor.on('init', function() {
+ if (++initCount === targets.length) {
+ provideResults(editors);
+ }
+ });
+
+ editor.targetElm = editor.targetElm || targetElm;
+ editor.render();
+ }
+
+ DOM.unbind(window, 'ready', initEditors);
+ execCallback('onpageload');
+
+ targets = $.unique(findTargets(settings));
+
+ // TODO: Deprecate this one
+ if (settings.types) {
+ each(settings.types, function(type) {
+ Tools.each(targets, function(elm) {
+ if (DOM.is(elm, type.selector)) {
+ createEditor(createId(elm), extend({}, settings, type), elm);
+ return false;
+ }
+
+ return true;
+ });
+ });
+
+ return;
+ }
+
+ Tools.each(targets, function(elm) {
+ purgeDestroyedEditor(self.get(elm.id));
+ });
+
+ targets = Tools.grep(targets, function(elm) {
+ return !self.get(elm.id);
+ });
+
+ each(targets, function(elm) {
+ if (isInvalidInlineTarget(settings, elm)) {
+ report('Could not initialize inline editor on invalid inline target element', elm);
+ } else {
+ createEditor(createId(elm), settings, elm);
+ }
+ });
+ }
+
+ self.settings = settings;
+ DOM.bind(window, 'ready', initEditors);
+
+ return new Promise(function(resolve) {
+ if (result) {
+ resolve(result);
+ } else {
+ provideResults = function(editors) {
+ resolve(editors);
+ };
+ }
+ });
+ },
+
+ /**
+ * Returns a editor instance by id.
+ *
+ * @method get
+ * @param {String/Number} id Editor instance id or index to return.
+ * @return {tinymce.Editor} Editor instance to return.
+ * @example
+ * // Adds an onclick event to an editor by id (shorter version)
+ * tinymce.get('mytextbox').on('click', function(e) {
+ * ed.windowManager.alert('Hello world!');
+ * });
+ *
+ * // Adds an onclick event to an editor by id (longer version)
+ * tinymce.EditorManager.get('mytextbox').on('click', function(e) {
+ * ed.windowManager.alert('Hello world!');
+ * });
+ */
+ get: function(id) {
+ if (!arguments.length) {
+ return this.editors;
+ }
+
+ return id in this.editors ? this.editors[id] : null;
+ },
+
+ /**
+ * Adds an editor instance to the editor collection. This will also set it as the active editor.
+ *
+ * @method add
+ * @param {tinymce.Editor} editor Editor instance to add to the collection.
+ * @return {tinymce.Editor} The same instance that got passed in.
+ */
+ add: function(editor) {
+ var self = this, editors = self.editors;
+
+ // Add named and index editor instance
+ editors[editor.id] = editor;
+ editors.push(editor);
+
+ toggleGlobalEvents(editors, true);
+
+ // Doesn't call setActive method since we don't want
+ // to fire a bunch of activate/deactivate calls while initializing
+ self.activeEditor = editor;
+
+ /**
+ * Fires when an editor is added to the EditorManager collection.
+ *
+ * @event AddEditor
+ * @param {Object} e Event arguments.
+ */
+ self.fire('AddEditor', {editor: editor});
+
+ if (!beforeUnloadDelegate) {
+ beforeUnloadDelegate = function() {
+ self.fire('BeforeUnload');
+ };
+
+ DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
+ }
+
+ return editor;
+ },
+
+ /**
+ * Creates an editor instance and adds it to the EditorManager collection.
+ *
+ * @method createEditor
+ * @param {String} id Instance id to use for editor.
+ * @param {Object} settings Editor instance settings.
+ * @return {tinymce.Editor} Editor instance that got created.
+ */
+ createEditor: function(id, settings) {
+ return this.add(new Editor(id, settings, this));
+ },
+
+ /**
+ * Removes a editor or editors form page.
+ *
+ * @example
+ * // Remove all editors bound to divs
+ * tinymce.remove('div');
+ *
+ * // Remove all editors bound to textareas
+ * tinymce.remove('textarea');
+ *
+ * // Remove all editors
+ * tinymce.remove();
+ *
+ * // Remove specific instance by id
+ * tinymce.remove('#id');
+ *
+ * @method remove
+ * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
+ * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
+ */
+ remove: function(selector) {
+ var self = this, i, editors = self.editors, editor;
+
+ // Remove all editors
+ if (!selector) {
+ for (i = editors.length - 1; i >= 0; i--) {
+ self.remove(editors[i]);
+ }
+
+ return;
+ }
+
+ // Remove editors by selector
+ if (typeof selector == "string") {
+ selector = selector.selector || selector;
+
+ each(DOM.select(selector), function(elm) {
+ editor = editors[elm.id];
+
+ if (editor) {
+ self.remove(editor);
+ }
+ });
+
+ return;
+ }
+
+ // Remove specific editor
+ editor = selector;
+
+ // Not in the collection
+ if (!editors[editor.id]) {
+ return null;
+ }
+
+ /**
+ * Fires when an editor is removed from EditorManager collection.
+ *
+ * @event RemoveEditor
+ * @param {Object} e Event arguments.
+ */
+ if (removeEditorFromList(editor)) {
+ self.fire('RemoveEditor', {editor: editor});
+ }
+
+ if (!editors.length) {
+ DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
+ }
+
+ editor.remove();
+
+ toggleGlobalEvents(editors, editors.length > 0);
+
+ return editor;
+ },
+
+ /**
+ * Executes a specific command on the currently active editor.
+ *
+ * @method execCommand
+ * @param {String} cmd Command to perform for example Bold.
+ * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
+ * @param {String} value Optional value parameter like for example an URL to a link.
+ * @return {Boolean} true/false if the command was executed or not.
+ */
+ execCommand: function(cmd, ui, value) {
+ var self = this, editor = self.get(value);
+
+ // Manager commands
+ switch (cmd) {
+ case "mceAddEditor":
+ if (!self.get(value)) {
+ new Editor(value, self.settings, self).render();
+ }
+
+ return true;
+
+ case "mceRemoveEditor":
+ if (editor) {
+ editor.remove();
+ }
+
+ return true;
+
+ case 'mceToggleEditor':
+ if (!editor) {
+ self.execCommand('mceAddEditor', 0, value);
+ return true;
+ }
+
+ if (editor.isHidden()) {
+ editor.show();
+ } else {
+ editor.hide();
+ }
+
+ return true;
+ }
+
+ // Run command on active editor
+ if (self.activeEditor) {
+ return self.activeEditor.execCommand(cmd, ui, value);
+ }
+
+ return false;
+ },
+
+ /**
+ * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
+ *
+ * @method triggerSave
+ * @example
+ * // Saves all contents
+ * tinyMCE.triggerSave();
+ */
+ triggerSave: function() {
+ each(this.editors, function(editor) {
+ editor.save();
+ });
+ },
+
+ /**
+ * Adds a language pack, this gets called by the loaded language files like en.js.
+ *
+ * @method addI18n
+ * @param {String} code Optional language code.
+ * @param {Object} items Name/value object with translations.
+ */
+ addI18n: function(code, items) {
+ I18n.add(code, items);
+ },
+
+ /**
+ * Translates the specified string using the language pack items.
+ *
+ * @method translate
+ * @param {String/Array/Object} text String to translate
+ * @return {String} Translated string.
+ */
+ translate: function(text) {
+ return I18n.translate(text);
+ },
+
+ /**
+ * Sets the active editor instance and fires the deactivate/activate events.
+ *
+ * @method setActive
+ * @param {tinymce.Editor} editor Editor instance to set as the active instance.
+ */
+ setActive: function(editor) {
+ var activeEditor = this.activeEditor;
+
+ if (this.activeEditor != editor) {
+ if (activeEditor) {
+ activeEditor.fire('deactivate', {relatedTarget: editor});
+ }
+
+ editor.fire('activate', {relatedTarget: activeEditor});
+ }
+
+ this.activeEditor = editor;
+ }
+ };
+
+ extend(EditorManager, Observable);
+
+ EditorManager.setup();
+
+ // Export EditorManager as tinymce/tinymce in global namespace
+ window.tinymce = window.tinyMCE = EditorManager;
+
+ return EditorManager;
+});
+
+// Included from: js/tinymce/classes/LegacyInput.js
+
+/**
+ * LegacyInput.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Converts legacy input to modern HTML.
+ *
+ * @class tinymce.LegacyInput
+ * @private
+ */
+define("tinymce/LegacyInput", [
+ "tinymce/EditorManager",
+ "tinymce/util/Tools"
+], function(EditorManager, Tools) {
+ var each = Tools.each, explode = Tools.explode;
+
+ EditorManager.on('AddEditor', function(e) {
+ var editor = e.editor;
+
+ editor.on('preInit', function() {
+ var filters, fontSizes, dom, settings = editor.settings;
+
+ function replaceWithSpan(node, styles) {
+ each(styles, function(value, name) {
+ if (value) {
+ dom.setStyle(node, name, value);
+ }
+ });
+
+ dom.rename(node, 'span');
+ }
+
+ function convert(e) {
+ dom = editor.dom;
+
+ if (settings.convert_fonts_to_spans) {
+ each(dom.select('font,u,strike', e.node), function(node) {
+ filters[node.nodeName.toLowerCase()](dom, node);
+ });
+ }
+ }
+
+ if (settings.inline_styles) {
+ fontSizes = explode(settings.font_size_legacy_values);
+
+ filters = {
+ font: function(dom, node) {
+ replaceWithSpan(node, {
+ backgroundColor: node.style.backgroundColor,
+ color: node.color,
+ fontFamily: node.face,
+ fontSize: fontSizes[parseInt(node.size, 10) - 1]
+ });
+ },
+
+ u: function(dom, node) {
+ // HTML5 allows U element
+ if (editor.settings.schema === "html4") {
+ replaceWithSpan(node, {
+ textDecoration: 'underline'
+ });
+ }
+ },
+
+ strike: function(dom, node) {
+ replaceWithSpan(node, {
+ textDecoration: 'line-through'
+ });
+ }
+ };
+
+ editor.on('PreProcess SetContent', convert);
+ }
+ });
+ });
+});
+
+// Included from: js/tinymce/classes/util/XHR.js
+
+/**
+ * XHR.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to send XMLHTTPRequests cross browser.
+ * @class tinymce.util.XHR
+ * @mixes tinymce.util.Observable
+ * @static
+ * @example
+ * // Sends a low level Ajax request
+ * tinymce.util.XHR.send({
+ * url: 'someurl',
+ * success: function(text) {
+ * console.debug(text);
+ * }
+ * });
+ *
+ * // Add custom header to XHR request
+ * tinymce.util.XHR.on('beforeSend', function(e) {
+ * e.xhr.setRequestHeader('X-Requested-With', 'Something');
+ * });
+ */
+define("tinymce/util/XHR", [
+ "tinymce/util/Observable",
+ "tinymce/util/Tools"
+], function(Observable, Tools) {
+ var XHR = {
+ /**
+ * Sends a XMLHTTPRequest.
+ * Consult the Wiki for details on what settings this method takes.
+ *
+ * @method send
+ * @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
+ */
+ send: function(settings) {
+ var xhr, count = 0;
+
+ function ready() {
+ if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
+ if (settings.success && count < 10000 && xhr.status == 200) {
+ settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
+ } else if (settings.error) {
+ settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
+ }
+
+ xhr = null;
+ } else {
+ setTimeout(ready, 10);
+ }
+ }
+
+ // Default settings
+ settings.scope = settings.scope || this;
+ settings.success_scope = settings.success_scope || settings.scope;
+ settings.error_scope = settings.error_scope || settings.scope;
+ settings.async = settings.async === false ? false : true;
+ settings.data = settings.data || '';
+
+ XHR.fire('beforeInitialize', {settings: settings});
+
+ xhr = new XMLHttpRequest();
+
+ if (xhr) {
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType(settings.content_type);
+ }
+
+ xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
+
+ if (settings.crossDomain) {
+ xhr.withCredentials = true;
+ }
+
+ if (settings.content_type) {
+ xhr.setRequestHeader('Content-Type', settings.content_type);
+ }
+
+ if (settings.requestheaders) {
+ Tools.each(settings.requestheaders, function(header) {
+ xhr.setRequestHeader(header.key, header.value);
+ });
+ }
+
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+
+ xhr = XHR.fire('beforeSend', {xhr: xhr, settings: settings}).xhr;
+ xhr.send(settings.data);
+
+ // Syncronous request
+ if (!settings.async) {
+ return ready();
+ }
+
+ // Wait for response, onReadyStateChange can not be used since it leaks memory in IE
+ setTimeout(ready, 10);
+ }
+ }
+ };
+
+ Tools.extend(XHR, Observable);
+
+ return XHR;
+});
+
+// Included from: js/tinymce/classes/util/JSON.js
+
+/**
+ * JSON.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * JSON parser and serializer class.
+ *
+ * @class tinymce.util.JSON
+ * @static
+ * @example
+ * // JSON parse a string into an object
+ * var obj = tinymce.util.JSON.parse(somestring);
+ *
+ * // JSON serialize a object into an string
+ * var str = tinymce.util.JSON.serialize(obj);
+ */
+define("tinymce/util/JSON", [], function() {
+ function serialize(o, quote) {
+ var i, v, t, name;
+
+ quote = quote || '"';
+
+ if (o === null) {
+ return 'null';
+ }
+
+ t = typeof o;
+
+ if (t == 'string') {
+ v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
+
+ /*eslint no-control-regex:0 */
+ return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
+ // Make sure single quotes never get encoded inside double quotes for JSON compatibility
+ if (quote === '"' && a === "'") {
+ return a;
+ }
+
+ i = v.indexOf(b);
+
+ if (i + 1) {
+ return '\\' + v.charAt(i + 1);
+ }
+
+ a = b.charCodeAt().toString(16);
+
+ return '\\u' + '0000'.substring(a.length) + a;
+ }) + quote;
+ }
+
+ if (t == 'object') {
+ if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
+ for (i = 0, v = '['; i < o.length; i++) {
+ v += (i > 0 ? ',' : '') + serialize(o[i], quote);
+ }
+
+ return v + ']';
+ }
+
+ v = '{';
+
+ for (name in o) {
+ if (o.hasOwnProperty(name)) {
+ v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
+ quote + ':' + serialize(o[name], quote) : '';
+ }
+ }
+
+ return v + '}';
+ }
+
+ return '' + o;
+ }
+
+ return {
+ /**
+ * Serializes the specified object as a JSON string.
+ *
+ * @method serialize
+ * @param {Object} obj Object to serialize as a JSON string.
+ * @param {String} quote Optional quote string defaults to ".
+ * @return {string} JSON string serialized from input.
+ */
+ serialize: serialize,
+
+ /**
+ * Unserializes/parses the specified JSON string into a object.
+ *
+ * @method parse
+ * @param {string} s JSON String to parse into a JavaScript object.
+ * @return {Object} Object from input JSON string or undefined if it failed.
+ */
+ parse: function(text) {
+ try {
+ // Trick uglify JS
+ return window[String.fromCharCode(101) + 'val']('(' + text + ')');
+ } catch (ex) {
+ // Ignore
+ }
+ }
+
+ /**#@-*/
+ };
+});
+
+// Included from: js/tinymce/classes/util/JSONRequest.js
+
+/**
+ * JSONRequest.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to use JSON-RPC to call backend methods.
+ *
+ * @class tinymce.util.JSONRequest
+ * @example
+ * var json = new tinymce.util.JSONRequest({
+ * url: 'somebackend.php'
+ * });
+ *
+ * // Send RPC call 1
+ * json.send({
+ * method: 'someMethod1',
+ * params: ['a', 'b'],
+ * success: function(result) {
+ * console.dir(result);
+ * }
+ * });
+ *
+ * // Send RPC call 2
+ * json.send({
+ * method: 'someMethod2',
+ * params: ['a', 'b'],
+ * success: function(result) {
+ * console.dir(result);
+ * }
+ * });
+ */
+define("tinymce/util/JSONRequest", [
+ "tinymce/util/JSON",
+ "tinymce/util/XHR",
+ "tinymce/util/Tools"
+], function(JSON, XHR, Tools) {
+ var extend = Tools.extend;
+
+ function JSONRequest(settings) {
+ this.settings = extend({}, settings);
+ this.count = 0;
+ }
+
+ /**
+ * Simple helper function to send a JSON-RPC request without the need to initialize an object.
+ * Consult the Wiki API documentation for more details on what you can pass to this function.
+ *
+ * @method sendRPC
+ * @static
+ * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
+ */
+ JSONRequest.sendRPC = function(o) {
+ return new JSONRequest().send(o);
+ };
+
+ JSONRequest.prototype = {
+ /**
+ * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
+ *
+ * @method send
+ * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
+ */
+ send: function(args) {
+ var ecb = args.error, scb = args.success;
+
+ args = extend(this.settings, args);
+
+ args.success = function(c, x) {
+ c = JSON.parse(c);
+
+ if (typeof c == 'undefined') {
+ c = {
+ error: 'JSON Parse error.'
+ };
+ }
+
+ if (c.error) {
+ ecb.call(args.error_scope || args.scope, c.error, x);
+ } else {
+ scb.call(args.success_scope || args.scope, c.result);
+ }
+ };
+
+ args.error = function(ty, x) {
+ if (ecb) {
+ ecb.call(args.error_scope || args.scope, ty, x);
+ }
+ };
+
+ args.data = JSON.serialize({
+ id: args.id || 'c' + (this.count++),
+ method: args.method,
+ params: args.params
+ });
+
+ // JSON content type for Ruby on rails. Bug: #1883287
+ args.content_type = 'application/json';
+
+ XHR.send(args);
+ }
+ };
+
+ return JSONRequest;
+});
+
+// Included from: js/tinymce/classes/util/JSONP.js
+
+/**
+ * JSONP.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define("tinymce/util/JSONP", [
+ "tinymce/dom/DOMUtils"
+], function(DOMUtils) {
+ return {
+ callbacks: {},
+ count: 0,
+
+ send: function(settings) {
+ var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
+ var id = 'tinymce_jsonp_' + count;
+
+ self.callbacks[count] = function(json) {
+ dom.remove(id);
+ delete self.callbacks[count];
+
+ settings.callback(json);
+ };
+
+ dom.add(dom.doc.body, 'script', {
+ id: id,
+ src: settings.url,
+ type: 'text/javascript'
+ });
+
+ self.count++;
+ }
+ };
+});
+
+// Included from: js/tinymce/classes/util/LocalStorage.js
+
+/**
+ * LocalStorage.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
+ * Storage is done using userData on IE 7 and a special serialization format. The format is designed
+ * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
+ * makes it possible to store for example HTML data.
+ *
+ * Storage format for userData:
+ * ,,,,...
+ *
+ * For example this data key1=value1,key2=value2 would be:
+ * 4,key1,6,value1,4,key2,6,value2
+ *
+ * @class tinymce.util.LocalStorage
+ * @static
+ * @version 4.0
+ * @example
+ * tinymce.util.LocalStorage.setItem('key', 'value');
+ * var value = tinymce.util.LocalStorage.getItem('key');
+ */
+define("tinymce/util/LocalStorage", [], function() {
+ var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
+
+ // Check for native support
+ try {
+ if (window.localStorage) {
+ return localStorage;
+ }
+ } catch (ex) {
+ // Ignore
+ }
+
+ userDataKey = "tinymce";
+ storageElm = document.documentElement;
+ hasOldIEDataSupport = !!storageElm.addBehavior;
+
+ if (hasOldIEDataSupport) {
+ storageElm.addBehavior('#default#userData');
+ }
+
+ /**
+ * Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
+ */
+ function updateKeys() {
+ keys = [];
+
+ for (var key in items) {
+ keys.push(key);
+ }
+
+ LocalStorage.length = keys.length;
+ }
+
+ /**
+ * Loads the userData string and parses it into the items structure.
+ */
+ function load() {
+ var key, data, value, pos = 0;
+
+ items = {};
+
+ // localStorage can be disabled on WebKit/Gecko so make a dummy storage
+ if (!hasOldIEDataSupport) {
+ return;
+ }
+
+ function next(end) {
+ var value, nextPos;
+
+ nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
+ if (nextPos === -1 || nextPos > data.length) {
+ return null;
+ }
+
+ value = data.substring(pos, nextPos);
+ pos = nextPos + 1;
+
+ return value;
+ }
+
+ storageElm.load(userDataKey);
+ data = storageElm.getAttribute(userDataKey) || '';
+
+ do {
+ var offset = next();
+ if (offset === null) {
+ break;
+ }
+
+ key = next(parseInt(offset, 32) || 0);
+ if (key !== null) {
+ offset = next();
+ if (offset === null) {
+ break;
+ }
+
+ value = next(parseInt(offset, 32) || 0);
+
+ if (key) {
+ items[key] = value;
+ }
+ }
+ } while (key !== null);
+
+ updateKeys();
+ }
+
+ /**
+ * Saves the items structure into a the userData format.
+ */
+ function save() {
+ var value, data = '';
+
+ // localStorage can be disabled on WebKit/Gecko so make a dummy storage
+ if (!hasOldIEDataSupport) {
+ return;
+ }
+
+ for (var key in items) {
+ value = items[key];
+ data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
+ }
+
+ storageElm.setAttribute(userDataKey, data);
+
+ try {
+ storageElm.save(userDataKey);
+ } catch (ex) {
+ // Ignore disk full
+ }
+
+ updateKeys();
+ }
+
+ LocalStorage = {
+ /**
+ * Length of the number of items in storage.
+ *
+ * @property length
+ * @type Number
+ * @return {Number} Number of items in storage.
+ */
+ //length:0,
+
+ /**
+ * Returns the key name by index.
+ *
+ * @method key
+ * @param {Number} index Index of key to return.
+ * @return {String} Key value or null if it wasn't found.
+ */
+ key: function(index) {
+ return keys[index];
+ },
+
+ /**
+ * Returns the value if the specified key or null if it wasn't found.
+ *
+ * @method getItem
+ * @param {String} key Key of item to retrieve.
+ * @return {String} Value of the specified item or null if it wasn't found.
+ */
+ getItem: function(key) {
+ return key in items ? items[key] : null;
+ },
+
+ /**
+ * Sets the value of the specified item by it's key.
+ *
+ * @method setItem
+ * @param {String} key Key of the item to set.
+ * @param {String} value Value of the item to set.
+ */
+ setItem: function(key, value) {
+ items[key] = "" + value;
+ save();
+ },
+
+ /**
+ * Removes the specified item by key.
+ *
+ * @method removeItem
+ * @param {String} key Key of item to remove.
+ */
+ removeItem: function(key) {
+ delete items[key];
+ save();
+ },
+
+ /**
+ * Removes all items.
+ *
+ * @method clear
+ */
+ clear: function() {
+ items = {};
+ save();
+ }
+ };
+
+ load();
+
+ return LocalStorage;
+});
+
+// Included from: js/tinymce/classes/Compat.js
+
+/**
+ * Compat.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * TinyMCE core class.
+ *
+ * @static
+ * @class tinymce
+ * @borrow-members tinymce.EditorManager
+ * @borrow-members tinymce.util.Tools
+ */
+define("tinymce/Compat", [
+ "tinymce/dom/DOMUtils",
+ "tinymce/dom/EventUtils",
+ "tinymce/dom/ScriptLoader",
+ "tinymce/AddOnManager",
+ "tinymce/util/Tools",
+ "tinymce/Env"
+], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
+ var tinymce = window.tinymce;
+
+ /**
+ * @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
+ * @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
+ * @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
+ * @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
+ */
+ tinymce.DOM = DOMUtils.DOM;
+ tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
+ tinymce.PluginManager = AddOnManager.PluginManager;
+ tinymce.ThemeManager = AddOnManager.ThemeManager;
+
+ tinymce.dom = tinymce.dom || {};
+ tinymce.dom.Event = EventUtils.Event;
+
+ Tools.each(Tools, function(func, key) {
+ tinymce[key] = func;
+ });
+
+ Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) {
+ tinymce[name] = Env[name.substr(2).toLowerCase()];
+ });
+
+ return {};
+});
+
+// Describe the different namespaces
+
+/**
+ * Root level namespace this contains classes directly related to the TinyMCE editor.
+ *
+ * @namespace tinymce
+ */
+
+/**
+ * Contains classes for handling the browsers DOM.
+ *
+ * @namespace tinymce.dom
+ */
+
+/**
+ * Contains html parser and serializer logic.
+ *
+ * @namespace tinymce.html
+ */
+
+/**
+ * Contains the different UI types such as buttons, listboxes etc.
+ *
+ * @namespace tinymce.ui
+ */
+
+/**
+ * Contains various utility classes such as json parser, cookies etc.
+ *
+ * @namespace tinymce.util
+ */
+
+/**
+ * Contains modules to handle data binding.
+ *
+ * @namespace tinymce.data
+ */
+
+// Included from: js/tinymce/classes/ui/Layout.js
+
+/**
+ * Layout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Base layout manager class.
+ *
+ * @class tinymce.ui.Layout
+ */
+define("tinymce/ui/Layout", [
+ "tinymce/util/Class",
+ "tinymce/util/Tools"
+], function(Class, Tools) {
+ "use strict";
+
+ return Class.extend({
+ Defaults: {
+ firstControlClass: 'first',
+ lastControlClass: 'last'
+ },
+
+ /**
+ * Constructs a layout instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ this.settings = Tools.extend({}, this.Defaults, settings);
+ },
+
+ /**
+ * This method gets invoked before the layout renders the controls.
+ *
+ * @method preRender
+ * @param {tinymce.ui.Container} container Container instance to preRender.
+ */
+ preRender: function(container) {
+ container.bodyClasses.add(this.settings.containerClass);
+ },
+
+ /**
+ * Applies layout classes to the container.
+ *
+ * @private
+ */
+ applyClasses: function(items) {
+ var self = this, settings = self.settings, firstClass, lastClass, firstItem, lastItem;
+
+ firstClass = settings.firstControlClass;
+ lastClass = settings.lastControlClass;
+
+ items.each(function(item) {
+ item.classes.remove(firstClass).remove(lastClass).add(settings.controlClass);
+
+ if (item.visible()) {
+ if (!firstItem) {
+ firstItem = item;
+ }
+
+ lastItem = item;
+ }
+ });
+
+ if (firstItem) {
+ firstItem.classes.add(firstClass);
+ }
+
+ if (lastItem) {
+ lastItem.classes.add(lastClass);
+ }
+ },
+
+ /**
+ * Renders the specified container and any layout specific HTML.
+ *
+ * @method renderHtml
+ * @param {tinymce.ui.Container} container Container to render HTML for.
+ */
+ renderHtml: function(container) {
+ var self = this, html = '';
+
+ self.applyClasses(container.items());
+
+ container.items().each(function(item) {
+ html += item.renderHtml();
+ });
+
+ return html;
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function() {
+ },
+
+ /**
+ * This method gets invoked after the layout renders the controls.
+ *
+ * @method postRender
+ * @param {tinymce.ui.Container} container Container instance to postRender.
+ */
+ postRender: function() {
+ },
+
+ isNative: function() {
+ return false;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/AbsoluteLayout.js
+
+/**
+ * AbsoluteLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * LayoutManager for absolute positioning. This layout manager is more of
+ * a base class for other layouts but can be created and used directly.
+ *
+ * @-x-less AbsoluteLayout.less
+ * @class tinymce.ui.AbsoluteLayout
+ * @extends tinymce.ui.Layout
+ */
+define("tinymce/ui/AbsoluteLayout", [
+ "tinymce/ui/Layout"
+], function(Layout) {
+ "use strict";
+
+ return Layout.extend({
+ Defaults: {
+ containerClass: 'abs-layout',
+ controlClass: 'abs-layout-item'
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ container.items().filter(':visible').each(function(ctrl) {
+ var settings = ctrl.settings;
+
+ ctrl.layoutRect({
+ x: settings.x,
+ y: settings.y,
+ w: settings.w,
+ h: settings.h
+ });
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ },
+
+ /**
+ * Renders the specified container and any layout specific HTML.
+ *
+ * @method renderHtml
+ * @param {tinymce.ui.Container} container Container to render HTML for.
+ */
+ renderHtml: function(container) {
+ return '' + this._super(container);
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Button.js
+
+/**
+ * Button.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is used to create buttons. You can create them directly or through the Factory.
+ *
+ * @example
+ * // Create and render a button to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'button',
+ * text: 'My button'
+ * }).renderTo(document.body);
+ *
+ * @-x-less Button.less
+ * @class tinymce.ui.Button
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Button", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "widget btn",
+ role: "button"
+ },
+
+ /**
+ * Constructs a new button instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} size Size of the button small|medium|large.
+ * @setting {String} image Image to use for icon.
+ * @setting {String} icon Icon to use for button.
+ */
+ init: function(settings) {
+ var self = this, size;
+
+ self._super(settings);
+ settings = self.settings;
+
+ size = self.settings.size;
+
+ self.on('click mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ self.on('touchstart', function(e) {
+ self.fire('click', e);
+ e.preventDefault();
+ });
+
+ if (settings.subtype) {
+ self.classes.add(settings.subtype);
+ }
+
+ if (size) {
+ self.classes.add('btn-' + size);
+ }
+
+ if (settings.icon) {
+ self.icon(settings.icon);
+ }
+ },
+
+ /**
+ * Sets/gets the current button icon.
+ *
+ * @method icon
+ * @param {String} [icon] New icon identifier.
+ * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance.
+ */
+ icon: function(icon) {
+ if (!arguments.length) {
+ return this.state.get('icon');
+ }
+
+ this.state.set('icon', icon);
+
+ return this;
+ },
+
+ /**
+ * Repaints the button for example after it's been resizes by a layout engine.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var btnElm = this.getEl().firstChild,
+ btnStyle;
+
+ if (btnElm) {
+ btnStyle = btnElm.style;
+ btnStyle.width = btnStyle.height = "100%";
+ }
+
+ this._super();
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+ var icon = self.state.get('icon'), image, text = self.state.get('text'), textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '' + self.encode(text) + '';
+ }
+
+ icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ return (
+ '' +
+ '' +
+ '
'
+ );
+ },
+
+ bindStates: function() {
+ var self = this, $ = self.$, textCls = self.classPrefix + 'txt';
+
+ function setButtonText(text) {
+ var $span = $('span.' + textCls, self.getEl());
+
+ if (text) {
+ if (!$span[0]) {
+ $('button:first', self.getEl()).append('');
+ $span = $('span.' + textCls, self.getEl());
+ }
+
+ $span.html(self.encode(text));
+ } else {
+ $span.remove();
+ }
+
+ self.classes.toggle('btn-has-text', !!text);
+ }
+
+ self.state.on('change:text', function(e) {
+ setButtonText(e.value);
+ });
+
+ self.state.on('change:icon', function(e) {
+ var icon = e.value, prefix = self.classPrefix;
+
+ self.settings.icon = icon;
+ icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+
+ var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
+
+ if (icon) {
+ if (!iconElm || iconElm != btnElm.firstChild) {
+ iconElm = document.createElement('i');
+ btnElm.insertBefore(iconElm, btnElm.firstChild);
+ }
+
+ iconElm.className = icon;
+ } else if (iconElm) {
+ btnElm.removeChild(iconElm);
+ }
+
+ setButtonText(self.state.get('text'));
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ButtonGroup.js
+
+/**
+ * ButtonGroup.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control enables you to put multiple buttons into a group. This is
+ * useful when you want to combine similar toolbar buttons into a group.
+ *
+ * @example
+ * // Create and render a buttongroup with two buttons to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'buttongroup',
+ * items: [
+ * {text: 'Button A'},
+ * {text: 'Button B'}
+ * ]
+ * }).renderTo(document.body);
+ *
+ * @-x-less ButtonGroup.less
+ * @class tinymce.ui.ButtonGroup
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/ButtonGroup", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ defaultType: 'button',
+ role: 'group'
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout;
+
+ self.classes.add('btn-group');
+ self.preRender();
+ layout.preRender(self);
+
+ return (
+ '' +
+ '
' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '
' +
+ '
'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Checkbox.js
+
+/**
+ * Checkbox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control creates a custom checkbox.
+ *
+ * @example
+ * // Create and render a checkbox to the body element
+ * tinymce.ui.Factory.create({
+ * type: 'checkbox',
+ * checked: true,
+ * text: 'My checkbox'
+ * }).renderTo(document.body);
+ *
+ * @-x-less Checkbox.less
+ * @class tinymce.ui.Checkbox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Checkbox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "checkbox",
+ role: "checkbox",
+ checked: false
+ },
+
+ /**
+ * Constructs a new Checkbox instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} checked True if the checkbox should be checked by default.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ self.on('click mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ self.on('click', function(e) {
+ e.preventDefault();
+
+ if (!self.disabled()) {
+ self.checked(!self.checked());
+ }
+ });
+
+ self.checked(self.settings.checked);
+ },
+
+ /**
+ * Getter/setter function for the checked state.
+ *
+ * @method checked
+ * @param {Boolean} [state] State to be set.
+ * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
+ */
+ checked: function(state) {
+ if (!arguments.length) {
+ return this.state.get('checked');
+ }
+
+ this.state.set('checked', state);
+
+ return this;
+ },
+
+ /**
+ * Getter/setter function for the value state.
+ *
+ * @method value
+ * @param {Boolean} [state] State to be set.
+ * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
+ */
+ value: function(state) {
+ if (!arguments.length) {
+ return this.checked();
+ }
+
+ return this.checked(state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+
+ return (
+ '' +
+ '' +
+ '' + self.encode(self.state.get('text')) + '' +
+ '
'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ function checked(state) {
+ self.classes.toggle("checked", state);
+ self.aria('checked', state);
+ }
+
+ self.state.on('change:text', function(e) {
+ self.getEl('al').firstChild.data = self.translate(e.value);
+ });
+
+ self.state.on('change:checked change:value', function(e) {
+ self.fire('change');
+ checked(e.value);
+ });
+
+ self.state.on('change:icon', function(e) {
+ var icon = e.value, prefix = self.classPrefix;
+
+ if (typeof icon == 'undefined') {
+ return self.settings.icon;
+ }
+
+ self.settings.icon = icon;
+ icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+
+ var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
+
+ if (icon) {
+ if (!iconElm || iconElm != btnElm.firstChild) {
+ iconElm = document.createElement('i');
+ btnElm.insertBefore(iconElm, btnElm.firstChild);
+ }
+
+ iconElm.className = icon;
+ } else if (iconElm) {
+ btnElm.removeChild(iconElm);
+ }
+ });
+
+ if (self.state.get('checked')) {
+ checked(true);
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ComboBox.js
+
+/**
+ * ComboBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a combobox control. Select box that you select a value from or
+ * type a value into.
+ *
+ * @-x-less ComboBox.less
+ * @class tinymce.ui.ComboBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ComboBox", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/Factory",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery"
+], function(Widget, Factory, DomUtils, $) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} placeholder Placeholder text to display.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ settings = self.settings;
+
+ self.classes.add('combobox');
+ self.subinput = true;
+ self.ariaTarget = 'inp'; // TODO: Figure out a better way
+
+ settings.menu = settings.menu || settings.values;
+
+ if (settings.menu) {
+ settings.icon = 'caret';
+ }
+
+ self.on('click', function(e) {
+ var elm = e.target, root = self.getEl();
+
+ if (!$.contains(root, elm) && elm != root) {
+ return;
+ }
+
+ while (elm && elm != root) {
+ if (elm.id && elm.id.indexOf('-open') != -1) {
+ self.fire('action');
+
+ if (settings.menu) {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.items()[0].focus();
+ }
+ }
+ }
+
+ elm = elm.parentNode;
+ }
+ });
+
+ // TODO: Rework this
+ self.on('keydown', function(e) {
+ if (e.target.nodeName == "INPUT" && e.keyCode == 13) {
+ self.parents().reverse().each(function(ctrl) {
+ var stateValue = self.state.get('value'), inputValue = self.getEl('inp').value;
+
+ e.preventDefault();
+
+ self.state.set('value', inputValue);
+
+ if (stateValue != inputValue) {
+ self.fire('change');
+ }
+
+ if (ctrl.hasEventListeners('submit') && ctrl.toJSON) {
+ ctrl.fire('submit', {data: ctrl.toJSON()});
+ return false;
+ }
+ });
+ }
+ });
+
+ self.on('keyup', function(e) {
+ if (e.target.nodeName == "INPUT") {
+ self.state.set('value', e.target.value);
+ }
+ });
+ },
+
+ showMenu: function() {
+ var self = this, settings = self.settings, menu;
+
+ if (!self.menu) {
+ menu = settings.menu || [];
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm());
+ self.fire('createmenu');
+ self.menu.reflow();
+ self.menu.on('cancel', function(e) {
+ if (e.control === self.menu) {
+ self.focus();
+ }
+ });
+
+ self.menu.on('show hide', function(e) {
+ e.control.items().each(function(ctrl) {
+ ctrl.active(ctrl.value() == self.value());
+ });
+ }).fire('show');
+
+ self.menu.on('select', function(e) {
+ self.value(e.control.value());
+ });
+
+ self.on('focusin', function(e) {
+ if (e.target.tagName.toUpperCase() == 'INPUT') {
+ self.menu.hide();
+ }
+ });
+
+ self.aria('expanded', true);
+ }
+
+ self.menu.show();
+ self.menu.layoutRect({w: self.layoutRect().w});
+ self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
+ },
+
+ /**
+ * Focuses the input area of the control.
+ *
+ * @method focus
+ */
+ focus: function() {
+ this.getEl('inp').focus();
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect();
+ var width, lineHeight;
+
+ if (openElm) {
+ width = rect.w - DomUtils.getSize(openElm).width - 10;
+ } else {
+ width = rect.w - 10;
+ }
+
+ // Detect old IE 7+8 add lineHeight to align caret vertically in the middle
+ var doc = document;
+ if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
+ lineHeight = (self.layoutRect().h - 2) + 'px';
+ }
+
+ $(elm.firstChild).css({
+ width: width,
+ lineHeight: lineHeight
+ });
+
+ self._super();
+
+ return self;
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ComboBox} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this;
+
+ $(this.getEl('inp')).on('change', function(e) {
+ self.state.set('value', e.target.value);
+ self.fire('change', e);
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix;
+ var value = self.state.get('value') || '';
+ var icon, text, openBtnHtml = '', extraAttrs = '';
+
+ if ("spellcheck" in settings) {
+ extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
+ }
+
+ if (settings.maxLength) {
+ extraAttrs += ' maxlength="' + settings.maxLength + '"';
+ }
+
+ if (settings.size) {
+ extraAttrs += ' size="' + settings.size + '"';
+ }
+
+ if (settings.subtype) {
+ extraAttrs += ' type="' + settings.subtype + '"';
+ }
+
+ if (self.disabled()) {
+ extraAttrs += ' disabled="disabled"';
+ }
+
+ icon = settings.icon;
+ if (icon && icon != 'caret') {
+ icon = prefix + 'ico ' + prefix + 'i-' + settings.icon;
+ }
+
+ text = self.state.get('text');
+
+ if (icon || text) {
+ openBtnHtml = (
+ '' +
+ '' +
+ '
'
+ );
+
+ self.classes.add('has-open');
+ }
+
+ return (
+ '' +
+ '' +
+ openBtnHtml +
+ '
'
+ );
+ },
+
+ value: function(value) {
+ if (arguments.length) {
+ this.state.set('value', value);
+ return this;
+ }
+
+ // Make sure the real state is in sync
+ if (this.state.get('rendered')) {
+ this.state.set('value', this.getEl('inp').value);
+ }
+
+ return this.state.get('value');
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.getEl('inp').value != e.value) {
+ self.getEl('inp').value = e.value;
+ }
+ });
+
+ self.state.on('change:disabled', function(e) {
+ self.getEl('inp').disabled = e.value;
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ $(this.getEl('inp')).off();
+ this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ColorBox.js
+
+/**
+ * ColorBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This widget lets you enter colors and browse for colors by pressing the color button. It also displays
+ * a preview of the current color.
+ *
+ * @-x-less ColorBox.less
+ * @class tinymce.ui.ColorBox
+ * @extends tinymce.ui.ComboBox
+ */
+define("tinymce/ui/ColorBox", [
+ "tinymce/ui/ComboBox"
+], function(ComboBox) {
+ "use strict";
+
+ return ComboBox.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ settings.spellcheck = false;
+
+ if (settings.onaction) {
+ settings.icon = 'none';
+ }
+
+ self._super(settings);
+
+ self.classes.add('colorbox');
+ self.on('change keyup postrender', function() {
+ self.repaintColor(self.value());
+ });
+ },
+
+ repaintColor: function(value) {
+ var elm = this.getEl().getElementsByTagName('i')[0];
+
+ if (elm) {
+ try {
+ elm.style.background = value;
+ } catch (ex) {
+ // Ignore
+ }
+ }
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.state.get('rendered')) {
+ self.repaintColor(e.value);
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/PanelButton.js
+
+/**
+ * PanelButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new panel button.
+ *
+ * @class tinymce.ui.PanelButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/PanelButton", [
+ "tinymce/ui/Button",
+ "tinymce/ui/FloatPanel"
+], function(Button, FloatPanel) {
+ "use strict";
+
+ return Button.extend({
+ /**
+ * Shows the panel for the button.
+ *
+ * @method showPanel
+ */
+ showPanel: function() {
+ var self = this, settings = self.settings;
+
+ self.active(true);
+
+ if (!self.panel) {
+ var panelSettings = settings.panel;
+
+ // Wrap panel in grid layout if type if specified
+ // This makes it possible to add forms or other containers directly in the panel option
+ if (panelSettings.type) {
+ panelSettings = {
+ layout: 'grid',
+ items: panelSettings
+ };
+ }
+
+ panelSettings.role = panelSettings.role || 'dialog';
+ panelSettings.popover = true;
+ panelSettings.autohide = true;
+ panelSettings.ariaRoot = true;
+
+ self.panel = new FloatPanel(panelSettings).on('hide', function() {
+ self.active(false);
+ }).on('cancel', function(e) {
+ e.stopPropagation();
+ self.focus();
+ self.hidePanel();
+ }).parent(self).renderTo(self.getContainerElm());
+
+ self.panel.fire('show');
+ self.panel.reflow();
+ } else {
+ self.panel.show();
+ }
+
+ self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
+ },
+
+ /**
+ * Hides the panel for the button.
+ *
+ * @method hidePanel
+ */
+ hidePanel: function() {
+ var self = this;
+
+ if (self.panel) {
+ self.panel.hide();
+ }
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.aria('haspopup', true);
+
+ self.on('click', function(e) {
+ if (e.control === self) {
+ if (self.panel && self.panel.visible()) {
+ self.hidePanel();
+ } else {
+ self.showPanel();
+ self.panel.focus(!!e.aria);
+ }
+ }
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ if (this.panel) {
+ this.panel.remove();
+ this.panel = null;
+ }
+
+ return this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ColorButton.js
+
+/**
+ * ColorButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a color button control. This is a split button in which the main
+ * button has a visual representation of the currently selected color. When clicked
+ * the caret button displays a color picker, allowing the user to select a new color.
+ *
+ * @-x-less ColorButton.less
+ * @class tinymce.ui.ColorButton
+ * @extends tinymce.ui.PanelButton
+ */
+define("tinymce/ui/ColorButton", [
+ "tinymce/ui/PanelButton",
+ "tinymce/dom/DOMUtils"
+], function(PanelButton, DomUtils) {
+ "use strict";
+
+ var DOM = DomUtils.DOM;
+
+ return PanelButton.extend({
+ /**
+ * Constructs a new ColorButton instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ this._super(settings);
+ this.classes.add('colorbutton');
+ },
+
+ /**
+ * Getter/setter for the current color.
+ *
+ * @method color
+ * @param {String} [color] Color to set.
+ * @return {String|tinymce.ui.ColorButton} Current color or current instance.
+ */
+ color: function(color) {
+ if (color) {
+ this._color = color;
+ this.getEl('preview').style.backgroundColor = color;
+ return this;
+ }
+
+ return this._color;
+ },
+
+ /**
+ * Resets the current color.
+ *
+ * @method resetColor
+ * @return {tinymce.ui.ColorButton} Current instance.
+ */
+ resetColor: function() {
+ this._color = null;
+ this.getEl('preview').style.backgroundColor = null;
+ return this;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text');
+ var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
+ var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '',
+ textHtml = '';
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '' + self.encode(text) + '';
+ }
+
+ return (
+ '' +
+ '' +
+ '' +
+ '
'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, onClickHandler = self.settings.onclick;
+
+ self.on('click', function(e) {
+ if (e.aria && e.aria.key == 'down') {
+ return;
+ }
+
+ if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
+ e.stopImmediatePropagation();
+ onClickHandler.call(self, e);
+ }
+ });
+
+ delete self.settings.onclick;
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/util/Color.js
+
+/**
+ * Color.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class lets you parse/serialize colors and convert rgb/hsb.
+ *
+ * @class tinymce.util.Color
+ * @example
+ * var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
+ * var red = new tinymce.util.Color('#FF0000');
+ *
+ * console.log(white.toHex(), red.toHsv());
+ */
+define("tinymce/util/Color", [], function() {
+ var min = Math.min, max = Math.max, round = Math.round;
+
+ /**
+ * Constructs a new color instance.
+ *
+ * @constructor
+ * @method Color
+ * @param {String} value Optional initial value to parse.
+ */
+ function Color(value) {
+ var self = this, r = 0, g = 0, b = 0;
+
+ function rgb2hsv(r, g, b) {
+ var h, s, v, d, minRGB, maxRGB;
+
+ h = 0;
+ s = 0;
+ v = 0;
+ r = r / 255;
+ g = g / 255;
+ b = b / 255;
+
+ minRGB = min(r, min(g, b));
+ maxRGB = max(r, max(g, b));
+
+ if (minRGB == maxRGB) {
+ v = minRGB;
+
+ return {
+ h: 0,
+ s: 0,
+ v: v * 100
+ };
+ }
+
+ /*eslint no-nested-ternary:0 */
+ d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
+ h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
+ h = 60 * (h - d / (maxRGB - minRGB));
+ s = (maxRGB - minRGB) / maxRGB;
+ v = maxRGB;
+
+ return {
+ h: round(h),
+ s: round(s * 100),
+ v: round(v * 100)
+ };
+ }
+
+ function hsvToRgb(hue, saturation, brightness) {
+ var side, chroma, x, match;
+
+ hue = (parseInt(hue, 10) || 0) % 360;
+ saturation = parseInt(saturation, 10) / 100;
+ brightness = parseInt(brightness, 10) / 100;
+ saturation = max(0, min(saturation, 1));
+ brightness = max(0, min(brightness, 1));
+
+ if (saturation === 0) {
+ r = g = b = round(255 * brightness);
+ return;
+ }
+
+ side = hue / 60;
+ chroma = brightness * saturation;
+ x = chroma * (1 - Math.abs(side % 2 - 1));
+ match = brightness - chroma;
+
+ switch (Math.floor(side)) {
+ case 0:
+ r = chroma;
+ g = x;
+ b = 0;
+ break;
+
+ case 1:
+ r = x;
+ g = chroma;
+ b = 0;
+ break;
+
+ case 2:
+ r = 0;
+ g = chroma;
+ b = x;
+ break;
+
+ case 3:
+ r = 0;
+ g = x;
+ b = chroma;
+ break;
+
+ case 4:
+ r = x;
+ g = 0;
+ b = chroma;
+ break;
+
+ case 5:
+ r = chroma;
+ g = 0;
+ b = x;
+ break;
+
+ default:
+ r = g = b = 0;
+ }
+
+ r = round(255 * (r + match));
+ g = round(255 * (g + match));
+ b = round(255 * (b + match));
+ }
+
+ /**
+ * Returns the hex string of the current color. For example: #ff00ff
+ *
+ * @method toHex
+ * @return {String} Hex string of current color.
+ */
+ function toHex() {
+ function hex(val) {
+ val = parseInt(val, 10).toString(16);
+
+ return val.length > 1 ? val : '0' + val;
+ }
+
+ return '#' + hex(r) + hex(g) + hex(b);
+ }
+
+ /**
+ * Returns the r, g, b values of the color. Each channel has a range from 0-255.
+ *
+ * @method toRgb
+ * @return {Object} Object with r, g, b fields.
+ */
+ function toRgb() {
+ return {
+ r: r,
+ g: g,
+ b: b
+ };
+ }
+
+ /**
+ * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
+ *
+ * @method toHsv
+ * @return {Object} Object with h, s, v fields.
+ */
+ function toHsv() {
+ return rgb2hsv(r, g, b);
+ }
+
+ /**
+ * Parses the specified value and populates the color instance.
+ *
+ * Supported format examples:
+ * * rbg(255,0,0)
+ * * #ff0000
+ * * #fff
+ * * {r: 255, g: 0, b: 0}
+ * * {h: 360, s: 100, v: 100}
+ *
+ * @method parse
+ * @param {Object/String} value Color value to parse.
+ * @return {tinymce.util.Color} Current color instance.
+ */
+ function parse(value) {
+ var matches;
+
+ if (typeof value == 'object') {
+ if ("r" in value) {
+ r = value.r;
+ g = value.g;
+ b = value.b;
+ } else if ("v" in value) {
+ hsvToRgb(value.h, value.s, value.v);
+ }
+ } else {
+ if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
+ r = parseInt(matches[1], 10);
+ g = parseInt(matches[2], 10);
+ b = parseInt(matches[3], 10);
+ } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
+ r = parseInt(matches[1], 16);
+ g = parseInt(matches[2], 16);
+ b = parseInt(matches[3], 16);
+ } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
+ r = parseInt(matches[1] + matches[1], 16);
+ g = parseInt(matches[2] + matches[2], 16);
+ b = parseInt(matches[3] + matches[3], 16);
+ }
+ }
+
+ r = r < 0 ? 0 : (r > 255 ? 255 : r);
+ g = g < 0 ? 0 : (g > 255 ? 255 : g);
+ b = b < 0 ? 0 : (b > 255 ? 255 : b);
+
+ return self;
+ }
+
+ if (value) {
+ parse(value);
+ }
+
+ self.toRgb = toRgb;
+ self.toHsv = toHsv;
+ self.toHex = toHex;
+ self.parse = parse;
+ }
+
+ return Color;
+});
+
+// Included from: js/tinymce/classes/ui/ColorPicker.js
+
+/**
+ * ColorPicker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Color picker widget lets you select colors.
+ *
+ * @-x-less ColorPicker.less
+ * @class tinymce.ui.ColorPicker
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ColorPicker", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/DomUtils",
+ "tinymce/util/Color"
+], function(Widget, DragHelper, DomUtils, Color) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ classes: "widget colorpicker"
+ },
+
+ /**
+ * Constructs a new colorpicker instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} color Initial color value.
+ */
+ init: function(settings) {
+ this._super(settings);
+ },
+
+ postRender: function() {
+ var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm;
+
+ hueRootElm = self.getEl('h');
+ huePointElm = self.getEl('hp');
+ svRootElm = self.getEl('sv');
+ svPointElm = self.getEl('svp');
+
+ function getPos(elm, event) {
+ var pos = DomUtils.getPos(elm), x, y;
+
+ x = event.pageX - pos.x;
+ y = event.pageY - pos.y;
+
+ x = Math.max(0, Math.min(x / elm.clientWidth, 1));
+ y = Math.max(0, Math.min(y / elm.clientHeight, 1));
+
+ return {
+ x: x,
+ y: y
+ };
+ }
+
+ function updateColor(hsv, hueUpdate) {
+ var hue = (360 - hsv.h) / 360;
+
+ DomUtils.css(huePointElm, {
+ top: (hue * 100) + '%'
+ });
+
+ if (!hueUpdate) {
+ DomUtils.css(svPointElm, {
+ left: hsv.s + '%',
+ top: (100 - hsv.v) + '%'
+ });
+ }
+
+ svRootElm.style.background = new Color({s: 100, v: 100, h: hsv.h}).toHex();
+ self.color().parse({s: hsv.s, v: hsv.v, h: hsv.h});
+ }
+
+ function updateSaturationAndValue(e) {
+ var pos;
+
+ pos = getPos(svRootElm, e);
+ hsv.s = pos.x * 100;
+ hsv.v = (1 - pos.y) * 100;
+
+ updateColor(hsv);
+ self.fire('change');
+ }
+
+ function updateHue(e) {
+ var pos;
+
+ pos = getPos(hueRootElm, e);
+ hsv = color.toHsv();
+ hsv.h = (1 - pos.y) * 360;
+ updateColor(hsv, true);
+ self.fire('change');
+ }
+
+ self._repaint = function() {
+ hsv = color.toHsv();
+ updateColor(hsv);
+ };
+
+ self._super();
+
+ self._svdraghelper = new DragHelper(self._id + '-sv', {
+ start: updateSaturationAndValue,
+ drag: updateSaturationAndValue
+ });
+
+ self._hdraghelper = new DragHelper(self._id + '-h', {
+ start: updateHue,
+ drag: updateHue
+ });
+
+ self._repaint();
+ },
+
+ rgb: function() {
+ return this.color().toRgb();
+ },
+
+ value: function(value) {
+ var self = this;
+
+ if (arguments.length) {
+ self.color().parse(value);
+
+ if (self._rendered) {
+ self._repaint();
+ }
+ } else {
+ return self.color().toHex();
+ }
+ },
+
+ color: function() {
+ if (!this._color) {
+ this._color = new Color();
+ }
+
+ return this._color;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, hueHtml;
+ var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000';
+
+ function getOldIeFallbackHtml() {
+ var i, l, html = '', gradientPrefix, stopsList;
+
+ gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=';
+ stopsList = stops.split(',');
+ for (i = 0, l = stopsList.length - 1; i < l; i++) {
+ html += (
+ ''
+ );
+ }
+
+ return html;
+ }
+
+ var gradientCssText = (
+ 'background: -ms-linear-gradient(top,' + stops + ');' +
+ 'background: linear-gradient(to bottom,' + stops + ');'
+ );
+
+ hueHtml = (
+ '' +
+ getOldIeFallbackHtml() +
+ '
' +
+ '
'
+ );
+
+ return (
+ '' +
+ '
' +
+ hueHtml +
+ '
'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Path.js
+
+/**
+ * Path.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new path control.
+ *
+ * @-x-less Path.less
+ * @class tinymce.ui.Path
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Path", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {String} delimiter Delimiter to display between row in path.
+ */
+ init: function(settings) {
+ var self = this;
+
+ if (!settings.delimiter) {
+ settings.delimiter = '\u00BB';
+ }
+
+ self._super(settings);
+ self.classes.add('path');
+ self.canFocus = true;
+
+ self.on('click', function(e) {
+ var index, target = e.target;
+
+ if ((index = target.getAttribute('data-index'))) {
+ self.fire('select', {value: self.row()[index], index: index});
+ }
+ });
+
+ self.row(self.settings.row);
+ },
+
+ /**
+ * Focuses the current control.
+ *
+ * @method focus
+ * @return {tinymce.ui.Control} Current control instance.
+ */
+ focus: function() {
+ var self = this;
+
+ self.getEl().firstChild.focus();
+
+ return self;
+ },
+
+ /**
+ * Sets/gets the data to be used for the path.
+ *
+ * @method row
+ * @param {Array} row Array with row name is rendered to path.
+ */
+ row: function(row) {
+ if (!arguments.length) {
+ return this.state.get('row');
+ }
+
+ this.state.set('row', row);
+
+ return this;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ return (
+ '' +
+ self._getDataPathHtml(self.state.get('row')) +
+ '
'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:row', function(e) {
+ self.innerHtml(self._getDataPathHtml(e.value));
+ });
+
+ return self._super();
+ },
+
+ _getDataPathHtml: function(data) {
+ var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix;
+
+ for (i = 0, l = parts.length; i < l; i++) {
+ html += (
+ (i > 0 ? ' ' + self.settings.delimiter + '
' : '') +
+ '' + parts[i].name + '
'
+ );
+ }
+
+ if (!html) {
+ html = '\u00a0
';
+ }
+
+ return html;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ElementPath.js
+
+/**
+ * ElementPath.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This control creates an path for the current selections parent elements in TinyMCE.
+ *
+ * @class tinymce.ui.ElementPath
+ * @extends tinymce.ui.Path
+ */
+define("tinymce/ui/ElementPath", [
+ "tinymce/ui/Path"
+], function(Path) {
+ return Path.extend({
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ElementPath} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this, editor = self.settings.editor;
+
+ function isHidden(elm) {
+ if (elm.nodeType === 1) {
+ if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) {
+ return true;
+ }
+
+ if (elm.getAttribute('data-mce-type') === 'bookmark') {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ if (editor.settings.elementpath !== false) {
+ self.on('select', function(e) {
+ editor.focus();
+ editor.selection.select(this.row()[e.index].element);
+ editor.nodeChanged();
+ });
+
+ editor.on('nodeChange', function(e) {
+ var outParents = [], parents = e.parents, i = parents.length;
+
+ while (i--) {
+ if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
+ var args = editor.fire('ResolveName', {
+ name: parents[i].nodeName.toLowerCase(),
+ target: parents[i]
+ });
+
+ if (!args.isDefaultPrevented()) {
+ outParents.push({name: args.name, element: parents[i]});
+ }
+
+ if (args.isPropagationStopped()) {
+ break;
+ }
+ }
+ }
+
+ self.row(outParents);
+ });
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FormItem.js
+
+/**
+ * FormItem.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class is a container created by the form element with
+ * a label and control item.
+ *
+ * @class tinymce.ui.FormItem
+ * @extends tinymce.ui.Container
+ * @setting {String} label Label to display for the form item.
+ */
+define("tinymce/ui/FormItem", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ layout: 'flex',
+ align: 'center',
+ defaults: {
+ flex: 1
+ }
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, prefix = self.classPrefix;
+
+ self.classes.add('formitem');
+ layout.preRender(self);
+
+ return (
+ '' +
+ (self.settings.title ? ('
' +
+ self.settings.title + '
') : '') +
+ '
' +
+ (self.settings.html || '') + layout.renderHtml(self) +
+ '
' +
+ '
'
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Form.js
+
+/**
+ * Form.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a form container. A form container has the ability
+ * to automatically wrap items in tinymce.ui.FormItem instances.
+ *
+ * Each FormItem instance is a container for the label and the item.
+ *
+ * @example
+ * tinymce.ui.Factory.create({
+ * type: 'form',
+ * items: [
+ * {type: 'textbox', label: 'My text box'}
+ * ]
+ * }).renderTo(document.body);
+ *
+ * @class tinymce.ui.Form
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/Form", [
+ "tinymce/ui/Container",
+ "tinymce/ui/FormItem",
+ "tinymce/util/Tools"
+], function(Container, FormItem, Tools) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ containerCls: 'form',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ flex: 1,
+ padding: 20,
+ labelGap: 30,
+ spacing: 10,
+ callbacks: {
+ submit: function() {
+ this.submit();
+ }
+ }
+ },
+
+ /**
+ * This method gets invoked before the control is rendered.
+ *
+ * @method preRender
+ */
+ preRender: function() {
+ var self = this, items = self.items();
+
+ if (!self.settings.formItemDefaults) {
+ self.settings.formItemDefaults = {
+ layout: 'flex',
+ autoResize: "overflow",
+ defaults: {flex: 1}
+ };
+ }
+
+ // Wrap any labeled items in FormItems
+ items.each(function(ctrl) {
+ var formItem, label = ctrl.settings.label;
+
+ if (label) {
+ formItem = new FormItem(Tools.extend({
+ items: {
+ type: 'label',
+ id: ctrl._id + '-l',
+ text: label,
+ flex: 0,
+ forId: ctrl._id,
+ disabled: ctrl.disabled()
+ }
+ }, self.settings.formItemDefaults));
+
+ formItem.type = 'formitem';
+ ctrl.aria('labelledby', ctrl._id + '-l');
+
+ if (typeof ctrl.settings.flex == "undefined") {
+ ctrl.settings.flex = 1;
+ }
+
+ self.replace(ctrl, formItem);
+ formItem.add(ctrl);
+ }
+ });
+ },
+
+ /**
+ * Fires a submit event with the serialized form.
+ *
+ * @method submit
+ * @return {Object} Event arguments object.
+ */
+ submit: function() {
+ return this.fire('submit', {data: this.toJSON()});
+ },
+
+ /**
+ * Post render method. Called after the control has been rendered to the target.
+ *
+ * @method postRender
+ * @return {tinymce.ui.ComboBox} Current combobox instance.
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+ self.fromJSON(self.settings.data);
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self._super();
+
+ function recalcLabels() {
+ var maxLabelWidth = 0, labels = [], i, labelGap, items;
+
+ if (self.settings.labelGapCalc === false) {
+ return;
+ }
+
+ if (self.settings.labelGapCalc == "children") {
+ items = self.find('formitem');
+ } else {
+ items = self.items();
+ }
+
+ items.filter('formitem').each(function(item) {
+ var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth;
+
+ maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth;
+ labels.push(labelCtrl);
+ });
+
+ labelGap = self.settings.labelGap || 0;
+
+ i = labels.length;
+ while (i--) {
+ labels[i].settings.minWidth = maxLabelWidth + labelGap;
+ }
+ }
+
+ self.on('show', recalcLabels);
+ recalcLabels();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FieldSet.js
+
+/**
+ * FieldSet.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates fieldset containers.
+ *
+ * @-x-less FieldSet.less
+ * @class tinymce.ui.FieldSet
+ * @extends tinymce.ui.Form
+ */
+define("tinymce/ui/FieldSet", [
+ "tinymce/ui/Form"
+], function(Form) {
+ "use strict";
+
+ return Form.extend({
+ Defaults: {
+ containerCls: 'fieldset',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ flex: 1,
+ padding: "25 15 5 15",
+ labelGap: 30,
+ spacing: 10,
+ border: 1
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, prefix = self.classPrefix;
+
+ self.preRender();
+ layout.preRender(self);
+
+ return (
+ ''
+ );
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FilePicker.js
+
+/**
+ * FilePicker.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*global tinymce:true */
+
+/**
+ * This class creates a file picker control.
+ *
+ * @class tinymce.ui.FilePicker
+ * @extends tinymce.ui.ComboBox
+ */
+define("tinymce/ui/FilePicker", [
+ "tinymce/ui/ComboBox",
+ "tinymce/util/Tools"
+], function(ComboBox, Tools) {
+ "use strict";
+
+ return ComboBox.extend({
+ /**
+ * Constructs a new control instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this, editor = tinymce.activeEditor, editorSettings = editor.settings;
+ var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes;
+
+ settings.spellcheck = false;
+
+ fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types;
+ if (fileBrowserCallbackTypes) {
+ fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/);
+ }
+
+ if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype]) {
+ fileBrowserCallback = editorSettings.file_picker_callback;
+ if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) {
+ actionCallback = function() {
+ var meta = self.fire('beforecall').meta;
+
+ meta = Tools.extend({filetype: settings.filetype}, meta);
+
+ // file_picker_callback(callback, currentValue, metaData)
+ fileBrowserCallback.call(
+ editor,
+ function(value, meta) {
+ self.value(value).fire('change', {meta: meta});
+ },
+ self.value(),
+ meta
+ );
+ };
+ } else {
+ // Legacy callback: file_picker_callback(id, currentValue, filetype, window)
+ fileBrowserCallback = editorSettings.file_browser_callback;
+ if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) {
+ actionCallback = function() {
+ fileBrowserCallback(
+ self.getEl('inp').id,
+ self.value(),
+ settings.filetype,
+ window
+ );
+ };
+ }
+ }
+ }
+
+ if (actionCallback) {
+ settings.icon = 'browse';
+ settings.onaction = actionCallback;
+ }
+
+ self._super(settings);
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FitLayout.js
+
+/**
+ * FitLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager will resize the control to be the size of it's parent container.
+ * In other words width: 100% and height: 100%.
+ *
+ * @-x-less FitLayout.less
+ * @class tinymce.ui.FitLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/FitLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox;
+
+ container.items().filter(':visible').each(function(ctrl) {
+ ctrl.layoutRect({
+ x: paddingBox.left,
+ y: paddingBox.top,
+ w: contLayoutRect.innerW - paddingBox.right - paddingBox.left,
+ h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom
+ });
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FlexLayout.js
+
+/**
+ * FlexLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager works similar to the CSS flex box.
+ *
+ * @setting {String} direction row|row-reverse|column|column-reverse
+ * @setting {Number} flex A positive-number to flex by.
+ * @setting {String} align start|end|center|stretch
+ * @setting {String} pack start|end|justify
+ *
+ * @class tinymce.ui.FlexLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/FlexLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ // A ton of variables, needs to be in the same scope for performance
+ var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
+ var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
+ var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
+ var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
+ var alignDeltaSizeName, alignContentSizeName;
+ var max = Math.max, min = Math.min;
+
+ // Get container items, properties and settings
+ items = container.items().filter(':visible');
+ contLayoutRect = container.layoutRect();
+ contPaddingBox = container.paddingBox;
+ contSettings = container.settings;
+ direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
+ align = contSettings.align;
+ pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
+ spacing = contSettings.spacing || 0;
+
+ if (direction == "row-reversed" || direction == "column-reverse") {
+ items = items.set(items.toArray().reverse());
+ direction = direction.split('-')[0];
+ }
+
+ // Setup axis variable name for row/column direction since the calculations is the same
+ if (direction == "column") {
+ posName = "y";
+ sizeName = "h";
+ minSizeName = "minH";
+ maxSizeName = "maxH";
+ innerSizeName = "innerH";
+ beforeName = 'top';
+ deltaSizeName = "deltaH";
+ contentSizeName = "contentH";
+
+ alignBeforeName = "left";
+ alignSizeName = "w";
+ alignAxisName = "x";
+ alignInnerSizeName = "innerW";
+ alignMinSizeName = "minW";
+ alignAfterName = "right";
+ alignDeltaSizeName = "deltaW";
+ alignContentSizeName = "contentW";
+ } else {
+ posName = "x";
+ sizeName = "w";
+ minSizeName = "minW";
+ maxSizeName = "maxW";
+ innerSizeName = "innerW";
+ beforeName = 'left';
+ deltaSizeName = "deltaW";
+ contentSizeName = "contentW";
+
+ alignBeforeName = "top";
+ alignSizeName = "h";
+ alignAxisName = "y";
+ alignInnerSizeName = "innerH";
+ alignMinSizeName = "minH";
+ alignAfterName = "bottom";
+ alignDeltaSizeName = "deltaH";
+ alignContentSizeName = "contentH";
+ }
+
+ // Figure out total flex, availableSpace and collect any max size elements
+ availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
+ maxAlignEndPos = totalFlex = 0;
+ for (i = 0, l = items.length; i < l; i++) {
+ ctrl = items[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ ctrlSettings = ctrl.settings;
+ flex = ctrlSettings.flex;
+ availableSpace -= (i < l - 1 ? spacing : 0);
+
+ if (flex > 0) {
+ totalFlex += flex;
+
+ // Flexed item has a max size then we need to check if we will hit that size
+ if (ctrlLayoutRect[maxSizeName]) {
+ maxSizeItems.push(ctrl);
+ }
+
+ ctrlLayoutRect.flex = flex;
+ }
+
+ availableSpace -= ctrlLayoutRect[minSizeName];
+
+ // Calculate the align end position to be used to check for overflow/underflow
+ size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
+ if (size > maxAlignEndPos) {
+ maxAlignEndPos = size;
+ }
+ }
+
+ // Calculate minW/minH
+ rect = {};
+ if (availableSpace < 0) {
+ rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
+ } else {
+ rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
+ }
+
+ rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];
+
+ rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
+ rect[alignContentSizeName] = maxAlignEndPos;
+ rect.minW = min(rect.minW, contLayoutRect.maxW);
+ rect.minH = min(rect.minH, contLayoutRect.maxH);
+ rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
+ rect.minH = max(rect.minH, contLayoutRect.startMinHeight);
+
+ // Resize container container if minSize was changed
+ if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
+ rect.w = rect.minW;
+ rect.h = rect.minH;
+
+ container.layoutRect(rect);
+ this.recalc(container);
+
+ // Forced recalc for example if items are hidden/shown
+ if (container._lastRect === null) {
+ var parentCtrl = container.parent();
+ if (parentCtrl) {
+ parentCtrl._lastRect = null;
+ parentCtrl.recalc();
+ }
+ }
+
+ return;
+ }
+
+ // Handle max size elements, check if they will become to wide with current options
+ ratio = availableSpace / totalFlex;
+ for (i = 0, l = maxSizeItems.length; i < l; i++) {
+ ctrl = maxSizeItems[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ maxSize = ctrlLayoutRect[maxSizeName];
+ size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;
+
+ if (size > maxSize) {
+ availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
+ totalFlex -= ctrlLayoutRect.flex;
+ ctrlLayoutRect.flex = 0;
+ ctrlLayoutRect.maxFlexSize = maxSize;
+ } else {
+ ctrlLayoutRect.maxFlexSize = 0;
+ }
+ }
+
+ // Setup new ratio, target layout rect, start position
+ ratio = availableSpace / totalFlex;
+ pos = contPaddingBox[beforeName];
+ rect = {};
+
+ // Handle pack setting moves the start position to end, center
+ if (totalFlex === 0) {
+ if (pack == "end") {
+ pos = availableSpace + contPaddingBox[beforeName];
+ } else if (pack == "center") {
+ pos = Math.round(
+ (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
+ ) + contPaddingBox[beforeName];
+
+ if (pos < 0) {
+ pos = contPaddingBox[beforeName];
+ }
+ } else if (pack == "justify") {
+ pos = contPaddingBox[beforeName];
+ spacing = Math.floor(availableSpace / (items.length - 1));
+ }
+ }
+
+ // Default aligning (start) the other ones needs to be calculated while doing the layout
+ rect[alignAxisName] = contPaddingBox[alignBeforeName];
+
+ // Start laying out controls
+ for (i = 0, l = items.length; i < l; i++) {
+ ctrl = items[i];
+ ctrlLayoutRect = ctrl.layoutRect();
+ size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];
+
+ // Align the control on the other axis
+ if (align === "center") {
+ rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
+ } else if (align === "stretch") {
+ rect[alignSizeName] = max(
+ ctrlLayoutRect[alignMinSizeName] || 0,
+ contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
+ );
+ rect[alignAxisName] = contPaddingBox[alignBeforeName];
+ } else if (align === "end") {
+ rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
+ }
+
+ // Calculate new size based on flex
+ if (ctrlLayoutRect.flex > 0) {
+ size += ctrlLayoutRect.flex * ratio;
+ }
+
+ rect[sizeName] = size;
+ rect[posName] = pos;
+ ctrl.layoutRect(rect);
+
+ // Recalculate containers
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+
+ // Move x/y position
+ pos += size + spacing;
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FlowLayout.js
+
+/**
+ * FlowLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager will place the controls by using the browsers native layout.
+ *
+ * @-x-less FlowLayout.less
+ * @class tinymce.ui.FlowLayout
+ * @extends tinymce.ui.Layout
+ */
+define("tinymce/ui/FlowLayout", [
+ "tinymce/ui/Layout"
+], function(Layout) {
+ return Layout.extend({
+ Defaults: {
+ containerClass: 'flow-layout',
+ controlClass: 'flow-layout-item',
+ endClass: 'break'
+ },
+
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ container.items().filter(':visible').each(function(ctrl) {
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ });
+ },
+
+ isNative: function() {
+ return true;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/FormatControls.js
+
+/**
+ * FormatControls.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Internal class containing all TinyMCE specific control types such as
+ * format listboxes, fontlist boxes, toolbar buttons etc.
+ *
+ * @class tinymce.ui.FormatControls
+ */
+define("tinymce/ui/FormatControls", [
+ "tinymce/ui/Control",
+ "tinymce/ui/Widget",
+ "tinymce/ui/FloatPanel",
+ "tinymce/util/Tools",
+ "tinymce/dom/DOMUtils",
+ "tinymce/EditorManager",
+ "tinymce/Env"
+], function(Control, Widget, FloatPanel, Tools, DOMUtils, EditorManager, Env) {
+ var each = Tools.each;
+
+ EditorManager.on('AddEditor', function(e) {
+ var editor = e.editor;
+
+ setupRtlMode(editor);
+ registerControls(editor);
+ setupContainer(editor);
+ });
+
+ Control.translate = function(text) {
+ return EditorManager.translate(text);
+ };
+
+ Widget.tooltips = !Env.iOS;
+
+ function setupContainer(editor) {
+ if (editor.settings.ui_container) {
+ Env.container = DOMUtils.DOM.select(editor.settings.ui_container)[0];
+ }
+ }
+
+ function setupRtlMode(editor) {
+ editor.on('ScriptsLoaded', function () {
+ if (editor.rtl) {
+ Control.rtl = true;
+ }
+ });
+ }
+
+ function registerControls(editor) {
+ var formatMenu;
+
+ function createListBoxChangeHandler(items, formatName) {
+ return function() {
+ var self = this;
+
+ editor.on('nodeChange', function(e) {
+ var formatter = editor.formatter;
+ var value = null;
+
+ each(e.parents, function(node) {
+ each(items, function(item) {
+ if (formatName) {
+ if (formatter.matchNode(node, formatName, {value: item.value})) {
+ value = item.value;
+ }
+ } else {
+ if (formatter.matchNode(node, item.value)) {
+ value = item.value;
+ }
+ }
+
+ if (value) {
+ return false;
+ }
+ });
+
+ if (value) {
+ return false;
+ }
+ });
+
+ self.value(value);
+ });
+ };
+ }
+
+ function createFormats(formats) {
+ formats = formats.replace(/;$/, '').split(';');
+
+ var i = formats.length;
+ while (i--) {
+ formats[i] = formats[i].split('=');
+ }
+
+ return formats;
+ }
+
+ function createFormatMenu() {
+ var count = 0, newFormats = [];
+
+ var defaultStyleFormats = [
+ {title: 'Headings', items: [
+ {title: 'Heading 1', format: 'h1'},
+ {title: 'Heading 2', format: 'h2'},
+ {title: 'Heading 3', format: 'h3'},
+ {title: 'Heading 4', format: 'h4'},
+ {title: 'Heading 5', format: 'h5'},
+ {title: 'Heading 6', format: 'h6'}
+ ]},
+
+ {title: 'Inline', items: [
+ {title: 'Bold', icon: 'bold', format: 'bold'},
+ {title: 'Italic', icon: 'italic', format: 'italic'},
+ {title: 'Underline', icon: 'underline', format: 'underline'},
+ {title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough'},
+ {title: 'Superscript', icon: 'superscript', format: 'superscript'},
+ {title: 'Subscript', icon: 'subscript', format: 'subscript'},
+ {title: 'Code', icon: 'code', format: 'code'}
+ ]},
+
+ {title: 'Blocks', items: [
+ {title: 'Paragraph', format: 'p'},
+ {title: 'Blockquote', format: 'blockquote'},
+ {title: 'Div', format: 'div'},
+ {title: 'Pre', format: 'pre'}
+ ]},
+
+ {title: 'Alignment', items: [
+ {title: 'Left', icon: 'alignleft', format: 'alignleft'},
+ {title: 'Center', icon: 'aligncenter', format: 'aligncenter'},
+ {title: 'Right', icon: 'alignright', format: 'alignright'},
+ {title: 'Justify', icon: 'alignjustify', format: 'alignjustify'}
+ ]}
+ ];
+
+ function createMenu(formats) {
+ var menu = [];
+
+ if (!formats) {
+ return;
+ }
+
+ each(formats, function(format) {
+ var menuItem = {
+ text: format.title,
+ icon: format.icon
+ };
+
+ if (format.items) {
+ menuItem.menu = createMenu(format.items);
+ } else {
+ var formatName = format.format || "custom" + count++;
+
+ if (!format.format) {
+ format.name = formatName;
+ newFormats.push(format);
+ }
+
+ menuItem.format = formatName;
+ menuItem.cmd = format.cmd;
+ }
+
+ menu.push(menuItem);
+ });
+
+ return menu;
+ }
+
+ function createStylesMenu() {
+ var menu;
+
+ if (editor.settings.style_formats_merge) {
+ if (editor.settings.style_formats) {
+ menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats));
+ } else {
+ menu = createMenu(defaultStyleFormats);
+ }
+ } else {
+ menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
+ }
+
+ return menu;
+ }
+
+ editor.on('init', function() {
+ each(newFormats, function(format) {
+ editor.formatter.register(format.name, format);
+ });
+ });
+
+ return {
+ type: 'menu',
+ items: createStylesMenu(),
+ onPostRender: function(e) {
+ editor.fire('renderFormatsMenu', {control: e.control});
+ },
+ itemDefaults: {
+ preview: true,
+
+ textStyle: function() {
+ if (this.settings.format) {
+ return editor.formatter.getCssText(this.settings.format);
+ }
+ },
+
+ onPostRender: function() {
+ var self = this;
+
+ self.parent().on('show', function() {
+ var formatName, command;
+
+ formatName = self.settings.format;
+ if (formatName) {
+ self.disabled(!editor.formatter.canApply(formatName));
+ self.active(editor.formatter.match(formatName));
+ }
+
+ command = self.settings.cmd;
+ if (command) {
+ self.active(editor.queryCommandState(command));
+ }
+ });
+ },
+
+ onclick: function() {
+ if (this.settings.format) {
+ toggleFormat(this.settings.format);
+ }
+
+ if (this.settings.cmd) {
+ editor.execCommand(this.settings.cmd);
+ }
+ }
+ }
+ };
+ }
+
+ formatMenu = createFormatMenu();
+
+ function initOnPostRender(name) {
+ return function() {
+ var self = this;
+
+ // TODO: Fix this
+ if (editor.formatter) {
+ editor.formatter.formatChanged(name, function(state) {
+ self.active(state);
+ });
+ } else {
+ editor.on('init', function() {
+ editor.formatter.formatChanged(name, function(state) {
+ self.active(state);
+ });
+ });
+ }
+ };
+ }
+
+ // Simple format controls :
+ each({
+ bold: 'Bold',
+ italic: 'Italic',
+ underline: 'Underline',
+ strikethrough: 'Strikethrough',
+ subscript: 'Subscript',
+ superscript: 'Superscript'
+ }, function(text, name) {
+ editor.addButton(name, {
+ tooltip: text,
+ onPostRender: initOnPostRender(name),
+ onclick: function() {
+ toggleFormat(name);
+ }
+ });
+ });
+
+ // Simple command controls :[,]
+ each({
+ outdent: ['Decrease indent', 'Outdent'],
+ indent: ['Increase indent', 'Indent'],
+ cut: ['Cut', 'Cut'],
+ copy: ['Copy', 'Copy'],
+ paste: ['Paste', 'Paste'],
+ help: ['Help', 'mceHelp'],
+ selectall: ['Select all', 'SelectAll'],
+ removeformat: ['Clear formatting', 'RemoveFormat'],
+ visualaid: ['Visual aids', 'mceToggleVisualAid'],
+ newdocument: ['New document', 'mceNewDocument']
+ }, function(item, name) {
+ editor.addButton(name, {
+ tooltip: item[0],
+ cmd: item[1]
+ });
+ });
+
+ // Simple command controls with format state
+ each({
+ blockquote: ['Blockquote', 'mceBlockQuote'],
+ numlist: ['Numbered list', 'InsertOrderedList'],
+ bullist: ['Bullet list', 'InsertUnorderedList'],
+ subscript: ['Subscript', 'Subscript'],
+ superscript: ['Superscript', 'Superscript'],
+ alignleft: ['Align left', 'JustifyLeft'],
+ aligncenter: ['Align center', 'JustifyCenter'],
+ alignright: ['Align right', 'JustifyRight'],
+ alignjustify: ['Justify', 'JustifyFull'],
+ alignnone: ['No alignment', 'JustifyNone']
+ }, function(item, name) {
+ editor.addButton(name, {
+ tooltip: item[0],
+ cmd: item[1],
+ onPostRender: initOnPostRender(name)
+ });
+ });
+
+ function toggleUndoRedoState(type) {
+ return function() {
+ var self = this;
+
+ type = type == 'redo' ? 'hasRedo' : 'hasUndo';
+
+ function checkState() {
+ return editor.undoManager ? editor.undoManager[type]() : false;
+ }
+
+ self.disabled(!checkState());
+ editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function() {
+ self.disabled(editor.readonly || !checkState());
+ });
+ };
+ }
+
+ function toggleVisualAidState() {
+ var self = this;
+
+ editor.on('VisualAid', function(e) {
+ self.active(e.hasVisual);
+ });
+
+ self.active(editor.hasVisual);
+ }
+
+ editor.addButton('undo', {
+ tooltip: 'Undo',
+ onPostRender: toggleUndoRedoState('undo'),
+ cmd: 'undo'
+ });
+
+ editor.addButton('redo', {
+ tooltip: 'Redo',
+ onPostRender: toggleUndoRedoState('redo'),
+ cmd: 'redo'
+ });
+
+ editor.addMenuItem('newdocument', {
+ text: 'New document',
+ icon: 'newdocument',
+ cmd: 'mceNewDocument'
+ });
+
+ editor.addMenuItem('undo', {
+ text: 'Undo',
+ icon: 'undo',
+ shortcut: 'Meta+Z',
+ onPostRender: toggleUndoRedoState('undo'),
+ cmd: 'undo'
+ });
+
+ editor.addMenuItem('redo', {
+ text: 'Redo',
+ icon: 'redo',
+ shortcut: 'Meta+Y',
+ onPostRender: toggleUndoRedoState('redo'),
+ cmd: 'redo'
+ });
+
+ editor.addMenuItem('visualaid', {
+ text: 'Visual aids',
+ selectable: true,
+ onPostRender: toggleVisualAidState,
+ cmd: 'mceToggleVisualAid'
+ });
+
+ editor.addButton('remove', {
+ tooltip: 'Remove',
+ icon: 'remove',
+ cmd: 'Delete'
+ });
+
+ each({
+ cut: ['Cut', 'Cut', 'Meta+X'],
+ copy: ['Copy', 'Copy', 'Meta+C'],
+ paste: ['Paste', 'Paste', 'Meta+V'],
+ selectall: ['Select all', 'SelectAll', 'Meta+A'],
+ bold: ['Bold', 'Bold', 'Meta+B'],
+ italic: ['Italic', 'Italic', 'Meta+I'],
+ underline: ['Underline', 'Underline'],
+ strikethrough: ['Strikethrough', 'Strikethrough'],
+ subscript: ['Subscript', 'Subscript'],
+ superscript: ['Superscript', 'Superscript'],
+ removeformat: ['Clear formatting', 'RemoveFormat']
+ }, function(item, name) {
+ editor.addMenuItem(name, {
+ text: item[0],
+ icon: name,
+ shortcut: item[2],
+ cmd: item[1]
+ });
+ });
+
+ editor.on('mousedown', function() {
+ FloatPanel.hideAll();
+ });
+
+ function toggleFormat(fmt) {
+ if (fmt.control) {
+ fmt = fmt.control.value();
+ }
+
+ if (fmt) {
+ editor.execCommand('mceToggleFormat', false, fmt);
+ }
+ }
+
+ editor.addButton('styleselect', {
+ type: 'menubutton',
+ text: 'Formats',
+ menu: formatMenu
+ });
+
+ editor.addButton('formatselect', function() {
+ var items = [], blocks = createFormats(editor.settings.block_formats ||
+ 'Paragraph=p;' +
+ 'Heading 1=h1;' +
+ 'Heading 2=h2;' +
+ 'Heading 3=h3;' +
+ 'Heading 4=h4;' +
+ 'Heading 5=h5;' +
+ 'Heading 6=h6;' +
+ 'Preformatted=pre'
+ );
+
+ each(blocks, function(block) {
+ items.push({
+ text: block[0],
+ value: block[1],
+ textStyle: function() {
+ return editor.formatter.getCssText(block[1]);
+ }
+ });
+ });
+
+ return {
+ type: 'listbox',
+ text: blocks[0][0],
+ values: items,
+ fixedWidth: true,
+ onselect: toggleFormat,
+ onPostRender: createListBoxChangeHandler(items)
+ };
+ });
+
+ editor.addButton('fontselect', function() {
+ var defaultFontsFormats =
+ 'Andale Mono=andale mono,monospace;' +
+ 'Arial=arial,helvetica,sans-serif;' +
+ 'Arial Black=arial black,sans-serif;' +
+ 'Book Antiqua=book antiqua,palatino,serif;' +
+ 'Comic Sans MS=comic sans ms,sans-serif;' +
+ 'Courier New=courier new,courier,monospace;' +
+ 'Georgia=georgia,palatino,serif;' +
+ 'Helvetica=helvetica,arial,sans-serif;' +
+ 'Impact=impact,sans-serif;' +
+ 'Symbol=symbol;' +
+ 'Tahoma=tahoma,arial,helvetica,sans-serif;' +
+ 'Terminal=terminal,monaco,monospace;' +
+ 'Times New Roman=times new roman,times,serif;' +
+ 'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
+ 'Verdana=verdana,geneva,sans-serif;' +
+ 'Webdings=webdings;' +
+ 'Wingdings=wingdings,zapf dingbats';
+
+ var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats);
+
+ each(fonts, function(font) {
+ items.push({
+ text: {raw: font[0]},
+ value: font[1],
+ textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : ''
+ });
+ });
+
+ return {
+ type: 'listbox',
+ text: 'Font Family',
+ tooltip: 'Font Family',
+ values: items,
+ fixedWidth: true,
+ onPostRender: createListBoxChangeHandler(items, 'fontname'),
+ onselect: function(e) {
+ if (e.control.settings.value) {
+ editor.execCommand('FontName', false, e.control.settings.value);
+ }
+ }
+ };
+ });
+
+ editor.addButton('fontsizeselect', function() {
+ var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt';
+ var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats;
+
+ each(fontsize_formats.split(' '), function(item) {
+ var text = item, value = item;
+ // Allow text=value font sizes.
+ var values = item.split('=');
+ if (values.length > 1) {
+ text = values[0];
+ value = values[1];
+ }
+ items.push({text: text, value: value});
+ });
+
+ return {
+ type: 'listbox',
+ text: 'Font Sizes',
+ tooltip: 'Font Sizes',
+ values: items,
+ fixedWidth: true,
+ onPostRender: createListBoxChangeHandler(items, 'fontsize'),
+ onclick: function(e) {
+ if (e.control.settings.value) {
+ editor.execCommand('FontSize', false, e.control.settings.value);
+ }
+ }
+ };
+ });
+
+ editor.addMenuItem('formats', {
+ text: 'Formats',
+ menu: formatMenu
+ });
+ }
+});
+
+// Included from: js/tinymce/classes/ui/GridLayout.js
+
+/**
+ * GridLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout manager places controls in a grid.
+ *
+ * @setting {Number} spacing Spacing between controls.
+ * @setting {Number} spacingH Horizontal spacing between controls.
+ * @setting {Number} spacingV Vertical spacing between controls.
+ * @setting {Number} columns Number of columns to use.
+ * @setting {String/Array} alignH start|end|center|stretch or array of values for each column.
+ * @setting {String/Array} alignV start|end|center|stretch or array of values for each column.
+ * @setting {String} pack start|end
+ *
+ * @class tinymce.ui.GridLayout
+ * @extends tinymce.ui.AbsoluteLayout
+ */
+define("tinymce/ui/GridLayout", [
+ "tinymce/ui/AbsoluteLayout"
+], function(AbsoluteLayout) {
+ "use strict";
+
+ return AbsoluteLayout.extend({
+ /**
+ * Recalculates the positions of the controls in the specified container.
+ *
+ * @method recalc
+ * @param {tinymce.ui.Container} container Container instance to recalc.
+ */
+ recalc: function(container) {
+ var settings, rows, cols, items, contLayoutRect, width, height, rect,
+ ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY,
+ colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx;
+
+ // Get layout settings
+ settings = container.settings;
+ items = container.items().filter(':visible');
+ contLayoutRect = container.layoutRect();
+ cols = settings.columns || Math.ceil(Math.sqrt(items.length));
+ rows = Math.ceil(items.length / cols);
+ spacingH = settings.spacingH || settings.spacing || 0;
+ spacingV = settings.spacingV || settings.spacing || 0;
+ alignH = settings.alignH || settings.align;
+ alignV = settings.alignV || settings.align;
+ contPaddingBox = container.paddingBox;
+ reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl();
+
+ if (alignH && typeof alignH == "string") {
+ alignH = [alignH];
+ }
+
+ if (alignV && typeof alignV == "string") {
+ alignV = [alignV];
+ }
+
+ // Zero padd columnWidths
+ for (x = 0; x < cols; x++) {
+ colWidths.push(0);
+ }
+
+ // Zero padd rowHeights
+ for (y = 0; y < rows; y++) {
+ rowHeights.push(0);
+ }
+
+ // Calculate columnWidths and rowHeights
+ for (y = 0; y < rows; y++) {
+ for (x = 0; x < cols; x++) {
+ ctrl = items[y * cols + x];
+
+ // Out of bounds
+ if (!ctrl) {
+ break;
+ }
+
+ ctrlLayoutRect = ctrl.layoutRect();
+ ctrlMinWidth = ctrlLayoutRect.minW;
+ ctrlMinHeight = ctrlLayoutRect.minH;
+
+ colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x];
+ rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y];
+ }
+ }
+
+ // Calculate maxX
+ availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right;
+ for (maxX = 0, x = 0; x < cols; x++) {
+ maxX += colWidths[x] + (x > 0 ? spacingH : 0);
+ availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x];
+ }
+
+ // Calculate maxY
+ availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom;
+ for (maxY = 0, y = 0; y < rows; y++) {
+ maxY += rowHeights[y] + (y > 0 ? spacingV : 0);
+ availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y];
+ }
+
+ maxX += contPaddingBox.left + contPaddingBox.right;
+ maxY += contPaddingBox.top + contPaddingBox.bottom;
+
+ // Calculate minW/minH
+ rect = {};
+ rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW);
+ rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH);
+
+ rect.contentW = rect.minW - contLayoutRect.deltaW;
+ rect.contentH = rect.minH - contLayoutRect.deltaH;
+ rect.minW = Math.min(rect.minW, contLayoutRect.maxW);
+ rect.minH = Math.min(rect.minH, contLayoutRect.maxH);
+ rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth);
+ rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight);
+
+ // Resize container container if minSize was changed
+ if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
+ rect.w = rect.minW;
+ rect.h = rect.minH;
+
+ container.layoutRect(rect);
+ this.recalc(container);
+
+ // Forced recalc for example if items are hidden/shown
+ if (container._lastRect === null) {
+ var parentCtrl = container.parent();
+ if (parentCtrl) {
+ parentCtrl._lastRect = null;
+ parentCtrl.recalc();
+ }
+ }
+
+ return;
+ }
+
+ // Update contentW/contentH so absEnd moves correctly
+ if (contLayoutRect.autoResize) {
+ rect = container.layoutRect(rect);
+ rect.contentW = rect.minW - contLayoutRect.deltaW;
+ rect.contentH = rect.minH - contLayoutRect.deltaH;
+ }
+
+ var flexV;
+
+ if (settings.packV == 'start') {
+ flexV = 0;
+ } else {
+ flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0;
+ }
+
+ // Calculate totalFlex
+ var totalFlex = 0;
+ var flexWidths = settings.flexWidths;
+ if (flexWidths) {
+ for (x = 0; x < flexWidths.length; x++) {
+ totalFlex += flexWidths[x];
+ }
+ } else {
+ totalFlex = cols;
+ }
+
+ // Calculate new column widths based on flex values
+ var ratio = availableWidth / totalFlex;
+ for (x = 0; x < cols; x++) {
+ colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio;
+ }
+
+ // Move/resize controls
+ posY = contPaddingBox.top;
+ for (y = 0; y < rows; y++) {
+ posX = contPaddingBox.left;
+ height = rowHeights[y] + flexV;
+
+ for (x = 0; x < cols; x++) {
+ if (reverseRows) {
+ idx = y * cols + cols - 1 - x;
+ } else {
+ idx = y * cols + x;
+ }
+
+ ctrl = items[idx];
+
+ // No more controls to render then break
+ if (!ctrl) {
+ break;
+ }
+
+ // Get control settings and calculate x, y
+ ctrlSettings = ctrl.settings;
+ ctrlLayoutRect = ctrl.layoutRect();
+ width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth);
+ ctrlLayoutRect.x = posX;
+ ctrlLayoutRect.y = posY;
+
+ // Align control horizontal
+ align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null);
+ if (align == "center") {
+ ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2);
+ } else if (align == "right") {
+ ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w;
+ } else if (align == "stretch") {
+ ctrlLayoutRect.w = width;
+ }
+
+ // Align control vertical
+ align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null);
+ if (align == "center") {
+ ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2);
+ } else if (align == "bottom") {
+ ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h;
+ } else if (align == "stretch") {
+ ctrlLayoutRect.h = height;
+ }
+
+ ctrl.layoutRect(ctrlLayoutRect);
+
+ posX += width + spacingH;
+
+ if (ctrl.recalc) {
+ ctrl.recalc();
+ }
+ }
+
+ posY += height + spacingV;
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Iframe.js
+
+/**
+ * Iframe.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint scripturl:true */
+
+/**
+ * This class creates an iframe.
+ *
+ * @setting {String} url Url to open in the iframe.
+ *
+ * @-x-less Iframe.less
+ * @class tinymce.ui.Iframe
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Iframe", [
+ "tinymce/ui/Widget",
+ "tinymce/util/Delay"
+], function(Widget, Delay) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ self.classes.add('iframe');
+ self.canFocus = false;
+
+ /*eslint no-script-url:0 */
+ return (
+ ''
+ );
+ },
+
+ /**
+ * Setter for the iframe source.
+ *
+ * @method src
+ * @param {String} src Source URL for iframe.
+ */
+ src: function(src) {
+ this.getEl().src = src;
+ },
+
+ /**
+ * Inner HTML for the iframe.
+ *
+ * @method html
+ * @param {String} html HTML string to set as HTML inside the iframe.
+ * @param {function} callback Optional callback to execute when the iframe body is filled with contents.
+ * @return {tinymce.ui.Iframe} Current iframe control.
+ */
+ html: function(html, callback) {
+ var self = this, body = this.getEl().contentWindow.document.body;
+
+ // Wait for iframe to initialize IE 10 takes time
+ if (!body) {
+ Delay.setTimeout(function() {
+ self.html(html);
+ });
+ } else {
+ body.innerHTML = html;
+
+ if (callback) {
+ callback();
+ }
+ }
+
+ return this;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/InfoBox.js
+
+/**
+ * InfoBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2016 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * ....
+ *
+ * @-x-less InfoBox.less
+ * @class tinymce.ui.InfoBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/InfoBox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline Multiline label.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('widget').add('infobox');
+ self.canFocus = false;
+ },
+
+ severity: function(level) {
+ this.classes.remove('error');
+ this.classes.remove('warning');
+ this.classes.remove('success');
+ this.classes.add(level);
+ },
+
+ help: function(state) {
+ this.state.set('help', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ return (
+ '' +
+ '
' +
+ self.encode(self.state.get('text')) +
+ '' +
+ '
' +
+ '
'
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.getEl('body').firstChild.data = self.encode(e.value);
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ self.state.on('change:help', function(e) {
+ self.classes.toggle('has-help', e.value);
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Label.js
+
+/**
+ * Label.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class creates a label element. A label is a simple text control
+ * that can be bound to other controls.
+ *
+ * @-x-less Label.less
+ * @class tinymce.ui.Label
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Label", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DomUtils"
+], function(Widget, DomUtils) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline Multiline label.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('widget').add('label');
+ self.canFocus = false;
+
+ if (settings.multiline) {
+ self.classes.add('autoscroll');
+ }
+
+ if (settings.strong) {
+ self.classes.add('strong');
+ }
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, layoutRect = self._super();
+
+ if (self.settings.multiline) {
+ var size = DomUtils.getSize(self.getEl());
+
+ // Check if the text fits within maxW if not then try word wrapping it
+ if (size.width > layoutRect.maxW) {
+ layoutRect.minW = layoutRect.maxW;
+ self.classes.add('multiline');
+ }
+
+ self.getEl().style.width = layoutRect.minW + 'px';
+ layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height);
+ }
+
+ return layoutRect;
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this;
+
+ if (!self.settings.multiline) {
+ self.getEl().style.lineHeight = self.layoutRect().h + 'px';
+ }
+
+ return self._super();
+ },
+
+ severity: function(level) {
+ this.classes.remove('error');
+ this.classes.remove('warning');
+ this.classes.remove('success');
+ this.classes.add(level);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, targetCtrl, forName, forId = self.settings.forId;
+
+ if (!forId && (forName = self.settings.forName)) {
+ targetCtrl = self.getRoot().find('#' + forName)[0];
+
+ if (targetCtrl) {
+ forId = targetCtrl._id;
+ }
+ }
+
+ if (forId) {
+ return (
+ ''
+ );
+ }
+
+ return (
+ '' +
+ self.encode(self.state.get('text')) +
+ ''
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:text', function(e) {
+ self.innerHtml(self.encode(e.value));
+
+ if (self.state.get('rendered')) {
+ self.updateLayoutRect();
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Toolbar.js
+
+/**
+ * Toolbar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new toolbar.
+ *
+ * @class tinymce.ui.Toolbar
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/Toolbar", [
+ "tinymce/ui/Container"
+], function(Container) {
+ "use strict";
+
+ return Container.extend({
+ Defaults: {
+ role: 'toolbar',
+ layout: 'flow'
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+ self.classes.add('toolbar');
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.items().each(function(ctrl) {
+ ctrl.classes.add('toolbar-item');
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/MenuBar.js
+
+/**
+ * MenuBar.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menubar.
+ *
+ * @-x-less MenuBar.less
+ * @class tinymce.ui.MenuBar
+ * @extends tinymce.ui.Container
+ */
+define("tinymce/ui/MenuBar", [
+ "tinymce/ui/Toolbar"
+], function(Toolbar) {
+ "use strict";
+
+ return Toolbar.extend({
+ Defaults: {
+ role: 'menubar',
+ containerCls: 'menubar',
+ ariaRoot: true,
+ defaults: {
+ type: 'menubutton'
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/MenuButton.js
+
+/**
+ * MenuButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu button.
+ *
+ * @-x-less MenuButton.less
+ * @class tinymce.ui.MenuButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/MenuButton", [
+ "tinymce/ui/Button",
+ "tinymce/ui/Factory",
+ "tinymce/ui/MenuBar"
+], function(Button, Factory, MenuBar) {
+ "use strict";
+
+ // TODO: Maybe add as some global function
+ function isChildOf(node, parent) {
+ while (node) {
+ if (parent === node) {
+ return true;
+ }
+
+ node = node.parentNode;
+ }
+
+ return false;
+ }
+
+ var MenuButton = Button.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._renderOpen = true;
+
+ self._super(settings);
+ settings = self.settings;
+
+ self.classes.add('menubtn');
+
+ if (settings.fixedWidth) {
+ self.classes.add('fixed-width');
+ }
+
+ self.aria('haspopup', true);
+
+ self.state.set('menu', settings.menu || self.render());
+ },
+
+ /**
+ * Shows the menu for the button.
+ *
+ * @method showMenu
+ */
+ showMenu: function() {
+ var self = this, menu;
+
+ if (self.menu && self.menu.visible()) {
+ return self.hideMenu();
+ }
+
+ if (!self.menu) {
+ menu = self.state.get('menu') || [];
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ if (!menu.renderTo) {
+ self.menu = Factory.create(menu).parent(self).renderTo();
+ } else {
+ self.menu = menu.parent(self).show().renderTo();
+ }
+
+ self.fire('createmenu');
+ self.menu.reflow();
+ self.menu.on('cancel', function(e) {
+ if (e.control.parent() === self.menu) {
+ e.stopPropagation();
+ self.focus();
+ self.hideMenu();
+ }
+ });
+
+ // Move focus to button when a menu item is selected/clicked
+ self.menu.on('select', function() {
+ self.focus();
+ });
+
+ self.menu.on('show hide', function(e) {
+ if (e.control == self.menu) {
+ self.activeMenu(e.type == 'show');
+ }
+
+ self.aria('expanded', e.type == 'show');
+ }).fire('show');
+ }
+
+ self.menu.show();
+ self.menu.layoutRect({w: self.layoutRect().w});
+ self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
+ },
+
+ /**
+ * Hides the menu for the button.
+ *
+ * @method hideMenu
+ */
+ hideMenu: function() {
+ var self = this;
+
+ if (self.menu) {
+ self.menu.items().each(function(item) {
+ if (item.hideMenu) {
+ item.hideMenu();
+ }
+ });
+
+ self.menu.hide();
+ }
+ },
+
+ /**
+ * Sets the active menu state.
+ *
+ * @private
+ */
+ activeMenu: function(state) {
+ this.classes.toggle('active', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+ var icon = self.settings.icon, image, text = self.state.get('text'),
+ textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '' + self.encode(text) + '';
+ }
+
+ icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
+
+ return (
+ '' +
+ '' +
+ '
'
+ );
+ },
+
+ /**
+ * Gets invoked after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.on('click', function(e) {
+ if (e.control === self && isChildOf(e.target, self.getEl())) {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.items()[0].focus();
+ }
+ }
+ });
+
+ self.on('mouseenter', function(e) {
+ var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu;
+
+ if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) {
+ parent.items().filter('MenuButton').each(function(ctrl) {
+ if (ctrl.hideMenu && ctrl != overCtrl) {
+ if (ctrl.menu && ctrl.menu.visible()) {
+ hasVisibleSiblingMenu = true;
+ }
+
+ ctrl.hideMenu();
+ }
+ });
+
+ if (hasVisibleSiblingMenu) {
+ overCtrl.focus(); // Fix for: #5887
+ overCtrl.showMenu();
+ }
+ }
+ });
+
+ return self._super();
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:menu', function() {
+ if (self.menu) {
+ self.menu.remove();
+ }
+
+ self.menu = null;
+ });
+
+ return self._super();
+ },
+
+ /**
+ * Removes the control and it's menus.
+ *
+ * @method remove
+ */
+ remove: function() {
+ this._super();
+
+ if (this.menu) {
+ this.menu.remove();
+ }
+ }
+ });
+
+ return MenuButton;
+});
+
+// Included from: js/tinymce/classes/ui/MenuItem.js
+
+/**
+ * MenuItem.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu item.
+ *
+ * @-x-less MenuItem.less
+ * @class tinymce.ui.MenuItem
+ * @extends tinymce.ui.Control
+ */
+define("tinymce/ui/MenuItem", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/Factory",
+ "tinymce/Env",
+ "tinymce/util/Delay"
+], function(Widget, Factory, Env, Delay) {
+ "use strict";
+
+ return Widget.extend({
+ Defaults: {
+ border: 0,
+ role: 'menuitem'
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} selectable Selectable menu.
+ * @setting {Array} menu Submenu array with items.
+ * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X
+ */
+ init: function(settings) {
+ var self = this, text;
+
+ self._super(settings);
+
+ settings = self.settings;
+
+ self.classes.add('menu-item');
+
+ if (settings.menu) {
+ self.classes.add('menu-item-expand');
+ }
+
+ if (settings.preview) {
+ self.classes.add('menu-item-preview');
+ }
+
+ text = self.state.get('text');
+ if (text === '-' || text === '|') {
+ self.classes.add('menu-item-sep');
+ self.aria('role', 'separator');
+ self.state.set('text', '-');
+ }
+
+ if (settings.selectable) {
+ self.aria('role', 'menuitemcheckbox');
+ self.classes.add('menu-item-checkbox');
+ settings.icon = 'selected';
+ }
+
+ if (!settings.preview && !settings.selectable) {
+ self.classes.add('menu-item-normal');
+ }
+
+ self.on('mousedown', function(e) {
+ e.preventDefault();
+ });
+
+ if (settings.menu && !settings.ariaHideMenu) {
+ self.aria('haspopup', true);
+ }
+ },
+
+ /**
+ * Returns true/false if the menuitem has sub menu.
+ *
+ * @method hasMenus
+ * @return {Boolean} True/false state if it has submenu.
+ */
+ hasMenus: function() {
+ return !!this.settings.menu;
+ },
+
+ /**
+ * Shows the menu for the menu item.
+ *
+ * @method showMenu
+ */
+ showMenu: function() {
+ var self = this, settings = self.settings, menu, parent = self.parent();
+
+ parent.items().each(function(ctrl) {
+ if (ctrl !== self) {
+ ctrl.hideMenu();
+ }
+ });
+
+ if (settings.menu) {
+ menu = self.menu;
+
+ if (!menu) {
+ menu = settings.menu;
+
+ // Is menu array then auto constuct menu control
+ if (menu.length) {
+ menu = {
+ type: 'menu',
+ items: menu
+ };
+ } else {
+ menu.type = menu.type || 'menu';
+ }
+
+ if (parent.settings.itemDefaults) {
+ menu.itemDefaults = parent.settings.itemDefaults;
+ }
+
+ menu = self.menu = Factory.create(menu).parent(self).renderTo();
+ menu.reflow();
+ menu.on('cancel', function(e) {
+ e.stopPropagation();
+ self.focus();
+ menu.hide();
+ });
+ menu.on('show hide', function(e) {
+ e.control.items().each(function(ctrl) {
+ ctrl.active(ctrl.settings.selected);
+ });
+ }).fire('show');
+
+ menu.on('hide', function(e) {
+ if (e.control === menu) {
+ self.classes.remove('selected');
+ }
+ });
+
+ menu.submenu = true;
+ } else {
+ menu.show();
+ }
+
+ menu._parentMenu = parent;
+
+ menu.classes.add('menu-sub');
+
+ var rel = menu.testMoveRel(
+ self.getEl(),
+ self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br']
+ );
+
+ menu.moveRel(self.getEl(), rel);
+ menu.rel = rel;
+
+ rel = 'menu-sub-' + rel;
+ menu.classes.remove(menu._lastRel).add(rel);
+ menu._lastRel = rel;
+
+ self.classes.add('selected');
+ self.aria('expanded', true);
+ }
+ },
+
+ /**
+ * Hides the menu for the menu item.
+ *
+ * @method hideMenu
+ */
+ hideMenu: function() {
+ var self = this;
+
+ if (self.menu) {
+ self.menu.items().each(function(item) {
+ if (item.hideMenu) {
+ item.hideMenu();
+ }
+ });
+
+ self.menu.hide();
+ self.aria('expanded', false);
+ }
+
+ return self;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.encode(self.state.get('text'));
+ var icon = self.settings.icon, image = '', shortcut = settings.shortcut;
+
+ // Converts shortcut format to Mac/PC variants
+ function convertShortcut(shortcut) {
+ var i, value, replace = {};
+
+ if (Env.mac) {
+ replace = {
+ alt: '⌥',
+ ctrl: '⌘',
+ shift: '⇧',
+ meta: '⌘'
+ };
+ } else {
+ replace = {
+ meta: 'Ctrl'
+ };
+ }
+
+ shortcut = shortcut.split('+');
+
+ for (i = 0; i < shortcut.length; i++) {
+ value = replace[shortcut[i].toLowerCase()];
+
+ if (value) {
+ shortcut[i] = value;
+ }
+ }
+
+ return shortcut.join('+');
+ }
+
+ if (icon) {
+ self.parent().classes.add('menu-has-icons');
+ }
+
+ if (settings.image) {
+ image = ' style="background-image: url(\'' + settings.image + '\')"';
+ }
+
+ if (shortcut) {
+ shortcut = convertShortcut(shortcut);
+ }
+
+ icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
+
+ return (
+ '' +
+ (text !== '-' ? '
\u00a0' : '') +
+ (text !== '-' ? '
' + text + '' : '') +
+ (shortcut ? '' : '') +
+ (settings.menu ? '
' : '') +
+ '
'
+ );
+ },
+
+ /**
+ * Gets invoked after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, settings = self.settings;
+
+ var textStyle = settings.textStyle;
+ if (typeof textStyle == "function") {
+ textStyle = textStyle.call(this);
+ }
+
+ if (textStyle) {
+ var textElm = self.getEl('text');
+ if (textElm) {
+ textElm.setAttribute('style', textStyle);
+ }
+ }
+
+ self.on('mouseenter click', function(e) {
+ if (e.control === self) {
+ if (!settings.menu && e.type === 'click') {
+ self.fire('select');
+
+ // Edge will crash if you stress it see #2660
+ Delay.requestAnimationFrame(function() {
+ self.parent().hideAll();
+ });
+ } else {
+ self.showMenu();
+
+ if (e.aria) {
+ self.menu.focus(true);
+ }
+ }
+ }
+ });
+
+ self._super();
+
+ return self;
+ },
+
+ hover: function() {
+ var self = this;
+
+ self.parent().items().each(function(ctrl) {
+ ctrl.classes.remove('selected');
+ });
+
+ self.classes.toggle('selected', true);
+
+ return self;
+ },
+
+ active: function(state) {
+ if (typeof state != "undefined") {
+ this.aria('checked', state);
+ }
+
+ return this._super(state);
+ },
+
+ /**
+ * Removes the control and it's menus.
+ *
+ * @method remove
+ */
+ remove: function() {
+ this._super();
+
+ if (this.menu) {
+ this.menu.remove();
+ }
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Throbber.js
+
+/**
+ * Throbber.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class enables you to display a Throbber for any element.
+ *
+ * @-x-less Throbber.less
+ * @class tinymce.ui.Throbber
+ */
+define("tinymce/ui/Throbber", [
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/Control",
+ "tinymce/util/Delay"
+], function($, Control, Delay) {
+ "use strict";
+
+ /**
+ * Constructs a new throbber.
+ *
+ * @constructor
+ * @param {Element} elm DOM Html element to display throbber in.
+ * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll.
+ */
+ return function(elm, inline) {
+ var self = this, state, classPrefix = Control.classPrefix, timer;
+
+ /**
+ * Shows the throbber.
+ *
+ * @method show
+ * @param {Number} [time] Time to wait before showing.
+ * @param {function} [callback] Optional callback to execute when the throbber is shown.
+ * @return {tinymce.ui.Throbber} Current throbber instance.
+ */
+ self.show = function(time, callback) {
+ function render() {
+ if (state) {
+ $(elm).append(
+ ''
+ );
+
+ if (callback) {
+ callback();
+ }
+ }
+ }
+
+ self.hide();
+
+ state = true;
+
+ if (time) {
+ timer = Delay.setTimeout(render, time);
+ } else {
+ render();
+ }
+
+ return self;
+ };
+
+ /**
+ * Hides the throbber.
+ *
+ * @method hide
+ * @return {tinymce.ui.Throbber} Current throbber instance.
+ */
+ self.hide = function() {
+ var child = elm.lastChild;
+
+ Delay.clearTimeout(timer);
+
+ if (child && child.className.indexOf('throbber') != -1) {
+ child.parentNode.removeChild(child);
+ }
+
+ state = false;
+
+ return self;
+ };
+ };
+});
+
+// Included from: js/tinymce/classes/ui/Menu.js
+
+/**
+ * Menu.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new menu.
+ *
+ * @-x-less Menu.less
+ * @class tinymce.ui.Menu
+ * @extends tinymce.ui.FloatPanel
+ */
+define("tinymce/ui/Menu", [
+ "tinymce/ui/FloatPanel",
+ "tinymce/ui/MenuItem",
+ "tinymce/ui/Throbber",
+ "tinymce/util/Tools"
+], function(FloatPanel, MenuItem, Throbber, Tools) {
+ "use strict";
+
+ return FloatPanel.extend({
+ Defaults: {
+ defaultType: 'menuitem',
+ border: 1,
+ layout: 'stack',
+ role: 'application',
+ bodyRole: 'menu',
+ ariaRoot: true
+ },
+
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ */
+ init: function(settings) {
+ var self = this;
+
+ settings.autohide = true;
+ settings.constrainToViewport = true;
+
+ if (typeof settings.items === 'function') {
+ settings.itemsFactory = settings.items;
+ settings.items = [];
+ }
+
+ if (settings.itemDefaults) {
+ var items = settings.items, i = items.length;
+
+ while (i--) {
+ items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
+ }
+ }
+
+ self._super(settings);
+ self.classes.add('menu');
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ this.classes.toggle('menu-align', true);
+
+ this._super();
+
+ this.getEl().style.height = '';
+ this.getEl('body').style.height = '';
+
+ return this;
+ },
+
+ /**
+ * Hides/closes the menu.
+ *
+ * @method cancel
+ */
+ cancel: function() {
+ var self = this;
+
+ self.hideAll();
+ self.fire('select');
+ },
+
+ /**
+ * Loads new items from the factory items function.
+ *
+ * @method load
+ */
+ load: function() {
+ var self = this, time, factory;
+
+ function hideThrobber() {
+ if (self.throbber) {
+ self.throbber.hide();
+ self.throbber = null;
+ }
+ }
+
+ factory = self.settings.itemsFactory;
+ if (!factory) {
+ return;
+ }
+
+ if (!self.throbber) {
+ self.throbber = new Throbber(self.getEl('body'), true);
+
+ if (self.items().length === 0) {
+ self.throbber.show();
+ self.fire('loading');
+ } else {
+ self.throbber.show(100, function() {
+ self.items().remove();
+ self.fire('loading');
+ });
+ }
+
+ self.on('hide close', hideThrobber);
+ }
+
+ self.requestTime = time = new Date().getTime();
+
+ self.settings.itemsFactory(function(items) {
+ if (items.length === 0) {
+ self.hide();
+ return;
+ }
+
+ if (self.requestTime !== time) {
+ return;
+ }
+
+ self.getEl().style.width = '';
+ self.getEl('body').style.width = '';
+
+ hideThrobber();
+ self.items().remove();
+ self.getEl('body').innerHTML = '';
+
+ self.add(items);
+ self.renderNew();
+ self.fire('loaded');
+ });
+ },
+
+ /**
+ * Hide menu and all sub menus.
+ *
+ * @method hideAll
+ */
+ hideAll: function() {
+ var self = this;
+
+ this.find('menuitem').exec('hideMenu');
+
+ return self._super();
+ },
+
+ /**
+ * Invoked before the menu is rendered.
+ *
+ * @method preRender
+ */
+ preRender: function() {
+ var self = this;
+
+ self.items().each(function(ctrl) {
+ var settings = ctrl.settings;
+
+ if (settings.icon || settings.image || settings.selectable) {
+ self._hasIcons = true;
+ return false;
+ }
+ });
+
+ if (self.settings.itemsFactory) {
+ self.on('postrender', function() {
+ if (self.settings.itemsFactory) {
+ self.load();
+ }
+ });
+ }
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ListBox.js
+
+/**
+ * ListBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new list box control.
+ *
+ * @-x-less ListBox.less
+ * @class tinymce.ui.ListBox
+ * @extends tinymce.ui.MenuButton
+ */
+define("tinymce/ui/ListBox", [
+ "tinymce/ui/MenuButton",
+ "tinymce/ui/Menu"
+], function(MenuButton, Menu) {
+ "use strict";
+
+ return MenuButton.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Array} values Array with values to add to list box.
+ */
+ init: function(settings) {
+ var self = this, values, selected, selectedText, lastItemCtrl;
+
+ function setSelected(menuValues) {
+ // Try to find a selected value
+ for (var i = 0; i < menuValues.length; i++) {
+ selected = menuValues[i].selected || settings.value === menuValues[i].value;
+
+ if (selected) {
+ selectedText = selectedText || menuValues[i].text;
+ self.state.set('value', menuValues[i].value);
+ return true;
+ }
+
+ // If the value has a submenu, try to find the selected values in that menu
+ if (menuValues[i].menu) {
+ if (setSelected(menuValues[i].menu)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ self._super(settings);
+ settings = self.settings;
+
+ self._values = values = settings.values;
+ if (values) {
+ if (typeof settings.value != "undefined") {
+ setSelected(values);
+ }
+
+ // Default with first item
+ if (!selected && values.length > 0) {
+ selectedText = values[0].text;
+ self.state.set('value', values[0].value);
+ }
+
+ self.state.set('menu', values);
+ }
+
+ self.state.set('text', settings.text || selectedText);
+
+ self.classes.add('listbox');
+
+ self.on('select', function(e) {
+ var ctrl = e.control;
+
+ if (lastItemCtrl) {
+ e.lastControl = lastItemCtrl;
+ }
+
+ if (settings.multiple) {
+ ctrl.active(!ctrl.active());
+ } else {
+ self.value(e.control.value());
+ }
+
+ lastItemCtrl = ctrl;
+ });
+ },
+
+ /**
+ * Getter/setter function for the control value.
+ *
+ * @method value
+ * @param {String} [value] Value to be set.
+ * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation.
+ */
+ bindStates: function() {
+ var self = this;
+
+ function activateMenuItemsByValue(menu, value) {
+ if (menu instanceof Menu) {
+ menu.items().each(function(ctrl) {
+ if (!ctrl.hasMenus()) {
+ ctrl.active(ctrl.value() === value);
+ }
+ });
+ }
+ }
+
+ function getSelectedItem(menuValues, value) {
+ var selectedItem;
+
+ if (!menuValues) {
+ return;
+ }
+
+ for (var i = 0; i < menuValues.length; i++) {
+ if (menuValues[i].value === value) {
+ return menuValues[i];
+ }
+
+ if (menuValues[i].menu) {
+ selectedItem = getSelectedItem(menuValues[i].menu, value);
+ if (selectedItem) {
+ return selectedItem;
+ }
+ }
+ }
+ }
+
+ self.on('show', function(e) {
+ activateMenuItemsByValue(e.control, self.value());
+ });
+
+ self.state.on('change:value', function(e) {
+ var selectedItem = getSelectedItem(self.state.get('menu'), e.value);
+
+ if (selectedItem) {
+ self.text(selectedItem.text);
+ } else {
+ self.text(self.settings.text);
+ }
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Radio.js
+
+/**
+ * Radio.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new radio button.
+ *
+ * @-x-less Radio.less
+ * @class tinymce.ui.Radio
+ * @extends tinymce.ui.Checkbox
+ */
+define("tinymce/ui/Radio", [
+ "tinymce/ui/Checkbox"
+], function(Checkbox) {
+ "use strict";
+
+ return Checkbox.extend({
+ Defaults: {
+ classes: "radio",
+ role: "radio"
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/ResizeHandle.js
+
+/**
+ * ResizeHandle.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events.
+ *
+ * @-x-less ResizeHandle.less
+ * @class tinymce.ui.ResizeHandle
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/ResizeHandle", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper"
+], function(Widget, DragHelper) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, prefix = self.classPrefix;
+
+ self.classes.add('resizehandle');
+
+ if (self.settings.direction == "both") {
+ self.classes.add('resizehandle-both');
+ }
+
+ self.canFocus = false;
+
+ return (
+ '' +
+ '' +
+ '
'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+
+ self.resizeDragHelper = new DragHelper(this._id, {
+ start: function() {
+ self.fire('ResizeStart');
+ },
+
+ drag: function(e) {
+ if (self.settings.direction != "both") {
+ e.deltaX = 0;
+ }
+
+ self.fire('Resize', e);
+ },
+
+ stop: function() {
+ self.fire('ResizeEnd');
+ }
+ });
+ },
+
+ remove: function() {
+ if (this.resizeDragHelper) {
+ this.resizeDragHelper.destroy();
+ }
+
+ return this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/SelectBox.js
+
+/**
+ * SelectBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new select box control.
+ *
+ * @-x-less SelectBox.less
+ * @class tinymce.ui.SelectBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/SelectBox", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ function createOptions(options) {
+ var strOptions = '';
+ if (options) {
+ for (var i = 0; i < options.length; i++) {
+ strOptions += '';
+ }
+ }
+ return strOptions;
+ }
+
+ return Widget.extend({
+ Defaults: {
+ classes: "selectbox",
+ role: "selectbox",
+ options: []
+ },
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Array} values Array with values to add to list box.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ if (self.settings.size) {
+ self.size = self.settings.size;
+ }
+
+ if (self.settings.options) {
+ self._options = self.settings.options;
+ }
+
+ self.on('keydown', function(e) {
+ var rootControl;
+
+ if (e.keyCode == 13) {
+ e.preventDefault();
+
+ // Find root control that we can do toJSON on
+ self.parents().reverse().each(function(ctrl) {
+ if (ctrl.toJSON) {
+ rootControl = ctrl;
+ return false;
+ }
+ });
+
+ // Fire event on current text box with the serialized data of the whole form
+ self.fire('submit', {data: rootControl.toJSON()});
+ }
+ });
+ },
+
+ /**
+ * Getter/setter function for the options state.
+ *
+ * @method options
+ * @param {Array} [state] State to be set.
+ * @return {Array|tinymce.ui.SelectBox} Array of string options.
+ */
+ options: function(state) {
+ if (!arguments.length) {
+ return this.state.get('options');
+ }
+
+ this.state.set('options', state);
+
+ return this;
+ },
+
+ renderHtml: function() {
+ var self = this, options, size = '';
+
+ options = createOptions(self._options);
+
+ if (self.size) {
+ size = ' size = "' + self.size + '"';
+ }
+
+ return (
+ ''
+ );
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:options', function(e) {
+ self.getEl().innerHTML = createOptions(e.value);
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Slider.js
+
+/**
+ * Slider.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Slider control.
+ *
+ * @-x-less Slider.less
+ * @class tinymce.ui.Slider
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Slider", [
+ "tinymce/ui/Widget",
+ "tinymce/ui/DragHelper",
+ "tinymce/ui/DomUtils"
+], function(Widget, DragHelper, DomUtils) {
+ "use strict";
+
+ function constrain(value, minVal, maxVal) {
+ if (value < minVal) {
+ value = minVal;
+ }
+
+ if (value > maxVal) {
+ value = maxVal;
+ }
+
+ return value;
+ }
+
+ function setAriaProp(el, name, value) {
+ el.setAttribute('aria-' + name, value);
+ }
+
+ function updateSliderHandle(ctrl, value) {
+ var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl;
+
+ if (ctrl.settings.orientation == "v") {
+ stylePosName = "top";
+ sizeName = "height";
+ shortSizeName = "h";
+ } else {
+ stylePosName = "left";
+ sizeName = "width";
+ shortSizeName = "w";
+ }
+
+ handleEl = ctrl.getEl('handle');
+ maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
+
+ styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px';
+ handleEl.style[stylePosName] = styleValue;
+ handleEl.style.height = ctrl.layoutRect().h + 'px';
+
+ setAriaProp(handleEl, 'valuenow', value);
+ setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value));
+ setAriaProp(handleEl, 'valuemin', ctrl._minValue);
+ setAriaProp(handleEl, 'valuemax', ctrl._maxValue);
+ }
+
+ return Widget.extend({
+ init: function(settings) {
+ var self = this;
+
+ if (!settings.previewFilter) {
+ settings.previewFilter = function(value) {
+ return Math.round(value * 100) / 100.0;
+ };
+ }
+
+ self._super(settings);
+ self.classes.add('slider');
+
+ if (settings.orientation == "v") {
+ self.classes.add('vertical');
+ }
+
+ self._minValue = settings.minValue || 0;
+ self._maxValue = settings.maxValue || 100;
+ self._initValue = self.state.get('value');
+ },
+
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix;
+
+ return (
+ ''
+ );
+ },
+
+ reset: function() {
+ this.value(this._initValue).repaint();
+ },
+
+ postRender: function() {
+ var self = this, minValue, maxValue, screenCordName,
+ stylePosName, sizeName, shortSizeName;
+
+ function toFraction(min, max, val) {
+ return (val + min) / (max - min);
+ }
+
+ function fromFraction(min, max, val) {
+ return (val * (max - min)) - min;
+ }
+
+ function handleKeyboard(minValue, maxValue) {
+ function alter(delta) {
+ var value;
+
+ value = self.value();
+ value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05));
+ value = constrain(value, minValue, maxValue);
+
+ self.value(value);
+
+ self.fire('dragstart', {value: value});
+ self.fire('drag', {value: value});
+ self.fire('dragend', {value: value});
+ }
+
+ self.on('keydown', function(e) {
+ switch (e.keyCode) {
+ case 37:
+ case 38:
+ alter(-1);
+ break;
+
+ case 39:
+ case 40:
+ alter(1);
+ break;
+ }
+ });
+ }
+
+ function handleDrag(minValue, maxValue, handleEl) {
+ var startPos, startHandlePos, maxHandlePos, handlePos, value;
+
+ self._dragHelper = new DragHelper(self._id, {
+ handle: self._id + "-handle",
+
+ start: function(e) {
+ startPos = e[screenCordName];
+ startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10);
+ maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
+ self.fire('dragstart', {value: value});
+ },
+
+ drag: function(e) {
+ var delta = e[screenCordName] - startPos;
+
+ handlePos = constrain(startHandlePos + delta, 0, maxHandlePos);
+ handleEl.style[stylePosName] = handlePos + 'px';
+
+ value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue);
+ self.value(value);
+
+ self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc');
+
+ self.fire('drag', {value: value});
+ },
+
+ stop: function() {
+ self.tooltip().hide();
+ self.fire('dragend', {value: value});
+ }
+ });
+ }
+
+ minValue = self._minValue;
+ maxValue = self._maxValue;
+
+ if (self.settings.orientation == "v") {
+ screenCordName = "screenY";
+ stylePosName = "top";
+ sizeName = "height";
+ shortSizeName = "h";
+ } else {
+ screenCordName = "screenX";
+ stylePosName = "left";
+ sizeName = "width";
+ shortSizeName = "w";
+ }
+
+ self._super();
+
+ handleKeyboard(minValue, maxValue, self.getEl('handle'));
+ handleDrag(minValue, maxValue, self.getEl('handle'));
+ },
+
+ repaint: function() {
+ this._super();
+ updateSliderHandle(this, this.value());
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ updateSliderHandle(self, e.value);
+ });
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/Spacer.js
+
+/**
+ * Spacer.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a spacer. This control is used in flex layouts for example.
+ *
+ * @-x-less Spacer.less
+ * @class tinymce.ui.Spacer
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/Spacer", [
+ "tinymce/ui/Widget"
+], function(Widget) {
+ "use strict";
+
+ return Widget.extend({
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this;
+
+ self.classes.add('spacer');
+ self.canFocus = false;
+
+ return '';
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/SplitButton.js
+
+/**
+ * SplitButton.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a split button.
+ *
+ * @-x-less SplitButton.less
+ * @class tinymce.ui.SplitButton
+ * @extends tinymce.ui.Button
+ */
+define("tinymce/ui/SplitButton", [
+ "tinymce/ui/MenuButton",
+ "tinymce/ui/DomUtils",
+ "tinymce/dom/DomQuery"
+], function(MenuButton, DomUtils, $) {
+ return MenuButton.extend({
+ Defaults: {
+ classes: "widget btn splitbtn",
+ role: "button"
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm;
+
+ self._super();
+
+ mainButtonElm = elm.firstChild;
+ menuButtonElm = elm.lastChild;
+
+ $(mainButtonElm).css({
+ width: rect.w - DomUtils.getSize(menuButtonElm).width,
+ height: rect.h - 2
+ });
+
+ $(menuButtonElm).css({
+ height: rect.h - 2
+ });
+
+ return self;
+ },
+
+ /**
+ * Sets the active menu state.
+ *
+ * @private
+ */
+ activeMenu: function(state) {
+ var self = this;
+
+ $(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state);
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, id = self._id, prefix = self.classPrefix, image;
+ var icon = self.state.get('icon'), text = self.state.get('text'),
+ textHtml = '';
+
+ image = self.settings.image;
+ if (image) {
+ icon = 'none';
+
+ // Support for [high dpi, low dpi] image sources
+ if (typeof image != "string") {
+ image = window.getSelection ? image[0] : image[1];
+ }
+
+ image = ' style="background-image: url(\'' + image + '\')"';
+ } else {
+ image = '';
+ }
+
+ icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
+
+ if (text) {
+ self.classes.add('btn-has-text');
+ textHtml = '' + self.encode(text) + '';
+ }
+
+ return (
+ '' +
+ '' +
+ '' +
+ '
'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this, onClickHandler = self.settings.onclick;
+
+ self.on('click', function(e) {
+ var node = e.target;
+
+ if (e.control == this) {
+ // Find clicks that is on the main button
+ while (node) {
+ if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) {
+ e.stopImmediatePropagation();
+
+ if (onClickHandler) {
+ onClickHandler.call(this, e);
+ }
+
+ return;
+ }
+
+ node = node.parentNode;
+ }
+ }
+ });
+
+ delete self.settings.onclick;
+
+ return self._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/StackLayout.js
+
+/**
+ * StackLayout.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This layout uses the browsers layout when the items are blocks.
+ *
+ * @-x-less StackLayout.less
+ * @class tinymce.ui.StackLayout
+ * @extends tinymce.ui.FlowLayout
+ */
+define("tinymce/ui/StackLayout", [
+ "tinymce/ui/FlowLayout"
+], function(FlowLayout) {
+ "use strict";
+
+ return FlowLayout.extend({
+ Defaults: {
+ containerClass: 'stack-layout',
+ controlClass: 'stack-layout-item',
+ endClass: 'break'
+ },
+
+ isNative: function() {
+ return true;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/TabPanel.js
+
+/**
+ * TabPanel.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a tab panel control.
+ *
+ * @-x-less TabPanel.less
+ * @class tinymce.ui.TabPanel
+ * @extends tinymce.ui.Panel
+ *
+ * @setting {Number} activeTab Active tab index.
+ */
+define("tinymce/ui/TabPanel", [
+ "tinymce/ui/Panel",
+ "tinymce/dom/DomQuery",
+ "tinymce/ui/DomUtils"
+], function(Panel, $, DomUtils) {
+ "use strict";
+
+ return Panel.extend({
+ Defaults: {
+ layout: 'absolute',
+ defaults: {
+ type: 'panel'
+ }
+ },
+
+ /**
+ * Activates the specified tab by index.
+ *
+ * @method activateTab
+ * @param {Number} idx Index of the tab to activate.
+ */
+ activateTab: function(idx) {
+ var activeTabElm;
+
+ if (this.activeTabId) {
+ activeTabElm = this.getEl(this.activeTabId);
+ $(activeTabElm).removeClass(this.classPrefix + 'active');
+ activeTabElm.setAttribute('aria-selected', "false");
+ }
+
+ this.activeTabId = 't' + idx;
+
+ activeTabElm = this.getEl('t' + idx);
+ activeTabElm.setAttribute('aria-selected', "true");
+ $(activeTabElm).addClass(this.classPrefix + 'active');
+
+ this.items()[idx].show().fire('showtab');
+ this.reflow();
+
+ this.items().each(function(item, i) {
+ if (idx != i) {
+ item.hide();
+ }
+ });
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix;
+
+ self.preRender();
+ layout.preRender(self);
+
+ self.items().each(function(ctrl, i) {
+ var id = self._id + '-t' + i;
+
+ ctrl.aria('role', 'tabpanel');
+ ctrl.aria('labelledby', id);
+
+ tabsHtml += (
+ '' +
+ self.encode(ctrl.settings.title) +
+ '
'
+ );
+ });
+
+ return (
+ '' +
+ '
' +
+ tabsHtml +
+ '
' +
+ '
' +
+ layout.renderHtml(self) +
+ '
' +
+ '
'
+ );
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self._super();
+
+ self.settings.activeTab = self.settings.activeTab || 0;
+ self.activateTab(self.settings.activeTab);
+
+ this.on('click', function(e) {
+ var targetParent = e.target.parentNode;
+
+ if (e.target.parentNode.id == self._id + '-head') {
+ var i = targetParent.childNodes.length;
+
+ while (i--) {
+ if (targetParent.childNodes[i] == e.target) {
+ self.activateTab(i);
+ }
+ }
+ }
+ });
+ },
+
+ /**
+ * Initializes the current controls layout rect.
+ * This will be executed by the layout managers to determine the
+ * default minWidth/minHeight etc.
+ *
+ * @method initLayoutRect
+ * @return {Object} Layout rect instance.
+ */
+ initLayoutRect: function() {
+ var self = this, rect, minW, minH;
+
+ minW = DomUtils.getSize(self.getEl('head')).width;
+ minW = minW < 0 ? 0 : minW;
+ minH = 0;
+
+ self.items().each(function(item) {
+ minW = Math.max(minW, item.layoutRect().minW);
+ minH = Math.max(minH, item.layoutRect().minH);
+ });
+
+ self.items().each(function(ctrl) {
+ ctrl.settings.x = 0;
+ ctrl.settings.y = 0;
+ ctrl.settings.w = minW;
+ ctrl.settings.h = minH;
+
+ ctrl.layoutRect({
+ x: 0,
+ y: 0,
+ w: minW,
+ h: minH
+ });
+ });
+
+ var headH = DomUtils.getSize(self.getEl('head')).height;
+
+ self.settings.minWidth = minW;
+ self.settings.minHeight = minH + headH;
+
+ rect = self._super();
+ rect.deltaH += headH;
+ rect.innerH = rect.h - rect.deltaH;
+
+ return rect;
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/ui/TextBox.js
+
+/**
+ * TextBox.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Creates a new textbox.
+ *
+ * @-x-less TextBox.less
+ * @class tinymce.ui.TextBox
+ * @extends tinymce.ui.Widget
+ */
+define("tinymce/ui/TextBox", [
+ "tinymce/ui/Widget",
+ "tinymce/util/Tools",
+ "tinymce/ui/DomUtils"
+], function(Widget, Tools, DomUtils) {
+ return Widget.extend({
+ /**
+ * Constructs a instance with the specified settings.
+ *
+ * @constructor
+ * @param {Object} settings Name/value object with settings.
+ * @setting {Boolean} multiline True if the textbox is a multiline control.
+ * @setting {Number} maxLength Max length for the textbox.
+ * @setting {Number} size Size of the textbox in characters.
+ */
+ init: function(settings) {
+ var self = this;
+
+ self._super(settings);
+
+ self.classes.add('textbox');
+
+ if (settings.multiline) {
+ self.classes.add('multiline');
+ } else {
+ self.on('keydown', function(e) {
+ var rootControl;
+
+ if (e.keyCode == 13) {
+ e.preventDefault();
+
+ // Find root control that we can do toJSON on
+ self.parents().reverse().each(function(ctrl) {
+ if (ctrl.toJSON) {
+ rootControl = ctrl;
+ return false;
+ }
+ });
+
+ // Fire event on current text box with the serialized data of the whole form
+ self.fire('submit', {data: rootControl.toJSON()});
+ }
+ });
+
+ self.on('keyup', function(e) {
+ self.state.set('value', e.target.value);
+ });
+ }
+ },
+
+ /**
+ * Repaints the control after a layout operation.
+ *
+ * @method repaint
+ */
+ repaint: function() {
+ var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect;
+
+ style = self.getEl().style;
+ rect = self._layoutRect;
+ lastRepaintRect = self._lastRepaintRect || {};
+
+ // Detect old IE 7+8 add lineHeight to align caret vertically in the middle
+ var doc = document;
+ if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
+ style.lineHeight = (rect.h - borderH) + 'px';
+ }
+
+ borderBox = self.borderBox;
+ borderW = borderBox.left + borderBox.right + 8;
+ borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
+
+ if (rect.x !== lastRepaintRect.x) {
+ style.left = rect.x + 'px';
+ lastRepaintRect.x = rect.x;
+ }
+
+ if (rect.y !== lastRepaintRect.y) {
+ style.top = rect.y + 'px';
+ lastRepaintRect.y = rect.y;
+ }
+
+ if (rect.w !== lastRepaintRect.w) {
+ style.width = (rect.w - borderW) + 'px';
+ lastRepaintRect.w = rect.w;
+ }
+
+ if (rect.h !== lastRepaintRect.h) {
+ style.height = (rect.h - borderH) + 'px';
+ lastRepaintRect.h = rect.h;
+ }
+
+ self._lastRepaintRect = lastRepaintRect;
+ self.fire('repaint', {}, false);
+
+ return self;
+ },
+
+ /**
+ * Renders the control as a HTML string.
+ *
+ * @method renderHtml
+ * @return {String} HTML representing the control.
+ */
+ renderHtml: function() {
+ var self = this, settings = self.settings, attrs, elm;
+
+ attrs = {
+ id: self._id,
+ hidefocus: '1'
+ };
+
+ Tools.each([
+ 'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min',
+ 'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple'
+ ], function(name) {
+ attrs[name] = settings[name];
+ });
+
+ if (self.disabled()) {
+ attrs.disabled = 'disabled';
+ }
+
+ if (settings.subtype) {
+ attrs.type = settings.subtype;
+ }
+
+ elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs);
+ elm.value = self.state.get('value');
+ elm.className = self.classes;
+
+ return elm.outerHTML;
+ },
+
+ value: function(value) {
+ if (arguments.length) {
+ this.state.set('value', value);
+ return this;
+ }
+
+ // Make sure the real state is in sync
+ if (this.state.get('rendered')) {
+ this.state.set('value', this.getEl().value);
+ }
+
+ return this.state.get('value');
+ },
+
+ /**
+ * Called after the control has been rendered.
+ *
+ * @method postRender
+ */
+ postRender: function() {
+ var self = this;
+
+ self.getEl().value = self.state.get('value');
+ self._super();
+
+ self.$el.on('change', function(e) {
+ self.state.set('value', e.target.value);
+ self.fire('change', e);
+ });
+ },
+
+ bindStates: function() {
+ var self = this;
+
+ self.state.on('change:value', function(e) {
+ if (self.getEl().value != e.value) {
+ self.getEl().value = e.value;
+ }
+ });
+
+ self.state.on('change:disabled', function(e) {
+ self.getEl().disabled = e.value;
+ });
+
+ return self._super();
+ },
+
+ remove: function() {
+ this.$el.off();
+ this._super();
+ }
+ });
+});
+
+// Included from: js/tinymce/classes/Register.js
+
+/**
+ * Register.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This registers tinymce in common module loaders.
+ *
+ * @private
+ * @class tinymce.Register
+ */
+define("tinymce/Register", [
+], function() {
+ /*eslint consistent-this: 0 */
+ var context = this || window;
+
+ var tinymce = function() {
+ return context.tinymce;
+ };
+
+ if (typeof context.define === "function") {
+ // Bolt
+ if (!context.define.amd) {
+ context.define("ephox/tinymce", [], tinymce);
+ }
+ }
+
+ return {};
+});
+
+expose(["tinymce/geom/Rect","tinymce/util/Promise","tinymce/util/Delay","tinymce/Env","tinymce/dom/EventUtils","tinymce/dom/Sizzle","tinymce/util/Tools","tinymce/dom/DomQuery","tinymce/html/Styles","tinymce/dom/TreeWalker","tinymce/html/Entities","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/dom/RangeUtils","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/Formatter","tinymce/UndoManager","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/util/Observable","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/ReflowQueue","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Progress","tinymce/ui/Notification","tinymce/NotificationManager","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/ComboBox","tinymce/ui/ColorBox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/util/Color","tinymce/ui/ColorPicker","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/InfoBox","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/MenuItem","tinymce/ui/Throbber","tinymce/ui/Menu","tinymce/ui/ListBox","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/SelectBox","tinymce/ui/Slider","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox"]);
+})(this);
+!function(a){function b(){function b(a){"remove"===a&&this.each(function(a,b){var c=e(b);c&&c.remove()}),this.find("span.mceEditor,div.mceEditor").each(function(a,b){var c=tinymce.get(b.id.replace(/_parent$/,""));c&&c.remove()})}function d(a){var c,d=this;if(null!=a)b.call(d),d.each(function(b,c){var d;(d=tinymce.get(c.id))&&d.setContent(a)});else if(d.length>0&&(c=tinymce.get(d[0].id)))return c.getContent()}function e(a){var b=null;return a&&a.id&&g.tinymce&&(b=tinymce.get(a.id)),b}function f(a){return!!(a&&a.length&&g.tinymce&&a.is(":tinymce"))}var h={};a.each(["text","html","val"],function(b,g){var i=h[g]=a.fn[g],j="text"===g;a.fn[g]=function(b){var g=this;if(!f(g))return i.apply(g,arguments);if(b!==c)return d.call(g.filter(":tinymce"),b),i.apply(g.not(":tinymce"),arguments),g;var h="",k=arguments;return(j?g:g.eq(0)).each(function(b,c){var d=e(c);h+=d?j?d.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g,""):d.getContent({save:!0}):i.apply(a(c),k)}),h}}),a.each(["append","prepend"],function(b,d){var g=h[d]=a.fn[d],i="prepend"===d;a.fn[d]=function(a){var b=this;return f(b)?a!==c?("string"==typeof a&&b.filter(":tinymce").each(function(b,c){var d=e(c);d&&d.setContent(i?a+d.getContent():d.getContent()+a)}),g.apply(b.not(":tinymce"),arguments),b):void 0:g.apply(b,arguments)}}),a.each(["remove","replaceWith","replaceAll","empty"],function(c,d){var e=h[d]=a.fn[d];a.fn[d]=function(){return b.call(this,d),e.apply(this,arguments)}}),h.attr=a.fn.attr,a.fn.attr=function(b,g){var i=this,j=arguments;if(!b||"value"!==b||!f(i))return g!==c?h.attr.apply(i,j):h.attr.apply(i,j);if(g!==c)return d.call(i.filter(":tinymce"),g),h.attr.apply(i.not(":tinymce"),j),i;var k=i[0],l=e(k);return l?l.getContent({save:!0}):h.attr.apply(a(k),j)}}var c,d,e,f=[],g=window;a.fn.tinymce=function(c){function h(){var d=[],f=0;e||(b(),e=!0),l.each(function(a,b){var e,g=b.id,h=c.oninit;g||(b.id=g=tinymce.DOM.uniqueId()),tinymce.get(g)||(e=new tinymce.Editor(g,c,tinymce.EditorManager),d.push(e),e.on("init",function(){var a,b=h;l.css("visibility",""),h&&++f==d.length&&("string"==typeof b&&(a=-1===b.indexOf(".")?null:tinymce.resolve(b.replace(/\.\w+$/,"")),b=tinymce.resolve(b)),b.apply(a||tinymce,d))}))}),a.each(d,function(a,b){b.render()})}var i,j,k,l=this,m="";if(!l.length)return l;if(!c)return window.tinymce?tinymce.get(l[0].id):null;if(l.css("visibility","hidden"),g.tinymce||d||!(i=c.script_url))1===d?f.push(h):h();else{d=1,j=i.substring(0,i.lastIndexOf("/")),-1!=i.indexOf(".min")&&(m=".min"),g.tinymce=g.tinyMCEPreInit||{base:j,suffix:m},-1!=i.indexOf("gzip")&&(k=c.language||"en",i=i+(/\?/.test(i)?"&":"?")+"js=true&core=true&suffix="+escape(m)+"&themes="+escape(c.theme||"modern")+"&plugins="+escape(c.plugins||"")+"&languages="+(k||""),g.tinyMCE_GZ||(g.tinyMCE_GZ={start:function(){function b(a){tinymce.ScriptLoader.markDone(tinymce.baseURI.toAbsolute(a))}b("langs/"+k+".js"),b("themes/"+c.theme+"/theme"+m+".js"),b("themes/"+c.theme+"/langs/"+k+".js"),a.each(c.plugins.split(","),function(a,c){c&&(b("plugins/"+c+"/plugin"+m+".js"),b("plugins/"+c+"/langs/"+k+".js"))})},end:function(){}}));var n=document.createElement("script");n.type="text/javascript",n.onload=n.onreadystatechange=function(b){b=b||window.event,2===d||"load"!=b.type&&!/complete|loaded/.test(n.readyState)||(tinymce.dom.Event.domLoaded=1,d=2,c.script_loaded&&c.script_loaded(),h(),a.each(f,function(a,b){b()}))},n.src=i,document.body.appendChild(n)}return l},a.extend(a.expr[":"],{tinymce:function(a){var b;return!!(a.id&&"tinymce"in window&&(b=tinymce.get(a.id),b&&b.editorManager===tinymce))}})}(jQuery);
+
+
+
+/*!
+ * Modernizr v2.7.1
+ * www.modernizr.com
+ *
+ * Copyright (c) Faruk Ates, Paul Irish, Alex Sexton
+ * Available under the BSD and MIT licenses: www.modernizr.com/license/
+ */
+
+/*
+ * Modernizr tests which native CSS3 and HTML5 features are available in
+ * the current UA and makes the results available to you in two ways:
+ * as properties on a global Modernizr object, and as classes on the
+ * element. This information allows you to progressively enhance
+ * your pages with a granular level of control over the experience.
+ *
+ * Modernizr has an optional (not included) conditional resource loader
+ * called Modernizr.load(), based on Yepnope.js (yepnopejs.com).
+ * To get a build that includes Modernizr.load(), as well as choosing
+ * which tests to include, go to www.modernizr.com/download/
+ *
+ * Authors Faruk Ates, Paul Irish, Alex Sexton
+ * Contributors Ryan Seddon, Ben Alman
+ */
+
+
+window.Modernizr = (function( window, document, undefined ) {
+
+ var version = '2.7.1',
+
+ Modernizr = {},
+
+ /*>>cssclasses*/
+ // option for enabling the HTML classes to be added
+ enableClasses = true,
+ /*>>cssclasses*/
+
+ docElement = document.documentElement,
+
+ /**
+ * Create our "modernizr" element that we do most feature tests on.
+ */
+ mod = 'modernizr',
+ modElem = document.createElement(mod),
+ mStyle = modElem.style,
+
+ /**
+ * Create the input element for various Web Forms feature tests.
+ */
+ inputElem /*>>inputelem*/ = document.createElement('input') /*>>inputelem*/ ,
+
+ /*>>smile*/
+ smile = ':)',
+ /*>>smile*/
+
+ toString = {}.toString,
+
+ // TODO :: make the prefixes more granular
+ /*>>prefixes*/
+ // List of property values to set for css tests. See ticket #21
+ prefixes = ' -webkit- -moz- -o- -ms- '.split(' '),
+ /*>>prefixes*/
+
+ /*>>domprefixes*/
+ // Following spec is to expose vendor-specific style properties as:
+ // elem.style.WebkitBorderRadius
+ // and the following would be incorrect:
+ // elem.style.webkitBorderRadius
+
+ // Webkit ghosts their properties in lowercase but Opera & Moz do not.
+ // Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+
+ // erik.eae.net/archives/2008/03/10/21.48.10/
+
+ // More here: github.com/Modernizr/Modernizr/issues/issue/21
+ omPrefixes = 'Webkit Moz O ms',
+
+ cssomPrefixes = omPrefixes.split(' '),
+
+ domPrefixes = omPrefixes.toLowerCase().split(' '),
+ /*>>domprefixes*/
+
+ /*>>ns*/
+ ns = {'svg': 'http://www.w3.org/2000/svg'},
+ /*>>ns*/
+
+ tests = {},
+ inputs = {},
+ attrs = {},
+
+ classes = [],
+
+ slice = classes.slice,
+
+ featureName, // used in testing loop
+
+
+ /*>>teststyles*/
+ // Inject element with style element and some CSS rules
+ injectElementWithStyles = function( rule, callback, nodes, testnames ) {
+
+ var style, ret, node, docOverflow,
+ div = document.createElement('div'),
+ // After page load injecting a fake body doesn't work so check if body exists
+ body = document.body,
+ // IE6 and 7 won't return offsetWidth or offsetHeight unless it's in the body element, so we fake it.
+ fakeBody = body || document.createElement('body');
+
+ if ( parseInt(nodes, 10) ) {
+ // In order not to give false positives we create a node for each test
+ // This also allows the method to scale for unspecified uses
+ while ( nodes-- ) {
+ node = document.createElement('div');
+ node.id = testnames ? testnames[nodes] : mod + (nodes + 1);
+ div.appendChild(node);
+ }
+ }
+
+ // '].join('');
+ div.id = mod;
+ // IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody.
+ // Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270
+ (body ? div : fakeBody).innerHTML += style;
+ fakeBody.appendChild(div);
+ if ( !body ) {
+ //avoid crashing IE8, if background image is used
+ fakeBody.style.background = '';
+ //Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible
+ fakeBody.style.overflow = 'hidden';
+ docOverflow = docElement.style.overflow;
+ docElement.style.overflow = 'hidden';
+ docElement.appendChild(fakeBody);
+ }
+
+ ret = callback(div, rule);
+ // If this is done after page load we don't want to remove the body so check if body exists
+ if ( !body ) {
+ fakeBody.parentNode.removeChild(fakeBody);
+ docElement.style.overflow = docOverflow;
+ } else {
+ div.parentNode.removeChild(div);
+ }
+
+ return !!ret;
+
+ },
+ /*>>teststyles*/
+
+ /*>>mq*/
+ // adapted from matchMedia polyfill
+ // by Scott Jehl and Paul Irish
+ // gist.github.com/786768
+ testMediaQuery = function( mq ) {
+
+ var matchMedia = window.matchMedia || window.msMatchMedia;
+ if ( matchMedia ) {
+ return matchMedia(mq).matches;
+ }
+
+ var bool;
+
+ injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) {
+ bool = (window.getComputedStyle ?
+ getComputedStyle(node, null) :
+ node.currentStyle)['position'] == 'absolute';
+ });
+
+ return bool;
+
+ },
+ /*>>mq*/
+
+
+ /*>>hasevent*/
+ //
+ // isEventSupported determines if a given element supports the given event
+ // kangax.github.com/iseventsupported/
+ //
+ // The following results are known incorrects:
+ // Modernizr.hasEvent("webkitTransitionEnd", elem) // false negative
+ // Modernizr.hasEvent("textInput") // in Webkit. github.com/Modernizr/Modernizr/issues/333
+ // ...
+ isEventSupported = (function() {
+
+ var TAGNAMES = {
+ 'select': 'input', 'change': 'input',
+ 'submit': 'form', 'reset': 'form',
+ 'error': 'img', 'load': 'img', 'abort': 'img'
+ };
+
+ function isEventSupported( eventName, element ) {
+
+ element = element || document.createElement(TAGNAMES[eventName] || 'div');
+ eventName = 'on' + eventName;
+
+ // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those
+ var isSupported = eventName in element;
+
+ if ( !isSupported ) {
+ // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element
+ if ( !element.setAttribute ) {
+ element = document.createElement('div');
+ }
+ if ( element.setAttribute && element.removeAttribute ) {
+ element.setAttribute(eventName, '');
+ isSupported = is(element[eventName], 'function');
+
+ // If property was created, "remove it" (by setting value to `undefined`)
+ if ( !is(element[eventName], 'undefined') ) {
+ element[eventName] = undefined;
+ }
+ element.removeAttribute(eventName);
+ }
+ }
+
+ element = null;
+ return isSupported;
+ }
+ return isEventSupported;
+ })(),
+ /*>>hasevent*/
+
+ // TODO :: Add flag for hasownprop ? didn't last time
+
+ // hasOwnProperty shim by kangax needed for Safari 2.0 support
+ _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;
+
+ if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {
+ hasOwnProp = function (object, property) {
+ return _hasOwnProperty.call(object, property);
+ };
+ }
+ else {
+ hasOwnProp = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */
+ return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
+ };
+ }
+
+ // Adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js
+ // es5.github.com/#x15.3.4.5
+
+ if (!Function.prototype.bind) {
+ Function.prototype.bind = function bind(that) {
+
+ var target = this;
+
+ if (typeof target != "function") {
+ throw new TypeError();
+ }
+
+ var args = slice.call(arguments, 1),
+ bound = function () {
+
+ if (this instanceof bound) {
+
+ var F = function(){};
+ F.prototype = target.prototype;
+ var self = new F();
+
+ var result = target.apply(
+ self,
+ args.concat(slice.call(arguments))
+ );
+ if (Object(result) === result) {
+ return result;
+ }
+ return self;
+
+ } else {
+
+ return target.apply(
+ that,
+ args.concat(slice.call(arguments))
+ );
+
+ }
+
+ };
+
+ return bound;
+ };
+ }
+
+ /**
+ * setCss applies given styles to the Modernizr DOM node.
+ */
+ function setCss( str ) {
+ mStyle.cssText = str;
+ }
+
+ /**
+ * setCssAll extrapolates all vendor-specific css strings.
+ */
+ function setCssAll( str1, str2 ) {
+ return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));
+ }
+
+ /**
+ * is returns a boolean for if typeof obj is exactly type.
+ */
+ function is( obj, type ) {
+ return typeof obj === type;
+ }
+
+ /**
+ * contains returns a boolean for if substr is found within str.
+ */
+ function contains( str, substr ) {
+ return !!~('' + str).indexOf(substr);
+ }
+
+ /*>>testprop*/
+
+ // testProps is a generic CSS / DOM property test.
+
+ // In testing support for a given CSS property, it's legit to test:
+ // `elem.style[styleName] !== undefined`
+ // If the property is supported it will return an empty string,
+ // if unsupported it will return undefined.
+
+ // We'll take advantage of this quick test and skip setting a style
+ // on our modernizr element, but instead just testing undefined vs
+ // empty string.
+
+ // Because the testing of the CSS property names (with "-", as
+ // opposed to the camelCase DOM properties) is non-portable and
+ // non-standard but works in WebKit and IE (but not Gecko or Opera),
+ // we explicitly reject properties with dashes so that authors
+ // developing in WebKit or IE first don't end up with
+ // browser-specific content by accident.
+
+ function testProps( props, prefixed ) {
+ for ( var i in props ) {
+ var prop = props[i];
+ if ( !contains(prop, "-") && mStyle[prop] !== undefined ) {
+ return prefixed == 'pfx' ? prop : true;
+ }
+ }
+ return false;
+ }
+ /*>>testprop*/
+
+ // TODO :: add testDOMProps
+ /**
+ * testDOMProps is a generic DOM property test; if a browser supports
+ * a certain property, it won't return undefined for it.
+ */
+ function testDOMProps( props, obj, elem ) {
+ for ( var i in props ) {
+ var item = obj[props[i]];
+ if ( item !== undefined) {
+
+ // return the property name as a string
+ if (elem === false) return props[i];
+
+ // let's bind a function
+ if (is(item, 'function')){
+ // default to autobind unless override
+ return item.bind(elem || obj);
+ }
+
+ // return the unbound function or obj or value
+ return item;
+ }
+ }
+ return false;
+ }
+
+ /*>>testallprops*/
+ /**
+ * testPropsAll tests a list of DOM properties we want to check against.
+ * We specify literally ALL possible (known and/or likely) properties on
+ * the element including the non-vendor prefixed one, for forward-
+ * compatibility.
+ */
+ function testPropsAll( prop, prefixed, elem ) {
+
+ var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1),
+ props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' ');
+
+ // did they call .prefixed('boxSizing') or are we just testing a prop?
+ if(is(prefixed, "string") || is(prefixed, "undefined")) {
+ return testProps(props, prefixed);
+
+ // otherwise, they called .prefixed('requestAnimationFrame', window[, elem])
+ } else {
+ props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' ');
+ return testDOMProps(props, prefixed, elem);
+ }
+ }
+ /*>>testallprops*/
+
+
+ /**
+ * Tests
+ * -----
+ */
+
+ // The *new* flexbox
+ // dev.w3.org/csswg/css3-flexbox
+
+ tests['flexbox'] = function() {
+ return testPropsAll('flexWrap');
+ };
+
+ // The *old* flexbox
+ // www.w3.org/TR/2009/WD-css3-flexbox-20090723/
+
+ tests['flexboxlegacy'] = function() {
+ return testPropsAll('boxDirection');
+ };
+
+ // On the S60 and BB Storm, getContext exists, but always returns undefined
+ // so we actually have to call getContext() to verify
+ // github.com/Modernizr/Modernizr/issues/issue/97/
+
+ tests['canvas'] = function() {
+ var elem = document.createElement('canvas');
+ return !!(elem.getContext && elem.getContext('2d'));
+ };
+
+ tests['canvastext'] = function() {
+ return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function'));
+ };
+
+ // webk.it/70117 is tracking a legit WebGL feature detect proposal
+
+ // We do a soft detect which may false positive in order to avoid
+ // an expensive context creation: bugzil.la/732441
+
+ tests['webgl'] = function() {
+ return !!window.WebGLRenderingContext;
+ };
+
+ /*
+ * The Modernizr.touch test only indicates if the browser supports
+ * touch events, which does not necessarily reflect a touchscreen
+ * device, as evidenced by tablets running Windows 7 or, alas,
+ * the Palm Pre / WebOS (touch) phones.
+ *
+ * Additionally, Chrome (desktop) used to lie about its support on this,
+ * but that has since been rectified: crbug.com/36415
+ *
+ * We also test for Firefox 4 Multitouch Support.
+ *
+ * For more info, see: modernizr.github.com/Modernizr/touch.html
+ */
+
+ tests['touch'] = function() {
+ var bool;
+
+ if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
+ bool = true;
+ } else {
+ injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) {
+ bool = node.offsetTop === 9;
+ });
+ }
+
+ return bool;
+ };
+
+
+ // geolocation is often considered a trivial feature detect...
+ // Turns out, it's quite tricky to get right:
+ //
+ // Using !!navigator.geolocation does two things we don't want. It:
+ // 1. Leaks memory in IE9: github.com/Modernizr/Modernizr/issues/513
+ // 2. Disables page caching in WebKit: webk.it/43956
+ //
+ // Meanwhile, in Firefox < 8, an about:config setting could expose
+ // a false positive that would throw an exception: bugzil.la/688158
+
+ tests['geolocation'] = function() {
+ return 'geolocation' in navigator;
+ };
+
+
+ tests['postmessage'] = function() {
+ return !!window.postMessage;
+ };
+
+
+ // Chrome incognito mode used to throw an exception when using openDatabase
+ // It doesn't anymore.
+ tests['websqldatabase'] = function() {
+ return !!window.openDatabase;
+ };
+
+ // Vendors had inconsistent prefixing with the experimental Indexed DB:
+ // - Webkit's implementation is accessible through webkitIndexedDB
+ // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB
+ // For speed, we don't test the legacy (and beta-only) indexedDB
+ tests['indexedDB'] = function() {
+ return !!testPropsAll("indexedDB", window);
+ };
+
+ // documentMode logic from YUI to filter out IE8 Compat Mode
+ // which false positives.
+ tests['hashchange'] = function() {
+ return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7);
+ };
+
+ // Per 1.6:
+ // This used to be Modernizr.historymanagement but the longer
+ // name has been deprecated in favor of a shorter and property-matching one.
+ // The old API is still available in 1.6, but as of 2.0 will throw a warning,
+ // and in the first release thereafter disappear entirely.
+ tests['history'] = function() {
+ return !!(window.history && history.pushState);
+ };
+
+ tests['draganddrop'] = function() {
+ var div = document.createElement('div');
+ return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
+ };
+
+ // FF3.6 was EOL'ed on 4/24/12, but the ESR version of FF10
+ // will be supported until FF19 (2/12/13), at which time, ESR becomes FF17.
+ // FF10 still uses prefixes, so check for it until then.
+ // for more ESR info, see: mozilla.org/en-US/firefox/organizations/faq/
+ tests['websockets'] = function() {
+ return 'WebSocket' in window || 'MozWebSocket' in window;
+ };
+
+
+ // css-tricks.com/rgba-browser-support/
+ tests['rgba'] = function() {
+ // Set an rgba() color and check the returned value
+
+ setCss('background-color:rgba(150,255,150,.5)');
+
+ return contains(mStyle.backgroundColor, 'rgba');
+ };
+
+ tests['hsla'] = function() {
+ // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally,
+ // except IE9 who retains it as hsla
+
+ setCss('background-color:hsla(120,40%,100%,.5)');
+
+ return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
+ };
+
+ tests['multiplebgs'] = function() {
+ // Setting multiple images AND a color on the background shorthand property
+ // and then querying the style.background property value for the number of
+ // occurrences of "url(" is a reliable method for detecting ACTUAL support for this!
+
+ setCss('background:url(https://),url(https://),red url(https://)');
+
+ // If the UA supports multiple backgrounds, there should be three occurrences
+ // of the string "url(" in the return value for elemStyle.background
+
+ return (/(url\s*\(.*?){3}/).test(mStyle.background);
+ };
+
+
+
+ // this will false positive in Opera Mini
+ // github.com/Modernizr/Modernizr/issues/396
+
+ tests['backgroundsize'] = function() {
+ return testPropsAll('backgroundSize');
+ };
+
+ tests['borderimage'] = function() {
+ return testPropsAll('borderImage');
+ };
+
+
+ // Super comprehensive table about all the unique implementations of
+ // border-radius: muddledramblings.com/table-of-css3-border-radius-compliance
+
+ tests['borderradius'] = function() {
+ return testPropsAll('borderRadius');
+ };
+
+ // WebOS unfortunately false positives on this test.
+ tests['boxshadow'] = function() {
+ return testPropsAll('boxShadow');
+ };
+
+ // FF3.0 will false positive on this test
+ tests['textshadow'] = function() {
+ return document.createElement('div').style.textShadow === '';
+ };
+
+
+ tests['opacity'] = function() {
+ // Browsers that actually have CSS Opacity implemented have done so
+ // according to spec, which means their return values are within the
+ // range of [0.0,1.0] - including the leading zero.
+
+ setCssAll('opacity:.55');
+
+ // The non-literal . in this regex is intentional:
+ // German Chrome returns this value as 0,55
+ // github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632
+ return (/^0.55$/).test(mStyle.opacity);
+ };
+
+
+ // Note, Android < 4 will pass this test, but can only animate
+ // a single property at a time
+ // daneden.me/2011/12/putting-up-with-androids-bullshit/
+ tests['cssanimations'] = function() {
+ return testPropsAll('animationName');
+ };
+
+
+ tests['csscolumns'] = function() {
+ return testPropsAll('columnCount');
+ };
+
+
+ tests['cssgradients'] = function() {
+ /**
+ * For CSS Gradients syntax, please see:
+ * webkit.org/blog/175/introducing-css-gradients/
+ * developer.mozilla.org/en/CSS/-moz-linear-gradient
+ * developer.mozilla.org/en/CSS/-moz-radial-gradient
+ * dev.w3.org/csswg/css3-images/#gradients-
+ */
+
+ var str1 = 'background-image:',
+ str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));',
+ str3 = 'linear-gradient(left top,#9f9, white);';
+
+ setCss(
+ // legacy webkit syntax (FIXME: remove when syntax not in use anymore)
+ (str1 + '-webkit- '.split(' ').join(str2 + str1) +
+ // standard syntax // trailing 'background-image:'
+ prefixes.join(str3 + str1)).slice(0, -str1.length)
+ );
+
+ return contains(mStyle.backgroundImage, 'gradient');
+ };
+
+
+ tests['cssreflections'] = function() {
+ return testPropsAll('boxReflect');
+ };
+
+
+ tests['csstransforms'] = function() {
+ return !!testPropsAll('transform');
+ };
+
+
+ tests['csstransforms3d'] = function() {
+
+ var ret = !!testPropsAll('perspective');
+
+ // Webkit's 3D transforms are passed off to the browser's own graphics renderer.
+ // It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in
+ // some conditions. As a result, Webkit typically recognizes the syntax but
+ // will sometimes throw a false positive, thus we must do a more thorough check:
+ if ( ret && 'webkitPerspective' in docElement.style ) {
+
+ // Webkit allows this media query to succeed only if the feature is enabled.
+ // `@media (transform-3d),(-webkit-transform-3d){ ... }`
+ injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) {
+ ret = node.offsetLeft === 9 && node.offsetHeight === 3;
+ });
+ }
+ return ret;
+ };
+
+
+ tests['csstransitions'] = function() {
+ return testPropsAll('transition');
+ };
+
+
+ /*>>fontface*/
+ // @font-face detection routine by Diego Perini
+ // javascript.nwbox.com/CSSSupport/
+
+ // false positives:
+ // WebOS github.com/Modernizr/Modernizr/issues/342
+ // WP7 github.com/Modernizr/Modernizr/issues/538
+ tests['fontface'] = function() {
+ var bool;
+
+ injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) {
+ var style = document.getElementById('smodernizr'),
+ sheet = style.sheet || style.styleSheet,
+ cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : '';
+
+ bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0;
+ });
+
+ return bool;
+ };
+ /*>>fontface*/
+
+ // CSS generated content detection
+ tests['generatedcontent'] = function() {
+ var bool;
+
+ injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) {
+ bool = node.offsetHeight >= 3;
+ });
+
+ return bool;
+ };
+
+
+
+ // These tests evaluate support of the video/audio elements, as well as
+ // testing what types of content they support.
+ //
+ // We're using the Boolean constructor here, so that we can extend the value
+ // e.g. Modernizr.video // true
+ // Modernizr.video.ogg // 'probably'
+ //
+ // Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845
+ // thx to NielsLeenheer and zcorpan
+
+ // Note: in some older browsers, "no" was a return value instead of empty string.
+ // It was live in FF3.5.0 and 3.5.1, but fixed in 3.5.2
+ // It was also live in Safari 4.0.0 - 4.0.4, but fixed in 4.0.5
+
+ tests['video'] = function() {
+ var elem = document.createElement('video'),
+ bool = false;
+
+ // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224
+ try {
+ if ( bool = !!elem.canPlayType ) {
+ bool = new Boolean(bool);
+ bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,'');
+
+ // Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546
+ bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,'');
+
+ bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,'');
+ }
+
+ } catch(e) { }
+
+ return bool;
+ };
+
+ tests['audio'] = function() {
+ var elem = document.createElement('audio'),
+ bool = false;
+
+ try {
+ if ( bool = !!elem.canPlayType ) {
+ bool = new Boolean(bool);
+ bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,'');
+ bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,'');
+
+ // Mimetypes accepted:
+ // developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements
+ // bit.ly/iphoneoscodecs
+ bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,'');
+ bool.m4a = ( elem.canPlayType('audio/x-m4a;') ||
+ elem.canPlayType('audio/aac;')) .replace(/^no$/,'');
+ }
+ } catch(e) { }
+
+ return bool;
+ };
+
+
+ // In FF4, if disabled, window.localStorage should === null.
+
+ // Normally, we could not test that directly and need to do a
+ // `('localStorage' in window) && ` test first because otherwise Firefox will
+ // throw bugzil.la/365772 if cookies are disabled
+
+ // Also in iOS5 Private Browsing mode, attempting to use localStorage.setItem
+ // will throw the exception:
+ // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
+ // Peculiarly, getItem and removeItem calls do not throw.
+
+ // Because we are forced to try/catch this, we'll go aggressive.
+
+ // Just FWIW: IE8 Compat mode supports these features completely:
+ // www.quirksmode.org/dom/html5.html
+ // But IE8 doesn't support either with local files
+
+ tests['localstorage'] = function() {
+ try {
+ localStorage.setItem(mod, mod);
+ localStorage.removeItem(mod);
+ return true;
+ } catch(e) {
+ return false;
+ }
+ };
+
+ tests['sessionstorage'] = function() {
+ try {
+ sessionStorage.setItem(mod, mod);
+ sessionStorage.removeItem(mod);
+ return true;
+ } catch(e) {
+ return false;
+ }
+ };
+
+
+ tests['webworkers'] = function() {
+ return !!window.Worker;
+ };
+
+
+ tests['applicationcache'] = function() {
+ return !!window.applicationCache;
+ };
+
+
+ // Thanks to Erik Dahlstrom
+ tests['svg'] = function() {
+ return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;
+ };
+
+ // specifically for SVG inline in HTML, not within XHTML
+ // test page: paulirish.com/demo/inline-svg
+ tests['inlinesvg'] = function() {
+ var div = document.createElement('div');
+ div.innerHTML = '';
+ return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;
+ };
+
+ // SVG SMIL animation
+ tests['smil'] = function() {
+ return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate')));
+ };
+
+ // This test is only for clip paths in SVG proper, not clip paths on HTML content
+ // demo: srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg
+
+ // However read the comments to dig into applying SVG clippaths to HTML content here:
+ // github.com/Modernizr/Modernizr/issues/213#issuecomment-1149491
+ tests['svgclippaths'] = function() {
+ return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath')));
+ };
+
+ /*>>webforms*/
+ // input features and input types go directly onto the ret object, bypassing the tests loop.
+ // Hold this guy to execute in a moment.
+ function webforms() {
+ /*>>input*/
+ // Run through HTML5's new input attributes to see if the UA understands any.
+ // We're using f which is the element created early on
+ // Mike Taylr has created a comprehensive resource for testing these attributes
+ // when applied to all input types:
+ // miketaylr.com/code/input-type-attr.html
+ // spec: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary
+
+ // Only input placeholder is tested while textarea's placeholder is not.
+ // Currently Safari 4 and Opera 11 have support only for the input placeholder
+ // Both tests are available in feature-detects/forms-placeholder.js
+ Modernizr['input'] = (function( props ) {
+ for ( var i = 0, len = props.length; i < len; i++ ) {
+ attrs[ props[i] ] = !!(props[i] in inputElem);
+ }
+ if (attrs.list){
+ // safari false positive's on datalist: webk.it/74252
+ // see also github.com/Modernizr/Modernizr/issues/146
+ attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement);
+ }
+ return attrs;
+ })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' '));
+ /*>>input*/
+
+ /*>>inputtypes*/
+ // Run through HTML5's new input types to see if the UA understands any.
+ // This is put behind the tests runloop because it doesn't return a
+ // true/false like all the other tests; instead, it returns an object
+ // containing each input type with its corresponding true/false value
+
+ // Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/
+ Modernizr['inputtypes'] = (function(props) {
+
+ for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) {
+
+ inputElem.setAttribute('type', inputElemType = props[i]);
+ bool = inputElem.type !== 'text';
+
+ // We first check to see if the type we give it sticks..
+ // If the type does, we feed it a textual value, which shouldn't be valid.
+ // If the value doesn't stick, we know there's input sanitization which infers a custom UI
+ if ( bool ) {
+
+ inputElem.value = smile;
+ inputElem.style.cssText = 'position:absolute;visibility:hidden;';
+
+ if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) {
+
+ docElement.appendChild(inputElem);
+ defaultView = document.defaultView;
+
+ // Safari 2-4 allows the smiley as a value, despite making a slider
+ bool = defaultView.getComputedStyle &&
+ defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' &&
+ // Mobile android web browser has false positive, so must
+ // check the height to see if the widget is actually there.
+ (inputElem.offsetHeight !== 0);
+
+ docElement.removeChild(inputElem);
+
+ } else if ( /^(search|tel)$/.test(inputElemType) ){
+ // Spec doesn't define any special parsing or detectable UI
+ // behaviors so we pass these through as true
+
+ // Interestingly, opera fails the earlier test, so it doesn't
+ // even make it here.
+
+ } else if ( /^(url|email)$/.test(inputElemType) ) {
+ // Real url and email support comes with prebaked validation.
+ bool = inputElem.checkValidity && inputElem.checkValidity() === false;
+
+ } else {
+ // If the upgraded input compontent rejects the :) text, we got a winner
+ bool = inputElem.value != smile;
+ }
+ }
+
+ inputs[ props[i] ] = !!bool;
+ }
+ return inputs;
+ })('search tel url email datetime date month week time datetime-local number range color'.split(' '));
+ /*>>inputtypes*/
+ }
+ /*>>webforms*/
+
+
+ // End of test definitions
+ // -----------------------
+
+
+
+ // Run through all tests and detect their support in the current UA.
+ // todo: hypothetically we could be doing an array of tests and use a basic loop here.
+ for ( var feature in tests ) {
+ if ( hasOwnProp(tests, feature) ) {
+ // run the test, throw the return value into the Modernizr,
+ // then based on that boolean, define an appropriate className
+ // and push it into an array of classes we'll join later.
+ featureName = feature.toLowerCase();
+ Modernizr[featureName] = tests[feature]();
+
+ classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
+ }
+ }
+
+ /*>>webforms*/
+ // input tests need to run.
+ Modernizr.input || webforms();
+ /*>>webforms*/
+
+
+ /**
+ * addTest allows the user to define their own feature tests
+ * the result will be added onto the Modernizr object,
+ * as well as an appropriate className set on the html element
+ *
+ * @param feature - String naming the feature
+ * @param test - Function returning true if feature is supported, false if not
+ */
+ Modernizr.addTest = function ( feature, test ) {
+ if ( typeof feature == 'object' ) {
+ for ( var key in feature ) {
+ if ( hasOwnProp( feature, key ) ) {
+ Modernizr.addTest( key, feature[ key ] );
+ }
+ }
+ } else {
+
+ feature = feature.toLowerCase();
+
+ if ( Modernizr[feature] !== undefined ) {
+ // we're going to quit if you're trying to overwrite an existing test
+ // if we were to allow it, we'd do this:
+ // var re = new RegExp("\\b(no-)?" + feature + "\\b");
+ // docElement.className = docElement.className.replace( re, '' );
+ // but, no rly, stuff 'em.
+ return Modernizr;
+ }
+
+ test = typeof test == 'function' ? test() : test;
+
+ if (typeof enableClasses !== "undefined" && enableClasses) {
+ docElement.className += ' ' + (test ? '' : 'no-') + feature;
+ }
+ Modernizr[feature] = test;
+
+ }
+
+ return Modernizr; // allow chaining.
+ };
+
+
+ // Reset modElem.cssText to nothing to reduce memory footprint.
+ setCss('');
+ modElem = inputElem = null;
+
+ /*>>shiv*/
+ /**
+ * @preserve HTML5 Shiv prev3.7.1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
+ */
+ ;(function(window, document) {
+ /*jshint evil:true */
+ /** version */
+ var version = '3.7.0';
+
+ /** Preset options */
+ var options = window.html5 || {};
+
+ /** Used to skip problem elements */
+ var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
+
+ /** Not all elements can be cloned in IE **/
+ var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
+
+ /** Detect whether the browser supports default html5 styles */
+ var supportsHtml5Styles;
+
+ /** Name of the expando, to work with multiple documents or to re-shiv one document */
+ var expando = '_html5shiv';
+
+ /** The id for the the documents expando */
+ var expanID = 0;
+
+ /** Cached data for each document */
+ var expandoData = {};
+
+ /** Detect whether the browser supports unknown elements */
+ var supportsUnknownElements;
+
+ (function() {
+ try {
+ var a = document.createElement('a');
+ a.innerHTML = '';
+ //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
+ supportsHtml5Styles = ('hidden' in a);
+
+ supportsUnknownElements = a.childNodes.length == 1 || (function() {
+ // assign a false positive if unable to shiv
+ (document.createElement)('a');
+ var frag = document.createDocumentFragment();
+ return (
+ typeof frag.cloneNode == 'undefined' ||
+ typeof frag.createDocumentFragment == 'undefined' ||
+ typeof frag.createElement == 'undefined'
+ );
+ }());
+ } catch(e) {
+ // assign a false positive if detection fails => unable to shiv
+ supportsHtml5Styles = true;
+ supportsUnknownElements = true;
+ }
+
+ }());
+
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * Creates a style sheet with the given CSS text and adds it to the document.
+ * @private
+ * @param {Document} ownerDocument The document.
+ * @param {String} cssText The CSS text.
+ * @returns {StyleSheet} The style element.
+ */
+ function addStyleSheet(ownerDocument, cssText) {
+ var p = ownerDocument.createElement('p'),
+ parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
+
+ p.innerHTML = 'x';
+ return parent.insertBefore(p.lastChild, parent.firstChild);
+ }
+
+ /**
+ * Returns the value of `html5.elements` as an array.
+ * @private
+ * @returns {Array} An array of shived element node names.
+ */
+ function getElements() {
+ var elements = html5.elements;
+ return typeof elements == 'string' ? elements.split(' ') : elements;
+ }
+
+ /**
+ * Returns the data associated to the given document
+ * @private
+ * @param {Document} ownerDocument The document.
+ * @returns {Object} An object of data.
+ */
+ function getExpandoData(ownerDocument) {
+ var data = expandoData[ownerDocument[expando]];
+ if (!data) {
+ data = {};
+ expanID++;
+ ownerDocument[expando] = expanID;
+ expandoData[expanID] = data;
+ }
+ return data;
+ }
+
+ /**
+ * returns a shived element for the given nodeName and document
+ * @memberOf html5
+ * @param {String} nodeName name of the element
+ * @param {Document} ownerDocument The context document.
+ * @returns {Object} The shived element.
+ */
+ function createElement(nodeName, ownerDocument, data){
+ if (!ownerDocument) {
+ ownerDocument = document;
+ }
+ if(supportsUnknownElements){
+ return ownerDocument.createElement(nodeName);
+ }
+ if (!data) {
+ data = getExpandoData(ownerDocument);
+ }
+ var node;
+
+ if (data.cache[nodeName]) {
+ node = data.cache[nodeName].cloneNode();
+ } else if (saveClones.test(nodeName)) {
+ node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
+ } else {
+ node = data.createElem(nodeName);
+ }
+
+ // Avoid adding some elements to fragments in IE < 9 because
+ // * Attributes like `name` or `type` cannot be set/changed once an element
+ // is inserted into a document/fragment
+ // * Link elements with `src` attributes that are inaccessible, as with
+ // a 403 response, will cause the tab/window to crash
+ // * Script elements appended to fragments will execute when their `src`
+ // or `text` property is set
+ return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
+ }
+
+ /**
+ * returns a shived DocumentFragment for the given document
+ * @memberOf html5
+ * @param {Document} ownerDocument The context document.
+ * @returns {Object} The shived DocumentFragment.
+ */
+ function createDocumentFragment(ownerDocument, data){
+ if (!ownerDocument) {
+ ownerDocument = document;
+ }
+ if(supportsUnknownElements){
+ return ownerDocument.createDocumentFragment();
+ }
+ data = data || getExpandoData(ownerDocument);
+ var clone = data.frag.cloneNode(),
+ i = 0,
+ elems = getElements(),
+ l = elems.length;
+ for(;i>shiv*/
+
+ // Assign private properties to the return object with prefix
+ Modernizr._version = version;
+
+ // expose these for the plugin API. Look in the source for how to join() them against your input
+ /*>>prefixes*/
+ Modernizr._prefixes = prefixes;
+ /*>>prefixes*/
+ /*>>domprefixes*/
+ Modernizr._domPrefixes = domPrefixes;
+ Modernizr._cssomPrefixes = cssomPrefixes;
+ /*>>domprefixes*/
+
+ /*>>mq*/
+ // Modernizr.mq tests a given media query, live against the current state of the window
+ // A few important notes:
+ // * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false
+ // * A max-width or orientation query will be evaluated against the current state, which may change later.
+ // * You must specify values. Eg. If you are testing support for the min-width media query use:
+ // Modernizr.mq('(min-width:0)')
+ // usage:
+ // Modernizr.mq('only screen and (max-width:768)')
+ Modernizr.mq = testMediaQuery;
+ /*>>mq*/
+
+ /*>>hasevent*/
+ // Modernizr.hasEvent() detects support for a given event, with an optional element to test on
+ // Modernizr.hasEvent('gesturestart', elem)
+ Modernizr.hasEvent = isEventSupported;
+ /*>>hasevent*/
+
+ /*>>testprop*/
+ // Modernizr.testProp() investigates whether a given style property is recognized
+ // Note that the property names must be provided in the camelCase variant.
+ // Modernizr.testProp('pointerEvents')
+ Modernizr.testProp = function(prop){
+ return testProps([prop]);
+ };
+ /*>>testprop*/
+
+ /*>>testallprops*/
+ // Modernizr.testAllProps() investigates whether a given style property,
+ // or any of its vendor-prefixed variants, is recognized
+ // Note that the property names must be provided in the camelCase variant.
+ // Modernizr.testAllProps('boxSizing')
+ Modernizr.testAllProps = testPropsAll;
+ /*>>testallprops*/
+
+
+ /*>>teststyles*/
+ // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards
+ // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... })
+ Modernizr.testStyles = injectElementWithStyles;
+ /*>>teststyles*/
+
+
+ /*>>prefixed*/
+ // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input
+ // Modernizr.prefixed('boxSizing') // 'MozBoxSizing'
+
+ // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style.
+ // Return values will also be the camelCase variant, if you need to translate that to hypenated style use:
+ //
+ // str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-');
+
+ // If you're trying to ascertain which transition end event to bind to, you might do something like...
+ //
+ // var transEndEventNames = {
+ // 'WebkitTransition' : 'webkitTransitionEnd',
+ // 'MozTransition' : 'transitionend',
+ // 'OTransition' : 'oTransitionEnd',
+ // 'msTransition' : 'MSTransitionEnd',
+ // 'transition' : 'transitionend'
+ // },
+ // transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];
+
+ Modernizr.prefixed = function(prop, obj, elem){
+ if(!obj) {
+ return testPropsAll(prop, 'pfx');
+ } else {
+ // Testing DOM property e.g. Modernizr.prefixed('requestAnimationFrame', window) // 'mozRequestAnimationFrame'
+ return testPropsAll(prop, obj, elem);
+ }
+ };
+ /*>>prefixed*/
+
+
+ /*>>cssclasses*/
+ // Remove "no-js" class from element, if it exists:
+ docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') +
+
+ // Add the new classes to the element.
+ (enableClasses ? ' js ' + classes.join(' ') : '');
+ /*>>cssclasses*/
+
+ return Modernizr;
+
+})(this, this.document);
+!function(a){var b=function(){window.asyncWebshims||(window.asyncWebshims={cfg:[],ready:[]})},c=function(){window.jQuery&&(a(jQuery),a=function(){return window.webshims})};window.webshims={setOptions:function(){b(),window.asyncWebshims.cfg.push(arguments)},ready:function(){b(),window.asyncWebshims.ready.push(arguments)},activeLang:function(a){b(),window.asyncWebshims.lang=a},polyfill:function(a){b(),window.asyncWebshims.polyfill=a},_curScript:function(){var a,b,c,d,e,f=document.currentScript;if(!f){try{throw new Error("")}catch(g){for(c=(g.sourceURL||g.stack||"").split("\n"),e=/(?:fil|htt|wid|abo|app|res)(.)+/i,b=0;b1?q[b]=a.isPlainObject(c)?a.extend(!0,q[b]||{},c):c:"object"==typeof b&&a.extend(!0,q,b)},_getAutoEnhance:n,addPolyfill:function(b,c){c=c||{};var d=c.f||b;r[d]||(r[d]=[],f.featureList.push(d),q[d]={}),r[d].push(b),c.options=a.extend(q[d],c.options),y(b,c),c.methodNames&&a.each(c.methodNames,function(a,b){f.addMethodName(b)})},polyfill:function(){return function(a){a||(a=f.featureList),"string"==typeof a&&(a=a.split(" "));return f._polyfill(a)}}(),_polyfill:function(b){var d,e,f=[];c.run||(d=-1!==a.inArray("forms-ext",b),c(),e=d&&!v["form-number-date-ui"].test()||!p&&-1!==a.inArray("mediacapture",b),d&&-1==a.inArray("forms",b)&&b.push("forms"),q.loadStyles&&w.loadCSS("styles/shim"+(e?"-ext":"")+".css")),q.waitReady&&(a.readyWait++,t(b,function(){a.ready(!0)})),a.each(b,function(a,b){return b=o[b]||b,r[b]?(b!==r[b][0]&&t(r[b],function(){s(b,!0)}),void(f=f.concat(r[b]))):void s(b,!0)}),x(f),a.each(b,function(a,b){var c=q[b];c&&("mediaelement"==b&&(c.replaceUI=n(c.replaceUI))&&c.plugins.unshift("mediacontrols"),c.plugins&&c.plugins.length&&x(q[b].plugins))})},reTest:function(){var b,c=function(c,d){var e=v[d],f=d+"Ready";!e||e.loaded||(e.test&&a.isFunction(e.test)?e.test([]):e.test)||(h[f]&&delete h[f],r[e.f],b.push(d))};return function(d){"string"==typeof d&&(d=d.split(" ")),b=[],a.each(d,c),x(b)}}(),isReady:function(b,c){if(b+="Ready",c){if(h[b]&&h[b].add)return!0;h[b]=a.extend(h[b]||{},{add:function(a){a.handler.call(this,b)}}),a(document).triggerHandler(b)}return!(!h[b]||!h[b].add)||!1},ready:function(b,c){var d=arguments[2];if("string"==typeof b&&(b=b.split(" ")),d||(b=a.map(a.grep(b,function(a){return!s(a)}),function(a){return a+"Ready"})),!b.length)return void c(a,f,window,document);var e=b.shift(),g=function(){t(b,c,!0)};a(document).one(e,g)},capturingEvents:function(b,c){document.addEventListener&&("string"==typeof b&&(b=[b]),a.each(b,function(b,d){var e=function(b){return b=a.event.fix(b),c&&f.capturingEventPrevented&&f.capturingEventPrevented(b),a.event.dispatch.call(this,b)};h[d]=h[d]||{},h[d].setup||h[d].teardown||a.extend(h[d],{setup:function(){this.addEventListener(d,e,!0)},teardown:function(){this.removeEventListener(d,e,!0)}})}))},register:function(b,c){var d=v[b];if(!d)return void f.error("can't find module: "+b);d.loaded=!0;var e=function(){c(a,f,window,document,void 0,d.options),s(b,!0)};d.d&&d.d.length?t(d.d,e):e()},c:{},loader:{addModule:function(b,c){v[b]=c,c.name=c.name||b,c.c||(c.c=[]),a.each(c.c,function(a,c){f.c[c]||(f.c[c]=[]),f.c[c].push(b)})},loadList:function(){var b=[],c=function(c,d){"string"==typeof d&&(d=[d]),a.merge(b,d),w.loadScript(c,!1,d)},d=function(c,d){if(s(c)||-1!=a.inArray(c,b))return!0;var e,f=v[c];return f?(e=f.test&&a.isFunction(f.test)?f.test(d):f.test,e?(s(c,!0),!0):!1):!0},e=function(b,c){if(b.d&&b.d.length){var e=function(b,e){d(e,c)||-1!=a.inArray(e,c)||c.push(e)};a.each(b.d,function(b,c){v[c]?v[c].loaded||e(b,c):r[c]&&(a.each(r[c],e),t(r[c],function(){s(c,!0)}))}),b.noAutoCallback||(b.noAutoCallback=!0)}};return function(g){var h,i,j,k,l=[],m=function(d,e){return k=e,a.each(f.c[e],function(c,d){return-1==a.inArray(d,l)||-1!=a.inArray(d,b)?(k=!1,!1):void 0}),k?(c("combos/"+k,f.c[k]),!1):void 0};for(i=0;ii;i++)k=!1,h=l[i],-1==a.inArray(h,b)&&("noCombo"!=q.debug&&a.each(v[h].c,m),k||c(v[h].src||h,h))}}(),makePath:function(a){return-1!=a.indexOf("//")||0===a.indexOf("/")?a:(-1==a.indexOf(".")&&(a+=".js"),q.addCacheBuster&&(a+=q.addCacheBuster),q.basePath+a)},loadCSS:function(){var b,c={};return function(d){d=this.makePath(d),c[d]||(b=b||a("link, style")[0]||a("script")[0],c[d]=1,a('').insertBefore(b).attr({href:d}))}}(),loadScript:function(){var b={};return function(c,d,e,f){if(f||(c=w.makePath(c)),!b[c]){var g=function(){d&&d(),e&&("string"==typeof e&&(e=e.split(" ")),a.each(e,function(a,b){v[b]&&(v[b].afterLoad&&v[b].afterLoad(),s(v[b].noAutoCallback?b+"FileLoaded":b,!0))}))};b[c]=1,q.loadScript(c,g,a.noop)}}}()}});var q=f.cfg,r=f.features,s=f.isReady,t=f.ready,u=f.addPolyfill,v=f.modules,w=f.loader,x=w.loadList,y=w.addModule,z=f.bugs,A=[],B={warn:1,error:1},C=a.fn,D=b("video");f.addMethodName=function(a){a=a.split(":");var b=a[1];1==a.length?(b=a[0],a=a[0]):a=a[0],C[a]=function(){return this.callProp(b,arguments)}},C.callProp=function(b,c){var d;return c||(c=[]),this.each(function(){var e=a.prop(this,b);if(e&&e.apply){if(d=e.apply(this,c),void 0!==d)return!1}else f.warn(b+" is not a method of "+this)}),void 0!==d?d:this},f.activeLang=function(){"language"in e||(e.language=e.browserLanguage||"");var b=a.attr(document.documentElement,"lang")||e.language;return t("webshimLocalization",function(){f.activeLang(b)}),function(a){if(a)if("string"==typeof a)b=a;else if("object"==typeof a){var c=arguments,d=this;t("webshimLocalization",function(){f.activeLang.apply(d,c)})}return b}}(),f.errorLog=[],a.each(["log","error","warn","info"],function(a,b){f[b]=function(a){(B[b]&&q.debug!==!1||q.debug)&&(f.errorLog.push(a),window.console&&console.log&&console[console[b]?b:"log"](a))}}),function(){a.isDOMReady=a.isReady;var b=function(){a.isDOMReady=!0,s("DOM",!0),setTimeout(function(){s("WINDOWLOAD",!0)},9999)};c=function(){if(!c.run){if(!a.isDOMReady&&q.waitReady){var d=a.ready;a.ready=function(a){return a!==!0&&document.body&&b(),d.apply(this,arguments)},a.ready.promise=d.promise}q.readyEvt?a(document).one(q.readyEvt,b):a(b)}c.run=!0},a(window).on("load",function(){b(),setTimeout(function(){s("WINDOWLOAD",!0)},9)});var d=[],e=function(){1==this.nodeType&&f.triggerDomUpdate(this)};a.extend(f,{addReady:function(a){var b=function(b,c){f.ready("DOM",function(){a(b,c)})};d.push(b),q.wsdoc&&b(q.wsdoc,i)},triggerDomUpdate:function(b){if(!b||!b.nodeType)return void(b&&b.jquery&&b.each(function(){f.triggerDomUpdate(this)}));var c=b.nodeType;if(1==c||9==c){var e=b!==document?a(b):i;a.each(d,function(a,c){c(b,e)})}}}),C.clonePolyfill=C.clone,C.htmlPolyfill=function(b){if(!arguments.length)return a(this.clonePolyfill()).html();var c=C.html.call(this,b);return c===this&&a.isDOMReady&&this.each(e),c},C.jProp=function(){return this.pushStack(a(C.prop.apply(this,arguments)||[]))},a.each(["after","before","append","prepend","replaceWith"],function(b,c){C[c+"Polyfill"]=function(b){return b=a(b),C[c].call(this,b),a.isDOMReady&&b.each(e),this}}),a.each(["insertAfter","insertBefore","appendTo","prependTo","replaceAll"],function(b,c){C[c.replace(/[A-Z]/,function(a){return"Polyfill"+a})]=function(){return C[c].apply(this,arguments),a.isDOMReady&&f.triggerDomUpdate(this),this}}),C.updatePolyfill=function(){return a.isDOMReady&&f.triggerDomUpdate(this),this},a.each(["getNativeElement","getShadowElement","getShadowFocusElement"],function(a,b){C[b]=function(){return this.pushStack(this)}})}(),l.create&&(f.objectCreate=function(b,c,d){var e=l.create(b);return d&&(e.options=a.extend(!0,{},e.options||{},d),d=e.options),e._create&&a.isFunction(e._create)&&e._create(d),e}),y("swfmini",{test:function(){return window.swfobject&&!window.swfmini&&(window.swfmini=window.swfobject),"swfmini"in window},c:[16,7,2,8,1,12,23]}),v.swfmini.test(),y("sizzle",{test:a.expr.filters}),u("es5",{test:!(!k.ES5||!Function.prototype.bind),d:["sizzle"]}),u("dom-extend",{f:g,noAutoCallback:!0,d:["es5"],c:[16,7,2,15,30,3,8,4,9,10,25,31,34]}),b("picture"),u("picture",{test:"picturefill"in window||!!window.HTMLPictureElement||"respimage"in window,d:["matchMedia"],c:[18],loadInit:function(){s("picture",!0)}}),u("matchMedia",{test:!(!window.matchMedia||!matchMedia("all").addListener),c:[18]}),u("sticky",{test:-1!=(a(b("b")).attr("style","position: -webkit-sticky; position: sticky").css("position")||"").indexOf("sticky"),d:["es5","matchMedia"]}),u("es6",{test:!!(Math.imul&&Number.MIN_SAFE_INTEGER&&l.is&&window.Promise&&Promise.all),d:["es5"]}),u("geolocation",{test:"geolocation"in e,options:{destroyWrite:!0},c:[21]}),function(){u("canvas",{src:"excanvas",test:"getContext"in b("canvas"),options:{type:"flash"},noAutoCallback:!0,loadInit:function(){var a=this.options.type;!a||-1===a.indexOf("flash")||v.swfmini.test()&&!swfmini.hasFlashPlayerVersion("9.0.0")||(this.src="flash"==a?"FlashCanvas/flashcanvas":"FlashCanvasPro/flashcanvas")},methodNames:["getContext"],d:[g]})}();var E="getUserMedia"in e;u("usermedia-core",{f:"usermedia",test:E&&window.URL,d:["url",g]}),u("usermedia-shim",{f:"usermedia",test:!!(E||e.webkitGetUserMedia||e.mozGetUserMedia||e.msGetUserMedia),d:["url","mediaelement",g]}),u("mediacapture",{test:p,d:["swfmini","usermedia",g,"filereader","forms","canvas"]}),function(){var c,d,h="form-shim-extend",i="formvalidation",j="form-number-date-api",l=!1,m=!1,o=!1,p={},r=b("progress"),s=b("output"),t=function(){var d,f,g="1(",j=b("input");if(f=a('')[0],k.inputtypes=p,a.each(["range","date","datetime-local","month","color","number"],function(a,b){j.setAttribute("type",b),p[b]=j.type==b&&(j.value=g)&&j.value!=g}),k.datalist=!!("options"in b("datalist")&&window.HTMLDataListElement),k[i]="checkValidity"in j,k.fieldsetelements="elements"in f,k.fieldsetdisabled="disabled"in f){try{f.querySelector(":invalid")&&(f.disabled=!0,d=!f.querySelector(":invalid")&&f.querySelector(":disabled"))}catch(n){}k.fieldsetdisabled=!!d}if(k[i]&&(m=!(k.fieldsetdisabled&&k.fieldsetelements&&"value"in r&&"value"in s),o=m&&/Android/i.test(e.userAgent),l=window.opera||z.bustedValidity||m||!k.datalist,!l&&p.number)){l=!0;try{j.type="number",j.value="",j.stepUp(),l="1"!=j.value}catch(q){}}return z.bustedValidity=l,c=k[i]&&!l?"form-native-extend":h,t=a.noop,!1},w=function(b){var c=!0;return b._types||(b._types=b.types.split(" ")),a.each(b._types,function(a,b){return b in p&&!p[b]?(c=!1,!1):void 0}),c};f.validationMessages=f.validityMessages={langSrc:"i18n/formcfg-",availableLangs:"ar bg ca cs el es fa fi fr he hi hu it ja lt nl no pl pt pt-BR pt-PT ru sv zh-CN zh-TW".split(" ")},f.formcfg=a.extend({},f.validationMessages),f.inputTypes={},u("form-core",{f:"forms",test:t,d:["es5"],options:{placeholderType:"value",messagePopover:{},list:{popover:{constrainWidth:!0}},iVal:{sel:".ws-validate",handleBubble:"hide",recheckDelay:400}},methodNames:["setCustomValidity","checkValidity","setSelectionRange"],c:[16,7,2,8,1,15,30,3,31]}),d=q.forms,u("form-native-extend",{f:"forms",test:function(b){return t(),!k[i]||l||-1==a.inArray(j,b||[])||v[j].test()},d:["form-core",g,"form-message"],c:[6,5,14,29]}),u(h,{f:"forms",test:function(){return t(),k[i]&&!l},d:["form-core",g,"sizzle"],c:[16,15,28]}),u(h+"2",{f:"forms",test:function(){return t(),k[i]&&!m},d:[h],c:[27]}),u("form-message",{f:"forms",test:function(a){return t(),!(d.customMessages||!k[i]||l||!v[c].test(a))},d:[g],c:[16,7,15,30,3,8,4,14,28]}),u(j,{f:"forms-ext",options:{types:"date time range number"},test:function(){t();var a=!l;return a&&(a=w(this.options)),a},methodNames:["stepUp","stepDown"],d:["forms",g],c:[6,5,17,14,28,29,33]}),y("range-ui",{options:{},noAutoCallback:!0,test:function(){return!!C.rangeUI},d:["es5"],c:[6,5,9,10,17,11]}),u("form-number-date-ui",{f:"forms-ext",test:function(){var a=this.options;return a.replaceUI=n(a.replaceUI),t(),!a.replaceUI&&o&&(a.replaceUI=!0),!a.replaceUI&&w(a)},d:["forms",g,j,"range-ui"],options:{widgets:{calculateWidth:!0,animate:!0}},c:[6,5,9,10,17,11]}),u("form-datalist",{f:"forms",test:function(){return t(),o&&(d.customDatalist=!0),k.datalist&&!d.fD},d:["form-core",g],c:[16,7,6,2,9,15,30,31,28,33]})}();var F="FileReader"in window&&"FormData"in window;return u("filereader-xhr",{f:"filereader",test:F,d:[g,"swfmini"],c:[25,27]}),u("canvas-blob",{f:"filereader",methodNames:["toBlob"],test:!(F&&!b("canvas").toBlob)}),u("details",{test:"open"in b("details"),d:[g],options:{text:"Details"},c:[21,22]}),u("url",{test:function(){var a=!1;try{a=new URL("b","http://a"),a=!(!a.searchParams||"http://a/b"!=a.href)}catch(b){}return a},d:["es5"]}),function(){f.mediaelement={};var c=b("track");if(k.mediaelement="canPlayType"in D,k.texttrackapi="addTextTrack"in D,k.track="kind"in c,b("audio"),!(z.track=!k.texttrackapi))try{z.track=!("oncuechange"in D.addTextTrack("metadata"))}catch(d){}u("mediaelement-core",{f:"mediaelement",noAutoCallback:!0,options:{jme:{},plugins:[],vars:{},params:{},attrs:{},changeSWF:a.noop},methodNames:["play","pause","canPlayType","mediaLoad:load"],d:["swfmini"],c:[16,7,2,8,1,12,13,23]}),u("mediaelement-jaris",{f:"mediaelement",d:["mediaelement-core",g],test:function(){var a=this.options;return!k.mediaelement||f.mediaelement.loadSwf?!1:(a.preferFlash&&!v.swfmini.test()&&(a.preferFlash=!1),!(a.preferFlash&&swfmini.hasFlashPlayerVersion("11.3")))},c:[21,25]}),u("track",{options:{positionDisplay:!0,override:z.track},test:function(){var a=this.options;return a.override=n(a.override),!a.override&&!z.track},d:["mediaelement",g],methodNames:["addTextTrack"],c:[21,12,13,22,34]}),y("jmebase",{src:"jme/base",c:[98,99,97]}),a.each([["mediacontrols",{c:[98,99],css:"jme/controls.css"}],["playlist",{c:[98,97]}],["alternate-media"]],function(b,c){y(c[0],a.extend({src:"jme/"+c[0],d:["jmebase"]},c[1]))}),y("track-ui",{d:["track",g]})}(),u("feature-dummy",{test:!0,loaded:!0,c:A}),f.$=a,a.webshims=f,a.webshim=webshim,f.callAsync=function(){f.callAsync=a.noop,j&&(j.cfg&&(j.cfg.length||(j.cfg=[[j.cfg]]),a.each(j.cfg,function(a,b){f.setOptions.apply(f,b)})),j.ready&&a.each(j.ready,function(a,b){f.ready.apply(f,b)}),j.lang&&f.activeLang(j.lang),"polyfill"in j&&f.polyfill(j.polyfill)),f.isReady("jquery",!0)},f.callAsync(),f});
+/*
+Copyright 2012 Igor Vaynberg
+
+Version: 3.5.4 Timestamp: Sun Aug 30 13:30:32 EDT 2015
+
+This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
+General Public License version 2 (the "GPL License"). You may choose either license to govern your
+use of this software only upon the condition that you accept all of the terms of either the Apache
+License or the GPL License.
+
+You may obtain a copy of the Apache License and the GPL License at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+ http://www.gnu.org/licenses/gpl-2.0.html
+
+Unless required by applicable law or agreed to in writing, software distributed under the
+Apache License or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for
+the specific language governing permissions and limitations under the Apache License and the GPL License.
+*/
+
+(function ($) {
+ if(typeof $.fn.each2 == "undefined") {
+ $.extend($.fn, {
+ /*
+ * 4-10 times faster .each replacement
+ * use it carefully, as it overrides jQuery context of element on each iteration
+ */
+ each2 : function (c) {
+ var j = $([0]), i = -1, l = this.length;
+ while (
+ ++i < l
+ && (j.context = j[0] = this[i])
+ && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
+ );
+ return this;
+ }
+ });
+ }
+})(jQuery);
+
+(function ($, undefined) {
+ "use strict";
+ /*global document, window, jQuery, console */
+
+ if (window.Select2 !== undefined) {
+ return;
+ }
+
+ var AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
+ lastMousePosition={x:0,y:0}, $document, scrollBarDimensions,
+
+ KEY = {
+ TAB: 9,
+ ENTER: 13,
+ ESC: 27,
+ SPACE: 32,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ SHIFT: 16,
+ CTRL: 17,
+ ALT: 18,
+ PAGE_UP: 33,
+ PAGE_DOWN: 34,
+ HOME: 36,
+ END: 35,
+ BACKSPACE: 8,
+ DELETE: 46,
+ isArrow: function (k) {
+ k = k.which ? k.which : k;
+ switch (k) {
+ case KEY.LEFT:
+ case KEY.RIGHT:
+ case KEY.UP:
+ case KEY.DOWN:
+ return true;
+ }
+ return false;
+ },
+ isControl: function (e) {
+ var k = e.which;
+ switch (k) {
+ case KEY.SHIFT:
+ case KEY.CTRL:
+ case KEY.ALT:
+ return true;
+ }
+
+ if (e.metaKey) return true;
+
+ return false;
+ },
+ isFunctionKey: function (k) {
+ k = k.which ? k.which : k;
+ return k >= 112 && k <= 123;
+ }
+ },
+ MEASURE_SCROLLBAR_TEMPLATE = "",
+
+ DIACRITICS = {"\u24B6":"A","\uFF21":"A","\u00C0":"A","\u00C1":"A","\u00C2":"A","\u1EA6":"A","\u1EA4":"A","\u1EAA":"A","\u1EA8":"A","\u00C3":"A","\u0100":"A","\u0102":"A","\u1EB0":"A","\u1EAE":"A","\u1EB4":"A","\u1EB2":"A","\u0226":"A","\u01E0":"A","\u00C4":"A","\u01DE":"A","\u1EA2":"A","\u00C5":"A","\u01FA":"A","\u01CD":"A","\u0200":"A","\u0202":"A","\u1EA0":"A","\u1EAC":"A","\u1EB6":"A","\u1E00":"A","\u0104":"A","\u023A":"A","\u2C6F":"A","\uA732":"AA","\u00C6":"AE","\u01FC":"AE","\u01E2":"AE","\uA734":"AO","\uA736":"AU","\uA738":"AV","\uA73A":"AV","\uA73C":"AY","\u24B7":"B","\uFF22":"B","\u1E02":"B","\u1E04":"B","\u1E06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24B8":"C","\uFF23":"C","\u0106":"C","\u0108":"C","\u010A":"C","\u010C":"C","\u00C7":"C","\u1E08":"C","\u0187":"C","\u023B":"C","\uA73E":"C","\u24B9":"D","\uFF24":"D","\u1E0A":"D","\u010E":"D","\u1E0C":"D","\u1E10":"D","\u1E12":"D","\u1E0E":"D","\u0110":"D","\u018B":"D","\u018A":"D","\u0189":"D","\uA779":"D","\u01F1":"DZ","\u01C4":"DZ","\u01F2":"Dz","\u01C5":"Dz","\u24BA":"E","\uFF25":"E","\u00C8":"E","\u00C9":"E","\u00CA":"E","\u1EC0":"E","\u1EBE":"E","\u1EC4":"E","\u1EC2":"E","\u1EBC":"E","\u0112":"E","\u1E14":"E","\u1E16":"E","\u0114":"E","\u0116":"E","\u00CB":"E","\u1EBA":"E","\u011A":"E","\u0204":"E","\u0206":"E","\u1EB8":"E","\u1EC6":"E","\u0228":"E","\u1E1C":"E","\u0118":"E","\u1E18":"E","\u1E1A":"E","\u0190":"E","\u018E":"E","\u24BB":"F","\uFF26":"F","\u1E1E":"F","\u0191":"F","\uA77B":"F","\u24BC":"G","\uFF27":"G","\u01F4":"G","\u011C":"G","\u1E20":"G","\u011E":"G","\u0120":"G","\u01E6":"G","\u0122":"G","\u01E4":"G","\u0193":"G","\uA7A0":"G","\uA77D":"G","\uA77E":"G","\u24BD":"H","\uFF28":"H","\u0124":"H","\u1E22":"H","\u1E26":"H","\u021E":"H","\u1E24":"H","\u1E28":"H","\u1E2A":"H","\u0126":"H","\u2C67":"H","\u2C75":"H","\uA78D":"H","\u24BE":"I","\uFF29":"I","\u00CC":"I","\u00CD":"I","\u00CE":"I","\u0128":"I","\u012A":"I","\u012C":"I","\u0130":"I","\u00CF":"I","\u1E2E":"I","\u1EC8":"I","\u01CF":"I","\u0208":"I","\u020A":"I","\u1ECA":"I","\u012E":"I","\u1E2C":"I","\u0197":"I","\u24BF":"J","\uFF2A":"J","\u0134":"J","\u0248":"J","\u24C0":"K","\uFF2B":"K","\u1E30":"K","\u01E8":"K","\u1E32":"K","\u0136":"K","\u1E34":"K","\u0198":"K","\u2C69":"K","\uA740":"K","\uA742":"K","\uA744":"K","\uA7A2":"K","\u24C1":"L","\uFF2C":"L","\u013F":"L","\u0139":"L","\u013D":"L","\u1E36":"L","\u1E38":"L","\u013B":"L","\u1E3C":"L","\u1E3A":"L","\u0141":"L","\u023D":"L","\u2C62":"L","\u2C60":"L","\uA748":"L","\uA746":"L","\uA780":"L","\u01C7":"LJ","\u01C8":"Lj","\u24C2":"M","\uFF2D":"M","\u1E3E":"M","\u1E40":"M","\u1E42":"M","\u2C6E":"M","\u019C":"M","\u24C3":"N","\uFF2E":"N","\u01F8":"N","\u0143":"N","\u00D1":"N","\u1E44":"N","\u0147":"N","\u1E46":"N","\u0145":"N","\u1E4A":"N","\u1E48":"N","\u0220":"N","\u019D":"N","\uA790":"N","\uA7A4":"N","\u01CA":"NJ","\u01CB":"Nj","\u24C4":"O","\uFF2F":"O","\u00D2":"O","\u00D3":"O","\u00D4":"O","\u1ED2":"O","\u1ED0":"O","\u1ED6":"O","\u1ED4":"O","\u00D5":"O","\u1E4C":"O","\u022C":"O","\u1E4E":"O","\u014C":"O","\u1E50":"O","\u1E52":"O","\u014E":"O","\u022E":"O","\u0230":"O","\u00D6":"O","\u022A":"O","\u1ECE":"O","\u0150":"O","\u01D1":"O","\u020C":"O","\u020E":"O","\u01A0":"O","\u1EDC":"O","\u1EDA":"O","\u1EE0":"O","\u1EDE":"O","\u1EE2":"O","\u1ECC":"O","\u1ED8":"O","\u01EA":"O","\u01EC":"O","\u00D8":"O","\u01FE":"O","\u0186":"O","\u019F":"O","\uA74A":"O","\uA74C":"O","\u01A2":"OI","\uA74E":"OO","\u0222":"OU","\u24C5":"P","\uFF30":"P","\u1E54":"P","\u1E56":"P","\u01A4":"P","\u2C63":"P","\uA750":"P","\uA752":"P","\uA754":"P","\u24C6":"Q","\uFF31":"Q","\uA756":"Q","\uA758":"Q","\u024A":"Q","\u24C7":"R","\uFF32":"R","\u0154":"R","\u1E58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1E5A":"R","\u1E5C":"R","\u0156":"R","\u1E5E":"R","\u024C":"R","\u2C64":"R","\uA75A":"R","\uA7A6":"R","\uA782":"R","\u24C8":"S","\uFF33":"S","\u1E9E":"S","\u015A":"S","\u1E64":"S","\u015C":"S","\u1E60":"S","\u0160":"S","\u1E66":"S","\u1E62":"S","\u1E68":"S","\u0218":"S","\u015E":"S","\u2C7E":"S","\uA7A8":"S","\uA784":"S","\u24C9":"T","\uFF34":"T","\u1E6A":"T","\u0164":"T","\u1E6C":"T","\u021A":"T","\u0162":"T","\u1E70":"T","\u1E6E":"T","\u0166":"T","\u01AC":"T","\u01AE":"T","\u023E":"T","\uA786":"T","\uA728":"TZ","\u24CA":"U","\uFF35":"U","\u00D9":"U","\u00DA":"U","\u00DB":"U","\u0168":"U","\u1E78":"U","\u016A":"U","\u1E7A":"U","\u016C":"U","\u00DC":"U","\u01DB":"U","\u01D7":"U","\u01D5":"U","\u01D9":"U","\u1EE6":"U","\u016E":"U","\u0170":"U","\u01D3":"U","\u0214":"U","\u0216":"U","\u01AF":"U","\u1EEA":"U","\u1EE8":"U","\u1EEE":"U","\u1EEC":"U","\u1EF0":"U","\u1EE4":"U","\u1E72":"U","\u0172":"U","\u1E76":"U","\u1E74":"U","\u0244":"U","\u24CB":"V","\uFF36":"V","\u1E7C":"V","\u1E7E":"V","\u01B2":"V","\uA75E":"V","\u0245":"V","\uA760":"VY","\u24CC":"W","\uFF37":"W","\u1E80":"W","\u1E82":"W","\u0174":"W","\u1E86":"W","\u1E84":"W","\u1E88":"W","\u2C72":"W","\u24CD":"X","\uFF38":"X","\u1E8A":"X","\u1E8C":"X","\u24CE":"Y","\uFF39":"Y","\u1EF2":"Y","\u00DD":"Y","\u0176":"Y","\u1EF8":"Y","\u0232":"Y","\u1E8E":"Y","\u0178":"Y","\u1EF6":"Y","\u1EF4":"Y","\u01B3":"Y","\u024E":"Y","\u1EFE":"Y","\u24CF":"Z","\uFF3A":"Z","\u0179":"Z","\u1E90":"Z","\u017B":"Z","\u017D":"Z","\u1E92":"Z","\u1E94":"Z","\u01B5":"Z","\u0224":"Z","\u2C7F":"Z","\u2C6B":"Z","\uA762":"Z","\u24D0":"a","\uFF41":"a","\u1E9A":"a","\u00E0":"a","\u00E1":"a","\u00E2":"a","\u1EA7":"a","\u1EA5":"a","\u1EAB":"a","\u1EA9":"a","\u00E3":"a","\u0101":"a","\u0103":"a","\u1EB1":"a","\u1EAF":"a","\u1EB5":"a","\u1EB3":"a","\u0227":"a","\u01E1":"a","\u00E4":"a","\u01DF":"a","\u1EA3":"a","\u00E5":"a","\u01FB":"a","\u01CE":"a","\u0201":"a","\u0203":"a","\u1EA1":"a","\u1EAD":"a","\u1EB7":"a","\u1E01":"a","\u0105":"a","\u2C65":"a","\u0250":"a","\uA733":"aa","\u00E6":"ae","\u01FD":"ae","\u01E3":"ae","\uA735":"ao","\uA737":"au","\uA739":"av","\uA73B":"av","\uA73D":"ay","\u24D1":"b","\uFF42":"b","\u1E03":"b","\u1E05":"b","\u1E07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24D2":"c","\uFF43":"c","\u0107":"c","\u0109":"c","\u010B":"c","\u010D":"c","\u00E7":"c","\u1E09":"c","\u0188":"c","\u023C":"c","\uA73F":"c","\u2184":"c","\u24D3":"d","\uFF44":"d","\u1E0B":"d","\u010F":"d","\u1E0D":"d","\u1E11":"d","\u1E13":"d","\u1E0F":"d","\u0111":"d","\u018C":"d","\u0256":"d","\u0257":"d","\uA77A":"d","\u01F3":"dz","\u01C6":"dz","\u24D4":"e","\uFF45":"e","\u00E8":"e","\u00E9":"e","\u00EA":"e","\u1EC1":"e","\u1EBF":"e","\u1EC5":"e","\u1EC3":"e","\u1EBD":"e","\u0113":"e","\u1E15":"e","\u1E17":"e","\u0115":"e","\u0117":"e","\u00EB":"e","\u1EBB":"e","\u011B":"e","\u0205":"e","\u0207":"e","\u1EB9":"e","\u1EC7":"e","\u0229":"e","\u1E1D":"e","\u0119":"e","\u1E19":"e","\u1E1B":"e","\u0247":"e","\u025B":"e","\u01DD":"e","\u24D5":"f","\uFF46":"f","\u1E1F":"f","\u0192":"f","\uA77C":"f","\u24D6":"g","\uFF47":"g","\u01F5":"g","\u011D":"g","\u1E21":"g","\u011F":"g","\u0121":"g","\u01E7":"g","\u0123":"g","\u01E5":"g","\u0260":"g","\uA7A1":"g","\u1D79":"g","\uA77F":"g","\u24D7":"h","\uFF48":"h","\u0125":"h","\u1E23":"h","\u1E27":"h","\u021F":"h","\u1E25":"h","\u1E29":"h","\u1E2B":"h","\u1E96":"h","\u0127":"h","\u2C68":"h","\u2C76":"h","\u0265":"h","\u0195":"hv","\u24D8":"i","\uFF49":"i","\u00EC":"i","\u00ED":"i","\u00EE":"i","\u0129":"i","\u012B":"i","\u012D":"i","\u00EF":"i","\u1E2F":"i","\u1EC9":"i","\u01D0":"i","\u0209":"i","\u020B":"i","\u1ECB":"i","\u012F":"i","\u1E2D":"i","\u0268":"i","\u0131":"i","\u24D9":"j","\uFF4A":"j","\u0135":"j","\u01F0":"j","\u0249":"j","\u24DA":"k","\uFF4B":"k","\u1E31":"k","\u01E9":"k","\u1E33":"k","\u0137":"k","\u1E35":"k","\u0199":"k","\u2C6A":"k","\uA741":"k","\uA743":"k","\uA745":"k","\uA7A3":"k","\u24DB":"l","\uFF4C":"l","\u0140":"l","\u013A":"l","\u013E":"l","\u1E37":"l","\u1E39":"l","\u013C":"l","\u1E3D":"l","\u1E3B":"l","\u017F":"l","\u0142":"l","\u019A":"l","\u026B":"l","\u2C61":"l","\uA749":"l","\uA781":"l","\uA747":"l","\u01C9":"lj","\u24DC":"m","\uFF4D":"m","\u1E3F":"m","\u1E41":"m","\u1E43":"m","\u0271":"m","\u026F":"m","\u24DD":"n","\uFF4E":"n","\u01F9":"n","\u0144":"n","\u00F1":"n","\u1E45":"n","\u0148":"n","\u1E47":"n","\u0146":"n","\u1E4B":"n","\u1E49":"n","\u019E":"n","\u0272":"n","\u0149":"n","\uA791":"n","\uA7A5":"n","\u01CC":"nj","\u24DE":"o","\uFF4F":"o","\u00F2":"o","\u00F3":"o","\u00F4":"o","\u1ED3":"o","\u1ED1":"o","\u1ED7":"o","\u1ED5":"o","\u00F5":"o","\u1E4D":"o","\u022D":"o","\u1E4F":"o","\u014D":"o","\u1E51":"o","\u1E53":"o","\u014F":"o","\u022F":"o","\u0231":"o","\u00F6":"o","\u022B":"o","\u1ECF":"o","\u0151":"o","\u01D2":"o","\u020D":"o","\u020F":"o","\u01A1":"o","\u1EDD":"o","\u1EDB":"o","\u1EE1":"o","\u1EDF":"o","\u1EE3":"o","\u1ECD":"o","\u1ED9":"o","\u01EB":"o","\u01ED":"o","\u00F8":"o","\u01FF":"o","\u0254":"o","\uA74B":"o","\uA74D":"o","\u0275":"o","\u01A3":"oi","\u0223":"ou","\uA74F":"oo","\u24DF":"p","\uFF50":"p","\u1E55":"p","\u1E57":"p","\u01A5":"p","\u1D7D":"p","\uA751":"p","\uA753":"p","\uA755":"p","\u24E0":"q","\uFF51":"q","\u024B":"q","\uA757":"q","\uA759":"q","\u24E1":"r","\uFF52":"r","\u0155":"r","\u1E59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1E5B":"r","\u1E5D":"r","\u0157":"r","\u1E5F":"r","\u024D":"r","\u027D":"r","\uA75B":"r","\uA7A7":"r","\uA783":"r","\u24E2":"s","\uFF53":"s","\u00DF":"s","\u015B":"s","\u1E65":"s","\u015D":"s","\u1E61":"s","\u0161":"s","\u1E67":"s","\u1E63":"s","\u1E69":"s","\u0219":"s","\u015F":"s","\u023F":"s","\uA7A9":"s","\uA785":"s","\u1E9B":"s","\u24E3":"t","\uFF54":"t","\u1E6B":"t","\u1E97":"t","\u0165":"t","\u1E6D":"t","\u021B":"t","\u0163":"t","\u1E71":"t","\u1E6F":"t","\u0167":"t","\u01AD":"t","\u0288":"t","\u2C66":"t","\uA787":"t","\uA729":"tz","\u24E4":"u","\uFF55":"u","\u00F9":"u","\u00FA":"u","\u00FB":"u","\u0169":"u","\u1E79":"u","\u016B":"u","\u1E7B":"u","\u016D":"u","\u00FC":"u","\u01DC":"u","\u01D8":"u","\u01D6":"u","\u01DA":"u","\u1EE7":"u","\u016F":"u","\u0171":"u","\u01D4":"u","\u0215":"u","\u0217":"u","\u01B0":"u","\u1EEB":"u","\u1EE9":"u","\u1EEF":"u","\u1EED":"u","\u1EF1":"u","\u1EE5":"u","\u1E73":"u","\u0173":"u","\u1E77":"u","\u1E75":"u","\u0289":"u","\u24E5":"v","\uFF56":"v","\u1E7D":"v","\u1E7F":"v","\u028B":"v","\uA75F":"v","\u028C":"v","\uA761":"vy","\u24E6":"w","\uFF57":"w","\u1E81":"w","\u1E83":"w","\u0175":"w","\u1E87":"w","\u1E85":"w","\u1E98":"w","\u1E89":"w","\u2C73":"w","\u24E7":"x","\uFF58":"x","\u1E8B":"x","\u1E8D":"x","\u24E8":"y","\uFF59":"y","\u1EF3":"y","\u00FD":"y","\u0177":"y","\u1EF9":"y","\u0233":"y","\u1E8F":"y","\u00FF":"y","\u1EF7":"y","\u1E99":"y","\u1EF5":"y","\u01B4":"y","\u024F":"y","\u1EFF":"y","\u24E9":"z","\uFF5A":"z","\u017A":"z","\u1E91":"z","\u017C":"z","\u017E":"z","\u1E93":"z","\u1E95":"z","\u01B6":"z","\u0225":"z","\u0240":"z","\u2C6C":"z","\uA763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038A":"\u0399","\u03AA":"\u0399","\u038C":"\u039F","\u038E":"\u03A5","\u03AB":"\u03A5","\u038F":"\u03A9","\u03AC":"\u03B1","\u03AD":"\u03B5","\u03AE":"\u03B7","\u03AF":"\u03B9","\u03CA":"\u03B9","\u0390":"\u03B9","\u03CC":"\u03BF","\u03CD":"\u03C5","\u03CB":"\u03C5","\u03B0":"\u03C5","\u03C9":"\u03C9","\u03C2":"\u03C3"};
+
+ $document = $(document);
+
+ nextUid=(function() { var counter=1; return function() { return counter++; }; }());
+
+
+ function reinsertElement(element) {
+ var placeholder = $(document.createTextNode(''));
+
+ element.before(placeholder);
+ placeholder.before(element);
+ placeholder.remove();
+ }
+
+ function stripDiacritics(str) {
+ // Used 'uni range + named function' from http://jsperf.com/diacritics/18
+ function match(a) {
+ return DIACRITICS[a] || a;
+ }
+
+ return str.replace(/[^\u0000-\u007E]/g, match);
+ }
+
+ function indexOf(value, array) {
+ var i = 0, l = array.length;
+ for (; i < l; i = i + 1) {
+ if (equal(value, array[i])) return i;
+ }
+ return -1;
+ }
+
+ function measureScrollbar () {
+ var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
+ $template.appendTo(document.body);
+
+ var dim = {
+ width: $template.width() - $template[0].clientWidth,
+ height: $template.height() - $template[0].clientHeight
+ };
+ $template.remove();
+
+ return dim;
+ }
+
+ /**
+ * Compares equality of a and b
+ * @param a
+ * @param b
+ */
+ function equal(a, b) {
+ if (a === b) return true;
+ if (a === undefined || b === undefined) return false;
+ if (a === null || b === null) return false;
+ // Check whether 'a' or 'b' is a string (primitive or object).
+ // The concatenation of an empty string (+'') converts its argument to a string's primitive.
+ if (a.constructor === String) return a+'' === b+''; // a+'' - in case 'a' is a String object
+ if (b.constructor === String) return b+'' === a+''; // b+'' - in case 'b' is a String object
+ return false;
+ }
+
+ /**
+ * Splits the string into an array of values, transforming each value. An empty array is returned for nulls or empty
+ * strings
+ * @param string
+ * @param separator
+ */
+ function splitVal(string, separator, transform) {
+ var val, i, l;
+ if (string === null || string.length < 1) return [];
+ val = string.split(separator);
+ for (i = 0, l = val.length; i < l; i = i + 1) val[i] = transform(val[i]);
+ return val;
+ }
+
+ function getSideBorderPadding(element) {
+ return element.outerWidth(false) - element.width();
+ }
+
+ function installKeyUpChangeEvent(element) {
+ var key="keyup-change-value";
+ element.on("keydown", function () {
+ if ($.data(element, key) === undefined) {
+ $.data(element, key, element.val());
+ }
+ });
+ element.on("keyup", function () {
+ var val= $.data(element, key);
+ if (val !== undefined && element.val() !== val) {
+ $.removeData(element, key);
+ element.trigger("keyup-change");
+ }
+ });
+ }
+
+
+ /**
+ * filters mouse events so an event is fired only if the mouse moved.
+ *
+ * filters out mouse events that occur when mouse is stationary but
+ * the elements under the pointer are scrolled.
+ */
+ function installFilteredMouseMove(element) {
+ element.on("mousemove", function (e) {
+ var lastpos = lastMousePosition;
+ if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
+ $(e.target).trigger("mousemove-filtered", e);
+ }
+ });
+ }
+
+ /**
+ * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
+ * within the last quietMillis milliseconds.
+ *
+ * @param quietMillis number of milliseconds to wait before invoking fn
+ * @param fn function to be debounced
+ * @param ctx object to be used as this reference within fn
+ * @return debounced version of fn
+ */
+ function debounce(quietMillis, fn, ctx) {
+ ctx = ctx || undefined;
+ var timeout;
+ return function () {
+ var args = arguments;
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function() {
+ fn.apply(ctx, args);
+ }, quietMillis);
+ };
+ }
+
+ function installDebouncedScroll(threshold, element) {
+ var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
+ element.on("scroll", function (e) {
+ if (indexOf(e.target, element.get()) >= 0) notify(e);
+ });
+ }
+
+ function focus($el) {
+ if ($el[0] === document.activeElement) return;
+
+ /* set the focus in a 0 timeout - that way the focus is set after the processing
+ of the current event has finished - which seems like the only reliable way
+ to set focus */
+ window.setTimeout(function() {
+ var el=$el[0], pos=$el.val().length, range;
+
+ $el.focus();
+
+ /* make sure el received focus so we do not error out when trying to manipulate the caret.
+ sometimes modals or others listeners may steal it after its set */
+ var isVisible = (el.offsetWidth > 0 || el.offsetHeight > 0);
+ if (isVisible && el === document.activeElement) {
+
+ /* after the focus is set move the caret to the end, necessary when we val()
+ just before setting focus */
+ if(el.setSelectionRange)
+ {
+ el.setSelectionRange(pos, pos);
+ }
+ else if (el.createTextRange) {
+ range = el.createTextRange();
+ range.collapse(false);
+ range.select();
+ }
+ }
+ }, 0);
+ }
+
+ function getCursorInfo(el) {
+ el = $(el)[0];
+ var offset = 0;
+ var length = 0;
+ if ('selectionStart' in el) {
+ offset = el.selectionStart;
+ length = el.selectionEnd - offset;
+ } else if ('selection' in document) {
+ el.focus();
+ var sel = document.selection.createRange();
+ length = document.selection.createRange().text.length;
+ sel.moveStart('character', -el.value.length);
+ offset = sel.text.length - length;
+ }
+ return { offset: offset, length: length };
+ }
+
+ function killEvent(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ function killEventImmediately(event) {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+
+ function measureTextWidth(e) {
+ if (!sizer){
+ var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
+ sizer = $(document.createElement("div")).css({
+ position: "absolute",
+ left: "-10000px",
+ top: "-10000px",
+ display: "none",
+ fontSize: style.fontSize,
+ fontFamily: style.fontFamily,
+ fontStyle: style.fontStyle,
+ fontWeight: style.fontWeight,
+ letterSpacing: style.letterSpacing,
+ textTransform: style.textTransform,
+ whiteSpace: "nowrap"
+ });
+ sizer.attr("class","select2-sizer");
+ $(document.body).append(sizer);
+ }
+ sizer.text(e.val());
+ return sizer.width();
+ }
+
+ function syncCssClasses(dest, src, adapter) {
+ var classes, replacements = [], adapted;
+
+ classes = $.trim(dest.attr("class"));
+
+ if (classes) {
+ classes = '' + classes; // for IE which returns object
+
+ $(classes.split(/\s+/)).each2(function() {
+ if (this.indexOf("select2-") === 0) {
+ replacements.push(this);
+ }
+ });
+ }
+
+ classes = $.trim(src.attr("class"));
+
+ if (classes) {
+ classes = '' + classes; // for IE which returns object
+
+ $(classes.split(/\s+/)).each2(function() {
+ if (this.indexOf("select2-") !== 0) {
+ adapted = adapter(this);
+
+ if (adapted) {
+ replacements.push(adapted);
+ }
+ }
+ });
+ }
+
+ dest.attr("class", replacements.join(" "));
+ }
+
+
+ function markMatch(text, term, markup, escapeMarkup) {
+ var match=stripDiacritics(text.toUpperCase()).indexOf(stripDiacritics(term.toUpperCase())),
+ tl=term.length;
+
+ if (match<0) {
+ markup.push(escapeMarkup(text));
+ return;
+ }
+
+ markup.push(escapeMarkup(text.substring(0, match)));
+ markup.push("");
+ markup.push(escapeMarkup(text.substring(match, match + tl)));
+ markup.push("");
+ markup.push(escapeMarkup(text.substring(match + tl, text.length)));
+ }
+
+ function defaultEscapeMarkup(markup) {
+ var replace_map = {
+ '\\': '\',
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": ''',
+ "/": '/'
+ };
+
+ return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
+ return replace_map[match];
+ });
+ }
+
+ /**
+ * Produces an ajax-based query function
+ *
+ * @param options object containing configuration parameters
+ * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
+ * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
+ * @param options.url url for the data
+ * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
+ * @param options.dataType request data type: ajax, jsonp, other datatypes supported by jQuery's $.ajax function or the transport function if specified
+ * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
+ * @param options.results a function(remoteData, pageNumber, query) that converts data returned form the remote request to the format expected by Select2.
+ * The expected format is an object containing the following keys:
+ * results array of objects that will be used as choices
+ * more (optional) boolean indicating whether there are more results available
+ * Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
+ */
+ function ajax(options) {
+ var timeout, // current scheduled but not yet executed request
+ handler = null,
+ quietMillis = options.quietMillis || 100,
+ ajaxUrl = options.url,
+ self = this;
+
+ return function (query) {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(function () {
+ var data = options.data, // ajax data function
+ url = ajaxUrl, // ajax url string or function
+ transport = options.transport || $.fn.select2.ajaxDefaults.transport,
+ // deprecated - to be removed in 4.0 - use params instead
+ deprecated = {
+ type: options.type || 'GET', // set type of request (GET or POST)
+ cache: options.cache || false,
+ jsonpCallback: options.jsonpCallback||undefined,
+ dataType: options.dataType||"json"
+ },
+ params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
+
+ data = data ? data.call(self, query.term, query.page, query.context) : null;
+ url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
+
+ if (handler && typeof handler.abort === "function") { handler.abort(); }
+
+ if (options.params) {
+ if ($.isFunction(options.params)) {
+ $.extend(params, options.params.call(self));
+ } else {
+ $.extend(params, options.params);
+ }
+ }
+
+ $.extend(params, {
+ url: url,
+ dataType: options.dataType,
+ data: data,
+ success: function (data) {
+ // TODO - replace query.page with query so users have access to term, page, etc.
+ // added query as third paramter to keep backwards compatibility
+ var results = options.results(data, query.page, query);
+ query.callback(results);
+ },
+ error: function(jqXHR, textStatus, errorThrown){
+ var results = {
+ hasError: true,
+ jqXHR: jqXHR,
+ textStatus: textStatus,
+ errorThrown: errorThrown
+ };
+
+ query.callback(results);
+ }
+ });
+ handler = transport.call(self, params);
+ }, quietMillis);
+ };
+ }
+
+ /**
+ * Produces a query function that works with a local array
+ *
+ * @param options object containing configuration parameters. The options parameter can either be an array or an
+ * object.
+ *
+ * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
+ *
+ * If the object form is used it is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+ * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
+ * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
+ * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
+ * the text.
+ */
+ function local(options) {
+ var data = options, // data elements
+ dataText,
+ tmp,
+ text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
+
+ if ($.isArray(data)) {
+ tmp = data;
+ data = { results: tmp };
+ }
+
+ if ($.isFunction(data) === false) {
+ tmp = data;
+ data = function() { return tmp; };
+ }
+
+ var dataItem = data();
+ if (dataItem.text) {
+ text = dataItem.text;
+ // if text is not a function we assume it to be a key name
+ if (!$.isFunction(text)) {
+ dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
+ text = function (item) { return item[dataText]; };
+ }
+ }
+
+ return function (query) {
+ var t = query.term, filtered = { results: [] }, process;
+ if (t === "") {
+ query.callback(data());
+ return;
+ }
+
+ process = function(datum, collection) {
+ var group, attr;
+ datum = datum[0];
+ if (datum.children) {
+ group = {};
+ for (attr in datum) {
+ if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
+ }
+ group.children=[];
+ $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
+ if (group.children.length || query.matcher(t, text(group), datum)) {
+ collection.push(group);
+ }
+ } else {
+ if (query.matcher(t, text(datum), datum)) {
+ collection.push(datum);
+ }
+ }
+ };
+
+ $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
+ query.callback(filtered);
+ };
+ }
+
+ // TODO javadoc
+ function tags(data) {
+ var isFunc = $.isFunction(data);
+ return function (query) {
+ var t = query.term, filtered = {results: []};
+ var result = isFunc ? data(query) : data;
+ if ($.isArray(result)) {
+ $(result).each(function () {
+ var isObject = this.text !== undefined,
+ text = isObject ? this.text : this;
+ if (t === "" || query.matcher(t, text)) {
+ filtered.results.push(isObject ? this : {id: this, text: this});
+ }
+ });
+ query.callback(filtered);
+ }
+ };
+ }
+
+ /**
+ * Checks if the formatter function should be used.
+ *
+ * Throws an error if it is not a function. Returns true if it should be used,
+ * false if no formatting should be performed.
+ *
+ * @param formatter
+ */
+ function checkFormatter(formatter, formatterName) {
+ if ($.isFunction(formatter)) return true;
+ if (!formatter) return false;
+ if (typeof(formatter) === 'string') return true;
+ throw new Error(formatterName +" must be a string, function, or falsy value");
+ }
+
+ /**
+ * Returns a given value
+ * If given a function, returns its output
+ *
+ * @param val string|function
+ * @param context value of "this" to be passed to function
+ * @returns {*}
+ */
+ function evaluate(val, context) {
+ if ($.isFunction(val)) {
+ var args = Array.prototype.slice.call(arguments, 2);
+ return val.apply(context, args);
+ }
+ return val;
+ }
+
+ function countResults(results) {
+ var count = 0;
+ $.each(results, function(i, item) {
+ if (item.children) {
+ count += countResults(item.children);
+ } else {
+ count++;
+ }
+ });
+ return count;
+ }
+
+ /**
+ * Default tokenizer. This function uses breaks the input on substring match of any string from the
+ * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
+ * two options have to be defined in order for the tokenizer to work.
+ *
+ * @param input text user has typed so far or pasted into the search field
+ * @param selection currently selected choices
+ * @param selectCallback function(choice) callback tho add the choice to selection
+ * @param opts select2's opts
+ * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
+ */
+ function defaultTokenizer(input, selection, selectCallback, opts) {
+ var original = input, // store the original so we can compare and know if we need to tell the search to update its text
+ dupe = false, // check for whether a token we extracted represents a duplicate selected choice
+ token, // token
+ index, // position at which the separator was found
+ i, l, // looping variables
+ separator; // the matched separator
+
+ if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
+
+ while (true) {
+ index = -1;
+
+ for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
+ separator = opts.tokenSeparators[i];
+ index = input.indexOf(separator);
+ if (index >= 0) break;
+ }
+
+ if (index < 0) break; // did not find any token separator in the input string, bail
+
+ token = input.substring(0, index);
+ input = input.substring(index + separator.length);
+
+ if (token.length > 0) {
+ token = opts.createSearchChoice.call(this, token, selection);
+ if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
+ dupe = false;
+ for (i = 0, l = selection.length; i < l; i++) {
+ if (equal(opts.id(token), opts.id(selection[i]))) {
+ dupe = true; break;
+ }
+ }
+
+ if (!dupe) selectCallback(token);
+ }
+ }
+ }
+
+ if (original!==input) return input;
+ }
+
+ function cleanupJQueryElements() {
+ var self = this;
+
+ $.each(arguments, function (i, element) {
+ self[element].remove();
+ self[element] = null;
+ });
+ }
+
+ /**
+ * Creates a new class
+ *
+ * @param superClass
+ * @param methods
+ */
+ function clazz(SuperClass, methods) {
+ var constructor = function () {};
+ constructor.prototype = new SuperClass;
+ constructor.prototype.constructor = constructor;
+ constructor.prototype.parent = SuperClass.prototype;
+ constructor.prototype = $.extend(constructor.prototype, methods);
+ return constructor;
+ }
+
+ AbstractSelect2 = clazz(Object, {
+
+ // abstract
+ bind: function (func) {
+ var self = this;
+ return function () {
+ func.apply(self, arguments);
+ };
+ },
+
+ // abstract
+ init: function (opts) {
+ var results, search, resultsSelector = ".select2-results";
+
+ // prepare options
+ this.opts = opts = this.prepareOpts(opts);
+
+ this.id=opts.id;
+
+ // destroy if called on an existing component
+ if (opts.element.data("select2") !== undefined &&
+ opts.element.data("select2") !== null) {
+ opts.element.data("select2").destroy();
+ }
+
+ this.container = this.createContainer();
+
+ this.liveRegion = $('.select2-hidden-accessible');
+ if (this.liveRegion.length == 0) {
+ this.liveRegion = $("", {
+ role: "status",
+ "aria-live": "polite"
+ })
+ .addClass("select2-hidden-accessible")
+ .appendTo(document.body);
+ }
+
+ this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
+ this.containerEventName= this.containerId
+ .replace(/([.])/g, '_')
+ .replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
+ this.container.attr("id", this.containerId);
+
+ this.container.attr("title", opts.element.attr("title"));
+
+ this.body = $(document.body);
+
+ syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
+
+ this.container.attr("style", opts.element.attr("style"));
+ this.container.css(evaluate(opts.containerCss, this.opts.element));
+ this.container.addClass(evaluate(opts.containerCssClass, this.opts.element));
+
+ this.elementTabIndex = this.opts.element.attr("tabindex");
+
+ // swap container for the element
+ this.opts.element
+ .data("select2", this)
+ .attr("tabindex", "-1")
+ .before(this.container)
+ .on("click.select2", killEvent); // do not leak click events
+
+ this.container.data("select2", this);
+
+ this.dropdown = this.container.find(".select2-drop");
+
+ syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
+
+ this.dropdown.addClass(evaluate(opts.dropdownCssClass, this.opts.element));
+ this.dropdown.data("select2", this);
+ this.dropdown.on("click", killEvent);
+
+ this.results = results = this.container.find(resultsSelector);
+ this.search = search = this.container.find("input.select2-input");
+
+ this.queryCount = 0;
+ this.resultsPage = 0;
+ this.context = null;
+
+ // initialize the container
+ this.initContainer();
+
+ this.container.on("click", killEvent);
+
+ installFilteredMouseMove(this.results);
+
+ this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent));
+ this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) {
+ this._touchEvent = true;
+ this.highlightUnderEvent(event);
+ }));
+ this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
+ this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
+
+ // Waiting for a click event on touch devices to select option and hide dropdown
+ // otherwise click will be triggered on an underlying element
+ this.dropdown.on('click', this.bind(function (event) {
+ if (this._touchEvent) {
+ this._touchEvent = false;
+ this.selectHighlighted();
+ }
+ }));
+
+ installDebouncedScroll(80, this.results);
+ this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
+
+ // do not propagate change event from the search field out of the component
+ $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
+ $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
+
+ // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
+ if ($.fn.mousewheel) {
+ results.mousewheel(function (e, delta, deltaX, deltaY) {
+ var top = results.scrollTop();
+ if (deltaY > 0 && top - deltaY <= 0) {
+ results.scrollTop(0);
+ killEvent(e);
+ } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
+ results.scrollTop(results.get(0).scrollHeight - results.height());
+ killEvent(e);
+ }
+ });
+ }
+
+ installKeyUpChangeEvent(search);
+ search.on("keyup-change input paste", this.bind(this.updateResults));
+ search.on("focus", function () { search.addClass("select2-focused"); });
+ search.on("blur", function () { search.removeClass("select2-focused");});
+
+ this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
+ if ($(e.target).closest(".select2-result-selectable").length > 0) {
+ this.highlightUnderEvent(e);
+ this.selectHighlighted(e);
+ }
+ }));
+
+ // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
+ // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
+ // dom it will trigger the popup close, which is not what we want
+ // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
+ this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); });
+
+ this.lastSearchTerm = undefined;
+
+ if ($.isFunction(this.opts.initSelection)) {
+ // initialize selection based on the current value of the source element
+ this.initSelection();
+
+ // if the user has provided a function that can set selection based on the value of the source element
+ // we monitor the change event on the element and trigger it, allowing for two way synchronization
+ this.monitorSource();
+ }
+
+ if (opts.maximumInputLength !== null) {
+ this.search.attr("maxlength", opts.maximumInputLength);
+ }
+
+ var disabled = opts.element.prop("disabled");
+ if (disabled === undefined) disabled = false;
+ this.enable(!disabled);
+
+ var readonly = opts.element.prop("readonly");
+ if (readonly === undefined) readonly = false;
+ this.readonly(readonly);
+
+ // Calculate size of scrollbar
+ scrollBarDimensions = scrollBarDimensions || measureScrollbar();
+
+ this.autofocus = opts.element.prop("autofocus");
+ opts.element.prop("autofocus", false);
+ if (this.autofocus) this.focus();
+
+ this.search.attr("placeholder", opts.searchInputPlaceholder);
+ },
+
+ // abstract
+ destroy: function () {
+ var element=this.opts.element, select2 = element.data("select2"), self = this;
+
+ this.close();
+
+ if (element.length && element[0].detachEvent && self._sync) {
+ element.each(function () {
+ if (self._sync) {
+ this.detachEvent("onpropertychange", self._sync);
+ }
+ });
+ }
+ if (this.propertyObserver) {
+ this.propertyObserver.disconnect();
+ this.propertyObserver = null;
+ }
+ this._sync = null;
+
+ if (select2 !== undefined) {
+ select2.container.remove();
+ select2.liveRegion.remove();
+ select2.dropdown.remove();
+ element.removeData("select2")
+ .off(".select2");
+ if (!element.is("input[type='hidden']")) {
+ element
+ .show()
+ .prop("autofocus", this.autofocus || false);
+ if (this.elementTabIndex) {
+ element.attr({tabindex: this.elementTabIndex});
+ } else {
+ element.removeAttr("tabindex");
+ }
+ element.show();
+ } else {
+ element.css("display", "");
+ }
+ }
+
+ cleanupJQueryElements.call(this,
+ "container",
+ "liveRegion",
+ "dropdown",
+ "results",
+ "search"
+ );
+ },
+
+ // abstract
+ optionToData: function(element) {
+ if (element.is("option")) {
+ return {
+ id:element.prop("value"),
+ text:element.text(),
+ element: element.get(),
+ css: element.attr("class"),
+ disabled: element.prop("disabled"),
+ locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
+ };
+ } else if (element.is("optgroup")) {
+ return {
+ text:element.attr("label"),
+ children:[],
+ element: element.get(),
+ css: element.attr("class")
+ };
+ }
+ },
+
+ // abstract
+ prepareOpts: function (opts) {
+ var element, select, idKey, ajaxUrl, self = this;
+
+ element = opts.element;
+
+ if (element.get(0).tagName.toLowerCase() === "select") {
+ this.select = select = opts.element;
+ }
+
+ if (select) {
+ // these options are not allowed when attached to a select because they are picked up off the element itself
+ $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
+ if (this in opts) {
+ throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a