(function () {
var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}
// Used when there is no 'main' module.
// The name is probably (hopefully) unique so minification removes for releases.
var register_3795 = function (id) {
var module = dem(id);
var fragments = id.split('.');
var target = Function('return this;')();
for (var i = 0; i < fragments.length - 1; ++i) {
if (target[fragments[i]] === undefined)
target[fragments[i]] = {};
target = target[fragments[i]];
}
target[fragments[fragments.length - 1]] = module;
};
var instantiate = function (id) {
var actual = defs[id];
var dependencies = actual.deps;
var definition = actual.defn;
var len = dependencies.length;
var instances = new Array(len);
for (var i = 0; i < len; ++i)
instances[i] = dem(dependencies[i]);
var defResult = definition.apply(null, instances);
if (defResult === undefined)
throw 'module [' + id + '] returned undefined';
actual.instance = defResult;
};
var def = function (id, dependencies, definition) {
if (typeof id !== 'string')
throw 'module id must be a string';
else if (dependencies === undefined)
throw 'no dependencies for ' + id;
else if (definition === undefined)
throw 'no definition function for ' + id;
defs[id] = {
deps: dependencies,
defn: definition,
instance: undefined
};
};
var dem = function (id) {
var actual = defs[id];
if (actual === undefined)
throw 'module [' + id + '] was undefined';
else if (actual.instance === undefined)
instantiate(id);
return actual.instance;
};
var req = function (ids, callback) {
var len = ids.length;
var instances = new Array(len);
for (var i = 0; i < len; ++i)
instances.push(dem(ids[i]));
callback.apply(null, callback);
};
var ephox = {};
ephox.bolt = {
module: {
api: {
define: def,
require: req,
demand: dem
}
}
};
var define = def;
var require = req;
var demand = dem;
// this helps with minificiation when using a lot of global references
var defineGlobal = function (id, ref) {
define(id, [], function () { return ref; });
};
/*jsc
["tinymce.core.api.Main","tinymce.core.api.Tinymce","tinymce.core.Register","tinymce.core.geom.Rect","tinymce.core.util.Promise","tinymce.core.util.Delay","tinymce.core.Env","tinymce.core.dom.EventUtils","tinymce.core.dom.Sizzle","tinymce.core.util.Tools","tinymce.core.dom.DomQuery","tinymce.core.html.Styles","tinymce.core.dom.TreeWalker","tinymce.core.html.Entities","tinymce.core.dom.DOMUtils","tinymce.core.dom.ScriptLoader","tinymce.core.AddOnManager","tinymce.core.dom.RangeUtils","tinymce.core.html.Node","tinymce.core.html.Schema","tinymce.core.html.SaxParser","tinymce.core.html.DomParser","tinymce.core.html.Writer","tinymce.core.html.Serializer","tinymce.core.dom.Serializer","tinymce.core.util.VK","tinymce.core.dom.ControlSelection","tinymce.core.dom.BookmarkManager","tinymce.core.dom.Selection","tinymce.core.Formatter","tinymce.core.UndoManager","tinymce.core.EditorCommands","tinymce.core.util.URI","tinymce.core.util.Class","tinymce.core.util.EventDispatcher","tinymce.core.util.Observable","tinymce.core.WindowManager","tinymce.core.NotificationManager","tinymce.core.EditorObservable","tinymce.core.Shortcuts","tinymce.core.Editor","tinymce.core.util.I18n","tinymce.core.FocusManager","tinymce.core.EditorManager","tinymce.core.util.XHR","tinymce.core.util.JSON","tinymce.core.util.JSONRequest","tinymce.core.util.JSONP","tinymce.core.util.LocalStorage","tinymce.core.api.Compat","tinymce.core.util.Color","tinymce.core.ui.Api","tinymce.core.util.Arr","tinymce.core.dom.Range","tinymce.core.dom.StyleSheetLoader","tinymce.core.dom.NodeType","tinymce.core.caret.CaretContainer","tinymce.core.text.Zwsp","tinymce.core.caret.CaretBookmark","tinymce.core.caret.CaretPosition","tinymce.core.dom.ScrollIntoView","tinymce.core.dom.TridentSelection","tinymce.core.dom.ElementUtils","tinymce.core.util.Fun","tinymce.core.fmt.Preview","tinymce.core.fmt.Hooks","tinymce.core.undo.Levels","tinymce.core.delete.DeleteCommands","tinymce.core.InsertContent","global!document","tinymce.core.ui.Window","tinymce.core.ui.MessageBox","tinymce.core.ui.Notification","tinymce.core.init.Render","tinymce.core.Mode","tinymce.core.ui.Sidebar","tinymce.core.util.Uuid","tinymce.core.ErrorReporter","tinymce.core.LegacyInput","tinymce.core.ui.Selector","tinymce.core.ui.Collection","tinymce.core.ui.ReflowQueue","tinymce.core.ui.Control","tinymce.core.ui.Factory","tinymce.core.ui.KeyboardNavigation","tinymce.core.ui.Container","tinymce.core.ui.DragHelper","tinymce.core.ui.Scrollable","tinymce.core.ui.Panel","tinymce.core.ui.Movable","tinymce.core.ui.Resizable","tinymce.core.ui.FloatPanel","tinymce.core.ui.Tooltip","tinymce.core.ui.Widget","tinymce.core.ui.Progress","tinymce.core.ui.Layout","tinymce.core.ui.AbsoluteLayout","tinymce.core.ui.Button","tinymce.core.ui.ButtonGroup","tinymce.core.ui.Checkbox","tinymce.core.ui.ComboBox","tinymce.core.ui.ColorBox","tinymce.core.ui.PanelButton","tinymce.core.ui.ColorButton","tinymce.core.ui.ColorPicker","tinymce.core.ui.Path","tinymce.core.ui.ElementPath","tinymce.core.ui.FormItem","tinymce.core.ui.Form","tinymce.core.ui.FieldSet","tinymce.core.ui.FilePicker","tinymce.core.ui.FitLayout","tinymce.core.ui.FlexLayout","tinymce.core.ui.FlowLayout","tinymce.core.ui.FormatControls","tinymce.core.ui.GridLayout","tinymce.core.ui.Iframe","tinymce.core.ui.InfoBox","tinymce.core.ui.Label","tinymce.core.ui.Toolbar","tinymce.core.ui.MenuBar","tinymce.core.ui.MenuButton","tinymce.core.ui.MenuItem","tinymce.core.ui.Throbber","tinymce.core.ui.Menu","tinymce.core.ui.ListBox","tinymce.core.ui.Radio","tinymce.core.ui.ResizeHandle","tinymce.core.ui.SelectBox","tinymce.core.ui.Slider","tinymce.core.ui.Spacer","tinymce.core.ui.SplitButton","tinymce.core.ui.StackLayout","tinymce.core.ui.TabPanel","tinymce.core.ui.TextBox","ephox.katamari.api.Arr","ephox.katamari.api.Fun","ephox.katamari.api.Future","ephox.katamari.api.Futures","ephox.katamari.api.Result","tinymce.core.caret.CaretCandidate","tinymce.core.geom.ClientRect","tinymce.core.text.ExtendingChar","tinymce.core.undo.Fragments","tinymce.core.delete.BlockBoundaryDelete","tinymce.core.delete.BlockRangeDelete","tinymce.core.delete.CefDelete","tinymce.core.delete.InlineBoundaryDelete","tinymce.core.caret.CaretWalker","tinymce.core.dom.RangeNormalizer","tinymce.core.InsertList","tinymce.core.data.ObservableObject","tinymce.core.ui.DomUtils","tinymce.core.ui.BoxUtils","tinymce.core.ui.ClassList","global!window","tinymce.core.init.Init","tinymce.core.PluginManager","tinymce.core.ThemeManager","tinymce.core.content.LinkTargets","tinymce.core.fmt.FontInfo","ephox.katamari.api.Option","global!Array","global!Error","global!String","ephox.katamari.api.LazyValue","ephox.katamari.async.Bounce","ephox.katamari.async.AsyncValues","tinymce.core.undo.Diff","tinymce.core.delete.BlockBoundary","tinymce.core.delete.MergeBlocks","ephox.katamari.api.Options","ephox.sugar.api.dom.Compare","ephox.sugar.api.node.Element","tinymce.core.delete.DeleteUtils","tinymce.core.caret.CaretUtils","tinymce.core.delete.CefDeleteAction","tinymce.core.delete.DeleteElement","tinymce.core.keyboard.BoundaryCaret","tinymce.core.keyboard.BoundaryLocation","tinymce.core.keyboard.BoundarySelection","tinymce.core.keyboard.InlineUtils","tinymce.core.caret.CaretFinder","tinymce.core.data.Binding","tinymce.core.init.InitContentBody","global!Object","global!setTimeout","ephox.katamari.api.Struct","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","ephox.sugar.api.search.Selectors","global!console","ephox.sugar.api.node.Node","ephox.sugar.api.search.PredicateFind","ephox.sugar.api.search.Traverse","tinymce.core.dom.Empty","ephox.sugar.api.dom.Insert","ephox.sugar.api.dom.Remove","ephox.katamari.api.Adt","tinymce.core.text.Bidi","tinymce.core.caret.CaretContainerInline","tinymce.core.caret.CaretContainerRemove","tinymce.core.util.LazyEvaluator","ephox.katamari.api.Cell","tinymce.core.caret.CaretContainerInput","tinymce.core.EditorUpload","tinymce.core.ForceBlocks","tinymce.core.keyboard.KeyboardOverrides","tinymce.core.NodeChange","tinymce.core.SelectionOverrides","tinymce.core.util.Quirks","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sand.util.Global","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","global!navigator","ephox.sugar.api.node.NodeTypes","ephox.katamari.api.Type","ephox.sugar.api.node.Body","ephox.sugar.impl.ClosestOrAncestor","ephox.sugar.alien.Recurse","ephox.sugar.api.search.SelectorExists","ephox.sugar.api.dom.InsertAll","ephox.katamari.api.Obj","tinymce.core.file.Uploader","tinymce.core.file.ImageScanner","tinymce.core.file.BlobCache","tinymce.core.file.UploadStatus","tinymce.core.keyboard.ArrowKeys","tinymce.core.keyboard.DeleteBackspaceKeys","tinymce.core.keyboard.EnterKey","tinymce.core.keyboard.SpaceKey","tinymce.core.caret.FakeCaret","tinymce.core.caret.LineWalker","tinymce.core.caret.LineUtils","tinymce.core.DragDropOverrides","tinymce.core.dom.NodePath","ephox.katamari.util.BagUtils","ephox.katamari.api.Resolve","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","ephox.sugar.api.search.SelectorFind","tinymce.core.file.Conversions","global!URL","tinymce.core.keyboard.MatchKeys","tinymce.core.keyboard.InsertSpace","tinymce.core.dom.Dimensions","tinymce.core.dom.MousePosition","ephox.katamari.api.Global","ephox.sand.detect.Version","ephox.katamari.api.Strings","ephox.katamari.api.Merger","global!Number","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts"]
jsc*/
/**
* Rect.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Contains various tools for rect/position calculation.
*
* @class tinymce.geom.Rect
*/
define(
'tinymce.core.geom.Rect',
[
],
function () {
"use strict";
var min = Math.min, max = Math.max, round = Math.round;
/**
* Returns the rect positioned based on the relative position name
* to the target rect.
*
* @method relativePosition
* @param {Rect} rect Source rect to modify into a new rect.
* @param {Rect} targetRect Rect to move relative to based on the rel option.
* @param {String} rel Relative position. For example: tr-bl.
*/
function relativePosition(rect, targetRect, rel) {
var x, y, w, h, targetW, targetH;
x = targetRect.x;
y = targetRect.y;
w = rect.w;
h = rect.h;
targetW = targetRect.w;
targetH = targetRect.h;
rel = (rel || '').split('');
if (rel[0] === 'b') {
y += targetH;
}
if (rel[1] === 'r') {
x += targetW;
}
if (rel[0] === 'c') {
y += round(targetH / 2);
}
if (rel[1] === 'c') {
x += round(targetW / 2);
}
if (rel[3] === 'b') {
y -= h;
}
if (rel[4] === 'r') {
x -= w;
}
if (rel[3] === 'c') {
y -= round(h / 2);
}
if (rel[4] === 'c') {
x -= round(w / 2);
}
return create(x, y, w, h);
}
/**
* Tests various positions to get the most suitable one.
*
* @method findBestRelativePosition
* @param {Rect} rect Rect to use as source.
* @param {Rect} targetRect Rect to move relative to.
* @param {Rect} constrainRect Rect to constrain within.
* @param {Array} rels Array of relative positions to test against.
*/
function findBestRelativePosition(rect, targetRect, constrainRect, rels) {
var pos, i;
for (i = 0; i < rels.length; i++) {
pos = relativePosition(rect, targetRect, rels[i]);
if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x &&
pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) {
return rels[i];
}
}
return null;
}
/**
* Inflates the rect in all directions.
*
* @method inflate
* @param {Rect} rect Rect to expand.
* @param {Number} w Relative width to expand by.
* @param {Number} h Relative height to expand by.
* @return {Rect} New expanded rect.
*/
function inflate(rect, w, h) {
return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2);
}
/**
* Returns the intersection of the specified rectangles.
*
* @method intersect
* @param {Rect} rect The first rectangle to compare.
* @param {Rect} cropRect The second rectangle to compare.
* @return {Rect} The intersection of the two rectangles or null if they don't intersect.
*/
function intersect(rect, cropRect) {
var x1, y1, x2, y2;
x1 = max(rect.x, cropRect.x);
y1 = max(rect.y, cropRect.y);
x2 = min(rect.x + rect.w, cropRect.x + cropRect.w);
y2 = min(rect.y + rect.h, cropRect.y + cropRect.h);
if (x2 - x1 < 0 || y2 - y1 < 0) {
return null;
}
return create(x1, y1, x2 - x1, y2 - y1);
}
/**
* Returns a rect clamped within the specified clamp rect. This forces the
* rect to be inside the clamp rect.
*
* @method clamp
* @param {Rect} rect Rectangle to force within clamp rect.
* @param {Rect} clampRect Rectable to force within.
* @param {Boolean} fixedSize True/false if size should be fixed.
* @return {Rect} Clamped rect.
*/
function clamp(rect, clampRect, fixedSize) {
var underflowX1, underflowY1, overflowX2, overflowY2,
x1, y1, x2, y2, cx2, cy2;
x1 = rect.x;
y1 = rect.y;
x2 = rect.x + rect.w;
y2 = rect.y + rect.h;
cx2 = clampRect.x + clampRect.w;
cy2 = clampRect.y + clampRect.h;
underflowX1 = max(0, clampRect.x - x1);
underflowY1 = max(0, clampRect.y - y1);
overflowX2 = max(0, x2 - cx2);
overflowY2 = max(0, y2 - cy2);
x1 += underflowX1;
y1 += underflowY1;
if (fixedSize) {
x2 += underflowX1;
y2 += underflowY1;
x1 -= overflowX2;
y1 -= overflowY2;
}
x2 -= overflowX2;
y2 -= overflowY2;
return create(x1, y1, x2 - x1, y2 - y1);
}
/**
* Creates a new rectangle object.
*
* @method create
* @param {Number} x Rectangle x location.
* @param {Number} y Rectangle y location.
* @param {Number} w Rectangle width.
* @param {Number} h Rectangle height.
* @return {Rect} New rectangle object.
*/
function create(x, y, w, h) {
return { x: x, y: y, w: w, h: h };
}
/**
* Creates a new rectangle object form a clientRects object.
*
* @method fromClientRect
* @param {ClientRect} clientRect DOM ClientRect object.
* @return {Rect} New rectangle object.
*/
function fromClientRect(clientRect) {
return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
}
return {
inflate: inflate,
relativePosition: relativePosition,
findBestRelativePosition: findBestRelativePosition,
intersect: intersect,
clamp: clamp,
create: create,
fromClientRect: fromClientRect
};
}
);
/**
* Promise.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/* eslint-disable */
/* jshint ignore:start */
/**
* Modifed to be a feature fill and wrapped as tinymce module.
*/
define(
'tinymce.core.util.Promise',
[],
function () {
if (window.Promise) {
return window.Promise;
}
// Use polyfill for setImmediate for performance gains
var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) ||
function (fn) { setTimeout(fn, 1); };
// Polyfill for Function.prototype.bind
function bind(fn, thisArg) {
return function () {
fn.apply(thisArg, arguments);
};
}
var isArray = Array.isArray || function (value) { return Object.prototype.toString.call(value) === "[object Array]"; };
function Promise(fn) {
if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new');
if (typeof fn !== 'function') throw new TypeError('not a function');
this._state = null;
this._value = null;
this._deferreds = [];
doResolve(fn, bind(resolve, this), bind(reject, this));
}
function handle(deferred) {
var me = this;
if (this._state === null) {
this._deferreds.push(deferred);
return;
}
asap(function () {
var cb = me._state ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(me._state ? deferred.resolve : deferred.reject)(me._value);
return;
}
var ret;
try {
ret = cb(me._value);
}
catch (e) {
deferred.reject(e);
return;
}
deferred.resolve(ret);
});
}
function resolve(newValue) {
try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.');
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this));
return;
}
}
this._state = true;
this._value = newValue;
finale.call(this);
} catch (e) { reject.call(this, e); }
}
function reject(newValue) {
this._state = false;
this._value = newValue;
finale.call(this);
}
function finale() {
for (var i = 0, len = this._deferreds.length; i < len; i++) {
handle.call(this, this._deferreds[i]);
}
this._deferreds = null;
}
function Handler(onFulfilled, onRejected, resolve, reject) {
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.resolve = resolve;
this.reject = reject;
}
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*/
function doResolve(fn, onFulfilled, onRejected) {
var done = false;
try {
fn(function (value) {
if (done) return;
done = true;
onFulfilled(value);
}, function (reason) {
if (done) return;
done = true;
onRejected(reason);
});
} catch (ex) {
if (done) return;
done = true;
onRejected(ex);
}
}
Promise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function (onFulfilled, onRejected) {
var me = this;
return new Promise(function (resolve, reject) {
handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject));
});
};
Promise.all = function () {
var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments);
return new Promise(function (resolve, reject) {
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
try {
if (val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if (typeof then === 'function') {
then.call(val, function (val) { res(i, val); }, reject);
return;
}
}
args[i] = val;
if (--remaining === 0) {
resolve(args);
}
} catch (ex) {
reject(ex);
}
}
for (var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
};
Promise.resolve = function (value) {
if (value && typeof value === 'object' && value.constructor === Promise) {
return value;
}
return new Promise(function (resolve) {
resolve(value);
});
};
Promise.reject = function (value) {
return new Promise(function (resolve, reject) {
reject(value);
});
};
Promise.race = function (values) {
return new Promise(function (resolve, reject) {
for (var i = 0, len = values.length; i < len; i++) {
values[i].then(resolve, reject);
}
});
};
return Promise;
}
);
/* jshint ignore:end */
/* eslint-enable */
/**
* Delay.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* Utility class for working with delayed actions like setTimeout.
*
* @class tinymce.util.Delay
*/
define(
'tinymce.core.util.Delay',
[
"tinymce.core.util.Promise"
],
function (Promise) {
var requestAnimationFramePromise;
function requestAnimationFrame(callback, element) {
var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit'];
function featurefill(callback) {
window.setTimeout(callback, 0);
}
for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) {
requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame'];
}
if (!requestAnimationFrameFunc) {
requestAnimationFrameFunc = featurefill;
}
requestAnimationFrameFunc(callback, element);
}
function wrappedSetTimeout(callback, time) {
if (typeof time != 'number') {
time = 0;
}
return setTimeout(callback, time);
}
function wrappedSetInterval(callback, time) {
if (typeof time != 'number') {
time = 1; // IE 8 needs it to be > 0
}
return setInterval(callback, time);
}
function wrappedClearTimeout(id) {
return clearTimeout(id);
}
function wrappedClearInterval(id) {
return clearInterval(id);
}
function debounce(callback, time) {
var timer, func;
func = function () {
var args = arguments;
clearTimeout(timer);
timer = wrappedSetTimeout(function () {
callback.apply(this, args);
}, time);
};
func.stop = function () {
clearTimeout(timer);
};
return func;
}
return {
/**
* Requests an animation frame and fallbacks to a timeout on older browsers.
*
* @method requestAnimationFrame
* @param {function} callback Callback to execute when a new frame is available.
* @param {DOMElement} element Optional element to scope it to.
*/
requestAnimationFrame: function (callback, element) {
if (requestAnimationFramePromise) {
requestAnimationFramePromise.then(callback);
return;
}
requestAnimationFramePromise = new Promise(function (resolve) {
if (!element) {
element = document.body;
}
requestAnimationFrame(resolve, element);
}).then(callback);
},
/**
* Sets a timer in ms and executes the specified callback when the timer runs out.
*
* @method setTimeout
* @param {function} callback Callback to execute when timer runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setTimeout: wrappedSetTimeout,
/**
* Sets an interval timer in ms and executes the specified callback at every interval of that time.
*
* @method setInterval
* @param {function} callback Callback to execute when interval time runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setInterval: wrappedSetInterval,
/**
* Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is
* still alive when the callback gets executed.
*
* @method setEditorTimeout
* @param {tinymce.Editor} editor Editor instance to check the removed state on.
* @param {function} callback Callback to execute when timer runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setEditorTimeout: function (editor, callback, time) {
return wrappedSetTimeout(function () {
if (!editor.removed) {
callback();
}
}, time);
},
/**
* Sets an interval timer it's similar to setInterval except that it checks if the editor instance is
* still alive when the callback gets executed.
*
* @method setEditorInterval
* @param {function} callback Callback to execute when interval time runs out.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Number} Timeout id number.
*/
setEditorInterval: function (editor, callback, time) {
var timer;
timer = wrappedSetInterval(function () {
if (!editor.removed) {
callback();
} else {
clearInterval(timer);
}
}, time);
return timer;
},
/**
* Creates debounced callback function that only gets executed once within the specified time.
*
* @method debounce
* @param {function} callback Callback to execute when timer finishes.
* @param {Number} time Optional time to wait before the callback is executed, defaults to 0.
* @return {Function} debounced function callback.
*/
debounce: debounce,
// Throttle needs to be debounce due to backwards compatibility.
throttle: debounce,
/**
* Clears an interval timer so it won't execute.
*
* @method clearInterval
* @param {Number} Interval timer id number.
*/
clearInterval: wrappedClearInterval,
/**
* Clears an timeout timer so it won't execute.
*
* @method clearTimeout
* @param {Number} Timeout timer id number.
*/
clearTimeout: wrappedClearTimeout
};
}
);
/**
* Env.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class contains various environment constants like browser versions etc.
* Normally you don't want to sniff specific browser versions but sometimes you have
* to when it's impossible to feature detect. So use this with care.
*
* @class tinymce.Env
* @static
*/
define(
'tinymce.core.Env',
[
],
function () {
var nav = navigator, userAgent = nav.userAgent;
var opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone;
function matchMediaQuery(query) {
return "matchMedia" in window ? matchMedia(query).matches : false;
}
opera = window.opera && window.opera.buildNumber;
android = /Android/.test(userAgent);
webkit = /WebKit/.test(userAgent);
ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName);
ie = ie && /MSIE (\w+)\./.exec(userAgent)[1];
ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false;
ie12 = (userAgent.indexOf('Edge/') != -1 && !ie && !ie11) ? 12 : false;
ie = ie || ie11 || ie12;
gecko = !webkit && !ie11 && /Gecko/.test(userAgent);
mac = userAgent.indexOf('Mac') != -1;
iDevice = /(iPad|iPhone)/.test(userAgent);
fileApi = "FormData" in window && "FileReader" in window && "URL" in window && !!URL.createObjectURL;
phone = matchMediaQuery("only screen and (max-device-width: 480px)") && (android || iDevice);
tablet = matchMediaQuery("only screen and (min-width: 800px)") && (android || iDevice);
windowsPhone = userAgent.indexOf('Windows Phone') != -1;
if (ie12) {
webkit = false;
}
// Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions
// says it has contentEditable support but there is no visible caret.
var contentEditable = !iDevice || fileApi || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534;
return {
/**
* Constant that is true if the browser is Opera.
*
* @property opera
* @type Boolean
* @final
*/
opera: opera,
/**
* Constant that is true if the browser is WebKit (Safari/Chrome).
*
* @property webKit
* @type Boolean
* @final
*/
webkit: webkit,
/**
* Constant that is more than zero if the browser is IE.
*
* @property ie
* @type Boolean
* @final
*/
ie: ie,
/**
* Constant that is true if the browser is Gecko.
*
* @property gecko
* @type Boolean
* @final
*/
gecko: gecko,
/**
* Constant that is true if the os is Mac OS.
*
* @property mac
* @type Boolean
* @final
*/
mac: mac,
/**
* Constant that is true if the os is iOS.
*
* @property iOS
* @type Boolean
* @final
*/
iOS: iDevice,
/**
* Constant that is true if the os is android.
*
* @property android
* @type Boolean
* @final
*/
android: android,
/**
* Constant that is true if the browser supports editing.
*
* @property contentEditable
* @type Boolean
* @final
*/
contentEditable: contentEditable,
/**
* Transparent image data url.
*
* @property transparentSrc
* @type Boolean
* @final
*/
transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
/**
* Returns true/false if the browser can or can't place the caret after a inline block like an image.
*
* @property noCaretAfter
* @type Boolean
* @final
*/
caretAfter: ie != 8,
/**
* Constant that is true if the browser supports native DOM Ranges. IE 9+.
*
* @property range
* @type Boolean
*/
range: window.getSelection && "Range" in window,
/**
* Returns the IE document mode for non IE browsers this will fake IE 10.
*
* @property documentMode
* @type Number
*/
documentMode: ie && !ie12 ? (document.documentMode || 7) : 10,
/**
* Constant that is true if the browser has a modern file api.
*
* @property fileApi
* @type Boolean
*/
fileApi: fileApi,
/**
* Constant that is true if the browser supports contentEditable=false regions.
*
* @property ceFalse
* @type Boolean
*/
ceFalse: (ie === false || ie > 8),
/**
* Constant if CSP mode is possible or not. Meaning we can't use script urls for the iframe.
*/
canHaveCSP: (ie === false || ie > 11),
desktop: !phone && !tablet,
windowsPhone: windowsPhone
};
}
);
/**
* EventUtils.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*jshint loopfunc:true*/
/*eslint no-loop-func:0 */
/**
* This class wraps the browsers native event logic with more convenient methods.
*
* @class tinymce.dom.EventUtils
*/
define(
'tinymce.core.dom.EventUtils',
[
"tinymce.core.util.Delay",
"tinymce.core.Env"
],
function (Delay, Env) {
"use strict";
var eventExpandoPrefix = "mce-data-";
var mouseEventRe = /^(?:mouse|contextmenu)|click/;
var deprecated = {
keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1,
webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1
};
// Checks if it is our own isDefaultPrevented function
var hasIsDefaultPrevented = function (event) {
return event.isDefaultPrevented === returnTrue || event.isDefaultPrevented === returnFalse;
};
// Dummy function that gets replaced on the delegation state functions
var returnFalse = function () {
return false;
};
// Dummy function that gets replaced on the delegation state functions
var returnTrue = function () {
return true;
};
/**
* Binds a native event to a callback on the speified target.
*/
function addEvent(target, name, callback, capture) {
if (target.addEventListener) {
target.addEventListener(name, callback, capture || false);
} else if (target.attachEvent) {
target.attachEvent('on' + name, callback);
}
}
/**
* Unbinds a native event callback on the specified target.
*/
function removeEvent(target, name, callback, capture) {
if (target.removeEventListener) {
target.removeEventListener(name, callback, capture || false);
} else if (target.detachEvent) {
target.detachEvent('on' + name, callback);
}
}
/**
* Gets the event target based on shadow dom properties like path and deepPath.
*/
function getTargetFromShadowDom(event, defaultTarget) {
var path, target = defaultTarget;
// When target element is inside Shadow DOM we need to take first element from path
// otherwise we'll get Shadow Root parent, not actual target element
// Normalize target for WebComponents v0 implementation (in Chrome)
path = event.path;
if (path && path.length > 0) {
target = path[0];
}
// Normalize target for WebComponents v1 implementation (standard)
if (event.deepPath) {
path = event.deepPath();
if (path && path.length > 0) {
target = path[0];
}
}
return target;
}
/**
* Normalizes a native event object or just adds the event specific methods on a custom event.
*/
function fix(originalEvent, data) {
var name, event = data || {}, undef;
// Copy all properties from the original event
for (name in originalEvent) {
// layerX/layerY is deprecated in Chrome and produces a warning
if (!deprecated[name]) {
event[name] = originalEvent[name];
}
}
// Normalize target IE uses srcElement
if (!event.target) {
event.target = event.srcElement || document;
}
// Experimental shadow dom support
if (Env.experimentalShadowDom) {
event.target = getTargetFromShadowDom(originalEvent, event.target);
}
// Calculate pageX/Y if missing and clientX/Y available
if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
var eventDoc = event.target.ownerDocument || document;
var doc = eventDoc.documentElement;
var body = eventDoc.body;
event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0);
}
// Add preventDefault method
event.preventDefault = function () {
event.isDefaultPrevented = returnTrue;
// Execute preventDefault on the original event object
if (originalEvent) {
if (originalEvent.preventDefault) {
originalEvent.preventDefault();
} else {
originalEvent.returnValue = false; // IE
}
}
};
// Add stopPropagation
event.stopPropagation = function () {
event.isPropagationStopped = returnTrue;
// Execute stopPropagation on the original event object
if (originalEvent) {
if (originalEvent.stopPropagation) {
originalEvent.stopPropagation();
} else {
originalEvent.cancelBubble = true; // IE
}
}
};
// Add stopImmediatePropagation
event.stopImmediatePropagation = function () {
event.isImmediatePropagationStopped = returnTrue;
event.stopPropagation();
};
// Add event delegation states
if (hasIsDefaultPrevented(event) === false) {
event.isDefaultPrevented = returnFalse;
event.isPropagationStopped = returnFalse;
event.isImmediatePropagationStopped = returnFalse;
}
// Add missing metaKey for IE 8
if (typeof event.metaKey == 'undefined') {
event.metaKey = false;
}
return event;
}
/**
* Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
* It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
*/
function bindOnReady(win, callback, eventUtils) {
var doc = win.document, event = { type: 'ready' };
if (eventUtils.domLoaded) {
callback(event);
return;
}
function isDocReady() {
// Check complete or interactive state if there is a body
// element on some iframes IE 8 will produce a null body
return doc.readyState === "complete" || (doc.readyState === "interactive" && doc.body);
}
// Gets called when the DOM is ready
function readyHandler() {
if (!eventUtils.domLoaded) {
eventUtils.domLoaded = true;
callback(event);
}
}
function waitForDomLoaded() {
if (isDocReady()) {
removeEvent(doc, "readystatechange", waitForDomLoaded);
readyHandler();
}
}
function tryScroll() {
try {
// If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
// http://javascript.nwbox.com/IEContentLoaded/
doc.documentElement.doScroll("left");
} catch (ex) {
Delay.setTimeout(tryScroll);
return;
}
readyHandler();
}
// Use W3C method (exclude IE 9,10 - readyState "interactive" became valid only in IE 11)
if (doc.addEventListener && !(Env.ie && Env.ie < 11)) {
if (isDocReady()) {
readyHandler();
} else {
addEvent(win, 'DOMContentLoaded', readyHandler);
}
} else {
// Use IE method
addEvent(doc, "readystatechange", waitForDomLoaded);
// Wait until we can scroll, when we can the DOM is initialized
if (doc.documentElement.doScroll && win.self === win.top) {
tryScroll();
}
}
// Fallback if any of the above methods should fail for some odd reason
addEvent(win, 'load', readyHandler);
}
/**
* This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
*/
function EventUtils() {
var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
expando = eventExpandoPrefix + (+new Date()).toString(32);
hasMouseEnterLeave = "onmouseenter" in document.documentElement;
hasFocusIn = "onfocusin" in document.documentElement;
mouseEnterLeave = { mouseenter: 'mouseover', mouseleave: 'mouseout' };
count = 1;
// State if the DOMContentLoaded was executed or not
self.domLoaded = false;
self.events = events;
/**
* Executes all event handler callbacks for a specific event.
*
* @private
* @param {Event} evt Event object.
* @param {String} id Expando id value to look for.
*/
function executeHandlers(evt, id) {
var callbackList, i, l, callback, container = events[id];
callbackList = container && container[evt.type];
if (callbackList) {
for (i = 0, l = callbackList.length; i < l; i++) {
callback = callbackList[i];
// Check if callback exists might be removed if a unbind is called inside the callback
if (callback && callback.func.call(callback.scope, evt) === false) {
evt.preventDefault();
}
// Should we stop propagation to immediate listeners
if (evt.isImmediatePropagationStopped()) {
return;
}
}
}
}
/**
* Binds a callback to an event on the specified target.
*
* @method bind
* @param {Object} target Target node/window or custom object.
* @param {String} names Name of the event to bind.
* @param {function} callback Callback function to execute when the event occurs.
* @param {Object} scope Scope to call the callback function on, defaults to target.
* @return {function} Callback function that got bound.
*/
self.bind = function (target, names, callback, scope) {
var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
// Native event handler function patches the event and executes the callbacks for the expando
function defaultNativeHandler(evt) {
executeHandlers(fix(evt || win.event), id);
}
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return;
}
// Create or get events id for the target
if (!target[expando]) {
id = count++;
target[expando] = id;
events[id] = {};
} else {
id = target[expando];
}
// Setup the specified scope or use the target as a default
scope = scope || target;
// Split names and bind each event, enables you to bind multiple events with one call
names = names.split(' ');
i = names.length;
while (i--) {
name = names[i];
nativeHandler = defaultNativeHandler;
fakeName = capture = false;
// Use ready instead of DOMContentLoaded
if (name === "DOMContentLoaded") {
name = "ready";
}
// DOM is already ready
if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
callback.call(scope, fix({ type: name }));
continue;
}
// Handle mouseenter/mouseleaver
if (!hasMouseEnterLeave) {
fakeName = mouseEnterLeave[name];
if (fakeName) {
nativeHandler = function (evt) {
var current, related;
current = evt.currentTarget;
related = evt.relatedTarget;
// Check if related is inside the current target if it's not then the event should
// be ignored since it's a mouseover/mouseout inside the element
if (related && current.contains) {
// Use contains for performance
related = current.contains(related);
} else {
while (related && related !== current) {
related = related.parentNode;
}
}
// Fire fake event
if (!related) {
evt = fix(evt || win.event);
evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
evt.target = current;
executeHandlers(evt, id);
}
};
}
}
// Fake bubbling of focusin/focusout
if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
capture = true;
fakeName = name === "focusin" ? "focus" : "blur";
nativeHandler = function (evt) {
evt = fix(evt || win.event);
evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
executeHandlers(evt, id);
};
}
// Setup callback list and bind native event
callbackList = events[id][name];
if (!callbackList) {
events[id][name] = callbackList = [{ func: callback, scope: scope }];
callbackList.fakeName = fakeName;
callbackList.capture = capture;
//callbackList.callback = callback;
// Add the nativeHandler to the callback list so that we can later unbind it
callbackList.nativeHandler = nativeHandler;
// Check if the target has native events support
if (name === "ready") {
bindOnReady(target, nativeHandler, self);
} else {
addEvent(target, fakeName || name, nativeHandler, capture);
}
} else {
if (name === "ready" && self.domLoaded) {
callback({ type: name });
} else {
// If it already has an native handler then just push the callback
callbackList.push({ func: callback, scope: scope });
}
}
}
target = callbackList = 0; // Clean memory for IE
return callback;
};
/**
* Unbinds the specified event by name, name and callback or all events on the target.
*
* @method unbind
* @param {Object} target Target node/window or custom object.
* @param {String} names Optional event name to unbind.
* @param {function} callback Optional callback function to unbind.
* @return {EventUtils} Event utils instance.
*/
self.unbind = function (target, names, callback) {
var id, callbackList, i, ci, name, eventMap;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Unbind event or events if the target has the expando
id = target[expando];
if (id) {
eventMap = events[id];
// Specific callback
if (names) {
names = names.split(' ');
i = names.length;
while (i--) {
name = names[i];
callbackList = eventMap[name];
// Unbind the event if it exists in the map
if (callbackList) {
// Remove specified callback
if (callback) {
ci = callbackList.length;
while (ci--) {
if (callbackList[ci].func === callback) {
var nativeHandler = callbackList.nativeHandler;
var fakeName = callbackList.fakeName, capture = callbackList.capture;
// Clone callbackList since unbind inside a callback would otherwise break the handlers loop
callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
callbackList.nativeHandler = nativeHandler;
callbackList.fakeName = fakeName;
callbackList.capture = capture;
eventMap[name] = callbackList;
}
}
}
// Remove all callbacks if there isn't a specified callback or there is no callbacks left
if (!callback || callbackList.length === 0) {
delete eventMap[name];
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
}
}
}
} else {
// All events for a specific element
for (name in eventMap) {
callbackList = eventMap[name];
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
}
eventMap = {};
}
// Check if object is empty, if it isn't then we won't remove the expando map
for (name in eventMap) {
return self;
}
// Delete event object
delete events[id];
// Remove expando from target
try {
// IE will fail here since it can't delete properties from window
delete target[expando];
} catch (ex) {
// IE will set it to null
target[expando] = null;
}
}
return self;
};
/**
* Fires the specified event on the specified target.
*
* @method fire
* @param {Object} target Target node/window or custom object.
* @param {String} name Event name to fire.
* @param {Object} args Optional arguments to send to the observers.
* @return {EventUtils} Event utils instance.
*/
self.fire = function (target, name, args) {
var id;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Build event object by patching the args
args = fix(null, args);
args.type = name;
args.target = target;
do {
// Found an expando that means there is listeners to execute
id = target[expando];
if (id) {
executeHandlers(args, id);
}
// Walk up the DOM
target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
} while (target && !args.isPropagationStopped());
return self;
};
/**
* Removes all bound event listeners for the specified target. This will also remove any bound
* listeners to child nodes within that target.
*
* @method clean
* @param {Object} target Target node/window object.
* @return {EventUtils} Event utils instance.
*/
self.clean = function (target) {
var i, children, unbind = self.unbind;
// Don't bind to text nodes or comments
if (!target || target.nodeType === 3 || target.nodeType === 8) {
return self;
}
// Unbind any element on the specified target
if (target[expando]) {
unbind(target);
}
// Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
if (!target.getElementsByTagName) {
target = target.document;
}
// Remove events from each child element
if (target && target.getElementsByTagName) {
unbind(target);
children = target.getElementsByTagName('*');
i = children.length;
while (i--) {
target = children[i];
if (target[expando]) {
unbind(target);
}
}
}
return self;
};
/**
* Destroys the event object. Call this on IE to remove memory leaks.
*/
self.destroy = function () {
events = {};
};
// Legacy function for canceling events
self.cancel = function (e) {
if (e) {
e.preventDefault();
e.stopImmediatePropagation();
}
return false;
};
}
EventUtils.Event = new EventUtils();
EventUtils.Event.bind(window, 'ready', function () { });
return EventUtils;
}
);
/**
* Sizzle.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*
* @ignore-file
*/
/*jshint bitwise:false, expr:true, noempty:false, sub:true, eqnull:true, latedef:false, maxlen:255 */
/*eslint-disable */
/**
* Sizzle CSS Selector Engine v@VERSION
* http://sizzlejs.com/
*
* Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors
* Released under the MIT license
* http://jquery.org/license
*
* Date: @DATE
*/
define(
'tinymce.core.dom.Sizzle',
[],
function () {
var i,
support,
Expr,
getText,
isXML,
tokenize,
compile,
select,
outermostContext,
sortInput,
hasDuplicate,
// Local document vars
setDocument,
document,
docElem,
documentIsHTML,
rbuggyQSA,
rbuggyMatches,
matches,
contains,
// Instance-specific data
expando = "sizzle" + -(new Date()),
preferredDoc = window.document,
dirruns = 0,
done = 0,
classCache = createCache(),
tokenCache = createCache(),
compilerCache = createCache(),
sortOrder = function (a, b) {
if (a === b) {
hasDuplicate = true;
}
return 0;
},
// General-purpose constants
strundefined = typeof undefined,
MAX_NEGATIVE = 1 << 31,
// Instance methods
hasOwn = ({}).hasOwnProperty,
arr = [],
pop = arr.pop,
push_native = arr.push,
push = arr.push,
slice = arr.slice,
// Use a stripped-down indexOf if we can't use a native one
indexOf = arr.indexOf || function (elem) {
var i = 0,
len = this.length;
for (; i < len; i++) {
if (this[i] === elem) {
return i;
}
}
return -1;
},
booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
// Regular expressions
// http://www.w3.org/TR/css3-selectors/#whitespace
whitespace = "[\\x20\\t\\r\\n\\f]",
// http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
// Operator (capture 2)
"*([*^$|!~]?=)" + whitespace +
// "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
"*\\]",
pseudos = ":(" + identifier + ")(?:\\((" +
// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
// 1. quoted (capture 3; capture 4 or capture 5)
"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
// 2. simple (capture 6)
"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
// 3. anything else (capture 2)
".*" +
")\\)|)",
// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
rtrim = new RegExp("^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g"),
rcomma = new RegExp("^" + whitespace + "*," + whitespace + "*"),
rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*"),
rattributeQuotes = new RegExp("=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g"),
rpseudo = new RegExp(pseudos),
ridentifier = new RegExp("^" + identifier + "$"),
matchExpr = {
"ID": new RegExp("^#(" + identifier + ")"),
"CLASS": new RegExp("^\\.(" + identifier + ")"),
"TAG": new RegExp("^(" + identifier + "|[*])"),
"ATTR": new RegExp("^" + attributes),
"PSEUDO": new RegExp("^" + pseudos),
"CHILD": new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
"*(\\d+)|))" + whitespace + "*\\)|)", "i"),
"bool": new RegExp("^(?:" + booleans + ")$", "i"),
// For use in libraries implementing .is()
// We use this for POS matching in `select`
"needsContext": new RegExp("^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i")
},
rinputs = /^(?:input|select|textarea|button)$/i,
rheader = /^h\d$/i,
rnative = /^[^{]+\{\s*\[native \w/,
// Easily-parseable/retrievable ID or TAG or CLASS selectors
rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
rsibling = /[+~]/,
rescape = /'|\\/g,
// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
runescape = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig"),
funescape = function (_, escaped, escapedWhitespace) {
var high = "0x" + escaped - 0x10000;
// NaN means non-codepoint
// Support: Firefox<24
// Workaround erroneous numeric interpretation of +"0x"
return high !== high || escapedWhitespace ?
escaped :
high < 0 ?
// BMP codepoint
String.fromCharCode(high + 0x10000) :
// Supplemental Plane codepoint (surrogate pair)
String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00);
};
// Optimize for push.apply( _, NodeList )
try {
push.apply(
(arr = slice.call(preferredDoc.childNodes)),
preferredDoc.childNodes
);
// Support: Android<4.0
// Detect silently failing push.apply
arr[preferredDoc.childNodes.length].nodeType;
} catch (e) {
push = {
apply: arr.length ?
// Leverage slice if possible
function (target, els) {
push_native.apply(target, slice.call(els));
} :
// Support: IE<9
// Otherwise append directly
function (target, els) {
var j = target.length,
i = 0;
// Can't trust NodeList.length
while ((target[j++] = els[i++])) { }
target.length = j - 1;
}
};
}
function Sizzle(selector, context, results, seed) {
var match, elem, m, nodeType,
// QSA vars
i, groups, old, nid, newContext, newSelector;
if ((context ? context.ownerDocument || context : preferredDoc) !== document) {
setDocument(context);
}
context = context || document;
results = results || [];
if (!selector || typeof selector !== "string") {
return results;
}
if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) {
return [];
}
if (documentIsHTML && !seed) {
// Shortcuts
if ((match = rquickExpr.exec(selector))) {
// Speed-up: Sizzle("#ID")
if ((m = match[1])) {
if (nodeType === 9) {
elem = context.getElementById(m);
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document (jQuery #6963)
if (elem && elem.parentNode) {
// Handle the case where IE, Opera, and Webkit return items
// by name instead of ID
if (elem.id === m) {
results.push(elem);
return results;
}
} else {
return results;
}
} else {
// Context is not a document
if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) &&
contains(context, elem) && elem.id === m) {
results.push(elem);
return results;
}
}
// Speed-up: Sizzle("TAG")
} else if (match[2]) {
push.apply(results, context.getElementsByTagName(selector));
return results;
// Speed-up: Sizzle(".CLASS")
} else if ((m = match[3]) && support.getElementsByClassName) {
push.apply(results, context.getElementsByClassName(m));
return results;
}
}
// QSA path
if (support.qsa && (!rbuggyQSA || !rbuggyQSA.test(selector))) {
nid = old = expando;
newContext = context;
newSelector = nodeType === 9 && selector;
// qSA works strangely on Element-rooted queries
// We can work around this by specifying an extra ID on the root
// and working up from there (Thanks to Andrew Dupont for the technique)
// IE 8 doesn't work on object elements
if (nodeType === 1 && context.nodeName.toLowerCase() !== "object") {
groups = tokenize(selector);
if ((old = context.getAttribute("id"))) {
nid = old.replace(rescape, "\\$&");
} else {
context.setAttribute("id", nid);
}
nid = "[id='" + nid + "'] ";
i = groups.length;
while (i--) {
groups[i] = nid + toSelector(groups[i]);
}
newContext = rsibling.test(selector) && testContext(context.parentNode) || context;
newSelector = groups.join(",");
}
if (newSelector) {
try {
push.apply(results,
newContext.querySelectorAll(newSelector)
);
return results;
} catch (qsaError) {
} finally {
if (!old) {
context.removeAttribute("id");
}
}
}
}
}
// All others
return select(selector.replace(rtrim, "$1"), context, results, seed);
}
/**
* Create key-value caches of limited size
* @returns {Function(string, Object)} Returns the Object data after storing it on itself with
* property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
* deleting the oldest entry
*/
function createCache() {
var keys = [];
function cache(key, value) {
// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
if (keys.push(key + " ") > Expr.cacheLength) {
// Only keep the most recent entries
delete cache[keys.shift()];
}
return (cache[key + " "] = value);
}
return cache;
}
/**
* Mark a function for special use by Sizzle
* @param {Function} fn The function to mark
*/
function markFunction(fn) {
fn[expando] = true;
return fn;
}
/**
* Support testing using an element
* @param {Function} fn Passed the created div and expects a boolean result
*/
function assert(fn) {
var div = document.createElement("div");
try {
return !!fn(div);
} catch (e) {
return false;
} finally {
// Remove from its parent by default
if (div.parentNode) {
div.parentNode.removeChild(div);
}
// release memory in IE
div = null;
}
}
/**
* Adds the same handler for all of the specified attrs
* @param {String} attrs Pipe-separated list of attributes
* @param {Function} handler The method that will be applied
*/
function addHandle(attrs, handler) {
var arr = attrs.split("|"),
i = attrs.length;
while (i--) {
Expr.attrHandle[arr[i]] = handler;
}
}
/**
* Checks document order of two siblings
* @param {Element} a
* @param {Element} b
* @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
*/
function siblingCheck(a, b) {
var cur = b && a,
diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
(~b.sourceIndex || MAX_NEGATIVE) -
(~a.sourceIndex || MAX_NEGATIVE);
// Use IE sourceIndex if available on both nodes
if (diff) {
return diff;
}
// Check if b follows a
if (cur) {
while ((cur = cur.nextSibling)) {
if (cur === b) {
return -1;
}
}
}
return a ? 1 : -1;
}
/**
* Returns a function to use in pseudos for input types
* @param {String} type
*/
function createInputPseudo(type) {
return function (elem) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for buttons
* @param {String} type
*/
function createButtonPseudo(type) {
return function (elem) {
var name = elem.nodeName.toLowerCase();
return (name === "input" || name === "button") && elem.type === type;
};
}
/**
* Returns a function to use in pseudos for positionals
* @param {Function} fn
*/
function createPositionalPseudo(fn) {
return markFunction(function (argument) {
argument = +argument;
return markFunction(function (seed, matches) {
var j,
matchIndexes = fn([], seed.length, argument),
i = matchIndexes.length;
// Match elements found at the specified indexes
while (i--) {
if (seed[(j = matchIndexes[i])]) {
seed[j] = !(matches[j] = seed[j]);
}
}
});
});
}
/**
* Checks a node for validity as a Sizzle context
* @param {Element|Object=} context
* @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
*/
function testContext(context) {
return context && typeof context.getElementsByTagName !== strundefined && context;
}
// Expose support vars for convenience
support = Sizzle.support = {};
/**
* Detects XML nodes
* @param {Element|Object} elem An element or a document
* @returns {Boolean} True iff elem is a non-HTML XML node
*/
isXML = Sizzle.isXML = function (elem) {
// documentElement is verified for cases where it doesn't yet exist
// (such as loading iframes in IE - #4833)
var documentElement = elem && (elem.ownerDocument || elem).documentElement;
return documentElement ? documentElement.nodeName !== "HTML" : false;
};
/**
* Sets document-related variables once based on the current document
* @param {Element|Object} [doc] An element or document object to use to set the document
* @returns {Object} Returns the current document
*/
setDocument = Sizzle.setDocument = function (node) {
var hasCompare,
doc = node ? node.ownerDocument || node : preferredDoc,
parent = doc.defaultView;
function getTop(win) {
// Edge throws a lovely Object expected if you try to get top on a detached reference see #2642
try {
return win.top;
} catch (ex) {
// Ignore
}
return null;
}
// If no document and documentElement is available, return
if (doc === document || doc.nodeType !== 9 || !doc.documentElement) {
return document;
}
// Set our document
document = doc;
docElem = doc.documentElement;
// Support tests
documentIsHTML = !isXML(doc);
// Support: IE>8
// If iframe document is assigned to "document" variable and if iframe has been reloaded,
// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
// IE6-8 do not support the defaultView property so parent will be undefined
if (parent && parent !== getTop(parent)) {
// IE11 does not have attachEvent, so all must suffer
if (parent.addEventListener) {
parent.addEventListener("unload", function () {
setDocument();
}, false);
} else if (parent.attachEvent) {
parent.attachEvent("onunload", function () {
setDocument();
});
}
}
/* Attributes
---------------------------------------------------------------------- */
// Support: IE<8
// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
support.attributes = assert(function (div) {
div.className = "i";
return !div.getAttribute("className");
});
/* getElement(s)By*
---------------------------------------------------------------------- */
// Check if getElementsByTagName("*") returns only elements
support.getElementsByTagName = assert(function (div) {
div.appendChild(doc.createComment(""));
return !div.getElementsByTagName("*").length;
});
// Support: IE<9
support.getElementsByClassName = rnative.test(doc.getElementsByClassName);
// Support: IE<10
// Check if getElementById returns elements by name
// The broken getElementById methods don't pick up programatically-set names,
// so use a roundabout getElementsByName test
support.getById = assert(function (div) {
docElem.appendChild(div).id = expando;
return !doc.getElementsByName || !doc.getElementsByName(expando).length;
});
// ID find and filter
if (support.getById) {
Expr.find["ID"] = function (id, context) {
if (typeof context.getElementById !== strundefined && documentIsHTML) {
var m = context.getElementById(id);
// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
return m && m.parentNode ? [m] : [];
}
};
Expr.filter["ID"] = function (id) {
var attrId = id.replace(runescape, funescape);
return function (elem) {
return elem.getAttribute("id") === attrId;
};
};
} else {
// Support: IE6/7
// getElementById is not reliable as a find shortcut
delete Expr.find["ID"];
Expr.filter["ID"] = function (id) {
var attrId = id.replace(runescape, funescape);
return function (elem) {
var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
return node && node.value === attrId;
};
};
}
// Tag
Expr.find["TAG"] = support.getElementsByTagName ?
function (tag, context) {
if (typeof context.getElementsByTagName !== strundefined) {
return context.getElementsByTagName(tag);
}
} :
function (tag, context) {
var elem,
tmp = [],
i = 0,
results = context.getElementsByTagName(tag);
// Filter out possible comments
if (tag === "*") {
while ((elem = results[i++])) {
if (elem.nodeType === 1) {
tmp.push(elem);
}
}
return tmp;
}
return results;
};
// Class
Expr.find["CLASS"] = support.getElementsByClassName && function (className, context) {
if (documentIsHTML) {
return context.getElementsByClassName(className);
}
};
/* QSA/matchesSelector
---------------------------------------------------------------------- */
// QSA and matchesSelector support
// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
rbuggyMatches = [];
// qSa(:focus) reports false when true (Chrome 21)
// We allow this because of a bug in IE8/9 that throws an error
// whenever `document.activeElement` is accessed on an iframe
// So, we allow :focus to pass through QSA all the time to avoid the IE error
// See http://bugs.jquery.com/ticket/13378
rbuggyQSA = [];
if ((support.qsa = rnative.test(doc.querySelectorAll))) {
// Build QSA regex
// Regex strategy adopted from Diego Perini
assert(function (div) {
// Select is set to empty string on purpose
// This is to test IE's treatment of not explicitly
// setting a boolean content attribute,
// since its presence should be enough
// http://bugs.jquery.com/ticket/12359
div.innerHTML = "";
// Support: IE8, Opera 11-12.16
// Nothing should be selected when empty strings follow ^= or $= or *=
// The test attribute must be unknown in Opera but "safe" for WinRT
// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
if (div.querySelectorAll("[msallowcapture^='']").length) {
rbuggyQSA.push("[*^$]=" + whitespace + "*(?:''|\"\")");
}
// Support: IE8
// Boolean attributes and "value" are not treated correctly
if (!div.querySelectorAll("[selected]").length) {
rbuggyQSA.push("\\[" + whitespace + "*(?:value|" + booleans + ")");
}
// Webkit/Opera - :checked should return selected option elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
// IE8 throws error here and will not see later tests
if (!div.querySelectorAll(":checked").length) {
rbuggyQSA.push(":checked");
}
});
assert(function (div) {
// Support: Windows 8 Native Apps
// The type and name attributes are restricted during .innerHTML assignment
var input = doc.createElement("input");
input.setAttribute("type", "hidden");
div.appendChild(input).setAttribute("name", "D");
// Support: IE8
// Enforce case-sensitivity of name attribute
if (div.querySelectorAll("[name=d]").length) {
rbuggyQSA.push("name" + whitespace + "*[*^$|!~]?=");
}
// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
// IE8 throws error here and will not see later tests
if (!div.querySelectorAll(":enabled").length) {
rbuggyQSA.push(":enabled", ":disabled");
}
// Opera 10-11 does not throw on post-comma invalid pseudos
div.querySelectorAll("*,:x");
rbuggyQSA.push(",.*:");
});
}
if ((support.matchesSelector = rnative.test((matches = docElem.matches ||
docElem.webkitMatchesSelector ||
docElem.mozMatchesSelector ||
docElem.oMatchesSelector ||
docElem.msMatchesSelector)))) {
assert(function (div) {
// Check to see if it's possible to do matchesSelector
// on a disconnected node (IE 9)
support.disconnectedMatch = matches.call(div, "div");
// This should fail with an exception
// Gecko does not error, returns false instead
matches.call(div, "[s!='']:x");
rbuggyMatches.push("!=", pseudos);
});
}
rbuggyQSA = rbuggyQSA.length && new RegExp(rbuggyQSA.join("|"));
rbuggyMatches = rbuggyMatches.length && new RegExp(rbuggyMatches.join("|"));
/* Contains
---------------------------------------------------------------------- */
hasCompare = rnative.test(docElem.compareDocumentPosition);
// Element contains another
// Purposefully does not implement inclusive descendent
// As in, an element does not contain itself
contains = hasCompare || rnative.test(docElem.contains) ?
function (a, b) {
var adown = a.nodeType === 9 ? a.documentElement : a,
bup = b && b.parentNode;
return a === bup || !!(bup && bup.nodeType === 1 && (
adown.contains ?
adown.contains(bup) :
a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16
));
} :
function (a, b) {
if (b) {
while ((b = b.parentNode)) {
if (b === a) {
return true;
}
}
}
return false;
};
/* Sorting
---------------------------------------------------------------------- */
// Document order sorting
sortOrder = hasCompare ?
function (a, b) {
// Flag for duplicate removal
if (a === b) {
hasDuplicate = true;
return 0;
}
// Sort on method existence if only one input has compareDocumentPosition
var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
if (compare) {
return compare;
}
// Calculate position if both inputs belong to the same document
compare = (a.ownerDocument || a) === (b.ownerDocument || b) ?
a.compareDocumentPosition(b) :
// Otherwise we know they are disconnected
1;
// Disconnected nodes
if (compare & 1 ||
(!support.sortDetached && b.compareDocumentPosition(a) === compare)) {
// Choose the first element that is related to our preferred document
if (a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a)) {
return -1;
}
if (b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b)) {
return 1;
}
// Maintain original order
return sortInput ?
(indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) :
0;
}
return compare & 4 ? -1 : 1;
} :
function (a, b) {
// Exit early if the nodes are identical
if (a === b) {
hasDuplicate = true;
return 0;
}
var cur,
i = 0,
aup = a.parentNode,
bup = b.parentNode,
ap = [a],
bp = [b];
// Parentless nodes are either documents or disconnected
if (!aup || !bup) {
return a === doc ? -1 :
b === doc ? 1 :
aup ? -1 :
bup ? 1 :
sortInput ?
(indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) :
0;
// If the nodes are siblings, we can do a quick check
} else if (aup === bup) {
return siblingCheck(a, b);
}
// Otherwise we need full lists of their ancestors for comparison
cur = a;
while ((cur = cur.parentNode)) {
ap.unshift(cur);
}
cur = b;
while ((cur = cur.parentNode)) {
bp.unshift(cur);
}
// Walk down the tree looking for a discrepancy
while (ap[i] === bp[i]) {
i++;
}
return i ?
// Do a sibling check if the nodes have a common ancestor
siblingCheck(ap[i], bp[i]) :
// Otherwise nodes in our document sort first
ap[i] === preferredDoc ? -1 :
bp[i] === preferredDoc ? 1 :
0;
};
return doc;
};
Sizzle.matches = function (expr, elements) {
return Sizzle(expr, null, null, elements);
};
Sizzle.matchesSelector = function (elem, expr) {
// Set document vars if needed
if ((elem.ownerDocument || elem) !== document) {
setDocument(elem);
}
// Make sure that attribute selectors are quoted
expr = expr.replace(rattributeQuotes, "='$1']");
if (support.matchesSelector && documentIsHTML &&
(!rbuggyMatches || !rbuggyMatches.test(expr)) &&
(!rbuggyQSA || !rbuggyQSA.test(expr))) {
try {
var ret = matches.call(elem, expr);
// IE 9's matchesSelector returns false on disconnected nodes
if (ret || support.disconnectedMatch ||
// As well, disconnected nodes are said to be in a document
// fragment in IE 9
elem.document && elem.document.nodeType !== 11) {
return ret;
}
} catch (e) { }
}
return Sizzle(expr, document, null, [elem]).length > 0;
};
Sizzle.contains = function (context, elem) {
// Set document vars if needed
if ((context.ownerDocument || context) !== document) {
setDocument(context);
}
return contains(context, elem);
};
Sizzle.attr = function (elem, name) {
// Set document vars if needed
if ((elem.ownerDocument || elem) !== document) {
setDocument(elem);
}
var fn = Expr.attrHandle[name.toLowerCase()],
// Don't get fooled by Object.prototype properties (jQuery #13807)
val = fn && hasOwn.call(Expr.attrHandle, name.toLowerCase()) ?
fn(elem, name, !documentIsHTML) :
undefined;
return val !== undefined ?
val :
support.attributes || !documentIsHTML ?
elem.getAttribute(name) :
(val = elem.getAttributeNode(name)) && val.specified ?
val.value :
null;
};
Sizzle.error = function (msg) {
throw new Error("Syntax error, unrecognized expression: " + msg);
};
/**
* Document sorting and removing duplicates
* @param {ArrayLike} results
*/
Sizzle.uniqueSort = function (results) {
var elem,
duplicates = [],
j = 0,
i = 0;
// Unless we *know* we can detect duplicates, assume their presence
hasDuplicate = !support.detectDuplicates;
sortInput = !support.sortStable && results.slice(0);
results.sort(sortOrder);
if (hasDuplicate) {
while ((elem = results[i++])) {
if (elem === results[i]) {
j = duplicates.push(i);
}
}
while (j--) {
results.splice(duplicates[j], 1);
}
}
// Clear input after sorting to release objects
// See https://github.com/jquery/sizzle/pull/225
sortInput = null;
return results;
};
/**
* Utility function for retrieving the text value of an array of DOM nodes
* @param {Array|Element} elem
*/
getText = Sizzle.getText = function (elem) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if (!nodeType) {
// If no nodeType, this is expected to be an array
while ((node = elem[i++])) {
// Do not traverse comment nodes
ret += getText(node);
}
} else if (nodeType === 1 || nodeType === 9 || nodeType === 11) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if (typeof elem.textContent === "string") {
return elem.textContent;
} else {
// Traverse its children
for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
ret += getText(elem);
}
}
} else if (nodeType === 3 || nodeType === 4) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
Expr = Sizzle.selectors = {
// Can be adjusted by the user
cacheLength: 50,
createPseudo: markFunction,
match: matchExpr,
attrHandle: {},
find: {},
relative: {
">": { dir: "parentNode", first: true },
" ": { dir: "parentNode" },
"+": { dir: "previousSibling", first: true },
"~": { dir: "previousSibling" }
},
preFilter: {
"ATTR": function (match) {
match[1] = match[1].replace(runescape, funescape);
// Move the given value to match[3] whether quoted or unquoted
match[3] = (match[3] || match[4] || match[5] || "").replace(runescape, funescape);
if (match[2] === "~=") {
match[3] = " " + match[3] + " ";
}
return match.slice(0, 4);
},
"CHILD": function (match) {
/* matches from matchExpr["CHILD"]
1 type (only|nth|...)
2 what (child|of-type)
3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
4 xn-component of xn+y argument ([+-]?\d*n|)
5 sign of xn-component
6 x of xn-component
7 sign of y-component
8 y of y-component
*/
match[1] = match[1].toLowerCase();
if (match[1].slice(0, 3) === "nth") {
// nth-* requires argument
if (!match[3]) {
Sizzle.error(match[0]);
}
// numeric x and y parameters for Expr.filter.CHILD
// remember that false/true cast respectively to 0/1
match[4] = +(match[4] ? match[5] + (match[6] || 1) : 2 * (match[3] === "even" || match[3] === "odd"));
match[5] = +((match[7] + match[8]) || match[3] === "odd");
// other types prohibit arguments
} else if (match[3]) {
Sizzle.error(match[0]);
}
return match;
},
"PSEUDO": function (match) {
var excess,
unquoted = !match[6] && match[2];
if (matchExpr["CHILD"].test(match[0])) {
return null;
}
// Accept quoted arguments as-is
if (match[3]) {
match[2] = match[4] || match[5] || "";
// Strip excess characters from unquoted arguments
} else if (unquoted && rpseudo.test(unquoted) &&
// Get excess from tokenize (recursively)
(excess = tokenize(unquoted, true)) &&
// advance to the next closing parenthesis
(excess = unquoted.indexOf(")", unquoted.length - excess) - unquoted.length)) {
// excess is a negative index
match[0] = match[0].slice(0, excess);
match[2] = unquoted.slice(0, excess);
}
// Return only captures needed by the pseudo filter method (type and argument)
return match.slice(0, 3);
}
},
filter: {
"TAG": function (nodeNameSelector) {
var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase();
return nodeNameSelector === "*" ?
function () { return true; } :
function (elem) {
return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
};
},
"CLASS": function (className) {
var pattern = classCache[className + " "];
return pattern ||
(pattern = new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)")) &&
classCache(className, function (elem) {
return pattern.test(typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "");
});
},
"ATTR": function (name, operator, check) {
return function (elem) {
var result = Sizzle.attr(elem, name);
if (result == null) {
return operator === "!=";
}
if (!operator) {
return true;
}
result += "";
return operator === "=" ? result === check :
operator === "!=" ? result !== check :
operator === "^=" ? check && result.indexOf(check) === 0 :
operator === "*=" ? check && result.indexOf(check) > -1 :
operator === "$=" ? check && result.slice(-check.length) === check :
operator === "~=" ? (" " + result + " ").indexOf(check) > -1 :
operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" :
false;
};
},
"CHILD": function (type, what, argument, first, last) {
var simple = type.slice(0, 3) !== "nth",
forward = type.slice(-4) !== "last",
ofType = what === "of-type";
return first === 1 && last === 0 ?
// Shortcut for :nth-*(n)
function (elem) {
return !!elem.parentNode;
} :
function (elem, context, xml) {
var cache, outerCache, node, diff, nodeIndex, start,
dir = simple !== forward ? "nextSibling" : "previousSibling",
parent = elem.parentNode,
name = ofType && elem.nodeName.toLowerCase(),
useCache = !xml && !ofType;
if (parent) {
// :(first|last|only)-(child|of-type)
if (simple) {
while (dir) {
node = elem;
while ((node = node[dir])) {
if (ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) {
return false;
}
}
// Reverse direction for :only-* (if we haven't yet done so)
start = dir = type === "only" && !start && "nextSibling";
}
return true;
}
start = [forward ? parent.firstChild : parent.lastChild];
// non-xml :nth-child(...) stores cache data on `parent`
if (forward && useCache) {
// Seek `elem` from a previously-cached index
outerCache = parent[expando] || (parent[expando] = {});
cache = outerCache[type] || [];
nodeIndex = cache[0] === dirruns && cache[1];
diff = cache[0] === dirruns && cache[2];
node = nodeIndex && parent.childNodes[nodeIndex];
while ((node = ++nodeIndex && node && node[dir] ||
// Fallback to seeking `elem` from the start
(diff = nodeIndex = 0) || start.pop())) {
// When found, cache indexes on `parent` and break
if (node.nodeType === 1 && ++diff && node === elem) {
outerCache[type] = [dirruns, nodeIndex, diff];
break;
}
}
// Use previously-cached element index if available
} else if (useCache && (cache = (elem[expando] || (elem[expando] = {}))[type]) && cache[0] === dirruns) {
diff = cache[1];
// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
} else {
// Use the same loop as above to seek `elem` from the start
while ((node = ++nodeIndex && node && node[dir] ||
(diff = nodeIndex = 0) || start.pop())) {
if ((ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) && ++diff) {
// Cache the index of each encountered element
if (useCache) {
(node[expando] || (node[expando] = {}))[type] = [dirruns, diff];
}
if (node === elem) {
break;
}
}
}
}
// Incorporate the offset, then check against cycle size
diff -= last;
return diff === first || (diff % first === 0 && diff / first >= 0);
}
};
},
"PSEUDO": function (pseudo, argument) {
// pseudo-class names are case-insensitive
// http://www.w3.org/TR/selectors/#pseudo-classes
// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
// Remember that setFilters inherits from pseudos
var args,
fn = Expr.pseudos[pseudo] || Expr.setFilters[pseudo.toLowerCase()] ||
Sizzle.error("unsupported pseudo: " + pseudo);
// The user may use createPseudo to indicate that
// arguments are needed to create the filter function
// just as Sizzle does
if (fn[expando]) {
return fn(argument);
}
// But maintain support for old signatures
if (fn.length > 1) {
args = [pseudo, pseudo, "", argument];
return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase()) ?
markFunction(function (seed, matches) {
var idx,
matched = fn(seed, argument),
i = matched.length;
while (i--) {
idx = indexOf.call(seed, matched[i]);
seed[idx] = !(matches[idx] = matched[i]);
}
}) :
function (elem) {
return fn(elem, 0, args);
};
}
return fn;
}
},
pseudos: {
// Potentially complex pseudos
"not": markFunction(function (selector) {
// Trim the selector passed to compile
// to avoid treating leading and trailing
// spaces as combinators
var input = [],
results = [],
matcher = compile(selector.replace(rtrim, "$1"));
return matcher[expando] ?
markFunction(function (seed, matches, context, xml) {
var elem,
unmatched = matcher(seed, null, xml, []),
i = seed.length;
// Match elements unmatched by `matcher`
while (i--) {
if ((elem = unmatched[i])) {
seed[i] = !(matches[i] = elem);
}
}
}) :
function (elem, context, xml) {
input[0] = elem;
matcher(input, null, xml, results);
return !results.pop();
};
}),
"has": markFunction(function (selector) {
return function (elem) {
return Sizzle(selector, elem).length > 0;
};
}),
"contains": markFunction(function (text) {
text = text.replace(runescape, funescape);
return function (elem) {
return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1;
};
}),
// "Whether an element is represented by a :lang() selector
// is based solely on the element's language value
// being equal to the identifier C,
// or beginning with the identifier C immediately followed by "-".
// The matching of C against the element's language value is performed case-insensitively.
// The identifier C does not have to be a valid language name."
// http://www.w3.org/TR/selectors/#lang-pseudo
"lang": markFunction(function (lang) {
// lang value must be a valid identifier
if (!ridentifier.test(lang || "")) {
Sizzle.error("unsupported lang: " + lang);
}
lang = lang.replace(runescape, funescape).toLowerCase();
return function (elem) {
var elemLang;
do {
if ((elemLang = documentIsHTML ?
elem.lang :
elem.getAttribute("xml:lang") || elem.getAttribute("lang"))) {
elemLang = elemLang.toLowerCase();
return elemLang === lang || elemLang.indexOf(lang + "-") === 0;
}
} while ((elem = elem.parentNode) && elem.nodeType === 1);
return false;
};
}),
// Miscellaneous
"target": function (elem) {
var hash = window.location && window.location.hash;
return hash && hash.slice(1) === elem.id;
},
"root": function (elem) {
return elem === docElem;
},
"focus": function (elem) {
return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
},
// Boolean properties
"enabled": function (elem) {
return elem.disabled === false;
},
"disabled": function (elem) {
return elem.disabled === true;
},
"checked": function (elem) {
// In CSS3, :checked should return both checked and selected elements
// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
var nodeName = elem.nodeName.toLowerCase();
return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
},
"selected": function (elem) {
// Accessing this property makes selected-by-default
// options in Safari work properly
if (elem.parentNode) {
elem.parentNode.selectedIndex;
}
return elem.selected === true;
},
// Contents
"empty": function (elem) {
// http://www.w3.org/TR/selectors/#empty-pseudo
// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
// but not by others (comment: 8; processing instruction: 7; etc.)
// nodeType < 6 works because attributes (2) do not appear as children
for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
if (elem.nodeType < 6) {
return false;
}
}
return true;
},
"parent": function (elem) {
return !Expr.pseudos["empty"](elem);
},
// Element/input types
"header": function (elem) {
return rheader.test(elem.nodeName);
},
"input": function (elem) {
return rinputs.test(elem.nodeName);
},
"button": function (elem) {
var name = elem.nodeName.toLowerCase();
return name === "input" && elem.type === "button" || name === "button";
},
"text": function (elem) {
var attr;
return elem.nodeName.toLowerCase() === "input" &&
elem.type === "text" &&
// Support: IE<8
// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
((attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text");
},
// Position-in-collection
"first": createPositionalPseudo(function () {
return [0];
}),
"last": createPositionalPseudo(function (matchIndexes, length) {
return [length - 1];
}),
"eq": createPositionalPseudo(function (matchIndexes, length, argument) {
return [argument < 0 ? argument + length : argument];
}),
"even": createPositionalPseudo(function (matchIndexes, length) {
var i = 0;
for (; i < length; i += 2) {
matchIndexes.push(i);
}
return matchIndexes;
}),
"odd": createPositionalPseudo(function (matchIndexes, length) {
var i = 1;
for (; i < length; i += 2) {
matchIndexes.push(i);
}
return matchIndexes;
}),
"lt": createPositionalPseudo(function (matchIndexes, length, argument) {
var i = argument < 0 ? argument + length : argument;
for (; --i >= 0;) {
matchIndexes.push(i);
}
return matchIndexes;
}),
"gt": createPositionalPseudo(function (matchIndexes, length, argument) {
var i = argument < 0 ? argument + length : argument;
for (; ++i < length;) {
matchIndexes.push(i);
}
return matchIndexes;
})
}
};
Expr.pseudos["nth"] = Expr.pseudos["eq"];
// Add button/input type pseudos
for (i in { radio: true, checkbox: true, file: true, password: true, image: true }) {
Expr.pseudos[i] = createInputPseudo(i);
}
for (i in { submit: true, reset: true }) {
Expr.pseudos[i] = createButtonPseudo(i);
}
// Easy API for creating new setFilters
function setFilters() { }
setFilters.prototype = Expr.filters = Expr.pseudos;
Expr.setFilters = new setFilters();
tokenize = Sizzle.tokenize = function (selector, parseOnly) {
var matched, match, tokens, type,
soFar, groups, preFilters,
cached = tokenCache[selector + " "];
if (cached) {
return parseOnly ? 0 : cached.slice(0);
}
soFar = selector;
groups = [];
preFilters = Expr.preFilter;
while (soFar) {
// Comma and first run
if (!matched || (match = rcomma.exec(soFar))) {
if (match) {
// Don't consume trailing commas as valid
soFar = soFar.slice(match[0].length) || soFar;
}
groups.push((tokens = []));
}
matched = false;
// Combinators
if ((match = rcombinators.exec(soFar))) {
matched = match.shift();
tokens.push({
value: matched,
// Cast descendant combinators to space
type: match[0].replace(rtrim, " ")
});
soFar = soFar.slice(matched.length);
}
// Filters
for (type in Expr.filter) {
if ((match = matchExpr[type].exec(soFar)) && (!preFilters[type] ||
(match = preFilters[type](match)))) {
matched = match.shift();
tokens.push({
value: matched,
type: type,
matches: match
});
soFar = soFar.slice(matched.length);
}
}
if (!matched) {
break;
}
}
// Return the length of the invalid excess
// if we're just parsing
// Otherwise, throw an error or return tokens
return parseOnly ?
soFar.length :
soFar ?
Sizzle.error(selector) :
// Cache the tokens
tokenCache(selector, groups).slice(0);
};
function toSelector(tokens) {
var i = 0,
len = tokens.length,
selector = "";
for (; i < len; i++) {
selector += tokens[i].value;
}
return selector;
}
function addCombinator(matcher, combinator, base) {
var dir = combinator.dir,
checkNonElements = base && dir === "parentNode",
doneName = done++;
return combinator.first ?
// Check against closest ancestor/preceding element
function (elem, context, xml) {
while ((elem = elem[dir])) {
if (elem.nodeType === 1 || checkNonElements) {
return matcher(elem, context, xml);
}
}
} :
// Check against all ancestor/preceding elements
function (elem, context, xml) {
var oldCache, outerCache,
newCache = [dirruns, doneName];
// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
if (xml) {
while ((elem = elem[dir])) {
if (elem.nodeType === 1 || checkNonElements) {
if (matcher(elem, context, xml)) {
return true;
}
}
}
} else {
while ((elem = elem[dir])) {
if (elem.nodeType === 1 || checkNonElements) {
outerCache = elem[expando] || (elem[expando] = {});
if ((oldCache = outerCache[dir]) &&
oldCache[0] === dirruns && oldCache[1] === doneName) {
// Assign to newCache so results back-propagate to previous elements
return (newCache[2] = oldCache[2]);
} else {
// Reuse newcache so results back-propagate to previous elements
outerCache[dir] = newCache;
// A match means we're done; a fail means we have to keep checking
if ((newCache[2] = matcher(elem, context, xml))) {
return true;
}
}
}
}
}
};
}
function elementMatcher(matchers) {
return matchers.length > 1 ?
function (elem, context, xml) {
var i = matchers.length;
while (i--) {
if (!matchers[i](elem, context, xml)) {
return false;
}
}
return true;
} :
matchers[0];
}
function multipleContexts(selector, contexts, results) {
var i = 0,
len = contexts.length;
for (; i < len; i++) {
Sizzle(selector, contexts[i], results);
}
return results;
}
function condense(unmatched, map, filter, context, xml) {
var elem,
newUnmatched = [],
i = 0,
len = unmatched.length,
mapped = map != null;
for (; i < len; i++) {
if ((elem = unmatched[i])) {
if (!filter || filter(elem, context, xml)) {
newUnmatched.push(elem);
if (mapped) {
map.push(i);
}
}
}
}
return newUnmatched;
}
function setMatcher(preFilter, selector, matcher, postFilter, postFinder, postSelector) {
if (postFilter && !postFilter[expando]) {
postFilter = setMatcher(postFilter);
}
if (postFinder && !postFinder[expando]) {
postFinder = setMatcher(postFinder, postSelector);
}
return markFunction(function (seed, results, context, xml) {
var temp, i, elem,
preMap = [],
postMap = [],
preexisting = results.length,
// Get initial elements from seed or context
elems = seed || multipleContexts(selector || "*", context.nodeType ? [context] : context, []),
// Prefilter to get matcher input, preserving a map for seed-results synchronization
matcherIn = preFilter && (seed || !selector) ?
condense(elems, preMap, preFilter, context, xml) :
elems,
matcherOut = matcher ?
// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
postFinder || (seed ? preFilter : preexisting || postFilter) ?
// ...intermediate processing is necessary
[] :
// ...otherwise use results directly
results :
matcherIn;
// Find primary matches
if (matcher) {
matcher(matcherIn, matcherOut, context, xml);
}
// Apply postFilter
if (postFilter) {
temp = condense(matcherOut, postMap);
postFilter(temp, [], context, xml);
// Un-match failing elements by moving them back to matcherIn
i = temp.length;
while (i--) {
if ((elem = temp[i])) {
matcherOut[postMap[i]] = !(matcherIn[postMap[i]] = elem);
}
}
}
if (seed) {
if (postFinder || preFilter) {
if (postFinder) {
// Get the final matcherOut by condensing this intermediate into postFinder contexts
temp = [];
i = matcherOut.length;
while (i--) {
if ((elem = matcherOut[i])) {
// Restore matcherIn since elem is not yet a final match
temp.push((matcherIn[i] = elem));
}
}
postFinder(null, (matcherOut = []), temp, xml);
}
// Move matched elements from seed to results to keep them synchronized
i = matcherOut.length;
while (i--) {
if ((elem = matcherOut[i]) &&
(temp = postFinder ? indexOf.call(seed, elem) : preMap[i]) > -1) {
seed[temp] = !(results[temp] = elem);
}
}
}
// Add elements to results, through postFinder if defined
} else {
matcherOut = condense(
matcherOut === results ?
matcherOut.splice(preexisting, matcherOut.length) :
matcherOut
);
if (postFinder) {
postFinder(null, results, matcherOut, xml);
} else {
push.apply(results, matcherOut);
}
}
});
}
function matcherFromTokens(tokens) {
var checkContext, matcher, j,
len = tokens.length,
leadingRelative = Expr.relative[tokens[0].type],
implicitRelative = leadingRelative || Expr.relative[" "],
i = leadingRelative ? 1 : 0,
// The foundational matcher ensures that elements are reachable from top-level context(s)
matchContext = addCombinator(function (elem) {
return elem === checkContext;
}, implicitRelative, true),
matchAnyContext = addCombinator(function (elem) {
return indexOf.call(checkContext, elem) > -1;
}, implicitRelative, true),
matchers = [function (elem, context, xml) {
return (!leadingRelative && (xml || context !== outermostContext)) || (
(checkContext = context).nodeType ?
matchContext(elem, context, xml) :
matchAnyContext(elem, context, xml));
}];
for (; i < len; i++) {
if ((matcher = Expr.relative[tokens[i].type])) {
matchers = [addCombinator(elementMatcher(matchers), matcher)];
} else {
matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches);
// Return special upon seeing a positional matcher
if (matcher[expando]) {
// Find the next relative operator (if any) for proper handling
j = ++i;
for (; j < len; j++) {
if (Expr.relative[tokens[j].type]) {
break;
}
}
return setMatcher(
i > 1 && elementMatcher(matchers),
i > 1 && toSelector(
// If the preceding token was a descendant combinator, insert an implicit any-element `*`
tokens.slice(0, i - 1).concat({ value: tokens[i - 2].type === " " ? "*" : "" })
).replace(rtrim, "$1"),
matcher,
i < j && matcherFromTokens(tokens.slice(i, j)),
j < len && matcherFromTokens((tokens = tokens.slice(j))),
j < len && toSelector(tokens)
);
}
matchers.push(matcher);
}
}
return elementMatcher(matchers);
}
function matcherFromGroupMatchers(elementMatchers, setMatchers) {
var bySet = setMatchers.length > 0,
byElement = elementMatchers.length > 0,
superMatcher = function (seed, context, xml, results, outermost) {
var elem, j, matcher,
matchedCount = 0,
i = "0",
unmatched = seed && [],
setMatched = [],
contextBackup = outermostContext,
// We must always have either seed elements or outermost context
elems = seed || byElement && Expr.find["TAG"]("*", outermost),
// Use integer dirruns iff this is the outermost matcher
dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
len = elems.length;
if (outermost) {
outermostContext = context !== document && context;
}
// Add elements passing elementMatchers directly to results
// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
// Support: IE<9, Safari
// Tolerate NodeList properties (IE: "length"; Safari: abcabc123 abc 123 text 1CHOPtext 2 text 1 text 2 text 1 text 2 a b | [a x| a b
' + html;
target.removeChild(target.firstChild);
} catch (ex) {
// IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p
DomQuery('').html('
' + html).contents().slice(1).appendTo(target);
}
return html;
});
} else {
elm.html(html);
}
},
/**
* Returns the outer HTML of an element.
*
* @method getOuterHTML
* @param {String/Element} elm Element ID or element object to get outer HTML from.
* @return {String} Outer HTML string.
* @example
* tinymce.DOM.getOuterHTML(editorElement);
* tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
*/
getOuterHTML: function (elm) {
elm = this.get(elm);
// Older FF doesn't have outerHTML 3.6 is still used by some orgaizations
return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : DomQuery('').append(DomQuery(elm).clone()).html();
},
/**
* Sets the specified outer HTML on an element or elements.
*
* @method setOuterHTML
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on.
* @param {Object} html HTML code to set as outer value for the element.
* @example
* // Sets the outer HTML of all paragraphs in the active editor
* tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '
name = node.nodeName.toLowerCase();
if (elements && elements[name]) {
// Ignore single BR elements in blocks like
|]
if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') {
return;
}
if (hasContentEditableFalseParent(container) || isCaretContainer(container)) {
return;
}
// Don't walk into elements that doesn't have any child nodes like a IMG
if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
// Walk the DOM to find a text node to place the caret at or a BR
node = container;
walker = new TreeWalker(container, body);
do {
if (isContentEditableFalse(node) || isCaretContainer(node)) {
normalized = false;
break;
}
// Found a text node use that position
if (node.nodeType === 3 && node.nodeValue.length > 0) {
offset = directionLeft ? 0 : node.nodeValue.length;
container = node;
normalized = true;
break;
}
// Found a BR/IMG element that we can place the caret before
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) {
offset = dom.nodeIndex(node);
container = node.parentNode;
// Put caret after image when moving the end point
if (node.nodeName == "IMG" && !directionLeft) {
offset++;
}
normalized = true;
break;
}
} while ((node = (directionLeft ? walker.next() : walker.prev())));
}
}
}
// Lean the caret to the left if possible
if (collapsed) {
// So this: x|x
// Becomes: x|x
// Seems that only gecko has issues with this
if (container.nodeType === 3 && offset === 0) {
findTextNodeRelative(true);
}
// Lean left into empty inline elements when the caret is before a BR
// So this: |
// Becomes: |
// Seems that only gecko has issues with this.
// Special edge case for
if (elements[node.name]) {
return false;
}
// Keep bookmark nodes and name attribute like
i = node.attributes.length;
while (i--) {
name = node.attributes[i].name;
if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
return false;
}
}
}
// Keep comments
if (node.type === 8) {
return false;
}
// Keep non whitespace text nodes
if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) {
return false;
}
// Keep whitespace preserve elements
if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) {
return false;
}
} while ((node = walk(node, self)));
}
return true;
},
/**
* Walks to the next or previous node and returns that node or null if it wasn't found.
*
* @method walk
* @param {Boolean} prev Optional previous node state defaults to false.
* @return {tinymce.html.Node} Node that is next to or previous of the current node.
*/
walk: function (prev) {
return walk(this, null, prev);
}
};
/**
* Creates a node of a specific type.
*
* @static
* @method create
* @param {String} name Name of the node type to create for example "b" or "#text".
* @param {Object} attrs Name/value collection of attributes that will be applied to elements.
*/
Node.create = function (name, attrs) {
var node, attrName;
// Create node
node = new Node(name, typeLookup[name] || 1);
// Add attributes if needed
if (attrs) {
for (attrName in attrs) {
node.attr(attrName, attrs[attrName]);
}
}
return node;
};
return Node;
}
);
/**
* SaxParser.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/*eslint max-depth:[2, 9] */
/**
* This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
* always execute the events in the right order for tag soup code like . It will also remove elements
* and attributes that doesn't fit the schema if the validate setting is enabled.
*
* @example
* var parser = new tinymce.html.SaxParser({
* validate: true,
*
* comment: function(text) {
* console.log('Comment:', text);
* },
*
* cdata: function(text) {
* console.log('CDATA:', text);
* },
*
* text: function(text, raw) {
* console.log('Text:', text, 'Raw:', raw);
* },
*
* start: function(name, attrs, empty) {
* console.log('Start:', name, attrs, empty);
* },
*
* end: function(name) {
* console.log('End:', name);
* },
*
* pi: function(name, text) {
* console.log('PI:', name, text);
* },
*
* doctype: function(text) {
* console.log('DocType:', text);
* }
* }, schema);
* @class tinymce.html.SaxParser
* @version 3.4
*/
define(
'tinymce.core.html.SaxParser',
[
"tinymce.core.html.Schema",
"tinymce.core.html.Entities",
"tinymce.core.util.Tools"
],
function (Schema, Entities, Tools) {
var each = Tools.each;
var isValidPrefixAttrName = function (name) {
return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0;
};
/**
* Returns the index of the end tag for a specific start tag. This can be
* used to skip all children of a parent element from being processed.
*
* @private
* @method findEndTag
* @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
* @param {String} html HTML string to find the end tag in.
* @param {Number} startIndex Indext to start searching at should be after the start tag.
* @return {Number} Index of the end tag.
*/
function findEndTag(schema, html, startIndex) {
var count = 1, index, matches, tokenRegExp, shortEndedElements;
shortEndedElements = schema.getShortEndedElements();
tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
tokenRegExp.lastIndex = index = startIndex;
while ((matches = tokenRegExp.exec(html))) {
index = tokenRegExp.lastIndex;
if (matches[1] === '/') { // End element
count--;
} else if (!matches[1]) { // Start element
if (matches[2] in shortEndedElements) {
continue;
}
count++;
}
if (count === 0) {
break;
}
}
return index;
}
/**
* Constructs a new SaxParser instance.
*
* @constructor
* @method SaxParser
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
*/
function SaxParser(settings, schema) {
var self = this;
function noop() { }
settings = settings || {};
self.schema = schema = schema || new Schema();
if (settings.fix_self_closing !== false) {
settings.fix_self_closing = true;
}
// Add handler functions from settings and setup default handlers
each('comment cdata text start end pi doctype'.split(' '), function (name) {
if (name) {
self[name] = settings[name] || noop;
}
});
/**
* Parses the specified HTML string and executes the callbacks for each item it finds.
*
* @example
* new SaxParser({...}).parse('text');
* @method parse
* @param {String} html Html string to sax parse.
*/
self.parse = function (html) {
var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
var attributesRequired, attributesDefault, attributesForced;
var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;
function processEndTag(name) {
var pos, i;
// Find position of parent of the same type
pos = stack.length;
while (pos--) {
if (stack[pos].name === name) {
break;
}
}
// Found parent
if (pos >= 0) {
// Close all the open elements
for (i = stack.length - 1; i >= pos; i--) {
name = stack[i];
if (name.valid) {
self.end(name.name);
}
}
// Remove the open elements from the stack
stack.length = pos;
}
}
function parseAttribute(match, name, value, val2, val3) {
var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
name = name.toLowerCase();
value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
// Validate name and value pass through all data- attributes
if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) {
attrRule = validAttributesMap[name];
// Find rule by pattern matching
if (!attrRule && validAttributePatterns) {
i = validAttributePatterns.length;
while (i--) {
attrRule = validAttributePatterns[i];
if (attrRule.pattern.test(name)) {
break;
}
}
// No rule matched
if (i === -1) {
attrRule = null;
}
}
// No attribute rule found
if (!attrRule) {
return;
}
// Validate value
if (attrRule.validValues && !(value in attrRule.validValues)) {
return;
}
}
// Block any javascript: urls or non image data uris
if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
var uri = value.replace(trimRegExp, '');
try {
// Might throw malformed URI sequence
uri = decodeURIComponent(uri);
} catch (ex) {
// Fallback to non UTF-8 decoder
uri = unescape(uri);
}
if (scriptUriRegExp.test(uri)) {
return;
}
if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
return;
}
}
// Add attribute to list and map
attrList.map[name] = value;
attrList.push({
name: name,
value: value
});
}
// Precompile RegExps and map objects
tokenRegExp = new RegExp('<(?:' +
'(?:!--([\\w\\W]*?)-->)|' + // Comment
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
'(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|' + // End element
'(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
')', 'g');
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
// Setup lookup tables for empty elements and boolean attributes
shortEndedElements = schema.getShortEndedElements();
selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
fillAttrsMap = schema.getBoolAttrs();
validate = settings.validate;
removeInternalElements = settings.remove_internals;
fixSelfClosing = settings.fix_self_closing;
specialElements = schema.getSpecialElements();
while ((matches = tokenRegExp.exec(html + '>'))) { // Adds and extra '>' to keep regexps from doing catastrofic backtracking on malformed html
// Text
if (index < matches.index) {
self.text(decode(html.substr(index, matches.index - index)));
}
if ((value = matches[6])) { // End element
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
processEndTag(value);
} else if ((value = matches[7])) { // Start element
// Did we consume the extra character then treat it as text
// This handles the case with html like this: "text a html.length) {
self.text(decode(html.substr(matches.index)));
index = matches.index + matches[0].length;
continue;
}
value = value.toLowerCase();
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
if (value.charAt(0) === ':') {
value = value.substr(1);
}
isShortEnded = value in shortEndedElements;
// Is self closing tag for example an
a
b
c
* * @example * var parser = new tinymce.html.DomParser({validate: true}, schema); * var rootNode = parser.parse('x
->x
function trim(rootBlockNode) { if (rootBlockNode) { node = rootBlockNode.firstChild; if (node && node.type == 3) { node.value = node.value.replace(startWhiteSpaceRegExp, ''); } node = rootBlockNode.lastChild; if (node && node.type == 3) { node.value = node.value.replace(endWhiteSpaceRegExp, ''); } } } // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { return; } while (node) { next = node.next; if (node.type == 3 || (node.type == 1 && node.name !== 'p' && !blockElements[node.name] && !node.attr('data-mce-type'))) { if (!rootBlockNode) { // Create a new root block element rootBlockNode = createNode(rootBlockName, 1); rootBlockNode.attr(settings.forced_root_block_attrs); rootNode.insert(rootBlockNode, node); rootBlockNode.append(node); } else { rootBlockNode.append(node); } } else { trim(rootBlockNode); rootBlockNode = null; } node = next; } trim(rootBlockNode); } function createNode(name, type) { var node = new Node(name, type), list; if (name in nodeFilters) { list = matchedNodes[name]; if (list) { list.push(node); } else { matchedNodes[name] = [node]; } } return node; } function removeWhitespaceBefore(node) { var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); for (textNode = node.prev; textNode && textNode.type === 3;) { textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); // Found a text node with non whitespace then trim that and break if (textVal.length > 0) { textNode.value = textVal; return; } textNodeNext = textNode.next; // Fix for bug #7543 where bogus nodes would produce empty // text nodes and these would be removed if a nested list was before it if (textNodeNext) { if (textNodeNext.type == 3 && textNodeNext.value.length) { textNode = textNode.prev; continue; } if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { textNode = textNode.prev; continue; } } sibling = textNode.prev; textNode.remove(); textNode = sibling; } } function cloneAndExcludeBlocks(input) { var name, output = {}; for (name in input) { if (name !== 'li' && name != 'p') { output[name] = input[name]; } } return output; } parser = new SaxParser({ validate: validate, allow_script_urls: settings.allow_script_urls, allow_conditional_comments: settings.allow_conditional_comments, // Exclude P and LI from DOM parsing since it's treated better by the DOM parser self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), cdata: function (text) { node.append(createNode('#cdata', 4)).value = text; }, text: function (text, raw) { var textNode; // Trim all redundant whitespace on non white space elements if (!isInWhiteSpacePreservedElement) { text = text.replace(allWhiteSpaceRegExp, ' '); if (node.lastChild && blockElements[node.lastChild.name]) { text = text.replace(startWhiteSpaceRegExp, ''); } } // Do we need to create the node if (text.length !== 0) { textNode = createNode('#text', 3); textNode.raw = !!raw; node.append(textNode).value = text; } }, comment: function (text) { node.append(createNode('#comment', 8)).value = text; }, pi: function (name, text) { node.append(createNode(name, 7)).value = text; removeWhitespaceBefore(node); }, doctype: function (text) { var newNode; newNode = node.append(createNode('#doctype', 10)); newNode.value = text; removeWhitespaceBefore(node); }, start: function (name, attrs, empty) { var newNode, attrFiltersLen, elementRule, attrName, parent; elementRule = validate ? schema.getElementRule(name) : {}; if (elementRule) { newNode = createNode(elementRule.outputName || name, 1); newNode.attributes = attrs; newNode.shortEnded = empty; node.append(newNode); // Check if node is valid child of the parent node is the child is // unknown we don't collect it since it's probably a custom element parent = children[node.name]; if (parent && children[newNode.name] && !parent[newNode.name]) { invalidChildren.push(newNode); } attrFiltersLen = attributeFilters.length; while (attrFiltersLen--) { attrName = attributeFilters[attrFiltersLen].name; if (attrName in attrs.map) { list = matchedAttributes[attrName]; if (list) { list.push(newNode); } else { matchedAttributes[attrName] = [newNode]; } } } // Trim whitespace before block if (blockElements[name]) { removeWhitespaceBefore(newNode); } // Change current node if the element wasn't empty i.e nota
lastParent = node; while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { lastParent = parent; if (blockElements[parent.name]) { break; } parent = parent.parent; } if (lastParent === parent && settings.padd_empty_with_br !== true) { textNode = new Node('#text', 3); textNode.value = '\u00a0'; node.replace(textNode); } } } }); } if (!settings.allow_unsafe_link_target) { self.addAttributeFilter('href', function (nodes) { var i = nodes.length, node, rel; var rules = 'noopener noreferrer'; function addTargetRules(rel) { rel = removeTargetRules(rel); return rel ? [rel, rules].join(' ') : rules; } function removeTargetRules(rel) { var regExp = new RegExp('(' + rules.replace(' ', '|') + ')', 'g'); if (rel) { rel = Tools.trim(rel.replace(regExp, '')); } return rel ? rel : null; } function toggleTargetRules(rel, isUnsafe) { return isUnsafe ? addTargetRules(rel) : removeTargetRules(rel); } while (i--) { node = nodes[i]; rel = node.attr('rel'); if (node.name === 'a') { node.attr('rel', toggleTargetRules(rel, node.attr('target') == '_blank')); } } }); } // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. if (!settings.allow_html_in_named_anchor) { self.addAttributeFilter('id,name', function (nodes) { var i = nodes.length, sibling, prevSibling, parent, node; while (i--) { node = nodes[i]; if (node.name === 'a' && node.firstChild && !node.attr('href')) { parent = node.parent; // Move children after current node sibling = node.lastChild; do { prevSibling = sibling.prev; parent.insert(sibling, node); sibling = prevSibling; } while (sibling); } } }); } if (settings.fix_list_elements) { self.addNodeFilter('ul,ol', function (nodes) { var i = nodes.length, node, parentNode; while (i--) { node = nodes[i]; parentNode = node.parent; if (parentNode.name === 'ul' || parentNode.name === 'ol') { if (node.prev && node.prev.name === 'li') { node.prev.append(node); } else { var li = new Node('li', 1); li.attr('style', 'list-style-type: none'); node.wrap(li); } } } }); } if (settings.validate && schema.getValidClasses()) { self.addAttributeFilter('class', function (nodes) { var i = nodes.length, node, classList, ci, className, classValue; var validClasses = schema.getValidClasses(), validClassesMap, valid; while (i--) { node = nodes[i]; classList = node.attr('class').split(' '); classValue = ''; for (ci = 0; ci < classList.length; ci++) { className = classList[ci]; valid = false; validClassesMap = validClasses['*']; if (validClassesMap && validClassesMap[className]) { valid = true; } validClassesMap = validClasses[node.name]; if (!valid && validClassesMap && validClassesMap[className]) { valid = true; } if (valid) { if (classValue) { classValue += ' '; } classValue += className; } } if (!classValue.length) { classValue = null; } node.attr('class', classValue); } }); } }; } ); /** * Writer.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. * * @class tinymce.html.Writer * @example * var writer = new tinymce.html.Writer({indent: true}); * var parser = new tinymce.html.SaxParser(writer).parse('
.
*
* @method start
* @param {String} name Name of the element.
* @param {Array} attrs Optional attribute array or undefined if it hasn't any.
* @param {Boolean} empty Optional empty state if the tag should end like
.
*/
start: function (name, attrs, empty) {
var i, l, attr, value;
if (indent && indentBefore[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
html.push('<', name);
if (attrs) {
for (i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i];
html.push(' ', attr.name, '="', encode(attr.value, true), '"');
}
}
if (!empty || htmlOutput) {
html[html.length] = '>';
} else {
html[html.length] = ' />';
}
if (empty && indent && indentAfter[name] && html.length > 0) {
value = html[html.length - 1];
if (value.length > 0 && value !== '\n') {
html.push('\n');
}
}
},
/**
* Writes the a end element such as
text
')); * @class tinymce.html.Serializer * @version 3.4 */ define( 'tinymce.core.html.Serializer', [ "tinymce.core.html.Writer", "tinymce.core.html.Schema" ], function (Writer, Schema) { /** * Constructs a new Serializer instance. * * @constructor * @method Serializer * @param {Object} settings Name/value settings object. * @param {tinymce.html.Schema} schema Schema instance to use. */ return function (settings, schema) { var self = this, writer = new Writer(settings); settings = settings || {}; settings.validate = "validate" in settings ? settings.validate : true; self.schema = schema = schema || new Schema(); self.writer = writer; /** * Serializes the specified node into a string. * * @example * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); * @method serialize * @param {tinymce.html.Node} node Node instance to serialize. * @return {String} String with HTML based on DOM tree. */ self.serialize = function (node) { var handlers, validate; validate = settings.validate; handlers = { // #text 3: function (node) { writer.text(node.value, node.raw); }, // #comment 8: function (node) { writer.comment(node.value); }, // Processing instruction 7: function (node) { writer.pi(node.name, node.value); }, // Doctype 10: function (node) { writer.doctype(node.value); }, // CDATA 4: function (node) { writer.cdata(node.value); }, // Document fragment 11: function (node) { if ((node = node.firstChild)) { do { walk(node); } while ((node = node.next)); } } }; writer.reset(); function walk(node) { var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; if (!handler) { name = node.name; isEmpty = node.shortEnded; attrs = node.attributes; // Sort attributes if (validate && attrs && attrs.length > 1) { sortedAttrs = []; sortedAttrs.map = {}; elementRule = schema.getElementRule(node.name); if (elementRule) { for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { attrName = elementRule.attributesOrder[i]; if (attrName in attrs.map) { attrValue = attrs.map[attrName]; sortedAttrs.map[attrName] = attrValue; sortedAttrs.push({ name: attrName, value: attrValue }); } } for (i = 0, l = attrs.length; i < l; i++) { attrName = attrs[i].name; if (!(attrName in sortedAttrs.map)) { attrValue = attrs.map[attrName]; sortedAttrs.map[attrName] = attrValue; sortedAttrs.push({ name: attrName, value: attrValue }); } } attrs = sortedAttrs; } } writer.start(node.name, attrs, isEmpty); if (!isEmpty) { if ((node = node.firstChild)) { do { walk(node); } while ((node = node.next)); } writer.end(name); } } else { handler(node); } } // Serialize element and treat all non elements as fragments if (node.type == 1 && !settings.inner) { walk(node); } else { handlers[11](node); } return writer.getContent(); }; }; } ); /** * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for * more details and examples on how to use this class. * * @class tinymce.dom.Serializer */ define( 'tinymce.core.dom.Serializer', [ "tinymce.core.dom.DOMUtils", "tinymce.core.html.DomParser", "tinymce.core.html.SaxParser", "tinymce.core.html.Entities", "tinymce.core.html.Serializer", "tinymce.core.html.Node", "tinymce.core.html.Schema", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.text.Zwsp" ], function (DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) { var each = Tools.each, trim = Tools.trim; var DOM = DOMUtils.DOM; /** * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML * but not as the lastChild of the body. So this fix simply removes the last two * BR elements at the end of the document. * * Example of what happens: text becomes texta|c
* p[0]/img[0],before =|
|
]
rng.moveToElementText(rng2.parentElement()); if (rng.compareEndPoints('StartToEnd', rng2) === 0) { rng2.move('character', -1); } rng2.pasteHTML('' + chr + ''); } } catch (ex) { // IE might throw unspecified error so lets ignore it return null; } } else { // Control selection element = rng.item(0); name = element.nodeName; return { name: name, index: findIndex(name, element) }; } } else { element = selection.getNode(); name = element.nodeName; if (name == 'IMG') { return { name: name, index: findIndex(name, element) }; } // W3C method rng2 = normalizeTableCellSelection(rng.cloneRange()); // Insert end marker if (!collapsed) { rng2.collapse(false); rng2.insertNode(dom.create('span', { 'data-mce-type': "bookmark", id: id + '_end', style: styles }, chr)); } rng = normalizeTableCellSelection(rng); rng.collapse(true); rng.insertNode(dom.create('span', { 'data-mce-type': "bookmark", id: id + '_start', style: styles }, chr)); } selection.moveToBookmark({ id: id, keep: 1 }); return { id: id }; }; /** * Restores the selection to the specified bookmark. * * @method moveToBookmark * @param {Object} bookmark Bookmark to restore selection from. * @return {Boolean} true/false if it was successful or not. * @example * // Stores a bookmark of the current selection * var bm = tinymce.activeEditor.selection.getBookmark(); * * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); * * // Restore the selection bookmark * tinymce.activeEditor.selection.moveToBookmark(bm); */ this.moveToBookmark = function (bookmark) { var rng, root, startContainer, endContainer, startOffset, endOffset; function setEndPoint(start) { var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; if (point) { offset = point[0]; // Find container node for (node = root, i = point.length - 1; i >= 1; i--) { children = node.childNodes; if (point[i] > children.length - 1) { return; } node = children[point[i]]; } // Move text offset to best suitable location if (node.nodeType === 3) { offset = Math.min(point[0], node.nodeValue.length); } // Move element offset to best suitable location if (node.nodeType === 1) { offset = Math.min(point[0], node.childNodes.length); } // Set offset within container node if (start) { rng.setStart(node, offset); } else { rng.setEnd(node, offset); } } return true; } function restoreEndPoint(suffix) { var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; if (marker) { node = marker.parentNode; if (suffix == 'start') { if (!keep) { idx = dom.nodeIndex(marker); } else { node = marker.firstChild; idx = 1; } startContainer = endContainer = node; startOffset = endOffset = idx; } else { if (!keep) { idx = dom.nodeIndex(marker); } else { node = marker.firstChild; idx = 1; } endContainer = node; endOffset = idx; } if (!keep) { prev = marker.previousSibling; next = marker.nextSibling; // Remove all marker text nodes Tools.each(Tools.grep(marker.childNodes), function (node) { if (node.nodeType == 3) { node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); } }); // Remove marker but keep children if for example contents where inserted into the marker // Also remove duplicated instances of the marker for example by a // split operation or by WebKit auto split on paste feature while ((marker = dom.get(bookmark.id + '_' + suffix))) { dom.remove(marker, 1); } // If siblings are text nodes then merge them unless it's Opera since it some how removes the node // and we are sniffing since adding a lot of detection code for a browser with 3% of the market // isn't worth the effort. Sorry, Opera but it's just a fact if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { idx = prev.nodeValue.length; prev.appendData(next.nodeValue); dom.remove(next); if (suffix == 'start') { startContainer = endContainer = prev; startOffset = endOffset = idx; } else { endContainer = prev; endOffset = idx; } } } } } function addBogus(node) { // Adds a bogus BR element for empty block elements if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { node.innerHTML = '|
would become this:|
sibling = startContainer.previousSibling; if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { sibling.innerHTML = ''; } else { sibling = null; } startContainer.innerHTML = ''; ieRng.moveToElementText(startContainer.lastChild); ieRng.select(); dom.doc.selection.clear(); startContainer.innerHTML = ''; if (sibling) { sibling.innerHTML = ''; } return; } startOffset = dom.nodeIndex(startContainer); startContainer = startContainer.parentNode; } if (startOffset == endOffset - 1) { try { ctrlElm = startContainer.childNodes[startOffset]; ctrlRng = body.createControlRange(); ctrlRng.addElement(ctrlElm); ctrlRng.select(); // Check if the range produced is on the correct element and is a control range // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 nativeRng = selection.getRng(); if (nativeRng.item && ctrlElm === nativeRng.item(0)) { return; } } catch (ex) { // Ignore } } } // Set start/end point of selection setEndPoint(true); setEndPoint(); // Select the new range and scroll it into view ieRng.select(); }; // Expose range method this.getRangeAt = getRange; } return Selection; } ); /** * Selection.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles text and control selection it's an crossbrowser utility class. * Consult the TinyMCE Wiki API for more details and examples on how to use this class. * * @class tinymce.dom.Selection * @example * // Getting the currently selected node for the active editor * alert(tinymce.activeEditor.selection.getNode().nodeName); */ define( 'tinymce.core.dom.Selection', [ 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.BookmarkManager', 'tinymce.core.dom.ControlSelection', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.ScrollIntoView', 'tinymce.core.dom.TreeWalker', 'tinymce.core.dom.TridentSelection', 'tinymce.core.Env', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function (CaretPosition, BookmarkManager, ControlSelection, NodeType, RangeUtils, ScrollIntoView, TreeWalker, TridentSelection, Env, Zwsp, Tools) { var each = Tools.each, trim = Tools.trim; var isIE = Env.ie; /** * Constructs a new selection instance. * * @constructor * @method Selection * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. * @param {Window} win Window to bind the selection object to. * @param {tinymce.Editor} editor Editor instance of the selection. * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. */ function Selection(dom, win, serializer, editor) { var self = this; self.dom = dom; self.win = win; self.serializer = serializer; self.editor = editor; self.bookmarkManager = new BookmarkManager(self); self.controlSelection = new ControlSelection(self, editor); // No W3C Range support if (!self.win.getSelection) { self.tridentSel = new TridentSelection(self); } } Selection.prototype = { /** * Move the selection cursor range to the specified node and offset. * If there is no node specified it will move it to the first suitable location within the body. * * @method setCursorLocation * @param {Node} node Optional node to put the cursor in. * @param {Number} offset Optional offset from the start of the node to put the cursor at. */ setCursorLocation: function (node, offset) { var self = this, rng = self.dom.createRng(); if (!node) { self._moveEndPoint(rng, self.editor.getBody(), true); self.setRng(rng); } else { rng.setStart(node, offset); rng.setEnd(node, offset); self.setRng(rng); self.collapse(false); } }, /** * Returns the selected contents using the DOM serializer passed in to this class. * * @method getContent * @param {Object} args Optional settings class with for example output format text or html. * @return {String} Selected contents in for example HTML format. * @example * // Alerts the currently selected contents * alert(tinymce.activeEditor.selection.getContent()); * * // Alerts the currently selected contents as plain text * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); */ getContent: function (args) { var self = this, rng = self.getRng(), tmpElm = self.dom.create("body"); var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; args = args || {}; whiteSpaceBefore = whiteSpaceAfter = ''; args.get = true; args.format = args.format || 'html'; args.selection = true; self.editor.fire('BeforeGetContent', args); if (args.format === 'text') { return self.isCollapsed() ? '' : Zwsp.trim(rng.text || (se.toString ? se.toString() : '')); } if (rng.cloneContents) { fragment = rng.cloneContents(); if (fragment) { tmpElm.appendChild(fragment); } } else if (rng.item !== undefined || rng.htmlText !== undefined) { // IE will produce invalid markup if elements are present that // it doesn't understand like custom elements or HTML5 elements. // Adding a BR in front of the contents and then remoiving it seems to fix it though. tmpElm.innerHTML = '[a
]b
->[a]
b
function adjustSelectionToVisibleSelection() { function findSelectionEnd(start, end) { var walker = new TreeWalker(end); for (node = walker.prev2(); node; node = walker.prev2()) { if (node.nodeType == 3 && node.data.length > 0) { return node; } if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { return node; } } } // Adjust selection so that a end container with a end offset of zero is not included in the selection // as this isn't visible to the user. var rng = ed.selection.getRng(); var start = rng.startContainer; var end = rng.endContainer; if (start != end && rng.endOffset === 0) { var newEnd = findSelectionEnd(start, end); var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length; rng.setEnd(newEnd, endOffset); } return rng; } function applyRngStyle(rng, bookmark, nodeSpecific) { var newWrappers = [], wrapName, wrapElm, contentEditable = true; // Setup wrapper element wrapName = format.inline || format.block; wrapElm = dom.create(wrapName); setElementFormat(wrapElm); rangeUtils.walk(rng, function (nodes) { var currentWrapElm; /** * Process a list of nodes wrap them. */ function process(node) { var nodeName, parentName, hasContentEditableState, lastContentEditable; lastContentEditable = contentEditable; nodeName = node.nodeName.toLowerCase(); parentName = node.parentNode.nodeName.toLowerCase(); // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { lastContentEditable = contentEditable; contentEditable = getContentEditable(node) === "true"; hasContentEditableState = true; // We don't want to wrap the container only it's children } // Stop wrapping on br elements if (isEq(nodeName, 'br')) { currentWrapElm = 0; // Remove any br elements when we wrap things if (format.block) { dom.remove(node); } return; } // If node is wrapper type if (format.wrapper && matchNode(node, name, vars)) { currentWrapElm = 0; return; } // Can we rename the block // TODO: Break this if up, too complex if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { node = dom.rename(node, wrapName); setElementFormat(node); newWrappers.push(node); currentWrapElm = 0; return; } // Handle selector patterns if (format.selector) { var found = applyNodeStyle(formatList, node); // Continue processing if a selector match wasn't found and a inline element is defined if (!format.inline || found) { currentWrapElm = 0; return; } } // Is it valid to wrap this item // TODO: Break this if up, too complex if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && !(!nodeSpecific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node) && (!format.inline || !isBlock(node))) { // Start wrapping if (!currentWrapElm) { // Wrap the node currentWrapElm = dom.clone(wrapElm, FALSE); node.parentNode.insertBefore(currentWrapElm, node); newWrappers.push(currentWrapElm); } currentWrapElm.appendChild(node); } else { // Start a new wrapper for possible children currentWrapElm = 0; each(grep(node.childNodes), process); if (hasContentEditableState) { contentEditable = lastContentEditable; // Restore last contentEditable state from stack } // End the last wrapper currentWrapElm = 0; } } // Process siblings from range each(nodes, process); }); // Apply formats to links as well to get the color of the underline to change as well if (format.links === true) { each(newWrappers, function (node) { function process(node) { if (node.nodeName === 'A') { setElementFormat(node, format); } each(grep(node.childNodes), process); } process(node); }); } // Cleanup each(newWrappers, function (node) { var childCount; function getChildCount(node) { var count = 0; each(node.childNodes, function (node) { if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { count++; } }); return count; } function getChildElementNode(root) { var child = false; each(root.childNodes, function (node) { if (isElementNode(node)) { child = node; return false; // break loop } }); return child; } function matchNestedWrapper(node, filter) { do { if (getChildCount(node) !== 1) { break; } node = getChildElementNode(node); if (!node) { break; } else if (filter(node)) { return node; } } while (node); return null; } function mergeStyles(node) { var child, clone; child = getChildElementNode(node); // If child was found and of the same type as the current node if (child && !isBookmarkNode(child) && matchName(child, format)) { clone = dom.clone(child, FALSE); setElementFormat(clone); dom.replace(clone, node, TRUE); dom.remove(child, 1); } return clone || node; } childCount = getChildCount(node); // Remove empty nodes but only if there is multiple wrappers and they are not block // elements so never remove single since that would remove the // current empty block element where the caret is at if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { dom.remove(node, 1); return; } if (format.inline || format.wrapper) { // Merges the current node with it's children of similar type to reduce the number of elements if (!format.exact && childCount === 1) { node = mergeStyles(node); } // Remove/merge children each(formatList, function (format) { // Merge all children of similar type will move styles from child to parent // this: text // will become: text each(dom.select(format.inline, node), function (child) { if (!isElementNode(child)) { return; } removeFormat(format, vars, child, format.exact ? child : null); }); }); // Remove format if direct parent already has the same format if (matchNode(node.parentNode, name, vars)) { if (removeFormat(format, vars, node)) { node = 0; } } // Remove format if any ancestor already has the same format if (format.merge_with_parents) { dom.getParent(node.parentNode, function (parent) { if (matchNode(parent, name, vars)) { if (removeFormat(format, vars, node)) { node = 0; } return TRUE; } }); } // fontSize defines the line height for the whole branch of nested style wrappers, // therefore it should be set on the outermost wrapper if (node && !isBlock(node) && !getStyle(node, 'fontSize')) { var styleNode = matchNestedWrapper(node, hasStyle('fontSize')); if (styleNode) { apply('fontsize', { value: getStyle(styleNode, 'fontSize') }, node); } } // Merge next and previous siblings if they are similar texttext becomes texttext if (node && format.merge_siblings !== false) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } } }); } if (getContentEditable(selection.getNode()) === "false") { node = selection.getNode(); for (var i = 0, l = formatList.length; i < l; i++) { if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) { setElementFormat(node, formatList[i]); return; } } return; } if (format) { if (node) { if (node.nodeType) { if (!applyNodeStyle(formatList, node)) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); applyRngStyle(expandRng(rng, formatList), null, true); } } else { applyRngStyle(node, null, true); } } else { if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { // Obtain selection node before selection is unselected by applyRngStyle() var curSelNode = ed.selection.getNode(); // If the formats have a default block and we can't find a parent block then // start wrapping it with a DIV this is for forced_root_blocks: false // It's kind of a hack but people should be using the default block type P since all desktop editors work that way if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { apply(formatList[0].defaultBlock); } // Apply formatting to selection ed.selection.setRng(adjustSelectionToVisibleSelection()); bookmark = selection.getBookmark(); applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); if (format.styles) { // Colored nodes should be underlined so that the color of the underline matches the text color. if (format.styles.color || format.styles.textDecoration) { walk(curSelNode, processUnderlineAndColor, 'childNodes'); processUnderlineAndColor(curSelNode); } // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882) if (format.styles.backgroundColor) { processChildElements(curSelNode, hasStyle('fontSize'), applyStyle('backgroundColor', replaceVars(format.styles.backgroundColor, vars)) ); } } selection.moveToBookmark(bookmark); moveStart(selection.getRng(TRUE)); ed.nodeChanged(); } else { performCaretAction('apply', name, vars); } } Hooks.postProcess(name, ed); } } /** * Removes the specified format from the current selection or specified node. * * @method remove * @param {String} name Name of format to remove. * @param {Object} vars Optional list of variables to replace within format before removing it. * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. */ function remove(name, vars, node, similar) { var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; // Merges the styles for each node function process(node) { var children, i, l, lastContentEditable, hasContentEditableState; // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { lastContentEditable = contentEditable; contentEditable = getContentEditable(node) === "true"; hasContentEditableState = true; // We don't want to wrap the container only it's children } // Grab the children first since the nodelist might be changed children = grep(node.childNodes); // Process current node if (contentEditable && !hasContentEditableState) { for (i = 0, l = formatList.length; i < l; i++) { if (removeFormat(formatList[i], vars, node, node)) { break; } } } // Process the children if (format.deep) { if (children.length) { for (i = 0, l = children.length; i < l; i++) { process(children[i]); } if (hasContentEditableState) { contentEditable = lastContentEditable; // Restore last contentEditable state from stack } } } } function findFormatRoot(container) { var formatRoot; // Find format root each(getParents(container.parentNode).reverse(), function (parent) { var format; // Find format root element if (!formatRoot && parent.id != '_start' && parent.id != '_end') { // Is the node matching the format we are looking for format = matchNode(parent, name, vars, similar); if (format && format.split !== false) { formatRoot = parent; } } }); return formatRoot; } function wrapAndSplit(formatRoot, container, target, split) { var parent, clone, lastClone, firstClone, i, formatRootParent; // Format root found then clone formats and split it if (formatRoot) { formatRootParent = formatRoot.parentNode; for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { clone = dom.clone(parent, FALSE); for (i = 0; i < formatList.length; i++) { if (removeFormat(formatList[i], vars, clone, clone)) { clone = 0; break; } } // Build wrapper node if (clone) { if (lastClone) { clone.appendChild(lastClone); } if (!firstClone) { firstClone = clone; } lastClone = clone; } } // Never split block elements if the format is mixed if (split && (!format.mixed || !isBlock(formatRoot))) { container = dom.split(formatRoot, container); } // Wrap container in cloned formats if (lastClone) { target.parentNode.insertBefore(lastClone, target); firstClone.appendChild(target); } } return container; } function splitToFormatRoot(container) { return wrapAndSplit(findFormatRoot(container), container, container, true); } function unwrap(start) { var node = dom.get(start ? '_start' : '_end'), out = node[start ? 'firstChild' : 'lastChild']; // If the end is placed within the start the result will be removed // So this checks if the out node is a bookmark node if it is it // checks for another more suitable node if (isBookmarkNode(out)) { out = out[start ? 'firstChild' : 'lastChild']; } // Since dom.remove removes empty text nodes then we need to try to find a better node if (out.nodeType == 3 && out.data.length === 0) { out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; } dom.remove(node, true); return out; } function removeRngStyle(rng) { var startContainer, endContainer; var commonAncestorContainer = rng.commonAncestorContainer; rng = expandRng(rng, formatList, TRUE); if (format.split) { startContainer = getContainer(rng, TRUE); endContainer = getContainer(rng); if (startContainer != endContainer) { // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN // so let's see if we can use the first child instead // This will happen if you triple click a table cell and use remove formatting if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { if (startContainer.nodeName == "TR") { startContainer = startContainer.firstChild.firstChild || startContainer; } else { startContainer = startContainer.firstChild || startContainer; } } // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 if (commonAncestorContainer && /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && isTableCell(endContainer) && endContainer.firstChild) { endContainer = endContainer.firstChild || endContainer; } if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) && !isTableCell(startContainer) && !isTableCell(endContainer)) { startContainer = wrap(startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); splitToFormatRoot(startContainer); startContainer = unwrap(TRUE); return; } // Wrap start/end nodes in span element since these might be cloned/moved startContainer = wrap(startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); endContainer = wrap(endContainer, 'span', { id: '_end', 'data-mce-type': 'bookmark' }); // Split start/end splitToFormatRoot(startContainer); splitToFormatRoot(endContainer); // Unwrap start/end to get real elements again startContainer = unwrap(TRUE); endContainer = unwrap(); } else { startContainer = endContainer = splitToFormatRoot(startContainer); } // Update range positions since they might have changed after the split operations rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; rng.startOffset = nodeIndex(startContainer); rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; rng.endOffset = nodeIndex(endContainer) + 1; } // Remove items between start/end rangeUtils.walk(rng, function (nodes) { each(nodes, function (node) { process(node); // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { removeFormat({ 'deep': false, 'exact': true, 'inline': 'span', 'styles': { 'textDecoration': 'underline' } }, null, node); } }); }); } // Handle node if (node) { if (node.nodeType) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); removeRngStyle(rng); } else { removeRngStyle(node); } return; } if (getContentEditable(selection.getNode()) === "false") { node = selection.getNode(); for (var i = 0, l = formatList.length; i < l; i++) { if (formatList[i].ceFalseOverride) { if (removeFormat(formatList[i], vars, node, node)) { break; } } } return; } if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); // Check if start element still has formatting then we are at: "text|text" // and need to move the start into the next text node if (format.inline && match(name, vars, selection.getStart())) { moveStart(selection.getRng(true)); } ed.nodeChanged(); } else { performCaretAction('remove', name, vars, similar); } } /** * Toggles the specified format on/off. * * @method toggle * @param {String} name Name of format to apply/remove. * @param {Object} vars Optional list of variables to replace within format before applying/removing it. * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. */ function toggle(name, vars, node) { var fmt = get(name); if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { remove(name, vars, node); } else { apply(name, vars, node); } } /** * Return true/false if the specified node has the specified format. * * @method matchNode * @param {Node} node Node to check the format on. * @param {String} name Format name to check. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Boolean} similar Match format that has similar properties. * @return {Object} Returns the format object it matches or undefined if it doesn't match. */ function matchNode(node, name, vars, similar) { var formatList = get(name), format, i, classes; function matchItems(node, format, itemName) { var key, value, items = format[itemName], i; // Custom match if (format.onmatch) { return format.onmatch(node, format, itemName); } // Check all items if (items) { // Non indexed object if (items.length === undef) { for (key in items) { if (items.hasOwnProperty(key)) { if (itemName === 'attributes') { value = dom.getAttrib(node, key); } else { value = getStyle(node, key); } if (similar && !value && !format.exact) { return; } if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { return; } } } } else { // Only one match needed for indexed arrays for (i = 0; i < items.length; i++) { if (itemName === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { return format; } } } } return format; } if (formatList && node) { // Check each format in list for (i = 0; i < formatList.length; i++) { format = formatList[i]; // Name name, attributes, styles and classes if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { // Match classes if ((classes = format.classes)) { for (i = 0; i < classes.length; i++) { if (!dom.hasClass(node, classes[i])) { return; } } } return format; } } } } /** * Matches the current selection or specified node against the specified format name. * * @method match * @param {String} name Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Node} node Optional node to check. * @return {boolean} true/false if the specified selection/node matches the format. */ function match(name, vars, node) { var startNode; function matchParents(node) { var root = dom.getRoot(); if (node === root) { return false; } // Find first node with similar format settings node = dom.getParent(node, function (node) { if (matchesUnInheritedFormatSelector(node, name)) { return true; } return node.parentNode === root || !!matchNode(node, name, vars, true); }); // Do an exact check on the similar format element return matchNode(node, name, vars); } // Check specified node if (node) { return matchParents(node); } // Check selected node node = selection.getNode(); if (matchParents(node)) { return TRUE; } // Check start node if it's different startNode = selection.getStart(); if (startNode != node) { if (matchParents(startNode)) { return TRUE; } } return FALSE; } /** * Matches the current selection against the array of formats and returns a new array with matching formats. * * @method matchAll * @param {Array} names Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @return {Array} Array with matched formats. */ function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}; // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function (node) { var i, name; for (i = 0; i < names.length; i++) { name = names[i]; if (!checkedMap[name] && matchNode(node, name, vars)) { checkedMap[name] = true; matchedFormatNames.push(name); } } }, dom.getRoot()); return matchedFormatNames; } /** * Returns true/false if the specified format can be applied to the current selection or not. It * will currently only check the state for selector formats, it returns true on all other format types. * * @method canApply * @param {String} name Name of format to check. * @return {boolean} true/false if the specified format can be applied to the current selection/node. */ function canApply(name) { var formatList = get(name), startNode, parents, i, x, selector; if (formatList) { startNode = selection.getStart(); parents = getParents(startNode); for (x = formatList.length - 1; x >= 0; x--) { selector = formatList[x].selector; // Format is not selector based then always return TRUE // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line if (!selector || formatList[x].defaultBlock) { return TRUE; } for (i = parents.length - 1; i >= 0; i--) { if (dom.is(parents[i], selector)) { return TRUE; } } } } return FALSE; } /** * Executes the specified callback when the current selection matches the formats or not. * * @method formatChanged * @param {String} formats Comma separated list of formats to check for. * @param {function} callback Callback with state and args when the format is changed/toggled on/off. * @param {Boolean} similar True/false state if the match should handle similar or exact formats. */ function formatChanged(formats, callback, similar) { var currentFormats; // Setup format node change logic if (!formatChangeData) { formatChangeData = {}; currentFormats = {}; ed.on('NodeChange', function (e) { var parents = getParents(e.element), matchedFormats = {}; // Ignore bogus nodes like the tag created by moveStart() parents = Tools.grep(parents, function (node) { return node.nodeType == 1 && !node.getAttribute('data-mce-bogus'); }); // Check for new formats each(formatChangeData, function (callbacks, format) { each(parents, function (node) { if (matchNode(node, format, {}, callbacks.similar)) { if (!currentFormats[format]) { // Execute callbacks each(callbacks, function (callback) { callback(true, { node: node, format: format, parents: parents }); }); currentFormats[format] = callbacks; } matchedFormats[format] = callbacks; return false; } if (matchesUnInheritedFormatSelector(node, format)) { return false; } }); }); // Check if current formats still match each(currentFormats, function (callbacks, format) { if (!matchedFormats[format]) { delete currentFormats[format]; each(callbacks, function (callback) { callback(false, { node: e.element, format: format, parents: parents }); }); } }); }); } // Add format listeners each(formats.split(','), function (format) { if (!formatChangeData[format]) { formatChangeData[format] = []; formatChangeData[format].similar = similar; } formatChangeData[format].push(callback); }); return this; } /** * Returns a preview css text for the specified format. * * @method getCssText * @param {String/Object} format Format to generate preview css text for. * @return {String} Css text for the specified format. * @example * var cssText1 = editor.formatter.getCssText('bold'); * var cssText2 = editor.formatter.getCssText({inline: 'b'}); */ function getCssText(format) { return Preview.getCssText(ed, format); } // Expose to public extend(this, { get: get, register: register, unregister: unregister, apply: apply, remove: remove, toggle: toggle, match: match, matchAll: matchAll, matchNode: matchNode, canApply: canApply, formatChanged: formatChanged, getCssText: getCssText }); // Initialize defaultFormats(); addKeyboardShortcuts(); ed.on('BeforeGetContent', function (e) { if (markCaretContainersBogus && e.format != 'raw') { markCaretContainersBogus(); } }); ed.on('mouseup keydown', function (e) { if (disableCaretContainer) { disableCaretContainer(e); } }); // Private functions /** * Checks if the specified nodes name matches the format inline/block or selector. * * @private * @param {Node} node Node to match against the specified format. * @param {Object} format Format object o match with. * @return {boolean} true/false if the format matches. */ function matchName(node, format) { // Check for inline match if (isEq(node, format.inline)) { return TRUE; } // Check for block match if (isEq(node, format.block)) { return TRUE; } // Check for selector match if (format.selector) { return node.nodeType == 1 && dom.is(node, format.selector); } } /** * Compares two string/nodes regardless of their case. * * @private * @param {String/Node} str1 Node or string to compare. * @param {String/Node} str2 Node or string to compare. * @return {boolean} True/false if they match. */ function isEq(str1, str2) { str1 = str1 || ''; str2 = str2 || ''; str1 = '' + (str1.nodeName || str1); str2 = '' + (str2.nodeName || str2); return str1.toLowerCase() == str2.toLowerCase(); } function processChildElements(node, filter, process) { each(node.childNodes, function (node) { if (isElementNode(node)) { if (filter(node)) { process(node); } if (node.hasChildNodes()) { processChildElements(node, filter, process); } } }); } function isElementNode(node) { return node && node.nodeType === 1 && !isBookmarkNode(node) && !isCaretNode(node) && !NodeType.isBogus(node); } function hasStyle(name) { return Fun.curry(function (name, node) { return !!(node && getStyle(node, name)); }, name); } function applyStyle(name, value) { return Fun.curry(function (name, value, node) { dom.setStyle(node, name, value); }, name, value); } /** * Returns the style by name on the specified node. This method modifies the style * contents to make it more easy to match. This will resolve a few browser issues. * * @private * @param {Node} node to get style from. * @param {String} name Style name to get. * @return {String} Style item value. */ function getStyle(node, name) { return normalizeStyleValue(dom.getStyle(node, name), name); } /** * Normalize style value by name. This method modifies the style contents * to make it more easy to match. This will resolve a few browser issues. * * @private * @param {String} value Value to get style from. * @param {String} name Style name to get. * @return {String} Style item value. */ function normalizeStyleValue(value, name) { // Force the format to hex if (name == 'color' || name == 'backgroundColor') { value = dom.toHex(value); } // Opera will return bold as 700 if (name == 'fontWeight' && value == 700) { value = 'bold'; } // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" if (name == 'fontFamily') { value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); } return '' + value; } /** * Replaces variables in the value. The variable format is %var. * * @private * @param {String} value Value to replace variables in. * @param {Object} vars Name/value array with variables to replace. * @return {String} New value with replaced variables. */ function replaceVars(value, vars) { if (typeof value != "string") { value = value(vars); } else if (vars) { value = value.replace(/%(\w+)/g, function (str, name) { return vars[name] || str; }); } return value; } function isWhiteSpaceNode(node) { return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); } function wrap(node, name, attrs) { var wrapper = dom.create(name, attrs); node.parentNode.insertBefore(wrapper, node); wrapper.appendChild(node); return wrapper; } /** * Expands the specified range like object to depending on format. * * For example on block formats it will move the start/end position * to the beginning of the current block. * * @private * @param {Object} rng Range like object. * @param {Array} format Array with formats to expand by. * @param {Boolean} remove * @return {Object} Expanded range like object. */ function expandRng(rng, format, remove) { var lastIdx, leaf, endPoint, startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; // This function walks up the tree if there is no siblings before/after the node function findParentContainer(start) { var container, parent, sibling, siblingName, root; container = parent = start ? startContainer : endContainer; siblingName = start ? 'previousSibling' : 'nextSibling'; root = dom.getRoot(); function isBogusBr(node) { return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; } // If it's a text node and the offset is inside the text if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { return container; } } /*eslint no-constant-condition:0 */ while (true) { // Stop expanding on block elements if (!format[0].block_expand && isBlock(parent)) { return parent; } // Walk left/right for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { return parent; } } // Check if we can move up are we at root level or body level if (parent == root || parent.parentNode == root) { container = parent; break; } parent = parent.parentNode; } return container; } // This function walks down the tree to find the leaf at the selection. // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. function findLeaf(node, offset) { if (offset === undef) { offset = node.nodeType === 3 ? node.length : node.childNodes.length; } while (node && node.hasChildNodes()) { node = node.childNodes[offset]; if (node) { offset = node.nodeType === 3 ? node.length : node.childNodes.length; } } return { node: node, offset: offset }; } // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; if (startContainer.nodeType == 3) { startOffset = 0; } } // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { lastIdx = endContainer.childNodes.length - 1; endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; if (endContainer.nodeType == 3) { endOffset = endContainer.nodeValue.length; } } // Expands the node to the closes contentEditable false element if it exists function findParentContentEditable(node) { var parent = node; while (parent) { if (parent.nodeType === 1 && getContentEditable(parent)) { return getContentEditable(parent) === "false" ? parent : node; } parent = parent.parentNode; } return node; } function findWordEndPoint(container, offset, start) { var walker, node, pos, lastTextNode; function findSpace(node, offset) { var pos, pos2, str = node.nodeValue; if (typeof offset == "undefined") { offset = start ? str.length : 0; } if (start) { pos = str.lastIndexOf(' ', offset); pos2 = str.lastIndexOf('\u00a0', offset); pos = pos > pos2 ? pos : pos2; // Include the space on remove to avoid tag soup if (pos !== -1 && !remove) { pos++; } } else { pos = str.indexOf(' ', offset); pos2 = str.indexOf('\u00a0', offset); pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; } return pos; } if (container.nodeType === 3) { pos = findSpace(container, offset); if (pos !== -1) { return { container: container, offset: pos }; } lastTextNode = container; } // Walk the nodes inside the block walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); while ((node = walker[start ? 'prev' : 'next']())) { if (node.nodeType === 3) { lastTextNode = node; pos = findSpace(node); if (pos !== -1) { return { container: node, offset: pos }; } } else if (isBlock(node)) { break; } } if (lastTextNode) { if (start) { offset = 0; } else { offset = lastTextNode.length; } return { container: lastTextNode, offset: offset }; } } function findSelectorEndPoint(container, siblingName) { var parents, i, y, curFormat; if (container.nodeType == 3 && container.nodeValue.length === 0 && container[siblingName]) { container = container[siblingName]; } parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { curFormat = format[y]; // If collapsed state is set then skip formats that doesn't match that if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { continue; } if (dom.is(parents[i], curFormat.selector)) { return parents[i]; } } } return container; } function findBlockEndPoint(container, siblingName) { var node, root = dom.getRoot(); // Expand to block of similar type if (!format[0].wrapper) { node = dom.getParent(container, format[0].block, root); } // Expand to first wrappable block element or any block element if (!node) { node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function (node) { // Fixes #6183 where it would expand to editable parent element in inline mode return node != root && isTextBlock(node); }); } // Exclude inner lists from wrapping if (node && format[0].wrapper) { node = getParents(node, 'ul,ol').reverse()[0] || node; } // Didn't find a block element look for first/last wrappable element if (!node) { node = container; while (node[siblingName] && !isBlock(node[siblingName])) { node = node[siblingName]; // Break on BR but include it will be removed later on // we can't remove it now since we need to check if it can be wrapped if (isEq(node, 'br')) { break; } } } return node || container; } // Expand to closest contentEditable element startContainer = findParentContentEditable(startContainer); endContainer = findParentContentEditable(endContainer); // Exclude bookmark nodes if possible if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; if (startContainer.nodeType == 3) { startOffset = 0; } } if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; endContainer = endContainer.previousSibling || endContainer; if (endContainer.nodeType == 3) { endOffset = endContainer.length; } } if (format[0].inline) { if (rng.collapsed) { // Expand left to closest word boundary endPoint = findWordEndPoint(startContainer, startOffset, true); if (endPoint) { startContainer = endPoint.container; startOffset = endPoint.offset; } // Expand right to closest word boundary endPoint = findWordEndPoint(endContainer, endOffset); if (endPoint) { endContainer = endPoint.container; endOffset = endPoint.offset; } } // Avoid applying formatting to a trailing space. leaf = findLeaf(endContainer, endOffset); if (leaf.node) { while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { leaf = findLeaf(leaf.node.previousSibling); } if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { if (leaf.offset > 1) { endContainer = leaf.node; endContainer.splitText(leaf.offset - 1); } } } } // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !*texttext*
! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { startContainer = findParentContainer(true); } if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { endContainer = findParentContainer(); } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { // Find new startContainer/endContainer if there is better one startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); } // Expand start/end container to matching block element or text node if (format[0].block || format[0].selector) { // Find new startContainer/endContainer if there is better one startContainer = findBlockEndPoint(startContainer, 'previousSibling'); endContainer = findBlockEndPoint(endContainer, 'nextSibling'); // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) { startContainer = findParentContainer(true); } if (!isBlock(endContainer)) { endContainer = findParentContainer(); } } } // Setup index for startContainer if (startContainer.nodeType == 1) { startOffset = nodeIndex(startContainer); startContainer = startContainer.parentNode; } // Setup index for endContainer if (endContainer.nodeType == 1) { endOffset = nodeIndex(endContainer) + 1; endContainer = endContainer.parentNode; } // Return new range like object return { startContainer: startContainer, startOffset: startOffset, endContainer: endContainer, endOffset: endOffset }; } function isColorFormatAndAnchor(node, format) { return format.links && node.tagName == 'A'; } /** * Removes the specified format for the specified node. It will also remove the node if it doesn't have * any attributes if the format specifies it to do so. * * @private * @param {Object} format Format object with items to remove from node. * @param {Object} vars Name/value object with variables to apply to format. * @param {Node} node Node to remove the format styles on. * @param {Node} compareNode Optional compare node, if specified the styles will be compared to that node. * @return {Boolean} True/false if the node was removed or not. */ function removeFormat(format, vars, node, compareNode) { var i, attrs, stylesModified; // Check if node matches format if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { return FALSE; } // Should we compare with format attribs and styles if (format.remove != 'all') { // Remove styles each(format.styles, function (value, name) { value = normalizeStyleValue(replaceVars(value, vars), name); // Indexed array if (typeof name === 'number') { name = value; compareNode = 0; } if (format.remove_similar || (!compareNode || isEq(getStyle(compareNode, name), value))) { dom.setStyle(node, name, ''); } stylesModified = 1; }); // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') === '') { node.removeAttribute('style'); node.removeAttribute('data-mce-style'); } // Remove attributes each(format.attributes, function (value, name) { var valueOut; value = replaceVars(value, vars); // Indexed array if (typeof name === 'number') { name = value; compareNode = 0; } if (!compareNode || isEq(dom.getAttrib(compareNode, name), value)) { // Keep internal classes if (name == 'class') { value = dom.getAttrib(node, name); if (value) { // Build new class value where everything is removed except the internal prefixed classes valueOut = ''; each(value.split(/\s+/), function (cls) { if (/mce\-\w+/.test(cls)) { valueOut += (valueOut ? ' ' : '') + cls; } }); // We got some internal classes left if (valueOut) { dom.setAttrib(node, name, valueOut); return; } } } // IE6 has a bug where the attribute doesn't get removed correctly if (name == "class") { node.removeAttribute('className'); } // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) { node.removeAttribute('data-mce-' + name); } node.removeAttribute(name); } }); // Remove classes each(format.classes, function (value) { value = replaceVars(value, vars); if (!compareNode || dom.hasClass(compareNode, value)) { dom.removeClass(node, value); } }); // Check for non internal attributes attrs = dom.getAttribs(node); for (i = 0; i < attrs.length; i++) { var attrName = attrs[i].nodeName; if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) { return FALSE; } } } // Remove the inline child if it's empty for example or if (format.remove != 'none') { removeNode(node, format); return TRUE; } } /** * Removes the node and wrap it's children in paragraphs before doing so or * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. * * If the div in the node below gets removed: * text|
formatNode.parentNode.replaceChild(caretContainer, formatNode); } else { // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); } // Move selection to text node selection.setCursorLocation(node, 1); // If the formatNode is empty, we can remove it safely. if (dom.isEmpty(formatNode)) { dom.remove(formatNode); } } } // Checks if the parent caret container node isn't empty if that is the case it // will remove the bogus state on all children that isn't empty function unmarkBogusCaretParents() { var caretContainer; caretContainer = getParentCaretContainer(selection.getStart()); if (caretContainer && !dom.isEmpty(caretContainer)) { walk(caretContainer, function (node) { if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { dom.setAttrib(node, 'data-mce-bogus', null); } }, 'childNodes'); } } // Only bind the caret events once if (!ed._hasCaretEvents) { // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements markCaretContainersBogus = function () { var nodes = [], i; if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { // Mark children i = nodes.length; while (i--) { dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); } } }; disableCaretContainer = function (e) { var keyCode = e.keyCode; removeCaretContainer(); // Remove caret container if it's empty if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } // Remove caret container on keydown and it's left/right arrow keys if (keyCode == 37 || keyCode == 39) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } unmarkBogusCaretParents(); }; // Remove bogus state if they got filled by contents using editor.selection.setContent ed.on('SetContent', function (e) { if (e.selection) { unmarkBogusCaretParents(); } }); ed._hasCaretEvents = true; } // Do apply or remove caret format if (type == "apply") { applyCaretFormat(); } else { removeCaretFormat(); } } /** * Moves the start to the first suitable text node. */ function moveStart(rng) { var container = rng.startContainer, offset = rng.startOffset, walker, node, nodes; if (rng.startContainer == rng.endContainer) { if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { return; } } // Convert text node into index if possible if (container.nodeType == 3 && offset >= container.nodeValue.length) { // Get the parent container location and walk from there offset = nodeIndex(container); container = container.parentNode; } // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1) { nodes = container.childNodes; if (offset < nodes.length) { container = nodes[offset]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); } else { container = nodes[nodes.length - 1]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); walker.next(true); } for (node = walker.current(); node; node = walker.next()) { if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { rng.setStart(node, 0); selection.setRng(rng); return; } } } } }; } ); /** * Diff.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. * * @class tinymce.undo.Diff * @private */ define( 'tinymce.core.undo.Diff', [ ], function () { var KEEP = 0, INSERT = 1, DELETE = 2; var diff = function (left, right) { var size = left.length + right.length + 2; var vDown = new Array(size); var vUp = new Array(size); var snake = function (start, end, diag) { return { start: start, end: end, diag: diag }; }; var buildScript = function (start1, end1, start2, end2, script) { var middle = getMiddleSnake(start1, end1, start2, end2); if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || middle.end === start1 && middle.diag === start1 - start2) { var i = start1; var j = start2; while (i < end1 || j < end2) { if (i < end1 && j < end2 && left[i] === right[j]) { script.push([KEEP, left[i]]); ++i; ++j; } else { if (end1 - start1 > end2 - start2) { script.push([DELETE, left[i]]); ++i; } else { script.push([INSERT, right[j]]); ++j; } } } } else { buildScript(start1, middle.start, start2, middle.start - middle.diag, script); for (var i2 = middle.start; i2 < middle.end; ++i2) { script.push([KEEP, left[i2]]); } buildScript(middle.end, end1, middle.end - middle.diag, end2, script); } }; var buildSnake = function (start, diag, end1, end2) { var end = start; while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { ++end; } return snake(start, end, diag); }; var getMiddleSnake = function (start1, end1, start2, end2) { // Myers Algorithm // Initialisations var m = end1 - start1; var n = end2 - start2; if (m === 0 || n === 0) { return null; } var delta = m - n; var sum = n + m; var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; vDown[1 + offset] = start1; vUp[1 + offset] = end1 + 1; for (var d = 0; d <= offset; ++d) { // Down for (var k = -d; k <= d; k += 2) { // First step var i = k + offset; if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { vDown[i] = vDown[i + 1]; } else { vDown[i] = vDown[i - 1] + 1; } var x = vDown[i]; var y = x - start1 + start2 - k; while (x < end1 && y < end2 && left[x] === right[y]) { vDown[i] = ++x; ++y; } // Second step if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { if (vUp[i - delta] <= vDown[i]) { return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); } } } // Up for (k = delta - d; k <= delta + d; k += 2) { // First step i = k + offset - delta; if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { vUp[i] = vUp[i + 1] - 1; } else { vUp[i] = vUp[i - 1]; } x = vUp[i] - 1; y = x - start1 + start2 - k; while (x >= start1 && y >= start2 && left[x] === right[y]) { vUp[i] = x--; y--; } // Second step if (delta % 2 === 0 && -d <= k && k <= d) { if (vUp[i] <= vDown[i + delta]) { return buildSnake(vUp[i], k + start1 - start2, end1, end2); } } } } }; var script = []; buildScript(0, left.length, 0, right.length, script); return script; }; return { KEEP: KEEP, DELETE: DELETE, INSERT: INSERT, diff: diff }; } ); /** * Fragments.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module reads and applies html fragments from/to dom nodes. * * @class tinymce.undo.Fragments * @private */ define( 'tinymce.core.undo.Fragments', [ "tinymce.core.util.Arr", "tinymce.core.html.Entities", "tinymce.core.undo.Diff" ], function (Arr, Entities, Diff) { var getOuterHtml = function (elm) { if (elm.nodeType === 1) { return elm.outerHTML; } else if (elm.nodeType === 3) { return Entities.encodeRaw(elm.data, false); } else if (elm.nodeType === 8) { return ''; } return ''; }; var createFragment = function (html) { var frag, node, container; container = document.createElement("div"); frag = document.createDocumentFragment(); if (html) { container.innerHTML = html; } while ((node = container.firstChild)) { frag.appendChild(node); } return frag; }; var insertAt = function (elm, html, index) { var fragment = createFragment(html); if (elm.hasChildNodes() && index < elm.childNodes.length) { var target = elm.childNodes[index]; target.parentNode.insertBefore(fragment, target); } else { elm.appendChild(fragment); } }; var removeAt = function (elm, index) { if (elm.hasChildNodes() && index < elm.childNodes.length) { var target = elm.childNodes[index]; target.parentNode.removeChild(target); } }; var applyDiff = function (diff, elm) { var index = 0; Arr.each(diff, function (action) { if (action[0] === Diff.KEEP) { index++; } else if (action[0] === Diff.INSERT) { insertAt(elm, action[1], index); index++; } else if (action[0] === Diff.DELETE) { removeAt(elm, index); } }); }; var read = function (elm) { return Arr.map(elm.childNodes, getOuterHtml); }; var write = function (fragments, elm) { var currentFragments = Arr.map(elm.childNodes, getOuterHtml); applyDiff(Diff.diff(currentFragments, fragments), elm); return elm; }; return { read: read, write: write }; } ); /** * Levels.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module handles getting/setting undo levels to/from editor instances. * * @class tinymce.undo.Levels * @private */ define( 'tinymce.core.undo.Levels', [ "tinymce.core.util.Arr", "tinymce.core.undo.Fragments" ], function (Arr, Fragments) { var hasIframes = function (html) { return html.indexOf('') !== -1; }; var createFragmentedLevel = function (fragments) { return { type: 'fragmented', fragments: fragments, content: '', bookmark: null, beforeBookmark: null }; }; var createCompleteLevel = function (content) { return { type: 'complete', fragments: null, content: content, bookmark: null, beforeBookmark: null }; }; var createFromEditor = function (editor) { var fragments, content, trimmedFragments; fragments = Fragments.read(editor.getBody()); trimmedFragments = Arr.map(fragments, function (html) { return editor.serializer.trimContent(html); }); content = trimmedFragments.join(''); return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : createCompleteLevel(content); }; var applyToEditor = function (editor, level, before) { if (level.type === 'fragmented') { Fragments.write(level.fragments, editor.getBody()); } else { editor.setContent(level.content, { format: 'raw' }); } editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark); }; var getLevelContent = function (level) { return level.type === 'fragmented' ? level.fragments.join('') : level.content; }; var isEq = function (level1, level2) { return getLevelContent(level1) === getLevelContent(level2); }; return { createFragmentedLevel: createFragmentedLevel, createCompleteLevel: createCompleteLevel, createFromEditor: createFromEditor, applyToEditor: applyToEditor, isEq: isEq }; } ); /** * UndoManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed. * * @class tinymce.UndoManager */ define( 'tinymce.core.UndoManager', [ "tinymce.core.util.VK", "tinymce.core.util.Tools", "tinymce.core.undo.Levels" ], function (VK, Tools, Levels) { return function (editor) { var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; var isUnlocked = function () { return locks === 0; }; var setTyping = function (typing) { if (isUnlocked()) { self.typing = typing; } }; function setDirty(state) { editor.setDirty(state); } function addNonTypingUndoLevel(e) { setTyping(false); self.add({}, e); } function endTyping() { if (self.typing) { setTyping(false); self.add(); } } // Add initial undo level when the editor is initialized editor.on('init', function () { self.add(); }); // Get position before an execCommand is processed editor.on('BeforeExecCommand', function (e) { var cmd = e.command; if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { endTyping(); self.beforeChange(); } }); // Add undo level after an execCommand call was made editor.on('ExecCommand', function (e) { var cmd = e.command; if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { addNonTypingUndoLevel(e); } }); editor.on('ObjectResizeStart Cut', function () { self.beforeChange(); }); editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); editor.on('DragEnd', addNonTypingUndoLevel); editor.on('KeyUp', function (e) { var keyCode = e.keyCode; // If key is prevented then don't add undo level // This would happen on keyboard shortcuts for example if (e.isDefaultPrevented()) { return; } if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) { addNonTypingUndoLevel(); editor.nodeChanged(); } if (keyCode === 46 || keyCode === 8) { editor.nodeChanged(); } // Fire a TypingUndo/Change event on the first character entered if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) { if (editor.isDirty() === false) { setDirty(true); editor.fire('change', { level: data[0], lastLevel: null }); } editor.fire('TypingUndo'); isFirstTypedCharacter = false; editor.nodeChanged(); } }); editor.on('KeyDown', function (e) { var keyCode = e.keyCode; // If key is prevented then don't add undo level // This would happen on keyboard shortcuts for example if (e.isDefaultPrevented()) { return; } // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) { if (self.typing) { addNonTypingUndoLevel(e); } return; } // If key isn't Ctrl+Alt/AltGr var modKey = (e.ctrlKey && !e.altKey) || e.metaKey; if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) { self.beforeChange(); setTyping(true); self.add({}, e); isFirstTypedCharacter = true; } }); editor.on('MouseDown', function (e) { if (self.typing) { addNonTypingUndoLevel(e); } }); // Add keyboard shortcuts for undo/redo keys editor.addShortcut('meta+z', '', 'Undo'); editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); editor.on('AddUndo Undo Redo ClearUndos', function (e) { if (!e.isDefaultPrevented()) { editor.nodeChanged(); } }); /*eslint consistent-this:0 */ self = { // Explode for debugging reasons data: data, /** * State if the user is currently typing or not. This will add a typing operation into one undo * level instead of one new level for each keystroke. * * @field {Boolean} typing */ typing: false, /** * Stores away a bookmark to be used when performing an undo action so that the selection is before * the change has been made. * * @method beforeChange */ beforeChange: function () { if (isUnlocked()) { beforeBookmark = editor.selection.getBookmark(2, true); } }, /** * Adds a new undo level/snapshot to the undo list. * * @method add * @param {Object} level Optional undo level object to add. * @param {DOMEvent} event Optional event responsible for the creation of the undo level. * @return {Object} Undo level that got added or null it a level wasn't needed. */ add: function (level, event) { var i, settings = editor.settings, lastLevel, currentLevel; currentLevel = Levels.createFromEditor(editor); level = level || {}; level = Tools.extend(level, currentLevel); if (isUnlocked() === false || editor.removed) { return null; } lastLevel = data[index]; if (editor.fire('BeforeAddUndo', { level: level, lastLevel: lastLevel, originalEvent: event }).isDefaultPrevented()) { return null; } // Add undo level if needed if (lastLevel && Levels.isEq(lastLevel, level)) { return null; } // Set before bookmark on previous level if (data[index]) { data[index].beforeBookmark = beforeBookmark; } // Time to compress if (settings.custom_undo_redo_levels) { if (data.length > settings.custom_undo_redo_levels) { for (i = 0; i < data.length - 1; i++) { data[i] = data[i + 1]; } data.length--; index = data.length; } } // Get a non intrusive normalized bookmark level.bookmark = editor.selection.getBookmark(2, true); // Crop array if needed if (index < data.length - 1) { data.length = index + 1; } data.push(level); index = data.length - 1; var args = { level: level, lastLevel: lastLevel, originalEvent: event }; editor.fire('AddUndo', args); if (index > 0) { setDirty(true); editor.fire('change', args); } return level; }, /** * Undoes the last action. * * @method undo * @return {Object} Undo level or null if no undo was performed. */ undo: function () { var level; if (self.typing) { self.add(); self.typing = false; setTyping(false); } if (index > 0) { level = data[--index]; Levels.applyToEditor(editor, level, true); setDirty(true); editor.fire('undo', { level: level }); } return level; }, /** * Redoes the last action. * * @method redo * @return {Object} Redo level or null if no redo was performed. */ redo: function () { var level; if (index < data.length - 1) { level = data[++index]; Levels.applyToEditor(editor, level, false); setDirty(true); editor.fire('redo', { level: level }); } return level; }, /** * Removes all undo levels. * * @method clear */ clear: function () { data = []; index = 0; self.typing = false; self.data = data; editor.fire('ClearUndos'); }, /** * Returns true/false if the undo manager has any undo levels. * * @method hasUndo * @return {Boolean} true/false if the undo manager has any undo levels. */ hasUndo: function () { // Has undo levels or typing and content isn't the same as the initial level return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); }, /** * Returns true/false if the undo manager has any redo levels. * * @method hasRedo * @return {Boolean} true/false if the undo manager has any redo levels. */ hasRedo: function () { return index < data.length - 1 && !self.typing; }, /** * Executes the specified mutator function as an undo transaction. The selection * before the modification will be stored to the undo stack and if the DOM changes * it will add a new undo level. Any logic within the translation that adds undo levels will * be ignored. So a translation can include calls to execCommand or editor.insertContent. * * @method transact * @param {function} callback Function that gets executed and has dom manipulation logic in it. * @return {Object} Undo level that got added or null it a level wasn't needed. */ transact: function (callback) { endTyping(); self.beforeChange(); self.ignore(callback); return self.add(); }, /** * Executes the specified mutator function as an undo transaction. But without adding an undo level. * Any logic within the translation that adds undo levels will be ignored. So a translation can * include calls to execCommand or editor.insertContent. * * @method ignore * @param {function} callback Function that gets executed and has dom manipulation logic in it. * @return {Object} Undo level that got added or null it a level wasn't needed. */ ignore: function (callback) { try { locks++; callback(); } finally { locks--; } }, /** * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack * then roll back that change and do the second mutation on top of the stack. This will produce an extra * undo level that the user doesn't see until they undo. * * @method extra * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. * @param {function} callback2 Function that does mutation but gets displayed to the user. */ extra: function (callback1, callback2) { var lastLevel, bookmark; if (self.transact(callback1)) { bookmark = data[index].bookmark; lastLevel = data[index - 1]; Levels.applyToEditor(editor, lastLevel, true); if (self.transact(callback2)) { data[index - 1].beforeBookmark = bookmark; } } } }; return self; }; } ); define( 'ephox.katamari.api.Options', [ 'ephox.katamari.api.Option' ], function (Option) { /** cat :: [Option a] -> [a] */ var cat = function (arr) { var r = []; var push = function (x) { r.push(x); }; for (var i = 0; i < arr.length; i++) { arr[i].each(push); } return r; }; /** findMap :: ([a], (a, Int -> Option b)) -> Option b */ var findMap = function (arr, f) { for (var i = 0; i < arr.length; i++) { var r = f(arr[i], i); if (r.isSome()) { return r; } } return Option.none(); }; /** * if all elements in arr are 'some', their inner values are passed as arguments to f * f must have arity arr.length */ var liftN = function(arr, f) { var r = []; for (var i = 0; i < arr.length; i++) { var x = arr[i]; if (x.isSome()) { r.push(x.getOrDie()); } else { return Option.none(); } } return Option.some(f.apply(null, r)); }; return { cat: cat, findMap: findMap, liftN: liftN }; } ); define( 'ephox.katamari.data.Immutable', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'global!Array', 'global!Error' ], function (Arr, Fun, Array, Error) { return function () { var fields = arguments; return function(/* values */) { // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var values = new Array(arguments.length); for (var i = 0; i < values.length; i++) values[i] = arguments[i]; if (fields.length !== values.length) throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); var struct = {}; Arr.each(fields, function (name, i) { struct[name] = Fun.constant(values[i]); }); return struct; }; }; } ); define( 'ephox.katamari.api.Obj', [ 'ephox.katamari.api.Option', 'global!Object' ], function (Option, Object) { // There are many variations of Object iteration that are faster than the 'for-in' style: // http://jsperf.com/object-keys-iteration/107 // // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering var keys = (function () { var fastKeys = Object.keys; // This technically means that 'each' and 'find' on IE8 iterate through the object twice. // This code doesn't run on IE8 much, so it's an acceptable tradeoff. // If it becomes a problem we can always duplicate the feature detection inside each and find as well. var slowKeys = function (o) { var r = []; for (var i in o) { if (o.hasOwnProperty(i)) { r.push(i); } } return r; }; return fastKeys === undefined ? slowKeys : fastKeys; })(); var each = function (obj, f) { var props = keys(obj); for (var k = 0, len = props.length; k < len; k++) { var i = props[k]; var x = obj[i]; f(x, i, obj); } }; /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ var objectMap = function (obj, f) { return tupleMap(obj, function (x, i, obj) { return { k: i, v: f(x, i, obj) }; }); }; /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ var tupleMap = function (obj, f) { var r = {}; each(obj, function (x, i) { var tuple = f(x, i, obj); r[tuple.k] = tuple.v; }); return r; }; /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ var bifilter = function (obj, pred) { var t = {}; var f = {}; each(obj, function(x, i) { var branch = pred(x, i) ? t : f; branch[i] = x; }); return { t: t, f: f }; }; /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ var mapToArray = function (obj, f) { var r = []; each(obj, function(value, name) { r.push(f(value, name)); }); return r; }; /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ var find = function (obj, pred) { var props = keys(obj); for (var k = 0, len = props.length; k < len; k++) { var i = props[k]; var x = obj[i]; if (pred(x, i, obj)) { return Option.some(x); } } return Option.none(); }; /** values :: JsObj(k, v) -> [v] */ var values = function (obj) { return mapToArray(obj, function (v) { return v; }); }; var size = function (obj) { return values(obj).length; }; return { bifilter: bifilter, each: each, map: objectMap, mapToArray: mapToArray, tupleMap: tupleMap, find: find, keys: keys, values: values, size: size }; } ); define( 'ephox.katamari.api.Type', [ 'global!Array', 'global!String' ], function (Array, String) { var typeOf = function(x) { if (x === null) return 'null'; var t = typeof x; if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; return t; }; var isType = function (type) { return function (value) { return typeOf(value) === type; }; }; return { isString: isType('string'), isObject: isType('object'), isArray: isType('array'), isNull: isType('null'), isBoolean: isType('boolean'), isUndefined: isType('undefined'), isFunction: isType('function'), isNumber: isType('number') }; } ); define( 'ephox.katamari.util.BagUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Type', 'global!Error' ], function (Arr, Type, Error) { var sort = function (arr) { return arr.slice(0).sort(); }; var reqMessage = function (required, keys) { throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); }; var unsuppMessage = function (unsupported) { throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); }; var validateStrArr = function (label, array) { if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); Arr.each(array, function (a) { if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); }); }; var invalidTypeMessage = function (incorrect, type) { throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); }; var checkDupes = function (everything) { var sorted = sort(everything); var dupe = Arr.find(sorted, function (s, i) { return i < sorted.length -1 && s === sorted[i + 1]; }); dupe.each(function (d) { throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); }); }; return { sort: sort, reqMessage: reqMessage, unsuppMessage: unsuppMessage, validateStrArr: validateStrArr, invalidTypeMessage: invalidTypeMessage, checkDupes: checkDupes }; } ); define( 'ephox.katamari.data.MixedBag', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Option', 'ephox.katamari.util.BagUtils', 'global!Error', 'global!Object' ], function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { return function (required, optional) { var everything = required.concat(optional); if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); BagUtils.validateStrArr('required', required); BagUtils.validateStrArr('optional', optional); BagUtils.checkDupes(everything); return function (obj) { var keys = Obj.keys(obj); // Ensure all required keys are present. var allReqd = Arr.forall(required, function (req) { return Arr.contains(keys, req); }); if (! allReqd) BagUtils.reqMessage(required, keys); var unsupported = Arr.filter(keys, function (key) { return !Arr.contains(everything, key); }); if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); var r = {}; Arr.each(required, function (req) { r[req] = Fun.constant(obj[req]); }); Arr.each(optional, function (opt) { r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); }); return r; }; }; } ); define( 'ephox.katamari.api.Struct', [ 'ephox.katamari.data.Immutable', 'ephox.katamari.data.MixedBag' ], function (Immutable, MixedBag) { return { immutable: Immutable, immutableBag: MixedBag }; } ); define( 'ephox.katamari.api.Global', [ ], function () { return Function('return this;')(); } ); define( 'ephox.katamari.api.Resolve', [ 'ephox.katamari.api.Global' ], function (Global) { /** path :: ([String], JsObj?) -> JsObj */ var path = function (parts, scope) { var o = scope !== undefined ? scope : Global; for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) o = o[parts[i]]; return o; }; /** resolve :: (String, JsObj?) -> JsObj */ var resolve = function (p, scope) { var parts = p.split('.'); return path(parts, scope); }; /** step :: (JsObj, String) -> JsObj */ var step = function (o, part) { if (o[part] === undefined || o[part] === null) o[part] = {}; return o[part]; }; /** forge :: ([String], JsObj?) -> JsObj */ var forge = function (parts, target) { var o = target !== undefined ? target : Global; for (var i = 0; i < parts.length; ++i) o = step(o, parts[i]); return o; }; /** namespace :: (String, JsObj?) -> JsObj */ var namespace = function (name, target) { var parts = name.split('.'); return forge(parts, target); }; return { path: path, resolve: resolve, forge: forge, namespace: namespace }; } ); define( 'ephox.sand.util.Global', [ 'ephox.katamari.api.Resolve' ], function (Resolve) { var unsafe = function (name, scope) { return Resolve.resolve(name, scope); }; var getOrDie = function (name, scope) { var actual = unsafe(name, scope); if (actual === undefined) throw name + ' not available on this browser'; return actual; }; return { getOrDie: getOrDie }; } ); define( 'ephox.sand.api.Node', [ 'ephox.sand.util.Global' ], function (Global) { /* * MDN says (yes) for IE, but it's undefined on IE8 */ var node = function () { var f = Global.getOrDie('Node'); return f; }; /* * Most of numerosity doesn't alter the methods on the object. * We're making an exception for Node, because bitwise and is so easy to get wrong. * * Might be nice to ADT this at some point instead of having individual methods. */ var compareDocumentPosition = function (a, b, match) { // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition return (a.compareDocumentPosition(b) & match) !== 0; }; var documentPositionPreceding = function (a, b) { return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); }; var documentPositionContainedBy = function (a, b) { return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); }; return { documentPositionPreceding: documentPositionPreceding, documentPositionContainedBy: documentPositionContainedBy }; } ); define( 'ephox.katamari.api.Thunk', [ ], function () { var cached = function (f) { var called = false; var r; return function() { if (!called) { called = true; r = f.apply(null, arguments); } return r; }; }; return { cached: cached }; } ); defineGlobal("global!Number", Number); define( 'ephox.sand.detect.Version', [ 'ephox.katamari.api.Arr', 'global!Number', 'global!String' ], function (Arr, Number, String) { var firstMatch = function (regexes, s) { for (var i = 0; i < regexes.length; i++) { var x = regexes[i]; if (x.test(s)) return x; } return undefined; }; var find = function (regexes, agent) { var r = firstMatch(regexes, agent); if (!r) return { major : 0, minor : 0 }; var group = function(i) { return Number(agent.replace(r, '$' + i)); }; return nu(group(1), group(2)); }; var detect = function (versionRegexes, agent) { var cleanedAgent = String(agent).toLowerCase(); if (versionRegexes.length === 0) return unknown(); return find(versionRegexes, cleanedAgent); }; var unknown = function () { return nu(0, 0); }; var nu = function (major, minor) { return { major: major, minor: minor }; }; return { nu: nu, detect: detect, unknown: unknown }; } ); define( 'ephox.sand.core.Browser', [ 'ephox.katamari.api.Fun', 'ephox.sand.detect.Version' ], function (Fun, Version) { var edge = 'Edge'; var chrome = 'Chrome'; var ie = 'IE'; var opera = 'Opera'; var firefox = 'Firefox'; var safari = 'Safari'; var isBrowser = function (name, current) { return function () { return current === name; }; }; var unknown = function () { return nu({ current: undefined, version: Version.unknown() }); }; var nu = function (info) { var current = info.current; var version = info.version; return { current: current, version: version, // INVESTIGATE: Rename to Edge ? isEdge: isBrowser(edge, current), isChrome: isBrowser(chrome, current), // NOTE: isIe just looks too weird isIE: isBrowser(ie, current), isOpera: isBrowser(opera, current), isFirefox: isBrowser(firefox, current), isSafari: isBrowser(safari, current) }; }; return { unknown: unknown, nu: nu, edge: Fun.constant(edge), chrome: Fun.constant(chrome), ie: Fun.constant(ie), opera: Fun.constant(opera), firefox: Fun.constant(firefox), safari: Fun.constant(safari) }; } ); define( 'ephox.sand.core.OperatingSystem', [ 'ephox.katamari.api.Fun', 'ephox.sand.detect.Version' ], function (Fun, Version) { var windows = 'Windows'; var ios = 'iOS'; var android = 'Android'; var linux = 'Linux'; var osx = 'OSX'; var solaris = 'Solaris'; var freebsd = 'FreeBSD'; // Though there is a bit of dupe with this and Browser, trying to // reuse code makes it much harder to follow and change. var isOS = function (name, current) { return function () { return current === name; }; }; var unknown = function () { return nu({ current: undefined, version: Version.unknown() }); }; var nu = function (info) { var current = info.current; var version = info.version; return { current: current, version: version, isWindows: isOS(windows, current), // TODO: Fix capitalisation isiOS: isOS(ios, current), isAndroid: isOS(android, current), isOSX: isOS(osx, current), isLinux: isOS(linux, current), isSolaris: isOS(solaris, current), isFreeBSD: isOS(freebsd, current) }; }; return { unknown: unknown, nu: nu, windows: Fun.constant(windows), ios: Fun.constant(ios), android: Fun.constant(android), linux: Fun.constant(linux), osx: Fun.constant(osx), solaris: Fun.constant(solaris), freebsd: Fun.constant(freebsd) }; } ); define( 'ephox.sand.detect.DeviceType', [ 'ephox.katamari.api.Fun' ], function (Fun) { return function (os, browser, userAgent) { var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; var isiPhone = os.isiOS() && !isiPad; var isAndroid3 = os.isAndroid() && os.version.major === 3; var isAndroid4 = os.isAndroid() && os.version.major === 4; var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); var isTouch = os.isiOS() || os.isAndroid(); var isPhone = isTouch && !isTablet; var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; return { isiPad : Fun.constant(isiPad), isiPhone: Fun.constant(isiPhone), isTablet: Fun.constant(isTablet), isPhone: Fun.constant(isPhone), isTouch: Fun.constant(isTouch), isAndroid: os.isAndroid, isiOS: os.isiOS, isWebView: Fun.constant(iOSwebview) }; }; } ); define( 'ephox.sand.detect.UaString', [ 'ephox.katamari.api.Arr', 'ephox.sand.detect.Version', 'global!String' ], function (Arr, Version, String) { var detect = function (candidates, userAgent) { var agent = String(userAgent).toLowerCase(); return Arr.find(candidates, function (candidate) { return candidate.search(agent); }); }; // They (browser and os) are the same at the moment, but they might // not stay that way. var detectBrowser = function (browsers, userAgent) { return detect(browsers, userAgent).map(function (browser) { var version = Version.detect(browser.versionRegexes, userAgent); return { current: browser.name, version: version }; }); }; var detectOs = function (oses, userAgent) { return detect(oses, userAgent).map(function (os) { var version = Version.detect(os.versionRegexes, userAgent); return { current: os.name, version: version }; }); }; return { detectBrowser: detectBrowser, detectOs: detectOs }; } ); define( 'ephox.katamari.str.StrAppend', [ ], function () { var addToStart = function (str, prefix) { return prefix + str; }; var addToEnd = function (str, suffix) { return str + suffix; }; var removeFromStart = function (str, numChars) { return str.substring(numChars); }; var removeFromEnd = function (str, numChars) { return str.substring(0, str.length - numChars); }; return { addToStart: addToStart, addToEnd: addToEnd, removeFromStart: removeFromStart, removeFromEnd: removeFromEnd }; } ); define( 'ephox.katamari.str.StringParts', [ 'ephox.katamari.api.Option', 'global!Error' ], function (Option, Error) { /** Return the first 'count' letters from 'str'. - * e.g. first("abcde", 2) === "ab" - */ var first = function(str, count) { return str.substr(0, count); }; /** Return the last 'count' letters from 'str'. * e.g. last("abcde", 2) === "de" */ var last = function(str, count) { return str.substr(str.length - count, str.length); }; var head = function(str) { return str === '' ? Option.none() : Option.some(str.substr(0, 1)); }; var tail = function(str) { return str === '' ? Option.none() : Option.some(str.substring(1)); }; return { first: first, last: last, head: head, tail: tail }; } ); define( 'ephox.katamari.api.Strings', [ 'ephox.katamari.str.StrAppend', 'ephox.katamari.str.StringParts', 'global!Error' ], function (StrAppend, StringParts, Error) { var checkRange = function(str, substr, start) { if (substr === '') return true; if (str.length < substr.length) return false; var x = str.substr(start, start + substr.length); return x === substr; }; /** Given a string and object, perform template-replacements on the string, as specified by the object. * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. */ var supplant = function(str, obj) { var isStringOrNumber = function(a) { var t = typeof a; return t === 'string' || t === 'number'; }; return str.replace(/\${([^{}]*)}/g, function (a, b) { var value = obj[b]; return isStringOrNumber(value) ? value : a; } ); }; var removeLeading = function (str, prefix) { return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; }; var removeTrailing = function (str, prefix) { return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; }; var ensureLeading = function (str, prefix) { return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); }; var ensureTrailing = function (str, prefix) { return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); }; var contains = function(str, substr) { return str.indexOf(substr) !== -1; }; var capitalize = function(str) { return StringParts.head(str).bind(function (head) { return StringParts.tail(str).map(function (tail) { return head.toUpperCase() + tail; }); }).getOr(str); }; /** Does 'str' start with 'prefix'? * Note: all strings start with the empty string. * More formally, for all strings x, startsWith(x, ""). * This is so that for all strings x and y, startsWith(y + x, y) */ var startsWith = function(str, prefix) { return checkRange(str, prefix, 0); }; /** Does 'str' end with 'suffix'? * Note: all strings end with the empty string. * More formally, for all strings x, endsWith(x, ""). * This is so that for all strings x and y, endsWith(x + y, y) */ var endsWith = function(str, suffix) { return checkRange(str, suffix, str.length - suffix.length); }; /** removes all leading and trailing spaces */ var trim = function(str) { return str.replace(/^\s+|\s+$/g, ''); }; var lTrim = function(str) { return str.replace(/^\s+/g, ''); }; var rTrim = function(str) { return str.replace(/\s+$/g, ''); }; return { supplant: supplant, startsWith: startsWith, removeLeading: removeLeading, removeTrailing: removeTrailing, ensureLeading: ensureLeading, ensureTrailing: ensureTrailing, endsWith: endsWith, contains: contains, trim: trim, lTrim: lTrim, rTrim: rTrim, capitalize: capitalize }; } ); define( 'ephox.sand.info.PlatformInfo', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Strings' ], function (Fun, Strings) { var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; var checkContains = function (target) { return function (uastring) { return Strings.contains(uastring, target); }; }; var browsers = [ { name : 'Edge', versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], search: function (uastring) { var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); return monstrosity; } }, { name : 'Chrome', versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], search : function (uastring) { return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); } }, { name : 'IE', versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], search: function (uastring) { return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); } }, // INVESTIGATE: Is this still the Opera user agent? { name : 'Opera', versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], search : checkContains('opera') }, { name : 'Firefox', versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], search : checkContains('firefox') }, { name : 'Safari', versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], search : function (uastring) { return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); } } ]; var oses = [ { name : 'Windows', search : checkContains('win'), versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] }, { name : 'iOS', search : function (uastring) { return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); }, versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] }, { name : 'Android', search : checkContains('android'), versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] }, { name : 'OSX', search : checkContains('os x'), versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] }, { name : 'Linux', search : checkContains('linux'), versionRegexes: [ ] }, { name : 'Solaris', search : checkContains('sunos'), versionRegexes: [ ] }, { name : 'FreeBSD', search : checkContains('freebsd'), versionRegexes: [ ] } ]; return { browsers: Fun.constant(browsers), oses: Fun.constant(oses) }; } ); define( 'ephox.sand.core.PlatformDetection', [ 'ephox.sand.core.Browser', 'ephox.sand.core.OperatingSystem', 'ephox.sand.detect.DeviceType', 'ephox.sand.detect.UaString', 'ephox.sand.info.PlatformInfo' ], function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { var detect = function (userAgent) { var browsers = PlatformInfo.browsers(); var oses = PlatformInfo.oses(); var browser = UaString.detectBrowser(browsers, userAgent).fold( Browser.unknown, Browser.nu ); var os = UaString.detectOs(oses, userAgent).fold( OperatingSystem.unknown, OperatingSystem.nu ); var deviceType = DeviceType(os, browser, userAgent); return { browser: browser, os: os, deviceType: deviceType }; }; return { detect: detect }; } ); defineGlobal("global!navigator", navigator); define( 'ephox.sand.api.PlatformDetection', [ 'ephox.katamari.api.Thunk', 'ephox.sand.core.PlatformDetection', 'global!navigator' ], function (Thunk, PlatformDetection, navigator) { var detect = Thunk.cached(function () { var userAgent = navigator.userAgent; return PlatformDetection.detect(userAgent); }); return { detect: detect }; } ); define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); defineGlobal("global!document", document); define( 'ephox.sugar.api.node.Element', [ 'ephox.katamari.api.Fun', 'global!Error', 'global!console', 'global!document' ], function (Fun, Error, console, document) { var fromHtml = function (html, scope) { var doc = scope || document; var div = doc.createElement('div'); div.innerHTML = html; if (!div.hasChildNodes() || div.childNodes.length > 1) { console.error('HTML does not have a single root node', html); throw 'HTML must have a single root node'; } return fromDom(div.childNodes[0]); }; var fromTag = function (tag, scope) { var doc = scope || document; var node = doc.createElement(tag); return fromDom(node); }; var fromText = function (text, scope) { var doc = scope || document; var node = doc.createTextNode(text); return fromDom(node); }; var fromDom = function (node) { if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); return { dom: Fun.constant(node) }; }; return { fromHtml: fromHtml, fromTag: fromTag, fromText: fromText, fromDom: fromDom }; } ); define( 'ephox.sugar.api.node.NodeTypes', [ ], function () { return { ATTRIBUTE: 2, CDATA_SECTION: 4, COMMENT: 8, DOCUMENT: 9, DOCUMENT_TYPE: 10, DOCUMENT_FRAGMENT: 11, ELEMENT: 1, TEXT: 3, PROCESSING_INSTRUCTION: 7, ENTITY_REFERENCE: 5, ENTITY: 6, NOTATION: 12 }; } ); define( 'ephox.sugar.api.search.Selectors', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.NodeTypes', 'global!Error', 'global!document' ], function (Arr, Option, Element, NodeTypes, Error, document) { /* * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, * instead of doing object lookup feature detection on every call */ var STANDARD = 0; var MSSTANDARD = 1; var WEBKITSTANDARD = 2; var FIREFOXSTANDARD = 3; var selectorType = (function () { var test = document.createElement('span'); // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. // Still check for the others, but do it last. return test.matches !== undefined ? STANDARD : test.msMatchesSelector !== undefined ? MSSTANDARD : test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : -1; })(); var ELEMENT = NodeTypes.ELEMENT; var DOCUMENT = NodeTypes.DOCUMENT; var is = function (element, selector) { var elem = element.dom(); if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. // Still check for the others, but do it last. else if (selectorType === STANDARD) return elem.matches(selector); else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( }; var bypassSelector = function (dom) { // Only elements and documents support querySelector return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ dom.childElementCount === 0; }; var all = function (selector, scope) { var base = scope === undefined ? document : scope.dom(); return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); }; var one = function (selector, scope) { var base = scope === undefined ? document : scope.dom(); return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); }; return { all: all, is: is, one: one }; } ); define( 'ephox.sugar.api.dom.Compare', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sand.api.Node', 'ephox.sand.api.PlatformDetection', 'ephox.sugar.api.search.Selectors' ], function (Arr, Fun, Node, PlatformDetection, Selectors) { var eq = function (e1, e2) { return e1.dom() === e2.dom(); }; var isEqualNode = function (e1, e2) { return e1.dom().isEqualNode(e2.dom()); }; var member = function (element, elements) { return Arr.exists(elements, Fun.curry(eq, element)); }; // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). var regularContains = function (e1, e2) { var d1 = e1.dom(), d2 = e2.dom(); return d1 === d2 ? false : d1.contains(d2); }; var ieContains = function (e1, e2) { // IE only implements the contains() method for Element nodes. // It fails for Text nodes, so implement it using compareDocumentPosition() // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': // Also, compareDocumentPosition defines a node containing itself as false. return Node.documentPositionContainedBy(e1.dom(), e2.dom()); }; var browser = PlatformDetection.detect().browser; // Returns: true if node e1 contains e2, otherwise false. // (returns false if e1===e2: A node does not contain itself). var contains = browser.isIE() ? ieContains : regularContains; return { eq: eq, isEqualNode: isEqualNode, member: member, contains: contains, // Only used by DomUniverse. Remove (or should Selectors.is move here?) is: Selectors.is }; } ); define( 'ephox.sugar.api.node.Node', [ 'ephox.sugar.api.node.NodeTypes' ], function (NodeTypes) { var name = function (element) { var r = element.dom().nodeName; return r.toLowerCase(); }; var type = function (element) { return element.dom().nodeType; }; var value = function (element) { return element.dom().nodeValue; }; var isType = function (t) { return function (element) { return type(element) === t; }; }; var isComment = function (element) { return type(element) === NodeTypes.COMMENT || name(element) === '#comment'; }; var isElement = isType(NodeTypes.ELEMENT); var isText = isType(NodeTypes.TEXT); var isDocument = isType(NodeTypes.DOCUMENT); return { name: name, type: type, value: value, isElement: isElement, isText: isText, isDocument: isDocument, isComment: isComment }; } ); define( 'ephox.sugar.api.node.Body', [ 'ephox.katamari.api.Thunk', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'global!document' ], function (Thunk, Element, Node, document) { // Node.contains() is very, very, very good performance // http://jsperf.com/closest-vs-contains/5 var inBody = function (element) { // Technically this is only required on IE, where contains() returns false for text nodes. // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). var dom = Node.isText(element) ? element.dom().parentNode : element.dom(); // use ownerDocument.body to ensure this works inside iframes. // Normally contains is bad because an element "contains" itself, but here we want that. return dom !== undefined && dom !== null && dom.ownerDocument.body.contains(dom); }; var body = Thunk.cached(function() { return getBody(Element.fromDom(document)); }); var getBody = function (doc) { var body = doc.dom().body; if (body === null || body === undefined) throw 'Body is not available yet'; return Element.fromDom(body); }; return { body: body, getBody: getBody, inBody: inBody }; } ); define( 'ephox.sugar.impl.ClosestOrAncestor', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Option' ], function (Type, Option) { return function (is, ancestor, scope, a, isRoot) { return is(scope, a) ? Option.some(scope) : Type.isFunction(isRoot) && isRoot(scope) ? Option.none() : ancestor(scope, a, isRoot); }; } ); define( 'ephox.sugar.api.search.PredicateFind', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Body', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.impl.ClosestOrAncestor' ], function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { var first = function (predicate) { return descendant(Body.body(), predicate); }; var ancestor = function (scope, predicate, isRoot) { var element = scope.dom(); var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); while (element.parentNode) { element = element.parentNode; var el = Element.fromDom(element); if (predicate(el)) return Option.some(el); else if (stop(el)) break; } return Option.none(); }; var closest = function (scope, predicate, isRoot) { // This is required to avoid ClosestOrAncestor passing the predicate to itself var is = function (scope) { return predicate(scope); }; return ClosestOrAncestor(is, ancestor, scope, predicate, isRoot); }; var sibling = function (scope, predicate) { var element = scope.dom(); if (!element.parentNode) return Option.none(); return child(Element.fromDom(element.parentNode), function (x) { return !Compare.eq(scope, x) && predicate(x); }); }; var child = function (scope, predicate) { var result = Arr.find(scope.dom().childNodes, Fun.compose(predicate, Element.fromDom)); return result.map(Element.fromDom); }; var descendant = function (scope, predicate) { var descend = function (element) { for (var i = 0; i < element.childNodes.length; i++) { if (predicate(Element.fromDom(element.childNodes[i]))) return Option.some(Element.fromDom(element.childNodes[i])); var res = descend(element.childNodes[i]); if (res.isSome()) return res; } return Option.none(); }; return descend(scope.dom()); }; return { first: first, ancestor: ancestor, closest: closest, sibling: sibling, child: child, descendant: descendant }; } ); define( 'ephox.sugar.alien.Recurse', [ ], function () { /** * Applies f repeatedly until it completes (by returning Option.none()). * * Normally would just use recursion, but JavaScript lacks tail call optimisation. * * This is what recursion looks like when manually unravelled :) */ var toArray = function (target, f) { var r = []; var recurse = function (e) { r.push(e); return f(e); }; var cur = f(target); do { cur = cur.bind(recurse); } while (cur.isSome()); return r; }; return { toArray: toArray }; } ); define( 'ephox.sugar.api.search.Traverse', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Struct', 'ephox.sugar.alien.Recurse', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element' ], function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { // The document associated with the current element var owner = function (element) { return Element.fromDom(element.dom().ownerDocument); }; var documentElement = function (element) { // TODO: Avoid unnecessary wrap/unwrap here var doc = owner(element); return Element.fromDom(doc.dom().documentElement); }; // The window element associated with the element var defaultView = function (element) { var el = element.dom(); var defaultView = el.ownerDocument.defaultView; return Element.fromDom(defaultView); }; var parent = function (element) { var dom = element.dom(); return Option.from(dom.parentNode).map(Element.fromDom); }; var findIndex = function (element) { return parent(element).bind(function (p) { // TODO: Refactor out children so we can avoid the constant unwrapping var kin = children(p); return Arr.findIndex(kin, function (elem) { return Compare.eq(element, elem); }); }); }; var parents = function (element, isRoot) { var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); // This is used a *lot* so it needs to be performant, not recursive var dom = element.dom(); var ret = []; while (dom.parentNode !== null && dom.parentNode !== undefined) { var rawParent = dom.parentNode; var parent = Element.fromDom(rawParent); ret.push(parent); if (stop(parent) === true) break; else dom = rawParent; } return ret; }; var siblings = function (element) { // TODO: Refactor out children so we can just not add self instead of filtering afterwards var filterSelf = function (elements) { return Arr.filter(elements, function (x) { return !Compare.eq(element, x); }); }; return parent(element).map(children).map(filterSelf).getOr([]); }; var offsetParent = function (element) { var dom = element.dom(); return Option.from(dom.offsetParent).map(Element.fromDom); }; var prevSibling = function (element) { var dom = element.dom(); return Option.from(dom.previousSibling).map(Element.fromDom); }; var nextSibling = function (element) { var dom = element.dom(); return Option.from(dom.nextSibling).map(Element.fromDom); }; var prevSiblings = function (element) { // This one needs to be reversed, so they're still in DOM order return Arr.reverse(Recurse.toArray(element, prevSibling)); }; var nextSiblings = function (element) { return Recurse.toArray(element, nextSibling); }; var children = function (element) { var dom = element.dom(); return Arr.map(dom.childNodes, Element.fromDom); }; var child = function (element, index) { var children = element.dom().childNodes; return Option.from(children[index]).map(Element.fromDom); }; var firstChild = function (element) { return child(element, 0); }; var lastChild = function (element) { return child(element, element.dom().childNodes.length - 1); }; var spot = Struct.immutable('element', 'offset'); var leaf = function (element, offset) { var cs = children(element); return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); }; return { owner: owner, defaultView: defaultView, documentElement: documentElement, parent: parent, findIndex: findIndex, parents: parents, siblings: siblings, prevSibling: prevSibling, offsetParent: offsetParent, prevSiblings: prevSiblings, nextSibling: nextSibling, nextSiblings: nextSiblings, children: children, child: child, firstChild: firstChild, lastChild: lastChild, leaf: leaf }; } ); /** * CaretUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Utility functions shared by the caret logic. * * @private * @class tinymce.caret.CaretUtils */ define( 'tinymce.core.caret.CaretUtils', [ "tinymce.core.util.Fun", "tinymce.core.dom.TreeWalker", "tinymce.core.dom.NodeType", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretContainer", "tinymce.core.caret.CaretCandidate" ], function (Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) { var isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption'), isCaretContainer = CaretContainer.isCaretContainer, isCaretContainerBlock = CaretContainer.isCaretContainerBlock, curry = Fun.curry, isElement = NodeType.isElement, isCaretCandidate = CaretCandidate.isCaretCandidate; function isForwards(direction) { return direction > 0; } function isBackwards(direction) { return direction < 0; } function skipCaretContainers(walk, shallow) { var node; while ((node = walk(shallow))) { if (!isCaretContainerBlock(node)) { return node; } } return null; } function findNode(node, direction, predicateFn, rootNode, shallow) { var walker = new TreeWalker(node, rootNode); if (isBackwards(direction)) { if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { node = skipCaretContainers(walker.prev, true); if (predicateFn(node)) { return node; } } while ((node = skipCaretContainers(walker.prev, shallow))) { if (predicateFn(node)) { return node; } } } if (isForwards(direction)) { if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { node = skipCaretContainers(walker.next, true); if (predicateFn(node)) { return node; } } while ((node = skipCaretContainers(walker.next, shallow))) { if (predicateFn(node)) { return node; } } } return null; } function getEditingHost(node, rootNode) { for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { if (isContentEditableTrue(node)) { return node; } } return rootNode; } function getParentBlock(node, rootNode) { while (node && node != rootNode) { if (isBlockLike(node)) { return node; } node = node.parentNode; } return null; } function isInSameBlock(caretPosition1, caretPosition2, rootNode) { return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode); } function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) { return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode); } function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) { var container, offset; if (!caretPosition) { return null; } container = caretPosition.container(); offset = caretPosition.offset(); if (!isElement(container)) { return null; } return container.childNodes[offset + relativeOffset]; } function beforeAfter(before, node) { var range = node.ownerDocument.createRange(); if (before) { range.setStartBefore(node); range.setEndBefore(node); } else { range.setStartAfter(node); range.setEndAfter(node); } return range; } function isNodesInSameBlock(rootNode, node1, node2) { return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode); } function lean(left, rootNode, node) { var sibling, siblingName; if (left) { siblingName = 'previousSibling'; } else { siblingName = 'nextSibling'; } while (node && node != rootNode) { sibling = node[siblingName]; if (isCaretContainer(sibling)) { sibling = sibling[siblingName]; } if (isContentEditableFalse(sibling)) { if (isNodesInSameBlock(rootNode, sibling, node)) { return sibling; } break; } if (isCaretCandidate(sibling)) { break; } node = node.parentNode; } return null; } var before = curry(beforeAfter, true); var after = curry(beforeAfter, false); function normalizeRange(direction, rootNode, range) { var node, container, offset, location; var leanLeft = curry(lean, true, rootNode); var leanRight = curry(lean, false, rootNode); container = range.startContainer; offset = range.startOffset; if (CaretContainer.isCaretContainerBlock(container)) { if (!isElement(container)) { container = container.parentNode; } location = container.getAttribute('data-mce-caret'); if (location == 'before') { node = container.nextSibling; if (isContentEditableFalse(node)) { return before(node); } } if (location == 'after') { node = container.previousSibling; if (isContentEditableFalse(node)) { return after(node); } } } if (!range.collapsed) { return range; } if (NodeType.isText(container)) { if (isCaretContainer(container)) { if (direction === 1) { node = leanRight(container); if (node) { return before(node); } node = leanLeft(container); if (node) { return after(node); } } if (direction === -1) { node = leanLeft(container); if (node) { return after(node); } node = leanRight(container); if (node) { return before(node); } } return range; } if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) { if (direction === 1) { node = leanRight(container); if (node) { return before(node); } } return range; } if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) { if (direction === -1) { node = leanLeft(container); if (node) { return after(node); } } return range; } if (offset === container.data.length) { node = leanRight(container); if (node) { return before(node); } return range; } if (offset === 0) { node = leanLeft(container); if (node) { return after(node); } return range; } } return range; } function isNextToContentEditableFalse(relativeOffset, caretPosition) { return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition)); } return { isForwards: isForwards, isBackwards: isBackwards, findNode: findNode, getEditingHost: getEditingHost, getParentBlock: getParentBlock, isInSameBlock: isInSameBlock, isInSameEditingHost: isInSameEditingHost, isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0), isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1), normalizeRange: normalizeRange }; } ); /** * CaretWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module contains logic for moving around a virtual caret in logical order within a DOM element. * * It ignores the most obvious invalid caret locations such as within a script element or within a * contentEditable=false element but it will return locations that isn't possible to render visually. * * @private * @class tinymce.caret.CaretWalker * @example * var caretWalker = new CaretWalker(rootElm); * * var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range)); * var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range)); */ define( 'tinymce.core.caret.CaretWalker', [ "tinymce.core.dom.NodeType", "tinymce.core.caret.CaretCandidate", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretUtils", "tinymce.core.util.Arr", "tinymce.core.util.Fun" ], function (NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) { var isContentEditableFalse = NodeType.isContentEditableFalse, isText = NodeType.isText, isElement = NodeType.isElement, isBr = NodeType.isBr, isForwards = CaretUtils.isForwards, isBackwards = CaretUtils.isBackwards, isCaretCandidate = CaretCandidate.isCaretCandidate, isAtomic = CaretCandidate.isAtomic, isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate; function getParents(node, rootNode) { var parents = []; while (node && node != rootNode) { parents.push(node); node = node.parentNode; } return parents; } function nodeAtIndex(container, offset) { if (container.hasChildNodes() && offset < container.childNodes.length) { return container.childNodes[offset]; } return null; } function getCaretCandidatePosition(direction, node) { if (isForwards(direction)) { if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { return CaretPosition.before(node); } if (isText(node)) { return CaretPosition(node, 0); } } if (isBackwards(direction)) { if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { return CaretPosition.after(node); } if (isText(node)) { return CaretPosition(node, node.data.length); } } if (isBackwards(direction)) { if (isBr(node)) { return CaretPosition.before(node); } return CaretPosition.after(node); } return CaretPosition.before(node); } // Jumps over BR elements|
a
->|a
function isBrBeforeBlock(node, rootNode) { var next; if (!NodeType.isBr(node)) { return false; } next = findCaretPosition(1, CaretPosition.after(node), rootNode); if (!next) { return false; } return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode); } function findCaretPosition(direction, startCaretPosition, rootNode) { var container, offset, node, nextNode, innerNode, rootContentEditableFalseElm, caretPosition; if (!isElement(rootNode) || !startCaretPosition) { return null; } if (startCaretPosition.isEqual(CaretPosition.after(rootNode)) && rootNode.lastChild) { caretPosition = CaretPosition.after(rootNode.lastChild); if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) { return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition; } } else { caretPosition = startCaretPosition; } container = caretPosition.container(); offset = caretPosition.offset(); if (isText(container)) { if (isBackwards(direction) && offset > 0) { return CaretPosition(container, --offset); } if (isForwards(direction) && offset < container.length) { return CaretPosition(container, ++offset); } node = container; } else { if (isBackwards(direction) && offset > 0) { nextNode = nodeAtIndex(container, offset - 1); if (isCaretCandidate(nextNode)) { if (!isAtomic(nextNode)) { innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, innerNode.data.length); } return CaretPosition.after(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, nextNode.data.length); } return CaretPosition.before(nextNode); } } if (isForwards(direction) && offset < container.childNodes.length) { nextNode = nodeAtIndex(container, offset); if (isCaretCandidate(nextNode)) { if (isBrBeforeBlock(nextNode, rootNode)) { return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode); } if (!isAtomic(nextNode)) { innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, 0); } return CaretPosition.before(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, 0); } return CaretPosition.after(nextNode); } } node = caretPosition.getNode(); } if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) { node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true); if (isEditableCaretCandidate(node)) { return getCaretCandidatePosition(direction, node); } } nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse)); if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { if (isForwards(direction)) { caretPosition = CaretPosition.after(rootContentEditableFalseElm); } else { caretPosition = CaretPosition.before(rootContentEditableFalseElm); } return caretPosition; } if (nextNode) { return getCaretCandidatePosition(direction, nextNode); } return null; } return function (rootNode) { return { /** * Returns the next logical caret position from the specificed input * caretPoisiton or null if there isn't any more positions left for example * at the end specified root element. * * @method next * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ next: function (caretPosition) { return findCaretPosition(1, caretPosition, rootNode); }, /** * Returns the previous logical caret position from the specificed input * caretPoisiton or null if there isn't any more positions left for example * at the end specified root element. * * @method prev * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ prev: function (caretPosition) { return findCaretPosition(-1, caretPosition, rootNode); } }; }; } ); /** * CaretFinder.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.caret.CaretFinder', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretWalker' ], function (Fun, Option, CaretPosition, CaretWalker) { var fromPosition = function (forward, rootElement, position) { var walker = new CaretWalker(rootElement); return Option.from(forward ? walker.next(position) : walker.prev(position)); }; var positionIn = function (forward, element) { var caretWalker = new CaretWalker(element); var startPos = forward ? CaretPosition.before(element) : CaretPosition.after(element); return Option.from(forward ? caretWalker.next(startPos) : caretWalker.prev(startPos)); }; return { fromPosition: fromPosition, positionIn: positionIn }; } ); /** * DeleteUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.DeleteUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.PredicateFind' ], function (Arr, Option, Compare, Element, Node, PredicateFind) { var toLookup = function (names) { var lookup = Arr.foldl(names, function (acc, name) { acc[name] = true; return acc; }, { }); return function (elm) { return lookup[Node.name(elm)] === true; }; }; var isTextBlock = toLookup([ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', 'section', 'hgroup', 'aside', 'nav', 'figure' ]); var isBeforeRoot = function (rootNode) { return function (elm) { return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode)); }; }; var getParentTextBlock = function (rootNode, elm) { return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, isTextBlock, isBeforeRoot(rootNode)) : Option.none(); }; return { getParentTextBlock: getParentTextBlock }; } ); define( 'ephox.sugar.api.search.SelectorFind', [ 'ephox.sugar.api.search.PredicateFind', 'ephox.sugar.api.search.Selectors', 'ephox.sugar.impl.ClosestOrAncestor' ], function (PredicateFind, Selectors, ClosestOrAncestor) { // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything var first = function (selector) { return Selectors.one(selector); }; var ancestor = function (scope, selector, isRoot) { return PredicateFind.ancestor(scope, function (e) { return Selectors.is(e, selector); }, isRoot); }; var sibling = function (scope, selector) { return PredicateFind.sibling(scope, function (e) { return Selectors.is(e, selector); }); }; var child = function (scope, selector) { return PredicateFind.child(scope, function (e) { return Selectors.is(e, selector); }); }; var descendant = function (scope, selector) { return Selectors.one(selector, scope); }; var closest = function (scope, selector, isRoot) { return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); }; return { first: first, ancestor: ancestor, sibling: sibling, child: child, descendant: descendant, closest: closest }; } ); define( 'ephox.sugar.api.search.SelectorExists', [ 'ephox.sugar.api.search.SelectorFind' ], function (SelectorFind) { var any = function (selector) { return SelectorFind.first(selector).isSome(); }; var ancestor = function (scope, selector, isRoot) { return SelectorFind.ancestor(scope, selector, isRoot).isSome(); }; var sibling = function (scope, selector) { return SelectorFind.sibling(scope, selector).isSome(); }; var child = function (scope, selector) { return SelectorFind.child(scope, selector).isSome(); }; var descendant = function (scope, selector) { return SelectorFind.descendant(scope, selector).isSome(); }; var closest = function (scope, selector, isRoot) { return SelectorFind.closest(scope, selector, isRoot).isSome(); }; return { any: any, ancestor: ancestor, sibling: sibling, child: child, descendant: descendant, closest: closest }; } ); /** * Empty.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.dom.Empty', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.SelectorExists', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.TreeWalker' ], function (Fun, Compare, Element, SelectorExists, CaretCandidate, NodeType, TreeWalker) { var hasWhitespacePreserveParent = function (rootNode, node) { var rootElement = Element.fromDom(rootNode); var startNode = Element.fromDom(node); return SelectorExists.ancestor(startNode, 'pre,code', Fun.curry(Compare.eq, rootElement)); }; var isWhitespace = function (rootNode, node) { return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false; }; var isNamedAnchor = function (node) { return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name'); }; var isContent = function (rootNode, node) { return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node); }; var isBookmark = NodeType.hasAttribute('data-mce-bookmark'); var isBogus = NodeType.hasAttribute('data-mce-bogus'); var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all'); var isEmptyNode = function (targetNode) { var walker, node, brCount = 0; if (isContent(targetNode, targetNode)) { return false; } else { node = targetNode.firstChild; if (!node) { return true; } walker = new TreeWalker(node, targetNode); do { if (isBogusAll(node)) { node = walker.next(true); continue; } if (isBogus(node)) { node = walker.next(); continue; } if (NodeType.isBr(node)) { brCount++; node = walker.next(); continue; } if (isContent(targetNode, node)) { return false; } node = walker.next(); } while (node); return brCount <= 1; } }; var isEmpty = function (elm) { return isEmptyNode(elm.dom()); }; return { isEmpty: isEmpty }; } ); /** * BlockBoundary.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockBoundary', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.katamari.api.Struct', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.PredicateFind', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Arr, Fun, Option, Options, Struct, Compare, Element, Node, PredicateFind, Traverse, CaretFinder, CaretPosition, DeleteUtils, Empty, NodeType) { var BlockPosition = Struct.immutable('block', 'position'); var BlockBoundary = Struct.immutable('from', 'to'); var getBlockPosition = function (rootNode, pos) { var rootElm = Element.fromDom(rootNode); var containerElm = Element.fromDom(pos.container()); return DeleteUtils.getParentTextBlock(rootElm, containerElm).map(function (block) { return BlockPosition(block, pos); }); }; var isDifferentBlocks = function (blockBoundary) { return Compare.eq(blockBoundary.from().block(), blockBoundary.to().block()) === false; }; var hasSameParent = function (blockBoundary) { return Traverse.parent(blockBoundary.from().block()).bind(function (parent1) { return Traverse.parent(blockBoundary.to().block()).filter(function (parent2) { return Compare.eq(parent1, parent2); }); }).isSome(); }; var isEditable = function (blockBoundary) { return NodeType.isContentEditableFalse(blockBoundary.from().block()) === false && NodeType.isContentEditableFalse(blockBoundary.to().block()) === false; }; var skipLastBr = function (rootNode, forward, blockPosition) { if (NodeType.isBr(blockPosition.position().getNode()) && Empty.isEmpty(blockPosition.block()) === false) { return CaretFinder.positionIn(false, blockPosition.block().dom()).bind(function (lastPositionInBlock) { if (lastPositionInBlock.isEqual(blockPosition.position())) { return CaretFinder.fromPosition(forward, rootNode, lastPositionInBlock).bind(function (to) { return getBlockPosition(rootNode, to); }); } else { return Option.some(blockPosition); } }).getOr(blockPosition); } else { return blockPosition; } }; var readFromRange = function (rootNode, forward, rng) { var fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng)); var toBlockPos = fromBlockPos.bind(function (blockPos) { return CaretFinder.fromPosition(forward, rootNode, blockPos.position()).bind(function (to) { return getBlockPosition(rootNode, to).map(function (blockPos) { return skipLastBr(rootNode, forward, blockPos); }); }); }); return Options.liftN([fromBlockPos, toBlockPos], BlockBoundary).filter(function (blockBoundary) { return isDifferentBlocks(blockBoundary) && hasSameParent(blockBoundary) && isEditable(blockBoundary); }); }; var read = function (rootNode, forward, rng) { return rng.collapsed ? readFromRange(rootNode, forward, rng) : Option.none(); }; return { read: read }; } ); define( 'ephox.sugar.api.dom.Insert', [ 'ephox.sugar.api.search.Traverse' ], function (Traverse) { var before = function (marker, element) { var parent = Traverse.parent(marker); parent.each(function (v) { v.dom().insertBefore(element.dom(), marker.dom()); }); }; var after = function (marker, element) { var sibling = Traverse.nextSibling(marker); sibling.fold(function () { var parent = Traverse.parent(marker); parent.each(function (v) { append(v, element); }); }, function (v) { before(v, element); }); }; var prepend = function (parent, element) { var firstChild = Traverse.firstChild(parent); firstChild.fold(function () { append(parent, element); }, function (v) { parent.dom().insertBefore(element.dom(), v.dom()); }); }; var append = function (parent, element) { parent.dom().appendChild(element.dom()); }; var appendAt = function (parent, element, index) { Traverse.child(parent, index).fold(function () { append(parent, element); }, function (v) { before(v, element); }); }; var wrap = function (element, wrapper) { before(element, wrapper); append(wrapper, element); }; return { before: before, after: after, prepend: prepend, append: append, appendAt: appendAt, wrap: wrap }; } ); define( 'ephox.sugar.api.dom.InsertAll', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.Insert' ], function (Arr, Insert) { var before = function (marker, elements) { Arr.each(elements, function (x) { Insert.before(marker, x); }); }; var after = function (marker, elements) { Arr.each(elements, function (x, i) { var e = i === 0 ? marker : elements[i - 1]; Insert.after(e, x); }); }; var prepend = function (parent, elements) { Arr.each(elements.slice().reverse(), function (x) { Insert.prepend(parent, x); }); }; var append = function (parent, elements) { Arr.each(elements, function (x) { Insert.append(parent, x); }); }; return { before: before, after: after, prepend: prepend, append: append }; } ); define( 'ephox.sugar.api.dom.Remove', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.dom.InsertAll', 'ephox.sugar.api.search.Traverse' ], function (Arr, InsertAll, Traverse) { var empty = function (element) { // shortcut "empty node" trick. Requires IE 9. element.dom().textContent = ''; // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general // than removing every child node manually. // The following is (probably) safe for performance as 99.9% of the time the trick works and // Traverse.children will return an empty array. Arr.each(Traverse.children(element), function (rogue) { remove(rogue); }); }; var remove = function (element) { var dom = element.dom(); if (dom.parentNode !== null) dom.parentNode.removeChild(dom); }; var unwrap = function (wrapper) { var children = Traverse.children(wrapper); if (children.length > 0) InsertAll.before(wrapper, children); remove(wrapper); }; return { empty: empty, remove: remove, unwrap: unwrap }; } ); /** * MergeBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.MergeBlocks', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Arr, Option, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, Empty, NodeType) { var mergeBlocksAndReposition = function (forward, fromBlock, toBlock, toPosition) { var children = Traverse.children(fromBlock); if (NodeType.isBr(toPosition.getNode())) { Remove.remove(Element.fromDom(toPosition.getNode())); toPosition = CaretFinder.positionIn(false, toBlock.dom()).getOr(); } Arr.each(children, function (node) { Insert.append(toBlock, node); }); if (Empty.isEmpty(fromBlock)) { Remove.remove(fromBlock); } return children.length > 0 ? Option.from(toPosition) : Option.none(); }; var mergeBlocks = function (forward, block1, block2) { if (forward) { return CaretFinder.positionIn(false, block1.dom()).bind(function (toPosition) { return mergeBlocksAndReposition(forward, block2, block1, toPosition); }); } else { return CaretFinder.positionIn(false, block2.dom()).bind(function (toPosition) { return mergeBlocksAndReposition(forward, block1, block2, toPosition); }); } }; return { mergeBlocks: mergeBlocks }; } ); /** * BlockBoundaryDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockBoundaryDelete', [ 'tinymce.core.delete.BlockBoundary', 'tinymce.core.delete.MergeBlocks' ], function (BlockBoundary, MergeBlocks) { var backspaceDelete = function (editor, forward) { var position; position = BlockBoundary.read(editor.getBody(), forward, editor.selection.getRng()).bind(function (blockBoundary) { return MergeBlocks.mergeBlocks(forward, blockBoundary.from().block(), blockBoundary.to().block()); }); position.each(function (pos) { editor.selection.setRng(pos.toRange()); }); return position.isSome(); }; return { backspaceDelete: backspaceDelete }; } ); /** * BlockRangeDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockRangeDelete', [ 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.delete.MergeBlocks' ], function (Options, Compare, Element, DeleteUtils, MergeBlocks) { var deleteRange = function (rootNode, selection) { var rng = selection.getRng(); return Options.liftN([ DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.startContainer)), DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.endContainer)) ], function (block1, block2) { if (Compare.eq(block1, block2) === false) { rng.deleteContents(); MergeBlocks.mergeBlocks(true, block1, block2).each(function (pos) { selection.setRng(pos.toRange()); }); return true; } else { return false; } }).getOr(false); }; var backspaceDelete = function (editor, forward) { var rootNode = Element.fromDom(editor.getBody()); if (editor.selection.isCollapsed() === false) { return deleteRange(rootNode, editor.selection); } else { return false; } }; return { backspaceDelete: backspaceDelete }; } ); define( 'ephox.katamari.api.Adt', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Type', 'global!Array', 'global!Error', 'global!console' ], function (Arr, Obj, Type, Array, Error, console) { /* * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) * For syntax and use, look at the test code. */ var generate = function (cases) { // validation if (!Type.isArray(cases)) { throw new Error('cases must be an array'); } if (cases.length === 0) { throw new Error('there must be at least one case'); } var constructors = [ ]; // adt is mutated to add the individual cases var adt = {}; Arr.each(cases, function (acase, count) { var keys = Obj.keys(acase); // validation if (keys.length !== 1) { throw new Error('one and only one name per case'); } var key = keys[0]; var value = acase[key]; // validation if (adt[key] !== undefined) { throw new Error('duplicate key detected:' + key); } else if (key === 'cata') { throw new Error('cannot have a case named cata (sorry)'); } else if (!Type.isArray(value)) { // this implicitly checks if acase is an object throw new Error('case arguments must be an array'); } constructors.push(key); // // constructor for key // adt[key] = function () { var argLength = arguments.length; // validation if (argLength !== value.length) { throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); } // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var args = new Array(argLength); for (var i = 0; i < args.length; i++) args[i] = arguments[i]; var match = function (branches) { var branchKeys = Obj.keys(branches); if (constructors.length !== branchKeys.length) { throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); } var allReqd = Arr.forall(constructors, function (reqKey) { return Arr.contains(branchKeys, reqKey); }); if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); return branches[key].apply(null, args); }; // // the fold function for key // return { fold: function (/* arguments */) { // runtime validation if (arguments.length !== cases.length) { throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + arguments.length); } var target = arguments[count]; return target.apply(null, args); }, match: match, // NOTE: Only for debugging. log: function (label) { console.log(label, { constructors: constructors, constructor: key, params: args }); } }; }; }); return adt; }; return { generate: generate }; } ); /** * CefDeleteAction.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.CefDeleteAction', [ 'ephox.katamari.api.Adt', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) { var DeleteAction = Adt.generate([ { remove: [ 'element' ] }, { moveToElement: [ 'element' ] }, { moveToPosition: [ 'position' ] } ]); var isAtContentEditableBlockCaret = function (forward, from) { var elm = from.getNode(forward === false); var caretLocation = forward ? 'after' : 'before'; return NodeType.isElement(elm) && elm.getAttribute('data-mce-caret') === caretLocation; }; var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { var toCefElm = to.getNode(forward === false); return DeleteUtils.getParentTextBlock(Element.fromDom(rootNode), Element.fromDom(from.getNode())).map(function (blockElm) { return Empty.isEmpty(blockElm) ? DeleteAction.remove(blockElm.dom()) : DeleteAction.moveToElement(toCefElm); }).orThunk(function () { return Option.some(DeleteAction.moveToElement(toCefElm)); }); }; var findCefPosition = function (rootNode, forward, from) { return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) { if (forward && NodeType.isContentEditableFalse(to.getNode())) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else if (forward === false && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else { return Option.none(); } }); }; var getContentEditableBlockAction = function (forward, elm) { if (forward && NodeType.isContentEditableFalse(elm.nextSibling)) { return Option.some(DeleteAction.moveToElement(elm.nextSibling)); } else if (forward === false && NodeType.isContentEditableFalse(elm.previousSibling)) { return Option.some(DeleteAction.moveToElement(elm.previousSibling)); } else { return Option.none(); } }; var getContentEditableAction = function (rootNode, forward, from) { if (isAtContentEditableBlockCaret(forward, from)) { return getContentEditableBlockAction(forward, from.getNode(forward === false)) .fold( function () { return findCefPosition(rootNode, forward, from); }, Option.some ); } else { return findCefPosition(rootNode, forward, from); } }; var read = function (rootNode, forward, rng) { var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); var from = CaretPosition.fromRangeStart(normalizedRange); if (forward === false && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode(true))); } else if (forward && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode())); } else { return getContentEditableAction(rootNode, forward, from); } }; return { read: read }; } ); /** * Bidi.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.text.Bidi', [ ], function () { var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; var hasStrongRtl = function (text) { return strongRtl.test(text); }; return { hasStrongRtl: hasStrongRtl }; } ); /** * InlineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InlineUtils', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.dom.DOMUtils', 'tinymce.core.text.Bidi' ], function (Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, Bidi) { var isInlineTarget = function (elm) { return DOMUtils.DOM.is(elm, 'a[href],code'); }; var isRtl = function (element) { return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent); }; var findInline = function (rootNode, pos) { return Option.from(DOMUtils.DOM.getParent(pos.container(), isInlineTarget, rootNode)); }; var hasSameParentBlock = function (rootNode, node1, node2) { var block1 = CaretUtils.getParentBlock(node1, rootNode); var block2 = CaretUtils.getParentBlock(node2, rootNode); return block1 && block1 === block2; }; var isInInline = function (rootNode, pos) { return pos ? findInline(rootNode, pos).isSome() : false; }; var isAtInlineEndPoint = function (rootNode, pos) { return findInline(rootNode, pos).map(function (inline) { return findCaretPosition(inline, false, pos).isNone() || findCaretPosition(inline, true, pos).isNone(); }).getOr(false); }; var isAtZwsp = function (pos) { return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos); }; var findCaretPositionIn = function (node, forward) { return CaretFinder.positionIn(forward, node); }; var findCaretPosition = function (rootNode, forward, from) { return CaretFinder.fromPosition(forward, rootNode, from); }; var normalizePosition = function (forward, pos) { var container = pos.container(), offset = pos.offset(); if (forward) { return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos; } else { return CaretContainer.isAfterInline(pos) ? new CaretPosition(container, offset - 1) : pos; } }; var normalizeForwards = Fun.curry(normalizePosition, true); var normalizeBackwards = Fun.curry(normalizePosition, false); return { isInlineTarget: isInlineTarget, findInline: findInline, isInInline: isInInline, isRtl: isRtl, isAtInlineEndPoint: isAtInlineEndPoint, isAtZwsp: isAtZwsp, findCaretPositionIn: findCaretPositionIn, findCaretPosition: findCaretPosition, normalizePosition: normalizePosition, normalizeForwards: normalizeForwards, normalizeBackwards: normalizeBackwards, hasSameParentBlock: hasSameParentBlock }; } ); /** * DeleteElement.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.DeleteElement', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.PredicateFind', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.InlineUtils' ], function (Fun, Option, Insert, Remove, Element, Node, PredicateFind, CaretCandidate, CaretPosition, Empty, NodeType, InlineUtils) { var needsReposition = function (pos, elm) { var container = pos.container(); var offset = pos.offset(); return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); }; var reposition = function (elm, pos) { return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; }; var beforeOrStartOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); }; var afterOrEndOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); }; var getPreviousSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { return Option.some(afterOrEndOf(elm.previousSibling)); } else { return elm.previousSibling ? InlineUtils.findCaretPositionIn(elm.previousSibling, false) : Option.none(); } }; var getNextSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { return Option.some(beforeOrStartOf(elm.nextSibling)); } else { return elm.nextSibling ? InlineUtils.findCaretPositionIn(elm.nextSibling, true) : Option.none(); } }; var findCaretPositionBackwardsFromElm = function (rootElement, elm) { var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); return InlineUtils.findCaretPosition(rootElement, false, startPosition).fold( function () { return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)); }, Option.some ); }; var findCaretPositionForwardsFromElm = function (rootElement, elm) { return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)).fold( function () { return InlineUtils.findCaretPosition(rootElement, false, CaretPosition.before(elm)); }, Option.some ); }; var findCaretPositionBackwards = function (rootElement, elm) { return getPreviousSiblingCaretPosition(elm).orThunk(function () { return getNextSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionBackwardsFromElm(rootElement, elm); }); }; var findCaretPositionForward = function (rootElement, elm) { return getNextSiblingCaretPosition(elm).orThunk(function () { return getPreviousSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionForwardsFromElm(rootElement, elm); }); }; var findCaretPosition = function (forward, rootElement, elm) { return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); }; var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); }; var setSelection = function (editor, forward, pos) { pos.fold( function () { editor.focus(); }, function (pos) { editor.selection.setRng(pos.toRange(), forward); } ); }; var eqRawNode = function (rawNode) { return function (elm) { return elm.dom() === rawNode; }; }; var isBlock = function (editor, elm) { return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); }; var paddEmptyBlock = function (elm) { if (Empty.isEmpty(elm)) { var br = Element.fromHtml('a
b
// It would become this range in webkit: //[a
]b
// We would want it to be: //[a]
b
// Since it would otherwise produces spans out of thin air on insertContent for example. var normalizeBlockSelection = function (rng) { var startPos = CaretPosition.fromRangeStart(rng); var endPos = CaretPosition.fromRangeEnd(rng); var rootNode = rng.commonAncestorContainer; if (rng.collapsed === false && matchEndContainer(rng, isTextBlock) && rng.endOffset === 0) { return CaretFinder.fromPosition(false, rootNode, endPos) .map(function (newEndPos) { if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) { return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); } else { return rng; } }).getOr(rng); } else { return rng; } }; var normalize = function (rng) { return normalizeBlockSelection(rng); }; return { normalize: normalize }; } ); /** * InsertList.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Handles inserts of lists into the editor instance. * * @class tinymce.InsertList * @private */ define( 'tinymce.core.InsertList', [ "tinymce.core.util.Tools", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition" ], function (Tools, CaretWalker, CaretPosition) { var isListFragment = function (fragment) { var firstChild = fragment.firstChild; var lastChild = fragment.lastChild; // Skip meta since it's likely|
rng = selection.getRng(); var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); var body = editor.getBody(); if (caretElement === body && selection.isCollapsed()) { if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) { rng = dom.createRng(); rng.setStart(body.firstChild, 0); rng.setEnd(body.firstChild, 0); selection.setRng(rng); } } // Insert node maker where we will insert the new HTML and get it's parent if (!selection.isCollapsed()) { // Fix for #2595 seems that delete removes one extra character on // WebKit for some odd reason if you double click select a word editor.selection.setRng(RangeNormalizer.normalize(editor.selection.getRng())); editor.getDoc().execCommand('Delete', false, null); trimNbspAfterDeleteAndPaddValue(); } parentNode = selection.getNode(); // Parse the fragment within the context of the parent node var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data }; fragment = parser.parse(value, parserArgs); // Custom handling of lists if (details.paste === true && InsertList.isListFragment(fragment) && InsertList.isParentBlockLi(dom, parentNode)) { rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment); editor.selection.setRng(rng); editor.fire('SetContent', args); return; } markFragmentElements(fragment); // Move the caret to a more suitable location node = fragment.lastChild; if (node.attr('id') == 'mce_marker') { marker = node; for (node = node.prev; node; node = node.walk(true)) { if (node.type == 3 || !dom.isBlock(node.name)) { if (editor.schema.isValidChild(node.parent.name, 'span')) { node.parent.insert(marker, node, node.name === 'br'); } break; } } } editor._selectionOverrides.showBlockCaretContainer(parentNode); // If parser says valid we can insert the contents into that parent if (!parserArgs.invalid) { value = serializer.serialize(fragment); validInsertion(editor, value, parentNode); } else { // If the fragment was invalid within that context then we need // to parse and process the parent it's inserted into // Insert bookmark node and get the parent selection.setContent(bookmarkHtml); parentNode = selection.getNode(); rootNode = editor.getBody(); // Opera will return the document node when selection is in root if (parentNode.nodeType == 9) { parentNode = node = rootNode; } else { node = parentNode; } // Find the ancestor just before the root element while (node !== rootNode) { parentNode = node; node = node.parentNode; } // Get the outer/inner HTML depending on if we are in the root and parser and serialize that value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); value = serializer.serialize( parser.parse( // Need to replace by using a function since $ in the contents would otherwise be a problem value.replace(//i, function () { return serializer.serialize(fragment); }) ) ); // Set the inner/outer HTML depending on if we are in the root or not if (parentNode == rootNode) { dom.setHTML(rootNode, value); } else { dom.setOuterHTML(parentNode, value); } } reduceInlineTextElements(); moveSelectionToMarker(dom.get('mce_marker')); umarkFragmentElements(editor.getBody()); editor.fire('SetContent', args); editor.addVisual(); }; var processValue = function (value) { var details; if (typeof value !== 'string') { details = Tools.extend({ paste: value.paste, data: { paste: value.paste } }, value); return { content: value.content, details: details }; } return { content: value, details: {} }; }; var insertAtCaret = function (editor, value) { var result = processValue(value); insertHtmlAtCaret(editor, result.content, result.details); }; return { insertAtCaret: insertAtCaret }; } ); /** * EditorCommands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class enables you to add custom editor commands and it contains * overrides for native browser commands to address various bugs and issues. * * @class tinymce.EditorCommands */ define( 'tinymce.core.EditorCommands', [ 'tinymce.core.delete.DeleteCommands', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', 'tinymce.core.Env', 'tinymce.core.InsertContent', 'tinymce.core.util.Tools' ], function (DeleteCommands, NodeType, RangeUtils, TreeWalker, Env, InsertContent, Tools) { // Added for compression purposes var each = Tools.each, extend = Tools.extend; var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; var isOldIE = Env.ie && Env.ie < 11; var TRUE = true, FALSE = false; return function (editor) { var dom, selection, formatter, commands = { state: {}, exec: {}, value: {} }, settings = editor.settings, bookmark; editor.on('PreInit', function () { dom = editor.dom; selection = editor.selection; settings = editor.settings; formatter = editor.formatter; }); /** * Executes the specified command. * * @method execCommand * @param {String} command Command to execute. * @param {Boolean} ui Optional user interface state. * @param {Object} value Optional value for command. * @param {Object} args Optional extra arguments to the execCommand. * @return {Boolean} true/false if the command was found or not. */ function execCommand(command, ui, value, args) { var func, customCommand, state = 0; if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { editor.focus(); } args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value }); if (args.isDefaultPrevented()) { return false; } customCommand = command.toLowerCase(); if ((func = commands.exec[customCommand])) { func(customCommand, ui, value); editor.fire('ExecCommand', { command: command, ui: ui, value: value }); return true; } // Plugin commands each(editor.plugins, function (p) { if (p.execCommand && p.execCommand(command, ui, value)) { editor.fire('ExecCommand', { command: command, ui: ui, value: value }); state = true; return false; } }); if (state) { return state; } // Theme commands if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) { editor.fire('ExecCommand', { command: command, ui: ui, value: value }); return true; } // Browser commands try { state = editor.getDoc().execCommand(command, ui, value); } catch (ex) { // Ignore old IE errors } if (state) { editor.fire('ExecCommand', { command: command, ui: ui, value: value }); return true; } return false; } /** * Queries the current state for a command for example if the current selection is "bold". * * @method queryCommandState * @param {String} command Command to check the state of. * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. */ function queryCommandState(command) { var func; // Is hidden then return undefined if (editor.quirks.isHidden()) { return; } command = command.toLowerCase(); if ((func = commands.state[command])) { return func(command); } // Browser commands try { return editor.getDoc().queryCommandState(command); } catch (ex) { // Fails sometimes see bug: 1896577 } return false; } /** * Queries the command value for example the current fontsize. * * @method queryCommandValue * @param {String} command Command to check the value of. * @return {Object} Command value of false if it's not found. */ function queryCommandValue(command) { var func; // Is hidden then return undefined if (editor.quirks.isHidden()) { return; } command = command.toLowerCase(); if ((func = commands.value[command])) { return func(command); } // Browser commands try { return editor.getDoc().queryCommandValue(command); } catch (ex) { // Fails sometimes see bug: 1896577 } } /** * Adds commands to the command collection. * * @method addCommands * @param {Object} commandList Name/value collection with commands to add, the names can also be comma separated. * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. */ function addCommands(commandList, type) { type = type || 'exec'; each(commandList, function (callback, command) { each(command.toLowerCase().split(','), function (command) { commands[type][command] = callback; }); }); } function addCommand(command, callback, scope) { command = command.toLowerCase(); commands.exec[command] = function (command, ui, value, args) { return callback.call(scope || editor, ui, value, args); }; } /** * Returns true/false if the command is supported or not. * * @method queryCommandSupported * @param {String} command Command that we check support for. * @return {Boolean} true/false if the command is supported or not. */ function queryCommandSupported(command) { command = command.toLowerCase(); if (commands.exec[command]) { return true; } // Browser commands try { return editor.getDoc().queryCommandSupported(command); } catch (ex) { // Fails sometimes see bug: 1896577 } return false; } function addQueryStateHandler(command, callback, scope) { command = command.toLowerCase(); commands.state[command] = function () { return callback.call(scope || editor); }; } function addQueryValueHandler(command, callback, scope) { command = command.toLowerCase(); commands.value[command] = function () { return callback.call(scope || editor); }; } function hasCustomCommand(command) { command = command.toLowerCase(); return !!commands.exec[command]; } // Expose public methods extend(this, { execCommand: execCommand, queryCommandState: queryCommandState, queryCommandValue: queryCommandValue, queryCommandSupported: queryCommandSupported, addCommands: addCommands, addCommand: addCommand, addQueryStateHandler: addQueryStateHandler, addQueryValueHandler: addQueryValueHandler, hasCustomCommand: hasCustomCommand }); // Private methods function execNativeCommand(command, ui, value) { if (ui === undefined) { ui = FALSE; } if (value === undefined) { value = null; } return editor.getDoc().execCommand(command, ui, value); } function isFormatMatch(name) { return formatter.match(name); } function toggleFormat(name, value) { formatter.toggle(name, value ? { value: value } : undefined); editor.nodeChanged(); } function storeSelection(type) { bookmark = selection.getBookmark(type); } function restoreSelection() { selection.moveToBookmark(bookmark); } // Add execCommand overrides addCommands({ // Ignore these, added for compatibility 'mceResetDesignMode,mceBeginUndoLevel': function () { }, // Add undo manager logic 'mceEndUndoLevel,mceAddUndoLevel': function () { editor.undoManager.add(); }, 'Cut,Copy,Paste': function (command) { var doc = editor.getDoc(), failed; // Try executing the native command try { execNativeCommand(command); } catch (ex) { // Command failed failed = TRUE; } // Chrome reports the paste command as supported however older IE:s will return false for cut/paste if (command === 'paste' && !doc.queryCommandEnabled(command)) { failed = true; } // Present alert message about clipboard access not being available if (failed || !doc.queryCommandSupported(command)) { var msg = editor.translate( "Your browser doesn't support direct access to the clipboard. " + "Please use the Ctrl+X/C/V keyboard shortcuts instead." ); if (Env.mac) { msg = msg.replace(/Ctrl\+/g, '\u2318+'); } editor.notificationManager.open({ text: msg, type: 'error' }); } }, // Override unlink command unlink: function () { if (selection.isCollapsed()) { var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); if (elm) { editor.dom.remove(elm, true); } return; } formatter.remove("link"); }, // Override justify commands to use the text formatter engine 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) { var align = command.substring(7); if (align == 'full') { align = 'justify'; } // Remove all other alignments first each('left,center,right,justify'.split(','), function (name) { if (align != name) { formatter.remove('align' + name); } }); if (align != 'none') { toggleFormat('align' + align); } }, // Override list commands to fix WebKit bug 'InsertUnorderedList,InsertOrderedList': function (command) { var listElm, listParent; execNativeCommand(command); // WebKit produces lists within block elements so we need to split them // we will replace the native list creation logic to custom logic later on // TODO: Remove this when the list creation logic is removed listElm = dom.getParent(selection.getNode(), 'ol,ul'); if (listElm) { listParent = listElm.parentNode; // If list is within a text block then split that block if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { storeSelection(); dom.split(listParent, listElm); restoreSelection(); } } }, // Override commands to use the text formatter engine 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { toggleFormat(command); }, // Override commands to use the text formatter engine 'ForeColor,HiliteColor,FontName': function (command, ui, value) { toggleFormat(command, value); }, FontSize: function (command, ui, value) { var fontClasses, fontSizes; // Convert font size 1-7 to styles if (value >= 1 && value <= 7) { fontSizes = explode(settings.font_size_style_values); fontClasses = explode(settings.font_size_classes); if (fontClasses) { value = fontClasses[value - 1] || value; } else { value = fontSizes[value - 1] || value; } } toggleFormat(command, value); }, RemoveFormat: function (command) { formatter.remove(command); }, mceBlockQuote: function () { toggleFormat('blockquote'); }, FormatBlock: function (command, ui, value) { return toggleFormat(value || 'p'); }, mceCleanup: function () { var bookmark = selection.getBookmark(); editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE }); selection.moveToBookmark(bookmark); }, mceRemoveNode: function (command, ui, value) { var node = value || selection.getNode(); // Make sure that the body node isn't removed if (node != editor.getBody()) { storeSelection(); editor.dom.remove(node, TRUE); restoreSelection(); } }, mceSelectNodeDepth: function (command, ui, value) { var counter = 0; dom.getParent(selection.getNode(), function (node) { if (node.nodeType == 1 && counter++ == value) { selection.select(node); return FALSE; } }, editor.getBody()); }, mceSelectNode: function (command, ui, value) { selection.select(value); }, mceInsertContent: function (command, ui, value) { InsertContent.insertAtCaret(editor, value); }, mceInsertRawHTML: function (command, ui, value) { selection.setContent('tiny_mce_marker'); editor.setContent( editor.getContent().replace(/tiny_mce_marker/g, function () { return value; }) ); }, mceToggleFormat: function (command, ui, value) { toggleFormat(value); }, mceSetContent: function (command, ui, value) { editor.setContent(value); }, 'Indent,Outdent': function (command) { var intentValue, indentUnit, value; // Setup indent level intentValue = settings.indentation; indentUnit = /[a-z%]+$/i.exec(intentValue); intentValue = parseInt(intentValue, 10); if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { // If forced_root_blocks is set to false we don't have a block to indent so lets create a div if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { formatter.apply('div'); } each(selection.getSelectedBlocks(), function (element) { if (dom.getContentEditable(element) === "false") { return; } if (element.nodeName !== "LI") { var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName; indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; if (command == 'outdent') { value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); } else { value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; dom.setStyle(element, indentStyleName, value); } } }); } else { execNativeCommand(command); } }, mceRepaint: function () { }, InsertHorizontalRule: function () { editor.execCommand('mceInsertContent', false, '|
rng = selection.getRng(); if (!rng.item) { rng.moveToElementText(root); rng.select(); } } }, "delete": function () { DeleteCommands.deleteCommand(editor); }, "forwardDelete": function () { DeleteCommands.forwardDeleteCommand(editor); }, mceNewDocument: function () { editor.setContent(''); }, InsertLineBreak: function (command, ui, value) { // We load the current event in from EnterKey.js when appropriate to heed // certain event-specific variations such as ctrl-enter in a list var evt = value; var brElm, extraBr, marker; var rng = selection.getRng(true); new RangeUtils(dom).normalize(rng); var offset = rng.startOffset; var container = rng.startContainer; // Resolve node index if (container.nodeType == 1 && container.hasChildNodes()) { var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; if (isAfterLastNodeInContainer && container.nodeType == 3) { offset = container.nodeValue.length; } else { offset = 0; } } var parentBlock = dom.getParent(container, dom.isBlock); var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 // Enter inside block contained within a LI then split or insert before/after LI var isControlKey = evt && evt.ctrlKey; if (containerBlockName == 'LI' && !isControlKey) { parentBlock = containerBlock; parentBlockName = containerBlockName; } // Walks the parent block to the right and look for BR elements function hasRightSideContent() { var walker = new TreeWalker(container, parentBlock), node; var nonEmptyElementsMap = editor.schema.getNonEmptyElements(); while ((node = walker.next())) { if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { return true; } } } if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { // Insert extra BR element at the end block elements if (!isOldIE && !hasRightSideContent()) { brElm = dom.create('br'); rng.insertNode(brElm); rng.setStartAfter(brElm); rng.setEndAfter(brElm); extraBr = true; } } brElm = dom.create('br'); rng.insertNode(brElm); // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it var documentMode = dom.doc.documentMode; if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); } // Insert temp marker and scroll to that marker = dom.create('span', {}, ' '); brElm.parentNode.insertBefore(marker, brElm); selection.scrollIntoView(marker); dom.remove(marker); if (!extraBr) { rng.setStartAfter(brElm); rng.setEndAfter(brElm); } else { rng.setStartBefore(brElm); rng.setEndBefore(brElm); } selection.setRng(rng); editor.undoManager.add(); return TRUE; } }); // Add queryCommandState overrides addCommands({ // Override justify commands 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function (command) { var name = 'align' + command.substring(7); var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); var matches = map(nodes, function (node) { return !!formatter.matchNode(node, name); }); return inArray(matches, TRUE) !== -1; }, 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { return isFormatMatch(command); }, mceBlockQuote: function () { return isFormatMatch('blockquote'); }, Outdent: function () { var node; if (settings.inline_styles) { if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { return TRUE; } if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { return TRUE; } } return ( queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) ); }, 'InsertUnorderedList,InsertOrderedList': function (command) { var list = dom.getParent(selection.getNode(), 'ul,ol'); return list && ( command === 'insertunorderedlist' && list.tagName === 'UL' || command === 'insertorderedlist' && list.tagName === 'OL' ); } }, 'state'); // Add queryCommandValue overrides addCommands({ 'FontSize,FontName': function (command) { var value = 0, parent; if ((parent = dom.getParent(selection.getNode(), 'span'))) { if (command == 'fontsize') { value = parent.style.fontSize; } else { value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); } } return value; } }, 'value'); // Add undo manager logic addCommands({ Undo: function () { editor.undoManager.undo(); }, Redo: function () { editor.undoManager.redo(); } }); }; } ); /** * URI.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles parsing, modification and serialization of URI/URL strings. * @class tinymce.util.URI */ define( 'tinymce.core.util.URI', [ 'global!document', 'tinymce.core.util.Tools' ], function (document, Tools) { var each = Tools.each, trim = Tools.trim; var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' '); var DEFAULT_PORTS = { 'ftp': 21, 'http': 80, 'https': 443, 'mailto': 25 }; /** * Constructs a new URI instance. * * @constructor * @method URI * @param {String} url URI string to parse. * @param {Object} settings Optional settings object. */ function URI(url, settings) { var self = this, baseUri, baseUrl; url = trim(url); settings = self.settings = settings || {}; baseUri = settings.base_uri; // Strange app protocol that isn't http/https or local anchor // For example: mailto,skype,tel etc. if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { self.source = url; return; } var isProtocolRelative = url.indexOf('//') === 0; // Absolute path with no host, fake host and protocol if (url.indexOf('/') === 0 && !isProtocolRelative) { url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; } // Relative path http:// or protocol relative //path if (!/^[\w\-]*:?\/\//.test(url)) { baseUrl = settings.base_uri ? settings.base_uri.path : new URI(document.location.href).directory; if (settings.base_uri.protocol === "") { url = '//mce_host' + self.toAbsPath(baseUrl, url); } else { url = /([^#?]*)([#?]?.*)/.exec(url); url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, url[1]) + url[2]; } } // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something /*jshint maxlen: 255 */ /*eslint max-len: 0 */ url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); each(queryParts, function (v, i) { var part = url[i]; // Zope 3 workaround, they use @@something if (part) { part = part.replace(/\(mce_at\)/g, '@@'); } self[v] = part; }); if (baseUri) { if (!self.protocol) { self.protocol = baseUri.protocol; } if (!self.userInfo) { self.userInfo = baseUri.userInfo; } if (!self.port && self.host === 'mce_host') { self.port = baseUri.port; } if (!self.host || self.host === 'mce_host') { self.host = baseUri.host; } self.source = ''; } if (isProtocolRelative) { self.protocol = ''; } //t.path = t.path || '/'; } URI.prototype = { /** * Sets the internal path part of the URI. * * @method setPath * @param {string} path Path string to set. */ setPath: function (path) { var self = this; path = /^(.*?)\/?(\w+)?$/.exec(path); // Update path parts self.path = path[0]; self.directory = path[1]; self.file = path[2]; // Rebuild source self.source = ''; self.getURI(); }, /** * Converts the specified URI into a relative URI based on the current URI instance location. * * @method toRelative * @param {String} uri URI to convert into a relative path/URI. * @return {String} Relative URI from the point specified in the current URI instance. * @example * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); */ toRelative: function (uri) { var self = this, output; if (uri === "./") { return uri; } uri = new URI(uri, { base_uri: self }); // Not on same domain/port or protocol if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || (self.protocol != uri.protocol && uri.protocol !== "")) { return uri.getURI(); } var tu = self.getURI(), uu = uri.getURI(); // Allow usage of the base_uri when relative_urls = true if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { return tu; } output = self.toRelPath(self.path, uri.path); // Add query if (uri.query) { output += '?' + uri.query; } // Add anchor if (uri.anchor) { output += '#' + uri.anchor; } return output; }, /** * Converts the specified URI into a absolute URI based on the current URI instance location. * * @method toAbsolute * @param {String} uri URI to convert into a relative path/URI. * @param {Boolean} noHost No host and protocol prefix. * @return {String} Absolute URI from the point specified in the current URI instance. * @example * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); */ toAbsolute: function (uri, noHost) { uri = new URI(uri, { base_uri: this }); return uri.getURI(noHost && this.isSameOrigin(uri)); }, /** * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they * won't match, if the port specifications differ. * * @method isSameOrigin * @param {tinymce.util.URI} uri Uri instance to compare. * @returns {Boolean} True if the origins are the same. */ isSameOrigin: function (uri) { if (this.host == uri.host && this.protocol == uri.protocol) { if (this.port == uri.port) { return true; } var defaultPort = DEFAULT_PORTS[this.protocol]; if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { return true; } } return false; }, /** * Converts a absolute path into a relative path. * * @method toRelPath * @param {String} base Base point to convert the path from. * @param {String} path Absolute path to convert into a relative path. */ toRelPath: function (base, path) { var items, breakPoint = 0, out = '', i, l; // Split the paths base = base.substring(0, base.lastIndexOf('/')); base = base.split('/'); items = path.split('/'); if (base.length >= items.length) { for (i = 0, l = base.length; i < l; i++) { if (i >= items.length || base[i] != items[i]) { breakPoint = i + 1; break; } } } if (base.length < items.length) { for (i = 0, l = items.length; i < l; i++) { if (i >= base.length || base[i] != items[i]) { breakPoint = i + 1; break; } } } if (breakPoint === 1) { return path; } for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { out += "../"; } for (i = breakPoint - 1, l = items.length; i < l; i++) { if (i != breakPoint - 1) { out += "/" + items[i]; } else { out += items[i]; } } return out; }, /** * Converts a relative path into a absolute path. * * @method toAbsPath * @param {String} base Base point to convert the path from. * @param {String} path Relative path to convert into an absolute path. */ toAbsPath: function (base, path) { var i, nb = 0, o = [], tr, outPath; // Split paths tr = /\/$/.test(path) ? '/' : ''; base = base.split('/'); path = path.split('/'); // Remove empty chunks each(base, function (k) { if (k) { o.push(k); } }); base = o; // Merge relURLParts chunks for (i = path.length - 1, o = []; i >= 0; i--) { // Ignore empty or . if (path[i].length === 0 || path[i] === ".") { continue; } // Is parent if (path[i] === '..') { nb++; continue; } // Move up if (nb > 0) { nb--; continue; } o.push(path[i]); } i = base.length - nb; // If /a/b/c or / if (i <= 0) { outPath = o.reverse().join('/'); } else { outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); } // Add front / if it's needed if (outPath.indexOf('/') !== 0) { outPath = '/' + outPath; } // Add traling / if it's needed if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { outPath += tr; } return outPath; }, /** * Returns the full URI of the internal structure. * * @method getURI * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. */ getURI: function (noProtoHost) { var s, self = this; // Rebuild source if (!self.source || noProtoHost) { s = ''; if (!noProtoHost) { if (self.protocol) { s += self.protocol + '://'; } else { s += '//'; } if (self.userInfo) { s += self.userInfo + '@'; } if (self.host) { s += self.host; } if (self.port) { s += ':' + self.port; } } if (self.path) { s += self.path; } if (self.query) { s += '?' + self.query; } if (self.anchor) { s += '#' + self.anchor; } self.source = s; } return self.source; } }; URI.parseDataUri = function (uri) { var type, matches; uri = decodeURIComponent(uri).split(','); matches = /data:([^;]+)/.exec(uri[0]); if (matches) { type = matches[1]; } return { type: type, data: uri[1] }; }; URI.getDocumentBaseUrl = function (loc) { var baseUrl; // Pass applewebdata:// and other non web protocols though if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') { baseUrl = loc.href; } else { baseUrl = loc.protocol + '//' + loc.host + loc.pathname; } if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); if (!/[\/\\]$/.test(baseUrl)) { baseUrl += '/'; } } return baseUrl; }; return URI; } ); /** * Class.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This utilitiy class is used for easier inheritance. * * Features: * * Exposed super functions: this._super(); * * Mixins * * Dummy functions * * Property functions: var value = object.value(); and object.value(newValue); * * Static functions * * Defaults settings */ define( 'tinymce.core.util.Class', [ "tinymce.core.util.Tools" ], function (Tools) { var each = Tools.each, extend = Tools.extend; var extendClass, initializing; function Class() { } // Provides classical inheritance, based on code made by John Resig Class.extend = extendClass = function (prop) { var self = this, _super = self.prototype, prototype, name, member; // The dummy class constructor function Class() { var i, mixins, mixin, self = this; // All construction is actually done in the init method if (!initializing) { // Run class constuctor if (self.init) { self.init.apply(self, arguments); } // Run mixin constructors mixins = self.Mixins; if (mixins) { i = mixins.length; while (i--) { mixin = mixins[i]; if (mixin.init) { mixin.init.apply(self, arguments); } } } } } // Dummy function, needs to be extended in order to provide functionality function dummy() { return this; } // Creates a overloaded method for the class // this enables you to use this._super(); to call the super function function createMethod(name, fn) { return function () { var self = this, tmp = self._super, ret; self._super = _super[name]; ret = fn.apply(self, arguments); self._super = tmp; return ret; }; } // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; /*eslint new-cap:0 */ prototype = new self(); initializing = false; // Add mixins if (prop.Mixins) { each(prop.Mixins, function (mixin) { for (var name in mixin) { if (name !== "init") { prop[name] = mixin[name]; } } }); if (_super.Mixins) { prop.Mixins = _super.Mixins.concat(prop.Mixins); } } // Generate dummy methods if (prop.Methods) { each(prop.Methods.split(','), function (name) { prop[name] = dummy; }); } // Generate property methods if (prop.Properties) { each(prop.Properties.split(','), function (name) { var fieldName = '_' + name; prop[name] = function (value) { var self = this, undef; // Set value if (value !== undef) { self[fieldName] = value; return self; } // Get value return self[fieldName]; }; }); } // Static functions if (prop.Statics) { each(prop.Statics, function (func, name) { Class[name] = func; }); } // Default settings if (prop.Defaults && _super.Defaults) { prop.Defaults = extend({}, _super.Defaults, prop.Defaults); } // Copy the properties over onto the new prototype for (name in prop) { member = prop[name]; if (typeof member == "function" && _super[name]) { prototype[name] = createMethod(name, member); } else { prototype[name] = member; } } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.constructor = Class; // And make this class extendible Class.extend = extendClass; return Class; }; return Class; } ); /** * EventDispatcher.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class lets you add/remove and fire events by name on the specified scope. This makes * it easy to add event listener logic to any class. * * @class tinymce.util.EventDispatcher * @example * var eventDispatcher = new EventDispatcher(); * * eventDispatcher.on('click', function() {console.log('data');}); * eventDispatcher.fire('click', {data: 123}); */ define( 'tinymce.core.util.EventDispatcher', [ "tinymce.core.util.Tools" ], function (Tools) { var nativeEvents = Tools.makeMap( "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + "draggesture dragdrop drop drag submit " + "compositionstart compositionend compositionupdate touchstart touchmove touchend", ' ' ); function Dispatcher(settings) { var self = this, scope, bindings = {}, toggleEvent; function returnFalse() { return false; } function returnTrue() { return true; } settings = settings || {}; scope = settings.scope || self; toggleEvent = settings.toggleEvent || returnFalse; /** * Fires the specified event by name. * * @method fire * @param {String} name Name of the event to fire. * @param {Object?} args Event arguments. * @return {Object} Event args instance passed in. * @example * instance.fire('event', {...}); */ function fire(name, args) { var handlers, i, l, callback; name = name.toLowerCase(); args = args || {}; args.type = name; // Setup target is there isn't one if (!args.target) { args.target = scope; } // Add event delegation methods if they are missing if (!args.preventDefault) { // Add preventDefault method args.preventDefault = function () { args.isDefaultPrevented = returnTrue; }; // Add stopPropagation args.stopPropagation = function () { args.isPropagationStopped = returnTrue; }; // Add stopImmediatePropagation args.stopImmediatePropagation = function () { args.isImmediatePropagationStopped = returnTrue; }; // Add event delegation states args.isDefaultPrevented = returnFalse; args.isPropagationStopped = returnFalse; args.isImmediatePropagationStopped = returnFalse; } if (settings.beforeFire) { settings.beforeFire(args); } handlers = bindings[name]; if (handlers) { for (i = 0, l = handlers.length; i < l; i++) { callback = handlers[i]; // Unbind handlers marked with "once" if (callback.once) { off(name, callback.func); } // Stop immediate propagation if needed if (args.isImmediatePropagationStopped()) { args.stopPropagation(); return args; } // If callback returns false then prevent default and stop all propagation if (callback.func.call(scope, args) === false) { args.preventDefault(); return args; } } } return args; } /** * Binds an event listener to a specific event by name. * * @method on * @param {String} name Event name or space separated list of events to bind. * @param {callback} callback Callback to be executed when the event occurs. * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. * @return {Object} Current class instance. * @example * instance.on('event', function(e) { * // Callback logic * }); */ function on(name, callback, prepend, extra) { var handlers, names, i; if (callback === false) { callback = returnFalse; } if (callback) { callback = { func: callback }; if (extra) { Tools.extend(callback, extra); } names = name.toLowerCase().split(' '); i = names.length; while (i--) { name = names[i]; handlers = bindings[name]; if (!handlers) { handlers = bindings[name] = []; toggleEvent(name, true); } if (prepend) { handlers.unshift(callback); } else { handlers.push(callback); } } } return self; } /** * Unbinds an event listener to a specific event by name. * * @method off * @param {String?} name Name of the event to unbind. * @param {callback?} callback Callback to unbind. * @return {Object} Current class instance. * @example * // Unbind specific callback * instance.off('event', handler); * * // Unbind all listeners by name * instance.off('event'); * * // Unbind all events * instance.off(); */ function off(name, callback) { var i, handlers, bindingName, names, hi; if (name) { names = name.toLowerCase().split(' '); i = names.length; while (i--) { name = names[i]; handlers = bindings[name]; // Unbind all handlers if (!name) { for (bindingName in bindings) { toggleEvent(bindingName, false); delete bindings[bindingName]; } return self; } if (handlers) { // Unbind all by name if (!callback) { handlers.length = 0; } else { // Unbind specific ones hi = handlers.length; while (hi--) { if (handlers[hi].func === callback) { handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1)); bindings[name] = handlers; } } } if (!handlers.length) { toggleEvent(name, false); delete bindings[name]; } } } } else { for (name in bindings) { toggleEvent(name, false); } bindings = {}; } return self; } /** * Binds an event listener to a specific event by name * and automatically unbind the event once the callback fires. * * @method once * @param {String} name Event name or space separated list of events to bind. * @param {callback} callback Callback to be executed when the event occurs. * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. * @return {Object} Current class instance. * @example * instance.once('event', function(e) { * // Callback logic * }); */ function once(name, callback, prepend) { return on(name, callback, prepend, { once: true }); } /** * Returns true/false if the dispatcher has a event of the specified name. * * @method has * @param {String} name Name of the event to check for. * @return {Boolean} true/false if the event exists or not. */ function has(name) { name = name.toLowerCase(); return !(!bindings[name] || bindings[name].length === 0); } // Expose self.fire = fire; self.on = on; self.off = off; self.once = once; self.has = has; } /** * Returns true/false if the specified event name is a native browser event or not. * * @method isNative * @param {String} name Name to check if it's native. * @return {Boolean} true/false if the event is native or not. * @static */ Dispatcher.isNative = function (name) { return !!nativeEvents[name.toLowerCase()]; }; return Dispatcher; } ); /** * Observable.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This mixin will add event binding logic to classes. * * @mixin tinymce.util.Observable */ define( 'tinymce.core.util.Observable', [ "tinymce.core.util.EventDispatcher" ], function (EventDispatcher) { function getEventDispatcher(obj) { if (!obj._eventDispatcher) { obj._eventDispatcher = new EventDispatcher({ scope: obj, toggleEvent: function (name, state) { if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { obj.toggleNativeEvent(name, state); } } }); } return obj._eventDispatcher; } return { /** * Fires the specified event by name. Consult the * event reference for more details on each event. * * @method fire * @param {String} name Name of the event to fire. * @param {Object?} args Event arguments. * @param {Boolean?} bubble True/false if the event is to be bubbled. * @return {Object} Event args instance passed in. * @example * instance.fire('event', {...}); */ fire: function (name, args, bubble) { var self = this; // Prevent all events except the remove event after the instance has been removed if (self.removed && name !== "remove") { return args; } args = getEventDispatcher(self).fire(name, args, bubble); // Bubble event up to parents if (bubble !== false && self.parent) { var parent = self.parent(); while (parent && !args.isPropagationStopped()) { parent.fire(name, args, false); parent = parent.parent(); } } return args; }, /** * Binds an event listener to a specific event by name. Consult the * event reference for more details on each event. * * @method on * @param {String} name Event name or space separated list of events to bind. * @param {callback} callback Callback to be executed when the event occurs. * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. * @return {Object} Current class instance. * @example * instance.on('event', function(e) { * // Callback logic * }); */ on: function (name, callback, prepend) { return getEventDispatcher(this).on(name, callback, prepend); }, /** * Unbinds an event listener to a specific event by name. Consult the * event reference for more details on each event. * * @method off * @param {String?} name Name of the event to unbind. * @param {callback?} callback Callback to unbind. * @return {Object} Current class instance. * @example * // Unbind specific callback * instance.off('event', handler); * * // Unbind all listeners by name * instance.off('event'); * * // Unbind all events * instance.off(); */ off: function (name, callback) { return getEventDispatcher(this).off(name, callback); }, /** * Bind the event callback and once it fires the callback is removed. Consult the * event reference for more details on each event. * * @method once * @param {String} name Name of the event to bind. * @param {callback} callback Callback to bind only once. * @return {Object} Current class instance. */ once: function (name, callback) { return getEventDispatcher(this).once(name, callback); }, /** * Returns true/false if the object has a event of the specified name. * * @method hasEventListeners * @param {String} name Name of the event to check for. * @return {Boolean} true/false if the event exists or not. */ hasEventListeners: function (name) { return getEventDispatcher(this).has(name); } }; } ); /** * Binding.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class gets dynamically extended to provide a binding between two models. This makes it possible to * sync the state of two properties in two models by a layer of abstraction. * * @private * @class tinymce.data.Binding */ define( 'tinymce.core.data.Binding', [ ], function () { /** * Constructs a new bidning. * * @constructor * @method Binding * @param {Object} settings Settings to the binding. */ function Binding(settings) { this.create = settings.create; } /** * Creates a binding for a property on a model. * * @method create * @param {tinymce.data.ObservableObject} model Model to create binding to. * @param {String} name Name of property to bind. * @return {tinymce.data.Binding} Binding instance. */ Binding.create = function (model, name) { return new Binding({ create: function (otherModel, otherName) { var bindings; function fromSelfToOther(e) { otherModel.set(otherName, e.value); } function fromOtherToSelf(e) { model.set(name, e.value); } otherModel.on('change:' + otherName, fromOtherToSelf); model.on('change:' + name, fromSelfToOther); // Keep track of the bindings bindings = otherModel._bindings; if (!bindings) { bindings = otherModel._bindings = []; otherModel.on('destroy', function () { var i = bindings.length; while (i--) { bindings[i](); } }); } bindings.push(function () { model.off('change:' + name, fromSelfToOther); }); return model.get(name); } }); }; return Binding; } ); /** * ObservableObject.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class is a object that is observable when properties changes a change event gets emitted. * * @private * @class tinymce.data.ObservableObject */ define( 'tinymce.core.data.ObservableObject', [ 'tinymce.core.data.Binding', 'tinymce.core.util.Class', 'tinymce.core.util.Observable', 'tinymce.core.util.Tools' ], function (Binding, Class, Observable, Tools) { function isNode(node) { return node.nodeType > 0; } // Todo: Maybe this should be shallow compare since it might be huge object references function isEqual(a, b) { var k, checked; // Strict equals if (a === b) { return true; } // Compare null if (a === null || b === null) { return a === b; } // Compare number, boolean, string, undefined if (typeof a !== "object" || typeof b !== "object") { return a === b; } // Compare arrays if (Tools.isArray(b)) { if (a.length !== b.length) { return false; } k = a.length; while (k--) { if (!isEqual(a[k], b[k])) { return false; } } } // Shallow compare nodes if (isNode(a) || isNode(b)) { return a === b; } // Compare objects checked = {}; for (k in b) { if (!isEqual(a[k], b[k])) { return false; } checked[k] = true; } for (k in a) { if (!checked[k] && !isEqual(a[k], b[k])) { return false; } } return true; } return Class.extend({ Mixins: [Observable], /** * Constructs a new observable object instance. * * @constructor * @param {Object} data Initial data for the object. */ init: function (data) { var name, value; data = data || {}; for (name in data) { value = data[name]; if (value instanceof Binding) { data[name] = value.create(this, name); } } this.data = data; }, /** * Sets a property on the value this will call * observers if the value is a change from the current value. * * @method set * @param {String/object} name Name of the property to set or a object of items to set. * @param {Object} value Value to set for the property. * @return {tinymce.data.ObservableObject} Observable object instance. */ set: function (name, value) { var key, args, oldValue = this.data[name]; if (value instanceof Binding) { value = value.create(this, name); } if (typeof name === "object") { for (key in name) { this.set(key, name[key]); } return this; } if (!isEqual(oldValue, value)) { this.data[name] = value; args = { target: this, name: name, value: value, oldValue: oldValue }; this.fire('change:' + name, args); this.fire('change', args); } return this; }, /** * Gets a property by name. * * @method get * @param {String} name Name of the property to get. * @return {Object} Object value of propery. */ get: function (name) { return this.data[name]; }, /** * Returns true/false if the specified property exists. * * @method has * @param {String} name Name of the property to check for. * @return {Boolean} true/false if the item exists. */ has: function (name) { return name in this.data; }, /** * Returns a dynamic property binding for the specified property name. This makes * it possible to sync the state of two properties in two ObservableObject instances. * * @method bind * @param {String} name Name of the property to sync with the property it's inserted to. * @return {tinymce.data.Binding} Data binding instance. */ bind: function (name) { return Binding.create(this, name); }, /** * Destroys the observable object and fires the "destroy" * event and clean up any internal resources. * * @method destroy */ destroy: function () { this.fire('destroy'); } }); } ); /** * Selector.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /*eslint no-nested-ternary:0 */ /** * Selector engine, enables you to select controls by using CSS like expressions. * We currently only support basic CSS expressions to reduce the size of the core * and the ones we support should be enough for most cases. * * @example * Supported expressions: * element * element#name * element.class * element[attr] * element[attr*=value] * element[attr~=value] * element[attr!=value] * element[attr^=value] * element[attr$=value] * element:bug on IE 8 #6178 DOMUtils.DOM.setHTML(elm, html); } }; return funcs; } ); /** * BoxUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Utility class for box parsing and measuring. * * @private * @class tinymce.ui.BoxUtils */ define( 'tinymce.core.ui.BoxUtils', [ ], function () { "use strict"; return { /** * Parses the specified box value. A box value contains 1-4 properties in clockwise order. * * @method parseBox * @param {String/Number} value Box value "0 1 2 3" or "0" etc. * @return {Object} Object with top/right/bottom/left properties. * @private */ parseBox: function (value) { var len, radix = 10; if (!value) { return; } if (typeof value === "number") { value = value || 0; return { top: value, left: value, bottom: value, right: value }; } value = value.split(' '); len = value.length; if (len === 1) { value[1] = value[2] = value[3] = value[0]; } else if (len === 2) { value[2] = value[0]; value[3] = value[1]; } else if (len === 3) { value[3] = value[1]; } return { top: parseInt(value[0], radix) || 0, right: parseInt(value[1], radix) || 0, bottom: parseInt(value[2], radix) || 0, left: parseInt(value[3], radix) || 0 }; }, measureBox: function (elm, prefix) { function getStyle(name) { var defaultView = document.defaultView; if (defaultView) { // Remove camelcase name = name.replace(/[A-Z]/g, function (a) { return '-' + a; }); return defaultView.getComputedStyle(elm, null).getPropertyValue(name); } return elm.currentStyle[name]; } function getSide(name) { var val = parseFloat(getStyle(name), 10); return isNaN(val) ? 0 : val; } return { top: getSide(prefix + "TopWidth"), right: getSide(prefix + "RightWidth"), bottom: getSide(prefix + "BottomWidth"), left: getSide(prefix + "LeftWidth") }; } }; } ); /** * ClassList.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * Handles adding and removal of classes. * * @private * @class tinymce.ui.ClassList */ define( 'tinymce.core.ui.ClassList', [ "tinymce.core.util.Tools" ], function (Tools) { "use strict"; function noop() { } /** * Constructs a new class list the specified onchange * callback will be executed when the class list gets modifed. * * @constructor ClassList * @param {function} onchange Onchange callback to be executed. */ function ClassList(onchange) { this.cls = []; this.cls._map = {}; this.onchange = onchange || noop; this.prefix = ''; } Tools.extend(ClassList.prototype, { /** * Adds a new class to the class list. * * @method add * @param {String} cls Class to be added. * @return {tinymce.ui.ClassList} Current class list instance. */ add: function (cls) { if (cls && !this.contains(cls)) { this.cls._map[cls] = true; this.cls.push(cls); this._change(); } return this; }, /** * Removes the specified class from the class list. * * @method remove * @param {String} cls Class to be removed. * @return {tinymce.ui.ClassList} Current class list instance. */ remove: function (cls) { if (this.contains(cls)) { for (var i = 0; i < this.cls.length; i++) { if (this.cls[i] === cls) { break; } } this.cls.splice(i, 1); delete this.cls._map[cls]; this._change(); } return this; }, /** * Toggles a class in the class list. * * @method toggle * @param {String} cls Class to be added/removed. * @param {Boolean} state Optional state if it should be added/removed. * @return {tinymce.ui.ClassList} Current class list instance. */ toggle: function (cls, state) { var curState = this.contains(cls); if (curState !== state) { if (curState) { this.remove(cls); } else { this.add(cls); } this._change(); } return this; }, /** * Returns true if the class list has the specified class. * * @method contains * @param {String} cls Class to look for. * @return {Boolean} true/false if the class exists or not. */ contains: function (cls) { return !!this.cls._map[cls]; }, /** * Returns a space separated list of classes. * * @method toString * @return {String} Space separated list of classes. */ _change: function () { delete this.clsValue; this.onchange.call(this); } }); // IE 8 compatibility ClassList.prototype.toString = function () { var value; if (this.clsValue) { return this.clsValue; } value = ''; for (var i = 0; i < this.cls.length; i++) { if (i > 0) { value += ' '; } value += this.prefix + this.cls[i]; } return value; }; return ClassList; } ); /** * ReflowQueue.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class will automatically reflow controls on the next animation frame within a few milliseconds on older browsers. * If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states * changes that triggers a reflow. * * @class tinymce.ui.ReflowQueue * @static */ define( 'tinymce.core.ui.ReflowQueue', [ "tinymce.core.util.Delay" ], function (Delay) { var dirtyCtrls = {}, animationFrameRequested; return { /** * Adds a control to the next automatic reflow call. This is the control that had a state * change for example if the control was hidden/shown. * * @method add * @param {tinymce.ui.Control} ctrl Control to add to queue. */ add: function (ctrl) { var parent = ctrl.parent(); if (parent) { if (!parent._layout || parent._layout.isNative()) { return; } if (!dirtyCtrls[parent._id]) { dirtyCtrls[parent._id] = parent; } if (!animationFrameRequested) { animationFrameRequested = true; Delay.requestAnimationFrame(function () { var id, ctrl; animationFrameRequested = false; for (id in dirtyCtrls) { ctrl = dirtyCtrls[id]; if (ctrl.state.get('rendered')) { ctrl.reflow(); } } dirtyCtrls = {}; }, document.body); } } }, /** * Removes the specified control from the automatic reflow. This will happen when for example the user * manually triggers a reflow. * * @method remove * @param {tinymce.ui.Control} ctrl Control to remove from queue. */ remove: function (ctrl) { if (dirtyCtrls[ctrl._id]) { delete dirtyCtrls[ctrl._id]; } } }; } ); /** * Control.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /*eslint consistent-this:0 */ /** * This is the base class for all controls and containers. All UI control instances inherit * from this one as it has the base logic needed by all of them. * * @class tinymce.ui.Control */ define( 'tinymce.core.ui.Control', [ "tinymce.core.util.Class", "tinymce.core.util.Tools", "tinymce.core.util.EventDispatcher", "tinymce.core.data.ObservableObject", "tinymce.core.ui.Collection", "tinymce.core.ui.DomUtils", "tinymce.core.dom.DomQuery", "tinymce.core.ui.BoxUtils", "tinymce.core.ui.ClassList", "tinymce.core.ui.ReflowQueue" ], function (Class, Tools, EventDispatcher, ObservableObject, Collection, DomUtils, $, BoxUtils, ClassList, ReflowQueue) { "use strict"; var hasMouseWheelEventSupport = "onmousewheel" in document; var hasWheelEventSupport = false; var classPrefix = "mce-"; var Control, idCounter = 0; var proto = { Statics: { classPrefix: classPrefix }, isRtl: function () { return Control.rtl; }, /** * Class/id prefix to use for all controls. * * @final * @field {String} classPrefix */ classPrefix: classPrefix, /** * Constructs a new control instance with the specified settings. * * @constructor * @param {Object} settings Name/value object with settings. * @setting {String} style Style CSS properties to add. * @setting {String} border Border box values example: 1 1 1 1 * @setting {String} padding Padding box values example: 1 1 1 1 * @setting {String} margin Margin box values example: 1 1 1 1 * @setting {Number} minWidth Minimal width for the control. * @setting {Number} minHeight Minimal height for the control. * @setting {String} classes Space separated list of classes to add. * @setting {String} role WAI-ARIA role to use for control. * @setting {Boolean} hidden Is the control hidden by default. * @setting {Boolean} disabled Is the control disabled by default. * @setting {String} name Name of the control instance. */ init: function (settings) { var self = this, classes, defaultClasses; function applyClasses(classes) { var i; classes = classes.split(' '); for (i = 0; i < classes.length; i++) { self.classes.add(classes[i]); } } self.settings = settings = Tools.extend({}, self.Defaults, settings); // Initial states self._id = settings.id || ('mceu_' + (idCounter++)); self._aria = { role: settings.role }; self._elmCache = {}; self.$ = $; self.state = new ObservableObject({ visible: true, active: false, disabled: false, value: '' }); self.data = new ObservableObject(settings.data); self.classes = new ClassList(function () { if (self.state.get('rendered')) { self.getEl().className = this.toString(); } }); self.classes.prefix = self.classPrefix; // Setup classes classes = settings.classes; if (classes) { if (self.Defaults) { defaultClasses = self.Defaults.classes; if (defaultClasses && classes != defaultClasses) { applyClasses(defaultClasses); } } applyClasses(classes); } Tools.each('title text name visible disabled active value'.split(' '), function (name) { if (name in settings) { self[name](settings[name]); } }); self.on('click', function () { if (self.disabled()) { return false; } }); /** * Name/value object with settings for the current control. * * @field {Object} settings */ self.settings = settings; self.borderBox = BoxUtils.parseBox(settings.border); self.paddingBox = BoxUtils.parseBox(settings.padding); self.marginBox = BoxUtils.parseBox(settings.margin); if (settings.hidden) { self.hide(); } }, // Will generate getter/setter methods for these properties Properties: 'parent,name', /** * Returns the root element to render controls into. * * @method getContainerElm * @return {Element} HTML DOM element to render into. */ getContainerElm: function () { return DomUtils.getContainer(); }, /** * Returns a control instance for the current DOM element. * * @method getParentCtrl * @param {Element} elm HTML dom element to get parent control from. * @return {tinymce.ui.Control} Control instance or undefined. */ getParentCtrl: function (elm) { var ctrl, lookup = this.getRoot().controlIdLookup; while (elm && lookup) { ctrl = lookup[elm.id]; if (ctrl) { break; } elm = elm.parentNode; } return ctrl; }, /** * 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, settings = self.settings, borderBox, layoutRect; var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; var startMinWidth, startMinHeight, initialSize; // Measure the current element borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border'); self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding'); self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin'); initialSize = DomUtils.getSize(elm); // Setup minWidth/minHeight and width/height startMinWidth = settings.minWidth; startMinHeight = settings.minHeight; minWidth = startMinWidth || initialSize.width; minHeight = startMinHeight || initialSize.height; width = settings.width; height = settings.height; autoResize = settings.autoResize; autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height; width = width || minWidth; height = height || minHeight; var deltaW = borderBox.left + borderBox.right; var deltaH = borderBox.top + borderBox.bottom; var maxW = settings.maxWidth || 0xFFFF; var maxH = settings.maxHeight || 0xFFFF; // Setup initial layout rect self._layoutRect = layoutRect = { x: settings.x || 0, y: settings.y || 0, w: width, h: height, deltaW: deltaW, deltaH: deltaH, contentW: width - deltaW, contentH: height - deltaH, innerW: width - deltaW, innerH: height - deltaH, startMinWidth: startMinWidth || 0, startMinHeight: startMinHeight || 0, minW: Math.min(minWidth, maxW), minH: Math.min(minHeight, maxH), maxW: maxW, maxH: maxH, autoResize: autoResize, scrollW: 0 }; self._lastLayoutRect = {}; return layoutRect; }, /** * Getter/setter for the current layout rect. * * @method layoutRect * @param {Object} [newRect] Optional new layout rect. * @return {tinymce.ui.Control/Object} Current control or rect object. */ layoutRect: function (newRect) { var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; // Initialize default layout rect if (!curRect) { curRect = self.initLayoutRect(); } // Set new rect values if (newRect) { // Calc deltas between inner and outer sizes deltaWidth = curRect.deltaW; deltaHeight = curRect.deltaH; // Set x position if (newRect.x !== undef) { curRect.x = newRect.x; } // Set y position if (newRect.y !== undef) { curRect.y = newRect.y; } // Set minW if (newRect.minW !== undef) { curRect.minW = newRect.minW; } // Set minH if (newRect.minH !== undef) { curRect.minH = newRect.minH; } // Set new width and calculate inner width size = newRect.w; if (size !== undef) { size = size < curRect.minW ? curRect.minW : size; size = size > curRect.maxW ? curRect.maxW : size; curRect.w = size; curRect.innerW = size - deltaWidth; } // Set new height and calculate inner height size = newRect.h; if (size !== undef) { size = size < curRect.minH ? curRect.minH : size; size = size > curRect.maxH ? curRect.maxH : size; curRect.h = size; curRect.innerH = size - deltaHeight; } // Set new inner width and calculate width size = newRect.innerW; if (size !== undef) { size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; curRect.innerW = size; curRect.w = size + deltaWidth; } // Set new height and calculate inner height size = newRect.innerH; if (size !== undef) { size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; curRect.innerH = size; curRect.h = size + deltaHeight; } // Set new contentW if (newRect.contentW !== undef) { curRect.contentW = newRect.contentW; } // Set new contentH if (newRect.contentH !== undef) { curRect.contentH = newRect.contentH; } // Compare last layout rect with the current one to see if we need to repaint or not lastLayoutRect = self._lastLayoutRect; if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { repaintControls = Control.repaintControls; if (repaintControls) { if (repaintControls.map && !repaintControls.map[self._id]) { repaintControls.push(self); repaintControls.map[self._id] = true; } } lastLayoutRect.x = curRect.x; lastLayoutRect.y = curRect.y; lastLayoutRect.w = curRect.w; lastLayoutRect.h = curRect.h; } return self; } return curRect; }, /** * Repaints the control after a layout operation. * * @method repaint */ repaint: function () { var self = this, style, bodyStyle, bodyElm, rect, borderBox; var borderW, borderH, lastRepaintRect, round, value; // Use Math.round on all values on IE < 9 round = !document.createRange ? Math.round : function (value) { return value; }; style = self.getEl().style; rect = self._layoutRect; lastRepaintRect = self._lastRepaintRect || {}; borderBox = self.borderBox; borderW = borderBox.left + borderBox.right; borderH = borderBox.top + borderBox.bottom; if (rect.x !== lastRepaintRect.x) { style.left = round(rect.x) + 'px'; lastRepaintRect.x = rect.x; } if (rect.y !== lastRepaintRect.y) { style.top = round(rect.y) + 'px'; lastRepaintRect.y = rect.y; } if (rect.w !== lastRepaintRect.w) { value = round(rect.w - borderW); style.width = (value >= 0 ? value : 0) + 'px'; lastRepaintRect.w = rect.w; } if (rect.h !== lastRepaintRect.h) { value = round(rect.h - borderH); style.height = (value >= 0 ? value : 0) + 'px'; lastRepaintRect.h = rect.h; } // Update body if needed if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { value = round(rect.innerW); bodyElm = self.getEl('body'); if (bodyElm) { bodyStyle = bodyElm.style; bodyStyle.width = (value >= 0 ? value : 0) + 'px'; } lastRepaintRect.innerW = rect.innerW; } if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { value = round(rect.innerH); bodyElm = bodyElm || self.getEl('body'); if (bodyElm) { bodyStyle = bodyStyle || bodyElm.style; bodyStyle.height = (value >= 0 ? value : 0) + 'px'; } lastRepaintRect.innerH = rect.innerH; } self._lastRepaintRect = lastRepaintRect; self.fire('repaint', {}, false); }, /** * Updates the controls layout rect by re-measuing it. */ updateLayoutRect: function () { var self = this; self.parent()._lastRect = null; DomUtils.css(self.getEl(), { width: '', height: '' }); self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null; self.initLayoutRect(); }, /** * Binds a callback to the specified event. This event can both be * native browser events like "click" or custom ones like PostRender. * * The callback function will be passed a DOM event like object that enables yout do stop propagation. * * @method on * @param {String} name Name of the event to bind. For example "click". * @param {String/function} callback Callback function to execute ones the event occurs. * @return {tinymce.ui.Control} Current control object. */ on: function (name, callback) { var self = this; function resolveCallbackName(name) { var callback, scope; if (typeof name != 'string') { return name; } return function (e) { if (!callback) { self.parentsAndSelf().each(function (ctrl) { var callbacks = ctrl.settings.callbacks; if (callbacks && (callback = callbacks[name])) { scope = ctrl; return false; } }); } if (!callback) { e.action = name; this.fire('execute', e); return; } return callback.call(scope, e); }; } getEventDispatcher(self).on(name, resolveCallbackName(callback)); return self; }, /** * Unbinds the specified event and optionally a specific callback. If you omit the name * parameter all event handlers will be removed. If you omit the callback all event handles * by the specified name will be removed. * * @method off * @param {String} [name] Name for the event to unbind. * @param {function} [callback] Callback function to unbind. * @return {tinymce.ui.Control} Current control object. */ off: function (name, callback) { getEventDispatcher(this).off(name, callback); return this; }, /** * Fires the specified event by name and arguments on the control. This will execute all * bound event handlers. * * @method fire * @param {String} name Name of the event to fire. * @param {Object} [args] Arguments to pass to the event. * @param {Boolean} [bubble] Value to control bubbling. Defaults to true. * @return {Object} Current arguments object. */ fire: function (name, args, bubble) { var self = this; args = args || {}; if (!args.control) { args.control = self; } args = getEventDispatcher(self).fire(name, args); // Bubble event up to parents if (bubble !== false && self.parent) { var parent = self.parent(); while (parent && !args.isPropagationStopped()) { parent.fire(name, args, false); parent = parent.parent(); } } return args; }, /** * Returns true/false if the specified event has any listeners. * * @method hasEventListeners * @param {String} name Name of the event to check for. * @return {Boolean} True/false state if the event has listeners. */ hasEventListeners: function (name) { return getEventDispatcher(this).has(name); }, /** * Returns a control collection with all parent controls. * * @method parents * @param {String} selector Optional selector expression to find parents. * @return {tinymce.ui.Collection} Collection with all parent controls. */ parents: function (selector) { var self = this, ctrl, parents = new Collection(); // Add each parent to collection for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { parents.add(ctrl); } // Filter away everything that doesn't match the selector if (selector) { parents = parents.filter(selector); } return parents; }, /** * Returns the current control and it's parents. * * @method parentsAndSelf * @param {String} selector Optional selector expression to find parents. * @return {tinymce.ui.Collection} Collection with all parent controls. */ parentsAndSelf: function (selector) { return new Collection(this).add(this.parents(selector)); }, /** * Returns the control next to the current control. * * @method next * @return {tinymce.ui.Control} Next control instance. */ next: function () { var parentControls = this.parent().items(); return parentControls[parentControls.indexOf(this) + 1]; }, /** * Returns the control previous to the current control. * * @method prev * @return {tinymce.ui.Control} Previous control instance. */ prev: function () { var parentControls = this.parent().items(); return parentControls[parentControls.indexOf(this) - 1]; }, /** * Sets the inner HTML of the control element. * * @method innerHtml * @param {String} html Html string to set as inner html. * @return {tinymce.ui.Control} Current control object. */ innerHtml: function (html) { this.$el.html(html); return this; }, /** * Returns the control DOM element or sub element. * * @method getEl * @param {String} [suffix] Suffix to get element by. * @return {Element} HTML DOM element for the current control or it's children. */ getEl: function (suffix) { var id = suffix ? this._id + '-' + suffix : this._id; if (!this._elmCache[id]) { this._elmCache[id] = $('#' + id)[0]; } return this._elmCache[id]; }, /** * Sets the visible state to true. * * @method show * @return {tinymce.ui.Control} Current control instance. */ show: function () { return this.visible(true); }, /** * Sets the visible state to false. * * @method hide * @return {tinymce.ui.Control} Current control instance. */ hide: function () { return this.visible(false); }, /** * Focuses the current control. * * @method focus * @return {tinymce.ui.Control} Current control instance. */ focus: function () { try { this.getEl().focus(); } catch (ex) { // Ignore IE error } return this; }, /** * Blurs the current control. * * @method blur * @return {tinymce.ui.Control} Current control instance. */ blur: function () { this.getEl().blur(); return this; }, /** * Sets the specified aria property. * * @method aria * @param {String} name Name of the aria property to set. * @param {String} value Value of the aria property. * @return {tinymce.ui.Control} Current control instance. */ aria: function (name, value) { var self = this, elm = self.getEl(self.ariaTarget); if (typeof value === "undefined") { return self._aria[name]; } self._aria[name] = value; if (self.state.get('rendered')) { elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); } return self; }, /** * Encodes the specified string with HTML entities. It will also * translate the string to different languages. * * @method encode * @param {String/Object/Array} text Text to entity encode. * @param {Boolean} [translate=true] False if the contents shouldn't be translated. * @return {String} Encoded and possible traslated string. */ encode: function (text, translate) { if (translate !== false) { text = this.translate(text); } return (text || '').replace(/[&<>"]/g, function (match) { return '' + match.charCodeAt(0) + ';'; }); }, /** * Returns the translated string. * * @method translate * @param {String} text Text to translate. * @return {String} Translated string or the same as the input. */ translate: function (text) { return Control.translate ? Control.translate(text) : text; }, /** * Adds items before the current control. * * @method before * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. * @return {tinymce.ui.Control} Current control instance. */ before: function (items) { var self = this, parent = self.parent(); if (parent) { parent.insert(items, parent.items().indexOf(self), true); } return self; }, /** * Adds items after the current control. * * @method after * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. * @return {tinymce.ui.Control} Current control instance. */ after: function (items) { var self = this, parent = self.parent(); if (parent) { parent.insert(items, parent.items().indexOf(self)); } return self; }, /** * Removes the current control from DOM and from UI collections. * * @method remove * @return {tinymce.ui.Control} Current control instance. */ remove: function () { var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; if (self.items) { var controls = self.items().toArray(); i = controls.length; while (i--) { controls[i].remove(); } } if (parent && parent.items) { newItems = []; parent.items().each(function (item) { if (item !== self) { newItems.push(item); } }); parent.items().set(newItems); parent._lastRect = null; } if (self._eventsRoot && self._eventsRoot == self) { $(elm).off(); } var lookup = self.getRoot().controlIdLookup; if (lookup) { delete lookup[self._id]; } if (elm && elm.parentNode) { elm.parentNode.removeChild(elm); } self.state.set('rendered', false); self.state.destroy(); self.fire('remove'); return self; }, /** * Renders the control before the specified element. * * @method renderBefore * @param {Element} elm Element to render before. * @return {tinymce.ui.Control} Current control instance. */ renderBefore: function (elm) { $(elm).before(this.renderHtml()); this.postRender(); return this; }, /** * Renders the control to the specified element. * * @method renderBefore * @param {Element} elm Element to render to. * @return {tinymce.ui.Control} Current control instance. */ renderTo: function (elm) { $(elm || this.getContainerElm()).append(this.renderHtml()); this.postRender(); return this; }, preRender: function () { }, render: function () { }, renderHtml: function () { return '
'; }, /** * Post render method. Called after the control has been rendered to the target. * * @method postRender * @return {tinymce.ui.Control} Current control instance. */ postRender: function () { var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; self.$el = $(self.getEl()); self.state.set('rendered', true); // Bind onx
becomes this:x
var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { var node = block, firstChilds = [], i; if (!node) { return; } // Find inner most first child ex:*
while ((node = node.firstChild)) { if (dom.isBlock(node)) { return; } if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { firstChilds.push(node); } } i = firstChilds.length; while (i--) { node = firstChilds[i]; if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { dom.remove(node); } else { if (isEmptyAnchor(node)) { dom.remove(node); } } } }; var normalizeZwspOffset = function (start, container, offset) { if (NodeType.isText(container) === false) { return offset; } if (start) { return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; } else { return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; } }; var includeZwspInRange = function (rng) { var newRng = rng.cloneRange(); newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); return newRng; }; var firstNonWhiteSpaceNodeSibling = function (node) { while (node) { if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { return node; } node = node.nextSibling; } }; var setup = function (editor) { var dom = editor.dom, selection = editor.selection, settings = editor.settings; var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(), moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); function handleEnterKey(evt) { var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; // Moves the caret to a suitable position within the root for example in the first non // pure whitespace text node or before an image function moveToCaretPosition(root) { var walker, node, rng, lastNode = root, tempElm; if (!root) { return; } // Old IE versions doesn't properly render blocks with br elements in them // For exampletext|
text|text2
a |
if (dom.isEmpty(newBlock)) { dom.remove(newBlock); insertNewBlockAfter(); } else { moveToCaretPosition(newBlock); } } dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique // Allow custom handling of new blocks editor.fire('NewBlock', { newBlock: newBlock }); undoManager.typing = false; undoManager.add(); } editor.on('keydown', function (evt) { if (evt.keyCode == 13) { if (handleEnterKey(evt) !== false) { evt.preventDefault(); } } }); }; return { setup: setup }; } ); /** * InsertSpace.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InsertSpace', [ 'ephox.katamari.api.Fun', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.BoundaryLocation' ], function (Fun, CaretPosition, NodeType, BoundaryLocation) { var isValidInsertPoint = function (location, caretPosition) { return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); }; var insertNbspAtPosition = function (editor, caretPosition) { var container = caretPosition.container(); var offset = caretPosition.offset(); container.insertData(offset, '\u00a0'); editor.selection.setCursorLocation(container, offset + 1); }; var insertAtLocation = function (editor, caretPosition, location) { if (isValidInsertPoint(location, caretPosition)) { insertNbspAtPosition(editor, caretPosition); return true; } else { return false; } }; var insertAtCaret = function (editor) { var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); var boundaryLocation = BoundaryLocation.readLocation(editor.getBody(), caretPosition); return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); }; var isAtStartOrEnd = function (location) { return location.fold( Fun.constant(false), // Before Fun.constant(true), // Start Fun.constant(true), // End Fun.constant(false) // After ); }; var insertAtSelection = function (editor) { return editor.selection.isCollapsed() ? insertAtCaret(editor) : false; }; return { insertAtSelection: insertAtSelection }; } ); /** * SpaceKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.SpaceKey', [ 'ephox.katamari.api.Arr', 'tinymce.core.keyboard.InsertSpace', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (Arr, InsertSpace, MatchKeys, VK) { var setupKeyDownHandler = function (editor, caret) { editor.on('keydown', function (evt) { var matches = MatchKeys.match([ { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } ], evt); Arr.find(matches, function (pattern) { return pattern.action(); }).each(function (_) { evt.preventDefault(); }); }); }; var setup = function (editor) { setupKeyDownHandler(editor); }; return { setup: setup }; } ); /** * KeyboardOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.KeyboardOverrides', [ 'tinymce.core.keyboard.ArrowKeys', 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.DeleteBackspaceKeys', 'tinymce.core.keyboard.EnterKey', 'tinymce.core.keyboard.SpaceKey' ], function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) { var setup = function (editor) { var caret = BoundarySelection.setupSelectedState(editor); ArrowKeys.setup(editor, caret); DeleteBackspaceKeys.setup(editor, caret); EnterKey.setup(editor); SpaceKey.setup(editor); }; return { setup: setup }; } ); /** * NodeChange.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This class handles the nodechange event dispatching both manual and through selection change events. * * @class tinymce.NodeChange * @private */ define( 'tinymce.core.NodeChange', [ "tinymce.core.dom.RangeUtils", "tinymce.core.Env", "tinymce.core.util.Delay" ], function (RangeUtils, Env, Delay) { return function (editor) { var lastRng, lastPath = []; /** * Returns true/false if the current element path has been changed or not. * * @private * @return {Boolean} True if the element path is the same false if it's not. */ function isSameElementPath(startElm) { var i, currentPath; currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm); if (currentPath.length === lastPath.length) { for (i = currentPath.length; i >= 0; i--) { if (currentPath[i] !== lastPath[i]) { break; } } if (i === -1) { lastPath = currentPath; return true; } } lastPath = currentPath; return false; } // Gecko doesn't support the "selectionchange" event if (!('onselectionchange' in editor.getDoc())) { editor.on('NodeChange Click MouseUp KeyUp Focus', function (e) { var nativeRng, fakeRng; // Since DOM Ranges mutate on modification // of the DOM we need to clone it's contents nativeRng = editor.selection.getRng(); fakeRng = { startContainer: nativeRng.startContainer, startOffset: nativeRng.startOffset, endContainer: nativeRng.endContainer, endOffset: nativeRng.endOffset }; // Always treat nodechange as a selectionchange since applying // formatting to the current range wouldn't update the range but it's parent if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) { editor.fire('SelectionChange'); } lastRng = fakeRng; }); } // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body // When the contextmenu event fires the selection is located at the right location editor.on('contextmenu', function () { editor.fire('SelectionChange'); }); // Selection change is delayed ~200ms on IE when you click inside the current range editor.on('SelectionChange', function () { var startElm = editor.selection.getStart(true); // IE 8 will fire a selectionchange event with an incorrect selection // when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event if (!Env.range && editor.selection.isCollapsed()) { return; } if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { editor.nodeChanged({ selectionChange: true }); } }); // Fire an extra nodeChange on mouseup for compatibility reasons editor.on('MouseUp', function (e) { if (!e.isDefaultPrevented()) { // Delay nodeChanged call for WebKit edge case issue where the range // isn't updated until after you click outside a selected image if (editor.selection.getNode().nodeName == 'IMG') { Delay.setEditorTimeout(editor, function () { editor.nodeChanged(); }); } else { editor.nodeChanged(); } } }); /** * 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. */ this.nodeChanged = function (args) { var selection = editor.selection, node, parents, root; // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) { // Get start node root = editor.getBody(); node = selection.getStart(true) || root; // Make sure the node is within the editor root or is the editor root if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) { node = root; } // Get parents and add them to object parents = []; editor.dom.getParent(node, function (node) { if (node === root) { return true; } parents.push(node); }); args = args || {}; args.element = node; args.parents = parents; editor.fire('NodeChange', args); } }; }; } ); /** * FakeCaret.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.FakeCaret', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretContainerRemove', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.geom.ClientRect', 'tinymce.core.util.Delay' ], function (CaretContainer, CaretContainerRemove, CaretPosition, DomQuery, 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 = DomQuery('*[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; hide(); if (isBlock(node)) { caretContainerNode = CaretContainer.insertBlock('p', node, before); clientRect = getAbsoluteClientRect(node, before); DomQuery(caretContainerNode).css('top', clientRect.top); $lastVisualCaret = DomQuery('').css(clientRect).appendTo(rootNode); if (before) { $lastVisualCaret.addClass('mce-visual-caret-before'); } startBlink(); rng = node.ownerDocument.createRange(); rng.setStart(caretContainerNode, 0); rng.setEnd(caretContainerNode, 0); } 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) { CaretContainerRemove.remove(caretContainerNode); caretContainerNode = null; } if ($lastVisualCaret) { $lastVisualCaret.remove(); $lastVisualCaret = null; } clearInterval(cursorInterval); } function startBlink() { cursorInterval = Delay.setInterval(function () { DomQuery('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 }; }; } ); /** * Dimensions.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.Dimensions', [ "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.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 }; } ); /** * LineWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineWalker', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.Dimensions", "tinymce.core.caret.CaretCandidate", "tinymce.core.caret.CaretUtils", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition", "tinymce.core.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) }; } ); /** * LineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.caret.LineUtils', [ "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.dom.NodeType", "tinymce.core.dom.Dimensions", "tinymce.core.geom.ClientRect", "tinymce.core.caret.CaretUtils", "tinymce.core.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 }; } ); /** * MousePosition.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.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 }; } ); /** * DragDropOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.DragDropOverrides', [ "tinymce.core.dom.NodeType", "tinymce.core.util.Arr", "tinymce.core.util.Fun", "tinymce.core.util.Delay", "tinymce.core.dom.DOMUtils", "tinymce.core.dom.MousePosition" ], function ( NodeType, Arr, Fun, Delay, DOMUtils, MousePosition ) { var isContentEditableFalse = NodeType.isContentEditableFalse, isContentEditableTrue = NodeType.isContentEditableTrue; var isDraggable = function (rootElm, elm) { return isContentEditableFalse(elm) && elm !== rootElm; }; 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(editor.getBody(), 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); } }; }; // Returns the raw element instead of the fake cE=false element var getRawTarget = function (selection) { var rng = selection.getSel().getRangeAt(0); var startContainer = rng.startContainer; return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer; }; var drop = function (state, editor) { return function (e) { if (state.dragging) { if (isValidDropTarget(editor, getRawTarget(editor.selection), 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 }; } ); /** * SelectionOverrides.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.SelectionOverrides', [ "tinymce.core.Env", "tinymce.core.caret.CaretWalker", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretContainer", "tinymce.core.caret.CaretContainerRemove", "tinymce.core.caret.CaretUtils", "tinymce.core.caret.FakeCaret", "tinymce.core.caret.LineWalker", "tinymce.core.caret.LineUtils", "tinymce.core.dom.NodeType", "tinymce.core.dom.RangeUtils", "tinymce.core.geom.ClientRect", "tinymce.core.util.VK", "tinymce.core.util.Fun", "tinymce.core.util.Arr", "tinymce.core.util.Delay", "tinymce.core.DragDropOverrides" ], function ( Env, CaretWalker, CaretPosition, CaretContainer, CaretContainerRemove, CaretUtils, FakeCaret, LineWalker, LineUtils, NodeType, RangeUtils, ClientRect, VK, Fun, Arr, Delay, DragDropOverrides ) { var curry = Fun.curry, isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, 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; function isFakeSelectionElement(elm) { return editor.dom.hasClass(elm, 'mce-offscreen-selection'); } 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; 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 BRabc|
\u00a0
').append(targetClone); range.setStartAfter($realSelectionContainer[0].firstChild.firstChild); range.setEndAfter(targetClone); } 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; hideFakeCaret(); return range; } function removeContentEditableSelection() { 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; } ); /** * NodePath.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.dom.NodePath', [ "tinymce.core.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 }; } ); /** * Quirks.js * * Released under LGPL License. * Copyright (c) 1999-2017 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.core.util.Quirks', [ "tinymce.core.util.VK", "tinymce.core.dom.RangeUtils", "tinymce.core.dom.TreeWalker", "tinymce.core.dom.NodePath", "tinymce.core.html.Node", "tinymce.core.html.Entities", "tinymce.core.Env", "tinymce.core.util.Tools", "tinymce.core.util.Delay", "tinymce.core.caret.CaretContainer", "tinymce.core.caret.CaretPosition", "tinymce.core.caret.CaretWalker" ], function (VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) { return function (editor) { var each = Tools.each; 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. * @param {Boolean} internal State if the paste is to be considered internal or external. */ function insertClipboardContents(content, internal) { if (editor.queryCommandSupported('mceInsertClipboardContent')) { editor.execCommand('mceInsertClipboardContent', false, { content: content, internal: internal }); } else { editor.execCommand('mceInsertContent', false, content); } } /** * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. * * For example: *|
* * Or: *|
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(); editor.selection.select(target); 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: ** * Becomes: *|x
|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: * * * Becomes this: * */ 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, true); } } }); } function refreshContentEditable() { // No-op since Mozilla seems to have fixed the caret repaint issues } 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); var prev = caretWalker.prev(startCaretPos); var next = caretWalker.next(endCaretPos); return !editor.selection.isCollapsed() && (!prev || (prev.isAtStart() && startCaretPos.isEqual(prev))) && (!next || (next.isAtEnd() && startCaretPos.isEqual(next))); } // 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(); 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 }; }; } ); /** * InitContentBody.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.init.InitContentBody', [ 'global!document', 'global!window', 'tinymce.core.caret.CaretContainerInput', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.Selection', 'tinymce.core.dom.Serializer', 'tinymce.core.EditorUpload', 'tinymce.core.ErrorReporter', 'tinymce.core.ForceBlocks', 'tinymce.core.Formatter', 'tinymce.core.html.DomParser', 'tinymce.core.html.Node', 'tinymce.core.html.Schema', 'tinymce.core.keyboard.KeyboardOverrides', 'tinymce.core.NodeChange', 'tinymce.core.SelectionOverrides', 'tinymce.core.UndoManager', 'tinymce.core.util.Delay', 'tinymce.core.util.Quirks', 'tinymce.core.util.Tools' ], function ( document, window, CaretContainerInput, DOMUtils, Selection, Serializer, EditorUpload, ErrorReporter, ForceBlocks, Formatter, DomParser, Node, Schema, KeyboardOverrides, NodeChange, SelectionOverrides, UndoManager, Delay, Quirks, Tools ) { var DOM = DOMUtils.DOM; var createParser = function (editor) { var parser = new DomParser(editor.settings, editor.schema); // Convert src and href into data-mce-src, data-mce-href and data-mce-style parser.addAttributeFilter('src,href,style,tabindex', function (nodes, name) { var i = nodes.length, node, dom = editor.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, editor.convertURL(value, name, node.name)); } } } }); // Keep scripts from executing 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); } } }); 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 + ']]'; } }); parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function (nodes) { var i = nodes.length, node, nonEmptyElements = editor.schema.getNonEmptyElements(); while (i--) { node = nodes[i]; if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) { node.append(new Node('br', 1)).shortEnded = true; } } }); return parser; }; var autoFocus = function (editor) { if (editor.settings.auto_focus) { Delay.setEditorTimeout(editor, function () { var focusEditor; if (editor.settings.auto_focus === true) { focusEditor = editor; } else { focusEditor = editor.editorManager.get(editor.settings.auto_focus); } if (!focusEditor.destroyed) { focusEditor.focus(); } }, 100); } }; var initEditor = function (editor) { editor.bindPendingEventDelegates(); editor.initialized = true; editor.fire('init'); editor.focus(true); editor.nodeChanged({ initial: true }); editor.execCallback('init_instance_callback', editor); autoFocus(editor); }; var initContentBody = function (editor, skipWrite) { var settings = editor.settings, targetElm = editor.getElement(), doc = editor.getDoc(), body, contentCssText; // Restore visibility on target element if (!settings.inline) { editor.getElement().style.visibility = editor.orgVisibility; } // Setup iframe body if (!skipWrite && !settings.content_editable) { doc.open(); doc.write(editor.iframeHTML); doc.close(); } if (settings.content_editable) { editor.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'); editor.contentDocument = doc = settings.content_document || document; editor.contentWindow = settings.content_window || window; editor.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 = editor.getBody(); body.disabled = true; editor.readonly = settings.readonly; if (!editor.readonly) { if (editor.inline && DOM.getStyle(body, 'position', true) === 'static') { body.style.position = 'relative'; } body.contentEditable = editor.getParam('content_editable_state', true); } body.disabled = false; editor.editorUpload = new EditorUpload(editor); editor.schema = new Schema(settings); editor.dom = new DOMUtils(doc, { keep_values: true, url_converter: editor.convertURL, url_converter_scope: editor, hex_colors: settings.force_hex_style_colors, class_filter: settings.class_filter, update_styles: true, root_element: editor.inline ? editor.getBody() : null, collect: settings.content_editable, schema: editor.schema, onSetAttrib: function (e) { editor.fire('SetAttrib', e); } }); editor.parser = createParser(editor); editor.serializer = new Serializer(settings, editor); editor.selection = new Selection(editor.dom, editor.getWin(), editor.serializer, editor); editor.formatter = new Formatter(editor); editor.undoManager = new UndoManager(editor); editor._nodeChangeDispatcher = new NodeChange(editor); editor._selectionOverrides = new SelectionOverrides(editor); CaretContainerInput.setup(editor); KeyboardOverrides.setup(editor); ForceBlocks.setup(editor); editor.fire('PreInit'); if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { doc.body.spellcheck = false; // Gecko DOM.setAttrib(body, "spellcheck", "false"); } editor.quirks = new Quirks(editor); editor.fire('PostRender'); if (settings.directionality) { body.dir = settings.directionality; } if (settings.nowrap) { body.style.whiteSpace = "nowrap"; } if (settings.protect) { editor.on('BeforeSetContent', function (e) { Tools.each(settings.protect, function (pattern) { e.content = e.content.replace(pattern, function (str) { return ''; }); }); }); } editor.on('SetContent', function () { editor.addVisual(editor.getBody()); }); // Remove empty contents if (settings.padd_empty_editor) { editor.on('PostProcess', function (e) { e.content = e.content.replace(/^(]*>( | |\s|\u00a0|
|)<\/p>[\r\n]*|
[\r\n]*)$/, '');
});
}
editor.load({ initial: true, format: 'html' });
editor.startContent = editor.getContent({ format: 'raw' });
editor.on('compositionstart compositionend', function (e) {
editor.composing = e.type === 'compositionstart';
});
// Add editor specific CSS styles
if (editor.contentStyles.length > 0) {
contentCssText = '';
Tools.each(editor.contentStyles, function (style) {
contentCssText += style + "\r\n";
});
editor.dom.addStyle(contentCssText);
}
editor.dom.styleSheetLoader.loadAll(
editor.contentCSS,
function (_) {
initEditor(editor);
},
function (urls) {
initEditor(editor);
ErrorReporter.contentCssError(editor, urls);
}
);
};
return {
initContentBody: initContentBody
};
}
);
/**
* PluginManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.PluginManager',
[
'tinymce.core.AddOnManager'
],
function (AddOnManager) {
return AddOnManager.PluginManager;
}
);
/**
* ThemeManager.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.ThemeManager',
[
'tinymce.core.AddOnManager'
],
function (AddOnManager) {
return AddOnManager.ThemeManager;
}
);
/**
* Init.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.init.Init',
[
'global!document',
'global!window',
'tinymce.core.dom.DOMUtils',
'tinymce.core.Env',
'tinymce.core.init.InitContentBody',
'tinymce.core.PluginManager',
'tinymce.core.ThemeManager',
'tinymce.core.util.Tools',
'tinymce.core.util.Uuid'
],
function (document, window, DOMUtils, Env, InitContentBody, PluginManager, ThemeManager, Tools, Uuid) {
var DOM = DOMUtils.DOM;
var initPlugin = function (editor, initializedPlugins, plugin) {
var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance;
pluginUrl = PluginManager.urls[plugin] || editor.documentBaseUrl.replace(/\/$/, '');
plugin = Tools.trim(plugin);
if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) {
Tools.each(PluginManager.dependencies(plugin), function (dep) {
initPlugin(editor, initializedPlugins, dep);
});
if (editor.plugins[plugin]) {
return;
}
pluginInstance = new Plugin(editor, pluginUrl, editor.$);
editor.plugins[plugin] = pluginInstance;
if (pluginInstance.init) {
pluginInstance.init(editor, pluginUrl);
initializedPlugins.push(plugin);
}
}
};
var initPlugins = function (editor) {
var initializedPlugins = [];
Tools.each(editor.settings.plugins.replace(/\-/g, '').split(/[ ,]/), function (name) {
initPlugin(editor, initializedPlugins, name);
});
};
var initTheme = function (editor) {
var Theme, settings = editor.settings;
if (settings.theme) {
if (typeof settings.theme != "function") {
settings.theme = settings.theme.replace(/-/, '');
Theme = ThemeManager.get(settings.theme);
editor.theme = new Theme(editor, ThemeManager.urls[settings.theme]);
if (editor.theme.init) {
editor.theme.init(editor, ThemeManager.urls[settings.theme] || editor.documentBaseUrl.replace(/\/$/, ''), editor.$);
}
} else {
editor.theme = settings.theme;
}
}
};
var measueBox = function (editor) {
var w, h, minHeight, re, o, settings = editor.settings, elm = editor.getElement();
// Measure box
if (settings.render_ui && editor.theme) {
editor.orgDisplay = elm.style.display;
if (typeof settings.theme != "function") {
w = settings.width || DOM.getStyle(elm, 'width') || '100%';
h = settings.height || DOM.getStyle(elm, '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 = editor.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(editor, elm);
if (o.editorContainer.nodeType) {
o.editorContainer.id = o.editorContainer.id || editor.id + "_parent";
}
if (o.iframeContainer.nodeType) {
o.iframeContainer.id = o.iframeContainer.id || editor.id + "_iframecontainer";
}
// Use specified iframe height or the targets offsetHeight
h = o.iframeHeight || elm.offsetHeight;
}
editor.editorContainer = o.editorContainer;
o.height = h;
}
return o;
};
var createIframe = function (editor, o) {
var settings = editor.settings, bodyId, bodyClass, url;
editor.iframeHTML = settings.doctype + '
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 () { Render.render(this); }, /** * 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 sidebar for the editor instance. * * @method addSidebar * @param {String} name Sidebar name to add. * @param {Object} settings Settings object with icon, onshow etc. * @example * // Adds a custom sidebar that when clicked logs the panel element * tinymce.init({ * ... * setup: function(ed) { * ed.addSidebar('example', { * tooltip: 'My sidebar', * icon: 'my-side-bar', * onshow: function(api) { * console.log(api.element()); * } * }); * } * }); */ addSidebar: function (name, settings) { return Sidebar.add(this, 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 ? '' : '