54552 lines
1.6 MiB
54552 lines
1.6 MiB
(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 = "<select msallowcapture=''><option selected=''></option></select>";
|
|
|
|
// 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: <number>) matching elements by id
|
|
for (; i !== len && (elem = elems[i]) != null; i++) {
|
|
if (byElement && elem) {
|
|
j = 0;
|
|
while ((matcher = elementMatchers[j++])) {
|
|
if (matcher(elem, context, xml)) {
|
|
results.push(elem);
|
|
break;
|
|
}
|
|
}
|
|
if (outermost) {
|
|
dirruns = dirrunsUnique;
|
|
}
|
|
}
|
|
|
|
// Track unmatched elements for set filters
|
|
if (bySet) {
|
|
// They will have gone through all possible matchers
|
|
if ((elem = !matcher && elem)) {
|
|
matchedCount--;
|
|
}
|
|
|
|
// Lengthen the array for every element, matched or not
|
|
if (seed) {
|
|
unmatched.push(elem);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply set filters to unmatched elements
|
|
matchedCount += i;
|
|
if (bySet && i !== matchedCount) {
|
|
j = 0;
|
|
while ((matcher = setMatchers[j++])) {
|
|
matcher(unmatched, setMatched, context, xml);
|
|
}
|
|
|
|
if (seed) {
|
|
// Reintegrate element matches to eliminate the need for sorting
|
|
if (matchedCount > 0) {
|
|
while (i--) {
|
|
if (!(unmatched[i] || setMatched[i])) {
|
|
setMatched[i] = pop.call(results);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discard index placeholder values to get only actual matches
|
|
setMatched = condense(setMatched);
|
|
}
|
|
|
|
// Add matches to results
|
|
push.apply(results, setMatched);
|
|
|
|
// Seedless set matches succeeding multiple successful matchers stipulate sorting
|
|
if (outermost && !seed && setMatched.length > 0 &&
|
|
(matchedCount + setMatchers.length) > 1) {
|
|
|
|
Sizzle.uniqueSort(results);
|
|
}
|
|
}
|
|
|
|
// Override manipulation of globals by nested matchers
|
|
if (outermost) {
|
|
dirruns = dirrunsUnique;
|
|
outermostContext = contextBackup;
|
|
}
|
|
|
|
return unmatched;
|
|
};
|
|
|
|
return bySet ?
|
|
markFunction(superMatcher) :
|
|
superMatcher;
|
|
}
|
|
|
|
compile = Sizzle.compile = function (selector, match /* Internal Use Only */) {
|
|
var i,
|
|
setMatchers = [],
|
|
elementMatchers = [],
|
|
cached = compilerCache[selector + " "];
|
|
|
|
if (!cached) {
|
|
// Generate a function of recursive functions that can be used to check each element
|
|
if (!match) {
|
|
match = tokenize(selector);
|
|
}
|
|
i = match.length;
|
|
while (i--) {
|
|
cached = matcherFromTokens(match[i]);
|
|
if (cached[expando]) {
|
|
setMatchers.push(cached);
|
|
} else {
|
|
elementMatchers.push(cached);
|
|
}
|
|
}
|
|
|
|
// Cache the compiled function
|
|
cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));
|
|
|
|
// Save selector and tokenization
|
|
cached.selector = selector;
|
|
}
|
|
return cached;
|
|
};
|
|
|
|
/**
|
|
* A low-level selection function that works with Sizzle's compiled
|
|
* selector functions
|
|
* @param {String|Function} selector A selector or a pre-compiled
|
|
* selector function built with Sizzle.compile
|
|
* @param {Element} context
|
|
* @param {Array} [results]
|
|
* @param {Array} [seed] A set of elements to match against
|
|
*/
|
|
select = Sizzle.select = function (selector, context, results, seed) {
|
|
var i, tokens, token, type, find,
|
|
compiled = typeof selector === "function" && selector,
|
|
match = !seed && tokenize((selector = compiled.selector || selector));
|
|
|
|
results = results || [];
|
|
|
|
// Try to minimize operations if there is no seed and only one group
|
|
if (match.length === 1) {
|
|
|
|
// Take a shortcut and set the context if the root selector is an ID
|
|
tokens = match[0] = match[0].slice(0);
|
|
if (tokens.length > 2 && (token = tokens[0]).type === "ID" &&
|
|
support.getById && context.nodeType === 9 && documentIsHTML &&
|
|
Expr.relative[tokens[1].type]) {
|
|
|
|
context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0];
|
|
if (!context) {
|
|
return results;
|
|
|
|
// Precompiled matchers will still verify ancestry, so step up a level
|
|
} else if (compiled) {
|
|
context = context.parentNode;
|
|
}
|
|
|
|
selector = selector.slice(tokens.shift().value.length);
|
|
}
|
|
|
|
// Fetch a seed set for right-to-left matching
|
|
i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length;
|
|
while (i--) {
|
|
token = tokens[i];
|
|
|
|
// Abort if we hit a combinator
|
|
if (Expr.relative[(type = token.type)]) {
|
|
break;
|
|
}
|
|
if ((find = Expr.find[type])) {
|
|
// Search, expanding context for leading sibling combinators
|
|
if ((seed = find(
|
|
token.matches[0].replace(runescape, funescape),
|
|
rsibling.test(tokens[0].type) && testContext(context.parentNode) || context
|
|
))) {
|
|
|
|
// If seed is empty or no tokens remain, we can return early
|
|
tokens.splice(i, 1);
|
|
selector = seed.length && toSelector(tokens);
|
|
if (!selector) {
|
|
push.apply(results, seed);
|
|
return results;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compile and execute a filtering function if one is not provided
|
|
// Provide `match` to avoid retokenization if we modified the selector above
|
|
(compiled || compile(selector, match))(
|
|
seed,
|
|
context,
|
|
!documentIsHTML,
|
|
results,
|
|
rsibling.test(selector) && testContext(context.parentNode) || context
|
|
);
|
|
return results;
|
|
};
|
|
|
|
// One-time assignments
|
|
|
|
// Sort stability
|
|
support.sortStable = expando.split("").sort(sortOrder).join("") === expando;
|
|
|
|
// Support: Chrome 14-35+
|
|
// Always assume duplicates if they aren't passed to the comparison function
|
|
support.detectDuplicates = !!hasDuplicate;
|
|
|
|
// Initialize against the default document
|
|
setDocument();
|
|
|
|
// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
|
|
// Detached nodes confoundingly follow *each other*
|
|
support.sortDetached = assert(function (div1) {
|
|
// Should return 1, but returns 4 (following)
|
|
return div1.compareDocumentPosition(document.createElement("div")) & 1;
|
|
}
|
|
);
|
|
|
|
// Support: IE<8
|
|
// Prevent attribute/property "interpolation"
|
|
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
|
|
if (!assert(function (div) {
|
|
div.innerHTML = "<a href='#'></a>";
|
|
return div.firstChild.getAttribute("href") === "#";
|
|
})) {
|
|
addHandle("type|href|height|width", function (elem, name, isXML) {
|
|
if (!isXML) {
|
|
return elem.getAttribute(name, name.toLowerCase() === "type" ? 1 : 2);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Use defaultValue in place of getAttribute("value")
|
|
if (!support.attributes || !assert(function (div) {
|
|
div.innerHTML = "<input/>";
|
|
div.firstChild.setAttribute("value", "");
|
|
return div.firstChild.getAttribute("value") === "";
|
|
})) {
|
|
addHandle("value", function (elem, name, isXML) {
|
|
if (!isXML && elem.nodeName.toLowerCase() === "input") {
|
|
return elem.defaultValue;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Support: IE<9
|
|
// Use getAttributeNode to fetch booleans when getAttribute lies
|
|
if (!assert(function (div) {
|
|
return div.getAttribute("disabled") == null;
|
|
})) {
|
|
addHandle(booleans, function (elem, name, isXML) {
|
|
var val;
|
|
if (!isXML) {
|
|
return elem[name] === true ? name.toLowerCase() :
|
|
(val = elem.getAttributeNode(name)) && val.specified ?
|
|
val.value :
|
|
null;
|
|
}
|
|
});
|
|
}
|
|
|
|
// EXPOSE
|
|
return Sizzle;
|
|
}
|
|
);
|
|
|
|
/*eslint-enable */
|
|
|
|
/**
|
|
* Arr.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
|
|
*/
|
|
|
|
/**
|
|
* Array utility class.
|
|
*
|
|
* @private
|
|
* @class tinymce.util.Arr
|
|
*/
|
|
define(
|
|
'tinymce.core.util.Arr',
|
|
[
|
|
],
|
|
function () {
|
|
var isArray = Array.isArray || function (obj) {
|
|
return Object.prototype.toString.call(obj) === "[object Array]";
|
|
};
|
|
|
|
function toArray(obj) {
|
|
var array = obj, i, l;
|
|
|
|
if (!isArray(obj)) {
|
|
array = [];
|
|
for (i = 0, l = obj.length; i < l; i++) {
|
|
array[i] = obj[i];
|
|
}
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
function each(o, cb, s) {
|
|
var n, l;
|
|
|
|
if (!o) {
|
|
return 0;
|
|
}
|
|
|
|
s = s || o;
|
|
|
|
if (o.length !== undefined) {
|
|
// Indexed arrays, needed for Safari
|
|
for (n = 0, l = o.length; n < l; n++) {
|
|
if (cb.call(s, o[n], n, o) === false) {
|
|
return 0;
|
|
}
|
|
}
|
|
} else {
|
|
// Hashtables
|
|
for (n in o) {
|
|
if (o.hasOwnProperty(n)) {
|
|
if (cb.call(s, o[n], n, o) === false) {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
function map(array, callback) {
|
|
var out = [];
|
|
|
|
each(array, function (item, index) {
|
|
out.push(callback(item, index, array));
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
function filter(a, f) {
|
|
var o = [];
|
|
|
|
each(a, function (v, index) {
|
|
if (!f || f(v, index, a)) {
|
|
o.push(v);
|
|
}
|
|
});
|
|
|
|
return o;
|
|
}
|
|
|
|
function indexOf(a, v) {
|
|
var i, l;
|
|
|
|
if (a) {
|
|
for (i = 0, l = a.length; i < l; i++) {
|
|
if (a[i] === v) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function reduce(collection, iteratee, accumulator, thisArg) {
|
|
var i = 0;
|
|
|
|
if (arguments.length < 3) {
|
|
accumulator = collection[0];
|
|
}
|
|
|
|
for (; i < collection.length; i++) {
|
|
accumulator = iteratee.call(thisArg, accumulator, collection[i], i);
|
|
}
|
|
|
|
return accumulator;
|
|
}
|
|
|
|
function findIndex(array, predicate, thisArg) {
|
|
var i, l;
|
|
|
|
for (i = 0, l = array.length; i < l; i++) {
|
|
if (predicate.call(thisArg, array[i], i, array)) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function find(array, predicate, thisArg) {
|
|
var idx = findIndex(array, predicate, thisArg);
|
|
|
|
if (idx !== -1) {
|
|
return array[idx];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function last(collection) {
|
|
return collection[collection.length - 1];
|
|
}
|
|
|
|
return {
|
|
isArray: isArray,
|
|
toArray: toArray,
|
|
each: each,
|
|
map: map,
|
|
filter: filter,
|
|
indexOf: indexOf,
|
|
reduce: reduce,
|
|
findIndex: findIndex,
|
|
find: find,
|
|
last: last
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Tools.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 utlity functions. These are also exposed
|
|
* directly on the tinymce namespace.
|
|
*
|
|
* @class tinymce.util.Tools
|
|
*/
|
|
define(
|
|
'tinymce.core.util.Tools',
|
|
[
|
|
"tinymce.core.Env",
|
|
"tinymce.core.util.Arr"
|
|
],
|
|
function (Env, Arr) {
|
|
/**
|
|
* Removes whitespace from the beginning and end of a string.
|
|
*
|
|
* @method trim
|
|
* @param {String} s String to remove whitespace from.
|
|
* @return {String} New string with removed whitespace.
|
|
*/
|
|
var whiteSpaceRegExp = /^\s*|\s*$/g;
|
|
|
|
function trim(str) {
|
|
return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
|
|
}
|
|
|
|
/**
|
|
* Checks if a object is of a specific type for example an array.
|
|
*
|
|
* @method is
|
|
* @param {Object} obj Object to check type of.
|
|
* @param {string} type Optional type to check for.
|
|
* @return {Boolean} true/false if the object is of the specified type.
|
|
*/
|
|
function is(obj, type) {
|
|
if (!type) {
|
|
return obj !== undefined;
|
|
}
|
|
|
|
if (type == 'array' && Arr.isArray(obj)) {
|
|
return true;
|
|
}
|
|
|
|
return typeof obj == type;
|
|
}
|
|
|
|
/**
|
|
* Makes a name/object map out of an array with names.
|
|
*
|
|
* @method makeMap
|
|
* @param {Array/String} items Items to make map out of.
|
|
* @param {String} delim Optional delimiter to split string by.
|
|
* @param {Object} map Optional map to add items to.
|
|
* @return {Object} Name/value map of items.
|
|
*/
|
|
function makeMap(items, delim, map) {
|
|
var i;
|
|
|
|
items = items || [];
|
|
delim = delim || ',';
|
|
|
|
if (typeof items == "string") {
|
|
items = items.split(delim);
|
|
}
|
|
|
|
map = map || {};
|
|
|
|
i = items.length;
|
|
while (i--) {
|
|
map[items[i]] = {};
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* JavaScript does not protect hasOwnProperty method, so it is possible to overwrite it. This is
|
|
* object independent version.
|
|
*
|
|
* @param {Object} obj
|
|
* @param {String} prop
|
|
* @returns {Boolean}
|
|
*/
|
|
function hasOwnProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
/**
|
|
* Creates a class, subclass or static singleton.
|
|
* More details on this method can be found in the Wiki.
|
|
*
|
|
* @method create
|
|
* @param {String} s Class name, inheritance and prefix.
|
|
* @param {Object} p Collection of methods to add to the class.
|
|
* @param {Object} root Optional root object defaults to the global window object.
|
|
* @example
|
|
* // Creates a basic class
|
|
* tinymce.create('tinymce.somepackage.SomeClass', {
|
|
* SomeClass: function() {
|
|
* // Class constructor
|
|
* },
|
|
*
|
|
* method: function() {
|
|
* // Some method
|
|
* }
|
|
* });
|
|
*
|
|
* // Creates a basic subclass class
|
|
* tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', {
|
|
* SomeSubClass: function() {
|
|
* // Class constructor
|
|
* this.parent(); // Call parent constructor
|
|
* },
|
|
*
|
|
* method: function() {
|
|
* // Some method
|
|
* this.parent(); // Call parent method
|
|
* },
|
|
*
|
|
* 'static': {
|
|
* staticMethod: function() {
|
|
* // Static method
|
|
* }
|
|
* }
|
|
* });
|
|
*
|
|
* // Creates a singleton/static class
|
|
* tinymce.create('static tinymce.somepackage.SomeSingletonClass', {
|
|
* method: function() {
|
|
* // Some method
|
|
* }
|
|
* });
|
|
*/
|
|
function create(s, p, root) {
|
|
var self = this, sp, ns, cn, scn, c, de = 0;
|
|
|
|
// Parse : <prefix> <class>:<super class>
|
|
s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
|
|
cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
|
|
|
|
// Create namespace for new class
|
|
ns = self.createNS(s[3].replace(/\.\w+$/, ''), root);
|
|
|
|
// Class already exists
|
|
if (ns[cn]) {
|
|
return;
|
|
}
|
|
|
|
// Make pure static class
|
|
if (s[2] == 'static') {
|
|
ns[cn] = p;
|
|
|
|
if (this.onCreate) {
|
|
this.onCreate(s[2], s[3], ns[cn]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Create default constructor
|
|
if (!p[cn]) {
|
|
p[cn] = function () { };
|
|
de = 1;
|
|
}
|
|
|
|
// Add constructor and methods
|
|
ns[cn] = p[cn];
|
|
self.extend(ns[cn].prototype, p);
|
|
|
|
// Extend
|
|
if (s[5]) {
|
|
sp = self.resolve(s[5]).prototype;
|
|
scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
|
|
|
|
// Extend constructor
|
|
c = ns[cn];
|
|
if (de) {
|
|
// Add passthrough constructor
|
|
ns[cn] = function () {
|
|
return sp[scn].apply(this, arguments);
|
|
};
|
|
} else {
|
|
// Add inherit constructor
|
|
ns[cn] = function () {
|
|
this.parent = sp[scn];
|
|
return c.apply(this, arguments);
|
|
};
|
|
}
|
|
ns[cn].prototype[cn] = ns[cn];
|
|
|
|
// Add super methods
|
|
self.each(sp, function (f, n) {
|
|
ns[cn].prototype[n] = sp[n];
|
|
});
|
|
|
|
// Add overridden methods
|
|
self.each(p, function (f, n) {
|
|
// Extend methods if needed
|
|
if (sp[n]) {
|
|
ns[cn].prototype[n] = function () {
|
|
this.parent = sp[n];
|
|
return f.apply(this, arguments);
|
|
};
|
|
} else {
|
|
if (n != cn) {
|
|
ns[cn].prototype[n] = f;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add static methods
|
|
/*jshint sub:true*/
|
|
/*eslint dot-notation:0*/
|
|
self.each(p['static'], function (f, n) {
|
|
ns[cn][n] = f;
|
|
});
|
|
}
|
|
|
|
function extend(obj, ext) {
|
|
var i, l, name, args = arguments, value;
|
|
|
|
for (i = 1, l = args.length; i < l; i++) {
|
|
ext = args[i];
|
|
for (name in ext) {
|
|
if (ext.hasOwnProperty(name)) {
|
|
value = ext[name];
|
|
|
|
if (value !== undefined) {
|
|
obj[name] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Executed the specified function for each item in a object tree.
|
|
*
|
|
* @method walk
|
|
* @param {Object} o Object tree to walk though.
|
|
* @param {function} f Function to call for each item.
|
|
* @param {String} n Optional name of collection inside the objects to walk for example childNodes.
|
|
* @param {String} s Optional scope to execute the function in.
|
|
*/
|
|
function walk(o, f, n, s) {
|
|
s = s || this;
|
|
|
|
if (o) {
|
|
if (n) {
|
|
o = o[n];
|
|
}
|
|
|
|
Arr.each(o, function (o, i) {
|
|
if (f.call(s, o, i, n) === false) {
|
|
return false;
|
|
}
|
|
|
|
walk(o, f, n, s);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a namespace on a specific object.
|
|
*
|
|
* @method createNS
|
|
* @param {String} n Namespace to create for example a.b.c.d.
|
|
* @param {Object} o Optional object to add namespace to, defaults to window.
|
|
* @return {Object} New namespace object the last item in path.
|
|
* @example
|
|
* // Create some namespace
|
|
* tinymce.createNS('tinymce.somepackage.subpackage');
|
|
*
|
|
* // Add a singleton
|
|
* var tinymce.somepackage.subpackage.SomeSingleton = {
|
|
* method: function() {
|
|
* // Some method
|
|
* }
|
|
* };
|
|
*/
|
|
function createNS(n, o) {
|
|
var i, v;
|
|
|
|
o = o || window;
|
|
|
|
n = n.split('.');
|
|
for (i = 0; i < n.length; i++) {
|
|
v = n[i];
|
|
|
|
if (!o[v]) {
|
|
o[v] = {};
|
|
}
|
|
|
|
o = o[v];
|
|
}
|
|
|
|
return o;
|
|
}
|
|
|
|
/**
|
|
* Resolves a string and returns the object from a specific structure.
|
|
*
|
|
* @method resolve
|
|
* @param {String} n Path to resolve for example a.b.c.d.
|
|
* @param {Object} o Optional object to search though, defaults to window.
|
|
* @return {Object} Last object in path or null if it couldn't be resolved.
|
|
* @example
|
|
* // Resolve a path into an object reference
|
|
* var obj = tinymce.resolve('a.b.c.d');
|
|
*/
|
|
function resolve(n, o) {
|
|
var i, l;
|
|
|
|
o = o || window;
|
|
|
|
n = n.split('.');
|
|
for (i = 0, l = n.length; i < l; i++) {
|
|
o = o[n[i]];
|
|
|
|
if (!o) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return o;
|
|
}
|
|
|
|
/**
|
|
* Splits a string but removes the whitespace before and after each value.
|
|
*
|
|
* @method explode
|
|
* @param {string} s String to split.
|
|
* @param {string} d Delimiter to split by.
|
|
* @example
|
|
* // Split a string into an array with a,b,c
|
|
* var arr = tinymce.explode('a, b, c');
|
|
*/
|
|
function explode(s, d) {
|
|
if (!s || is(s, 'array')) {
|
|
return s;
|
|
}
|
|
|
|
return Arr.map(s.split(d || ','), trim);
|
|
}
|
|
|
|
function _addCacheSuffix(url) {
|
|
var cacheSuffix = Env.cacheSuffix;
|
|
|
|
if (cacheSuffix) {
|
|
url += (url.indexOf('?') === -1 ? '?' : '&') + cacheSuffix;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
return {
|
|
trim: trim,
|
|
|
|
/**
|
|
* Returns true/false if the object is an array or not.
|
|
*
|
|
* @method isArray
|
|
* @param {Object} obj Object to check.
|
|
* @return {boolean} true/false state if the object is an array or not.
|
|
*/
|
|
isArray: Arr.isArray,
|
|
|
|
is: is,
|
|
|
|
/**
|
|
* Converts the specified object into a real JavaScript array.
|
|
*
|
|
* @method toArray
|
|
* @param {Object} obj Object to convert into array.
|
|
* @return {Array} Array object based in input.
|
|
*/
|
|
toArray: Arr.toArray,
|
|
makeMap: makeMap,
|
|
|
|
/**
|
|
* Performs an iteration of all items in a collection such as an object or array. This method will execure the
|
|
* callback function for each item in the collection, if the callback returns false the iteration will terminate.
|
|
* The callback has the following format: cb(value, key_or_index).
|
|
*
|
|
* @method each
|
|
* @param {Object} o Collection to iterate.
|
|
* @param {function} cb Callback function to execute for each item.
|
|
* @param {Object} s Optional scope to execute the callback in.
|
|
* @example
|
|
* // Iterate an array
|
|
* tinymce.each([1,2,3], function(v, i) {
|
|
* console.debug("Value: " + v + ", Index: " + i);
|
|
* });
|
|
*
|
|
* // Iterate an object
|
|
* tinymce.each({a: 1, b: 2, c: 3], function(v, k) {
|
|
* console.debug("Value: " + v + ", Key: " + k);
|
|
* });
|
|
*/
|
|
each: Arr.each,
|
|
|
|
/**
|
|
* Creates a new array by the return value of each iteration function call. This enables you to convert
|
|
* one array list into another.
|
|
*
|
|
* @method map
|
|
* @param {Array} array Array of items to iterate.
|
|
* @param {function} callback Function to call for each item. It's return value will be the new value.
|
|
* @return {Array} Array with new values based on function return values.
|
|
*/
|
|
map: Arr.map,
|
|
|
|
/**
|
|
* Filters out items from the input array by calling the specified function for each item.
|
|
* If the function returns false the item will be excluded if it returns true it will be included.
|
|
*
|
|
* @method grep
|
|
* @param {Array} a Array of items to loop though.
|
|
* @param {function} f Function to call for each item. Include/exclude depends on it's return value.
|
|
* @return {Array} New array with values imported and filtered based in input.
|
|
* @example
|
|
* // Filter out some items, this will return an array with 4 and 5
|
|
* var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;});
|
|
*/
|
|
grep: Arr.filter,
|
|
|
|
/**
|
|
* Returns an index of the item or -1 if item is not present in the array.
|
|
*
|
|
* @method inArray
|
|
* @param {any} item Item to search for.
|
|
* @param {Array} arr Array to search in.
|
|
* @return {Number} index of the item or -1 if item was not found.
|
|
*/
|
|
inArray: Arr.indexOf,
|
|
|
|
hasOwn: hasOwnProperty,
|
|
|
|
extend: extend,
|
|
create: create,
|
|
walk: walk,
|
|
createNS: createNS,
|
|
resolve: resolve,
|
|
explode: explode,
|
|
_addCacheSuffix: _addCacheSuffix
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* DomQuery.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 mimics most of the jQuery API:
|
|
*
|
|
* This is whats currently implemented:
|
|
* - Utility functions
|
|
* - DOM traversial
|
|
* - DOM manipulation
|
|
* - Event binding
|
|
*
|
|
* This is not currently implemented:
|
|
* - Dimension
|
|
* - Ajax
|
|
* - Animation
|
|
* - Advanced chaining
|
|
*
|
|
* @example
|
|
* var $ = tinymce.dom.DomQuery;
|
|
* $('p').attr('attr', 'value').addClass('class');
|
|
*
|
|
* @class tinymce.dom.DomQuery
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.DomQuery',
|
|
[
|
|
"tinymce.core.dom.EventUtils",
|
|
"tinymce.core.dom.Sizzle",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.Env"
|
|
],
|
|
function (EventUtils, Sizzle, Tools, Env) {
|
|
var doc = document, push = Array.prototype.push, slice = Array.prototype.slice;
|
|
var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
|
|
var Event = EventUtils.Event, undef;
|
|
var skipUniques = Tools.makeMap('children,contents,next,prev');
|
|
|
|
function isDefined(obj) {
|
|
return typeof obj !== 'undefined';
|
|
}
|
|
|
|
function isString(obj) {
|
|
return typeof obj === 'string';
|
|
}
|
|
|
|
function isWindow(obj) {
|
|
return obj && obj == obj.window;
|
|
}
|
|
|
|
function createFragment(html, fragDoc) {
|
|
var frag, node, container;
|
|
|
|
fragDoc = fragDoc || doc;
|
|
container = fragDoc.createElement('div');
|
|
frag = fragDoc.createDocumentFragment();
|
|
container.innerHTML = html;
|
|
|
|
while ((node = container.firstChild)) {
|
|
frag.appendChild(node);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
function domManipulate(targetNodes, sourceItem, callback, reverse) {
|
|
var i;
|
|
|
|
if (isString(sourceItem)) {
|
|
sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0]));
|
|
} else if (sourceItem.length && !sourceItem.nodeType) {
|
|
sourceItem = DomQuery.makeArray(sourceItem);
|
|
|
|
if (reverse) {
|
|
for (i = sourceItem.length - 1; i >= 0; i--) {
|
|
domManipulate(targetNodes, sourceItem[i], callback, reverse);
|
|
}
|
|
} else {
|
|
for (i = 0; i < sourceItem.length; i++) {
|
|
domManipulate(targetNodes, sourceItem[i], callback, reverse);
|
|
}
|
|
}
|
|
|
|
return targetNodes;
|
|
}
|
|
|
|
if (sourceItem.nodeType) {
|
|
i = targetNodes.length;
|
|
while (i--) {
|
|
callback.call(targetNodes[i], sourceItem);
|
|
}
|
|
}
|
|
|
|
return targetNodes;
|
|
}
|
|
|
|
function hasClass(node, className) {
|
|
return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1;
|
|
}
|
|
|
|
function wrap(elements, wrapper, all) {
|
|
var lastParent, newWrapper;
|
|
|
|
wrapper = DomQuery(wrapper)[0];
|
|
|
|
elements.each(function () {
|
|
var self = this;
|
|
|
|
if (!all || lastParent != self.parentNode) {
|
|
lastParent = self.parentNode;
|
|
newWrapper = wrapper.cloneNode(false);
|
|
self.parentNode.insertBefore(newWrapper, self);
|
|
newWrapper.appendChild(self);
|
|
} else {
|
|
newWrapper.appendChild(self);
|
|
}
|
|
});
|
|
|
|
return elements;
|
|
}
|
|
|
|
var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' ');
|
|
var booleanMap = Tools.makeMap('checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected', ' ');
|
|
var propFix = {
|
|
'for': 'htmlFor',
|
|
'class': 'className',
|
|
'readonly': 'readOnly'
|
|
};
|
|
var cssFix = {
|
|
'float': 'cssFloat'
|
|
};
|
|
|
|
var attrHooks = {}, cssHooks = {};
|
|
|
|
function DomQuery(selector, context) {
|
|
/*eslint new-cap:0 */
|
|
return new DomQuery.fn.init(selector, context);
|
|
}
|
|
|
|
function inArray(item, array) {
|
|
var i;
|
|
|
|
if (array.indexOf) {
|
|
return array.indexOf(item);
|
|
}
|
|
|
|
i = array.length;
|
|
while (i--) {
|
|
if (array[i] === item) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
var whiteSpaceRegExp = /^\s*|\s*$/g;
|
|
|
|
function trim(str) {
|
|
return (str === null || str === undef) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
|
|
}
|
|
|
|
function each(obj, callback) {
|
|
var length, key, i, undef, value;
|
|
|
|
if (obj) {
|
|
length = obj.length;
|
|
|
|
if (length === undef) {
|
|
// Loop object items
|
|
for (key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
value = obj[key];
|
|
if (callback.call(value, key, value) === false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Loop array items
|
|
for (i = 0; i < length; i++) {
|
|
value = obj[i];
|
|
if (callback.call(value, i, value) === false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
function grep(array, callback) {
|
|
var out = [];
|
|
|
|
each(array, function (i, item) {
|
|
if (callback(item, i)) {
|
|
out.push(item);
|
|
}
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
function getElementDocument(element) {
|
|
if (!element) {
|
|
return doc;
|
|
}
|
|
|
|
if (element.nodeType == 9) {
|
|
return element;
|
|
}
|
|
|
|
return element.ownerDocument;
|
|
}
|
|
|
|
DomQuery.fn = DomQuery.prototype = {
|
|
constructor: DomQuery,
|
|
|
|
/**
|
|
* Selector for the current set.
|
|
*
|
|
* @property selector
|
|
* @type String
|
|
*/
|
|
selector: "",
|
|
|
|
/**
|
|
* Context used to create the set.
|
|
*
|
|
* @property context
|
|
* @type Element
|
|
*/
|
|
context: null,
|
|
|
|
/**
|
|
* Number of items in the current set.
|
|
*
|
|
* @property length
|
|
* @type Number
|
|
*/
|
|
length: 0,
|
|
|
|
/**
|
|
* Constructs a new DomQuery instance with the specified selector or context.
|
|
*
|
|
* @constructor
|
|
* @method init
|
|
* @param {String/Array/DomQuery} selector Optional CSS selector/Array or array like object or HTML string.
|
|
* @param {Document/Element} context Optional context to search in.
|
|
*/
|
|
init: function (selector, context) {
|
|
var self = this, match, node;
|
|
|
|
if (!selector) {
|
|
return self;
|
|
}
|
|
|
|
if (selector.nodeType) {
|
|
self.context = self[0] = selector;
|
|
self.length = 1;
|
|
|
|
return self;
|
|
}
|
|
|
|
if (context && context.nodeType) {
|
|
self.context = context;
|
|
} else {
|
|
if (context) {
|
|
return DomQuery(selector).attr(context);
|
|
}
|
|
|
|
self.context = context = document;
|
|
}
|
|
|
|
if (isString(selector)) {
|
|
self.selector = selector;
|
|
|
|
if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
|
|
match = [null, selector, null];
|
|
} else {
|
|
match = rquickExpr.exec(selector);
|
|
}
|
|
|
|
if (match) {
|
|
if (match[1]) {
|
|
node = createFragment(selector, getElementDocument(context)).firstChild;
|
|
|
|
while (node) {
|
|
push.call(self, node);
|
|
node = node.nextSibling;
|
|
}
|
|
} else {
|
|
node = getElementDocument(context).getElementById(match[2]);
|
|
|
|
if (!node) {
|
|
return self;
|
|
}
|
|
|
|
if (node.id !== match[2]) {
|
|
return self.find(selector);
|
|
}
|
|
|
|
self.length = 1;
|
|
self[0] = node;
|
|
}
|
|
} else {
|
|
return DomQuery(context).find(selector);
|
|
}
|
|
} else {
|
|
this.add(selector, false);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Converts the current set to an array.
|
|
*
|
|
* @method toArray
|
|
* @return {Array} Array of all nodes in set.
|
|
*/
|
|
toArray: function () {
|
|
return Tools.toArray(this);
|
|
},
|
|
|
|
/**
|
|
* Adds new nodes to the set.
|
|
*
|
|
* @method add
|
|
* @param {Array/tinymce.core.dom.DomQuery} items Array of all nodes to add to set.
|
|
* @param {Boolean} sort Optional sort flag that enables sorting of elements.
|
|
* @return {tinymce.dom.DomQuery} New instance with nodes added.
|
|
*/
|
|
add: function (items, sort) {
|
|
var self = this, nodes, i;
|
|
|
|
if (isString(items)) {
|
|
return self.add(DomQuery(items));
|
|
}
|
|
|
|
if (sort !== false) {
|
|
nodes = DomQuery.unique(self.toArray().concat(DomQuery.makeArray(items)));
|
|
self.length = nodes.length;
|
|
for (i = 0; i < nodes.length; i++) {
|
|
self[i] = nodes[i];
|
|
}
|
|
} else {
|
|
push.apply(self, DomQuery.makeArray(items));
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Sets/gets attributes on the elements in the current set.
|
|
*
|
|
* @method attr
|
|
* @param {String/Object} name Name of attribute to get or an object with attributes to set.
|
|
* @param {String} value Optional value to set.
|
|
* @return {tinymce.dom.DomQuery/String} Current set or the specified attribute when only the name is specified.
|
|
*/
|
|
attr: function (name, value) {
|
|
var self = this, hook;
|
|
|
|
if (typeof name === "object") {
|
|
each(name, function (name, value) {
|
|
self.attr(name, value);
|
|
});
|
|
} else if (isDefined(value)) {
|
|
this.each(function () {
|
|
var hook;
|
|
|
|
if (this.nodeType === 1) {
|
|
hook = attrHooks[name];
|
|
if (hook && hook.set) {
|
|
hook.set(this, value);
|
|
return;
|
|
}
|
|
|
|
if (value === null) {
|
|
this.removeAttribute(name, 2);
|
|
} else {
|
|
this.setAttribute(name, value, 2);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
if (self[0] && self[0].nodeType === 1) {
|
|
hook = attrHooks[name];
|
|
if (hook && hook.get) {
|
|
return hook.get(self[0], name);
|
|
}
|
|
|
|
if (booleanMap[name]) {
|
|
return self.prop(name) ? name : undef;
|
|
}
|
|
|
|
value = self[0].getAttribute(name, 2);
|
|
|
|
if (value === null) {
|
|
value = undef;
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Removes attributse on the elements in the current set.
|
|
*
|
|
* @method removeAttr
|
|
* @param {String/Object} name Name of attribute to remove.
|
|
* @return {tinymce.dom.DomQuery/String} Current set.
|
|
*/
|
|
removeAttr: function (name) {
|
|
return this.attr(name, null);
|
|
},
|
|
|
|
/**
|
|
* Sets/gets properties on the elements in the current set.
|
|
*
|
|
* @method attr
|
|
* @param {String/Object} name Name of property to get or an object with properties to set.
|
|
* @param {String} value Optional value to set.
|
|
* @return {tinymce.dom.DomQuery/String} Current set or the specified property when only the name is specified.
|
|
*/
|
|
prop: function (name, value) {
|
|
var self = this;
|
|
|
|
name = propFix[name] || name;
|
|
|
|
if (typeof name === "object") {
|
|
each(name, function (name, value) {
|
|
self.prop(name, value);
|
|
});
|
|
} else if (isDefined(value)) {
|
|
this.each(function () {
|
|
if (this.nodeType == 1) {
|
|
this[name] = value;
|
|
}
|
|
});
|
|
} else {
|
|
if (self[0] && self[0].nodeType && name in self[0]) {
|
|
return self[0][name];
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Sets/gets styles on the elements in the current set.
|
|
*
|
|
* @method css
|
|
* @param {String/Object} name Name of style to get or an object with styles to set.
|
|
* @param {String} value Optional value to set.
|
|
* @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified.
|
|
*/
|
|
css: function (name, value) {
|
|
var self = this, elm, hook;
|
|
|
|
function camel(name) {
|
|
return name.replace(/-(\D)/g, function (a, b) {
|
|
return b.toUpperCase();
|
|
});
|
|
}
|
|
|
|
function dashed(name) {
|
|
return name.replace(/[A-Z]/g, function (a) {
|
|
return '-' + a;
|
|
});
|
|
}
|
|
|
|
if (typeof name === "object") {
|
|
each(name, function (name, value) {
|
|
self.css(name, value);
|
|
});
|
|
} else {
|
|
if (isDefined(value)) {
|
|
name = camel(name);
|
|
|
|
// Default px suffix on these
|
|
if (typeof value === 'number' && !numericCssMap[name]) {
|
|
value += 'px';
|
|
}
|
|
|
|
self.each(function () {
|
|
var style = this.style;
|
|
|
|
hook = cssHooks[name];
|
|
if (hook && hook.set) {
|
|
hook.set(this, value);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.style[cssFix[name] || name] = value;
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
|
|
if (value === null || value === '') {
|
|
if (style.removeProperty) {
|
|
style.removeProperty(dashed(name));
|
|
} else {
|
|
style.removeAttribute(name);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
elm = self[0];
|
|
|
|
hook = cssHooks[name];
|
|
if (hook && hook.get) {
|
|
return hook.get(elm);
|
|
}
|
|
|
|
if (elm.ownerDocument.defaultView) {
|
|
try {
|
|
return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name));
|
|
} catch (ex) {
|
|
return undef;
|
|
}
|
|
} else if (elm.currentStyle) {
|
|
return elm.currentStyle[camel(name)];
|
|
}
|
|
}
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Removes all nodes in set from the document.
|
|
*
|
|
* @method remove
|
|
* @return {tinymce.dom.DomQuery} Current set with the removed nodes.
|
|
*/
|
|
remove: function () {
|
|
var self = this, node, i = this.length;
|
|
|
|
while (i--) {
|
|
node = self[i];
|
|
Event.clean(node);
|
|
|
|
if (node.parentNode) {
|
|
node.parentNode.removeChild(node);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Empties all elements in set.
|
|
*
|
|
* @method empty
|
|
* @return {tinymce.dom.DomQuery} Current set with the empty nodes.
|
|
*/
|
|
empty: function () {
|
|
var self = this, node, i = this.length;
|
|
|
|
while (i--) {
|
|
node = self[i];
|
|
while (node.firstChild) {
|
|
node.removeChild(node.firstChild);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets or gets the HTML of the current set or first set node.
|
|
*
|
|
* @method html
|
|
* @param {String} value Optional innerHTML value to set on each element.
|
|
* @return {tinymce.dom.DomQuery/String} Current set or the innerHTML of the first element.
|
|
*/
|
|
html: function (value) {
|
|
var self = this, i;
|
|
|
|
if (isDefined(value)) {
|
|
i = self.length;
|
|
|
|
try {
|
|
while (i--) {
|
|
self[i].innerHTML = value;
|
|
}
|
|
} catch (ex) {
|
|
// Workaround for "Unknown runtime error" when DIV is added to P on IE
|
|
DomQuery(self[i]).empty().append(value);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
return self[0] ? self[0].innerHTML : '';
|
|
},
|
|
|
|
/**
|
|
* Sets or gets the text of the current set or first set node.
|
|
*
|
|
* @method text
|
|
* @param {String} value Optional innerText value to set on each element.
|
|
* @return {tinymce.dom.DomQuery/String} Current set or the innerText of the first element.
|
|
*/
|
|
text: function (value) {
|
|
var self = this, i;
|
|
|
|
if (isDefined(value)) {
|
|
i = self.length;
|
|
while (i--) {
|
|
if ("innerText" in self[i]) {
|
|
self[i].innerText = value;
|
|
} else {
|
|
self[0].textContent = value;
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
return self[0] ? (self[0].innerText || self[0].textContent) : '';
|
|
},
|
|
|
|
/**
|
|
* Appends the specified node/html or node set to the current set nodes.
|
|
*
|
|
* @method append
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to append to each element in set.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
append: function () {
|
|
return domManipulate(this, arguments, function (node) {
|
|
// Either element or Shadow Root
|
|
if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
|
|
this.appendChild(node);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Prepends the specified node/html or node set to the current set nodes.
|
|
*
|
|
* @method prepend
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to prepend to each element in set.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
prepend: function () {
|
|
return domManipulate(this, arguments, function (node) {
|
|
// Either element or Shadow Root
|
|
if (this.nodeType === 1 || (this.host && this.host.nodeType === 1)) {
|
|
this.insertBefore(node, this.firstChild);
|
|
}
|
|
}, true);
|
|
},
|
|
|
|
/**
|
|
* Adds the specified elements before current set nodes.
|
|
*
|
|
* @method before
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add before to each element in set.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
before: function () {
|
|
var self = this;
|
|
|
|
if (self[0] && self[0].parentNode) {
|
|
return domManipulate(self, arguments, function (node) {
|
|
this.parentNode.insertBefore(node, this);
|
|
});
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Adds the specified elements after current set nodes.
|
|
*
|
|
* @method after
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to add after to each element in set.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
after: function () {
|
|
var self = this;
|
|
|
|
if (self[0] && self[0].parentNode) {
|
|
return domManipulate(self, arguments, function (node) {
|
|
this.parentNode.insertBefore(node, this.nextSibling);
|
|
}, true);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Appends the specified set nodes to the specified selector/instance.
|
|
*
|
|
* @method appendTo
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} val Item to append the current set to.
|
|
* @return {tinymce.dom.DomQuery} Current set with the appended nodes.
|
|
*/
|
|
appendTo: function (val) {
|
|
DomQuery(val).append(this);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Prepends the specified set nodes to the specified selector/instance.
|
|
*
|
|
* @method prependTo
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} val Item to prepend the current set to.
|
|
* @return {tinymce.dom.DomQuery} Current set with the prepended nodes.
|
|
*/
|
|
prependTo: function (val) {
|
|
DomQuery(val).prepend(this);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Replaces the nodes in set with the specified content.
|
|
*
|
|
* @method replaceWith
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to replace nodes with.
|
|
* @return {tinymce.dom.DomQuery} Set with replaced nodes.
|
|
*/
|
|
replaceWith: function (content) {
|
|
return this.before(content).remove();
|
|
},
|
|
|
|
/**
|
|
* Wraps all elements in set with the specified wrapper.
|
|
*
|
|
* @method wrap
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
|
|
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
|
|
*/
|
|
wrap: function (content) {
|
|
return wrap(this, content);
|
|
},
|
|
|
|
/**
|
|
* Wraps all nodes in set with the specified wrapper. If the nodes are siblings all of them
|
|
* will be wrapped in the same wrapper.
|
|
*
|
|
* @method wrapAll
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
|
|
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
|
|
*/
|
|
wrapAll: function (content) {
|
|
return wrap(this, content, true);
|
|
},
|
|
|
|
/**
|
|
* Wraps all elements inner contents in set with the specified wrapper.
|
|
*
|
|
* @method wrapInner
|
|
* @param {String/Element/Array/tinymce.dom.DomQuery} content Content to wrap nodes with.
|
|
* @return {tinymce.dom.DomQuery} Set with wrapped nodes.
|
|
*/
|
|
wrapInner: function (content) {
|
|
this.each(function () {
|
|
DomQuery(this).contents().wrapAll(content);
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Unwraps all elements by removing the parent element of each item in set.
|
|
*
|
|
* @method unwrap
|
|
* @return {tinymce.dom.DomQuery} Set with unwrapped nodes.
|
|
*/
|
|
unwrap: function () {
|
|
return this.parent().each(function () {
|
|
DomQuery(this).replaceWith(this.childNodes);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Clones all nodes in set.
|
|
*
|
|
* @method clone
|
|
* @return {tinymce.dom.DomQuery} Set with cloned nodes.
|
|
*/
|
|
clone: function () {
|
|
var result = [];
|
|
|
|
this.each(function () {
|
|
result.push(this.cloneNode(true));
|
|
});
|
|
|
|
return DomQuery(result);
|
|
},
|
|
|
|
/**
|
|
* Adds the specified class name to the current set elements.
|
|
*
|
|
* @method addClass
|
|
* @param {String} className Class name to add.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
addClass: function (className) {
|
|
return this.toggleClass(className, true);
|
|
},
|
|
|
|
/**
|
|
* Removes the specified class name to the current set elements.
|
|
*
|
|
* @method removeClass
|
|
* @param {String} className Class name to remove.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
removeClass: function (className) {
|
|
return this.toggleClass(className, false);
|
|
},
|
|
|
|
/**
|
|
* Toggles the specified class name on the current set elements.
|
|
*
|
|
* @method toggleClass
|
|
* @param {String} className Class name to add/remove.
|
|
* @param {Boolean} state Optional state to toggle on/off.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
toggleClass: function (className, state) {
|
|
var self = this;
|
|
|
|
// Functions are not supported
|
|
if (typeof className != 'string') {
|
|
return self;
|
|
}
|
|
|
|
if (className.indexOf(' ') !== -1) {
|
|
each(className.split(' '), function () {
|
|
self.toggleClass(this, state);
|
|
});
|
|
} else {
|
|
self.each(function (index, node) {
|
|
var existingClassName, classState;
|
|
|
|
classState = hasClass(node, className);
|
|
if (classState !== state) {
|
|
existingClassName = node.className;
|
|
|
|
if (classState) {
|
|
node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' '));
|
|
} else {
|
|
node.className += existingClassName ? ' ' + className : className;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the first item in set has the specified class.
|
|
*
|
|
* @method hasClass
|
|
* @param {String} className Class name to check for.
|
|
* @return {Boolean} True/false if the set has the specified class.
|
|
*/
|
|
hasClass: function (className) {
|
|
return hasClass(this[0], className);
|
|
},
|
|
|
|
/**
|
|
* Executes the callback function for each item DomQuery collection. If you return false in the
|
|
* callback it will break the loop.
|
|
*
|
|
* @method each
|
|
* @param {function} callback Callback function to execute for each item.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
each: function (callback) {
|
|
return each(this, callback);
|
|
},
|
|
|
|
/**
|
|
* Binds an event with callback function to the elements in set.
|
|
*
|
|
* @method on
|
|
* @param {String} name Name of the event to bind.
|
|
* @param {function} callback Callback function to execute when the event occurs.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
on: function (name, callback) {
|
|
return this.each(function () {
|
|
Event.bind(this, name, callback);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Unbinds an event with callback function to the elements in set.
|
|
*
|
|
* @method off
|
|
* @param {String} name Optional name of the event to bind.
|
|
* @param {function} callback Optional callback function to execute when the event occurs.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
off: function (name, callback) {
|
|
return this.each(function () {
|
|
Event.unbind(this, name, callback);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Triggers the specified event by name or event object.
|
|
*
|
|
* @method trigger
|
|
* @param {String/Object} name Name of the event to trigger or event object.
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
trigger: function (name) {
|
|
return this.each(function () {
|
|
if (typeof name == 'object') {
|
|
Event.fire(this, name.type, name);
|
|
} else {
|
|
Event.fire(this, name);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Shows all elements in set.
|
|
*
|
|
* @method show
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
show: function () {
|
|
return this.css('display', '');
|
|
},
|
|
|
|
/**
|
|
* Hides all elements in set.
|
|
*
|
|
* @method hide
|
|
* @return {tinymce.dom.DomQuery} Current set.
|
|
*/
|
|
hide: function () {
|
|
return this.css('display', 'none');
|
|
},
|
|
|
|
/**
|
|
* Slices the current set.
|
|
*
|
|
* @method slice
|
|
* @param {Number} start Start index to slice at.
|
|
* @param {Number} end Optional end index to end slice at.
|
|
* @return {tinymce.dom.DomQuery} Sliced set.
|
|
*/
|
|
slice: function () {
|
|
return new DomQuery(slice.apply(this, arguments));
|
|
},
|
|
|
|
/**
|
|
* Makes the set equal to the specified index.
|
|
*
|
|
* @method eq
|
|
* @param {Number} index Index to set it equal to.
|
|
* @return {tinymce.dom.DomQuery} Single item set.
|
|
*/
|
|
eq: function (index) {
|
|
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
|
|
},
|
|
|
|
/**
|
|
* Makes the set equal to first element in set.
|
|
*
|
|
* @method first
|
|
* @return {tinymce.dom.DomQuery} Single item set.
|
|
*/
|
|
first: function () {
|
|
return this.eq(0);
|
|
},
|
|
|
|
/**
|
|
* Makes the set equal to last element in set.
|
|
*
|
|
* @method last
|
|
* @return {tinymce.dom.DomQuery} Single item set.
|
|
*/
|
|
last: function () {
|
|
return this.eq(-1);
|
|
},
|
|
|
|
/**
|
|
* Finds elements by the specified selector for each element in set.
|
|
*
|
|
* @method find
|
|
* @param {String} selector Selector to find elements by.
|
|
* @return {tinymce.dom.DomQuery} Set with matches elements.
|
|
*/
|
|
find: function (selector) {
|
|
var i, l, ret = [];
|
|
|
|
for (i = 0, l = this.length; i < l; i++) {
|
|
DomQuery.find(selector, this[i], ret);
|
|
}
|
|
|
|
return DomQuery(ret);
|
|
},
|
|
|
|
/**
|
|
* Filters the current set with the specified selector.
|
|
*
|
|
* @method filter
|
|
* @param {String/function} selector Selector to filter elements by.
|
|
* @return {tinymce.dom.DomQuery} Set with filtered elements.
|
|
*/
|
|
filter: function (selector) {
|
|
if (typeof selector == 'function') {
|
|
return DomQuery(grep(this.toArray(), function (item, i) {
|
|
return selector(i, item);
|
|
}));
|
|
}
|
|
|
|
return DomQuery(DomQuery.filter(selector, this.toArray()));
|
|
},
|
|
|
|
/**
|
|
* Gets the current node or any parent matching the specified selector.
|
|
*
|
|
* @method closest
|
|
* @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find.
|
|
* @return {tinymce.dom.DomQuery} Set with closest elements.
|
|
*/
|
|
closest: function (selector) {
|
|
var result = [];
|
|
|
|
if (selector instanceof DomQuery) {
|
|
selector = selector[0];
|
|
}
|
|
|
|
this.each(function (i, node) {
|
|
while (node) {
|
|
if (typeof selector == 'string' && DomQuery(node).is(selector)) {
|
|
result.push(node);
|
|
break;
|
|
} else if (node == selector) {
|
|
result.push(node);
|
|
break;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
});
|
|
|
|
return DomQuery(result);
|
|
},
|
|
|
|
/**
|
|
* Returns the offset of the first element in set or sets the top/left css properties of all elements in set.
|
|
*
|
|
* @method offset
|
|
* @param {Object} offset Optional offset object to set on each item.
|
|
* @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset.
|
|
*/
|
|
offset: function (offset) {
|
|
var elm, doc, docElm;
|
|
var x = 0, y = 0, pos;
|
|
|
|
if (!offset) {
|
|
elm = this[0];
|
|
|
|
if (elm) {
|
|
doc = elm.ownerDocument;
|
|
docElm = doc.documentElement;
|
|
|
|
if (elm.getBoundingClientRect) {
|
|
pos = elm.getBoundingClientRect();
|
|
x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft;
|
|
y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop;
|
|
}
|
|
}
|
|
|
|
return {
|
|
left: x,
|
|
top: y
|
|
};
|
|
}
|
|
|
|
return this.css(offset);
|
|
},
|
|
|
|
push: push,
|
|
sort: [].sort,
|
|
splice: [].splice
|
|
};
|
|
|
|
// Static members
|
|
Tools.extend(DomQuery, {
|
|
/**
|
|
* Extends the specified object with one or more objects.
|
|
*
|
|
* @static
|
|
* @method extend
|
|
* @param {Object} target Target object to extend with new items.
|
|
* @param {Object..} object Object to extend the target with.
|
|
* @return {Object} Extended input object.
|
|
*/
|
|
extend: Tools.extend,
|
|
|
|
/**
|
|
* Creates an array out of an array like object.
|
|
*
|
|
* @static
|
|
* @method makeArray
|
|
* @param {Object} object Object to convert to array.
|
|
* @return {Array} Array produced from object.
|
|
*/
|
|
makeArray: function (object) {
|
|
if (isWindow(object) || object.nodeType) {
|
|
return [object];
|
|
}
|
|
|
|
return Tools.toArray(object);
|
|
},
|
|
|
|
/**
|
|
* Returns the index of the specified item inside the array.
|
|
*
|
|
* @static
|
|
* @method inArray
|
|
* @param {Object} item Item to look for.
|
|
* @param {Array} array Array to look for item in.
|
|
* @return {Number} Index of the item or -1.
|
|
*/
|
|
inArray: inArray,
|
|
|
|
/**
|
|
* Returns true/false if the specified object is an array or not.
|
|
*
|
|
* @static
|
|
* @method isArray
|
|
* @param {Object} array Object to check if it's an array or not.
|
|
* @return {Boolean} True/false if the object is an array.
|
|
*/
|
|
isArray: Tools.isArray,
|
|
|
|
/**
|
|
* Executes the callback function for each item in array/object. If you return false in the
|
|
* callback it will break the loop.
|
|
*
|
|
* @static
|
|
* @method each
|
|
* @param {Object} obj Object to iterate.
|
|
* @param {function} callback Callback function to execute for each item.
|
|
*/
|
|
each: each,
|
|
|
|
/**
|
|
* Removes whitespace from the beginning and end of a string.
|
|
*
|
|
* @static
|
|
* @method trim
|
|
* @param {String} str String to remove whitespace from.
|
|
* @return {String} New string with removed whitespace.
|
|
*/
|
|
trim: trim,
|
|
|
|
/**
|
|
* Filters out items from the input array by calling the specified function for each item.
|
|
* If the function returns false the item will be excluded if it returns true it will be included.
|
|
*
|
|
* @static
|
|
* @method grep
|
|
* @param {Array} array Array of items to loop though.
|
|
* @param {function} callback Function to call for each item. Include/exclude depends on it's return value.
|
|
* @return {Array} New array with values imported and filtered based in input.
|
|
* @example
|
|
* // Filter out some items, this will return an array with 4 and 5
|
|
* var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;});
|
|
*/
|
|
grep: grep,
|
|
|
|
// Sizzle
|
|
find: Sizzle,
|
|
expr: Sizzle.selectors,
|
|
unique: Sizzle.uniqueSort,
|
|
text: Sizzle.getText,
|
|
contains: Sizzle.contains,
|
|
filter: function (expr, elems, not) {
|
|
var i = elems.length;
|
|
|
|
if (not) {
|
|
expr = ":not(" + expr + ")";
|
|
}
|
|
|
|
while (i--) {
|
|
if (elems[i].nodeType != 1) {
|
|
elems.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
if (elems.length === 1) {
|
|
elems = DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : [];
|
|
} else {
|
|
elems = DomQuery.find.matches(expr, elems);
|
|
}
|
|
|
|
return elems;
|
|
}
|
|
});
|
|
|
|
function dir(el, prop, until) {
|
|
var matched = [], cur = el[prop];
|
|
|
|
if (typeof until != 'string' && until instanceof DomQuery) {
|
|
until = until[0];
|
|
}
|
|
|
|
while (cur && cur.nodeType !== 9) {
|
|
if (until !== undefined) {
|
|
if (cur === until) {
|
|
break;
|
|
}
|
|
|
|
if (typeof until == 'string' && DomQuery(cur).is(until)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cur.nodeType === 1) {
|
|
matched.push(cur);
|
|
}
|
|
|
|
cur = cur[prop];
|
|
}
|
|
|
|
return matched;
|
|
}
|
|
|
|
function sibling(node, siblingName, nodeType, until) {
|
|
var result = [];
|
|
|
|
if (until instanceof DomQuery) {
|
|
until = until[0];
|
|
}
|
|
|
|
for (; node; node = node[siblingName]) {
|
|
if (nodeType && node.nodeType !== nodeType) {
|
|
continue;
|
|
}
|
|
|
|
if (until !== undefined) {
|
|
if (node === until) {
|
|
break;
|
|
}
|
|
|
|
if (typeof until == 'string' && DomQuery(node).is(until)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
result.push(node);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function firstSibling(node, siblingName, nodeType) {
|
|
for (node = node[siblingName]; node; node = node[siblingName]) {
|
|
if (node.nodeType == nodeType) {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
each({
|
|
/**
|
|
* Returns a new collection with the parent of each item in current collection matching the optional selector.
|
|
*
|
|
* @method parent
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
|
|
*/
|
|
parent: function (node) {
|
|
var parent = node.parentNode;
|
|
|
|
return parent && parent.nodeType !== 11 ? parent : null;
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection with the all the parents of each item in current collection matching the optional selector.
|
|
*
|
|
* @method parents
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to match parents against.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
|
|
*/
|
|
parents: function (node) {
|
|
return dir(node, "parentNode");
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection with next sibling of each item in current collection matching the optional selector.
|
|
*
|
|
* @method next
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to match the next element against.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
next: function (node) {
|
|
return firstSibling(node, 'nextSibling', 1);
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection with previous sibling of each item in current collection matching the optional selector.
|
|
*
|
|
* @method prev
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to match the previous element against.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
prev: function (node) {
|
|
return firstSibling(node, 'previousSibling', 1);
|
|
},
|
|
|
|
/**
|
|
* Returns all child elements matching the optional selector.
|
|
*
|
|
* @method children
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to match the elements against.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
children: function (node) {
|
|
return sibling(node.firstChild, 'nextSibling', 1);
|
|
},
|
|
|
|
/**
|
|
* Returns all child nodes matching the optional selector.
|
|
*
|
|
* @method contents
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to get the contents of.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
contents: function (node) {
|
|
return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
|
|
}
|
|
}, function (name, fn) {
|
|
DomQuery.fn[name] = function (selector) {
|
|
var self = this, result = [];
|
|
|
|
self.each(function () {
|
|
var nodes = fn.call(result, this, selector, result);
|
|
|
|
if (nodes) {
|
|
if (DomQuery.isArray(nodes)) {
|
|
result.push.apply(result, nodes);
|
|
} else {
|
|
result.push(nodes);
|
|
}
|
|
}
|
|
});
|
|
|
|
// If traversing on multiple elements we might get the same elements twice
|
|
if (this.length > 1) {
|
|
if (!skipUniques[name]) {
|
|
result = DomQuery.unique(result);
|
|
}
|
|
|
|
if (name.indexOf('parents') === 0) {
|
|
result = result.reverse();
|
|
}
|
|
}
|
|
|
|
result = DomQuery(result);
|
|
|
|
if (selector) {
|
|
return result.filter(selector);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
});
|
|
|
|
each({
|
|
/**
|
|
* Returns a new collection with the all the parents until the matching selector/element
|
|
* of each item in current collection matching the optional selector.
|
|
*
|
|
* @method parentsUntil
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to find parent of.
|
|
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
|
|
*/
|
|
parentsUntil: function (node, until) {
|
|
return dir(node, "parentNode", until);
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection with all next siblings of each item in current collection matching the optional selector.
|
|
*
|
|
* @method nextUntil
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to find next siblings on.
|
|
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
nextUntil: function (node, until) {
|
|
return sibling(node, 'nextSibling', 1, until).slice(1);
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
|
|
*
|
|
* @method prevUntil
|
|
* @param {Element/tinymce.dom.DomQuery} node Node to find previous siblings on.
|
|
* @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
|
|
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
|
|
*/
|
|
prevUntil: function (node, until) {
|
|
return sibling(node, 'previousSibling', 1, until).slice(1);
|
|
}
|
|
}, function (name, fn) {
|
|
DomQuery.fn[name] = function (selector, filter) {
|
|
var self = this, result = [];
|
|
|
|
self.each(function () {
|
|
var nodes = fn.call(result, this, selector, result);
|
|
|
|
if (nodes) {
|
|
if (DomQuery.isArray(nodes)) {
|
|
result.push.apply(result, nodes);
|
|
} else {
|
|
result.push(nodes);
|
|
}
|
|
}
|
|
});
|
|
|
|
// If traversing on multiple elements we might get the same elements twice
|
|
if (this.length > 1) {
|
|
result = DomQuery.unique(result);
|
|
|
|
if (name.indexOf('parents') === 0 || name === 'prevUntil') {
|
|
result = result.reverse();
|
|
}
|
|
}
|
|
|
|
result = DomQuery(result);
|
|
|
|
if (filter) {
|
|
return result.filter(filter);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Returns true/false if the current set items matches the selector.
|
|
*
|
|
* @method is
|
|
* @param {String} selector Selector to match the elements against.
|
|
* @return {Boolean} True/false if the current set matches the selector.
|
|
*/
|
|
DomQuery.fn.is = function (selector) {
|
|
return !!selector && this.filter(selector).length > 0;
|
|
};
|
|
|
|
DomQuery.fn.init.prototype = DomQuery.fn;
|
|
|
|
DomQuery.overrideDefaults = function (callback) {
|
|
var defaults;
|
|
|
|
function sub(selector, context) {
|
|
defaults = defaults || callback();
|
|
|
|
if (arguments.length === 0) {
|
|
selector = defaults.element;
|
|
}
|
|
|
|
if (!context) {
|
|
context = defaults.context;
|
|
}
|
|
|
|
return new sub.fn.init(selector, context);
|
|
}
|
|
|
|
DomQuery.extend(sub, this);
|
|
|
|
return sub;
|
|
};
|
|
|
|
function appendHooks(targetHooks, prop, hooks) {
|
|
each(hooks, function (name, func) {
|
|
targetHooks[name] = targetHooks[name] || {};
|
|
targetHooks[name][prop] = func;
|
|
});
|
|
}
|
|
|
|
if (Env.ie && Env.ie < 8) {
|
|
appendHooks(attrHooks, 'get', {
|
|
maxlength: function (elm) {
|
|
var value = elm.maxLength;
|
|
|
|
if (value === 0x7fffffff) {
|
|
return undef;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
size: function (elm) {
|
|
var value = elm.size;
|
|
|
|
if (value === 20) {
|
|
return undef;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
'class': function (elm) {
|
|
return elm.className;
|
|
},
|
|
|
|
style: function (elm) {
|
|
var value = elm.style.cssText;
|
|
|
|
if (value.length === 0) {
|
|
return undef;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
});
|
|
|
|
appendHooks(attrHooks, 'set', {
|
|
'class': function (elm, value) {
|
|
elm.className = value;
|
|
},
|
|
|
|
style: function (elm, value) {
|
|
elm.style.cssText = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (Env.ie && Env.ie < 9) {
|
|
/*jshint sub:true */
|
|
/*eslint dot-notation: 0*/
|
|
cssFix['float'] = 'styleFloat';
|
|
|
|
appendHooks(cssHooks, 'set', {
|
|
opacity: function (elm, value) {
|
|
var style = elm.style;
|
|
|
|
if (value === null || value === '') {
|
|
style.removeAttribute('filter');
|
|
} else {
|
|
style.zoom = 1;
|
|
style.filter = 'alpha(opacity=' + (value * 100) + ')';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
DomQuery.attrHooks = attrHooks;
|
|
DomQuery.cssHooks = cssHooks;
|
|
|
|
return DomQuery;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Styles.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 parse CSS styles it also compresses styles to reduce the output size.
|
|
*
|
|
* @example
|
|
* var Styles = new tinymce.html.Styles({
|
|
* url_converter: function(url) {
|
|
* return url;
|
|
* }
|
|
* });
|
|
*
|
|
* styles = Styles.parse('border: 1px solid red');
|
|
* styles.color = 'red';
|
|
*
|
|
* console.log(new tinymce.html.StyleSerializer().serialize(styles));
|
|
*
|
|
* @class tinymce.html.Styles
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.Styles',
|
|
[
|
|
],
|
|
function () {
|
|
return function (settings, schema) {
|
|
/*jshint maxlen:255 */
|
|
/*eslint max-len:0 */
|
|
var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
|
|
urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
|
|
styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
|
|
trimRightRegExp = /\s+$/,
|
|
i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF';
|
|
|
|
settings = settings || {};
|
|
|
|
if (schema) {
|
|
validStyles = schema.getValidStyles();
|
|
invalidStyles = schema.getInvalidStyles();
|
|
}
|
|
|
|
encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
|
|
for (i = 0; i < encodingItems.length; i++) {
|
|
encodingLookup[encodingItems[i]] = invisibleChar + i;
|
|
encodingLookup[invisibleChar + i] = encodingItems[i];
|
|
}
|
|
|
|
function toHex(match, r, g, b) {
|
|
function hex(val) {
|
|
val = parseInt(val, 10).toString(16);
|
|
|
|
return val.length > 1 ? val : '0' + val; // 0 -> 00
|
|
}
|
|
|
|
return '#' + hex(r) + hex(g) + hex(b);
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Parses the specified RGB color value and returns a hex version of that color.
|
|
*
|
|
* @method toHex
|
|
* @param {String} color RGB string value like rgb(1,2,3)
|
|
* @return {String} Hex version of that RGB value like #FF00FF.
|
|
*/
|
|
toHex: function (color) {
|
|
return color.replace(rgbRegExp, toHex);
|
|
},
|
|
|
|
/**
|
|
* Parses the specified style value into an object collection. This parser will also
|
|
* merge and remove any redundant items that browsers might have added. It will also convert non hex
|
|
* colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
|
|
*
|
|
* @method parse
|
|
* @param {String} css Style value to parse for example: border:1px solid red;.
|
|
* @return {Object} Object representation of that style like {border: '1px solid red'}
|
|
*/
|
|
parse: function (css) {
|
|
var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter;
|
|
var urlConverterScope = settings.url_converter_scope || this;
|
|
|
|
function compress(prefix, suffix, noJoin) {
|
|
var top, right, bottom, left;
|
|
|
|
top = styles[prefix + '-top' + suffix];
|
|
if (!top) {
|
|
return;
|
|
}
|
|
|
|
right = styles[prefix + '-right' + suffix];
|
|
if (!right) {
|
|
return;
|
|
}
|
|
|
|
bottom = styles[prefix + '-bottom' + suffix];
|
|
if (!bottom) {
|
|
return;
|
|
}
|
|
|
|
left = styles[prefix + '-left' + suffix];
|
|
if (!left) {
|
|
return;
|
|
}
|
|
|
|
var box = [top, right, bottom, left];
|
|
i = box.length - 1;
|
|
while (i--) {
|
|
if (box[i] !== box[i + 1]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i > -1 && noJoin) {
|
|
return;
|
|
}
|
|
|
|
styles[prefix + suffix] = i == -1 ? box[0] : box.join(' ');
|
|
delete styles[prefix + '-top' + suffix];
|
|
delete styles[prefix + '-right' + suffix];
|
|
delete styles[prefix + '-bottom' + suffix];
|
|
delete styles[prefix + '-left' + suffix];
|
|
}
|
|
|
|
/**
|
|
* Checks if the specific style can be compressed in other words if all border-width are equal.
|
|
*/
|
|
function canCompress(key) {
|
|
var value = styles[key], i;
|
|
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
value = value.split(' ');
|
|
i = value.length;
|
|
while (i--) {
|
|
if (value[i] !== value[0]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
styles[key] = value[0];
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Compresses multiple styles into one style.
|
|
*/
|
|
function compress2(target, a, b, c) {
|
|
if (!canCompress(a)) {
|
|
return;
|
|
}
|
|
|
|
if (!canCompress(b)) {
|
|
return;
|
|
}
|
|
|
|
if (!canCompress(c)) {
|
|
return;
|
|
}
|
|
|
|
// Compress
|
|
styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
|
|
delete styles[a];
|
|
delete styles[b];
|
|
delete styles[c];
|
|
}
|
|
|
|
// Encodes the specified string by replacing all \" \' ; : with _<num>
|
|
function encode(str) {
|
|
isEncoded = true;
|
|
|
|
return encodingLookup[str];
|
|
}
|
|
|
|
// Decodes the specified string by replacing all _<num> with it's original value \" \' etc
|
|
// It will also decode the \" \' if keepSlashes is set to fale or omitted
|
|
function decode(str, keepSlashes) {
|
|
if (isEncoded) {
|
|
str = str.replace(/\uFEFF[0-9]/g, function (str) {
|
|
return encodingLookup[str];
|
|
});
|
|
}
|
|
|
|
if (!keepSlashes) {
|
|
str = str.replace(/\\([\'\";:])/g, "$1");
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
function decodeSingleHexSequence(escSeq) {
|
|
return String.fromCharCode(parseInt(escSeq.slice(1), 16));
|
|
}
|
|
|
|
function decodeHexSequences(value) {
|
|
return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
|
|
}
|
|
|
|
function processUrl(match, url, url2, url3, str, str2) {
|
|
str = str || str2;
|
|
|
|
if (str) {
|
|
str = decode(str);
|
|
|
|
// Force strings into single quote format
|
|
return "'" + str.replace(/\'/g, "\\'") + "'";
|
|
}
|
|
|
|
url = decode(url || url2 || url3);
|
|
|
|
if (!settings.allow_script_urls) {
|
|
var scriptUrl = url.replace(/[\s\r\n]+/g, '');
|
|
|
|
if (/(java|vb)script:/i.test(scriptUrl)) {
|
|
return "";
|
|
}
|
|
|
|
if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// Convert the URL to relative/absolute depending on config
|
|
if (urlConverter) {
|
|
url = urlConverter.call(urlConverterScope, url, 'style');
|
|
}
|
|
|
|
// Output new URL format
|
|
return "url('" + url.replace(/\'/g, "\\'") + "')";
|
|
}
|
|
|
|
if (css) {
|
|
css = css.replace(/[\u0000-\u001F]/g, '');
|
|
|
|
// Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
|
|
css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function (str) {
|
|
return str.replace(/[;:]/g, encode);
|
|
});
|
|
|
|
// Parse styles
|
|
while ((matches = styleRegExp.exec(css))) {
|
|
styleRegExp.lastIndex = matches.index + matches[0].length;
|
|
name = matches[1].replace(trimRightRegExp, '').toLowerCase();
|
|
value = matches[2].replace(trimRightRegExp, '');
|
|
|
|
if (name && value) {
|
|
// Decode escaped sequences like \65 -> e
|
|
name = decodeHexSequences(name);
|
|
value = decodeHexSequences(value);
|
|
|
|
// Skip properties with double quotes and sequences like \" \' in their names
|
|
// See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations'
|
|
// https://cure53.de/fp170.pdf
|
|
if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) {
|
|
continue;
|
|
}
|
|
|
|
// Don't allow behavior name or expression/comments within the values
|
|
if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) {
|
|
continue;
|
|
}
|
|
|
|
// Opera will produce 700 instead of bold in their style values
|
|
if (name === 'font-weight' && value === '700') {
|
|
value = 'bold';
|
|
} else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
|
|
value = value.toLowerCase();
|
|
}
|
|
|
|
// Convert RGB colors to HEX
|
|
value = value.replace(rgbRegExp, toHex);
|
|
|
|
// Convert URLs and force them into url('value') format
|
|
value = value.replace(urlOrStrRegExp, processUrl);
|
|
styles[name] = isEncoded ? decode(value, true) : value;
|
|
}
|
|
}
|
|
// Compress the styles to reduce it's size for example IE will expand styles
|
|
compress("border", "", true);
|
|
compress("border", "-width");
|
|
compress("border", "-color");
|
|
compress("border", "-style");
|
|
compress("padding", "");
|
|
compress("margin", "");
|
|
compress2('border', 'border-width', 'border-style', 'border-color');
|
|
|
|
// Remove pointless border, IE produces these
|
|
if (styles.border === 'medium none') {
|
|
delete styles.border;
|
|
}
|
|
|
|
// IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
|
|
// So let us assume it shouldn't be there
|
|
if (styles['border-image'] === 'none') {
|
|
delete styles['border-image'];
|
|
}
|
|
}
|
|
|
|
return styles;
|
|
},
|
|
|
|
/**
|
|
* Serializes the specified style object into a string.
|
|
*
|
|
* @method serialize
|
|
* @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
|
|
* @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized.
|
|
* @return {String} String representation of the style object for example: border: 1px solid red.
|
|
*/
|
|
serialize: function (styles, elementName) {
|
|
var css = '', name, value;
|
|
|
|
function serializeStyles(name) {
|
|
var styleList, i, l, value;
|
|
|
|
styleList = validStyles[name];
|
|
if (styleList) {
|
|
for (i = 0, l = styleList.length; i < l; i++) {
|
|
name = styleList[i];
|
|
value = styles[name];
|
|
|
|
if (value) {
|
|
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isValid(name, elementName) {
|
|
var styleMap;
|
|
|
|
styleMap = invalidStyles['*'];
|
|
if (styleMap && styleMap[name]) {
|
|
return false;
|
|
}
|
|
|
|
styleMap = invalidStyles[elementName];
|
|
if (styleMap && styleMap[name]) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Serialize styles according to schema
|
|
if (elementName && validStyles) {
|
|
// Serialize global styles and element specific styles
|
|
serializeStyles('*');
|
|
serializeStyles(elementName);
|
|
} else {
|
|
// Output the styles in the order they are inside the object
|
|
for (name in styles) {
|
|
value = styles[name];
|
|
|
|
if (value && (!invalidStyles || isValid(name, elementName))) {
|
|
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
|
}
|
|
}
|
|
}
|
|
|
|
return css;
|
|
}
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* TreeWalker.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
|
|
*/
|
|
|
|
/**
|
|
* TreeWalker class enables you to walk the DOM in a linear manner.
|
|
*
|
|
* @class tinymce.dom.TreeWalker
|
|
* @example
|
|
* var walker = new tinymce.dom.TreeWalker(startNode);
|
|
*
|
|
* do {
|
|
* console.log(walker.current());
|
|
* } while (walker.next());
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.TreeWalker',
|
|
[
|
|
],
|
|
function () {
|
|
/**
|
|
* Constructs a new TreeWalker instance.
|
|
*
|
|
* @constructor
|
|
* @method TreeWalker
|
|
* @param {Node} startNode Node to start walking from.
|
|
* @param {node} rootNode Optional root node to never walk out of.
|
|
*/
|
|
return function (startNode, rootNode) {
|
|
var node = startNode;
|
|
|
|
function findSibling(node, startName, siblingName, shallow) {
|
|
var sibling, parent;
|
|
|
|
if (node) {
|
|
// Walk into nodes if it has a start
|
|
if (!shallow && node[startName]) {
|
|
return node[startName];
|
|
}
|
|
|
|
// Return the sibling if it has one
|
|
if (node != rootNode) {
|
|
sibling = node[siblingName];
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
|
|
// Walk up the parents to look for siblings
|
|
for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) {
|
|
sibling = parent[siblingName];
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function findPreviousNode(node, startName, siblingName, shallow) {
|
|
var sibling, parent, child;
|
|
|
|
if (node) {
|
|
sibling = node[siblingName];
|
|
if (rootNode && sibling === rootNode) {
|
|
return;
|
|
}
|
|
|
|
if (sibling) {
|
|
if (!shallow) {
|
|
// Walk up the parents to look for siblings
|
|
for (child = sibling[startName]; child; child = child[startName]) {
|
|
if (!child[startName]) {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
|
|
return sibling;
|
|
}
|
|
|
|
parent = node.parentNode;
|
|
if (parent && parent !== rootNode) {
|
|
return parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current node.
|
|
*
|
|
* @method current
|
|
* @return {Node} Current node where the walker is.
|
|
*/
|
|
this.current = function () {
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Walks to the next node in tree.
|
|
*
|
|
* @method next
|
|
* @return {Node} Current node where the walker is after moving to the next node.
|
|
*/
|
|
this.next = function (shallow) {
|
|
node = findSibling(node, 'firstChild', 'nextSibling', shallow);
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Walks to the previous node in tree.
|
|
*
|
|
* @method prev
|
|
* @return {Node} Current node where the walker is after moving to the previous node.
|
|
*/
|
|
this.prev = function (shallow) {
|
|
node = findSibling(node, 'lastChild', 'previousSibling', shallow);
|
|
return node;
|
|
};
|
|
|
|
this.prev2 = function (shallow) {
|
|
node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow);
|
|
return node;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Entities.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 bitwise:false */
|
|
/*eslint no-bitwise:0 */
|
|
|
|
/**
|
|
* Entity encoder class.
|
|
*
|
|
* @class tinymce.html.Entities
|
|
* @static
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.Entities',
|
|
[
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Tools) {
|
|
var makeMap = Tools.makeMap;
|
|
|
|
var namedEntities, baseEntities, reverseEntities,
|
|
attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
|
textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
|
rawCharsRegExp = /[<>&\"\']/g,
|
|
entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi,
|
|
asciiMap = {
|
|
128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020",
|
|
135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152",
|
|
142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022",
|
|
150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A",
|
|
156: "\u0153", 158: "\u017E", 159: "\u0178"
|
|
};
|
|
|
|
// Raw entities
|
|
baseEntities = {
|
|
'\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code
|
|
"'": ''',
|
|
'<': '<',
|
|
'>': '>',
|
|
'&': '&',
|
|
'\u0060': '`'
|
|
};
|
|
|
|
// Reverse lookup table for raw entities
|
|
reverseEntities = {
|
|
'<': '<',
|
|
'>': '>',
|
|
'&': '&',
|
|
'"': '"',
|
|
''': "'"
|
|
};
|
|
|
|
// Decodes text by using the browser
|
|
function nativeDecode(text) {
|
|
var elm;
|
|
|
|
elm = document.createElement("div");
|
|
elm.innerHTML = text;
|
|
|
|
return elm.textContent || elm.innerText || text;
|
|
}
|
|
|
|
// Build a two way lookup table for the entities
|
|
function buildEntitiesLookup(items, radix) {
|
|
var i, chr, entity, lookup = {};
|
|
|
|
if (items) {
|
|
items = items.split(',');
|
|
radix = radix || 10;
|
|
|
|
// Build entities lookup table
|
|
for (i = 0; i < items.length; i += 2) {
|
|
chr = String.fromCharCode(parseInt(items[i], radix));
|
|
|
|
// Only add non base entities
|
|
if (!baseEntities[chr]) {
|
|
entity = '&' + items[i + 1] + ';';
|
|
lookup[chr] = entity;
|
|
lookup[entity] = chr;
|
|
}
|
|
}
|
|
|
|
return lookup;
|
|
}
|
|
}
|
|
|
|
// Unpack entities lookup where the numbers are in radix 32 to reduce the size
|
|
namedEntities = buildEntitiesLookup(
|
|
'50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
|
|
'5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
|
|
'5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
|
|
'5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
|
|
'68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
|
|
'6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
|
|
'6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
|
|
'75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
|
|
'7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
|
|
'7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
|
|
'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
|
|
'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
|
|
't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
|
|
'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
|
|
'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
|
|
'81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
|
|
'8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
|
|
'8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
|
|
'8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
|
|
'8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
|
|
'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
|
|
'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
|
|
'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
|
|
'80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
|
|
'811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
|
|
|
|
var Entities = {
|
|
/**
|
|
* Encodes the specified string using raw entities. This means only the required XML base entities will be encoded.
|
|
*
|
|
* @method encodeRaw
|
|
* @param {String} text Text to encode.
|
|
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
|
* @return {String} Entity encoded text.
|
|
*/
|
|
encodeRaw: function (text, attr) {
|
|
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
|
|
return baseEntities[chr] || chr;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
|
|
* since it doesn't know if the context is within a attribute or text node. This was added for compatibility
|
|
* and is exposed as the DOMUtils.encode function.
|
|
*
|
|
* @method encodeAllRaw
|
|
* @param {String} text Text to encode.
|
|
* @return {String} Entity encoded text.
|
|
*/
|
|
encodeAllRaw: function (text) {
|
|
return ('' + text).replace(rawCharsRegExp, function (chr) {
|
|
return baseEntities[chr] || chr;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Encodes the specified string using numeric entities. The core entities will be
|
|
* encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
|
|
*
|
|
* @method encodeNumeric
|
|
* @param {String} text Text to encode.
|
|
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
|
* @return {String} Entity encoded text.
|
|
*/
|
|
encodeNumeric: function (text, attr) {
|
|
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
|
|
// Multi byte sequence convert it to a single entity
|
|
if (chr.length > 1) {
|
|
return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
|
|
}
|
|
|
|
return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Encodes the specified string using named entities. The core entities will be encoded
|
|
* as named ones but all non lower ascii characters will be encoded into named entities.
|
|
*
|
|
* @method encodeNamed
|
|
* @param {String} text Text to encode.
|
|
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
|
* @param {Object} entities Optional parameter with entities to use.
|
|
* @return {String} Entity encoded text.
|
|
*/
|
|
encodeNamed: function (text, attr, entities) {
|
|
entities = entities || namedEntities;
|
|
|
|
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
|
|
return baseEntities[chr] || entities[chr] || chr;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns an encode function based on the name(s) and it's optional entities.
|
|
*
|
|
* @method getEncodeFunc
|
|
* @param {String} name Comma separated list of encoders for example named,numeric.
|
|
* @param {String} entities Optional parameter with entities to use instead of the built in set.
|
|
* @return {function} Encode function to be used.
|
|
*/
|
|
getEncodeFunc: function (name, entities) {
|
|
entities = buildEntitiesLookup(entities) || namedEntities;
|
|
|
|
function encodeNamedAndNumeric(text, attr) {
|
|
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
|
|
return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
|
|
});
|
|
}
|
|
|
|
function encodeCustomNamed(text, attr) {
|
|
return Entities.encodeNamed(text, attr, entities);
|
|
}
|
|
|
|
// Replace + with , to be compatible with previous TinyMCE versions
|
|
name = makeMap(name.replace(/\+/g, ','));
|
|
|
|
// Named and numeric encoder
|
|
if (name.named && name.numeric) {
|
|
return encodeNamedAndNumeric;
|
|
}
|
|
|
|
// Named encoder
|
|
if (name.named) {
|
|
// Custom names
|
|
if (entities) {
|
|
return encodeCustomNamed;
|
|
}
|
|
|
|
return Entities.encodeNamed;
|
|
}
|
|
|
|
// Numeric
|
|
if (name.numeric) {
|
|
return Entities.encodeNumeric;
|
|
}
|
|
|
|
// Raw encoder
|
|
return Entities.encodeRaw;
|
|
},
|
|
|
|
/**
|
|
* Decodes the specified string, this will replace entities with raw UTF characters.
|
|
*
|
|
* @method decode
|
|
* @param {String} text Text to entity decode.
|
|
* @return {String} Entity decoded string.
|
|
*/
|
|
decode: function (text) {
|
|
return text.replace(entityRegExp, function (all, numeric) {
|
|
if (numeric) {
|
|
if (numeric.charAt(0).toLowerCase() === 'x') {
|
|
numeric = parseInt(numeric.substr(1), 16);
|
|
} else {
|
|
numeric = parseInt(numeric, 10);
|
|
}
|
|
|
|
// Support upper UTF
|
|
if (numeric > 0xFFFF) {
|
|
numeric -= 0x10000;
|
|
|
|
return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF));
|
|
}
|
|
|
|
return asciiMap[numeric] || String.fromCharCode(numeric);
|
|
}
|
|
|
|
return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
|
|
});
|
|
}
|
|
};
|
|
|
|
return Entities;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Range.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
|
|
*/
|
|
|
|
/**
|
|
* Old IE Range.
|
|
*
|
|
* @private
|
|
* @class tinymce.dom.Range
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.Range',
|
|
[
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Tools) {
|
|
// Range constructor
|
|
function Range(dom) {
|
|
var self = this,
|
|
doc = dom.doc,
|
|
EXTRACT = 0,
|
|
CLONE = 1,
|
|
DELETE = 2,
|
|
TRUE = true,
|
|
FALSE = false,
|
|
START_OFFSET = 'startOffset',
|
|
START_CONTAINER = 'startContainer',
|
|
END_CONTAINER = 'endContainer',
|
|
END_OFFSET = 'endOffset',
|
|
extend = Tools.extend,
|
|
nodeIndex = dom.nodeIndex;
|
|
|
|
function createDocumentFragment() {
|
|
return doc.createDocumentFragment();
|
|
}
|
|
|
|
function setStart(n, o) {
|
|
_setEndPoint(TRUE, n, o);
|
|
}
|
|
|
|
function setEnd(n, o) {
|
|
_setEndPoint(FALSE, n, o);
|
|
}
|
|
|
|
function setStartBefore(n) {
|
|
setStart(n.parentNode, nodeIndex(n));
|
|
}
|
|
|
|
function setStartAfter(n) {
|
|
setStart(n.parentNode, nodeIndex(n) + 1);
|
|
}
|
|
|
|
function setEndBefore(n) {
|
|
setEnd(n.parentNode, nodeIndex(n));
|
|
}
|
|
|
|
function setEndAfter(n) {
|
|
setEnd(n.parentNode, nodeIndex(n) + 1);
|
|
}
|
|
|
|
function collapse(ts) {
|
|
if (ts) {
|
|
self[END_CONTAINER] = self[START_CONTAINER];
|
|
self[END_OFFSET] = self[START_OFFSET];
|
|
} else {
|
|
self[START_CONTAINER] = self[END_CONTAINER];
|
|
self[START_OFFSET] = self[END_OFFSET];
|
|
}
|
|
|
|
self.collapsed = TRUE;
|
|
}
|
|
|
|
function selectNode(n) {
|
|
setStartBefore(n);
|
|
setEndAfter(n);
|
|
}
|
|
|
|
function selectNodeContents(n) {
|
|
setStart(n, 0);
|
|
setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
|
|
}
|
|
|
|
function compareBoundaryPoints(h, r) {
|
|
var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET],
|
|
rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
|
|
|
|
// Check START_TO_START
|
|
if (h === 0) {
|
|
return _compareBoundaryPoints(sc, so, rsc, rso);
|
|
}
|
|
|
|
// Check START_TO_END
|
|
if (h === 1) {
|
|
return _compareBoundaryPoints(ec, eo, rsc, rso);
|
|
}
|
|
|
|
// Check END_TO_END
|
|
if (h === 2) {
|
|
return _compareBoundaryPoints(ec, eo, rec, reo);
|
|
}
|
|
|
|
// Check END_TO_START
|
|
if (h === 3) {
|
|
return _compareBoundaryPoints(sc, so, rec, reo);
|
|
}
|
|
}
|
|
|
|
function deleteContents() {
|
|
_traverse(DELETE);
|
|
}
|
|
|
|
function extractContents() {
|
|
return _traverse(EXTRACT);
|
|
}
|
|
|
|
function cloneContents() {
|
|
return _traverse(CLONE);
|
|
}
|
|
|
|
function insertNode(n) {
|
|
var startContainer = this[START_CONTAINER],
|
|
startOffset = this[START_OFFSET], nn, o;
|
|
|
|
// Node is TEXT_NODE or CDATA
|
|
if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) {
|
|
if (!startOffset) {
|
|
// At the start of text
|
|
startContainer.parentNode.insertBefore(n, startContainer);
|
|
} else if (startOffset >= startContainer.nodeValue.length) {
|
|
// At the end of text
|
|
dom.insertAfter(n, startContainer);
|
|
} else {
|
|
// Middle, need to split
|
|
nn = startContainer.splitText(startOffset);
|
|
startContainer.parentNode.insertBefore(n, nn);
|
|
}
|
|
} else {
|
|
// Insert element node
|
|
if (startContainer.childNodes.length > 0) {
|
|
o = startContainer.childNodes[startOffset];
|
|
}
|
|
|
|
if (o) {
|
|
startContainer.insertBefore(n, o);
|
|
} else {
|
|
if (startContainer.nodeType == 3) {
|
|
dom.insertAfter(n, startContainer);
|
|
} else {
|
|
startContainer.appendChild(n);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function surroundContents(n) {
|
|
var f = self.extractContents();
|
|
|
|
self.insertNode(n);
|
|
n.appendChild(f);
|
|
self.selectNode(n);
|
|
}
|
|
|
|
function cloneRange() {
|
|
return extend(new Range(dom), {
|
|
startContainer: self[START_CONTAINER],
|
|
startOffset: self[START_OFFSET],
|
|
endContainer: self[END_CONTAINER],
|
|
endOffset: self[END_OFFSET],
|
|
collapsed: self.collapsed,
|
|
commonAncestorContainer: self.commonAncestorContainer
|
|
});
|
|
}
|
|
|
|
// Private methods
|
|
|
|
function _getSelectedNode(container, offset) {
|
|
var child;
|
|
|
|
// TEXT_NODE
|
|
if (container.nodeType == 3) {
|
|
return container;
|
|
}
|
|
|
|
if (offset < 0) {
|
|
return container;
|
|
}
|
|
|
|
child = container.firstChild;
|
|
while (child && offset > 0) {
|
|
--offset;
|
|
child = child.nextSibling;
|
|
}
|
|
|
|
if (child) {
|
|
return child;
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
function _isCollapsed() {
|
|
return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]);
|
|
}
|
|
|
|
function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
|
|
var c, offsetC, n, cmnRoot, childA, childB;
|
|
|
|
// In the first case the boundary-points have the same container. A is before B
|
|
// if its offset is less than the offset of B, A is equal to B if its offset is
|
|
// equal to the offset of B, and A is after B if its offset is greater than the
|
|
// offset of B.
|
|
if (containerA == containerB) {
|
|
if (offsetA == offsetB) {
|
|
return 0; // equal
|
|
}
|
|
|
|
if (offsetA < offsetB) {
|
|
return -1; // before
|
|
}
|
|
|
|
return 1; // after
|
|
}
|
|
|
|
// In the second case a child node C of the container of A is an ancestor
|
|
// container of B. In this case, A is before B if the offset of A is less than or
|
|
// equal to the index of the child node C and A is after B otherwise.
|
|
c = containerB;
|
|
while (c && c.parentNode != containerA) {
|
|
c = c.parentNode;
|
|
}
|
|
|
|
if (c) {
|
|
offsetC = 0;
|
|
n = containerA.firstChild;
|
|
|
|
while (n != c && offsetC < offsetA) {
|
|
offsetC++;
|
|
n = n.nextSibling;
|
|
}
|
|
|
|
if (offsetA <= offsetC) {
|
|
return -1; // before
|
|
}
|
|
|
|
return 1; // after
|
|
}
|
|
|
|
// In the third case a child node C of the container of B is an ancestor container
|
|
// of A. In this case, A is before B if the index of the child node C is less than
|
|
// the offset of B and A is after B otherwise.
|
|
c = containerA;
|
|
while (c && c.parentNode != containerB) {
|
|
c = c.parentNode;
|
|
}
|
|
|
|
if (c) {
|
|
offsetC = 0;
|
|
n = containerB.firstChild;
|
|
|
|
while (n != c && offsetC < offsetB) {
|
|
offsetC++;
|
|
n = n.nextSibling;
|
|
}
|
|
|
|
if (offsetC < offsetB) {
|
|
return -1; // before
|
|
}
|
|
|
|
return 1; // after
|
|
}
|
|
|
|
// In the fourth case, none of three other cases hold: the containers of A and B
|
|
// are siblings or descendants of sibling nodes. In this case, A is before B if
|
|
// the container of A is before the container of B in a pre-order traversal of the
|
|
// Ranges' context tree and A is after B otherwise.
|
|
cmnRoot = dom.findCommonAncestor(containerA, containerB);
|
|
childA = containerA;
|
|
|
|
while (childA && childA.parentNode != cmnRoot) {
|
|
childA = childA.parentNode;
|
|
}
|
|
|
|
if (!childA) {
|
|
childA = cmnRoot;
|
|
}
|
|
|
|
childB = containerB;
|
|
while (childB && childB.parentNode != cmnRoot) {
|
|
childB = childB.parentNode;
|
|
}
|
|
|
|
if (!childB) {
|
|
childB = cmnRoot;
|
|
}
|
|
|
|
if (childA == childB) {
|
|
return 0; // equal
|
|
}
|
|
|
|
n = cmnRoot.firstChild;
|
|
while (n) {
|
|
if (n == childA) {
|
|
return -1; // before
|
|
}
|
|
|
|
if (n == childB) {
|
|
return 1; // after
|
|
}
|
|
|
|
n = n.nextSibling;
|
|
}
|
|
}
|
|
|
|
function _setEndPoint(st, n, o) {
|
|
var ec, sc;
|
|
|
|
if (st) {
|
|
self[START_CONTAINER] = n;
|
|
self[START_OFFSET] = o;
|
|
} else {
|
|
self[END_CONTAINER] = n;
|
|
self[END_OFFSET] = o;
|
|
}
|
|
|
|
// If one boundary-point of a Range is set to have a root container
|
|
// other than the current one for the Range, the Range is collapsed to
|
|
// the new position. This enforces the restriction that both boundary-
|
|
// points of a Range must have the same root container.
|
|
ec = self[END_CONTAINER];
|
|
while (ec.parentNode) {
|
|
ec = ec.parentNode;
|
|
}
|
|
|
|
sc = self[START_CONTAINER];
|
|
while (sc.parentNode) {
|
|
sc = sc.parentNode;
|
|
}
|
|
|
|
if (sc == ec) {
|
|
// The start position of a Range is guaranteed to never be after the
|
|
// end position. To enforce this restriction, if the start is set to
|
|
// be at a position after the end, the Range is collapsed to that
|
|
// position.
|
|
if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) {
|
|
self.collapse(st);
|
|
}
|
|
} else {
|
|
self.collapse(st);
|
|
}
|
|
|
|
self.collapsed = _isCollapsed();
|
|
self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]);
|
|
}
|
|
|
|
function _traverse(how) {
|
|
var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
|
|
|
|
if (self[START_CONTAINER] == self[END_CONTAINER]) {
|
|
return _traverseSameContainer(how);
|
|
}
|
|
|
|
for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
|
|
if (p == self[START_CONTAINER]) {
|
|
return _traverseCommonStartContainer(c, how);
|
|
}
|
|
|
|
++endContainerDepth;
|
|
}
|
|
|
|
for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
|
|
if (p == self[END_CONTAINER]) {
|
|
return _traverseCommonEndContainer(c, how);
|
|
}
|
|
|
|
++startContainerDepth;
|
|
}
|
|
|
|
depthDiff = startContainerDepth - endContainerDepth;
|
|
|
|
startNode = self[START_CONTAINER];
|
|
while (depthDiff > 0) {
|
|
startNode = startNode.parentNode;
|
|
depthDiff--;
|
|
}
|
|
|
|
endNode = self[END_CONTAINER];
|
|
while (depthDiff < 0) {
|
|
endNode = endNode.parentNode;
|
|
depthDiff++;
|
|
}
|
|
|
|
// ascend the ancestor hierarchy until we have a common parent.
|
|
for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) {
|
|
startNode = sp;
|
|
endNode = ep;
|
|
}
|
|
|
|
return _traverseCommonAncestors(startNode, endNode, how);
|
|
}
|
|
|
|
function _traverseSameContainer(how) {
|
|
var frag, s, sub, n, cnt, sibling, xferNode, start, len;
|
|
|
|
if (how != DELETE) {
|
|
frag = createDocumentFragment();
|
|
}
|
|
|
|
// If selection is empty, just return the fragment
|
|
if (self[START_OFFSET] == self[END_OFFSET]) {
|
|
return frag;
|
|
}
|
|
|
|
// Text node needs special case handling
|
|
if (self[START_CONTAINER].nodeType == 3) { // TEXT_NODE
|
|
// get the substring
|
|
s = self[START_CONTAINER].nodeValue;
|
|
sub = s.substring(self[START_OFFSET], self[END_OFFSET]);
|
|
|
|
// set the original text node to its new value
|
|
if (how != CLONE) {
|
|
n = self[START_CONTAINER];
|
|
start = self[START_OFFSET];
|
|
len = self[END_OFFSET] - self[START_OFFSET];
|
|
|
|
if (start === 0 && len >= n.nodeValue.length - 1) {
|
|
n.parentNode.removeChild(n);
|
|
} else {
|
|
n.deleteData(start, len);
|
|
}
|
|
|
|
// Nothing is partially selected, so collapse to start point
|
|
self.collapse(TRUE);
|
|
}
|
|
|
|
if (how == DELETE) {
|
|
return;
|
|
}
|
|
|
|
if (sub.length > 0) {
|
|
frag.appendChild(doc.createTextNode(sub));
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
// Copy nodes between the start/end offsets.
|
|
n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]);
|
|
cnt = self[END_OFFSET] - self[START_OFFSET];
|
|
|
|
while (n && cnt > 0) {
|
|
sibling = n.nextSibling;
|
|
xferNode = _traverseFullySelected(n, how);
|
|
|
|
if (frag) {
|
|
frag.appendChild(xferNode);
|
|
}
|
|
|
|
--cnt;
|
|
n = sibling;
|
|
}
|
|
|
|
// Nothing is partially selected, so collapse to start point
|
|
if (how != CLONE) {
|
|
self.collapse(TRUE);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
function _traverseCommonStartContainer(endAncestor, how) {
|
|
var frag, n, endIdx, cnt, sibling, xferNode;
|
|
|
|
if (how != DELETE) {
|
|
frag = createDocumentFragment();
|
|
}
|
|
|
|
n = _traverseRightBoundary(endAncestor, how);
|
|
|
|
if (frag) {
|
|
frag.appendChild(n);
|
|
}
|
|
|
|
endIdx = nodeIndex(endAncestor);
|
|
cnt = endIdx - self[START_OFFSET];
|
|
|
|
if (cnt <= 0) {
|
|
// Collapse to just before the endAncestor, which
|
|
// is partially selected.
|
|
if (how != CLONE) {
|
|
self.setEndBefore(endAncestor);
|
|
self.collapse(FALSE);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
n = endAncestor.previousSibling;
|
|
while (cnt > 0) {
|
|
sibling = n.previousSibling;
|
|
xferNode = _traverseFullySelected(n, how);
|
|
|
|
if (frag) {
|
|
frag.insertBefore(xferNode, frag.firstChild);
|
|
}
|
|
|
|
--cnt;
|
|
n = sibling;
|
|
}
|
|
|
|
// Collapse to just before the endAncestor, which
|
|
// is partially selected.
|
|
if (how != CLONE) {
|
|
self.setEndBefore(endAncestor);
|
|
self.collapse(FALSE);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
function _traverseCommonEndContainer(startAncestor, how) {
|
|
var frag, startIdx, n, cnt, sibling, xferNode;
|
|
|
|
if (how != DELETE) {
|
|
frag = createDocumentFragment();
|
|
}
|
|
|
|
n = _traverseLeftBoundary(startAncestor, how);
|
|
if (frag) {
|
|
frag.appendChild(n);
|
|
}
|
|
|
|
startIdx = nodeIndex(startAncestor);
|
|
++startIdx; // Because we already traversed it
|
|
|
|
cnt = self[END_OFFSET] - startIdx;
|
|
n = startAncestor.nextSibling;
|
|
while (n && cnt > 0) {
|
|
sibling = n.nextSibling;
|
|
xferNode = _traverseFullySelected(n, how);
|
|
|
|
if (frag) {
|
|
frag.appendChild(xferNode);
|
|
}
|
|
|
|
--cnt;
|
|
n = sibling;
|
|
}
|
|
|
|
if (how != CLONE) {
|
|
self.setStartAfter(startAncestor);
|
|
self.collapse(TRUE);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
function _traverseCommonAncestors(startAncestor, endAncestor, how) {
|
|
var n, frag, startOffset, endOffset, cnt, sibling, nextSibling;
|
|
|
|
if (how != DELETE) {
|
|
frag = createDocumentFragment();
|
|
}
|
|
|
|
n = _traverseLeftBoundary(startAncestor, how);
|
|
if (frag) {
|
|
frag.appendChild(n);
|
|
}
|
|
|
|
startOffset = nodeIndex(startAncestor);
|
|
endOffset = nodeIndex(endAncestor);
|
|
++startOffset;
|
|
|
|
cnt = endOffset - startOffset;
|
|
sibling = startAncestor.nextSibling;
|
|
|
|
while (cnt > 0) {
|
|
nextSibling = sibling.nextSibling;
|
|
n = _traverseFullySelected(sibling, how);
|
|
|
|
if (frag) {
|
|
frag.appendChild(n);
|
|
}
|
|
|
|
sibling = nextSibling;
|
|
--cnt;
|
|
}
|
|
|
|
n = _traverseRightBoundary(endAncestor, how);
|
|
|
|
if (frag) {
|
|
frag.appendChild(n);
|
|
}
|
|
|
|
if (how != CLONE) {
|
|
self.setStartAfter(startAncestor);
|
|
self.collapse(TRUE);
|
|
}
|
|
|
|
return frag;
|
|
}
|
|
|
|
function _traverseRightBoundary(root, how) {
|
|
var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent;
|
|
var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER];
|
|
|
|
if (next == root) {
|
|
return _traverseNode(next, isFullySelected, FALSE, how);
|
|
}
|
|
|
|
parent = next.parentNode;
|
|
clonedParent = _traverseNode(parent, FALSE, FALSE, how);
|
|
|
|
while (parent) {
|
|
while (next) {
|
|
prevSibling = next.previousSibling;
|
|
clonedChild = _traverseNode(next, isFullySelected, FALSE, how);
|
|
|
|
if (how != DELETE) {
|
|
clonedParent.insertBefore(clonedChild, clonedParent.firstChild);
|
|
}
|
|
|
|
isFullySelected = TRUE;
|
|
next = prevSibling;
|
|
}
|
|
|
|
if (parent == root) {
|
|
return clonedParent;
|
|
}
|
|
|
|
next = parent.previousSibling;
|
|
parent = parent.parentNode;
|
|
|
|
clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
|
|
|
|
if (how != DELETE) {
|
|
clonedGrandParent.appendChild(clonedParent);
|
|
}
|
|
|
|
clonedParent = clonedGrandParent;
|
|
}
|
|
}
|
|
|
|
function _traverseLeftBoundary(root, how) {
|
|
var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER];
|
|
var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent;
|
|
|
|
if (next == root) {
|
|
return _traverseNode(next, isFullySelected, TRUE, how);
|
|
}
|
|
|
|
parent = next.parentNode;
|
|
clonedParent = _traverseNode(parent, FALSE, TRUE, how);
|
|
|
|
while (parent) {
|
|
while (next) {
|
|
nextSibling = next.nextSibling;
|
|
clonedChild = _traverseNode(next, isFullySelected, TRUE, how);
|
|
|
|
if (how != DELETE) {
|
|
clonedParent.appendChild(clonedChild);
|
|
}
|
|
|
|
isFullySelected = TRUE;
|
|
next = nextSibling;
|
|
}
|
|
|
|
if (parent == root) {
|
|
return clonedParent;
|
|
}
|
|
|
|
next = parent.nextSibling;
|
|
parent = parent.parentNode;
|
|
|
|
clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how);
|
|
|
|
if (how != DELETE) {
|
|
clonedGrandParent.appendChild(clonedParent);
|
|
}
|
|
|
|
clonedParent = clonedGrandParent;
|
|
}
|
|
}
|
|
|
|
function _traverseNode(n, isFullySelected, isLeft, how) {
|
|
var txtValue, newNodeValue, oldNodeValue, offset, newNode;
|
|
|
|
if (isFullySelected) {
|
|
return _traverseFullySelected(n, how);
|
|
}
|
|
|
|
// TEXT_NODE
|
|
if (n.nodeType == 3) {
|
|
txtValue = n.nodeValue;
|
|
|
|
if (isLeft) {
|
|
offset = self[START_OFFSET];
|
|
newNodeValue = txtValue.substring(offset);
|
|
oldNodeValue = txtValue.substring(0, offset);
|
|
} else {
|
|
offset = self[END_OFFSET];
|
|
newNodeValue = txtValue.substring(0, offset);
|
|
oldNodeValue = txtValue.substring(offset);
|
|
}
|
|
|
|
if (how != CLONE) {
|
|
n.nodeValue = oldNodeValue;
|
|
}
|
|
|
|
if (how == DELETE) {
|
|
return;
|
|
}
|
|
|
|
newNode = dom.clone(n, FALSE);
|
|
newNode.nodeValue = newNodeValue;
|
|
|
|
return newNode;
|
|
}
|
|
|
|
if (how == DELETE) {
|
|
return;
|
|
}
|
|
|
|
return dom.clone(n, FALSE);
|
|
}
|
|
|
|
function _traverseFullySelected(n, how) {
|
|
if (how != DELETE) {
|
|
return how == CLONE ? dom.clone(n, TRUE) : n;
|
|
}
|
|
|
|
n.parentNode.removeChild(n);
|
|
}
|
|
|
|
function toStringIE() {
|
|
return dom.create('body', null, cloneContents()).outerText;
|
|
}
|
|
|
|
extend(self, {
|
|
// Initial states
|
|
startContainer: doc,
|
|
startOffset: 0,
|
|
endContainer: doc,
|
|
endOffset: 0,
|
|
collapsed: TRUE,
|
|
commonAncestorContainer: doc,
|
|
|
|
// Range constants
|
|
START_TO_START: 0,
|
|
START_TO_END: 1,
|
|
END_TO_END: 2,
|
|
END_TO_START: 3,
|
|
|
|
// Public methods
|
|
setStart: setStart,
|
|
setEnd: setEnd,
|
|
setStartBefore: setStartBefore,
|
|
setStartAfter: setStartAfter,
|
|
setEndBefore: setEndBefore,
|
|
setEndAfter: setEndAfter,
|
|
collapse: collapse,
|
|
selectNode: selectNode,
|
|
selectNodeContents: selectNodeContents,
|
|
compareBoundaryPoints: compareBoundaryPoints,
|
|
deleteContents: deleteContents,
|
|
extractContents: extractContents,
|
|
cloneContents: cloneContents,
|
|
insertNode: insertNode,
|
|
surroundContents: surroundContents,
|
|
cloneRange: cloneRange,
|
|
toStringIE: toStringIE
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
// Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype
|
|
Range.prototype.toString = function () {
|
|
return this.toStringIE();
|
|
};
|
|
|
|
return Range;
|
|
}
|
|
);
|
|
|
|
defineGlobal("global!Array", Array);
|
|
defineGlobal("global!Error", Error);
|
|
define(
|
|
'ephox.katamari.api.Fun',
|
|
|
|
[
|
|
'global!Array',
|
|
'global!Error'
|
|
],
|
|
|
|
function (Array, Error) {
|
|
|
|
var noop = function () { };
|
|
|
|
var compose = function (fa, fb) {
|
|
return function () {
|
|
return fa(fb.apply(null, arguments));
|
|
};
|
|
};
|
|
|
|
var constant = function (value) {
|
|
return function () {
|
|
return value;
|
|
};
|
|
};
|
|
|
|
var identity = function (x) {
|
|
return x;
|
|
};
|
|
|
|
var tripleEquals = function(a, b) {
|
|
return a === b;
|
|
};
|
|
|
|
// Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
|
|
var curry = function (f) {
|
|
// equivalent to arguments.slice(1)
|
|
// starting at 1 because 0 is the f, makes things tricky.
|
|
// Pay attention to what variable is where, and the -1 magic.
|
|
// thankfully, we have tests for this.
|
|
var args = new Array(arguments.length - 1);
|
|
for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i];
|
|
|
|
return function () {
|
|
var newArgs = new Array(arguments.length);
|
|
for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j];
|
|
|
|
var all = args.concat(newArgs);
|
|
return f.apply(null, all);
|
|
};
|
|
};
|
|
|
|
var not = function (f) {
|
|
return function () {
|
|
return !f.apply(null, arguments);
|
|
};
|
|
};
|
|
|
|
var die = function (msg) {
|
|
return function () {
|
|
throw new Error(msg);
|
|
};
|
|
};
|
|
|
|
var apply = function (f) {
|
|
return f();
|
|
};
|
|
|
|
var call = function(f) {
|
|
f();
|
|
};
|
|
|
|
var never = constant(false);
|
|
var always = constant(true);
|
|
|
|
|
|
return {
|
|
noop: noop,
|
|
compose: compose,
|
|
constant: constant,
|
|
identity: identity,
|
|
tripleEquals: tripleEquals,
|
|
curry: curry,
|
|
not: not,
|
|
die: die,
|
|
apply: apply,
|
|
call: call,
|
|
never: never,
|
|
always: always
|
|
};
|
|
}
|
|
);
|
|
|
|
defineGlobal("global!Object", Object);
|
|
define(
|
|
'ephox.katamari.api.Option',
|
|
|
|
[
|
|
'ephox.katamari.api.Fun',
|
|
'global!Object'
|
|
],
|
|
|
|
function (Fun, Object) {
|
|
|
|
var never = Fun.never;
|
|
var always = Fun.always;
|
|
|
|
/**
|
|
Option objects support the following methods:
|
|
|
|
fold :: this Option a -> ((() -> b, a -> b)) -> Option b
|
|
|
|
is :: this Option a -> a -> Boolean
|
|
|
|
isSome :: this Option a -> () -> Boolean
|
|
|
|
isNone :: this Option a -> () -> Boolean
|
|
|
|
getOr :: this Option a -> a -> a
|
|
|
|
getOrThunk :: this Option a -> (() -> a) -> a
|
|
|
|
getOrDie :: this Option a -> String -> a
|
|
|
|
or :: this Option a -> Option a -> Option a
|
|
- if some: return self
|
|
- if none: return opt
|
|
|
|
orThunk :: this Option a -> (() -> Option a) -> Option a
|
|
- Same as "or", but uses a thunk instead of a value
|
|
|
|
map :: this Option a -> (a -> b) -> Option b
|
|
- "fmap" operation on the Option Functor.
|
|
- same as 'each'
|
|
|
|
ap :: this Option a -> Option (a -> b) -> Option b
|
|
- "apply" operation on the Option Apply/Applicative.
|
|
- Equivalent to <*> in Haskell/PureScript.
|
|
|
|
each :: this Option a -> (a -> b) -> Option b
|
|
- same as 'map'
|
|
|
|
bind :: this Option a -> (a -> Option b) -> Option b
|
|
- "bind"/"flatMap" operation on the Option Bind/Monad.
|
|
- Equivalent to >>= in Haskell/PureScript; flatMap in Scala.
|
|
|
|
flatten :: {this Option (Option a))} -> () -> Option a
|
|
- "flatten"/"join" operation on the Option Monad.
|
|
|
|
exists :: this Option a -> (a -> Boolean) -> Boolean
|
|
|
|
forall :: this Option a -> (a -> Boolean) -> Boolean
|
|
|
|
filter :: this Option a -> (a -> Boolean) -> Option a
|
|
|
|
equals :: this Option a -> Option a -> Boolean
|
|
|
|
equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean
|
|
|
|
toArray :: this Option a -> () -> [a]
|
|
|
|
*/
|
|
|
|
var none = function () { return NONE; };
|
|
|
|
var NONE = (function () {
|
|
var eq = function (o) {
|
|
return o.isNone();
|
|
};
|
|
|
|
// inlined from peanut, maybe a micro-optimisation?
|
|
var call = function (thunk) { return thunk(); };
|
|
var id = function (n) { return n; };
|
|
var noop = function () { };
|
|
|
|
var me = {
|
|
fold: function (n, s) { return n(); },
|
|
is: never,
|
|
isSome: never,
|
|
isNone: always,
|
|
getOr: id,
|
|
getOrThunk: call,
|
|
getOrDie: function (msg) {
|
|
throw new Error(msg || 'error: getOrDie called on none.');
|
|
},
|
|
or: id,
|
|
orThunk: call,
|
|
map: none,
|
|
ap: none,
|
|
each: noop,
|
|
bind: none,
|
|
flatten: none,
|
|
exists: never,
|
|
forall: always,
|
|
filter: none,
|
|
equals: eq,
|
|
equals_: eq,
|
|
toArray: function () { return []; },
|
|
toString: Fun.constant("none()")
|
|
};
|
|
if (Object.freeze) Object.freeze(me);
|
|
return me;
|
|
})();
|
|
|
|
|
|
/** some :: a -> Option a */
|
|
var some = function (a) {
|
|
|
|
// inlined from peanut, maybe a micro-optimisation?
|
|
var constant_a = function () { return a; };
|
|
|
|
var self = function () {
|
|
// can't Fun.constant this one
|
|
return me;
|
|
};
|
|
|
|
var map = function (f) {
|
|
return some(f(a));
|
|
};
|
|
|
|
var bind = function (f) {
|
|
return f(a);
|
|
};
|
|
|
|
var me = {
|
|
fold: function (n, s) { return s(a); },
|
|
is: function (v) { return a === v; },
|
|
isSome: always,
|
|
isNone: never,
|
|
getOr: constant_a,
|
|
getOrThunk: constant_a,
|
|
getOrDie: constant_a,
|
|
or: self,
|
|
orThunk: self,
|
|
map: map,
|
|
ap: function (optfab) {
|
|
return optfab.fold(none, function(fab) {
|
|
return some(fab(a));
|
|
});
|
|
},
|
|
each: function (f) {
|
|
f(a);
|
|
},
|
|
bind: bind,
|
|
flatten: constant_a,
|
|
exists: bind,
|
|
forall: bind,
|
|
filter: function (f) {
|
|
return f(a) ? me : NONE;
|
|
},
|
|
equals: function (o) {
|
|
return o.is(a);
|
|
},
|
|
equals_: function (o, elementEq) {
|
|
return o.fold(
|
|
never,
|
|
function (b) { return elementEq(a, b); }
|
|
);
|
|
},
|
|
toArray: function () {
|
|
return [a];
|
|
},
|
|
toString: function () {
|
|
return 'some(' + a + ')';
|
|
}
|
|
};
|
|
return me;
|
|
};
|
|
|
|
/** from :: undefined|null|a -> Option a */
|
|
var from = function (value) {
|
|
return value === null || value === undefined ? NONE : some(value);
|
|
};
|
|
|
|
return {
|
|
some: some,
|
|
none: none,
|
|
from: from
|
|
};
|
|
}
|
|
);
|
|
|
|
defineGlobal("global!String", String);
|
|
define(
|
|
'ephox.katamari.api.Arr',
|
|
|
|
[
|
|
'ephox.katamari.api.Option',
|
|
'global!Array',
|
|
'global!Error',
|
|
'global!String'
|
|
],
|
|
|
|
function (Option, Array, Error, String) {
|
|
// Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration
|
|
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
|
|
var rawIndexOf = (function () {
|
|
var pIndexOf = Array.prototype.indexOf;
|
|
|
|
var fastIndex = function (xs, x) { return pIndexOf.call(xs, x); };
|
|
|
|
var slowIndex = function(xs, x) { return slowIndexOf(xs, x); };
|
|
|
|
return pIndexOf === undefined ? slowIndex : fastIndex;
|
|
})();
|
|
|
|
var indexOf = function (xs, x) {
|
|
// The rawIndexOf method does not wrap up in an option. This is for performance reasons.
|
|
var r = rawIndexOf(xs, x);
|
|
return r === -1 ? Option.none() : Option.some(r);
|
|
};
|
|
|
|
var contains = function (xs, x) {
|
|
return rawIndexOf(xs, x) > -1;
|
|
};
|
|
|
|
// Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool)
|
|
// but if we need that micro-optimisation we can inline it later.
|
|
var exists = function (xs, pred) {
|
|
return findIndex(xs, pred).isSome();
|
|
};
|
|
|
|
var range = function (num, f) {
|
|
var r = [];
|
|
for (var i = 0; i < num; i++) {
|
|
r.push(f(i));
|
|
}
|
|
return r;
|
|
};
|
|
|
|
// It's a total micro optimisation, but these do make some difference.
|
|
// Particularly for browsers other than Chrome.
|
|
// - length caching
|
|
// http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69
|
|
// - not using push
|
|
// http://jsperf.com/array-direct-assignment-vs-push/2
|
|
|
|
var chunk = function (array, size) {
|
|
var r = [];
|
|
for (var i = 0; i < array.length; i += size) {
|
|
var s = array.slice(i, i + size);
|
|
r.push(s);
|
|
}
|
|
return r;
|
|
};
|
|
|
|
var map = function(xs, f) {
|
|
// pre-allocating array size when it's guaranteed to be known
|
|
// http://jsperf.com/push-allocated-vs-dynamic/22
|
|
var len = xs.length;
|
|
var r = new Array(len);
|
|
for (var i = 0; i < len; i++) {
|
|
var x = xs[i];
|
|
r[i] = f(x, i, xs);
|
|
}
|
|
return r;
|
|
};
|
|
|
|
// Unwound implementing other functions in terms of each.
|
|
// The code size is roughly the same, and it should allow for better optimisation.
|
|
var each = function(xs, f) {
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
f(x, i, xs);
|
|
}
|
|
};
|
|
|
|
var eachr = function (xs, f) {
|
|
for (var i = xs.length - 1; i >= 0; i--) {
|
|
var x = xs[i];
|
|
f(x, i, xs);
|
|
}
|
|
};
|
|
|
|
var partition = function(xs, pred) {
|
|
var pass = [];
|
|
var fail = [];
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
var arr = pred(x, i, xs) ? pass : fail;
|
|
arr.push(x);
|
|
}
|
|
return { pass: pass, fail: fail };
|
|
};
|
|
|
|
var filter = function(xs, pred) {
|
|
var r = [];
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
if (pred(x, i, xs)) {
|
|
r.push(x);
|
|
}
|
|
}
|
|
return r;
|
|
};
|
|
|
|
/*
|
|
* Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f.
|
|
*
|
|
* f is a function that derives a value from an element - e.g. true or false, or a string.
|
|
* Elements are like if this function generates the same value for them (according to ===).
|
|
*
|
|
*
|
|
* Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function.
|
|
* For a good explanation, see the group function (which is a special case of groupBy)
|
|
* http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group
|
|
*/
|
|
var groupBy = function (xs, f) {
|
|
if (xs.length === 0) {
|
|
return [];
|
|
} else {
|
|
var wasType = f(xs[0]); // initial case for matching
|
|
var r = [];
|
|
var group = [];
|
|
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
var type = f(x);
|
|
if (type !== wasType) {
|
|
r.push(group);
|
|
group = [];
|
|
}
|
|
wasType = type;
|
|
group.push(x);
|
|
}
|
|
if (group.length !== 0) {
|
|
r.push(group);
|
|
}
|
|
return r;
|
|
}
|
|
};
|
|
|
|
var foldr = function (xs, f, acc) {
|
|
eachr(xs, function (x) {
|
|
acc = f(acc, x);
|
|
});
|
|
return acc;
|
|
};
|
|
|
|
var foldl = function (xs, f, acc) {
|
|
each(xs, function (x) {
|
|
acc = f(acc, x);
|
|
});
|
|
return acc;
|
|
};
|
|
|
|
var find = function (xs, pred) {
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
if (pred(x, i, xs)) {
|
|
return Option.some(x);
|
|
}
|
|
}
|
|
return Option.none();
|
|
};
|
|
|
|
var findIndex = function (xs, pred) {
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
if (pred(x, i, xs)) {
|
|
return Option.some(i);
|
|
}
|
|
}
|
|
|
|
return Option.none();
|
|
};
|
|
|
|
var slowIndexOf = function (xs, x) {
|
|
for (var i = 0, len = xs.length; i < len; ++i) {
|
|
if (xs[i] === x) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
var push = Array.prototype.push;
|
|
var flatten = function (xs) {
|
|
// Note, this is possible because push supports multiple arguments:
|
|
// http://jsperf.com/concat-push/6
|
|
// Note that in the past, concat() would silently work (very slowly) for array-like objects.
|
|
// With this change it will throw an error.
|
|
var r = [];
|
|
for (var i = 0, len = xs.length; i < len; ++i) {
|
|
// Ensure that each value is an array itself
|
|
if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
|
|
push.apply(r, xs[i]);
|
|
}
|
|
return r;
|
|
};
|
|
|
|
var bind = function (xs, f) {
|
|
var output = map(xs, f);
|
|
return flatten(output);
|
|
};
|
|
|
|
var forall = function (xs, pred) {
|
|
for (var i = 0, len = xs.length; i < len; ++i) {
|
|
var x = xs[i];
|
|
if (pred(x, i, xs) !== true) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
var equal = function (a1, a2) {
|
|
return a1.length === a2.length && forall(a1, function (x, i) {
|
|
return x === a2[i];
|
|
});
|
|
};
|
|
|
|
var slice = Array.prototype.slice;
|
|
var reverse = function (xs) {
|
|
var r = slice.call(xs, 0);
|
|
r.reverse();
|
|
return r;
|
|
};
|
|
|
|
var difference = function (a1, a2) {
|
|
return filter(a1, function (x) {
|
|
return !contains(a2, x);
|
|
});
|
|
};
|
|
|
|
var mapToObject = function(xs, f) {
|
|
var r = {};
|
|
for (var i = 0, len = xs.length; i < len; i++) {
|
|
var x = xs[i];
|
|
r[String(x)] = f(x, i);
|
|
}
|
|
return r;
|
|
};
|
|
|
|
var pure = function(x) {
|
|
return [x];
|
|
};
|
|
|
|
var sort = function (xs, comparator) {
|
|
var copy = slice.call(xs, 0);
|
|
copy.sort(comparator);
|
|
return copy;
|
|
};
|
|
|
|
return {
|
|
map: map,
|
|
each: each,
|
|
eachr: eachr,
|
|
partition: partition,
|
|
filter: filter,
|
|
groupBy: groupBy,
|
|
indexOf: indexOf,
|
|
foldr: foldr,
|
|
foldl: foldl,
|
|
find: find,
|
|
findIndex: findIndex,
|
|
flatten: flatten,
|
|
bind: bind,
|
|
forall: forall,
|
|
exists: exists,
|
|
contains: contains,
|
|
equal: equal,
|
|
reverse: reverse,
|
|
chunk: chunk,
|
|
difference: difference,
|
|
mapToObject: mapToObject,
|
|
pure: pure,
|
|
sort: sort,
|
|
range: range
|
|
};
|
|
}
|
|
);
|
|
defineGlobal("global!setTimeout", setTimeout);
|
|
define(
|
|
'ephox.katamari.api.LazyValue',
|
|
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Option',
|
|
'global!setTimeout'
|
|
],
|
|
|
|
function (Arr, Option, setTimeout) {
|
|
var nu = function (baseFn) {
|
|
var data = Option.none();
|
|
var callbacks = [];
|
|
|
|
/** map :: this LazyValue a -> (a -> b) -> LazyValue b */
|
|
var map = function (f) {
|
|
return nu(function (nCallback) {
|
|
get(function (data) {
|
|
nCallback(f(data));
|
|
});
|
|
});
|
|
};
|
|
|
|
var get = function (nCallback) {
|
|
if (isReady()) call(nCallback);
|
|
else callbacks.push(nCallback);
|
|
};
|
|
|
|
var set = function (x) {
|
|
data = Option.some(x);
|
|
run(callbacks);
|
|
callbacks = [];
|
|
};
|
|
|
|
var isReady = function () {
|
|
return data.isSome();
|
|
};
|
|
|
|
var run = function (cbs) {
|
|
Arr.each(cbs, call);
|
|
};
|
|
|
|
var call = function(cb) {
|
|
data.each(function(x) {
|
|
setTimeout(function() {
|
|
cb(x);
|
|
}, 0);
|
|
});
|
|
};
|
|
|
|
// Lazy values cache the value and kick off immediately
|
|
baseFn(set);
|
|
|
|
return {
|
|
get: get,
|
|
map: map,
|
|
isReady: isReady
|
|
};
|
|
};
|
|
|
|
var pure = function (a) {
|
|
return nu(function (callback) {
|
|
callback(a);
|
|
});
|
|
};
|
|
|
|
return {
|
|
nu: nu,
|
|
pure: pure
|
|
};
|
|
}
|
|
);
|
|
define(
|
|
'ephox.katamari.async.Bounce',
|
|
|
|
[
|
|
'global!Array',
|
|
'global!setTimeout'
|
|
],
|
|
|
|
function (Array, setTimeout) {
|
|
|
|
var bounce = function(f) {
|
|
return function() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
var me = this;
|
|
setTimeout(function() {
|
|
f.apply(me, args);
|
|
}, 0);
|
|
};
|
|
};
|
|
|
|
return {
|
|
bounce: bounce
|
|
};
|
|
}
|
|
);
|
|
|
|
define(
|
|
'ephox.katamari.api.Future',
|
|
|
|
[
|
|
'ephox.katamari.api.LazyValue',
|
|
'ephox.katamari.async.Bounce'
|
|
],
|
|
|
|
/** A future value that is evaluated on demand. The base function is re-evaluated each time 'get' is called. */
|
|
function (LazyValue, Bounce) {
|
|
var nu = function (baseFn) {
|
|
var get = function(callback) {
|
|
baseFn(Bounce.bounce(callback));
|
|
};
|
|
|
|
/** map :: this Future a -> (a -> b) -> Future b */
|
|
var map = function (fab) {
|
|
return nu(function (callback) {
|
|
get(function (a) {
|
|
var value = fab(a);
|
|
callback(value);
|
|
});
|
|
});
|
|
};
|
|
|
|
/** bind :: this Future a -> (a -> Future b) -> Future b */
|
|
var bind = function (aFutureB) {
|
|
return nu(function (callback) {
|
|
get(function (a) {
|
|
aFutureB(a).get(callback);
|
|
});
|
|
});
|
|
};
|
|
|
|
/** anonBind :: this Future a -> Future b -> Future b
|
|
* Returns a future, which evaluates the first future, ignores the result, then evaluates the second.
|
|
*/
|
|
var anonBind = function (futureB) {
|
|
return nu(function (callback) {
|
|
get(function (a) {
|
|
futureB.get(callback);
|
|
});
|
|
});
|
|
};
|
|
|
|
var toLazy = function () {
|
|
return LazyValue.nu(get);
|
|
};
|
|
|
|
return {
|
|
map: map,
|
|
bind: bind,
|
|
anonBind: anonBind,
|
|
toLazy: toLazy,
|
|
get: get
|
|
};
|
|
|
|
};
|
|
|
|
/** a -> Future a */
|
|
var pure = function (a) {
|
|
return nu(function (callback) {
|
|
callback(a);
|
|
});
|
|
};
|
|
|
|
return {
|
|
nu: nu,
|
|
pure: pure
|
|
};
|
|
}
|
|
);
|
|
|
|
define(
|
|
'ephox.katamari.async.AsyncValues',
|
|
|
|
[
|
|
'ephox.katamari.api.Arr'
|
|
],
|
|
|
|
function (Arr) {
|
|
/*
|
|
* NOTE: an `asyncValue` must have a `get` function which gets given a callback and calls
|
|
* that callback with a value once it is ready
|
|
*
|
|
* e.g
|
|
* {
|
|
* get: function (callback) { callback(10); }
|
|
* }
|
|
*/
|
|
var par = function (asyncValues, nu) {
|
|
return nu(function(callback) {
|
|
var r = [];
|
|
var count = 0;
|
|
|
|
var cb = function(i) {
|
|
return function(value) {
|
|
r[i] = value;
|
|
count++;
|
|
if (count >= asyncValues.length) {
|
|
callback(r);
|
|
}
|
|
};
|
|
};
|
|
|
|
if (asyncValues.length === 0) {
|
|
callback([]);
|
|
} else {
|
|
Arr.each(asyncValues, function(asyncValue, i) {
|
|
asyncValue.get(cb(i));
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
return {
|
|
par: par
|
|
};
|
|
}
|
|
);
|
|
define(
|
|
'ephox.katamari.api.Futures',
|
|
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Future',
|
|
'ephox.katamari.async.AsyncValues'
|
|
],
|
|
|
|
function (Arr, Future, AsyncValues) {
|
|
/** par :: [Future a] -> Future [a] */
|
|
var par = function(futures) {
|
|
return AsyncValues.par(futures, Future.nu);
|
|
};
|
|
|
|
/** mapM :: [a] -> (a -> Future b) -> Future [b] */
|
|
var mapM = function(array, fn) {
|
|
var futures = Arr.map(array, fn);
|
|
return par(futures);
|
|
};
|
|
|
|
/** Kleisli composition of two functions: a -> Future b.
|
|
* Note the order of arguments: g is invoked first, then the result passed to f.
|
|
* This is in line with f . g = \x -> f (g a)
|
|
*
|
|
* compose :: ((b -> Future c), (a -> Future b)) -> a -> Future c
|
|
*/
|
|
var compose = function (f, g) {
|
|
return function (a) {
|
|
return g(a).bind(f);
|
|
};
|
|
};
|
|
|
|
return {
|
|
par: par,
|
|
mapM: mapM,
|
|
compose: compose
|
|
};
|
|
}
|
|
);
|
|
define(
|
|
'ephox.katamari.api.Result',
|
|
|
|
[
|
|
'ephox.katamari.api.Fun',
|
|
'ephox.katamari.api.Option'
|
|
],
|
|
|
|
function (Fun, Option) {
|
|
/* The type signatures for Result
|
|
* is :: this Result a -> a -> Bool
|
|
* or :: this Result a -> Result a -> Result a
|
|
* orThunk :: this Result a -> (_ -> Result a) -> Result a
|
|
* map :: this Result a -> (a -> b) -> Result b
|
|
* each :: this Result a -> (a -> _) -> _
|
|
* bind :: this Result a -> (a -> Result b) -> Result b
|
|
* fold :: this Result a -> (_ -> b, a -> b) -> b
|
|
* exists :: this Result a -> (a -> Bool) -> Bool
|
|
* forall :: this Result a -> (a -> Bool) -> Bool
|
|
* toOption :: this Result a -> Option a
|
|
* isValue :: this Result a -> Bool
|
|
* isError :: this Result a -> Bool
|
|
* getOr :: this Result a -> a -> a
|
|
* getOrThunk :: this Result a -> (_ -> a) -> a
|
|
* getOrDie :: this Result a -> a (or throws error)
|
|
*/
|
|
|
|
var value = function (o) {
|
|
var is = function (v) {
|
|
return o === v;
|
|
};
|
|
|
|
var or = function (opt) {
|
|
return value(o);
|
|
};
|
|
|
|
var orThunk = function (f) {
|
|
return value(o);
|
|
};
|
|
|
|
var map = function (f) {
|
|
return value(f(o));
|
|
};
|
|
|
|
var each = function (f) {
|
|
f(o);
|
|
};
|
|
|
|
var bind = function (f) {
|
|
return f(o);
|
|
};
|
|
|
|
var fold = function (_, onValue) {
|
|
return onValue(o);
|
|
};
|
|
|
|
var exists = function (f) {
|
|
return f(o);
|
|
};
|
|
|
|
var forall = function (f) {
|
|
return f(o);
|
|
};
|
|
|
|
var toOption = function () {
|
|
return Option.some(o);
|
|
};
|
|
|
|
return {
|
|
is: is,
|
|
isValue: Fun.constant(true),
|
|
isError: Fun.constant(false),
|
|
getOr: Fun.constant(o),
|
|
getOrThunk: Fun.constant(o),
|
|
getOrDie: Fun.constant(o),
|
|
or: or,
|
|
orThunk: orThunk,
|
|
fold: fold,
|
|
map: map,
|
|
each: each,
|
|
bind: bind,
|
|
exists: exists,
|
|
forall: forall,
|
|
toOption: toOption
|
|
};
|
|
};
|
|
|
|
var error = function (message) {
|
|
var getOrThunk = function (f) {
|
|
return f();
|
|
};
|
|
|
|
var getOrDie = function () {
|
|
return Fun.die(message)();
|
|
};
|
|
|
|
var or = function (opt) {
|
|
return opt;
|
|
};
|
|
|
|
var orThunk = function (f) {
|
|
return f();
|
|
};
|
|
|
|
var map = function (f) {
|
|
return error(message);
|
|
};
|
|
|
|
var bind = function (f) {
|
|
return error(message);
|
|
};
|
|
|
|
var fold = function (onError, _) {
|
|
return onError(message);
|
|
};
|
|
|
|
return {
|
|
is: Fun.constant(false),
|
|
isValue: Fun.constant(false),
|
|
isError: Fun.constant(true),
|
|
getOr: Fun.identity,
|
|
getOrThunk: getOrThunk,
|
|
getOrDie: getOrDie,
|
|
or: or,
|
|
orThunk: orThunk,
|
|
fold: fold,
|
|
map: map,
|
|
each: Fun.noop,
|
|
bind: bind,
|
|
exists: Fun.constant(false),
|
|
forall: Fun.constant(true),
|
|
toOption: Option.none
|
|
};
|
|
};
|
|
|
|
return {
|
|
value: value,
|
|
error: error
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* StyleSheetLoader.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 loading of external stylesheets and fires events when these are loaded.
|
|
*
|
|
* @class tinymce.dom.StyleSheetLoader
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.StyleSheetLoader',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Fun',
|
|
'ephox.katamari.api.Future',
|
|
'ephox.katamari.api.Futures',
|
|
'ephox.katamari.api.Result',
|
|
'tinymce.core.util.Delay',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (Arr, Fun, Future, Futures, Result, Delay, Tools) {
|
|
"use strict";
|
|
|
|
return function (document, settings) {
|
|
var idCount = 0, loadedStates = {}, maxLoadTime;
|
|
|
|
settings = settings || {};
|
|
maxLoadTime = settings.maxLoadTime || 5000;
|
|
|
|
function appendToHead(node) {
|
|
document.getElementsByTagName('head')[0].appendChild(node);
|
|
}
|
|
|
|
/**
|
|
* Loads the specified css style sheet file and call the loadedCallback once it's finished loading.
|
|
*
|
|
* @method load
|
|
* @param {String} url Url to be loaded.
|
|
* @param {Function} loadedCallback Callback to be executed when loaded.
|
|
* @param {Function} errorCallback Callback to be executed when failed loading.
|
|
*/
|
|
function load(url, loadedCallback, errorCallback) {
|
|
var link, style, startTime, state;
|
|
|
|
function passed() {
|
|
var callbacks = state.passed, i = callbacks.length;
|
|
|
|
while (i--) {
|
|
callbacks[i]();
|
|
}
|
|
|
|
state.status = 2;
|
|
state.passed = [];
|
|
state.failed = [];
|
|
}
|
|
|
|
function failed() {
|
|
var callbacks = state.failed, i = callbacks.length;
|
|
|
|
while (i--) {
|
|
callbacks[i]();
|
|
}
|
|
|
|
state.status = 3;
|
|
state.passed = [];
|
|
state.failed = [];
|
|
}
|
|
|
|
// Sniffs for older WebKit versions that have the link.onload but a broken one
|
|
function isOldWebKit() {
|
|
var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/);
|
|
return !!(webKitChunks && webKitChunks[1] < 536);
|
|
}
|
|
|
|
// Calls the waitCallback until the test returns true or the timeout occurs
|
|
function wait(testCallback, waitCallback) {
|
|
if (!testCallback()) {
|
|
// Wait for timeout
|
|
if ((new Date().getTime()) - startTime < maxLoadTime) {
|
|
Delay.setTimeout(waitCallback);
|
|
} else {
|
|
failed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Workaround for WebKit that doesn't properly support the onload event for link elements
|
|
// Or WebKit that fires the onload event before the StyleSheet is added to the document
|
|
function waitForWebKitLinkLoaded() {
|
|
wait(function () {
|
|
var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner;
|
|
|
|
while (i--) {
|
|
styleSheet = styleSheets[i];
|
|
owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement;
|
|
if (owner && owner.id === link.id) {
|
|
passed();
|
|
return true;
|
|
}
|
|
}
|
|
}, waitForWebKitLinkLoaded);
|
|
}
|
|
|
|
// Workaround for older Geckos that doesn't have any onload event for StyleSheets
|
|
function waitForGeckoLinkLoaded() {
|
|
wait(function () {
|
|
try {
|
|
// Accessing the cssRules will throw an exception until the CSS file is loaded
|
|
var cssRules = style.sheet.cssRules;
|
|
passed();
|
|
return !!cssRules;
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}, waitForGeckoLinkLoaded);
|
|
}
|
|
|
|
url = Tools._addCacheSuffix(url);
|
|
|
|
if (!loadedStates[url]) {
|
|
state = {
|
|
passed: [],
|
|
failed: []
|
|
};
|
|
|
|
loadedStates[url] = state;
|
|
} else {
|
|
state = loadedStates[url];
|
|
}
|
|
|
|
if (loadedCallback) {
|
|
state.passed.push(loadedCallback);
|
|
}
|
|
|
|
if (errorCallback) {
|
|
state.failed.push(errorCallback);
|
|
}
|
|
|
|
// Is loading wait for it to pass
|
|
if (state.status == 1) {
|
|
return;
|
|
}
|
|
|
|
// Has finished loading and was success
|
|
if (state.status == 2) {
|
|
passed();
|
|
return;
|
|
}
|
|
|
|
// Has finished loading and was a failure
|
|
if (state.status == 3) {
|
|
failed();
|
|
return;
|
|
}
|
|
|
|
// Start loading
|
|
state.status = 1;
|
|
link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.type = 'text/css';
|
|
link.id = 'u' + (idCount++);
|
|
link.async = false;
|
|
link.defer = false;
|
|
startTime = new Date().getTime();
|
|
|
|
// Feature detect onload on link element and sniff older webkits since it has an broken onload event
|
|
if ("onload" in link && !isOldWebKit()) {
|
|
link.onload = waitForWebKitLinkLoaded;
|
|
link.onerror = failed;
|
|
} else {
|
|
// Sniff for old Firefox that doesn't support the onload event on link elements
|
|
// TODO: Remove this in the future when everyone uses modern browsers
|
|
if (navigator.userAgent.indexOf("Firefox") > 0) {
|
|
style = document.createElement('style');
|
|
style.textContent = '@import "' + url + '"';
|
|
waitForGeckoLinkLoaded();
|
|
appendToHead(style);
|
|
return;
|
|
}
|
|
|
|
// Use the id owner on older webkits
|
|
waitForWebKitLinkLoaded();
|
|
}
|
|
|
|
appendToHead(link);
|
|
link.href = url;
|
|
}
|
|
|
|
var loadF = function (url) {
|
|
return Future.nu(function (resolve) {
|
|
load(
|
|
url,
|
|
Fun.compose(resolve, Fun.constant(Result.value(url))),
|
|
Fun.compose(resolve, Fun.constant(Result.error(url)))
|
|
);
|
|
});
|
|
};
|
|
|
|
var unbox = function (result) {
|
|
return result.fold(Fun.identity, Fun.identity);
|
|
};
|
|
|
|
var loadAll = function (urls, success, failure) {
|
|
Futures.par(Arr.map(urls, loadF)).get(function (result) {
|
|
var parts = Arr.partition(result, function (r) {
|
|
return r.isValue();
|
|
});
|
|
|
|
if (parts.fail.length > 0) {
|
|
failure(parts.fail.map(unbox));
|
|
} else {
|
|
success(parts.pass.map(unbox));
|
|
}
|
|
});
|
|
};
|
|
|
|
return {
|
|
load: load,
|
|
loadAll: loadAll
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Schema.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
|
|
*/
|
|
|
|
/**
|
|
* Schema validator class.
|
|
*
|
|
* @class tinymce.html.Schema
|
|
* @example
|
|
* if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
|
|
* alert('span is valid child of p.');
|
|
*
|
|
* if (tinymce.activeEditor.schema.getElementRule('p'))
|
|
* alert('P is a valid element.');
|
|
*
|
|
* @class tinymce.html.Schema
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.Schema',
|
|
[
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Tools) {
|
|
var mapCache = {}, dummyObj = {};
|
|
var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;
|
|
|
|
function split(items, delim) {
|
|
items = Tools.trim(items);
|
|
return items ? items.split(delim || ' ') : [];
|
|
}
|
|
|
|
/**
|
|
* Builds a schema lookup table
|
|
*
|
|
* @private
|
|
* @param {String} type html4, html5 or html5-strict schema type.
|
|
* @return {Object} Schema lookup table.
|
|
*/
|
|
function compileSchema(type) {
|
|
var schema = {}, globalAttributes, blockContent;
|
|
var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;
|
|
|
|
function add(name, attributes, children) {
|
|
var ni, attributesOrder, element;
|
|
|
|
function arrayToMap(array, obj) {
|
|
var map = {}, i, l;
|
|
|
|
for (i = 0, l = array.length; i < l; i++) {
|
|
map[array[i]] = obj || {};
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
children = children || [];
|
|
attributes = attributes || "";
|
|
|
|
if (typeof children === "string") {
|
|
children = split(children);
|
|
}
|
|
|
|
name = split(name);
|
|
ni = name.length;
|
|
while (ni--) {
|
|
attributesOrder = split([globalAttributes, attributes].join(' '));
|
|
|
|
element = {
|
|
attributes: arrayToMap(attributesOrder),
|
|
attributesOrder: attributesOrder,
|
|
children: arrayToMap(children, dummyObj)
|
|
};
|
|
|
|
schema[name[ni]] = element;
|
|
}
|
|
}
|
|
|
|
function addAttrs(name, attributes) {
|
|
var ni, schemaItem, i, l;
|
|
|
|
name = split(name);
|
|
ni = name.length;
|
|
attributes = split(attributes);
|
|
while (ni--) {
|
|
schemaItem = schema[name[ni]];
|
|
for (i = 0, l = attributes.length; i < l; i++) {
|
|
schemaItem.attributes[attributes[i]] = {};
|
|
schemaItem.attributesOrder.push(attributes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use cached schema
|
|
if (mapCache[type]) {
|
|
return mapCache[type];
|
|
}
|
|
|
|
// Attributes present on all elements
|
|
globalAttributes = "id accesskey class dir lang style tabindex title role";
|
|
|
|
// Event attributes can be opt-in/opt-out
|
|
/*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
|
|
"ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
|
|
"onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
|
|
"onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
|
|
"onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
|
|
"onwaiting"
|
|
);*/
|
|
|
|
// Block content elements
|
|
blockContent =
|
|
"address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul";
|
|
|
|
// Phrasing content elements from the HTML5 spec (inline)
|
|
phrasingContent =
|
|
"a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
|
|
"label map noscript object q s samp script select small span strong sub sup " +
|
|
"textarea u var #text #comment"
|
|
;
|
|
|
|
// Add HTML5 items to globalAttributes, blockContent, phrasingContent
|
|
if (type != "html4") {
|
|
globalAttributes += " contenteditable contextmenu draggable dropzone " +
|
|
"hidden spellcheck translate";
|
|
blockContent += " article aside details dialog figure header footer hgroup section nav";
|
|
phrasingContent += " audio canvas command datalist mark meter output picture " +
|
|
"progress time wbr video ruby bdi keygen";
|
|
}
|
|
|
|
// Add HTML4 elements unless it's html5-strict
|
|
if (type != "html5-strict") {
|
|
globalAttributes += " xml:lang";
|
|
|
|
html4PhrasingContent = "acronym applet basefont big font strike tt";
|
|
phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');
|
|
|
|
each(split(html4PhrasingContent), function (name) {
|
|
add(name, "", phrasingContent);
|
|
});
|
|
|
|
html4BlockContent = "center dir isindex noframes";
|
|
blockContent = [blockContent, html4BlockContent].join(' ');
|
|
|
|
// Flow content elements from the HTML5 spec (block+inline)
|
|
flowContent = [blockContent, phrasingContent].join(' ');
|
|
|
|
each(split(html4BlockContent), function (name) {
|
|
add(name, "", flowContent);
|
|
});
|
|
}
|
|
|
|
// Flow content elements from the HTML5 spec (block+inline)
|
|
flowContent = flowContent || [blockContent, phrasingContent].join(" ");
|
|
|
|
// HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
|
|
// Schema items <element name>, <specific attributes>, <children ..>
|
|
add("html", "manifest", "head body");
|
|
add("head", "", "base command link meta noscript script style title");
|
|
add("title hr noscript br");
|
|
add("base", "href target");
|
|
add("link", "href rel media hreflang type sizes hreflang");
|
|
add("meta", "name http-equiv content charset");
|
|
add("style", "media type scoped");
|
|
add("script", "src async defer type charset");
|
|
add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
|
|
"onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
|
|
"onpopstate onresize onscroll onstorage onunload", flowContent);
|
|
add("address dt dd div caption", "", flowContent);
|
|
add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
|
|
add("blockquote", "cite", flowContent);
|
|
add("ol", "reversed start type", "li");
|
|
add("ul", "", "li");
|
|
add("li", "value", flowContent);
|
|
add("dl", "", "dt dd");
|
|
add("a", "href target rel media hreflang type", phrasingContent);
|
|
add("q", "cite", phrasingContent);
|
|
add("ins del", "cite datetime", flowContent);
|
|
add("img", "src sizes srcset alt usemap ismap width height");
|
|
add("iframe", "src name width height", flowContent);
|
|
add("embed", "src type width height");
|
|
add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' '));
|
|
add("param", "name value");
|
|
add("map", "name", [flowContent, "area"].join(' '));
|
|
add("area", "alt coords shape href target rel media hreflang type");
|
|
add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
|
|
add("colgroup", "span", "col");
|
|
add("col", "span");
|
|
add("tbody thead tfoot", "", "tr");
|
|
add("tr", "", "td th");
|
|
add("td", "colspan rowspan headers", flowContent);
|
|
add("th", "colspan rowspan headers scope abbr", flowContent);
|
|
add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
|
|
add("fieldset", "disabled form name", [flowContent, "legend"].join(' '));
|
|
add("label", "form for", phrasingContent);
|
|
add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
|
|
"formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
|
|
);
|
|
add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
|
|
type == "html4" ? flowContent : phrasingContent);
|
|
add("select", "disabled form multiple name required size", "option optgroup");
|
|
add("optgroup", "disabled label", "option");
|
|
add("option", "disabled label selected value");
|
|
add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
|
|
add("menu", "type label", [flowContent, "li"].join(' '));
|
|
add("noscript", "", flowContent);
|
|
|
|
// Extend with HTML5 elements
|
|
if (type != "html4") {
|
|
add("wbr");
|
|
add("ruby", "", [phrasingContent, "rt rp"].join(' '));
|
|
add("figcaption", "", flowContent);
|
|
add("mark rt rp summary bdi", "", phrasingContent);
|
|
add("canvas", "width height", flowContent);
|
|
add("video", "src crossorigin poster preload autoplay mediagroup loop " +
|
|
"muted controls width height buffered", [flowContent, "track source"].join(' '));
|
|
add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " +
|
|
"buffered volume", [flowContent, "track source"].join(' '));
|
|
add("picture", "", "img source");
|
|
add("source", "src srcset type media sizes");
|
|
add("track", "kind src srclang label default");
|
|
add("datalist", "", [phrasingContent, "option"].join(' '));
|
|
add("article section nav aside header footer", "", flowContent);
|
|
add("hgroup", "", "h1 h2 h3 h4 h5 h6");
|
|
add("figure", "", [flowContent, "figcaption"].join(' '));
|
|
add("time", "datetime", phrasingContent);
|
|
add("dialog", "open", flowContent);
|
|
add("command", "type label icon disabled checked radiogroup command");
|
|
add("output", "for form name", phrasingContent);
|
|
add("progress", "value max", phrasingContent);
|
|
add("meter", "value min max low high optimum", phrasingContent);
|
|
add("details", "open", [flowContent, "summary"].join(' '));
|
|
add("keygen", "autofocus challenge disabled form keytype name");
|
|
}
|
|
|
|
// Extend with HTML4 attributes unless it's html5-strict
|
|
if (type != "html5-strict") {
|
|
addAttrs("script", "language xml:space");
|
|
addAttrs("style", "xml:space");
|
|
addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace");
|
|
addAttrs("embed", "align name hspace vspace");
|
|
addAttrs("param", "valuetype type");
|
|
addAttrs("a", "charset name rev shape coords");
|
|
addAttrs("br", "clear");
|
|
addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
|
|
addAttrs("img", "name longdesc align border hspace vspace");
|
|
addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
|
|
addAttrs("font basefont", "size color face");
|
|
addAttrs("input", "usemap align");
|
|
addAttrs("select", "onchange");
|
|
addAttrs("textarea");
|
|
addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
|
|
addAttrs("ul", "type compact");
|
|
addAttrs("li", "type");
|
|
addAttrs("ol dl menu dir", "compact");
|
|
addAttrs("pre", "width xml:space");
|
|
addAttrs("hr", "align noshade size width");
|
|
addAttrs("isindex", "prompt");
|
|
addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
|
|
addAttrs("col", "width align char charoff valign");
|
|
addAttrs("colgroup", "width align char charoff valign");
|
|
addAttrs("thead", "align char charoff valign");
|
|
addAttrs("tr", "align char charoff valign bgcolor");
|
|
addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
|
|
addAttrs("form", "accept");
|
|
addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
|
|
addAttrs("tfoot", "align char charoff valign");
|
|
addAttrs("tbody", "align char charoff valign");
|
|
addAttrs("area", "nohref");
|
|
addAttrs("body", "background bgcolor text link vlink alink");
|
|
}
|
|
|
|
// Extend with HTML5 attributes unless it's html4
|
|
if (type != "html4") {
|
|
addAttrs("input button select textarea", "autofocus");
|
|
addAttrs("input textarea", "placeholder");
|
|
addAttrs("a", "download");
|
|
addAttrs("link script img", "crossorigin");
|
|
addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc
|
|
}
|
|
|
|
// Special: iframe, ruby, video, audio, label
|
|
|
|
// Delete children of the same name from it's parent
|
|
// For example: form can't have a child of the name form
|
|
each(split('a form meter progress dfn'), function (name) {
|
|
if (schema[name]) {
|
|
delete schema[name].children[name];
|
|
}
|
|
});
|
|
|
|
// Delete header, footer, sectioning and heading content descendants
|
|
/*each('dt th address', function(name) {
|
|
delete schema[name].children[name];
|
|
});*/
|
|
|
|
// Caption can't have tables
|
|
delete schema.caption.children.table;
|
|
|
|
// Delete scripts by default due to possible XSS
|
|
delete schema.script;
|
|
|
|
// TODO: LI:s can only have value if parent is OL
|
|
|
|
// TODO: Handle transparent elements
|
|
// a ins del canvas map
|
|
|
|
mapCache[type] = schema;
|
|
|
|
return schema;
|
|
}
|
|
|
|
function compileElementMap(value, mode) {
|
|
var styles;
|
|
|
|
if (value) {
|
|
styles = {};
|
|
|
|
if (typeof value == 'string') {
|
|
value = {
|
|
'*': value
|
|
};
|
|
}
|
|
|
|
// Convert styles into a rule list
|
|
each(value, function (value, key) {
|
|
styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/);
|
|
});
|
|
}
|
|
|
|
return styles;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new Schema instance.
|
|
*
|
|
* @constructor
|
|
* @method Schema
|
|
* @param {Object} settings Name/value settings object.
|
|
*/
|
|
return function (settings) {
|
|
var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems;
|
|
var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses;
|
|
var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap;
|
|
var customElementsMap = {}, specialElements = {};
|
|
|
|
// Creates an lookup table map object for the specified option or the default value
|
|
function createLookupTable(option, defaultValue, extendWith) {
|
|
var value = settings[option];
|
|
|
|
if (!value) {
|
|
// Get cached default map or make it if needed
|
|
value = mapCache[option];
|
|
|
|
if (!value) {
|
|
value = makeMap(defaultValue, ' ', makeMap(defaultValue.toUpperCase(), ' '));
|
|
value = extend(value, extendWith);
|
|
|
|
mapCache[option] = value;
|
|
}
|
|
} else {
|
|
// Create custom map
|
|
value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/));
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
settings = settings || {};
|
|
schemaItems = compileSchema(settings.schema);
|
|
|
|
// Allow all elements and attributes if verify_html is set to false
|
|
if (settings.verify_html === false) {
|
|
settings.valid_elements = '*[*]';
|
|
}
|
|
|
|
validStyles = compileElementMap(settings.valid_styles);
|
|
invalidStyles = compileElementMap(settings.invalid_styles, 'map');
|
|
validClasses = compileElementMap(settings.valid_classes, 'map');
|
|
|
|
// Setup map objects
|
|
whiteSpaceElementsMap = createLookupTable(
|
|
'whitespace_elements',
|
|
'pre script noscript style textarea video audio iframe object code'
|
|
);
|
|
selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
|
|
shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
|
|
'meta param embed source wbr track');
|
|
boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
|
|
'noshade nowrap readonly selected autoplay loop controls');
|
|
nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object ' +
|
|
'script pre code', shortEndedElementsMap);
|
|
moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap);
|
|
textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
|
|
'blockquote center dir fieldset header footer article section hgroup aside nav figure');
|
|
blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
|
|
'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
|
|
'datalist select optgroup figcaption', textBlockElementsMap);
|
|
textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' +
|
|
'dfn code mark q sup sub samp');
|
|
|
|
each((settings.special || 'script noscript style textarea').split(' '), function (name) {
|
|
specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
|
|
});
|
|
|
|
// Converts a wildcard expression string to a regexp for example *a will become /.*a/.
|
|
function patternToRegExp(str) {
|
|
return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
|
|
}
|
|
|
|
// Parses the specified valid_elements string and adds to the current rules
|
|
// This function is a bit hard to read since it's heavily optimized for speed
|
|
function addValidElements(validElements) {
|
|
var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
|
|
prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
|
|
elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
|
|
attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
|
|
hasPatternsRegExp = /[*?+]/;
|
|
|
|
if (validElements) {
|
|
// Split valid elements into an array with rules
|
|
validElements = split(validElements, ',');
|
|
|
|
if (elements['@']) {
|
|
globalAttributes = elements['@'].attributes;
|
|
globalAttributesOrder = elements['@'].attributesOrder;
|
|
}
|
|
|
|
// Loop all rules
|
|
for (ei = 0, el = validElements.length; ei < el; ei++) {
|
|
// Parse element rule
|
|
matches = elementRuleRegExp.exec(validElements[ei]);
|
|
if (matches) {
|
|
// Setup local names for matches
|
|
prefix = matches[1];
|
|
elementName = matches[2];
|
|
outputName = matches[3];
|
|
attrData = matches[5];
|
|
|
|
// Create new attributes and attributesOrder
|
|
attributes = {};
|
|
attributesOrder = [];
|
|
|
|
// Create the new element
|
|
element = {
|
|
attributes: attributes,
|
|
attributesOrder: attributesOrder
|
|
};
|
|
|
|
// Padd empty elements prefix
|
|
if (prefix === '#') {
|
|
element.paddEmpty = true;
|
|
}
|
|
|
|
// Remove empty elements prefix
|
|
if (prefix === '-') {
|
|
element.removeEmpty = true;
|
|
}
|
|
|
|
if (matches[4] === '!') {
|
|
element.removeEmptyAttrs = true;
|
|
}
|
|
|
|
// Copy attributes from global rule into current rule
|
|
if (globalAttributes) {
|
|
for (key in globalAttributes) {
|
|
attributes[key] = globalAttributes[key];
|
|
}
|
|
|
|
attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
|
|
}
|
|
|
|
// Attributes defined
|
|
if (attrData) {
|
|
attrData = split(attrData, '|');
|
|
for (ai = 0, al = attrData.length; ai < al; ai++) {
|
|
matches = attrRuleRegExp.exec(attrData[ai]);
|
|
if (matches) {
|
|
attr = {};
|
|
attrType = matches[1];
|
|
attrName = matches[2].replace(/::/g, ':');
|
|
prefix = matches[3];
|
|
value = matches[4];
|
|
|
|
// Required
|
|
if (attrType === '!') {
|
|
element.attributesRequired = element.attributesRequired || [];
|
|
element.attributesRequired.push(attrName);
|
|
attr.required = true;
|
|
}
|
|
|
|
// Denied from global
|
|
if (attrType === '-') {
|
|
delete attributes[attrName];
|
|
attributesOrder.splice(inArray(attributesOrder, attrName), 1);
|
|
continue;
|
|
}
|
|
|
|
// Default value
|
|
if (prefix) {
|
|
// Default value
|
|
if (prefix === '=') {
|
|
element.attributesDefault = element.attributesDefault || [];
|
|
element.attributesDefault.push({ name: attrName, value: value });
|
|
attr.defaultValue = value;
|
|
}
|
|
|
|
// Forced value
|
|
if (prefix === ':') {
|
|
element.attributesForced = element.attributesForced || [];
|
|
element.attributesForced.push({ name: attrName, value: value });
|
|
attr.forcedValue = value;
|
|
}
|
|
|
|
// Required values
|
|
if (prefix === '<') {
|
|
attr.validValues = makeMap(value, '?');
|
|
}
|
|
}
|
|
|
|
// Check for attribute patterns
|
|
if (hasPatternsRegExp.test(attrName)) {
|
|
element.attributePatterns = element.attributePatterns || [];
|
|
attr.pattern = patternToRegExp(attrName);
|
|
element.attributePatterns.push(attr);
|
|
} else {
|
|
// Add attribute to order list if it doesn't already exist
|
|
if (!attributes[attrName]) {
|
|
attributesOrder.push(attrName);
|
|
}
|
|
|
|
attributes[attrName] = attr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global rule, store away these for later usage
|
|
if (!globalAttributes && elementName == '@') {
|
|
globalAttributes = attributes;
|
|
globalAttributesOrder = attributesOrder;
|
|
}
|
|
|
|
// Handle substitute elements such as b/strong
|
|
if (outputName) {
|
|
element.outputName = elementName;
|
|
elements[outputName] = element;
|
|
}
|
|
|
|
// Add pattern or exact element
|
|
if (hasPatternsRegExp.test(elementName)) {
|
|
element.pattern = patternToRegExp(elementName);
|
|
patternElements.push(element);
|
|
} else {
|
|
elements[elementName] = element;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setValidElements(validElements) {
|
|
elements = {};
|
|
patternElements = [];
|
|
|
|
addValidElements(validElements);
|
|
|
|
each(schemaItems, function (element, name) {
|
|
children[name] = element.children;
|
|
});
|
|
}
|
|
|
|
// Adds custom non HTML elements to the schema
|
|
function addCustomElements(customElements) {
|
|
var customElementRegExp = /^(~)?(.+)$/;
|
|
|
|
if (customElements) {
|
|
// Flush cached items since we are altering the default maps
|
|
mapCache.text_block_elements = mapCache.block_elements = null;
|
|
|
|
each(split(customElements, ','), function (rule) {
|
|
var matches = customElementRegExp.exec(rule),
|
|
inline = matches[1] === '~',
|
|
cloneName = inline ? 'span' : 'div',
|
|
name = matches[2];
|
|
|
|
children[name] = children[cloneName];
|
|
customElementsMap[name] = cloneName;
|
|
|
|
// If it's not marked as inline then add it to valid block elements
|
|
if (!inline) {
|
|
blockElementsMap[name.toUpperCase()] = {};
|
|
blockElementsMap[name] = {};
|
|
}
|
|
|
|
// Add elements clone if needed
|
|
if (!elements[name]) {
|
|
var customRule = elements[cloneName];
|
|
|
|
customRule = extend({}, customRule);
|
|
delete customRule.removeEmptyAttrs;
|
|
delete customRule.removeEmpty;
|
|
|
|
elements[name] = customRule;
|
|
}
|
|
|
|
// Add custom elements at span/div positions
|
|
each(children, function (element, elmName) {
|
|
if (element[cloneName]) {
|
|
children[elmName] = element = extend({}, children[elmName]);
|
|
element[name] = element[cloneName];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Adds valid children to the schema object
|
|
function addValidChildren(validChildren) {
|
|
var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
|
|
|
|
// Invalidate the schema cache if the schema is mutated
|
|
mapCache[settings.schema] = null;
|
|
|
|
if (validChildren) {
|
|
each(split(validChildren, ','), function (rule) {
|
|
var matches = childRuleRegExp.exec(rule), parent, prefix;
|
|
|
|
if (matches) {
|
|
prefix = matches[1];
|
|
|
|
// Add/remove items from default
|
|
if (prefix) {
|
|
parent = children[matches[2]];
|
|
} else {
|
|
parent = children[matches[2]] = { '#comment': {} };
|
|
}
|
|
|
|
parent = children[matches[2]];
|
|
|
|
each(split(matches[3], '|'), function (child) {
|
|
if (prefix === '-') {
|
|
delete parent[child];
|
|
} else {
|
|
parent[child] = {};
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function getElementRule(name) {
|
|
var element = elements[name], i;
|
|
|
|
// Exact match found
|
|
if (element) {
|
|
return element;
|
|
}
|
|
|
|
// No exact match then try the patterns
|
|
i = patternElements.length;
|
|
while (i--) {
|
|
element = patternElements[i];
|
|
|
|
if (element.pattern.test(name)) {
|
|
return element;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!settings.valid_elements) {
|
|
// No valid elements defined then clone the elements from the schema spec
|
|
each(schemaItems, function (element, name) {
|
|
elements[name] = {
|
|
attributes: element.attributes,
|
|
attributesOrder: element.attributesOrder
|
|
};
|
|
|
|
children[name] = element.children;
|
|
});
|
|
|
|
// Switch these on HTML4
|
|
if (settings.schema != "html5") {
|
|
each(split('strong/b em/i'), function (item) {
|
|
item = split(item, '/');
|
|
elements[item[1]].outputName = item[0];
|
|
});
|
|
}
|
|
|
|
// Add default alt attribute for images, removed since alt="" is treated as presentational.
|
|
// elements.img.attributesDefault = [{name: 'alt', value: ''}];
|
|
|
|
// Remove these if they are empty by default
|
|
each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function (name) {
|
|
if (elements[name]) {
|
|
elements[name].removeEmpty = true;
|
|
}
|
|
});
|
|
|
|
// Padd these by default
|
|
each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function (name) {
|
|
elements[name].paddEmpty = true;
|
|
});
|
|
|
|
// Remove these if they have no attributes
|
|
each(split('span'), function (name) {
|
|
elements[name].removeEmptyAttrs = true;
|
|
});
|
|
|
|
// Remove these by default
|
|
// TODO: Reenable in 4.1
|
|
/*each(split('script style'), function(name) {
|
|
delete elements[name];
|
|
});*/
|
|
} else {
|
|
setValidElements(settings.valid_elements);
|
|
}
|
|
|
|
addCustomElements(settings.custom_elements);
|
|
addValidChildren(settings.valid_children);
|
|
addValidElements(settings.extended_valid_elements);
|
|
|
|
// Todo: Remove this when we fix list handling to be valid
|
|
addValidChildren('+ol[ul|ol],+ul[ul|ol]');
|
|
|
|
|
|
// Some elements are not valid by themselves - require parents
|
|
each({
|
|
dd: 'dl',
|
|
dt: 'dl',
|
|
li: 'ul ol',
|
|
td: 'tr',
|
|
th: 'tr',
|
|
tr: 'tbody thead tfoot',
|
|
tbody: 'table',
|
|
thead: 'table',
|
|
tfoot: 'table',
|
|
legend: 'fieldset',
|
|
area: 'map',
|
|
param: 'video audio object'
|
|
}, function (parents, item) {
|
|
if (elements[item]) {
|
|
elements[item].parentsRequired = split(parents);
|
|
}
|
|
});
|
|
|
|
|
|
// Delete invalid elements
|
|
if (settings.invalid_elements) {
|
|
each(explode(settings.invalid_elements), function (item) {
|
|
if (elements[item]) {
|
|
delete elements[item];
|
|
}
|
|
});
|
|
}
|
|
|
|
// If the user didn't allow span only allow internal spans
|
|
if (!getElementRule('span')) {
|
|
addValidElements('span[!data-mce-type|*]');
|
|
}
|
|
|
|
/**
|
|
* Name/value map object with valid parents and children to those parents.
|
|
*
|
|
* @example
|
|
* children = {
|
|
* div:{p:{}, h1:{}}
|
|
* };
|
|
* @field children
|
|
* @type Object
|
|
*/
|
|
self.children = children;
|
|
|
|
/**
|
|
* Name/value map object with valid styles for each element.
|
|
*
|
|
* @method getValidStyles
|
|
* @type Object
|
|
*/
|
|
self.getValidStyles = function () {
|
|
return validStyles;
|
|
};
|
|
|
|
/**
|
|
* Name/value map object with valid styles for each element.
|
|
*
|
|
* @method getInvalidStyles
|
|
* @type Object
|
|
*/
|
|
self.getInvalidStyles = function () {
|
|
return invalidStyles;
|
|
};
|
|
|
|
/**
|
|
* Name/value map object with valid classes for each element.
|
|
*
|
|
* @method getValidClasses
|
|
* @type Object
|
|
*/
|
|
self.getValidClasses = function () {
|
|
return validClasses;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with boolean attributes.
|
|
*
|
|
* @method getBoolAttrs
|
|
* @return {Object} Name/value lookup map for boolean attributes.
|
|
*/
|
|
self.getBoolAttrs = function () {
|
|
return boolAttrMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with block elements.
|
|
*
|
|
* @method getBlockElements
|
|
* @return {Object} Name/value lookup map for block elements.
|
|
*/
|
|
self.getBlockElements = function () {
|
|
return blockElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with text block elements. Such as: p,h1-h6,div,address
|
|
*
|
|
* @method getTextBlockElements
|
|
* @return {Object} Name/value lookup map for block elements.
|
|
*/
|
|
self.getTextBlockElements = function () {
|
|
return textBlockElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map of inline text format nodes for example strong/span or ins.
|
|
*
|
|
* @method getTextInlineElements
|
|
* @return {Object} Name/value lookup map for text format elements.
|
|
*/
|
|
self.getTextInlineElements = function () {
|
|
return textInlineElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with short ended elements such as BR or IMG.
|
|
*
|
|
* @method getShortEndedElements
|
|
* @return {Object} Name/value lookup map for short ended elements.
|
|
*/
|
|
self.getShortEndedElements = function () {
|
|
return shortEndedElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with self closing tags such as <li>.
|
|
*
|
|
* @method getSelfClosingElements
|
|
* @return {Object} Name/value lookup map for self closing tags elements.
|
|
*/
|
|
self.getSelfClosingElements = function () {
|
|
return selfClosingElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with elements that should be treated as contents regardless if it has text
|
|
* content in them or not such as TD, VIDEO or IMG.
|
|
*
|
|
* @method getNonEmptyElements
|
|
* @return {Object} Name/value lookup map for non empty elements.
|
|
*/
|
|
self.getNonEmptyElements = function () {
|
|
return nonEmptyElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with elements that the caret should be moved in front of after enter is
|
|
* pressed
|
|
*
|
|
* @method getMoveCaretBeforeOnEnterElements
|
|
* @return {Object} Name/value lookup map for elements to place the caret in front of.
|
|
*/
|
|
self.getMoveCaretBeforeOnEnterElements = function () {
|
|
return moveCaretBeforeOnEnterElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
|
|
*
|
|
* @method getWhiteSpaceElements
|
|
* @return {Object} Name/value lookup map for white space elements.
|
|
*/
|
|
self.getWhiteSpaceElements = function () {
|
|
return whiteSpaceElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Returns a map with special elements. These are elements that needs to be parsed
|
|
* in a special way such as script, style, textarea etc. The map object values
|
|
* are regexps used to find the end of the element.
|
|
*
|
|
* @method getSpecialElements
|
|
* @return {Object} Name/value lookup map for special elements.
|
|
*/
|
|
self.getSpecialElements = function () {
|
|
return specialElements;
|
|
};
|
|
|
|
/**
|
|
* Returns true/false if the specified element and it's child is valid or not
|
|
* according to the schema.
|
|
*
|
|
* @method isValidChild
|
|
* @param {String} name Element name to check for.
|
|
* @param {String} child Element child to verify.
|
|
* @return {Boolean} True/false if the element is a valid child of the specified parent.
|
|
*/
|
|
self.isValidChild = function (name, child) {
|
|
var parent = children[name.toLowerCase()];
|
|
|
|
return !!(parent && parent[child.toLowerCase()]);
|
|
};
|
|
|
|
/**
|
|
* Returns true/false if the specified element name and optional attribute is
|
|
* valid according to the schema.
|
|
*
|
|
* @method isValid
|
|
* @param {String} name Name of element to check.
|
|
* @param {String} attr Optional attribute name to check for.
|
|
* @return {Boolean} True/false if the element and attribute is valid.
|
|
*/
|
|
self.isValid = function (name, attr) {
|
|
var attrPatterns, i, rule = getElementRule(name);
|
|
|
|
// Check if it's a valid element
|
|
if (rule) {
|
|
if (attr) {
|
|
// Check if attribute name exists
|
|
if (rule.attributes[attr]) {
|
|
return true;
|
|
}
|
|
|
|
// Check if attribute matches a regexp pattern
|
|
attrPatterns = rule.attributePatterns;
|
|
if (attrPatterns) {
|
|
i = attrPatterns.length;
|
|
while (i--) {
|
|
if (attrPatterns[i].pattern.test(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// No match
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Returns true/false if the specified element is valid or not
|
|
* according to the schema.
|
|
*
|
|
* @method getElementRule
|
|
* @param {String} name Element name to check for.
|
|
* @return {Object} Element object or undefined if the element isn't valid.
|
|
*/
|
|
self.getElementRule = getElementRule;
|
|
|
|
/**
|
|
* Returns an map object of all custom elements.
|
|
*
|
|
* @method getCustomElements
|
|
* @return {Object} Name/value map object of all custom elements.
|
|
*/
|
|
self.getCustomElements = function () {
|
|
return customElementsMap;
|
|
};
|
|
|
|
/**
|
|
* Parses a valid elements string and adds it to the schema. The valid elements
|
|
* format is for example "element[attr=default|otherattr]".
|
|
* Existing rules will be replaced with the ones specified, so this extends the schema.
|
|
*
|
|
* @method addValidElements
|
|
* @param {String} valid_elements String in the valid elements format to be parsed.
|
|
*/
|
|
self.addValidElements = addValidElements;
|
|
|
|
/**
|
|
* Parses a valid elements string and sets it to the schema. The valid elements
|
|
* format is for example "element[attr=default|otherattr]".
|
|
* Existing rules will be replaced with the ones specified, so this extends the schema.
|
|
*
|
|
* @method setValidElements
|
|
* @param {String} valid_elements String in the valid elements format to be parsed.
|
|
*/
|
|
self.setValidElements = setValidElements;
|
|
|
|
/**
|
|
* Adds custom non HTML elements to the schema.
|
|
*
|
|
* @method addCustomElements
|
|
* @param {String} custom_elements Comma separated list of custom elements to add.
|
|
*/
|
|
self.addCustomElements = addCustomElements;
|
|
|
|
/**
|
|
* Parses a valid children string and adds them to the schema structure. The valid children
|
|
* format is for example: "element[child1|child2]".
|
|
*
|
|
* @method addValidChildren
|
|
* @param {String} valid_children Valid children elements string to parse
|
|
*/
|
|
self.addValidChildren = addValidChildren;
|
|
|
|
self.elements = elements;
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* DOMUtils.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 various DOM manipulation and retrieval functions.
|
|
*
|
|
* @class tinymce.dom.DOMUtils
|
|
* @example
|
|
* // Add a class to an element by id in the page
|
|
* tinymce.DOM.addClass('someid', 'someclass');
|
|
*
|
|
* // Add a class to an element by id inside the editor
|
|
* tinymce.activeEditor.dom.addClass('someid', 'someclass');
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.DOMUtils',
|
|
[
|
|
'tinymce.core.dom.DomQuery',
|
|
'tinymce.core.dom.EventUtils',
|
|
'tinymce.core.dom.Range',
|
|
'tinymce.core.dom.Sizzle',
|
|
'tinymce.core.dom.StyleSheetLoader',
|
|
'tinymce.core.dom.TreeWalker',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.html.Entities',
|
|
'tinymce.core.html.Schema',
|
|
'tinymce.core.html.Styles',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (DomQuery, EventUtils, Range, Sizzle, StyleSheetLoader, TreeWalker, Env, Entities, Schema, Styles, Tools) {
|
|
// Shorten names
|
|
var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim;
|
|
var isIE = Env.ie;
|
|
var simpleSelectorRe = /^([a-z0-9],?)+$/i;
|
|
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
|
|
|
|
function setupAttrHooks(domUtils, settings) {
|
|
var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook;
|
|
|
|
keepUrlHook = {
|
|
set: function ($elm, value, name) {
|
|
if (settings.url_converter) {
|
|
value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]);
|
|
}
|
|
|
|
$elm.attr('data-mce-' + name, value).attr(name, value);
|
|
},
|
|
|
|
get: function ($elm, name) {
|
|
return $elm.attr('data-mce-' + name) || $elm.attr(name);
|
|
}
|
|
};
|
|
|
|
attrHooks = {
|
|
style: {
|
|
set: function ($elm, value) {
|
|
if (value !== null && typeof value === 'object') {
|
|
$elm.css(value);
|
|
return;
|
|
}
|
|
|
|
if (keepValues) {
|
|
$elm.attr('data-mce-style', value);
|
|
}
|
|
|
|
$elm.attr('style', value);
|
|
},
|
|
|
|
get: function ($elm) {
|
|
var value = $elm.attr('data-mce-style') || $elm.attr('style');
|
|
|
|
value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
|
|
|
|
return value;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (keepValues) {
|
|
attrHooks.href = attrHooks.src = keepUrlHook;
|
|
}
|
|
|
|
return attrHooks;
|
|
}
|
|
|
|
function updateInternalStyleAttr(domUtils, $elm) {
|
|
var value = $elm.attr('style');
|
|
|
|
value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
|
|
|
|
if (!value) {
|
|
value = null;
|
|
}
|
|
|
|
$elm.attr('data-mce-style', value);
|
|
}
|
|
|
|
function nodeIndex(node, normalized) {
|
|
var idx = 0, lastNodeType, nodeType;
|
|
|
|
if (node) {
|
|
for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) {
|
|
nodeType = node.nodeType;
|
|
|
|
// Normalize text nodes
|
|
if (normalized && nodeType == 3) {
|
|
if (nodeType == lastNodeType || !node.nodeValue.length) {
|
|
continue;
|
|
}
|
|
}
|
|
idx++;
|
|
lastNodeType = nodeType;
|
|
}
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class.
|
|
*
|
|
* @constructor
|
|
* @method DOMUtils
|
|
* @param {Document} doc Document reference to bind the utility class to.
|
|
* @param {settings} settings Optional settings collection.
|
|
*/
|
|
function DOMUtils(doc, settings) {
|
|
var self = this, blockElementsMap;
|
|
|
|
self.doc = doc;
|
|
self.win = window;
|
|
self.files = {};
|
|
self.counter = 0;
|
|
self.stdMode = !isIE || doc.documentMode >= 8;
|
|
self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode;
|
|
self.styleSheetLoader = new StyleSheetLoader(doc);
|
|
self.boundEvents = [];
|
|
self.settings = settings = settings || {};
|
|
self.schema = settings.schema ? settings.schema : new Schema({});
|
|
self.styles = new Styles({
|
|
url_converter: settings.url_converter,
|
|
url_converter_scope: settings.url_converter_scope
|
|
}, settings.schema);
|
|
|
|
self.fixDoc(doc);
|
|
self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event;
|
|
self.attrHooks = setupAttrHooks(self, settings);
|
|
blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {};
|
|
self.$ = DomQuery.overrideDefaults(function () {
|
|
return {
|
|
context: doc,
|
|
element: self.getRoot()
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Returns true/false if the specified element is a block element or not.
|
|
*
|
|
* @method isBlock
|
|
* @param {Node/String} node Element/Node to check.
|
|
* @return {Boolean} True/False state if the node is a block element or not.
|
|
*/
|
|
self.isBlock = function (node) {
|
|
// Fix for #5446
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
|
|
// This function is called in module pattern style since it might be executed with the wrong this scope
|
|
var type = node.nodeType;
|
|
|
|
// If it's a node then check the type and use the nodeName
|
|
if (type) {
|
|
return !!(type === 1 && blockElementsMap[node.nodeName]);
|
|
}
|
|
|
|
return !!blockElementsMap[node];
|
|
};
|
|
}
|
|
|
|
DOMUtils.prototype = {
|
|
$$: function (elm) {
|
|
if (typeof elm == 'string') {
|
|
elm = this.get(elm);
|
|
}
|
|
|
|
return this.$(elm);
|
|
},
|
|
|
|
root: null,
|
|
|
|
fixDoc: function (doc) {
|
|
var settings = this.settings, name;
|
|
|
|
if (isIE && settings.schema) {
|
|
// Add missing HTML 4/5 elements to IE
|
|
('abbr article aside audio canvas ' +
|
|
'details figcaption figure footer ' +
|
|
'header hgroup mark menu meter nav ' +
|
|
'output progress section summary ' +
|
|
'time video').replace(/\w+/g, function (name) {
|
|
doc.createElement(name);
|
|
});
|
|
|
|
// Create all custom elements
|
|
for (name in settings.schema.getCustomElements()) {
|
|
doc.createElement(name);
|
|
}
|
|
}
|
|
},
|
|
|
|
clone: function (node, deep) {
|
|
var self = this, clone, doc;
|
|
|
|
// TODO: Add feature detection here in the future
|
|
if (!isIE || node.nodeType !== 1 || deep) {
|
|
return node.cloneNode(deep);
|
|
}
|
|
|
|
doc = self.doc;
|
|
|
|
// Make a HTML5 safe shallow copy
|
|
if (!deep) {
|
|
clone = doc.createElement(node.nodeName);
|
|
|
|
// Copy attribs
|
|
each(self.getAttribs(node), function (attr) {
|
|
self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
|
|
});
|
|
|
|
return clone;
|
|
}
|
|
|
|
return clone.firstChild;
|
|
},
|
|
|
|
/**
|
|
* Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
|
|
* go above the point of this root node.
|
|
*
|
|
* @method getRoot
|
|
* @return {Element} Root element for the utility class.
|
|
*/
|
|
getRoot: function () {
|
|
var self = this;
|
|
|
|
return self.settings.root_element || self.doc.body;
|
|
},
|
|
|
|
/**
|
|
* Returns the viewport of the window.
|
|
*
|
|
* @method getViewPort
|
|
* @param {Window} win Optional window to get viewport of.
|
|
* @return {Object} Viewport object with fields x, y, w and h.
|
|
*/
|
|
getViewPort: function (win) {
|
|
var doc, rootElm;
|
|
|
|
win = !win ? this.win : win;
|
|
doc = win.document;
|
|
rootElm = this.boxModel ? doc.documentElement : doc.body;
|
|
|
|
// Returns viewport size excluding scrollbars
|
|
return {
|
|
x: win.pageXOffset || rootElm.scrollLeft,
|
|
y: win.pageYOffset || rootElm.scrollTop,
|
|
w: win.innerWidth || rootElm.clientWidth,
|
|
h: win.innerHeight || rootElm.clientHeight
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the rectangle for a specific element.
|
|
*
|
|
* @method getRect
|
|
* @param {Element/String} elm Element object or element ID to get rectangle from.
|
|
* @return {object} Rectangle for specified element object with x, y, w, h fields.
|
|
*/
|
|
getRect: function (elm) {
|
|
var self = this, pos, size;
|
|
|
|
elm = self.get(elm);
|
|
pos = self.getPos(elm);
|
|
size = self.getSize(elm);
|
|
|
|
return {
|
|
x: pos.x, y: pos.y,
|
|
w: size.w, h: size.h
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns the size dimensions of the specified element.
|
|
*
|
|
* @method getSize
|
|
* @param {Element/String} elm Element object or element ID to get rectangle from.
|
|
* @return {object} Rectangle for specified element object with w, h fields.
|
|
*/
|
|
getSize: function (elm) {
|
|
var self = this, w, h;
|
|
|
|
elm = self.get(elm);
|
|
w = self.getStyle(elm, 'width');
|
|
h = self.getStyle(elm, 'height');
|
|
|
|
// Non pixel value, then force offset/clientWidth
|
|
if (w.indexOf('px') === -1) {
|
|
w = 0;
|
|
}
|
|
|
|
// Non pixel value, then force offset/clientWidth
|
|
if (h.indexOf('px') === -1) {
|
|
h = 0;
|
|
}
|
|
|
|
return {
|
|
w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth,
|
|
h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Returns a node by the specified selector function. This function will
|
|
* loop through all parent nodes and call the specified function for each node.
|
|
* If the function then returns true indicating that it has found what it was looking for, the loop execution will then end
|
|
* and the node it found will be returned.
|
|
*
|
|
* @method getParent
|
|
* @param {Node/String} node DOM node to search parents on or ID string.
|
|
* @param {function} selector Selection function or CSS selector to execute on each node.
|
|
* @param {Node} root Optional root element, never go beyond this point.
|
|
* @return {Node} DOM Node or null if it wasn't found.
|
|
*/
|
|
getParent: function (node, selector, root) {
|
|
return this.getParents(node, selector, root, false);
|
|
},
|
|
|
|
/**
|
|
* Returns a node list of all parents matching the specified selector function or pattern.
|
|
* If the function then returns true indicating that it has found what it was looking for and that node will be collected.
|
|
*
|
|
* @method getParents
|
|
* @param {Node/String} node DOM node to search parents on or ID string.
|
|
* @param {function} selector Selection function to execute on each node or CSS pattern.
|
|
* @param {Node} root Optional root element, never go beyond this point.
|
|
* @return {Array} Array of nodes or null if it wasn't found.
|
|
*/
|
|
getParents: function (node, selector, root, collect) {
|
|
var self = this, selectorVal, result = [];
|
|
|
|
node = self.get(node);
|
|
collect = collect === undefined;
|
|
|
|
// Default root on inline mode
|
|
root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null);
|
|
|
|
// Wrap node name as func
|
|
if (is(selector, 'string')) {
|
|
selectorVal = selector;
|
|
|
|
if (selector === '*') {
|
|
selector = function (node) {
|
|
return node.nodeType == 1;
|
|
};
|
|
} else {
|
|
selector = function (node) {
|
|
return self.is(node, selectorVal);
|
|
};
|
|
}
|
|
}
|
|
|
|
while (node) {
|
|
if (node == root || !node.nodeType || node.nodeType === 9) {
|
|
break;
|
|
}
|
|
|
|
if (!selector || selector(node)) {
|
|
if (collect) {
|
|
result.push(node);
|
|
} else {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return collect ? result : null;
|
|
},
|
|
|
|
/**
|
|
* Returns the specified element by ID or the input element if it isn't a string.
|
|
*
|
|
* @method get
|
|
* @param {String/Element} n Element id to look for or element to just pass though.
|
|
* @return {Element} Element matching the specified id or null if it wasn't found.
|
|
*/
|
|
get: function (elm) {
|
|
var name;
|
|
|
|
if (elm && this.doc && typeof elm == 'string') {
|
|
name = elm;
|
|
elm = this.doc.getElementById(elm);
|
|
|
|
// IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick
|
|
if (elm && elm.id !== name) {
|
|
return this.doc.getElementsByName(name)[1];
|
|
}
|
|
}
|
|
|
|
return elm;
|
|
},
|
|
|
|
/**
|
|
* Returns the next node that matches selector or function
|
|
*
|
|
* @method getNext
|
|
* @param {Node} node Node to find siblings from.
|
|
* @param {String/function} selector Selector CSS expression or function.
|
|
* @return {Node} Next node item matching the selector or null if it wasn't found.
|
|
*/
|
|
getNext: function (node, selector) {
|
|
return this._findSib(node, selector, 'nextSibling');
|
|
},
|
|
|
|
/**
|
|
* Returns the previous node that matches selector or function
|
|
*
|
|
* @method getPrev
|
|
* @param {Node} node Node to find siblings from.
|
|
* @param {String/function} selector Selector CSS expression or function.
|
|
* @return {Node} Previous node item matching the selector or null if it wasn't found.
|
|
*/
|
|
getPrev: function (node, selector) {
|
|
return this._findSib(node, selector, 'previousSibling');
|
|
},
|
|
|
|
// #ifndef jquery
|
|
|
|
/**
|
|
* Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test".
|
|
* This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough
|
|
* on more complex patterns.
|
|
*
|
|
* @method select
|
|
* @param {String} selector CSS level 3 pattern to select/find elements by.
|
|
* @param {Object} scope Optional root element/scope element to search in.
|
|
* @return {Array} Array with all matched elements.
|
|
* @example
|
|
* // Adds a class to all paragraphs in the currently active editor
|
|
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
|
|
*
|
|
* // Adds a class to all spans that have the test class in the currently active editor
|
|
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
|
|
*/
|
|
select: function (selector, scope) {
|
|
var self = this;
|
|
|
|
/*eslint new-cap:0 */
|
|
return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []);
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the specified element matches the specified css pattern.
|
|
*
|
|
* @method is
|
|
* @param {Node/NodeList} elm DOM node to match or an array of nodes to match.
|
|
* @param {String} selector CSS pattern to match the element against.
|
|
*/
|
|
is: function (elm, selector) {
|
|
var i;
|
|
|
|
if (!elm) {
|
|
return false;
|
|
}
|
|
|
|
// If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance
|
|
if (elm.length === undefined) {
|
|
// Simple all selector
|
|
if (selector === '*') {
|
|
return elm.nodeType == 1;
|
|
}
|
|
|
|
// Simple selector just elements
|
|
if (simpleSelectorRe.test(selector)) {
|
|
selector = selector.toLowerCase().split(/,/);
|
|
elm = elm.nodeName.toLowerCase();
|
|
|
|
for (i = selector.length - 1; i >= 0; i--) {
|
|
if (selector[i] == elm) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Is non element
|
|
if (elm.nodeType && elm.nodeType != 1) {
|
|
return false;
|
|
}
|
|
|
|
var elms = elm.nodeType ? [elm] : elm;
|
|
|
|
/*eslint new-cap:0 */
|
|
return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0;
|
|
},
|
|
|
|
// #endif
|
|
|
|
/**
|
|
* Adds the specified element to another element or elements.
|
|
*
|
|
* @method add
|
|
* @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to.
|
|
* @param {String/Element} name Name of new element to add or existing element to add.
|
|
* @param {Object} attrs Optional object collection with arguments to add to the new element(s).
|
|
* @param {String} html Optional inner HTML contents to add for each element.
|
|
* @param {Boolean} create Optional flag if the element should be created or added.
|
|
* @return {Element/Array} Element that got created, or an array of created elements if multiple input elements
|
|
* were passed in.
|
|
* @example
|
|
* // Adds a new paragraph to the end of the active editor
|
|
* tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content');
|
|
*/
|
|
add: function (parentElm, name, attrs, html, create) {
|
|
var self = this;
|
|
|
|
return this.run(parentElm, function (parentElm) {
|
|
var newElm;
|
|
|
|
newElm = is(name, 'string') ? self.doc.createElement(name) : name;
|
|
self.setAttribs(newElm, attrs);
|
|
|
|
if (html) {
|
|
if (html.nodeType) {
|
|
newElm.appendChild(html);
|
|
} else {
|
|
self.setHTML(newElm, html);
|
|
}
|
|
}
|
|
|
|
return !create ? parentElm.appendChild(newElm) : newElm;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Creates a new element.
|
|
*
|
|
* @method create
|
|
* @param {String} name Name of new element.
|
|
* @param {Object} attrs Optional object name/value collection with element attributes.
|
|
* @param {String} html Optional HTML string to set as inner HTML of the element.
|
|
* @return {Element} HTML DOM node element that got created.
|
|
* @example
|
|
* // Adds an element where the caret/selection is in the active editor
|
|
* var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content');
|
|
* tinymce.activeEditor.selection.setNode(el);
|
|
*/
|
|
create: function (name, attrs, html) {
|
|
return this.add(this.doc.createElement(name), name, attrs, html, 1);
|
|
},
|
|
|
|
/**
|
|
* Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in.
|
|
*
|
|
* @method createHTML
|
|
* @param {String} name Name of new element.
|
|
* @param {Object} attrs Optional object name/value collection with element attributes.
|
|
* @param {String} html Optional HTML string to set as inner HTML of the element.
|
|
* @return {String} String with new HTML element, for example: <a href="#">test</a>.
|
|
* @example
|
|
* // Creates a html chunk and inserts it at the current selection/caret location
|
|
* tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line'));
|
|
*/
|
|
createHTML: function (name, attrs, html) {
|
|
var outHtml = '', key;
|
|
|
|
outHtml += '<' + name;
|
|
|
|
for (key in attrs) {
|
|
if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') {
|
|
outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"';
|
|
}
|
|
}
|
|
|
|
// A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime
|
|
if (typeof html != "undefined") {
|
|
return outHtml + '>' + html + '</' + name + '>';
|
|
}
|
|
|
|
return outHtml + ' />';
|
|
},
|
|
|
|
/**
|
|
* Creates a document fragment out of the specified HTML string.
|
|
*
|
|
* @method createFragment
|
|
* @param {String} html Html string to create fragment from.
|
|
* @return {DocumentFragment} Document fragment node.
|
|
*/
|
|
createFragment: function (html) {
|
|
var frag, node, doc = this.doc, container;
|
|
|
|
container = doc.createElement("div");
|
|
frag = doc.createDocumentFragment();
|
|
|
|
if (html) {
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
while ((node = container.firstChild)) {
|
|
frag.appendChild(node);
|
|
}
|
|
|
|
return frag;
|
|
},
|
|
|
|
/**
|
|
* Removes/deletes the specified element(s) from the DOM.
|
|
*
|
|
* @method remove
|
|
* @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
|
|
* @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
|
|
* placed at the location of the removed element.
|
|
* @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
|
|
* were passed in.
|
|
* @example
|
|
* // Removes all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
|
|
*
|
|
* // Removes an element by id in the document
|
|
* tinymce.DOM.remove('mydiv');
|
|
*/
|
|
remove: function (node, keepChildren) {
|
|
node = this.$$(node);
|
|
|
|
if (keepChildren) {
|
|
node.each(function () {
|
|
var child;
|
|
|
|
while ((child = this.firstChild)) {
|
|
if (child.nodeType == 3 && child.data.length === 0) {
|
|
this.removeChild(child);
|
|
} else {
|
|
this.parentNode.insertBefore(child, this);
|
|
}
|
|
}
|
|
}).remove();
|
|
} else {
|
|
node.remove();
|
|
}
|
|
|
|
return node.length > 1 ? node.toArray() : node[0];
|
|
},
|
|
|
|
/**
|
|
* Sets the CSS style value on a HTML element. The name can be a camelcase string
|
|
* or the CSS style name like background-color.
|
|
*
|
|
* @method setStyle
|
|
* @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on.
|
|
* @param {String} name Name of the style value to set.
|
|
* @param {String} value Value to set on the style.
|
|
* @example
|
|
* // Sets a style value on all paragraphs in the currently active editor
|
|
* tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red');
|
|
*
|
|
* // Sets a style value to an element by id in the current document
|
|
* tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
|
|
*/
|
|
setStyle: function (elm, name, value) {
|
|
elm = this.$$(elm).css(name, value);
|
|
|
|
if (this.settings.update_styles) {
|
|
updateInternalStyleAttr(this, elm);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the current style or runtime/computed value of an element.
|
|
*
|
|
* @method getStyle
|
|
* @param {String/Element} elm HTML element or element id string to get style from.
|
|
* @param {String} name Style name to return.
|
|
* @param {Boolean} computed Computed style.
|
|
* @return {String} Current style or computed style value of an element.
|
|
*/
|
|
getStyle: function (elm, name, computed) {
|
|
elm = this.$$(elm);
|
|
|
|
if (computed) {
|
|
return elm.css(name);
|
|
}
|
|
|
|
// Camelcase it, if needed
|
|
name = name.replace(/-(\D)/g, function (a, b) {
|
|
return b.toUpperCase();
|
|
});
|
|
|
|
if (name == 'float') {
|
|
name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat';
|
|
}
|
|
|
|
return elm[0] && elm[0].style ? elm[0].style[name] : undefined;
|
|
},
|
|
|
|
/**
|
|
* Sets multiple styles on the specified element(s).
|
|
*
|
|
* @method setStyles
|
|
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on.
|
|
* @param {Object} styles Name/Value collection of style items to add to the element(s).
|
|
* @example
|
|
* // Sets styles on all paragraphs in the currently active editor
|
|
* tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'});
|
|
*
|
|
* // Sets styles to an element by id in the current document
|
|
* tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'});
|
|
*/
|
|
setStyles: function (elm, styles) {
|
|
elm = this.$$(elm).css(styles);
|
|
|
|
if (this.settings.update_styles) {
|
|
updateInternalStyleAttr(this, elm);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes all attributes from an element or elements.
|
|
*
|
|
* @method removeAllAttribs
|
|
* @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
|
|
*/
|
|
removeAllAttribs: function (e) {
|
|
return this.run(e, function (e) {
|
|
var i, attrs = e.attributes;
|
|
for (i = attrs.length - 1; i >= 0; i--) {
|
|
e.removeAttributeNode(attrs.item(i));
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets the specified attribute of an element or elements.
|
|
*
|
|
* @method setAttrib
|
|
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on.
|
|
* @param {String} name Name of attribute to set.
|
|
* @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove
|
|
* the attribute instead.
|
|
* @example
|
|
* // Sets class attribute on all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass');
|
|
*
|
|
* // Sets class attribute on a specific element in the current page
|
|
* tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
|
|
*/
|
|
setAttrib: function (elm, name, value) {
|
|
var self = this, originalValue, hook, settings = self.settings;
|
|
|
|
if (value === '') {
|
|
value = null;
|
|
}
|
|
|
|
elm = self.$$(elm);
|
|
originalValue = elm.attr(name);
|
|
|
|
if (!elm.length) {
|
|
return;
|
|
}
|
|
|
|
hook = self.attrHooks[name];
|
|
if (hook && hook.set) {
|
|
hook.set(elm, value, name);
|
|
} else {
|
|
elm.attr(name, value);
|
|
}
|
|
|
|
if (originalValue != value && settings.onSetAttrib) {
|
|
settings.onSetAttrib({
|
|
attrElm: elm,
|
|
attrName: name,
|
|
attrValue: value
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets two or more specified attributes of an element or elements.
|
|
*
|
|
* @method setAttribs
|
|
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on.
|
|
* @param {Object} attrs Name/Value collection of attribute items to add to the element(s).
|
|
* @example
|
|
* // Sets class and title attributes on all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'});
|
|
*
|
|
* // Sets class and title attributes on a specific element in the current page
|
|
* tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'});
|
|
*/
|
|
setAttribs: function (elm, attrs) {
|
|
var self = this;
|
|
|
|
self.$$(elm).each(function (i, node) {
|
|
each(attrs, function (value, name) {
|
|
self.setAttrib(node, name, value);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns the specified attribute by name.
|
|
*
|
|
* @method getAttrib
|
|
* @param {String/Element} elm Element string id or DOM element to get attribute from.
|
|
* @param {String} name Name of attribute to get.
|
|
* @param {String} defaultVal Optional default value to return if the attribute didn't exist.
|
|
* @return {String} Attribute value string, default value or null if the attribute wasn't found.
|
|
*/
|
|
getAttrib: function (elm, name, defaultVal) {
|
|
var self = this, hook, value;
|
|
|
|
elm = self.$$(elm);
|
|
|
|
if (elm.length) {
|
|
hook = self.attrHooks[name];
|
|
|
|
if (hook && hook.get) {
|
|
value = hook.get(elm, name);
|
|
} else {
|
|
value = elm.attr(name);
|
|
}
|
|
}
|
|
|
|
if (typeof value == 'undefined') {
|
|
value = defaultVal || '';
|
|
}
|
|
|
|
return value;
|
|
},
|
|
|
|
/**
|
|
* Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
|
|
*
|
|
* @method getPos
|
|
* @param {Element/String} elm HTML element or element id to get x, y position from.
|
|
* @param {Element} rootElm Optional root element to stop calculations at.
|
|
* @return {object} Absolute position of the specified element object with x, y fields.
|
|
*/
|
|
getPos: function (elm, rootElm) {
|
|
var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos;
|
|
|
|
elm = self.get(elm);
|
|
rootElm = rootElm || body;
|
|
|
|
if (elm) {
|
|
// Use getBoundingClientRect if it exists since it's faster than looping offset nodes
|
|
// Fallback to offsetParent calculations if the body isn't static better since it stops at the body root
|
|
if (rootElm === body && elm.getBoundingClientRect && DomQuery(body).css('position') === 'static') {
|
|
pos = elm.getBoundingClientRect();
|
|
rootElm = self.boxModel ? doc.documentElement : body;
|
|
|
|
// Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit
|
|
// Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position
|
|
x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft;
|
|
y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop;
|
|
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
offsetParent = elm;
|
|
while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
|
|
x += offsetParent.offsetLeft || 0;
|
|
y += offsetParent.offsetTop || 0;
|
|
offsetParent = offsetParent.offsetParent;
|
|
}
|
|
|
|
offsetParent = elm.parentNode;
|
|
while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) {
|
|
x -= offsetParent.scrollLeft || 0;
|
|
y -= offsetParent.scrollTop || 0;
|
|
offsetParent = offsetParent.parentNode;
|
|
}
|
|
}
|
|
|
|
return { x: x, y: y };
|
|
},
|
|
|
|
/**
|
|
* Parses the specified style value into an object collection. This parser will also
|
|
* merge and remove any redundant items that browsers might have added. It will also convert non-hex
|
|
* colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
|
|
*
|
|
* @method parseStyle
|
|
* @param {String} cssText Style value to parse, for example: border:1px solid red;.
|
|
* @return {Object} Object representation of that style, for example: {border: '1px solid red'}
|
|
*/
|
|
parseStyle: function (cssText) {
|
|
return this.styles.parse(cssText);
|
|
},
|
|
|
|
/**
|
|
* Serializes the specified style object into a string.
|
|
*
|
|
* @method serializeStyle
|
|
* @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'}
|
|
* @param {String} name Optional element name.
|
|
* @return {String} String representation of the style object, for example: border: 1px solid red.
|
|
*/
|
|
serializeStyle: function (styles, name) {
|
|
return this.styles.serialize(styles, name);
|
|
},
|
|
|
|
/**
|
|
* Adds a style element at the top of the document with the specified cssText content.
|
|
*
|
|
* @method addStyle
|
|
* @param {String} cssText CSS Text style to add to top of head of document.
|
|
*/
|
|
addStyle: function (cssText) {
|
|
var self = this, doc = self.doc, head, styleElm;
|
|
|
|
// Prevent inline from loading the same styles twice
|
|
if (self !== DOMUtils.DOM && doc === document) {
|
|
var addedStyles = DOMUtils.DOM.addedStyles;
|
|
|
|
addedStyles = addedStyles || [];
|
|
if (addedStyles[cssText]) {
|
|
return;
|
|
}
|
|
|
|
addedStyles[cssText] = true;
|
|
DOMUtils.DOM.addedStyles = addedStyles;
|
|
}
|
|
|
|
// Create style element if needed
|
|
styleElm = doc.getElementById('mceDefaultStyles');
|
|
if (!styleElm) {
|
|
styleElm = doc.createElement('style');
|
|
styleElm.id = 'mceDefaultStyles';
|
|
styleElm.type = 'text/css';
|
|
|
|
head = doc.getElementsByTagName('head')[0];
|
|
if (head.firstChild) {
|
|
head.insertBefore(styleElm, head.firstChild);
|
|
} else {
|
|
head.appendChild(styleElm);
|
|
}
|
|
}
|
|
|
|
// Append style data to old or new style element
|
|
if (styleElm.styleSheet) {
|
|
styleElm.styleSheet.cssText += cssText;
|
|
} else {
|
|
styleElm.appendChild(doc.createTextNode(cssText));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Imports/loads the specified CSS file into the document bound to the class.
|
|
*
|
|
* @method loadCSS
|
|
* @param {String} url URL to CSS file to load.
|
|
* @example
|
|
* // Loads a CSS file dynamically into the current document
|
|
* tinymce.DOM.loadCSS('somepath/some.css');
|
|
*
|
|
* // Loads a CSS file into the currently active editor instance
|
|
* tinymce.activeEditor.dom.loadCSS('somepath/some.css');
|
|
*
|
|
* // Loads a CSS file into an editor instance by id
|
|
* tinymce.get('someid').dom.loadCSS('somepath/some.css');
|
|
*
|
|
* // Loads multiple CSS files into the current document
|
|
* tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css');
|
|
*/
|
|
loadCSS: function (url) {
|
|
var self = this, doc = self.doc, head;
|
|
|
|
// Prevent inline from loading the same CSS file twice
|
|
if (self !== DOMUtils.DOM && doc === document) {
|
|
DOMUtils.DOM.loadCSS(url);
|
|
return;
|
|
}
|
|
|
|
if (!url) {
|
|
url = '';
|
|
}
|
|
|
|
head = doc.getElementsByTagName('head')[0];
|
|
|
|
each(url.split(','), function (url) {
|
|
var link;
|
|
|
|
url = Tools._addCacheSuffix(url);
|
|
|
|
if (self.files[url]) {
|
|
return;
|
|
}
|
|
|
|
self.files[url] = true;
|
|
link = self.create('link', { rel: 'stylesheet', href: url });
|
|
|
|
// IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug
|
|
// This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading
|
|
// It's ugly but it seems to work fine.
|
|
if (isIE && doc.documentMode && doc.recalc) {
|
|
link.onload = function () {
|
|
if (doc.recalc) {
|
|
doc.recalc();
|
|
}
|
|
|
|
link.onload = null;
|
|
};
|
|
}
|
|
|
|
head.appendChild(link);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Adds a class to the specified element or elements.
|
|
*
|
|
* @method addClass
|
|
* @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
|
|
* @param {String} cls Class name to add to each element.
|
|
* @return {String/Array} String with new class value or array with new class values for all elements.
|
|
* @example
|
|
* // Adds a class to all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass');
|
|
*
|
|
* // Adds a class to a specific element in the current page
|
|
* tinymce.DOM.addClass('mydiv', 'myclass');
|
|
*/
|
|
addClass: function (elm, cls) {
|
|
this.$$(elm).addClass(cls);
|
|
},
|
|
|
|
/**
|
|
* Removes a class from the specified element or elements.
|
|
*
|
|
* @method removeClass
|
|
* @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs.
|
|
* @param {String} cls Class name to remove from each element.
|
|
* @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements
|
|
* were passed in.
|
|
* @example
|
|
* // Removes a class from all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass');
|
|
*
|
|
* // Removes a class from a specific element in the current page
|
|
* tinymce.DOM.removeClass('mydiv', 'myclass');
|
|
*/
|
|
removeClass: function (elm, cls) {
|
|
this.toggleClass(elm, cls, false);
|
|
},
|
|
|
|
/**
|
|
* Returns true if the specified element has the specified class.
|
|
*
|
|
* @method hasClass
|
|
* @param {String/Element} elm HTML element or element id string to check CSS class on.
|
|
* @param {String} cls CSS class to check for.
|
|
* @return {Boolean} true/false if the specified element has the specified class.
|
|
*/
|
|
hasClass: function (elm, cls) {
|
|
return this.$$(elm).hasClass(cls);
|
|
},
|
|
|
|
/**
|
|
* Toggles the specified class on/off.
|
|
*
|
|
* @method toggleClass
|
|
* @param {Element} elm Element to toggle class on.
|
|
* @param {[type]} cls Class to toggle on/off.
|
|
* @param {[type]} state Optional state to set.
|
|
*/
|
|
toggleClass: function (elm, cls, state) {
|
|
this.$$(elm).toggleClass(cls, state).each(function () {
|
|
if (this.className === '') {
|
|
DomQuery(this).attr('class', null);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Shows the specified element(s) by ID by setting the "display" style.
|
|
*
|
|
* @method show
|
|
* @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
|
|
*/
|
|
show: function (elm) {
|
|
this.$$(elm).show();
|
|
},
|
|
|
|
/**
|
|
* Hides the specified element(s) by ID by setting the "display" style.
|
|
*
|
|
* @method hide
|
|
* @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide.
|
|
* @example
|
|
* // Hides an element by id in the document
|
|
* tinymce.DOM.hide('myid');
|
|
*/
|
|
hide: function (elm) {
|
|
this.$$(elm).hide();
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the element is hidden or not by checking the "display" style.
|
|
*
|
|
* @method isHidden
|
|
* @param {String/Element} elm Id or element to check display state on.
|
|
* @return {Boolean} true/false if the element is hidden or not.
|
|
*/
|
|
isHidden: function (elm) {
|
|
return this.$$(elm).css('display') == 'none';
|
|
},
|
|
|
|
/**
|
|
* Returns a unique id. This can be useful when generating elements on the fly.
|
|
* This method will not check if the element already exists.
|
|
*
|
|
* @method uniqueId
|
|
* @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_".
|
|
* @return {String} Unique id.
|
|
*/
|
|
uniqueId: function (prefix) {
|
|
return (!prefix ? 'mce_' : prefix) + (this.counter++);
|
|
},
|
|
|
|
/**
|
|
* Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
|
|
* URLs will get converted, hex color values fixed etc. Check processHTML for details.
|
|
*
|
|
* @method setHTML
|
|
* @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
|
|
* @param {String} html HTML content to set as inner HTML of the element.
|
|
* @example
|
|
* // Sets the inner HTML of all paragraphs in the active editor
|
|
* tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
|
|
*
|
|
* // Sets the inner HTML of an element by id in the document
|
|
* tinymce.DOM.setHTML('mydiv', 'some inner html');
|
|
*/
|
|
setHTML: function (elm, html) {
|
|
elm = this.$$(elm);
|
|
|
|
if (isIE) {
|
|
elm.each(function (i, target) {
|
|
if (target.canHaveHTML === false) {
|
|
return;
|
|
}
|
|
|
|
// Remove all child nodes, IE keeps empty text nodes in DOM
|
|
while (target.firstChild) {
|
|
target.removeChild(target.firstChild);
|
|
}
|
|
|
|
try {
|
|
// IE will remove comments from the beginning
|
|
// unless you padd the contents with something
|
|
target.innerHTML = '<br>' + 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('<div></div>').html('<br>' + 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('<div></div>').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'), '<div>some html</div>');
|
|
*
|
|
* // Sets the outer HTML of an element by id in the document
|
|
* tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
|
|
*/
|
|
setOuterHTML: function (elm, html) {
|
|
var self = this;
|
|
|
|
self.$$(elm).each(function () {
|
|
try {
|
|
// Older FF doesn't have outerHTML 3.6 is still used by some organizations
|
|
if ("outerHTML" in this) {
|
|
this.outerHTML = html;
|
|
return;
|
|
}
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
|
|
// OuterHTML for IE it sometimes produces an "unknown runtime error"
|
|
self.remove(DomQuery(this).html(html), true);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Entity decodes a string. This method decodes any HTML entities, such as å.
|
|
*
|
|
* @method decode
|
|
* @param {String} s String to decode entities on.
|
|
* @return {String} Entity decoded string.
|
|
*/
|
|
decode: Entities.decode,
|
|
|
|
/**
|
|
* Entity encodes a string. This method encodes the most common entities, such as <>"&.
|
|
*
|
|
* @method encode
|
|
* @param {String} text String to encode with entities.
|
|
* @return {String} Entity encoded string.
|
|
*/
|
|
encode: Entities.encodeAllRaw,
|
|
|
|
/**
|
|
* Inserts an element after the reference element.
|
|
*
|
|
* @method insertAfter
|
|
* @param {Element} node Element to insert after the reference.
|
|
* @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after.
|
|
* @return {Element/Array} Element that got added or an array with elements.
|
|
*/
|
|
insertAfter: function (node, referenceNode) {
|
|
referenceNode = this.get(referenceNode);
|
|
|
|
return this.run(node, function (node) {
|
|
var parent, nextSibling;
|
|
|
|
parent = referenceNode.parentNode;
|
|
nextSibling = referenceNode.nextSibling;
|
|
|
|
if (nextSibling) {
|
|
parent.insertBefore(node, nextSibling);
|
|
} else {
|
|
parent.appendChild(node);
|
|
}
|
|
|
|
return node;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Replaces the specified element or elements with the new element specified. The new element will
|
|
* be cloned if multiple input elements are passed in.
|
|
*
|
|
* @method replace
|
|
* @param {Element} newElm New element to replace old ones with.
|
|
* @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace.
|
|
* @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added
|
|
* to new ones.
|
|
*/
|
|
replace: function (newElm, oldElm, keepChildren) {
|
|
var self = this;
|
|
|
|
return self.run(oldElm, function (oldElm) {
|
|
if (is(oldElm, 'array')) {
|
|
newElm = newElm.cloneNode(true);
|
|
}
|
|
|
|
if (keepChildren) {
|
|
each(grep(oldElm.childNodes), function (node) {
|
|
newElm.appendChild(node);
|
|
});
|
|
}
|
|
|
|
return oldElm.parentNode.replaceChild(newElm, oldElm);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renames the specified element and keeps its attributes and children.
|
|
*
|
|
* @method rename
|
|
* @param {Element} elm Element to rename.
|
|
* @param {String} name Name of the new element.
|
|
* @return {Element} New element or the old element if it needed renaming.
|
|
*/
|
|
rename: function (elm, name) {
|
|
var self = this, newElm;
|
|
|
|
if (elm.nodeName != name.toUpperCase()) {
|
|
// Rename block element
|
|
newElm = self.create(name);
|
|
|
|
// Copy attribs to new block
|
|
each(self.getAttribs(elm), function (attrNode) {
|
|
self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName));
|
|
});
|
|
|
|
// Replace block
|
|
self.replace(newElm, elm, 1);
|
|
}
|
|
|
|
return newElm || elm;
|
|
},
|
|
|
|
/**
|
|
* Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic.
|
|
*
|
|
* @method findCommonAncestor
|
|
* @param {Element} a Element to find common ancestor of.
|
|
* @param {Element} b Element to find common ancestor of.
|
|
* @return {Element} Common ancestor element of the two input elements.
|
|
*/
|
|
findCommonAncestor: function (a, b) {
|
|
var ps = a, pe;
|
|
|
|
while (ps) {
|
|
pe = b;
|
|
|
|
while (pe && ps != pe) {
|
|
pe = pe.parentNode;
|
|
}
|
|
|
|
if (ps == pe) {
|
|
break;
|
|
}
|
|
|
|
ps = ps.parentNode;
|
|
}
|
|
|
|
if (!ps && a.ownerDocument) {
|
|
return a.ownerDocument.documentElement;
|
|
}
|
|
|
|
return ps;
|
|
},
|
|
|
|
/**
|
|
* Parses the specified RGB color value and returns a hex version of that color.
|
|
*
|
|
* @method toHex
|
|
* @param {String} rgbVal RGB string value like rgb(1,2,3)
|
|
* @return {String} Hex version of that RGB value like #FF00FF.
|
|
*/
|
|
toHex: function (rgbVal) {
|
|
return this.styles.toHex(Tools.trim(rgbVal));
|
|
},
|
|
|
|
/**
|
|
* Executes the specified function on the element by id or dom element node or array of elements/id.
|
|
*
|
|
* @method run
|
|
* @param {String/Element/Array} elm ID or DOM element object or array with ids or elements.
|
|
* @param {function} func Function to execute for each item.
|
|
* @param {Object} scope Optional scope to execute the function in.
|
|
* @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in.
|
|
*/
|
|
run: function (elm, func, scope) {
|
|
var self = this, result;
|
|
|
|
if (typeof elm === 'string') {
|
|
elm = self.get(elm);
|
|
}
|
|
|
|
if (!elm) {
|
|
return false;
|
|
}
|
|
|
|
scope = scope || this;
|
|
if (!elm.nodeType && (elm.length || elm.length === 0)) {
|
|
result = [];
|
|
|
|
each(elm, function (elm, i) {
|
|
if (elm) {
|
|
if (typeof elm == 'string') {
|
|
elm = self.get(elm);
|
|
}
|
|
|
|
result.push(func.call(scope, elm, i));
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
return func.call(scope, elm);
|
|
},
|
|
|
|
/**
|
|
* Returns a NodeList with attributes for the element.
|
|
*
|
|
* @method getAttribs
|
|
* @param {HTMLElement/string} elm Element node or string id to get attributes from.
|
|
* @return {NodeList} NodeList with attributes.
|
|
*/
|
|
getAttribs: function (elm) {
|
|
var attrs;
|
|
|
|
elm = this.get(elm);
|
|
|
|
if (!elm) {
|
|
return [];
|
|
}
|
|
|
|
if (isIE) {
|
|
attrs = [];
|
|
|
|
// Object will throw exception in IE
|
|
if (elm.nodeName == 'OBJECT') {
|
|
return elm.attributes;
|
|
}
|
|
|
|
// IE doesn't keep the selected attribute if you clone option elements
|
|
if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) {
|
|
attrs.push({ specified: 1, nodeName: 'selected' });
|
|
}
|
|
|
|
// It's crazy that this is faster in IE but it's because it returns all attributes all the time
|
|
var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi;
|
|
elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function (a) {
|
|
attrs.push({ specified: 1, nodeName: a });
|
|
});
|
|
|
|
return attrs;
|
|
}
|
|
|
|
return elm.attributes;
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the specified node is to be considered empty or not.
|
|
*
|
|
* @example
|
|
* tinymce.DOM.isEmpty(node, {img: true});
|
|
* @method isEmpty
|
|
* @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements.
|
|
* @return {Boolean} true/false if the node is empty or not.
|
|
*/
|
|
isEmpty: function (node, elements) {
|
|
var self = this, i, attributes, type, whitespace, walker, name, brCount = 0;
|
|
|
|
node = node.firstChild;
|
|
if (node) {
|
|
walker = new TreeWalker(node, node.parentNode);
|
|
elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null);
|
|
whitespace = self.schema ? self.schema.getWhiteSpaceElements() : {};
|
|
|
|
do {
|
|
type = node.nodeType;
|
|
|
|
if (type === 1) {
|
|
// Ignore bogus elements
|
|
var bogusVal = node.getAttribute('data-mce-bogus');
|
|
if (bogusVal) {
|
|
node = walker.next(bogusVal === 'all');
|
|
continue;
|
|
}
|
|
|
|
// Keep empty elements like <img />
|
|
name = node.nodeName.toLowerCase();
|
|
if (elements && elements[name]) {
|
|
// Ignore single BR elements in blocks like <p><br /></p> or <p><span><br /></span></p>
|
|
if (name === 'br') {
|
|
brCount++;
|
|
node = walker.next();
|
|
continue;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Keep elements with data-bookmark attributes or name attribute like <a name="1"></a>
|
|
attributes = self.getAttribs(node);
|
|
i = attributes.length;
|
|
while (i--) {
|
|
name = attributes[i].nodeName;
|
|
if (name === "name" || name === 'data-mce-bookmark') {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep comment nodes
|
|
if (type == 8) {
|
|
return false;
|
|
}
|
|
|
|
// Keep non whitespace text nodes
|
|
if (type === 3 && !whiteSpaceRegExp.test(node.nodeValue)) {
|
|
return false;
|
|
}
|
|
|
|
// Keep whitespace preserve elements
|
|
if (type === 3 && node.parentNode && whitespace[node.parentNode.nodeName] && whiteSpaceRegExp.test(node.nodeValue)) {
|
|
return false;
|
|
}
|
|
|
|
node = walker.next();
|
|
} while (node);
|
|
}
|
|
|
|
return brCount <= 1;
|
|
},
|
|
|
|
/**
|
|
* Creates a new DOM Range object. This will use the native DOM Range API if it's
|
|
* available. If it's not, it will fall back to the custom TinyMCE implementation.
|
|
*
|
|
* @method createRng
|
|
* @return {DOMRange} DOM Range object.
|
|
* @example
|
|
* var rng = tinymce.DOM.createRng();
|
|
* alert(rng.startContainer + "," + rng.startOffset);
|
|
*/
|
|
createRng: function () {
|
|
var doc = this.doc;
|
|
|
|
return doc.createRange ? doc.createRange() : new Range(this);
|
|
},
|
|
|
|
/**
|
|
* Returns the index of the specified node within its parent.
|
|
*
|
|
* @method nodeIndex
|
|
* @param {Node} node Node to look for.
|
|
* @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization.
|
|
* @return {Number} Index of the specified node.
|
|
*/
|
|
nodeIndex: nodeIndex,
|
|
|
|
/**
|
|
* Splits an element into two new elements and places the specified split
|
|
* element or elements between the new ones. For example splitting the paragraph at the bold element in
|
|
* this example <p>abc<b>abc</b>123</p> would produce <p>abc</p><b>abc</b><p>123</p>.
|
|
*
|
|
* @method split
|
|
* @param {Element} parentElm Parent element to split.
|
|
* @param {Element} splitElm Element to split at.
|
|
* @param {Element} replacementElm Optional replacement element to replace the split element with.
|
|
* @return {Element} Returns the split element or the replacement element if that is specified.
|
|
*/
|
|
split: function (parentElm, splitElm, replacementElm) {
|
|
var self = this, r = self.createRng(), bef, aft, pa;
|
|
|
|
// W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense
|
|
// but we don't want that in our code since it serves no purpose for the end user
|
|
// For example splitting this html at the bold element:
|
|
// <p>text 1<span><b>CHOP</b></span>text 2</p>
|
|
// would produce:
|
|
// <p>text 1<span></span></p><b>CHOP</b><p><span></span>text 2</p>
|
|
// this function will then trim off empty edges and produce:
|
|
// <p>text 1</p><b>CHOP</b><p>text 2</p>
|
|
function trimNode(node) {
|
|
var i, children = node.childNodes, type = node.nodeType;
|
|
|
|
function surroundedBySpans(node) {
|
|
var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN';
|
|
var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN';
|
|
return previousIsSpan && nextIsSpan;
|
|
}
|
|
|
|
if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') {
|
|
return;
|
|
}
|
|
|
|
for (i = children.length - 1; i >= 0; i--) {
|
|
trimNode(children[i]);
|
|
}
|
|
|
|
if (type != 9) {
|
|
// Keep non whitespace text nodes
|
|
if (type == 3 && node.nodeValue.length > 0) {
|
|
// If parent element isn't a block or there isn't any useful contents for example "<p> </p>"
|
|
// Also keep text nodes with only spaces if surrounded by spans.
|
|
// eg. "<p><span>a</span> <span>b</span></p>" should keep space between a and b
|
|
var trimmedLength = trim(node.nodeValue).length;
|
|
if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) {
|
|
return;
|
|
}
|
|
} else if (type == 1) {
|
|
// If the only child is a bookmark then move it up
|
|
children = node.childNodes;
|
|
|
|
// TODO fix this complex if
|
|
if (children.length == 1 && children[0] && children[0].nodeType == 1 &&
|
|
children[0].getAttribute('data-mce-type') == 'bookmark') {
|
|
node.parentNode.insertBefore(children[0], node);
|
|
}
|
|
|
|
// Keep non empty elements or img, hr etc
|
|
if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.remove(node);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
if (parentElm && splitElm) {
|
|
// Get before chunk
|
|
r.setStart(parentElm.parentNode, self.nodeIndex(parentElm));
|
|
r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm));
|
|
bef = r.extractContents();
|
|
|
|
// Get after chunk
|
|
r = self.createRng();
|
|
r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1);
|
|
r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1);
|
|
aft = r.extractContents();
|
|
|
|
// Insert before chunk
|
|
pa = parentElm.parentNode;
|
|
pa.insertBefore(trimNode(bef), parentElm);
|
|
|
|
// Insert middle chunk
|
|
if (replacementElm) {
|
|
pa.insertBefore(replacementElm, parentElm);
|
|
//pa.replaceChild(replacementElm, splitElm);
|
|
} else {
|
|
pa.insertBefore(splitElm, parentElm);
|
|
}
|
|
|
|
// Insert after chunk
|
|
pa.insertBefore(trimNode(aft), parentElm);
|
|
self.remove(parentElm);
|
|
|
|
return replacementElm || splitElm;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds an event handler to the specified object.
|
|
*
|
|
* @method bind
|
|
* @param {Element/Document/Window/Array} target Target element to bind events to.
|
|
* handler to or an array of elements/ids/documents.
|
|
* @param {String} name Name of event handler to add, for example: click.
|
|
* @param {function} func Function to execute when the event occurs.
|
|
* @param {Object} scope Optional scope to execute the function in.
|
|
* @return {function} Function callback handler the same as the one passed in.
|
|
*/
|
|
bind: function (target, name, func, scope) {
|
|
var self = this;
|
|
|
|
if (Tools.isArray(target)) {
|
|
var i = target.length;
|
|
|
|
while (i--) {
|
|
target[i] = self.bind(target[i], name, func, scope);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
// Collect all window/document events bound by editor instance
|
|
if (self.settings.collect && (target === self.doc || target === self.win)) {
|
|
self.boundEvents.push([target, name, func, scope]);
|
|
}
|
|
|
|
return self.events.bind(target, name, func, scope || self);
|
|
},
|
|
|
|
/**
|
|
* Removes the specified event handler by name and function from an element or collection of elements.
|
|
*
|
|
* @method unbind
|
|
* @param {Element/Document/Window/Array} target Target element to unbind events on.
|
|
* @param {String} name Event handler name, for example: "click"
|
|
* @param {function} func Function to remove.
|
|
* @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements
|
|
* were passed in.
|
|
*/
|
|
unbind: function (target, name, func) {
|
|
var self = this, i;
|
|
|
|
if (Tools.isArray(target)) {
|
|
i = target.length;
|
|
|
|
while (i--) {
|
|
target[i] = self.unbind(target[i], name, func);
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
// Remove any bound events matching the input
|
|
if (self.boundEvents && (target === self.doc || target === self.win)) {
|
|
i = self.boundEvents.length;
|
|
|
|
while (i--) {
|
|
var item = self.boundEvents[i];
|
|
|
|
if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) {
|
|
this.events.unbind(item[0], item[1], item[2]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.events.unbind(target, name, func);
|
|
},
|
|
|
|
/**
|
|
* Fires the specified event name with object on target.
|
|
*
|
|
* @method fire
|
|
* @param {Node/Document/Window} target Target element or object to fire event on.
|
|
* @param {String} name Name of the event to fire.
|
|
* @param {Object} evt Event object to send.
|
|
* @return {Event} Event object.
|
|
*/
|
|
fire: function (target, name, evt) {
|
|
return this.events.fire(target, name, evt);
|
|
},
|
|
|
|
// Returns the content editable state of a node
|
|
getContentEditable: function (node) {
|
|
var contentEditable;
|
|
|
|
// Check type
|
|
if (!node || node.nodeType != 1) {
|
|
return null;
|
|
}
|
|
|
|
// Check for fake content editable
|
|
contentEditable = node.getAttribute("data-mce-contenteditable");
|
|
if (contentEditable && contentEditable !== "inherit") {
|
|
return contentEditable;
|
|
}
|
|
|
|
// Check for real content editable
|
|
return node.contentEditable !== "inherit" ? node.contentEditable : null;
|
|
},
|
|
|
|
getContentEditableParent: function (node) {
|
|
var root = this.getRoot(), state = null;
|
|
|
|
for (; node && node !== root; node = node.parentNode) {
|
|
state = this.getContentEditable(node);
|
|
|
|
if (state !== null) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
},
|
|
|
|
/**
|
|
* Destroys all internal references to the DOM to solve IE leak issues.
|
|
*
|
|
* @method destroy
|
|
*/
|
|
destroy: function () {
|
|
var self = this;
|
|
|
|
// Unbind all events bound to window/document by editor instance
|
|
if (self.boundEvents) {
|
|
var i = self.boundEvents.length;
|
|
|
|
while (i--) {
|
|
var item = self.boundEvents[i];
|
|
this.events.unbind(item[0], item[1], item[2]);
|
|
}
|
|
|
|
self.boundEvents = null;
|
|
}
|
|
|
|
// Restore sizzle document to window.document
|
|
// Since the current document might be removed producing "Permission denied" on IE see #6325
|
|
if (Sizzle.setDocument) {
|
|
Sizzle.setDocument();
|
|
}
|
|
|
|
self.win = self.doc = self.root = self.events = self.frag = null;
|
|
},
|
|
|
|
isChildOf: function (node, parent) {
|
|
while (node) {
|
|
if (parent === node) {
|
|
return true;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
// #ifdef debug
|
|
|
|
dumpRng: function (r) {
|
|
return (
|
|
'startContainer: ' + r.startContainer.nodeName +
|
|
', startOffset: ' + r.startOffset +
|
|
', endContainer: ' + r.endContainer.nodeName +
|
|
', endOffset: ' + r.endOffset
|
|
);
|
|
},
|
|
|
|
// #endif
|
|
|
|
_findSib: function (node, selector, name) {
|
|
var self = this, func = selector;
|
|
|
|
if (node) {
|
|
// If expression make a function of it using is
|
|
if (typeof func == 'string') {
|
|
func = function (node) {
|
|
return self.is(node, selector);
|
|
};
|
|
}
|
|
|
|
// Loop all siblings
|
|
for (node = node[name]; node; node = node[name]) {
|
|
if (func(node)) {
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Instance of DOMUtils for the current document.
|
|
*
|
|
* @static
|
|
* @property DOM
|
|
* @type tinymce.dom.DOMUtils
|
|
* @example
|
|
* // Example of how to add a class to some element by id
|
|
* tinymce.DOM.addClass('someid', 'someclass');
|
|
*/
|
|
DOMUtils.DOM = new DOMUtils(document);
|
|
DOMUtils.nodeIndex = nodeIndex;
|
|
|
|
return DOMUtils;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ScriptLoader.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
|
|
*/
|
|
|
|
/*globals console*/
|
|
|
|
/**
|
|
* This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks
|
|
* when various items gets loaded. This class is useful to load external JavaScript files.
|
|
*
|
|
* @class tinymce.dom.ScriptLoader
|
|
* @example
|
|
* // Load a script from a specific URL using the global script loader
|
|
* tinymce.ScriptLoader.load('somescript.js');
|
|
*
|
|
* // Load a script using a unique instance of the script loader
|
|
* var scriptLoader = new tinymce.dom.ScriptLoader();
|
|
*
|
|
* scriptLoader.load('somescript.js');
|
|
*
|
|
* // Load multiple scripts
|
|
* var scriptLoader = new tinymce.dom.ScriptLoader();
|
|
*
|
|
* scriptLoader.add('somescript1.js');
|
|
* scriptLoader.add('somescript2.js');
|
|
* scriptLoader.add('somescript3.js');
|
|
*
|
|
* scriptLoader.loadQueue(function() {
|
|
* alert('All scripts are now loaded.');
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.ScriptLoader',
|
|
[
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (DOMUtils, Tools) {
|
|
var DOM = DOMUtils.DOM;
|
|
var each = Tools.each, grep = Tools.grep;
|
|
|
|
var isFunction = function (f) {
|
|
return typeof f === 'function';
|
|
};
|
|
|
|
function ScriptLoader() {
|
|
var QUEUED = 0,
|
|
LOADING = 1,
|
|
LOADED = 2,
|
|
FAILED = 3,
|
|
states = {},
|
|
queue = [],
|
|
scriptLoadedCallbacks = {},
|
|
queueLoadedCallbacks = [],
|
|
loading = 0,
|
|
undef;
|
|
|
|
/**
|
|
* Loads a specific script directly without adding it to the load queue.
|
|
*
|
|
* @method load
|
|
* @param {String} url Absolute URL to script to add.
|
|
* @param {function} callback Optional success callback function when the script loaded successfully.
|
|
* @param {function} callback Optional failure callback function when the script failed to load.
|
|
*/
|
|
function loadScript(url, success, failure) {
|
|
var dom = DOM, elm, id;
|
|
|
|
// Execute callback when script is loaded
|
|
function done() {
|
|
dom.remove(id);
|
|
|
|
if (elm) {
|
|
elm.onreadystatechange = elm.onload = elm = null;
|
|
}
|
|
|
|
success();
|
|
}
|
|
|
|
function error() {
|
|
/*eslint no-console:0 */
|
|
|
|
// We can't mark it as done if there is a load error since
|
|
// A) We don't want to produce 404 errors on the server and
|
|
// B) the onerror event won't fire on all browsers.
|
|
// done();
|
|
|
|
if (isFunction(failure)) {
|
|
failure();
|
|
} else {
|
|
// Report the error so it's easier for people to spot loading errors
|
|
if (typeof console !== "undefined" && console.log) {
|
|
console.log("Failed to load script: " + url);
|
|
}
|
|
}
|
|
}
|
|
|
|
id = dom.uniqueId();
|
|
|
|
// Create new script element
|
|
elm = document.createElement('script');
|
|
elm.id = id;
|
|
elm.type = 'text/javascript';
|
|
elm.src = Tools._addCacheSuffix(url);
|
|
|
|
// Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly
|
|
if ("onreadystatechange" in elm) {
|
|
elm.onreadystatechange = function () {
|
|
if (/loaded|complete/.test(elm.readyState)) {
|
|
done();
|
|
}
|
|
};
|
|
} else {
|
|
elm.onload = done;
|
|
}
|
|
|
|
// Add onerror event will get fired on some browsers but not all of them
|
|
elm.onerror = error;
|
|
|
|
// Add script to document
|
|
(document.getElementsByTagName('head')[0] || document.body).appendChild(elm);
|
|
}
|
|
|
|
/**
|
|
* Returns true/false if a script has been loaded or not.
|
|
*
|
|
* @method isDone
|
|
* @param {String} url URL to check for.
|
|
* @return {Boolean} true/false if the URL is loaded.
|
|
*/
|
|
this.isDone = function (url) {
|
|
return states[url] == LOADED;
|
|
};
|
|
|
|
/**
|
|
* Marks a specific script to be loaded. This can be useful if a script got loaded outside
|
|
* the script loader or to skip it from loading some script.
|
|
*
|
|
* @method markDone
|
|
* @param {string} url Absolute URL to the script to mark as loaded.
|
|
*/
|
|
this.markDone = function (url) {
|
|
states[url] = LOADED;
|
|
};
|
|
|
|
/**
|
|
* Adds a specific script to the load queue of the script loader.
|
|
*
|
|
* @method add
|
|
* @param {String} url Absolute URL to script to add.
|
|
* @param {function} success Optional success callback function to execute when the script loades successfully.
|
|
* @param {Object} scope Optional scope to execute callback in.
|
|
* @param {function} failure Optional failure callback function to execute when the script failed to load.
|
|
*/
|
|
this.add = this.load = function (url, success, scope, failure) {
|
|
var state = states[url];
|
|
|
|
// Add url to load queue
|
|
if (state == undef) {
|
|
queue.push(url);
|
|
states[url] = QUEUED;
|
|
}
|
|
|
|
if (success) {
|
|
// Store away callback for later execution
|
|
if (!scriptLoadedCallbacks[url]) {
|
|
scriptLoadedCallbacks[url] = [];
|
|
}
|
|
|
|
scriptLoadedCallbacks[url].push({
|
|
success: success,
|
|
failure: failure,
|
|
scope: scope || this
|
|
});
|
|
}
|
|
};
|
|
|
|
this.remove = function (url) {
|
|
delete states[url];
|
|
delete scriptLoadedCallbacks[url];
|
|
};
|
|
|
|
/**
|
|
* Starts the loading of the queue.
|
|
*
|
|
* @method loadQueue
|
|
* @param {function} success Optional callback to execute when all queued items are loaded.
|
|
* @param {function} failure Optional callback to execute when queued items failed to load.
|
|
* @param {Object} scope Optional scope to execute the callback in.
|
|
*/
|
|
this.loadQueue = function (success, scope, failure) {
|
|
this.loadScripts(queue, success, scope, failure);
|
|
};
|
|
|
|
/**
|
|
* Loads the specified queue of files and executes the callback ones they are loaded.
|
|
* This method is generally not used outside this class but it might be useful in some scenarios.
|
|
*
|
|
* @method loadScripts
|
|
* @param {Array} scripts Array of queue items to load.
|
|
* @param {function} callback Optional callback to execute when scripts is loaded successfully.
|
|
* @param {Object} scope Optional scope to execute callback in.
|
|
* @param {function} callback Optional callback to execute if scripts failed to load.
|
|
*/
|
|
this.loadScripts = function (scripts, success, scope, failure) {
|
|
var loadScripts, failures = [];
|
|
|
|
function execCallbacks(name, url) {
|
|
// Execute URL callback functions
|
|
each(scriptLoadedCallbacks[url], function (callback) {
|
|
if (isFunction(callback[name])) {
|
|
callback[name].call(callback.scope);
|
|
}
|
|
});
|
|
|
|
scriptLoadedCallbacks[url] = undef;
|
|
}
|
|
|
|
queueLoadedCallbacks.push({
|
|
success: success,
|
|
failure: failure,
|
|
scope: scope || this
|
|
});
|
|
|
|
loadScripts = function () {
|
|
var loadingScripts = grep(scripts);
|
|
|
|
// Current scripts has been handled
|
|
scripts.length = 0;
|
|
|
|
// Load scripts that needs to be loaded
|
|
each(loadingScripts, function (url) {
|
|
// Script is already loaded then execute script callbacks directly
|
|
if (states[url] === LOADED) {
|
|
execCallbacks('success', url);
|
|
return;
|
|
}
|
|
|
|
if (states[url] === FAILED) {
|
|
execCallbacks('failure', url);
|
|
return;
|
|
}
|
|
|
|
// Is script not loading then start loading it
|
|
if (states[url] !== LOADING) {
|
|
states[url] = LOADING;
|
|
loading++;
|
|
|
|
loadScript(url, function () {
|
|
states[url] = LOADED;
|
|
loading--;
|
|
|
|
execCallbacks('success', url);
|
|
|
|
// Load more scripts if they where added by the recently loaded script
|
|
loadScripts();
|
|
}, function () {
|
|
states[url] = FAILED;
|
|
loading--;
|
|
|
|
failures.push(url);
|
|
execCallbacks('failure', url);
|
|
|
|
// Load more scripts if they where added by the recently loaded script
|
|
loadScripts();
|
|
});
|
|
}
|
|
});
|
|
|
|
// No scripts are currently loading then execute all pending queue loaded callbacks
|
|
if (!loading) {
|
|
each(queueLoadedCallbacks, function (callback) {
|
|
if (failures.length === 0) {
|
|
if (isFunction(callback.success)) {
|
|
callback.success.call(callback.scope);
|
|
}
|
|
} else {
|
|
if (isFunction(callback.failure)) {
|
|
callback.failure.call(callback.scope, failures);
|
|
}
|
|
}
|
|
});
|
|
|
|
queueLoadedCallbacks.length = 0;
|
|
}
|
|
};
|
|
|
|
loadScripts();
|
|
};
|
|
}
|
|
|
|
ScriptLoader.ScriptLoader = new ScriptLoader();
|
|
|
|
return ScriptLoader;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* AddOnManager.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 loading of themes/plugins or other add-ons and their language packs.
|
|
*
|
|
* @class tinymce.AddOnManager
|
|
*/
|
|
define(
|
|
'tinymce.core.AddOnManager',
|
|
[
|
|
"tinymce.core.dom.ScriptLoader",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (ScriptLoader, Tools) {
|
|
var each = Tools.each;
|
|
|
|
function AddOnManager() {
|
|
var self = this;
|
|
|
|
self.items = [];
|
|
self.urls = {};
|
|
self.lookup = {};
|
|
}
|
|
|
|
AddOnManager.prototype = {
|
|
/**
|
|
* Returns the specified add on by the short name.
|
|
*
|
|
* @method get
|
|
* @param {String} name Add-on to look for.
|
|
* @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
|
|
*/
|
|
get: function (name) {
|
|
if (this.lookup[name]) {
|
|
return this.lookup[name].instance;
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
dependencies: function (name) {
|
|
var result;
|
|
|
|
if (this.lookup[name]) {
|
|
result = this.lookup[name].dependencies;
|
|
}
|
|
|
|
return result || [];
|
|
},
|
|
|
|
/**
|
|
* Loads a language pack for the specified add-on.
|
|
*
|
|
* @method requireLangPack
|
|
* @param {String} name Short name of the add-on.
|
|
* @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
|
|
*/
|
|
requireLangPack: function (name, languages) {
|
|
var language = AddOnManager.language;
|
|
|
|
if (language && AddOnManager.languageLoad !== false) {
|
|
if (languages) {
|
|
languages = ',' + languages + ',';
|
|
|
|
// Load short form sv.js or long form sv_SE.js
|
|
if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) {
|
|
language = language.substr(0, 2);
|
|
} else if (languages.indexOf(',' + language + ',') == -1) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds a instance of the add-on by it's short name.
|
|
*
|
|
* @method add
|
|
* @param {String} id Short name/id for the add-on.
|
|
* @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add.
|
|
* @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in.
|
|
* @example
|
|
* // Create a simple plugin
|
|
* tinymce.create('tinymce.plugins.TestPlugin', {
|
|
* TestPlugin: function(ed, url) {
|
|
* ed.on('click', function(e) {
|
|
* ed.windowManager.alert('Hello World!');
|
|
* });
|
|
* }
|
|
* });
|
|
*
|
|
* // Register plugin using the add method
|
|
* tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin);
|
|
*
|
|
* // Initialize TinyMCE
|
|
* tinymce.init({
|
|
* ...
|
|
* plugins: '-test' // Init the plugin but don't try to load it
|
|
* });
|
|
*/
|
|
add: function (id, addOn, dependencies) {
|
|
this.items.push(addOn);
|
|
this.lookup[id] = { instance: addOn, dependencies: dependencies };
|
|
|
|
return addOn;
|
|
},
|
|
|
|
remove: function (name) {
|
|
delete this.urls[name];
|
|
delete this.lookup[name];
|
|
},
|
|
|
|
createUrl: function (baseUrl, dep) {
|
|
if (typeof dep === "object") {
|
|
return dep;
|
|
}
|
|
|
|
return { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix };
|
|
},
|
|
|
|
/**
|
|
* Add a set of components that will make up the add-on. Using the url of the add-on name as the base url.
|
|
* This should be used in development mode. A new compressor/javascript munger process will ensure that the
|
|
* components are put together into the plugin.js file and compressed correctly.
|
|
*
|
|
* @method addComponents
|
|
* @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins).
|
|
* @param {Array} scripts Array containing the names of the scripts to load.
|
|
*/
|
|
addComponents: function (pluginName, scripts) {
|
|
var pluginUrl = this.urls[pluginName];
|
|
|
|
each(scripts, function (script) {
|
|
ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Loads an add-on from a specific url.
|
|
*
|
|
* @method load
|
|
* @param {String} name Short name of the add-on that gets loaded.
|
|
* @param {String} addOnUrl URL to the add-on that will get loaded.
|
|
* @param {function} success Optional success callback to execute when an add-on is loaded.
|
|
* @param {Object} scope Optional scope to execute the callback in.
|
|
* @param {function} failure Optional failure callback to execute when an add-on failed to load.
|
|
* @example
|
|
* // Loads a plugin from an external URL
|
|
* tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
|
|
*
|
|
* // Initialize TinyMCE
|
|
* tinymce.init({
|
|
* ...
|
|
* plugins: '-myplugin' // Don't try to load it again
|
|
* });
|
|
*/
|
|
load: function (name, addOnUrl, success, scope, failure) {
|
|
var self = this, url = addOnUrl;
|
|
|
|
function loadDependencies() {
|
|
var dependencies = self.dependencies(name);
|
|
|
|
each(dependencies, function (dep) {
|
|
var newUrl = self.createUrl(addOnUrl, dep);
|
|
|
|
self.load(newUrl.resource, newUrl, undefined, undefined);
|
|
});
|
|
|
|
if (success) {
|
|
if (scope) {
|
|
success.call(scope);
|
|
} else {
|
|
success.call(ScriptLoader);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.urls[name]) {
|
|
return;
|
|
}
|
|
|
|
if (typeof addOnUrl === "object") {
|
|
url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
|
|
}
|
|
|
|
if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) {
|
|
url = AddOnManager.baseURL + '/' + url;
|
|
}
|
|
|
|
self.urls[name] = url.substring(0, url.lastIndexOf('/'));
|
|
|
|
if (self.lookup[name]) {
|
|
loadDependencies();
|
|
} else {
|
|
ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure);
|
|
}
|
|
}
|
|
};
|
|
|
|
AddOnManager.PluginManager = new AddOnManager();
|
|
AddOnManager.ThemeManager = new AddOnManager();
|
|
|
|
return AddOnManager;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* TinyMCE theme class.
|
|
*
|
|
* @class tinymce.Theme
|
|
*/
|
|
|
|
/**
|
|
* This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc.
|
|
*
|
|
* @method renderUI
|
|
* @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance.
|
|
* @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight.
|
|
*/
|
|
|
|
/**
|
|
* Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional.
|
|
*
|
|
* @class tinymce.Plugin
|
|
* @example
|
|
* tinymce.PluginManager.add('example', function(editor, url) {
|
|
* // Add a button that opens a window
|
|
* editor.addButton('example', {
|
|
* text: 'My button',
|
|
* icon: false,
|
|
* onclick: function() {
|
|
* // Open window
|
|
* editor.windowManager.open({
|
|
* title: 'Example plugin',
|
|
* body: [
|
|
* {type: 'textbox', name: 'title', label: 'Title'}
|
|
* ],
|
|
* onsubmit: function(e) {
|
|
* // Insert content when the window form is submitted
|
|
* editor.insertContent('Title: ' + e.data.title);
|
|
* }
|
|
* });
|
|
* }
|
|
* });
|
|
*
|
|
* // Adds a menu item to the tools menu
|
|
* editor.addMenuItem('example', {
|
|
* text: 'Example plugin',
|
|
* context: 'tools',
|
|
* onclick: function() {
|
|
* // Open window with a specific url
|
|
* editor.windowManager.open({
|
|
* title: 'TinyMCE site',
|
|
* url: 'http://www.tinymce.com',
|
|
* width: 800,
|
|
* height: 600,
|
|
* buttons: [{
|
|
* text: 'Close',
|
|
* onclick: 'close'
|
|
* }]
|
|
* });
|
|
* }
|
|
* });
|
|
* });
|
|
*/
|
|
|
|
/**
|
|
* NodeType.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 node validation functions.
|
|
*
|
|
* @private
|
|
* @class tinymce.dom.NodeType
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.NodeType',
|
|
[
|
|
],
|
|
function () {
|
|
function isNodeType(type) {
|
|
return function (node) {
|
|
return !!node && node.nodeType == type;
|
|
};
|
|
}
|
|
|
|
var isElement = isNodeType(1);
|
|
|
|
function matchNodeNames(names) {
|
|
names = names.toLowerCase().split(' ');
|
|
|
|
return function (node) {
|
|
var i, name;
|
|
|
|
if (node && node.nodeType) {
|
|
name = node.nodeName.toLowerCase();
|
|
|
|
for (i = 0; i < names.length; i++) {
|
|
if (name === names[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
function matchStyleValues(name, values) {
|
|
values = values.toLowerCase().split(' ');
|
|
|
|
return function (node) {
|
|
var i, cssValue;
|
|
|
|
if (isElement(node)) {
|
|
for (i = 0; i < values.length; i++) {
|
|
cssValue = node.ownerDocument.defaultView.getComputedStyle(node, null).getPropertyValue(name);
|
|
if (cssValue === values[i]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
function hasPropValue(propName, propValue) {
|
|
return function (node) {
|
|
return isElement(node) && node[propName] === propValue;
|
|
};
|
|
}
|
|
|
|
function hasAttribute(attrName, attrValue) {
|
|
return function (node) {
|
|
return isElement(node) && node.hasAttribute(attrName);
|
|
};
|
|
}
|
|
|
|
function hasAttributeValue(attrName, attrValue) {
|
|
return function (node) {
|
|
return isElement(node) && node.getAttribute(attrName) === attrValue;
|
|
};
|
|
}
|
|
|
|
function isBogus(node) {
|
|
return isElement(node) && node.hasAttribute('data-mce-bogus');
|
|
}
|
|
|
|
function hasContentEditableState(value) {
|
|
return function (node) {
|
|
if (isElement(node)) {
|
|
if (node.contentEditable === value) {
|
|
return true;
|
|
}
|
|
|
|
if (node.getAttribute('data-mce-contenteditable') === value) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
return {
|
|
isText: isNodeType(3),
|
|
isElement: isElement,
|
|
isComment: isNodeType(8),
|
|
isBr: matchNodeNames('br'),
|
|
isContentEditableTrue: hasContentEditableState('true'),
|
|
isContentEditableFalse: hasContentEditableState('false'),
|
|
matchNodeNames: matchNodeNames,
|
|
hasPropValue: hasPropValue,
|
|
hasAttribute: hasAttribute,
|
|
hasAttributeValue: hasAttributeValue,
|
|
matchStyleValues: matchStyleValues,
|
|
isBogus: isBogus
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Zwsp.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 zero width space
|
|
* characters used as character containers etc.
|
|
*
|
|
* @private
|
|
* @class tinymce.text.Zwsp
|
|
* @example
|
|
* var isZwsp = Zwsp.isZwsp('\uFEFF');
|
|
* var abc = Zwsp.trim('a\uFEFFc');
|
|
*/
|
|
define(
|
|
'tinymce.core.text.Zwsp',
|
|
[
|
|
],
|
|
function () {
|
|
// This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP
|
|
var ZWSP = '\uFEFF';
|
|
|
|
var isZwsp = function (chr) {
|
|
return chr === ZWSP;
|
|
};
|
|
|
|
var trim = function (text) {
|
|
return text.replace(new RegExp(ZWSP, 'g'), '');
|
|
};
|
|
|
|
return {
|
|
isZwsp: isZwsp,
|
|
ZWSP: ZWSP,
|
|
trim: trim
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CaretContainer.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 caret containers. A caret container is a node that
|
|
* holds the caret for positional purposes.
|
|
*
|
|
* @private
|
|
* @class tinymce.caret.CaretContainer
|
|
*/
|
|
define(
|
|
'tinymce.core.caret.CaretContainer',
|
|
[
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.text.Zwsp"
|
|
],
|
|
function (NodeType, Zwsp) {
|
|
var isElement = NodeType.isElement,
|
|
isText = NodeType.isText;
|
|
|
|
function isCaretContainerBlock(node) {
|
|
if (isText(node)) {
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return isElement(node) && node.hasAttribute('data-mce-caret');
|
|
}
|
|
|
|
function isCaretContainerInline(node) {
|
|
return isText(node) && Zwsp.isZwsp(node.data);
|
|
}
|
|
|
|
function isCaretContainer(node) {
|
|
return isCaretContainerBlock(node) || isCaretContainerInline(node);
|
|
}
|
|
|
|
var hasContent = function (node) {
|
|
return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild);
|
|
};
|
|
|
|
function insertInline(node, before) {
|
|
var doc, sibling, textNode, parentNode;
|
|
|
|
doc = node.ownerDocument;
|
|
textNode = doc.createTextNode(Zwsp.ZWSP);
|
|
parentNode = node.parentNode;
|
|
|
|
if (!before) {
|
|
sibling = node.nextSibling;
|
|
if (isText(sibling)) {
|
|
if (isCaretContainer(sibling)) {
|
|
return sibling;
|
|
}
|
|
|
|
if (startsWithCaretContainer(sibling)) {
|
|
sibling.splitText(1);
|
|
return sibling;
|
|
}
|
|
}
|
|
|
|
if (node.nextSibling) {
|
|
parentNode.insertBefore(textNode, node.nextSibling);
|
|
} else {
|
|
parentNode.appendChild(textNode);
|
|
}
|
|
} else {
|
|
sibling = node.previousSibling;
|
|
if (isText(sibling)) {
|
|
if (isCaretContainer(sibling)) {
|
|
return sibling;
|
|
}
|
|
|
|
if (endsWithCaretContainer(sibling)) {
|
|
return sibling.splitText(sibling.data.length - 1);
|
|
}
|
|
}
|
|
|
|
parentNode.insertBefore(textNode, node);
|
|
}
|
|
|
|
return textNode;
|
|
}
|
|
|
|
var prependInline = function (node) {
|
|
if (NodeType.isText(node)) {
|
|
var data = node.data;
|
|
if (data.length > 0 && data.charAt(0) !== Zwsp.ZWSP) {
|
|
node.insertData(0, Zwsp.ZWSP);
|
|
}
|
|
return node;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
var appendInline = function (node) {
|
|
if (NodeType.isText(node)) {
|
|
var data = node.data;
|
|
if (data.length > 0 && data.charAt(data.length - 1) !== Zwsp.ZWSP) {
|
|
node.insertData(data.length, Zwsp.ZWSP);
|
|
}
|
|
return node;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
var isBeforeInline = function (pos) {
|
|
return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset()) === Zwsp.ZWSP;
|
|
};
|
|
|
|
var isAfterInline = function (pos) {
|
|
return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset() - 1) === Zwsp.ZWSP;
|
|
};
|
|
|
|
function createBogusBr() {
|
|
var br = document.createElement('br');
|
|
br.setAttribute('data-mce-bogus', '1');
|
|
return br;
|
|
}
|
|
|
|
function insertBlock(blockName, node, before) {
|
|
var doc, blockNode, parentNode;
|
|
|
|
doc = node.ownerDocument;
|
|
blockNode = doc.createElement(blockName);
|
|
blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after');
|
|
blockNode.setAttribute('data-mce-bogus', 'all');
|
|
blockNode.appendChild(createBogusBr());
|
|
parentNode = node.parentNode;
|
|
|
|
if (!before) {
|
|
if (node.nextSibling) {
|
|
parentNode.insertBefore(blockNode, node.nextSibling);
|
|
} else {
|
|
parentNode.appendChild(blockNode);
|
|
}
|
|
} else {
|
|
parentNode.insertBefore(blockNode, node);
|
|
}
|
|
|
|
return blockNode;
|
|
}
|
|
|
|
function startsWithCaretContainer(node) {
|
|
return isText(node) && node.data[0] == Zwsp.ZWSP;
|
|
}
|
|
|
|
function endsWithCaretContainer(node) {
|
|
return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP;
|
|
}
|
|
|
|
function trimBogusBr(elm) {
|
|
var brs = elm.getElementsByTagName('br');
|
|
var lastBr = brs[brs.length - 1];
|
|
if (NodeType.isBogus(lastBr)) {
|
|
lastBr.parentNode.removeChild(lastBr);
|
|
}
|
|
}
|
|
|
|
function showCaretContainerBlock(caretContainer) {
|
|
if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) {
|
|
trimBogusBr(caretContainer);
|
|
caretContainer.removeAttribute('data-mce-caret');
|
|
caretContainer.removeAttribute('data-mce-bogus');
|
|
caretContainer.removeAttribute('style');
|
|
caretContainer.removeAttribute('_moz_abspos');
|
|
return caretContainer;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
isCaretContainer: isCaretContainer,
|
|
isCaretContainerBlock: isCaretContainerBlock,
|
|
isCaretContainerInline: isCaretContainerInline,
|
|
showCaretContainerBlock: showCaretContainerBlock,
|
|
insertInline: insertInline,
|
|
prependInline: prependInline,
|
|
appendInline: appendInline,
|
|
isBeforeInline: isBeforeInline,
|
|
isAfterInline: isAfterInline,
|
|
insertBlock: insertBlock,
|
|
hasContent: hasContent,
|
|
startsWithCaretContainer: startsWithCaretContainer,
|
|
endsWithCaretContainer: endsWithCaretContainer
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* RangeUtils.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 a few utility methods for ranges.
|
|
*
|
|
* @class tinymce.dom.RangeUtils
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.RangeUtils',
|
|
[
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.dom.TreeWalker",
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.dom.Range",
|
|
"tinymce.core.caret.CaretContainer"
|
|
],
|
|
function (Tools, TreeWalker, NodeType, Range, CaretContainer) {
|
|
var each = Tools.each,
|
|
isContentEditableTrue = NodeType.isContentEditableTrue,
|
|
isContentEditableFalse = NodeType.isContentEditableFalse,
|
|
isCaretContainer = CaretContainer.isCaretContainer;
|
|
|
|
function hasCeProperty(node) {
|
|
return isContentEditableTrue(node) || isContentEditableFalse(node);
|
|
}
|
|
|
|
function getEndChild(container, index) {
|
|
var childNodes = container.childNodes;
|
|
|
|
index--;
|
|
|
|
if (index > childNodes.length - 1) {
|
|
index = childNodes.length - 1;
|
|
} else if (index < 0) {
|
|
index = 0;
|
|
}
|
|
|
|
return childNodes[index] || container;
|
|
}
|
|
|
|
function findParent(node, rootNode, predicate) {
|
|
while (node && node !== rootNode) {
|
|
if (predicate(node)) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function hasParent(node, rootNode, predicate) {
|
|
return findParent(node, rootNode, predicate) !== null;
|
|
}
|
|
|
|
function hasParentWithName(node, rootNode, name) {
|
|
return hasParent(node, rootNode, function (node) {
|
|
return node.nodeName === name;
|
|
});
|
|
}
|
|
|
|
function isFormatterCaret(node) {
|
|
return node.id === '_mce_caret';
|
|
}
|
|
|
|
function isCeFalseCaretContainer(node, rootNode) {
|
|
return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false;
|
|
}
|
|
|
|
function RangeUtils(dom) {
|
|
/**
|
|
* Walks the specified range like object and executes the callback for each sibling collection it finds.
|
|
*
|
|
* @private
|
|
* @method walk
|
|
* @param {Object} rng Range like object.
|
|
* @param {function} callback Callback function to execute for each sibling collection.
|
|
*/
|
|
this.walk = function (rng, callback) {
|
|
var startContainer = rng.startContainer,
|
|
startOffset = rng.startOffset,
|
|
endContainer = rng.endContainer,
|
|
endOffset = rng.endOffset,
|
|
ancestor, startPoint,
|
|
endPoint, node, parent, siblings, nodes;
|
|
|
|
// Handle table cell selection the table plugin enables
|
|
// you to fake select table cells and perform formatting actions on them
|
|
nodes = dom.select('td[data-mce-selected],th[data-mce-selected]');
|
|
if (nodes.length > 0) {
|
|
each(nodes, function (node) {
|
|
callback([node]);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Excludes start/end text node if they are out side the range
|
|
*
|
|
* @private
|
|
* @param {Array} nodes Nodes to exclude items from.
|
|
* @return {Array} Array with nodes excluding the start/end container if needed.
|
|
*/
|
|
function exclude(nodes) {
|
|
var node;
|
|
|
|
// First node is excluded
|
|
node = nodes[0];
|
|
if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
|
|
nodes.splice(0, 1);
|
|
}
|
|
|
|
// Last node is excluded
|
|
node = nodes[nodes.length - 1];
|
|
if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
|
|
nodes.splice(nodes.length - 1, 1);
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
/**
|
|
* Collects siblings
|
|
*
|
|
* @private
|
|
* @param {Node} node Node to collect siblings from.
|
|
* @param {String} name Name of the sibling to check for.
|
|
* @param {Node} endNode
|
|
* @return {Array} Array of collected siblings.
|
|
*/
|
|
function collectSiblings(node, name, endNode) {
|
|
var siblings = [];
|
|
|
|
for (; node && node != endNode; node = node[name]) {
|
|
siblings.push(node);
|
|
}
|
|
|
|
return siblings;
|
|
}
|
|
|
|
/**
|
|
* Find an end point this is the node just before the common ancestor root.
|
|
*
|
|
* @private
|
|
* @param {Node} node Node to start at.
|
|
* @param {Node} root Root/ancestor element to stop just before.
|
|
* @return {Node} Node just before the root element.
|
|
*/
|
|
function findEndPoint(node, root) {
|
|
do {
|
|
if (node.parentNode == root) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
} while (node);
|
|
}
|
|
|
|
function walkBoundary(startNode, endNode, next) {
|
|
var siblingName = next ? 'nextSibling' : 'previousSibling';
|
|
|
|
for (node = startNode, parent = node.parentNode; node && node != endNode; node = parent) {
|
|
parent = node.parentNode;
|
|
siblings = collectSiblings(node == startNode ? node : node[siblingName], siblingName);
|
|
|
|
if (siblings.length) {
|
|
if (!next) {
|
|
siblings.reverse();
|
|
}
|
|
|
|
callback(exclude(siblings));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If index based start position then resolve it
|
|
if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
|
|
startContainer = startContainer.childNodes[startOffset];
|
|
}
|
|
|
|
// If index based end position then resolve it
|
|
if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
|
|
endContainer = getEndChild(endContainer, endOffset);
|
|
}
|
|
|
|
// Same container
|
|
if (startContainer == endContainer) {
|
|
return callback(exclude([startContainer]));
|
|
}
|
|
|
|
// Find common ancestor and end points
|
|
ancestor = dom.findCommonAncestor(startContainer, endContainer);
|
|
|
|
// Process left side
|
|
for (node = startContainer; node; node = node.parentNode) {
|
|
if (node === endContainer) {
|
|
return walkBoundary(startContainer, ancestor, true);
|
|
}
|
|
|
|
if (node === ancestor) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Process right side
|
|
for (node = endContainer; node; node = node.parentNode) {
|
|
if (node === startContainer) {
|
|
return walkBoundary(endContainer, ancestor);
|
|
}
|
|
|
|
if (node === ancestor) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find start/end point
|
|
startPoint = findEndPoint(startContainer, ancestor) || startContainer;
|
|
endPoint = findEndPoint(endContainer, ancestor) || endContainer;
|
|
|
|
// Walk left leaf
|
|
walkBoundary(startContainer, startPoint, true);
|
|
|
|
// Walk the middle from start to end point
|
|
siblings = collectSiblings(
|
|
startPoint == startContainer ? startPoint : startPoint.nextSibling,
|
|
'nextSibling',
|
|
endPoint == endContainer ? endPoint.nextSibling : endPoint
|
|
);
|
|
|
|
if (siblings.length) {
|
|
callback(exclude(siblings));
|
|
}
|
|
|
|
// Walk right leaf
|
|
walkBoundary(endContainer, endPoint);
|
|
};
|
|
|
|
/**
|
|
* Splits the specified range at it's start/end points.
|
|
*
|
|
* @private
|
|
* @param {Range/RangeObject} rng Range to split.
|
|
* @return {Object} Range position object.
|
|
*/
|
|
this.split = function (rng) {
|
|
var startContainer = rng.startContainer,
|
|
startOffset = rng.startOffset,
|
|
endContainer = rng.endContainer,
|
|
endOffset = rng.endOffset;
|
|
|
|
function splitText(node, offset) {
|
|
return node.splitText(offset);
|
|
}
|
|
|
|
// Handle single text node
|
|
if (startContainer == endContainer && startContainer.nodeType == 3) {
|
|
if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
|
endContainer = splitText(startContainer, startOffset);
|
|
startContainer = endContainer.previousSibling;
|
|
|
|
if (endOffset > startOffset) {
|
|
endOffset = endOffset - startOffset;
|
|
startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
|
|
endOffset = endContainer.nodeValue.length;
|
|
startOffset = 0;
|
|
} else {
|
|
endOffset = 0;
|
|
}
|
|
}
|
|
} else {
|
|
// Split startContainer text node if needed
|
|
if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
|
startContainer = splitText(startContainer, startOffset);
|
|
startOffset = 0;
|
|
}
|
|
|
|
// Split endContainer text node if needed
|
|
if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
|
|
endContainer = splitText(endContainer, endOffset).previousSibling;
|
|
endOffset = endContainer.nodeValue.length;
|
|
}
|
|
}
|
|
|
|
return {
|
|
startContainer: startContainer,
|
|
startOffset: startOffset,
|
|
endContainer: endContainer,
|
|
endOffset: endOffset
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Normalizes the specified range by finding the closest best suitable caret location.
|
|
*
|
|
* @private
|
|
* @param {Range} rng Range to normalize.
|
|
* @return {Boolean} True/false if the specified range was normalized or not.
|
|
*/
|
|
this.normalize = function (rng) {
|
|
var normalized = false, collapsed;
|
|
|
|
function normalizeEndPoint(start) {
|
|
var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
|
|
var directionLeft, isAfterNode;
|
|
|
|
function isTableCell(node) {
|
|
return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
|
|
}
|
|
|
|
function hasBrBeforeAfter(node, left) {
|
|
var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
|
|
|
|
while ((node = walker[left ? 'prev' : 'next']())) {
|
|
if (node.nodeName === "BR") {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function hasContentEditableFalseParent(node) {
|
|
while (node && node != body) {
|
|
if (isContentEditableFalse(node)) {
|
|
return true;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isPrevNode(node, name) {
|
|
return node.previousSibling && node.previousSibling.nodeName == name;
|
|
}
|
|
|
|
// Walks the dom left/right to find a suitable text node to move the endpoint into
|
|
// It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
|
|
function findTextNodeRelative(left, startNode) {
|
|
var walker, lastInlineElement, parentBlockContainer;
|
|
|
|
startNode = startNode || container;
|
|
parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
|
|
|
|
// Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
|
|
// This: <p><br>|</p> becomes <p>|<br></p>
|
|
if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
|
|
container = startNode.parentNode;
|
|
offset = dom.nodeIndex(startNode);
|
|
normalized = true;
|
|
return;
|
|
}
|
|
|
|
// Walk left until we hit a text node we can move to or a block/br/img
|
|
walker = new TreeWalker(startNode, parentBlockContainer);
|
|
while ((node = walker[left ? 'prev' : 'next']())) {
|
|
// Break if we hit a non content editable node
|
|
if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) {
|
|
return;
|
|
}
|
|
|
|
// Found text node that has a length
|
|
if (node.nodeType === 3 && node.nodeValue.length > 0) {
|
|
if (hasParentWithName(node, body, 'A') === false) {
|
|
container = node;
|
|
offset = left ? node.nodeValue.length : 0;
|
|
normalized = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Break if we find a block or a BR/IMG/INPUT etc
|
|
if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
|
|
return;
|
|
}
|
|
|
|
lastInlineElement = node;
|
|
}
|
|
|
|
// Only fetch the last inline element when in caret mode for now
|
|
if (collapsed && lastInlineElement) {
|
|
container = lastInlineElement;
|
|
normalized = true;
|
|
offset = 0;
|
|
}
|
|
}
|
|
|
|
container = rng[(start ? 'start' : 'end') + 'Container'];
|
|
offset = rng[(start ? 'start' : 'end') + 'Offset'];
|
|
isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
|
|
nonEmptyElementsMap = dom.schema.getNonEmptyElements();
|
|
directionLeft = start;
|
|
|
|
if (isCaretContainer(container)) {
|
|
return;
|
|
}
|
|
|
|
if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
|
|
directionLeft = false;
|
|
}
|
|
|
|
// If the container is a document move it to the body element
|
|
if (container.nodeType === 9) {
|
|
container = dom.getRoot();
|
|
offset = 0;
|
|
}
|
|
|
|
// If the container is body try move it into the closest text node or position
|
|
if (container === body) {
|
|
// If start is before/after a image, table etc
|
|
if (directionLeft) {
|
|
node = container.childNodes[offset > 0 ? offset - 1 : 0];
|
|
if (node) {
|
|
if (isCaretContainer(node)) {
|
|
return;
|
|
}
|
|
|
|
if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve the index
|
|
if (container.hasChildNodes()) {
|
|
offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
|
|
container = container.childNodes[offset];
|
|
offset = 0;
|
|
|
|
// Don't normalize non collapsed selections like <p>[a</p><table></table>]
|
|
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: <b>x</b><i>|x</i>
|
|
// Becomes: <b>x|</b><i>x</i>
|
|
// 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: <i><b></b><i>|<br></i>
|
|
// Becomes: <i><b>|</b><i><br></i>
|
|
// Seems that only gecko has issues with this.
|
|
// Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
|
|
if (container.nodeType === 1) {
|
|
node = container.childNodes[offset];
|
|
|
|
// Offset is after the containers last child
|
|
// then use the previous child for normalization
|
|
if (!node) {
|
|
node = container.childNodes[offset - 1];
|
|
}
|
|
|
|
if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
|
|
!hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
|
|
findTextNodeRelative(true, node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lean the start of the selection right if possible
|
|
// So this: x[<b>x]</b>
|
|
// Becomes: x<b>[x]</b>
|
|
if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
|
|
findTextNodeRelative(false);
|
|
}
|
|
|
|
// Set endpoint if it was normalized
|
|
if (normalized) {
|
|
rng['set' + (start ? 'Start' : 'End')](container, offset);
|
|
}
|
|
}
|
|
|
|
collapsed = rng.collapsed;
|
|
|
|
normalizeEndPoint(true);
|
|
|
|
if (!collapsed) {
|
|
normalizeEndPoint();
|
|
}
|
|
|
|
// If it was collapsed then make sure it still is
|
|
if (normalized && collapsed) {
|
|
rng.collapse(true);
|
|
}
|
|
|
|
return normalized;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compares two ranges and checks if they are equal.
|
|
*
|
|
* @static
|
|
* @method compareRanges
|
|
* @param {DOMRange} rng1 First range to compare.
|
|
* @param {DOMRange} rng2 First range to compare.
|
|
* @return {Boolean} true/false if the ranges are equal.
|
|
*/
|
|
RangeUtils.compareRanges = function (rng1, rng2) {
|
|
if (rng1 && rng2) {
|
|
// Compare native IE ranges
|
|
if (rng1.item || rng1.duplicate) {
|
|
// Both are control ranges and the selected element matches
|
|
if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
|
|
return true;
|
|
}
|
|
|
|
// Both are text ranges and the range matches
|
|
if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
|
|
return true;
|
|
}
|
|
} else {
|
|
// Compare w3c ranges
|
|
return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Finds the closest selection rect tries to get the range from that.
|
|
*/
|
|
function findClosestIeRange(clientX, clientY, doc) {
|
|
var element, rng, rects;
|
|
|
|
element = doc.elementFromPoint(clientX, clientY);
|
|
rng = doc.body.createTextRange();
|
|
|
|
if (!element || element.tagName == 'HTML') {
|
|
element = doc.body;
|
|
}
|
|
|
|
rng.moveToElementText(element);
|
|
rects = Tools.toArray(rng.getClientRects());
|
|
|
|
rects = rects.sort(function (a, b) {
|
|
a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY));
|
|
b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY));
|
|
|
|
return a - b;
|
|
});
|
|
|
|
if (rects.length > 0) {
|
|
clientY = (rects[0].bottom + rects[0].top) / 2;
|
|
|
|
try {
|
|
rng.moveToPoint(clientX, clientY);
|
|
rng.collapse(true);
|
|
|
|
return rng;
|
|
} catch (ex) {
|
|
// At least we tried
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function moveOutOfContentEditableFalse(rng, rootNode) {
|
|
var parentElement = rng && rng.parentElement ? rng.parentElement() : null;
|
|
return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng;
|
|
}
|
|
|
|
/**
|
|
* Gets the caret range for the given x/y location.
|
|
*
|
|
* @static
|
|
* @method getCaretRangeFromPoint
|
|
* @param {Number} clientX X coordinate for range
|
|
* @param {Number} clientY Y coordinate for range
|
|
* @param {Document} doc Document that x/y are relative to
|
|
* @returns {Range} caret range
|
|
*/
|
|
RangeUtils.getCaretRangeFromPoint = function (clientX, clientY, doc) {
|
|
var rng, point;
|
|
|
|
if (doc.caretPositionFromPoint) {
|
|
point = doc.caretPositionFromPoint(clientX, clientY);
|
|
rng = doc.createRange();
|
|
rng.setStart(point.offsetNode, point.offset);
|
|
rng.collapse(true);
|
|
} else if (doc.caretRangeFromPoint) {
|
|
rng = doc.caretRangeFromPoint(clientX, clientY);
|
|
} else if (doc.body.createTextRange) {
|
|
rng = doc.body.createTextRange();
|
|
|
|
try {
|
|
rng.moveToPoint(clientX, clientY);
|
|
rng.collapse(true);
|
|
} catch (ex) {
|
|
rng = findClosestIeRange(clientX, clientY, doc);
|
|
}
|
|
|
|
return moveOutOfContentEditableFalse(rng, doc.body);
|
|
}
|
|
|
|
return rng;
|
|
};
|
|
|
|
RangeUtils.getSelectedNode = function (range) {
|
|
var startContainer = range.startContainer,
|
|
startOffset = range.startOffset;
|
|
|
|
if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) {
|
|
return startContainer.childNodes[startOffset];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
RangeUtils.getNode = function (container, offset) {
|
|
if (container.nodeType == 1 && container.hasChildNodes()) {
|
|
if (offset >= container.childNodes.length) {
|
|
offset = container.childNodes.length - 1;
|
|
}
|
|
|
|
container = container.childNodes[offset];
|
|
}
|
|
|
|
return container;
|
|
};
|
|
|
|
return RangeUtils;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Node.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 minimalistic implementation of a DOM like node used by the DomParser class.
|
|
*
|
|
* @example
|
|
* var node = new tinymce.html.Node('strong', 1);
|
|
* someRoot.append(node);
|
|
*
|
|
* @class tinymce.html.Node
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.Node',
|
|
[
|
|
],
|
|
function () {
|
|
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
|
|
var typeLookup = {
|
|
'#text': 3,
|
|
'#comment': 8,
|
|
'#cdata': 4,
|
|
'#pi': 7,
|
|
'#doctype': 10,
|
|
'#document-fragment': 11
|
|
};
|
|
|
|
// Walks the tree left/right
|
|
function walk(node, rootNode, prev) {
|
|
var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
|
|
|
|
// Walk into nodes if it has a start
|
|
if (node[startName]) {
|
|
return node[startName];
|
|
}
|
|
|
|
// Return the sibling if it has one
|
|
if (node !== rootNode) {
|
|
sibling = node[siblingName];
|
|
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
|
|
// Walk up the parents to look for siblings
|
|
for (parent = node.parent; parent && parent !== rootNode; parent = parent.parent) {
|
|
sibling = parent[siblingName];
|
|
|
|
if (sibling) {
|
|
return sibling;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs a new Node instance.
|
|
*
|
|
* @constructor
|
|
* @method Node
|
|
* @param {String} name Name of the node type.
|
|
* @param {Number} type Numeric type representing the node.
|
|
*/
|
|
function Node(name, type) {
|
|
this.name = name;
|
|
this.type = type;
|
|
|
|
if (type === 1) {
|
|
this.attributes = [];
|
|
this.attributes.map = {};
|
|
}
|
|
}
|
|
|
|
Node.prototype = {
|
|
/**
|
|
* Replaces the current node with the specified one.
|
|
*
|
|
* @example
|
|
* someNode.replace(someNewNode);
|
|
*
|
|
* @method replace
|
|
* @param {tinymce.html.Node} node Node to replace the current node with.
|
|
* @return {tinymce.html.Node} The old node that got replaced.
|
|
*/
|
|
replace: function (node) {
|
|
var self = this;
|
|
|
|
if (node.parent) {
|
|
node.remove();
|
|
}
|
|
|
|
self.insert(node, self);
|
|
self.remove();
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Gets/sets or removes an attribute by name.
|
|
*
|
|
* @example
|
|
* someNode.attr("name", "value"); // Sets an attribute
|
|
* console.log(someNode.attr("name")); // Gets an attribute
|
|
* someNode.attr("name", null); // Removes an attribute
|
|
*
|
|
* @method attr
|
|
* @param {String} name Attribute name to set or get.
|
|
* @param {String} value Optional value to set.
|
|
* @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
|
|
*/
|
|
attr: function (name, value) {
|
|
var self = this, attrs, i, undef;
|
|
|
|
if (typeof name !== "string") {
|
|
for (i in name) {
|
|
self.attr(i, name[i]);
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
if ((attrs = self.attributes)) {
|
|
if (value !== undef) {
|
|
// Remove attribute
|
|
if (value === null) {
|
|
if (name in attrs.map) {
|
|
delete attrs.map[name];
|
|
|
|
i = attrs.length;
|
|
while (i--) {
|
|
if (attrs[i].name === name) {
|
|
attrs = attrs.splice(i, 1);
|
|
return self;
|
|
}
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
// Set attribute
|
|
if (name in attrs.map) {
|
|
// Set attribute
|
|
i = attrs.length;
|
|
while (i--) {
|
|
if (attrs[i].name === name) {
|
|
attrs[i].value = value;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
attrs.push({ name: name, value: value });
|
|
}
|
|
|
|
attrs.map[name] = value;
|
|
|
|
return self;
|
|
}
|
|
|
|
return attrs.map[name];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Does a shallow clones the node into a new node. It will also exclude id attributes since
|
|
* there should only be one id per document.
|
|
*
|
|
* @example
|
|
* var clonedNode = node.clone();
|
|
*
|
|
* @method clone
|
|
* @return {tinymce.html.Node} New copy of the original node.
|
|
*/
|
|
clone: function () {
|
|
var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
|
|
|
|
// Clone element attributes
|
|
if ((selfAttrs = self.attributes)) {
|
|
cloneAttrs = [];
|
|
cloneAttrs.map = {};
|
|
|
|
for (i = 0, l = selfAttrs.length; i < l; i++) {
|
|
selfAttr = selfAttrs[i];
|
|
|
|
// Clone everything except id
|
|
if (selfAttr.name !== 'id') {
|
|
cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value };
|
|
cloneAttrs.map[selfAttr.name] = selfAttr.value;
|
|
}
|
|
}
|
|
|
|
clone.attributes = cloneAttrs;
|
|
}
|
|
|
|
clone.value = self.value;
|
|
clone.shortEnded = self.shortEnded;
|
|
|
|
return clone;
|
|
},
|
|
|
|
/**
|
|
* Wraps the node in in another node.
|
|
*
|
|
* @example
|
|
* node.wrap(wrapperNode);
|
|
*
|
|
* @method wrap
|
|
*/
|
|
wrap: function (wrapper) {
|
|
var self = this;
|
|
|
|
self.parent.insert(wrapper, self);
|
|
wrapper.append(self);
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Unwraps the node in other words it removes the node but keeps the children.
|
|
*
|
|
* @example
|
|
* node.unwrap();
|
|
*
|
|
* @method unwrap
|
|
*/
|
|
unwrap: function () {
|
|
var self = this, node, next;
|
|
|
|
for (node = self.firstChild; node;) {
|
|
next = node.next;
|
|
self.insert(node, self, true);
|
|
node = next;
|
|
}
|
|
|
|
self.remove();
|
|
},
|
|
|
|
/**
|
|
* Removes the node from it's parent.
|
|
*
|
|
* @example
|
|
* node.remove();
|
|
*
|
|
* @method remove
|
|
* @return {tinymce.html.Node} Current node that got removed.
|
|
*/
|
|
remove: function () {
|
|
var self = this, parent = self.parent, next = self.next, prev = self.prev;
|
|
|
|
if (parent) {
|
|
if (parent.firstChild === self) {
|
|
parent.firstChild = next;
|
|
|
|
if (next) {
|
|
next.prev = null;
|
|
}
|
|
} else {
|
|
prev.next = next;
|
|
}
|
|
|
|
if (parent.lastChild === self) {
|
|
parent.lastChild = prev;
|
|
|
|
if (prev) {
|
|
prev.next = null;
|
|
}
|
|
} else {
|
|
next.prev = prev;
|
|
}
|
|
|
|
self.parent = self.next = self.prev = null;
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Appends a new node as a child of the current node.
|
|
*
|
|
* @example
|
|
* node.append(someNode);
|
|
*
|
|
* @method append
|
|
* @param {tinymce.html.Node} node Node to append as a child of the current one.
|
|
* @return {tinymce.html.Node} The node that got appended.
|
|
*/
|
|
append: function (node) {
|
|
var self = this, last;
|
|
|
|
if (node.parent) {
|
|
node.remove();
|
|
}
|
|
|
|
last = self.lastChild;
|
|
if (last) {
|
|
last.next = node;
|
|
node.prev = last;
|
|
self.lastChild = node;
|
|
} else {
|
|
self.lastChild = self.firstChild = node;
|
|
}
|
|
|
|
node.parent = self;
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Inserts a node at a specific position as a child of the current node.
|
|
*
|
|
* @example
|
|
* parentNode.insert(newChildNode, oldChildNode);
|
|
*
|
|
* @method insert
|
|
* @param {tinymce.html.Node} node Node to insert as a child of the current node.
|
|
* @param {tinymce.html.Node} refNode Reference node to set node before/after.
|
|
* @param {Boolean} before Optional state to insert the node before the reference node.
|
|
* @return {tinymce.html.Node} The node that got inserted.
|
|
*/
|
|
insert: function (node, refNode, before) {
|
|
var parent;
|
|
|
|
if (node.parent) {
|
|
node.remove();
|
|
}
|
|
|
|
parent = refNode.parent || this;
|
|
|
|
if (before) {
|
|
if (refNode === parent.firstChild) {
|
|
parent.firstChild = node;
|
|
} else {
|
|
refNode.prev.next = node;
|
|
}
|
|
|
|
node.prev = refNode.prev;
|
|
node.next = refNode;
|
|
refNode.prev = node;
|
|
} else {
|
|
if (refNode === parent.lastChild) {
|
|
parent.lastChild = node;
|
|
} else {
|
|
refNode.next.prev = node;
|
|
}
|
|
|
|
node.next = refNode.next;
|
|
node.prev = refNode;
|
|
refNode.next = node;
|
|
}
|
|
|
|
node.parent = parent;
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Get all children by name.
|
|
*
|
|
* @method getAll
|
|
* @param {String} name Name of the child nodes to collect.
|
|
* @return {Array} Array with child nodes matchin the specified name.
|
|
*/
|
|
getAll: function (name) {
|
|
var self = this, node, collection = [];
|
|
|
|
for (node = self.firstChild; node; node = walk(node, self)) {
|
|
if (node.name === name) {
|
|
collection.push(node);
|
|
}
|
|
}
|
|
|
|
return collection;
|
|
},
|
|
|
|
/**
|
|
* Removes all children of the current node.
|
|
*
|
|
* @method empty
|
|
* @return {tinymce.html.Node} The current node that got cleared.
|
|
*/
|
|
empty: function () {
|
|
var self = this, nodes, i, node;
|
|
|
|
// Remove all children
|
|
if (self.firstChild) {
|
|
nodes = [];
|
|
|
|
// Collect the children
|
|
for (node = self.firstChild; node; node = walk(node, self)) {
|
|
nodes.push(node);
|
|
}
|
|
|
|
// Remove the children
|
|
i = nodes.length;
|
|
while (i--) {
|
|
node = nodes[i];
|
|
node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
|
|
}
|
|
}
|
|
|
|
self.firstChild = self.lastChild = null;
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the node is to be considered empty or not.
|
|
*
|
|
* @example
|
|
* node.isEmpty({img: true});
|
|
* @method isEmpty
|
|
* @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
|
|
* @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables.
|
|
* @return {Boolean} true/false if the node is empty or not.
|
|
*/
|
|
isEmpty: function (elements, whitespace) {
|
|
var self = this, node = self.firstChild, i, name;
|
|
|
|
whitespace = whitespace || {};
|
|
|
|
if (node) {
|
|
do {
|
|
if (node.type === 1) {
|
|
// Ignore bogus elements
|
|
if (node.attributes.map['data-mce-bogus']) {
|
|
continue;
|
|
}
|
|
|
|
// Keep empty elements like <img />
|
|
if (elements[node.name]) {
|
|
return false;
|
|
}
|
|
|
|
// Keep bookmark nodes and name attribute like <a name="1"></a>
|
|
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 <b><p></b></p>. 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('<b>text</b>');
|
|
* @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<b text"
|
|
if (matches.index + matches[0].length > 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 <li> after an open <li>
|
|
if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
|
|
processEndTag(value);
|
|
}
|
|
|
|
// Validate element
|
|
if (!validate || (elementRule = schema.getElementRule(value))) {
|
|
isValidElement = true;
|
|
|
|
// Grab attributes map and patters when validation is enabled
|
|
if (validate) {
|
|
validAttributesMap = elementRule.attributes;
|
|
validAttributePatterns = elementRule.attributePatterns;
|
|
}
|
|
|
|
// Parse attributes
|
|
if ((attribsValue = matches[8])) {
|
|
isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
|
|
|
|
// If the element has internal attributes then remove it if we are told to do so
|
|
if (isInternalElement && removeInternalElements) {
|
|
isValidElement = false;
|
|
}
|
|
|
|
attrList = [];
|
|
attrList.map = {};
|
|
|
|
attribsValue.replace(attrRegExp, parseAttribute);
|
|
} else {
|
|
attrList = [];
|
|
attrList.map = {};
|
|
}
|
|
|
|
// Process attributes if validation is enabled
|
|
if (validate && !isInternalElement) {
|
|
attributesRequired = elementRule.attributesRequired;
|
|
attributesDefault = elementRule.attributesDefault;
|
|
attributesForced = elementRule.attributesForced;
|
|
anyAttributesRequired = elementRule.removeEmptyAttrs;
|
|
|
|
// Check if any attribute exists
|
|
if (anyAttributesRequired && !attrList.length) {
|
|
isValidElement = false;
|
|
}
|
|
|
|
// Handle forced attributes
|
|
if (attributesForced) {
|
|
i = attributesForced.length;
|
|
while (i--) {
|
|
attr = attributesForced[i];
|
|
name = attr.name;
|
|
attrValue = attr.value;
|
|
|
|
if (attrValue === '{$uid}') {
|
|
attrValue = 'mce_' + idCount++;
|
|
}
|
|
|
|
attrList.map[name] = attrValue;
|
|
attrList.push({ name: name, value: attrValue });
|
|
}
|
|
}
|
|
|
|
// Handle default attributes
|
|
if (attributesDefault) {
|
|
i = attributesDefault.length;
|
|
while (i--) {
|
|
attr = attributesDefault[i];
|
|
name = attr.name;
|
|
|
|
if (!(name in attrList.map)) {
|
|
attrValue = attr.value;
|
|
|
|
if (attrValue === '{$uid}') {
|
|
attrValue = 'mce_' + idCount++;
|
|
}
|
|
|
|
attrList.map[name] = attrValue;
|
|
attrList.push({ name: name, value: attrValue });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle required attributes
|
|
if (attributesRequired) {
|
|
i = attributesRequired.length;
|
|
while (i--) {
|
|
if (attributesRequired[i] in attrList.map) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// None of the required attributes where found
|
|
if (i === -1) {
|
|
isValidElement = false;
|
|
}
|
|
}
|
|
|
|
// Invalidate element if it's marked as bogus
|
|
if ((attr = attrList.map['data-mce-bogus'])) {
|
|
if (attr === 'all') {
|
|
index = findEndTag(schema, html, tokenRegExp.lastIndex);
|
|
tokenRegExp.lastIndex = index;
|
|
continue;
|
|
}
|
|
|
|
isValidElement = false;
|
|
}
|
|
}
|
|
|
|
if (isValidElement) {
|
|
self.start(value, attrList, isShortEnded);
|
|
}
|
|
} else {
|
|
isValidElement = false;
|
|
}
|
|
|
|
// Treat script, noscript and style a bit different since they may include code that looks like elements
|
|
if ((endRegExp = specialElements[value])) {
|
|
endRegExp.lastIndex = index = matches.index + matches[0].length;
|
|
|
|
if ((matches = endRegExp.exec(html))) {
|
|
if (isValidElement) {
|
|
text = html.substr(index, matches.index - index);
|
|
}
|
|
|
|
index = matches.index + matches[0].length;
|
|
} else {
|
|
text = html.substr(index);
|
|
index = html.length;
|
|
}
|
|
|
|
if (isValidElement) {
|
|
if (text.length > 0) {
|
|
self.text(text, true);
|
|
}
|
|
|
|
self.end(value);
|
|
}
|
|
|
|
tokenRegExp.lastIndex = index;
|
|
continue;
|
|
}
|
|
|
|
// Push value on to stack
|
|
if (!isShortEnded) {
|
|
if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) {
|
|
stack.push({ name: value, valid: isValidElement });
|
|
} else if (isValidElement) {
|
|
self.end(value);
|
|
}
|
|
}
|
|
} else if ((value = matches[1])) { // Comment
|
|
// Padd comment value to avoid browsers from parsing invalid comments as HTML
|
|
if (value.charAt(0) === '>') {
|
|
value = ' ' + value;
|
|
}
|
|
|
|
if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') {
|
|
value = ' ' + value;
|
|
}
|
|
|
|
self.comment(value);
|
|
} else if ((value = matches[2])) { // CDATA
|
|
self.cdata(value);
|
|
} else if ((value = matches[3])) { // DOCTYPE
|
|
self.doctype(value);
|
|
} else if ((value = matches[4])) { // PI
|
|
self.pi(value, matches[5]);
|
|
}
|
|
|
|
index = matches.index + matches[0].length;
|
|
}
|
|
|
|
// Text
|
|
if (index < html.length) {
|
|
self.text(decode(html.substr(index)));
|
|
}
|
|
|
|
// Close any open elements
|
|
for (i = stack.length - 1; i >= 0; i--) {
|
|
value = stack[i];
|
|
|
|
if (value.valid) {
|
|
self.end(value.name);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
SaxParser.findEndTag = findEndTag;
|
|
|
|
return SaxParser;
|
|
}
|
|
);
|
|
/**
|
|
* DomParser.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 parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
|
|
* sure that the node tree is valid according to the specified schema.
|
|
* So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
|
|
*
|
|
* @example
|
|
* var parser = new tinymce.html.DomParser({validate: true}, schema);
|
|
* var rootNode = parser.parse('<h1>content</h1>');
|
|
*
|
|
* @class tinymce.html.DomParser
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.DomParser',
|
|
[
|
|
"tinymce.core.html.Node",
|
|
"tinymce.core.html.Schema",
|
|
"tinymce.core.html.SaxParser",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Node, Schema, SaxParser, Tools) {
|
|
var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
|
|
|
|
var paddEmptyNode = function (settings, node) {
|
|
if (settings.padd_empty_with_br) {
|
|
node.empty().append(new Node('br', '1')).shortEnded = true;
|
|
} else {
|
|
node.empty().append(new Node('#text', '3')).value = '\u00a0';
|
|
}
|
|
};
|
|
|
|
var hasOnlyChild = function (node, name) {
|
|
return node && node.firstChild === node.lastChild && node.firstChild.name === name;
|
|
};
|
|
|
|
/**
|
|
* Constructs a new DomParser instance.
|
|
*
|
|
* @constructor
|
|
* @method DomParser
|
|
* @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.
|
|
*/
|
|
return function (settings, schema) {
|
|
var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
|
|
|
|
settings = settings || {};
|
|
settings.validate = "validate" in settings ? settings.validate : true;
|
|
settings.root_name = settings.root_name || 'body';
|
|
self.schema = schema = schema || new Schema();
|
|
|
|
function fixInvalidChildren(nodes) {
|
|
var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
|
|
var nonEmptyElements, whitespaceElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode;
|
|
|
|
nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
|
|
nonEmptyElements = schema.getNonEmptyElements();
|
|
whitespaceElements = schema.getWhiteSpaceElements();
|
|
textBlockElements = schema.getTextBlockElements();
|
|
specialElements = schema.getSpecialElements();
|
|
|
|
for (ni = 0; ni < nodes.length; ni++) {
|
|
node = nodes[ni];
|
|
|
|
// Already removed or fixed
|
|
if (!node.parent || node.fixed) {
|
|
continue;
|
|
}
|
|
|
|
// If the invalid element is a text block and the text block is within a parent LI element
|
|
// Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
|
|
if (textBlockElements[node.name] && node.parent.name == 'li') {
|
|
// Move sibling text blocks after LI element
|
|
sibling = node.next;
|
|
while (sibling) {
|
|
if (textBlockElements[sibling.name]) {
|
|
sibling.name = 'li';
|
|
sibling.fixed = true;
|
|
node.parent.insert(sibling, node.parent);
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
sibling = sibling.next;
|
|
}
|
|
|
|
// Unwrap current text block
|
|
node.unwrap(node);
|
|
continue;
|
|
}
|
|
|
|
// Get list of all parent nodes until we find a valid parent to stick the child into
|
|
parents = [node];
|
|
for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
|
|
!nonSplitableElements[parent.name]; parent = parent.parent) {
|
|
parents.push(parent);
|
|
}
|
|
|
|
// Found a suitable parent
|
|
if (parent && parents.length > 1) {
|
|
// Reverse the array since it makes looping easier
|
|
parents.reverse();
|
|
|
|
// Clone the related parent and insert that after the moved node
|
|
newParent = currentNode = self.filterNode(parents[0].clone());
|
|
|
|
// Start cloning and moving children on the left side of the target node
|
|
for (i = 0; i < parents.length - 1; i++) {
|
|
if (schema.isValidChild(currentNode.name, parents[i].name)) {
|
|
tempNode = self.filterNode(parents[i].clone());
|
|
currentNode.append(tempNode);
|
|
} else {
|
|
tempNode = currentNode;
|
|
}
|
|
|
|
for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) {
|
|
nextNode = childNode.next;
|
|
tempNode.append(childNode);
|
|
childNode = nextNode;
|
|
}
|
|
|
|
currentNode = tempNode;
|
|
}
|
|
|
|
if (!newParent.isEmpty(nonEmptyElements, whitespaceElements)) {
|
|
parent.insert(newParent, parents[0], true);
|
|
parent.insert(node, newParent);
|
|
} else {
|
|
parent.insert(node, parents[0], true);
|
|
}
|
|
|
|
// Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
|
|
parent = parents[0];
|
|
if (parent.isEmpty(nonEmptyElements, whitespaceElements) || hasOnlyChild(parent, 'br')) {
|
|
parent.empty().remove();
|
|
}
|
|
} else if (node.parent) {
|
|
// If it's an LI try to find a UL/OL for it or wrap it
|
|
if (node.name === 'li') {
|
|
sibling = node.prev;
|
|
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
|
|
sibling.append(node);
|
|
continue;
|
|
}
|
|
|
|
sibling = node.next;
|
|
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
|
|
sibling.insert(node, sibling.firstChild, true);
|
|
continue;
|
|
}
|
|
|
|
node.wrap(self.filterNode(new Node('ul', 1)));
|
|
continue;
|
|
}
|
|
|
|
// Try wrapping the element in a DIV
|
|
if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
|
|
node.wrap(self.filterNode(new Node('div', 1)));
|
|
} else {
|
|
// We failed wrapping it, then remove or unwrap it
|
|
if (specialElements[node.name]) {
|
|
node.empty().remove();
|
|
} else {
|
|
node.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs the specified node though the element and attributes filters.
|
|
*
|
|
* @method filterNode
|
|
* @param {tinymce.html.Node} Node the node to run filters on.
|
|
* @return {tinymce.html.Node} The passed in node.
|
|
*/
|
|
self.filterNode = function (node) {
|
|
var i, name, list;
|
|
|
|
// Run element filters
|
|
if (name in nodeFilters) {
|
|
list = matchedNodes[name];
|
|
|
|
if (list) {
|
|
list.push(node);
|
|
} else {
|
|
matchedNodes[name] = [node];
|
|
}
|
|
}
|
|
|
|
// Run attribute filters
|
|
i = attributeFilters.length;
|
|
while (i--) {
|
|
name = attributeFilters[i].name;
|
|
|
|
if (name in node.attributes.map) {
|
|
list = matchedAttributes[name];
|
|
|
|
if (list) {
|
|
list.push(node);
|
|
} else {
|
|
matchedAttributes[name] = [node];
|
|
}
|
|
}
|
|
}
|
|
|
|
return node;
|
|
};
|
|
|
|
/**
|
|
* Adds a node filter function to the parser, the parser will collect the specified nodes by name
|
|
* and then execute the callback ones it has finished parsing the document.
|
|
*
|
|
* @example
|
|
* parser.addNodeFilter('p,h1', function(nodes, name) {
|
|
* for (var i = 0; i < nodes.length; i++) {
|
|
* console.log(nodes[i].name);
|
|
* }
|
|
* });
|
|
* @method addNodeFilter
|
|
* @method {String} name Comma separated list of nodes to collect.
|
|
* @param {function} callback Callback function to execute once it has collected nodes.
|
|
*/
|
|
self.addNodeFilter = function (name, callback) {
|
|
each(explode(name), function (name) {
|
|
var list = nodeFilters[name];
|
|
|
|
if (!list) {
|
|
nodeFilters[name] = list = [];
|
|
}
|
|
|
|
list.push(callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
|
|
* and then execute the callback ones it has finished parsing the document.
|
|
*
|
|
* @example
|
|
* parser.addAttributeFilter('src,href', function(nodes, name) {
|
|
* for (var i = 0; i < nodes.length; i++) {
|
|
* console.log(nodes[i].name);
|
|
* }
|
|
* });
|
|
* @method addAttributeFilter
|
|
* @method {String} name Comma separated list of nodes to collect.
|
|
* @param {function} callback Callback function to execute once it has collected nodes.
|
|
*/
|
|
self.addAttributeFilter = function (name, callback) {
|
|
each(explode(name), function (name) {
|
|
var i;
|
|
|
|
for (i = 0; i < attributeFilters.length; i++) {
|
|
if (attributeFilters[i].name === name) {
|
|
attributeFilters[i].callbacks.push(callback);
|
|
return;
|
|
}
|
|
}
|
|
|
|
attributeFilters.push({ name: name, callbacks: [callback] });
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Parses the specified HTML string into a DOM like node tree and returns the result.
|
|
*
|
|
* @example
|
|
* var rootNode = new DomParser({...}).parse('<b>text</b>');
|
|
* @method parse
|
|
* @param {String} html Html string to sax parse.
|
|
* @param {Object} args Optional args object that gets passed to all filter functions.
|
|
* @return {tinymce.html.Node} Root node containing the tree.
|
|
*/
|
|
self.parse = function (html, args) {
|
|
var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
|
|
var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
|
|
var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
|
|
var children, nonEmptyElements, rootBlockName;
|
|
|
|
args = args || {};
|
|
matchedNodes = {};
|
|
matchedAttributes = {};
|
|
blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
|
|
nonEmptyElements = schema.getNonEmptyElements();
|
|
children = schema.children;
|
|
validate = settings.validate;
|
|
rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
|
|
|
|
whiteSpaceElements = schema.getWhiteSpaceElements();
|
|
startWhiteSpaceRegExp = /^[ \t\r\n]+/;
|
|
endWhiteSpaceRegExp = /[ \t\r\n]+$/;
|
|
allWhiteSpaceRegExp = /[ \t\r\n]+/g;
|
|
isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
|
|
|
|
function addRootBlocks() {
|
|
var node = rootNode.firstChild, next, rootBlockNode;
|
|
|
|
// Removes whitespace at beginning and end of block so:
|
|
// <p> x </p> -> <p>x</p>
|
|
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 not <br /> or <img />
|
|
if (!empty) {
|
|
node = newNode;
|
|
}
|
|
|
|
// Check if we are inside a whitespace preserved element
|
|
if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
|
isInWhiteSpacePreservedElement = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
end: function (name) {
|
|
var textNode, elementRule, text, sibling, tempNode;
|
|
|
|
elementRule = validate ? schema.getElementRule(name) : {};
|
|
if (elementRule) {
|
|
if (blockElements[name]) {
|
|
if (!isInWhiteSpacePreservedElement) {
|
|
// Trim whitespace of the first node in a block
|
|
textNode = node.firstChild;
|
|
if (textNode && textNode.type === 3) {
|
|
text = textNode.value.replace(startWhiteSpaceRegExp, '');
|
|
|
|
// Any characters left after trim or should we remove it
|
|
if (text.length > 0) {
|
|
textNode.value = text;
|
|
textNode = textNode.next;
|
|
} else {
|
|
sibling = textNode.next;
|
|
textNode.remove();
|
|
textNode = sibling;
|
|
|
|
// Remove any pure whitespace siblings
|
|
while (textNode && textNode.type === 3) {
|
|
text = textNode.value;
|
|
sibling = textNode.next;
|
|
|
|
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
|
textNode.remove();
|
|
textNode = sibling;
|
|
}
|
|
|
|
textNode = sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trim whitespace of the last node in a block
|
|
textNode = node.lastChild;
|
|
if (textNode && textNode.type === 3) {
|
|
text = textNode.value.replace(endWhiteSpaceRegExp, '');
|
|
|
|
// Any characters left after trim or should we remove it
|
|
if (text.length > 0) {
|
|
textNode.value = text;
|
|
textNode = textNode.prev;
|
|
} else {
|
|
sibling = textNode.prev;
|
|
textNode.remove();
|
|
textNode = sibling;
|
|
|
|
// Remove any pure whitespace siblings
|
|
while (textNode && textNode.type === 3) {
|
|
text = textNode.value;
|
|
sibling = textNode.prev;
|
|
|
|
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
|
textNode.remove();
|
|
textNode = sibling;
|
|
}
|
|
|
|
textNode = sibling;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trim start white space
|
|
// Removed due to: #5424
|
|
/*textNode = node.prev;
|
|
if (textNode && textNode.type === 3) {
|
|
text = textNode.value.replace(startWhiteSpaceRegExp, '');
|
|
|
|
if (text.length > 0)
|
|
textNode.value = text;
|
|
else
|
|
textNode.remove();
|
|
}*/
|
|
}
|
|
|
|
// Check if we exited a whitespace preserved element
|
|
if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
|
isInWhiteSpacePreservedElement = false;
|
|
}
|
|
|
|
// Handle empty nodes
|
|
if (elementRule.removeEmpty || elementRule.paddEmpty) {
|
|
if (node.isEmpty(nonEmptyElements, whiteSpaceElements)) {
|
|
if (elementRule.paddEmpty) {
|
|
paddEmptyNode(settings, node);
|
|
} else {
|
|
// Leave nodes that have a name like <a name="name">
|
|
if (!node.attributes.map.name && !node.attributes.map.id) {
|
|
tempNode = node.parent;
|
|
|
|
if (blockElements[node.name]) {
|
|
node.empty().remove();
|
|
} else {
|
|
node.unwrap();
|
|
}
|
|
|
|
node = tempNode;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
node = node.parent;
|
|
}
|
|
}
|
|
}, schema);
|
|
|
|
rootNode = node = new Node(args.context || settings.root_name, 11);
|
|
|
|
parser.parse(html);
|
|
|
|
// Fix invalid children or report invalid children in a contextual parsing
|
|
if (validate && invalidChildren.length) {
|
|
if (!args.context) {
|
|
fixInvalidChildren(invalidChildren);
|
|
} else {
|
|
args.invalid = true;
|
|
}
|
|
}
|
|
|
|
// Wrap nodes in the root into block elements if the root is body
|
|
if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
|
|
addRootBlocks();
|
|
}
|
|
|
|
// Run filters only when the contents is valid
|
|
if (!args.invalid) {
|
|
// Run node filters
|
|
for (name in matchedNodes) {
|
|
list = nodeFilters[name];
|
|
nodes = matchedNodes[name];
|
|
|
|
// Remove already removed children
|
|
fi = nodes.length;
|
|
while (fi--) {
|
|
if (!nodes[fi].parent) {
|
|
nodes.splice(fi, 1);
|
|
}
|
|
}
|
|
|
|
for (i = 0, l = list.length; i < l; i++) {
|
|
list[i](nodes, name, args);
|
|
}
|
|
}
|
|
|
|
// Run attribute filters
|
|
for (i = 0, l = attributeFilters.length; i < l; i++) {
|
|
list = attributeFilters[i];
|
|
|
|
if (list.name in matchedAttributes) {
|
|
nodes = matchedAttributes[list.name];
|
|
|
|
// Remove already removed children
|
|
fi = nodes.length;
|
|
while (fi--) {
|
|
if (!nodes[fi].parent) {
|
|
nodes.splice(fi, 1);
|
|
}
|
|
}
|
|
|
|
for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
|
|
list.callbacks[fi](nodes, list.name, args);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return rootNode;
|
|
};
|
|
|
|
// Remove <br> at end of block elements Gecko and WebKit injects BR elements to
|
|
// make it possible to place the caret inside empty blocks. This logic tries to remove
|
|
// these elements and keep br elements that where intended to be there intact
|
|
if (settings.remove_trailing_brs) {
|
|
self.addNodeFilter('br', function (nodes) {
|
|
var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
|
|
var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
|
|
var whiteSpaceElements = schema.getNonEmptyElements();
|
|
var elementRule, textNode;
|
|
|
|
// Remove brs from body element as well
|
|
blockElements.body = 1;
|
|
|
|
// Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
|
|
for (i = 0; i < l; i++) {
|
|
node = nodes[i];
|
|
parent = node.parent;
|
|
|
|
if (blockElements[node.parent.name] && node === parent.lastChild) {
|
|
// Loop all nodes to the left of the current node and check for other BR elements
|
|
// excluding bookmarks since they are invisible
|
|
prev = node.prev;
|
|
while (prev) {
|
|
prevName = prev.name;
|
|
|
|
// Ignore bookmarks
|
|
if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
|
|
// Found a non BR element
|
|
if (prevName !== "br") {
|
|
break;
|
|
}
|
|
|
|
// Found another br it's a <br><br> structure then don't remove anything
|
|
if (prevName === 'br') {
|
|
node = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
prev = prev.prev;
|
|
}
|
|
|
|
if (node) {
|
|
node.remove();
|
|
|
|
// Is the parent to be considered empty after we removed the BR
|
|
if (parent.isEmpty(nonEmptyElements, whiteSpaceElements)) {
|
|
elementRule = schema.getElementRule(parent.name);
|
|
|
|
// Remove or padd the element depending on schema rule
|
|
if (elementRule) {
|
|
if (elementRule.removeEmpty) {
|
|
parent.remove();
|
|
} else if (elementRule.paddEmpty) {
|
|
paddEmptyNode(settings, parent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
|
|
// so they become <p><b><i> </i></b></p>
|
|
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('<p><br></p>');
|
|
* console.log(writer.getContent());
|
|
*
|
|
* @class tinymce.html.Writer
|
|
* @version 3.4
|
|
*/
|
|
define(
|
|
'tinymce.core.html.Writer',
|
|
[
|
|
"tinymce.core.html.Entities",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Entities, Tools) {
|
|
var makeMap = Tools.makeMap;
|
|
|
|
/**
|
|
* Constructs a new Writer instance.
|
|
*
|
|
* @constructor
|
|
* @method Writer
|
|
* @param {Object} settings Name/value settings object.
|
|
*/
|
|
return function (settings) {
|
|
var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
|
|
|
|
settings = settings || {};
|
|
indent = settings.indent;
|
|
indentBefore = makeMap(settings.indent_before || '');
|
|
indentAfter = makeMap(settings.indent_after || '');
|
|
encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
|
|
htmlOutput = settings.element_format == "html";
|
|
|
|
return {
|
|
/**
|
|
* Writes the a start element such as <p id="a">.
|
|
*
|
|
* @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 <br />.
|
|
*/
|
|
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 </p>.
|
|
*
|
|
* @method end
|
|
* @param {String} name Name of the element.
|
|
*/
|
|
end: function (name) {
|
|
var 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 (indent && indentAfter[name] && html.length > 0) {
|
|
value = html[html.length - 1];
|
|
|
|
if (value.length > 0 && value !== '\n') {
|
|
html.push('\n');
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Writes a text node.
|
|
*
|
|
* @method text
|
|
* @param {String} text String to write out.
|
|
* @param {Boolean} raw Optional raw state if true the contents wont get encoded.
|
|
*/
|
|
text: function (text, raw) {
|
|
if (text.length > 0) {
|
|
html[html.length] = raw ? text : encode(text);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Writes a cdata node such as <![CDATA[data]]>.
|
|
*
|
|
* @method cdata
|
|
* @param {String} text String to write out inside the cdata.
|
|
*/
|
|
cdata: function (text) {
|
|
html.push('<![CDATA[', text, ']]>');
|
|
},
|
|
|
|
/**
|
|
* Writes a comment node such as <!-- Comment -->.
|
|
*
|
|
* @method cdata
|
|
* @param {String} text String to write out inside the comment.
|
|
*/
|
|
comment: function (text) {
|
|
html.push('<!--', text, '-->');
|
|
},
|
|
|
|
/**
|
|
* Writes a PI node such as <?xml attr="value" ?>.
|
|
*
|
|
* @method pi
|
|
* @param {String} name Name of the pi.
|
|
* @param {String} text String to write out inside the pi.
|
|
*/
|
|
pi: function (name, text) {
|
|
if (text) {
|
|
html.push('<?', name, ' ', encode(text), '?>');
|
|
} else {
|
|
html.push('<?', name, '?>');
|
|
}
|
|
|
|
if (indent) {
|
|
html.push('\n');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Writes a doctype node such as <!DOCTYPE data>.
|
|
*
|
|
* @method doctype
|
|
* @param {String} text String to write out inside the doctype.
|
|
*/
|
|
doctype: function (text) {
|
|
html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
|
|
},
|
|
|
|
/**
|
|
* Resets the internal buffer if one wants to reuse the writer.
|
|
*
|
|
* @method reset
|
|
*/
|
|
reset: function () {
|
|
html.length = 0;
|
|
},
|
|
|
|
/**
|
|
* Returns the contents that got serialized.
|
|
*
|
|
* @method getContent
|
|
* @return {String} HTML contents that got written down.
|
|
*/
|
|
getContent: function () {
|
|
return html.join('').replace(/\n$/, '');
|
|
}
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* 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 down the DOM tree into a string using a Writer instance.
|
|
*
|
|
*
|
|
* @example
|
|
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
|
|
* @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('<p>text</p>'));
|
|
* @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: <body>text</body> becomes <body>text<br><br></body>
|
|
*/
|
|
function trimTrailingBr(rootNode) {
|
|
var brNode1, brNode2;
|
|
|
|
function isBr(node) {
|
|
return node && node.name === 'br';
|
|
}
|
|
|
|
brNode1 = rootNode.lastChild;
|
|
if (isBr(brNode1)) {
|
|
brNode2 = brNode1.prev;
|
|
|
|
if (isBr(brNode2)) {
|
|
brNode1.remove();
|
|
brNode2.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs a new DOM serializer class.
|
|
*
|
|
* @constructor
|
|
* @method Serializer
|
|
* @param {Object} settings Serializer settings object.
|
|
* @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from.
|
|
*/
|
|
return function (settings, editor) {
|
|
var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"];
|
|
|
|
if (editor) {
|
|
dom = editor.dom;
|
|
schema = editor.schema;
|
|
}
|
|
|
|
function trimHtml(html) {
|
|
var trimContentRegExp = new RegExp([
|
|
'<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
|
|
'\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
|
|
].join('|'), 'gi');
|
|
|
|
html = Zwsp.trim(html.replace(trimContentRegExp, ''));
|
|
|
|
return html;
|
|
}
|
|
|
|
function trimContent(html) {
|
|
var content = html;
|
|
var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g;
|
|
var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema;
|
|
|
|
content = trimHtml(content);
|
|
shortEndedElements = schema.getShortEndedElements();
|
|
|
|
// Remove all bogus elements marked with "all"
|
|
while ((matches = bogusAllRegExp.exec(content))) {
|
|
index = bogusAllRegExp.lastIndex;
|
|
matchLength = matches[0].length;
|
|
|
|
if (shortEndedElements[matches[1]]) {
|
|
endTagIndex = index;
|
|
} else {
|
|
endTagIndex = SaxParser.findEndTag(schema, content, index);
|
|
}
|
|
|
|
content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
|
|
bogusAllRegExp.lastIndex = index - matchLength;
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Returns a trimmed version of the editor contents to be used for the undo level. This
|
|
* will remove any data-mce-bogus="all" marked elements since these are used for UI it will also
|
|
* remove the data-mce-selected attributes used for selection of objects and caret containers.
|
|
* It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will
|
|
* be removed by the serialization logic when you save.
|
|
*
|
|
* @private
|
|
* @return {String} HTML contents of the editor excluding some internal bogus elements.
|
|
*/
|
|
function getTrimmedContent() {
|
|
return trimContent(editor.getBody().innerHTML);
|
|
}
|
|
|
|
function addTempAttr(name) {
|
|
if (Tools.inArray(tempAttrs, name) === -1) {
|
|
htmlParser.addAttributeFilter(name, function (nodes, name) {
|
|
var i = nodes.length;
|
|
|
|
while (i--) {
|
|
nodes[i].attr(name, null);
|
|
}
|
|
});
|
|
|
|
tempAttrs.push(name);
|
|
}
|
|
}
|
|
|
|
// Default DOM and Schema if they are undefined
|
|
dom = dom || DOM;
|
|
schema = schema || new Schema(settings);
|
|
settings.entity_encoding = settings.entity_encoding || 'named';
|
|
settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true;
|
|
|
|
htmlParser = new DomParser(settings, schema);
|
|
|
|
// Convert tabindex back to elements when serializing contents
|
|
htmlParser.addAttributeFilter('data-mce-tabindex', function (nodes, name) {
|
|
var i = nodes.length, node;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
node.attr('tabindex', node.attributes.map['data-mce-tabindex']);
|
|
node.attr(name, null);
|
|
}
|
|
});
|
|
|
|
// Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed
|
|
htmlParser.addAttributeFilter('src,href,style', function (nodes, name) {
|
|
var i = nodes.length, node, value, internalName = 'data-mce-' + name;
|
|
var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
|
|
value = node.attributes.map[internalName];
|
|
if (value !== undef) {
|
|
// Set external name to internal value and remove internal
|
|
node.attr(name, value.length > 0 ? value : null);
|
|
node.attr(internalName, null);
|
|
} else {
|
|
// No internal attribute found then convert the value we have in the DOM
|
|
value = node.attributes.map[name];
|
|
|
|
if (name === "style") {
|
|
value = dom.serializeStyle(dom.parseStyle(value), node.name);
|
|
} else if (urlConverter) {
|
|
value = urlConverter.call(urlConverterScope, value, name, node.name);
|
|
}
|
|
|
|
node.attr(name, value.length > 0 ? value : null);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove internal classes mceItem<..> or mceSelected
|
|
htmlParser.addAttributeFilter('class', function (nodes) {
|
|
var i = nodes.length, node, value;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
value = node.attr('class');
|
|
|
|
if (value) {
|
|
value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
|
|
node.attr('class', value.length > 0 ? value : null);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove bookmark elements
|
|
htmlParser.addAttributeFilter('data-mce-type', function (nodes, name, args) {
|
|
var i = nodes.length, node;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
|
|
if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
|
|
node.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
htmlParser.addNodeFilter('noscript', function (nodes) {
|
|
var i = nodes.length, node;
|
|
|
|
while (i--) {
|
|
node = nodes[i].firstChild;
|
|
|
|
if (node) {
|
|
node.value = Entities.decode(node.value);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Force script into CDATA sections and remove the mce- prefix also add comments around styles
|
|
htmlParser.addNodeFilter('script,style', function (nodes, name) {
|
|
var i = nodes.length, node, value, type;
|
|
|
|
function trim(value) {
|
|
/*jshint maxlen:255 */
|
|
/*eslint max-len:0 */
|
|
return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
|
|
.replace(/^[\r\n]*|[\r\n]*$/g, '')
|
|
.replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
|
|
.replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
|
|
}
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
value = node.firstChild ? node.firstChild.value : '';
|
|
|
|
if (name === "script") {
|
|
// Remove mce- prefix from script elements and remove default type since the user specified
|
|
// a script element without type attribute
|
|
type = node.attr('type');
|
|
if (type) {
|
|
node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, ''));
|
|
}
|
|
|
|
if (value.length > 0) {
|
|
node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
|
|
}
|
|
} else {
|
|
if (value.length > 0) {
|
|
node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Convert comments to cdata and handle protected comments
|
|
htmlParser.addNodeFilter('#comment', function (nodes) {
|
|
var i = nodes.length, node;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
|
|
if (node.value.indexOf('[CDATA[') === 0) {
|
|
node.name = '#cdata';
|
|
node.type = 4;
|
|
node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, '');
|
|
} else if (node.value.indexOf('mce:protected ') === 0) {
|
|
node.name = "#text";
|
|
node.type = 3;
|
|
node.raw = true;
|
|
node.value = unescape(node.value).substr(14);
|
|
}
|
|
}
|
|
});
|
|
|
|
htmlParser.addNodeFilter('xml:namespace,input', function (nodes, name) {
|
|
var i = nodes.length, node;
|
|
|
|
while (i--) {
|
|
node = nodes[i];
|
|
if (node.type === 7) {
|
|
node.remove();
|
|
} else if (node.type === 1) {
|
|
if (name === "input" && !("type" in node.attributes.map)) {
|
|
node.attr('type', 'text');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove internal data attributes
|
|
htmlParser.addAttributeFilter(
|
|
'data-mce-src,data-mce-href,data-mce-style,' +
|
|
'data-mce-selected,data-mce-expando,' +
|
|
'data-mce-type,data-mce-resize',
|
|
|
|
function (nodes, name) {
|
|
var i = nodes.length;
|
|
|
|
while (i--) {
|
|
nodes[i].attr(name, null);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Return public methods
|
|
return {
|
|
/**
|
|
* Schema instance that was used to when the Serializer was constructed.
|
|
*
|
|
* @field {tinymce.html.Schema} schema
|
|
*/
|
|
schema: schema,
|
|
|
|
/**
|
|
* Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name
|
|
* and then execute the callback ones it has finished parsing the document.
|
|
*
|
|
* @example
|
|
* parser.addNodeFilter('p,h1', function(nodes, name) {
|
|
* for (var i = 0; i < nodes.length; i++) {
|
|
* console.log(nodes[i].name);
|
|
* }
|
|
* });
|
|
* @method addNodeFilter
|
|
* @method {String} name Comma separated list of nodes to collect.
|
|
* @param {function} callback Callback function to execute once it has collected nodes.
|
|
*/
|
|
addNodeFilter: htmlParser.addNodeFilter,
|
|
|
|
/**
|
|
* Adds a attribute filter function to the parser used by the serializer, the parser will
|
|
* collect nodes that has the specified attributes
|
|
* and then execute the callback ones it has finished parsing the document.
|
|
*
|
|
* @example
|
|
* parser.addAttributeFilter('src,href', function(nodes, name) {
|
|
* for (var i = 0; i < nodes.length; i++) {
|
|
* console.log(nodes[i].name);
|
|
* }
|
|
* });
|
|
* @method addAttributeFilter
|
|
* @method {String} name Comma separated list of nodes to collect.
|
|
* @param {function} callback Callback function to execute once it has collected nodes.
|
|
*/
|
|
addAttributeFilter: htmlParser.addAttributeFilter,
|
|
|
|
/**
|
|
* Serializes the specified browser DOM node into a HTML string.
|
|
*
|
|
* @method serialize
|
|
* @param {DOMNode} node DOM node to serialize.
|
|
* @param {Object} args Arguments option that gets passed to event handlers.
|
|
*/
|
|
serialize: function (node, args) {
|
|
var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode;
|
|
|
|
// Explorer won't clone contents of script and style and the
|
|
// selected index of select elements are cleared on a clone operation.
|
|
if (Env.ie && dom.select('script,style,select,map').length > 0) {
|
|
content = node.innerHTML;
|
|
node = node.cloneNode(false);
|
|
dom.setHTML(node, content);
|
|
} else {
|
|
node = node.cloneNode(true);
|
|
}
|
|
|
|
// Nodes needs to be attached to something in WebKit/Opera
|
|
// This fix will make DOM ranges and make Sizzle happy!
|
|
impl = document.implementation;
|
|
if (impl.createHTMLDocument) {
|
|
// Create an empty HTML document
|
|
doc = impl.createHTMLDocument("");
|
|
|
|
// Add the element or it's children if it's a body element to the new document
|
|
each(node.nodeName == 'BODY' ? node.childNodes : [node], function (node) {
|
|
doc.body.appendChild(doc.importNode(node, true));
|
|
});
|
|
|
|
// Grab first child or body element for serialization
|
|
if (node.nodeName != 'BODY') {
|
|
node = doc.body.firstChild;
|
|
} else {
|
|
node = doc.body;
|
|
}
|
|
|
|
// set the new document in DOMUtils so createElement etc works
|
|
oldDoc = dom.doc;
|
|
dom.doc = doc;
|
|
}
|
|
|
|
args = args || {};
|
|
args.format = args.format || 'html';
|
|
|
|
// Don't wrap content if we want selected html
|
|
if (args.selection) {
|
|
args.forced_root_block = '';
|
|
}
|
|
|
|
// Pre process
|
|
if (!args.no_events) {
|
|
args.node = node;
|
|
self.onPreProcess(args);
|
|
}
|
|
|
|
// Parse HTML
|
|
rootNode = htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args);
|
|
trimTrailingBr(rootNode);
|
|
|
|
// Serialize HTML
|
|
htmlSerializer = new Serializer(settings, schema);
|
|
args.content = htmlSerializer.serialize(rootNode);
|
|
|
|
// Replace all BOM characters for now until we can find a better solution
|
|
if (!args.cleanup) {
|
|
args.content = Zwsp.trim(args.content);
|
|
args.content = args.content.replace(/\uFEFF/g, '');
|
|
}
|
|
|
|
// Post process
|
|
if (!args.no_events) {
|
|
self.onPostProcess(args);
|
|
}
|
|
|
|
// Restore the old document if it was changed
|
|
if (oldDoc) {
|
|
dom.doc = oldDoc;
|
|
}
|
|
|
|
args.node = null;
|
|
|
|
return args.content;
|
|
},
|
|
|
|
/**
|
|
* Adds valid elements rules to the serializers schema instance this enables you to specify things
|
|
* like what elements should be outputted and what attributes specific elements might have.
|
|
* Consult the Wiki for more details on this format.
|
|
*
|
|
* @method addRules
|
|
* @param {String} rules Valid elements rules string to add to schema.
|
|
*/
|
|
addRules: function (rules) {
|
|
schema.addValidElements(rules);
|
|
},
|
|
|
|
/**
|
|
* Sets the valid elements rules to the serializers schema instance this enables you to specify things
|
|
* like what elements should be outputted and what attributes specific elements might have.
|
|
* Consult the Wiki for more details on this format.
|
|
*
|
|
* @method setRules
|
|
* @param {String} rules Valid elements rules string.
|
|
*/
|
|
setRules: function (rules) {
|
|
schema.setValidElements(rules);
|
|
},
|
|
|
|
onPreProcess: function (args) {
|
|
if (editor) {
|
|
editor.fire('PreProcess', args);
|
|
}
|
|
},
|
|
|
|
onPostProcess: function (args) {
|
|
if (editor) {
|
|
editor.fire('PostProcess', args);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds a temporary internal attribute these attributes will get removed on undo and
|
|
* when getting contents out of the editor.
|
|
*
|
|
* @method addTempAttr
|
|
* @param {String} name string
|
|
*/
|
|
addTempAttr: addTempAttr,
|
|
|
|
// Internal
|
|
trimHtml: trimHtml,
|
|
getTrimmedContent: getTrimmedContent,
|
|
trimContent: trimContent
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* VK.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 file exposes a set of the common KeyCodes for use. Please grow it as needed.
|
|
*/
|
|
define(
|
|
'tinymce.core.util.VK',
|
|
[
|
|
"tinymce.core.Env"
|
|
],
|
|
function (Env) {
|
|
return {
|
|
BACKSPACE: 8,
|
|
DELETE: 46,
|
|
DOWN: 40,
|
|
ENTER: 13,
|
|
LEFT: 37,
|
|
RIGHT: 39,
|
|
SPACEBAR: 32,
|
|
TAB: 9,
|
|
UP: 38,
|
|
|
|
modifierPressed: function (e) {
|
|
return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e);
|
|
},
|
|
|
|
metaKeyPressed: function (e) {
|
|
// Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states
|
|
return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey);
|
|
}
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ControlSelection.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 control selection of elements. Controls are elements
|
|
* that can be resized and needs to be selected as a whole. It adds custom resize handles
|
|
* to all browser engines that support properly disabling the built in resize logic.
|
|
*
|
|
* @class tinymce.dom.ControlSelection
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.ControlSelection',
|
|
[
|
|
"tinymce.core.util.VK",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.util.Delay",
|
|
"tinymce.core.Env",
|
|
"tinymce.core.dom.NodeType"
|
|
],
|
|
function (VK, Tools, Delay, Env, NodeType) {
|
|
var isContentEditableFalse = NodeType.isContentEditableFalse;
|
|
var isContentEditableTrue = NodeType.isContentEditableTrue;
|
|
|
|
function getContentEditableRoot(root, node) {
|
|
while (node && node != root) {
|
|
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return function (selection, editor) {
|
|
var dom = editor.dom, each = Tools.each;
|
|
var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent;
|
|
var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
|
|
var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11;
|
|
var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight;
|
|
|
|
// Details about each resize handle how to scale etc
|
|
resizeHandles = {
|
|
// Name: x multiplier, y multiplier, delta size x, delta size y
|
|
/*n: [0.5, 0, 0, -1],
|
|
e: [1, 0.5, 1, 0],
|
|
s: [0.5, 1, 0, 1],
|
|
w: [0, 0.5, -1, 0],*/
|
|
nw: [0, 0, -1, -1],
|
|
ne: [1, 0, 1, -1],
|
|
se: [1, 1, 1, 1],
|
|
sw: [0, 1, -1, 1]
|
|
};
|
|
|
|
// Add CSS for resize handles, cloned element and selected
|
|
var rootClass = '.mce-content-body';
|
|
editor.contentStyles.push(
|
|
rootClass + ' div.mce-resizehandle {' +
|
|
'position: absolute;' +
|
|
'border: 1px solid black;' +
|
|
'box-sizing: box-sizing;' +
|
|
'background: #FFF;' +
|
|
'width: 7px;' +
|
|
'height: 7px;' +
|
|
'z-index: 10000' +
|
|
'}' +
|
|
rootClass + ' .mce-resizehandle:hover {' +
|
|
'background: #000' +
|
|
'}' +
|
|
rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' +
|
|
'outline: 1px solid black;' +
|
|
'resize: none' + // Have been talks about implementing this in browsers
|
|
'}' +
|
|
rootClass + ' .mce-clonedresizable {' +
|
|
'position: absolute;' +
|
|
(Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing
|
|
'opacity: .5;' +
|
|
'filter: alpha(opacity=50);' +
|
|
'z-index: 10000' +
|
|
'}' +
|
|
rootClass + ' .mce-resize-helper {' +
|
|
'background: #555;' +
|
|
'background: rgba(0,0,0,0.75);' +
|
|
'border-radius: 3px;' +
|
|
'border: 1px;' +
|
|
'color: white;' +
|
|
'display: none;' +
|
|
'font-family: sans-serif;' +
|
|
'font-size: 12px;' +
|
|
'white-space: nowrap;' +
|
|
'line-height: 14px;' +
|
|
'margin: 5px 10px;' +
|
|
'padding: 5px;' +
|
|
'position: absolute;' +
|
|
'z-index: 10001' +
|
|
'}'
|
|
);
|
|
|
|
function isResizable(elm) {
|
|
var selector = editor.settings.object_resizing;
|
|
|
|
if (selector === false || Env.iOS) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof selector != 'string') {
|
|
selector = 'table,img,div';
|
|
}
|
|
|
|
if (elm.getAttribute('data-mce-resize') === 'false') {
|
|
return false;
|
|
}
|
|
|
|
if (elm == editor.getBody()) {
|
|
return false;
|
|
}
|
|
|
|
return editor.dom.is(elm, selector);
|
|
}
|
|
|
|
function resizeGhostElement(e) {
|
|
var deltaX, deltaY, proportional;
|
|
var resizeHelperX, resizeHelperY;
|
|
|
|
// Calc new width/height
|
|
deltaX = e.screenX - startX;
|
|
deltaY = e.screenY - startY;
|
|
|
|
// Calc new size
|
|
width = deltaX * selectedHandle[2] + startW;
|
|
height = deltaY * selectedHandle[3] + startH;
|
|
|
|
// Never scale down lower than 5 pixels
|
|
width = width < 5 ? 5 : width;
|
|
height = height < 5 ? 5 : height;
|
|
|
|
if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) {
|
|
proportional = !VK.modifierPressed(e);
|
|
} else {
|
|
proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0);
|
|
}
|
|
|
|
// Constrain proportions
|
|
if (proportional) {
|
|
if (abs(deltaX) > abs(deltaY)) {
|
|
height = round(width * ratio);
|
|
width = round(height / ratio);
|
|
} else {
|
|
width = round(height / ratio);
|
|
height = round(width * ratio);
|
|
}
|
|
}
|
|
|
|
// Update ghost size
|
|
dom.setStyles(selectedElmGhost, {
|
|
width: width,
|
|
height: height
|
|
});
|
|
|
|
// Update resize helper position
|
|
resizeHelperX = selectedHandle.startPos.x + deltaX;
|
|
resizeHelperY = selectedHandle.startPos.y + deltaY;
|
|
resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
|
|
resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;
|
|
|
|
dom.setStyles(resizeHelper, {
|
|
left: resizeHelperX,
|
|
top: resizeHelperY,
|
|
display: 'block'
|
|
});
|
|
|
|
resizeHelper.innerHTML = width + ' × ' + height;
|
|
|
|
// Update ghost X position if needed
|
|
if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
|
|
dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
|
|
}
|
|
|
|
// Update ghost Y position if needed
|
|
if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
|
|
dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
|
|
}
|
|
|
|
// Calculate how must overflow we got
|
|
deltaX = rootElement.scrollWidth - startScrollWidth;
|
|
deltaY = rootElement.scrollHeight - startScrollHeight;
|
|
|
|
// Re-position the resize helper based on the overflow
|
|
if (deltaX + deltaY !== 0) {
|
|
dom.setStyles(resizeHelper, {
|
|
left: resizeHelperX - deltaX,
|
|
top: resizeHelperY - deltaY
|
|
});
|
|
}
|
|
|
|
if (!resizeStarted) {
|
|
editor.fire('ObjectResizeStart', { target: selectedElm, width: startW, height: startH });
|
|
resizeStarted = true;
|
|
}
|
|
}
|
|
|
|
function endGhostResize() {
|
|
resizeStarted = false;
|
|
|
|
function setSizeProp(name, value) {
|
|
if (value) {
|
|
// Resize by using style or attribute
|
|
if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
|
|
dom.setStyle(selectedElm, name, value);
|
|
} else {
|
|
dom.setAttrib(selectedElm, name, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set width/height properties
|
|
setSizeProp('width', width);
|
|
setSizeProp('height', height);
|
|
|
|
dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
|
|
dom.unbind(editableDoc, 'mouseup', endGhostResize);
|
|
|
|
if (rootDocument != editableDoc) {
|
|
dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
|
|
dom.unbind(rootDocument, 'mouseup', endGhostResize);
|
|
}
|
|
|
|
// Remove ghost/helper and update resize handle positions
|
|
dom.remove(selectedElmGhost);
|
|
dom.remove(resizeHelper);
|
|
|
|
if (!isIE || selectedElm.nodeName == "TABLE") {
|
|
showResizeRect(selectedElm);
|
|
}
|
|
|
|
editor.fire('ObjectResized', { target: selectedElm, width: width, height: height });
|
|
dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
|
|
editor.nodeChanged();
|
|
}
|
|
|
|
function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) {
|
|
var position, targetWidth, targetHeight, e, rect;
|
|
|
|
hideResizeRect();
|
|
unbindResizeHandleEvents();
|
|
|
|
// Get position and size of target
|
|
position = dom.getPos(targetElm, rootElement);
|
|
selectedElmX = position.x;
|
|
selectedElmY = position.y;
|
|
rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
|
|
targetWidth = rect.width || (rect.right - rect.left);
|
|
targetHeight = rect.height || (rect.bottom - rect.top);
|
|
|
|
// Reset width/height if user selects a new image/table
|
|
if (selectedElm != targetElm) {
|
|
detachResizeStartListener();
|
|
selectedElm = targetElm;
|
|
width = height = 0;
|
|
}
|
|
|
|
// Makes it possible to disable resizing
|
|
e = editor.fire('ObjectSelected', { target: targetElm });
|
|
|
|
if (isResizable(targetElm) && !e.isDefaultPrevented()) {
|
|
each(resizeHandles, function (handle, name) {
|
|
var handleElm;
|
|
|
|
function startDrag(e) {
|
|
startX = e.screenX;
|
|
startY = e.screenY;
|
|
startW = selectedElm.clientWidth;
|
|
startH = selectedElm.clientHeight;
|
|
ratio = startH / startW;
|
|
selectedHandle = handle;
|
|
|
|
handle.startPos = {
|
|
x: targetWidth * handle[0] + selectedElmX,
|
|
y: targetHeight * handle[1] + selectedElmY
|
|
};
|
|
|
|
startScrollWidth = rootElement.scrollWidth;
|
|
startScrollHeight = rootElement.scrollHeight;
|
|
|
|
selectedElmGhost = selectedElm.cloneNode(true);
|
|
dom.addClass(selectedElmGhost, 'mce-clonedresizable');
|
|
dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
|
|
selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
|
|
selectedElmGhost.unSelectabe = true;
|
|
dom.setStyles(selectedElmGhost, {
|
|
left: selectedElmX,
|
|
top: selectedElmY,
|
|
margin: 0
|
|
});
|
|
|
|
selectedElmGhost.removeAttribute('data-mce-selected');
|
|
rootElement.appendChild(selectedElmGhost);
|
|
|
|
dom.bind(editableDoc, 'mousemove', resizeGhostElement);
|
|
dom.bind(editableDoc, 'mouseup', endGhostResize);
|
|
|
|
if (rootDocument != editableDoc) {
|
|
dom.bind(rootDocument, 'mousemove', resizeGhostElement);
|
|
dom.bind(rootDocument, 'mouseup', endGhostResize);
|
|
}
|
|
|
|
resizeHelper = dom.add(rootElement, 'div', {
|
|
'class': 'mce-resize-helper',
|
|
'data-mce-bogus': 'all'
|
|
}, startW + ' × ' + startH);
|
|
}
|
|
|
|
if (mouseDownHandleName) {
|
|
// Drag started by IE native resizestart
|
|
if (name == mouseDownHandleName) {
|
|
startDrag(mouseDownEvent);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Get existing or render resize handle
|
|
handleElm = dom.get('mceResizeHandle' + name);
|
|
if (handleElm) {
|
|
dom.remove(handleElm);
|
|
}
|
|
|
|
handleElm = dom.add(rootElement, 'div', {
|
|
id: 'mceResizeHandle' + name,
|
|
'data-mce-bogus': 'all',
|
|
'class': 'mce-resizehandle',
|
|
unselectable: true,
|
|
style: 'cursor:' + name + '-resize; margin:0; padding:0'
|
|
});
|
|
|
|
// Hides IE move layer cursor
|
|
// If we set it on Chrome we get this wounderful bug: #6725
|
|
if (Env.ie) {
|
|
handleElm.contentEditable = false;
|
|
}
|
|
|
|
dom.bind(handleElm, 'mousedown', function (e) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
startDrag(e);
|
|
});
|
|
|
|
handle.elm = handleElm;
|
|
|
|
// Position element
|
|
dom.setStyles(handleElm, {
|
|
left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
|
|
top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
|
|
});
|
|
});
|
|
} else {
|
|
hideResizeRect();
|
|
}
|
|
|
|
selectedElm.setAttribute('data-mce-selected', '1');
|
|
}
|
|
|
|
function hideResizeRect() {
|
|
var name, handleElm;
|
|
|
|
unbindResizeHandleEvents();
|
|
|
|
if (selectedElm) {
|
|
selectedElm.removeAttribute('data-mce-selected');
|
|
}
|
|
|
|
for (name in resizeHandles) {
|
|
handleElm = dom.get('mceResizeHandle' + name);
|
|
if (handleElm) {
|
|
dom.unbind(handleElm);
|
|
dom.remove(handleElm);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateResizeRect(e) {
|
|
var startElm, controlElm;
|
|
|
|
function isChildOrEqual(node, parent) {
|
|
if (node) {
|
|
do {
|
|
if (node === parent) {
|
|
return true;
|
|
}
|
|
} while ((node = node.parentNode));
|
|
}
|
|
}
|
|
|
|
// Ignore all events while resizing or if the editor instance was removed
|
|
if (resizeStarted || editor.removed) {
|
|
return;
|
|
}
|
|
|
|
// Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
|
|
each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function (img) {
|
|
img.removeAttribute('data-mce-selected');
|
|
});
|
|
|
|
controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
|
|
controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0];
|
|
|
|
if (isChildOrEqual(controlElm, rootElement)) {
|
|
disableGeckoResize();
|
|
startElm = selection.getStart(true);
|
|
|
|
if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
|
|
if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) {
|
|
showResizeRect(controlElm);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
hideResizeRect();
|
|
}
|
|
|
|
function attachEvent(elm, name, func) {
|
|
if (elm && elm.attachEvent) {
|
|
elm.attachEvent('on' + name, func);
|
|
}
|
|
}
|
|
|
|
function detachEvent(elm, name, func) {
|
|
if (elm && elm.detachEvent) {
|
|
elm.detachEvent('on' + name, func);
|
|
}
|
|
}
|
|
|
|
function resizeNativeStart(e) {
|
|
var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;
|
|
|
|
pos = target.getBoundingClientRect();
|
|
relativeX = lastMouseDownEvent.clientX - pos.left;
|
|
relativeY = lastMouseDownEvent.clientY - pos.top;
|
|
|
|
// Figure out what corner we are draging on
|
|
for (name in resizeHandles) {
|
|
corner = resizeHandles[name];
|
|
|
|
cornerX = target.offsetWidth * corner[0];
|
|
cornerY = target.offsetHeight * corner[1];
|
|
|
|
if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) {
|
|
selectedHandle = corner;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Remove native selection and let the magic begin
|
|
resizeStarted = true;
|
|
editor.fire('ObjectResizeStart', {
|
|
target: selectedElm,
|
|
width: selectedElm.clientWidth,
|
|
height: selectedElm.clientHeight
|
|
});
|
|
editor.getDoc().selection.empty();
|
|
showResizeRect(target, name, lastMouseDownEvent);
|
|
}
|
|
|
|
function preventDefault(e) {
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
} else {
|
|
e.returnValue = false; // IE
|
|
}
|
|
}
|
|
|
|
function isWithinContentEditableFalse(elm) {
|
|
return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm));
|
|
}
|
|
|
|
function nativeControlSelect(e) {
|
|
var target = e.srcElement;
|
|
|
|
if (isWithinContentEditableFalse(target)) {
|
|
preventDefault(e);
|
|
return;
|
|
}
|
|
|
|
if (target != selectedElm) {
|
|
editor.fire('ObjectSelected', { target: target });
|
|
detachResizeStartListener();
|
|
|
|
if (target.id.indexOf('mceResizeHandle') === 0) {
|
|
e.returnValue = false;
|
|
return;
|
|
}
|
|
|
|
if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
|
|
hideResizeRect();
|
|
selectedElm = target;
|
|
attachEvent(target, 'resizestart', resizeNativeStart);
|
|
}
|
|
}
|
|
}
|
|
|
|
function detachResizeStartListener() {
|
|
detachEvent(selectedElm, 'resizestart', resizeNativeStart);
|
|
}
|
|
|
|
function unbindResizeHandleEvents() {
|
|
for (var name in resizeHandles) {
|
|
var handle = resizeHandles[name];
|
|
|
|
if (handle.elm) {
|
|
dom.unbind(handle.elm);
|
|
delete handle.elm;
|
|
}
|
|
}
|
|
}
|
|
|
|
function disableGeckoResize() {
|
|
try {
|
|
// Disable object resizing on Gecko
|
|
editor.getDoc().execCommand('enableObjectResizing', false, false);
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
function controlSelect(elm) {
|
|
var ctrlRng;
|
|
|
|
if (!isIE) {
|
|
return;
|
|
}
|
|
|
|
ctrlRng = editableDoc.body.createControlRange();
|
|
|
|
try {
|
|
ctrlRng.addElement(elm);
|
|
ctrlRng.select();
|
|
return true;
|
|
} catch (ex) {
|
|
// Ignore since the element can't be control selected for example a P tag
|
|
}
|
|
}
|
|
|
|
editor.on('init', function () {
|
|
if (isIE) {
|
|
// Hide the resize rect on resize and reselect the image
|
|
editor.on('ObjectResized', function (e) {
|
|
if (e.target.nodeName != 'TABLE') {
|
|
hideResizeRect();
|
|
controlSelect(e.target);
|
|
}
|
|
});
|
|
|
|
attachEvent(rootElement, 'controlselect', nativeControlSelect);
|
|
|
|
editor.on('mousedown', function (e) {
|
|
lastMouseDownEvent = e;
|
|
});
|
|
} else {
|
|
disableGeckoResize();
|
|
|
|
// Sniff sniff, hard to feature detect this stuff
|
|
if (Env.ie >= 11) {
|
|
// Needs to be mousedown for drag/drop to work on IE 11
|
|
// Needs to be click on Edge to properly select images
|
|
editor.on('mousedown click', function (e) {
|
|
var target = e.target, nodeName = target.nodeName;
|
|
|
|
if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) {
|
|
editor.selection.select(target, nodeName == 'TABLE');
|
|
|
|
// Only fire once since nodeChange is expensive
|
|
if (e.type == 'mousedown') {
|
|
editor.nodeChanged();
|
|
}
|
|
}
|
|
});
|
|
|
|
editor.dom.bind(rootElement, 'mscontrolselect', function (e) {
|
|
function delayedSelect(node) {
|
|
Delay.setEditorTimeout(editor, function () {
|
|
editor.selection.select(node);
|
|
});
|
|
}
|
|
|
|
if (isWithinContentEditableFalse(e.target)) {
|
|
e.preventDefault();
|
|
delayedSelect(e.target);
|
|
return;
|
|
}
|
|
|
|
if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
|
|
e.preventDefault();
|
|
|
|
// This moves the selection from being a control selection to a text like selection like in WebKit #6753
|
|
// TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections.
|
|
if (e.target.tagName == 'IMG') {
|
|
delayedSelect(e.target);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var throttledUpdateResizeRect = Delay.throttle(function (e) {
|
|
if (!editor.composing) {
|
|
updateResizeRect(e);
|
|
}
|
|
});
|
|
|
|
editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect);
|
|
|
|
// Update resize rect while typing in a table
|
|
editor.on('keyup compositionend', function (e) {
|
|
// Don't update the resize rect while composing since it blows away the IME see: #2710
|
|
if (selectedElm && selectedElm.nodeName == "TABLE") {
|
|
throttledUpdateResizeRect(e);
|
|
}
|
|
});
|
|
|
|
editor.on('hide blur', hideResizeRect);
|
|
|
|
// Hide rect on focusout since it would float on top of windows otherwise
|
|
//editor.on('focusout', hideResizeRect);
|
|
});
|
|
|
|
editor.on('remove', unbindResizeHandleEvents);
|
|
|
|
function destroy() {
|
|
selectedElm = selectedElmGhost = null;
|
|
|
|
if (isIE) {
|
|
detachResizeStartListener();
|
|
detachEvent(rootElement, 'controlselect', nativeControlSelect);
|
|
}
|
|
}
|
|
|
|
return {
|
|
isResizable: isResizable,
|
|
showResizeRect: showResizeRect,
|
|
hideResizeRect: hideResizeRect,
|
|
updateResizeRect: updateResizeRect,
|
|
controlSelect: controlSelect,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Fun.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
|
|
*/
|
|
|
|
/**
|
|
* Functional utility class.
|
|
*
|
|
* @private
|
|
* @class tinymce.util.Fun
|
|
*/
|
|
define(
|
|
'tinymce.core.util.Fun',
|
|
[
|
|
],
|
|
function () {
|
|
var slice = [].slice;
|
|
|
|
function constant(value) {
|
|
return function () {
|
|
return value;
|
|
};
|
|
}
|
|
|
|
function negate(predicate) {
|
|
return function (x) {
|
|
return !predicate(x);
|
|
};
|
|
}
|
|
|
|
function compose(f, g) {
|
|
return function (x) {
|
|
return f(g(x));
|
|
};
|
|
}
|
|
|
|
function or() {
|
|
var args = slice.call(arguments);
|
|
|
|
return function (x) {
|
|
for (var i = 0; i < args.length; i++) {
|
|
if (args[i](x)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
function and() {
|
|
var args = slice.call(arguments);
|
|
|
|
return function (x) {
|
|
for (var i = 0; i < args.length; i++) {
|
|
if (!args[i](x)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
function curry(fn) {
|
|
var args = slice.call(arguments);
|
|
|
|
if (args.length - 1 >= fn.length) {
|
|
return fn.apply(this, args.slice(1));
|
|
}
|
|
|
|
return function () {
|
|
var tempArgs = args.concat([].slice.call(arguments));
|
|
return curry.apply(this, tempArgs);
|
|
};
|
|
}
|
|
|
|
function noop() {
|
|
}
|
|
|
|
return {
|
|
constant: constant,
|
|
negate: negate,
|
|
and: and,
|
|
or: or,
|
|
curry: curry,
|
|
compose: compose,
|
|
noop: noop
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CaretCandidate.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 handling caret candidates. A caret candidate is
|
|
* for example text nodes, images, input elements, cE=false elements etc.
|
|
*
|
|
* @private
|
|
* @class tinymce.caret.CaretCandidate
|
|
*/
|
|
define(
|
|
'tinymce.core.caret.CaretCandidate',
|
|
[
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.util.Arr",
|
|
"tinymce.core.caret.CaretContainer"
|
|
],
|
|
function (NodeType, Arr, CaretContainer) {
|
|
var isContentEditableTrue = NodeType.isContentEditableTrue,
|
|
isContentEditableFalse = NodeType.isContentEditableFalse,
|
|
isBr = NodeType.isBr,
|
|
isText = NodeType.isText,
|
|
isInvalidTextElement = NodeType.matchNodeNames('script style textarea'),
|
|
isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'),
|
|
isTable = NodeType.matchNodeNames('table'),
|
|
isCaretContainer = CaretContainer.isCaretContainer;
|
|
|
|
function isCaretCandidate(node) {
|
|
if (isCaretContainer(node)) {
|
|
return false;
|
|
}
|
|
|
|
if (isText(node)) {
|
|
if (isInvalidTextElement(node.parentNode)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node);
|
|
}
|
|
|
|
function isInEditable(node, rootNode) {
|
|
for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
|
|
if (isContentEditableFalse(node)) {
|
|
return false;
|
|
}
|
|
|
|
if (isContentEditableTrue(node)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function isAtomicContentEditableFalse(node) {
|
|
if (!isContentEditableFalse(node)) {
|
|
return false;
|
|
}
|
|
|
|
return Arr.reduce(node.getElementsByTagName('*'), function (result, elm) {
|
|
return result || isContentEditableTrue(elm);
|
|
}, false) !== true;
|
|
}
|
|
|
|
function isAtomic(node) {
|
|
return isAtomicInline(node) || isAtomicContentEditableFalse(node);
|
|
}
|
|
|
|
function isEditableCaretCandidate(node, rootNode) {
|
|
return isCaretCandidate(node) && isInEditable(node, rootNode);
|
|
}
|
|
|
|
return {
|
|
isCaretCandidate: isCaretCandidate,
|
|
isInEditable: isInEditable,
|
|
isAtomic: isAtomic,
|
|
isEditableCaretCandidate: isEditableCaretCandidate
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* ClientRect.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 client rects.
|
|
*
|
|
* @private
|
|
* @class tinymce.geom.ClientRect
|
|
*/
|
|
define(
|
|
'tinymce.core.geom.ClientRect',
|
|
[
|
|
],
|
|
function () {
|
|
var round = Math.round;
|
|
|
|
function clone(rect) {
|
|
if (!rect) {
|
|
return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 };
|
|
}
|
|
|
|
return {
|
|
left: round(rect.left),
|
|
top: round(rect.top),
|
|
bottom: round(rect.bottom),
|
|
right: round(rect.right),
|
|
width: round(rect.width),
|
|
height: round(rect.height)
|
|
};
|
|
}
|
|
|
|
function collapse(clientRect, toStart) {
|
|
clientRect = clone(clientRect);
|
|
|
|
if (toStart) {
|
|
clientRect.right = clientRect.left;
|
|
} else {
|
|
clientRect.left = clientRect.left + clientRect.width;
|
|
clientRect.right = clientRect.left;
|
|
}
|
|
|
|
clientRect.width = 0;
|
|
|
|
return clientRect;
|
|
}
|
|
|
|
function isEqual(rect1, rect2) {
|
|
return (
|
|
rect1.left === rect2.left &&
|
|
rect1.top === rect2.top &&
|
|
rect1.bottom === rect2.bottom &&
|
|
rect1.right === rect2.right
|
|
);
|
|
}
|
|
|
|
function isValidOverflow(overflowY, clientRect1, clientRect2) {
|
|
return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2;
|
|
|
|
}
|
|
|
|
function isAbove(clientRect1, clientRect2) {
|
|
if ((clientRect1.bottom - clientRect1.height / 2) < clientRect2.top) {
|
|
return true;
|
|
}
|
|
|
|
if (clientRect1.top > clientRect2.bottom) {
|
|
return false;
|
|
}
|
|
|
|
return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2);
|
|
}
|
|
|
|
function isBelow(clientRect1, clientRect2) {
|
|
if (clientRect1.top > clientRect2.bottom) {
|
|
return true;
|
|
}
|
|
|
|
if (clientRect1.bottom < clientRect2.top) {
|
|
return false;
|
|
}
|
|
|
|
return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2);
|
|
}
|
|
|
|
function isLeft(clientRect1, clientRect2) {
|
|
return clientRect1.left < clientRect2.left;
|
|
}
|
|
|
|
function isRight(clientRect1, clientRect2) {
|
|
return clientRect1.right > clientRect2.right;
|
|
}
|
|
|
|
function compare(clientRect1, clientRect2) {
|
|
if (isAbove(clientRect1, clientRect2)) {
|
|
return -1;
|
|
}
|
|
|
|
if (isBelow(clientRect1, clientRect2)) {
|
|
return 1;
|
|
}
|
|
|
|
if (isLeft(clientRect1, clientRect2)) {
|
|
return -1;
|
|
}
|
|
|
|
if (isRight(clientRect1, clientRect2)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function containsXY(clientRect, clientX, clientY) {
|
|
return (
|
|
clientX >= clientRect.left &&
|
|
clientX <= clientRect.right &&
|
|
clientY >= clientRect.top &&
|
|
clientY <= clientRect.bottom
|
|
);
|
|
}
|
|
|
|
return {
|
|
clone: clone,
|
|
collapse: collapse,
|
|
isEqual: isEqual,
|
|
isAbove: isAbove,
|
|
isBelow: isBelow,
|
|
isLeft: isLeft,
|
|
isRight: isRight,
|
|
compare: compare,
|
|
containsXY: containsXY
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ExtendingChar.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 logic for detecting extending characters.
|
|
*
|
|
* @private
|
|
* @class tinymce.text.ExtendingChar
|
|
* @example
|
|
* var isExtending = ExtendingChar.isExtendingChar('a');
|
|
*/
|
|
define(
|
|
'tinymce.core.text.ExtendingChar',
|
|
[
|
|
],
|
|
function () {
|
|
// Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt
|
|
// Only includes the characters in that fit into UCS-2 16 bit
|
|
var extendingChars = new RegExp(
|
|
"[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" +
|
|
"\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" +
|
|
"\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" +
|
|
"\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" +
|
|
"\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" +
|
|
"\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" +
|
|
"\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" +
|
|
"\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" +
|
|
"\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" +
|
|
"\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" +
|
|
"\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" +
|
|
"\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" +
|
|
"\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" +
|
|
"\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" +
|
|
"\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" +
|
|
"\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" +
|
|
"\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" +
|
|
"\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" +
|
|
"\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" +
|
|
"\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" +
|
|
"\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]"
|
|
);
|
|
|
|
function isExtendingChar(ch) {
|
|
return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch);
|
|
}
|
|
|
|
return {
|
|
isExtendingChar: isExtendingChar
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CaretPosition.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 creating caret positions within a document a caretposition
|
|
* is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight
|
|
* since it's now updated live when the DOM changes.
|
|
*
|
|
* @private
|
|
* @class tinymce.caret.CaretPosition
|
|
* @example
|
|
* var caretPos1 = new CaretPosition(container, offset);
|
|
* var caretPos2 = CaretPosition.fromRangeStart(someRange);
|
|
*/
|
|
define(
|
|
'tinymce.core.caret.CaretPosition',
|
|
[
|
|
"tinymce.core.util.Fun",
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.dom.RangeUtils",
|
|
"tinymce.core.caret.CaretCandidate",
|
|
"tinymce.core.geom.ClientRect",
|
|
"tinymce.core.text.ExtendingChar"
|
|
],
|
|
function (Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) {
|
|
var isElement = NodeType.isElement,
|
|
isCaretCandidate = CaretCandidate.isCaretCandidate,
|
|
isBlock = NodeType.matchStyleValues('display', 'block table'),
|
|
isFloated = NodeType.matchStyleValues('float', 'left right'),
|
|
isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)),
|
|
isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')),
|
|
isText = NodeType.isText,
|
|
isBr = NodeType.isBr,
|
|
nodeIndex = DOMUtils.nodeIndex,
|
|
resolveIndex = RangeUtils.getNode;
|
|
|
|
function createRange(doc) {
|
|
return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng();
|
|
}
|
|
|
|
function isWhiteSpace(chr) {
|
|
return chr && /[\r\n\t ]/.test(chr);
|
|
}
|
|
|
|
function isHiddenWhiteSpaceRange(range) {
|
|
var container = range.startContainer,
|
|
offset = range.startOffset,
|
|
text;
|
|
|
|
if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) {
|
|
text = container.data;
|
|
|
|
if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getCaretPositionClientRects(caretPosition) {
|
|
var clientRects = [], beforeNode, node;
|
|
|
|
// Hack for older WebKit versions that doesn't
|
|
// support getBoundingClientRect on BR elements
|
|
function getBrClientRect(brNode) {
|
|
var doc = brNode.ownerDocument,
|
|
rng = createRange(doc),
|
|
nbsp = doc.createTextNode('\u00a0'),
|
|
parentNode = brNode.parentNode,
|
|
clientRect;
|
|
|
|
parentNode.insertBefore(nbsp, brNode);
|
|
rng.setStart(nbsp, 0);
|
|
rng.setEnd(nbsp, 1);
|
|
clientRect = ClientRect.clone(rng.getBoundingClientRect());
|
|
parentNode.removeChild(nbsp);
|
|
|
|
return clientRect;
|
|
}
|
|
|
|
function getBoundingClientRect(item) {
|
|
var clientRect, clientRects;
|
|
|
|
clientRects = item.getClientRects();
|
|
if (clientRects.length > 0) {
|
|
clientRect = ClientRect.clone(clientRects[0]);
|
|
} else {
|
|
clientRect = ClientRect.clone(item.getBoundingClientRect());
|
|
}
|
|
|
|
if (isBr(item) && clientRect.left === 0) {
|
|
return getBrClientRect(item);
|
|
}
|
|
|
|
return clientRect;
|
|
}
|
|
|
|
function collapseAndInflateWidth(clientRect, toStart) {
|
|
clientRect = ClientRect.collapse(clientRect, toStart);
|
|
clientRect.width = 1;
|
|
clientRect.right = clientRect.left + 1;
|
|
|
|
return clientRect;
|
|
}
|
|
|
|
function addUniqueAndValidRect(clientRect) {
|
|
if (clientRect.height === 0) {
|
|
return;
|
|
}
|
|
|
|
if (clientRects.length > 0) {
|
|
if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
clientRects.push(clientRect);
|
|
}
|
|
|
|
function addCharacterOffset(container, offset) {
|
|
var range = createRange(container.ownerDocument);
|
|
|
|
if (offset < container.data.length) {
|
|
if (ExtendingChar.isExtendingChar(container.data[offset])) {
|
|
return clientRects;
|
|
}
|
|
|
|
// WebKit returns two client rects for a position after an extending
|
|
// character a\uxxx|b so expand on "b" and collapse to start of "b" box
|
|
if (ExtendingChar.isExtendingChar(container.data[offset - 1])) {
|
|
range.setStart(container, offset);
|
|
range.setEnd(container, offset + 1);
|
|
|
|
if (!isHiddenWhiteSpaceRange(range)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
|
|
return clientRects;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (offset > 0) {
|
|
range.setStart(container, offset - 1);
|
|
range.setEnd(container, offset);
|
|
|
|
if (!isHiddenWhiteSpaceRange(range)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false));
|
|
}
|
|
}
|
|
|
|
if (offset < container.data.length) {
|
|
range.setStart(container, offset);
|
|
range.setEnd(container, offset + 1);
|
|
|
|
if (!isHiddenWhiteSpaceRange(range)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isText(caretPosition.container())) {
|
|
addCharacterOffset(caretPosition.container(), caretPosition.offset());
|
|
return clientRects;
|
|
}
|
|
|
|
if (isElement(caretPosition.container())) {
|
|
if (caretPosition.isAtEnd()) {
|
|
node = resolveIndex(caretPosition.container(), caretPosition.offset());
|
|
if (isText(node)) {
|
|
addCharacterOffset(node, node.data.length);
|
|
}
|
|
|
|
if (isValidElementCaretCandidate(node) && !isBr(node)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
|
|
}
|
|
} else {
|
|
node = resolveIndex(caretPosition.container(), caretPosition.offset());
|
|
if (isText(node)) {
|
|
addCharacterOffset(node, 0);
|
|
}
|
|
|
|
if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false));
|
|
return clientRects;
|
|
}
|
|
|
|
beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1);
|
|
if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) {
|
|
if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false));
|
|
}
|
|
}
|
|
|
|
if (isValidElementCaretCandidate(node)) {
|
|
addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true));
|
|
}
|
|
}
|
|
}
|
|
|
|
return clientRects;
|
|
}
|
|
|
|
/**
|
|
* Represents a location within the document by a container and an offset.
|
|
*
|
|
* @constructor
|
|
* @param {Node} container Container node.
|
|
* @param {Number} offset Offset within that container node.
|
|
* @param {Array} clientRects Optional client rects array for the position.
|
|
*/
|
|
function CaretPosition(container, offset, clientRects) {
|
|
function isAtStart() {
|
|
if (isText(container)) {
|
|
return offset === 0;
|
|
}
|
|
|
|
return offset === 0;
|
|
}
|
|
|
|
function isAtEnd() {
|
|
if (isText(container)) {
|
|
return offset >= container.data.length;
|
|
}
|
|
|
|
return offset >= container.childNodes.length;
|
|
}
|
|
|
|
function toRange() {
|
|
var range;
|
|
|
|
range = createRange(container.ownerDocument);
|
|
range.setStart(container, offset);
|
|
range.setEnd(container, offset);
|
|
|
|
return range;
|
|
}
|
|
|
|
function getClientRects() {
|
|
if (!clientRects) {
|
|
clientRects = getCaretPositionClientRects(new CaretPosition(container, offset));
|
|
}
|
|
|
|
return clientRects;
|
|
}
|
|
|
|
function isVisible() {
|
|
return getClientRects().length > 0;
|
|
}
|
|
|
|
function isEqual(caretPosition) {
|
|
return caretPosition && container === caretPosition.container() && offset === caretPosition.offset();
|
|
}
|
|
|
|
function getNode(before) {
|
|
return resolveIndex(container, before ? offset - 1 : offset);
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Returns the container node.
|
|
*
|
|
* @method container
|
|
* @return {Node} Container node.
|
|
*/
|
|
container: Fun.constant(container),
|
|
|
|
/**
|
|
* Returns the offset within the container node.
|
|
*
|
|
* @method offset
|
|
* @return {Number} Offset within the container node.
|
|
*/
|
|
offset: Fun.constant(offset),
|
|
|
|
/**
|
|
* Returns a range out of a the caret position.
|
|
*
|
|
* @method toRange
|
|
* @return {DOMRange} range for the caret position.
|
|
*/
|
|
toRange: toRange,
|
|
|
|
/**
|
|
* Returns the client rects for the caret position. Might be multiple rects between
|
|
* block elements.
|
|
*
|
|
* @method getClientRects
|
|
* @return {Array} Array of client rects.
|
|
*/
|
|
getClientRects: getClientRects,
|
|
|
|
/**
|
|
* Returns true if the caret location is visible/displayed on screen.
|
|
*
|
|
* @method isVisible
|
|
* @return {Boolean} true/false if the position is visible or not.
|
|
*/
|
|
isVisible: isVisible,
|
|
|
|
/**
|
|
* Returns true if the caret location is at the beginning of text node or container.
|
|
*
|
|
* @method isVisible
|
|
* @return {Boolean} true/false if the position is at the beginning.
|
|
*/
|
|
isAtStart: isAtStart,
|
|
|
|
/**
|
|
* Returns true if the caret location is at the end of text node or container.
|
|
*
|
|
* @method isVisible
|
|
* @return {Boolean} true/false if the position is at the end.
|
|
*/
|
|
isAtEnd: isAtEnd,
|
|
|
|
/**
|
|
* Compares the caret position to another caret position. This will only compare the
|
|
* container and offset not it's visual position.
|
|
*
|
|
* @method isEqual
|
|
* @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with.
|
|
* @return {Boolean} true if the caret positions are equal.
|
|
*/
|
|
isEqual: isEqual,
|
|
|
|
/**
|
|
* Returns the closest resolved node from a node index. That means if you have an offset after the
|
|
* last node in a container it will return that last node.
|
|
*
|
|
* @method getNode
|
|
* @return {Node} Node that is closest to the index.
|
|
*/
|
|
getNode: getNode
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a caret position from the start of a range.
|
|
*
|
|
* @method fromRangeStart
|
|
* @param {DOMRange} range DOM Range to create caret position from.
|
|
* @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range.
|
|
*/
|
|
CaretPosition.fromRangeStart = function (range) {
|
|
return new CaretPosition(range.startContainer, range.startOffset);
|
|
};
|
|
|
|
/**
|
|
* Creates a caret position from the end of a range.
|
|
*
|
|
* @method fromRangeEnd
|
|
* @param {DOMRange} range DOM Range to create caret position from.
|
|
* @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range.
|
|
*/
|
|
CaretPosition.fromRangeEnd = function (range) {
|
|
return new CaretPosition(range.endContainer, range.endOffset);
|
|
};
|
|
|
|
/**
|
|
* Creates a caret position from a node and places the offset after it.
|
|
*
|
|
* @method after
|
|
* @param {Node} node Node to get caret position from.
|
|
* @return {tinymce.caret.CaretPosition} Caret position from the node.
|
|
*/
|
|
CaretPosition.after = function (node) {
|
|
return new CaretPosition(node.parentNode, nodeIndex(node) + 1);
|
|
};
|
|
|
|
/**
|
|
* Creates a caret position from a node and places the offset before it.
|
|
*
|
|
* @method before
|
|
* @param {Node} node Node to get caret position from.
|
|
* @return {tinymce.caret.CaretPosition} Caret position from the node.
|
|
*/
|
|
CaretPosition.before = function (node) {
|
|
return new CaretPosition(node.parentNode, nodeIndex(node));
|
|
};
|
|
|
|
CaretPosition.isAtStart = function (pos) {
|
|
return pos ? pos.isAtStart() : false;
|
|
};
|
|
|
|
CaretPosition.isAtEnd = function (pos) {
|
|
return pos ? pos.isAtEnd() : false;
|
|
};
|
|
|
|
CaretPosition.isTextPosition = function (pos) {
|
|
return pos ? NodeType.isText(pos.container()) : false;
|
|
};
|
|
|
|
return CaretPosition;
|
|
}
|
|
);
|
|
/**
|
|
* CaretBookmark.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 creates or resolves xpath like string representation of a CaretPositions.
|
|
*
|
|
* The format is a / separated list of chunks with:
|
|
* <element|text()>[index|after|before]
|
|
*
|
|
* For example:
|
|
* p[0]/b[0]/text()[0],1 = <p><b>a|c</b></p>
|
|
* p[0]/img[0],before = <p>|<img></p>
|
|
* p[0]/img[0],after = <p><img>|</p>
|
|
*
|
|
* @private
|
|
* @static
|
|
* @class tinymce.caret.CaretBookmark
|
|
* @example
|
|
* var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild));
|
|
* var caretPosition = CaretBookmark.resolve(bookmark);
|
|
*/
|
|
define(
|
|
'tinymce.core.caret.CaretBookmark',
|
|
[
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.dom.DOMUtils',
|
|
'tinymce.core.util.Fun',
|
|
'tinymce.core.util.Arr',
|
|
'tinymce.core.caret.CaretPosition'
|
|
],
|
|
function (NodeType, DomUtils, Fun, Arr, CaretPosition) {
|
|
var isText = NodeType.isText,
|
|
isBogus = NodeType.isBogus,
|
|
nodeIndex = DomUtils.nodeIndex;
|
|
|
|
function normalizedParent(node) {
|
|
var parentNode = node.parentNode;
|
|
|
|
if (isBogus(parentNode)) {
|
|
return normalizedParent(parentNode);
|
|
}
|
|
|
|
return parentNode;
|
|
}
|
|
|
|
function getChildNodes(node) {
|
|
if (!node) {
|
|
return [];
|
|
}
|
|
|
|
return Arr.reduce(node.childNodes, function (result, node) {
|
|
if (isBogus(node) && node.nodeName != 'BR') {
|
|
result = result.concat(getChildNodes(node));
|
|
} else {
|
|
result.push(node);
|
|
}
|
|
|
|
return result;
|
|
}, []);
|
|
}
|
|
|
|
function normalizedTextOffset(textNode, offset) {
|
|
while ((textNode = textNode.previousSibling)) {
|
|
if (!isText(textNode)) {
|
|
break;
|
|
}
|
|
|
|
offset += textNode.data.length;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
function equal(targetValue) {
|
|
return function (value) {
|
|
return targetValue === value;
|
|
};
|
|
}
|
|
|
|
function normalizedNodeIndex(node) {
|
|
var nodes, index, numTextFragments;
|
|
|
|
nodes = getChildNodes(normalizedParent(node));
|
|
index = Arr.findIndex(nodes, equal(node), node);
|
|
nodes = nodes.slice(0, index + 1);
|
|
numTextFragments = Arr.reduce(nodes, function (result, node, i) {
|
|
if (isText(node) && isText(nodes[i - 1])) {
|
|
result++;
|
|
}
|
|
|
|
return result;
|
|
}, 0);
|
|
|
|
nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName));
|
|
index = Arr.findIndex(nodes, equal(node), node);
|
|
|
|
return index - numTextFragments;
|
|
}
|
|
|
|
function createPathItem(node) {
|
|
var name;
|
|
|
|
if (isText(node)) {
|
|
name = 'text()';
|
|
} else {
|
|
name = node.nodeName.toLowerCase();
|
|
}
|
|
|
|
return name + '[' + normalizedNodeIndex(node) + ']';
|
|
}
|
|
|
|
function parentsUntil(rootNode, node, predicate) {
|
|
var parents = [];
|
|
|
|
for (node = node.parentNode; node != rootNode; node = node.parentNode) {
|
|
if (predicate && predicate(node)) {
|
|
break;
|
|
}
|
|
|
|
parents.push(node);
|
|
}
|
|
|
|
return parents;
|
|
}
|
|
|
|
function create(rootNode, caretPosition) {
|
|
var container, offset, path = [],
|
|
outputOffset, childNodes, parents;
|
|
|
|
container = caretPosition.container();
|
|
offset = caretPosition.offset();
|
|
|
|
if (isText(container)) {
|
|
outputOffset = normalizedTextOffset(container, offset);
|
|
} else {
|
|
childNodes = container.childNodes;
|
|
if (offset >= childNodes.length) {
|
|
outputOffset = 'after';
|
|
offset = childNodes.length - 1;
|
|
} else {
|
|
outputOffset = 'before';
|
|
}
|
|
|
|
container = childNodes[offset];
|
|
}
|
|
|
|
path.push(createPathItem(container));
|
|
parents = parentsUntil(rootNode, container);
|
|
parents = Arr.filter(parents, Fun.negate(NodeType.isBogus));
|
|
path = path.concat(Arr.map(parents, function (node) {
|
|
return createPathItem(node);
|
|
}));
|
|
|
|
return path.reverse().join('/') + ',' + outputOffset;
|
|
}
|
|
|
|
function resolvePathItem(node, name, index) {
|
|
var nodes = getChildNodes(node);
|
|
|
|
nodes = Arr.filter(nodes, function (node, index) {
|
|
return !isText(node) || !isText(nodes[index - 1]);
|
|
});
|
|
|
|
nodes = Arr.filter(nodes, NodeType.matchNodeNames(name));
|
|
return nodes[index];
|
|
}
|
|
|
|
function findTextPosition(container, offset) {
|
|
var node = container, targetOffset = 0, dataLen;
|
|
|
|
while (isText(node)) {
|
|
dataLen = node.data.length;
|
|
|
|
if (offset >= targetOffset && offset <= targetOffset + dataLen) {
|
|
container = node;
|
|
offset = offset - targetOffset;
|
|
break;
|
|
}
|
|
|
|
if (!isText(node.nextSibling)) {
|
|
container = node;
|
|
offset = dataLen;
|
|
break;
|
|
}
|
|
|
|
targetOffset += dataLen;
|
|
node = node.nextSibling;
|
|
}
|
|
|
|
if (offset > container.data.length) {
|
|
offset = container.data.length;
|
|
}
|
|
|
|
return new CaretPosition(container, offset);
|
|
}
|
|
|
|
function resolve(rootNode, path) {
|
|
var parts, container, offset;
|
|
|
|
if (!path) {
|
|
return null;
|
|
}
|
|
|
|
parts = path.split(',');
|
|
path = parts[0].split('/');
|
|
offset = parts.length > 1 ? parts[1] : 'before';
|
|
|
|
container = Arr.reduce(path, function (result, value) {
|
|
value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value);
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
if (value[1] === 'text()') {
|
|
value[1] = '#text';
|
|
}
|
|
|
|
return resolvePathItem(result, value[1], parseInt(value[2], 10));
|
|
}, rootNode);
|
|
|
|
if (!container) {
|
|
return null;
|
|
}
|
|
|
|
if (!isText(container)) {
|
|
if (offset === 'after') {
|
|
offset = nodeIndex(container) + 1;
|
|
} else {
|
|
offset = nodeIndex(container);
|
|
}
|
|
|
|
return new CaretPosition(container.parentNode, offset);
|
|
}
|
|
|
|
return findTextPosition(container, parseInt(offset, 10));
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Create a xpath bookmark location for the specified caret position.
|
|
*
|
|
* @method create
|
|
* @param {Node} rootNode Root node to create bookmark within.
|
|
* @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node.
|
|
* @return {String} String xpath like location of caret position.
|
|
*/
|
|
create: create,
|
|
|
|
/**
|
|
* Resolves a xpath like bookmark location to the a caret position.
|
|
*
|
|
* @method resolve
|
|
* @param {Node} rootNode Root node to resolve xpath bookmark within.
|
|
* @param {String} bookmark Bookmark string to resolve.
|
|
* @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark.
|
|
*/
|
|
resolve: resolve
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* BookmarkManager.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 selection bookmarks.
|
|
*
|
|
* @class tinymce.dom.BookmarkManager
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.BookmarkManager',
|
|
[
|
|
'tinymce.core.caret.CaretBookmark',
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.dom.RangeUtils',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.text.Zwsp',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (CaretBookmark, CaretContainer, CaretPosition, NodeType, RangeUtils, Env, Zwsp, Tools) {
|
|
var isContentEditableFalse = NodeType.isContentEditableFalse;
|
|
|
|
var getNormalizedTextOffset = function (container, offset) {
|
|
var node, trimmedOffset;
|
|
|
|
trimmedOffset = Zwsp.trim(container.data.slice(0, offset)).length;
|
|
for (node = container.previousSibling; node && node.nodeType === 3; node = node.previousSibling) {
|
|
trimmedOffset += Zwsp.trim(node.data).length;
|
|
}
|
|
|
|
return trimmedOffset;
|
|
};
|
|
|
|
/**
|
|
* Constructs a new BookmarkManager instance for a specific selection instance.
|
|
*
|
|
* @constructor
|
|
* @method BookmarkManager
|
|
* @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
|
|
*/
|
|
function BookmarkManager(selection) {
|
|
var dom = selection.dom;
|
|
|
|
/**
|
|
* Returns a bookmark location for the current selection. This bookmark object
|
|
* can then be used to restore the selection after some content modification to the document.
|
|
*
|
|
* @method getBookmark
|
|
* @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
|
|
* @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
|
|
* @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
|
|
* @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.getBookmark = function (type, normalized) {
|
|
var rng, rng2, id, collapsed, name, element, chr = '', styles;
|
|
|
|
function findIndex(name, element) {
|
|
var count = 0;
|
|
|
|
Tools.each(dom.select(name), function (node) {
|
|
if (node.getAttribute('data-mce-bogus') === 'all') {
|
|
return;
|
|
}
|
|
|
|
if (node == element) {
|
|
return false;
|
|
}
|
|
|
|
count++;
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
function normalizeTableCellSelection(rng) {
|
|
function moveEndPoint(start) {
|
|
var container, offset, childNodes, prefix = start ? 'start' : 'end';
|
|
|
|
container = rng[prefix + 'Container'];
|
|
offset = rng[prefix + 'Offset'];
|
|
|
|
if (container.nodeType == 1 && container.nodeName == "TR") {
|
|
childNodes = container.childNodes;
|
|
container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
|
|
if (container) {
|
|
offset = start ? 0 : container.childNodes.length;
|
|
rng['set' + (start ? 'Start' : 'End')](container, offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
moveEndPoint(true);
|
|
moveEndPoint();
|
|
|
|
return rng;
|
|
}
|
|
|
|
function getLocation(rng) {
|
|
var root = dom.getRoot(), bookmark = {};
|
|
|
|
function getPoint(rng, start) {
|
|
var container = rng[start ? 'startContainer' : 'endContainer'],
|
|
offset = rng[start ? 'startOffset' : 'endOffset'], point = [], childNodes, after = 0;
|
|
|
|
if (container.nodeType === 3) {
|
|
point.push(normalized ? getNormalizedTextOffset(container, offset) : offset);
|
|
} else {
|
|
childNodes = container.childNodes;
|
|
|
|
if (offset >= childNodes.length && childNodes.length) {
|
|
after = 1;
|
|
offset = Math.max(0, childNodes.length - 1);
|
|
}
|
|
|
|
point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
|
|
}
|
|
|
|
for (; container && container != root; container = container.parentNode) {
|
|
point.push(dom.nodeIndex(container, normalized));
|
|
}
|
|
|
|
return point;
|
|
}
|
|
|
|
bookmark.start = getPoint(rng, true);
|
|
|
|
if (!selection.isCollapsed()) {
|
|
bookmark.end = getPoint(rng);
|
|
}
|
|
|
|
return bookmark;
|
|
}
|
|
|
|
function findAdjacentContentEditableFalseElm(rng) {
|
|
function findSibling(node, offset) {
|
|
var sibling;
|
|
|
|
if (NodeType.isElement(node)) {
|
|
node = RangeUtils.getNode(node, offset);
|
|
if (isContentEditableFalse(node)) {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
if (CaretContainer.isCaretContainer(node)) {
|
|
if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) {
|
|
node = node.parentNode;
|
|
}
|
|
|
|
sibling = node.previousSibling;
|
|
if (isContentEditableFalse(sibling)) {
|
|
return sibling;
|
|
}
|
|
|
|
sibling = node.nextSibling;
|
|
if (isContentEditableFalse(sibling)) {
|
|
return sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset);
|
|
}
|
|
|
|
if (type == 2) {
|
|
element = selection.getNode();
|
|
name = element ? element.nodeName : null;
|
|
rng = selection.getRng();
|
|
|
|
if (isContentEditableFalse(element) || name == 'IMG') {
|
|
return { name: name, index: findIndex(name, element) };
|
|
}
|
|
|
|
if (selection.tridentSel) {
|
|
return selection.tridentSel.getBookmark(type);
|
|
}
|
|
|
|
element = findAdjacentContentEditableFalseElm(rng);
|
|
if (element) {
|
|
name = element.tagName;
|
|
return { name: name, index: findIndex(name, element) };
|
|
}
|
|
|
|
return getLocation(rng);
|
|
}
|
|
|
|
if (type == 3) {
|
|
rng = selection.getRng();
|
|
|
|
return {
|
|
start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)),
|
|
end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng))
|
|
};
|
|
}
|
|
|
|
// Handle simple range
|
|
if (type) {
|
|
return { rng: selection.getRng() };
|
|
}
|
|
|
|
rng = selection.getRng();
|
|
id = dom.uniqueId();
|
|
collapsed = selection.isCollapsed();
|
|
styles = 'overflow:hidden;line-height:0px';
|
|
|
|
// Explorer method
|
|
if (rng.duplicate || rng.item) {
|
|
// Text selection
|
|
if (!rng.item) {
|
|
rng2 = rng.duplicate();
|
|
|
|
try {
|
|
// Insert start marker
|
|
rng.collapse();
|
|
rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
|
|
|
|
// Insert end marker
|
|
if (!collapsed) {
|
|
rng2.collapse(false);
|
|
|
|
// Detect the empty space after block elements in IE and move the
|
|
// end back one character <p></p>] becomes <p>]</p>
|
|
rng.moveToElementText(rng2.parentElement());
|
|
if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
|
|
rng2.move('character', -1);
|
|
}
|
|
|
|
rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
|
|
}
|
|
} 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 = '<br data-mce-bogus="1" />';
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function resolveCaretPositionBookmark() {
|
|
var rng, pos;
|
|
|
|
rng = dom.createRng();
|
|
pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start);
|
|
rng.setStart(pos.container(), pos.offset());
|
|
|
|
pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end);
|
|
rng.setEnd(pos.container(), pos.offset());
|
|
|
|
return rng;
|
|
}
|
|
|
|
if (bookmark) {
|
|
if (Tools.isArray(bookmark.start)) {
|
|
rng = dom.createRng();
|
|
root = dom.getRoot();
|
|
|
|
if (selection.tridentSel) {
|
|
return selection.tridentSel.moveToBookmark(bookmark);
|
|
}
|
|
|
|
if (setEndPoint(true) && setEndPoint()) {
|
|
selection.setRng(rng);
|
|
}
|
|
} else if (typeof bookmark.start == 'string') {
|
|
selection.setRng(resolveCaretPositionBookmark(bookmark));
|
|
} else if (bookmark.id) {
|
|
// Restore start/end points
|
|
restoreEndPoint('start');
|
|
restoreEndPoint('end');
|
|
|
|
if (startContainer) {
|
|
rng = dom.createRng();
|
|
rng.setStart(addBogus(startContainer), startOffset);
|
|
rng.setEnd(addBogus(endContainer), endOffset);
|
|
selection.setRng(rng);
|
|
}
|
|
} else if (bookmark.name) {
|
|
selection.select(dom.select(bookmark.name)[bookmark.index]);
|
|
} else if (bookmark.rng) {
|
|
selection.setRng(bookmark.rng);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns true/false if the specified node is a bookmark node or not.
|
|
*
|
|
* @static
|
|
* @method isBookmarkNode
|
|
* @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
|
|
* @return {Boolean} true/false if the node is a bookmark node or not.
|
|
*/
|
|
BookmarkManager.isBookmarkNode = function (node) {
|
|
return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
|
|
};
|
|
|
|
return BookmarkManager;
|
|
}
|
|
);
|
|
/**
|
|
* ScrollIntoView.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.ScrollIntoView',
|
|
[
|
|
'tinymce.core.dom.NodeType'
|
|
],
|
|
function (NodeType) {
|
|
var getPos = function (elm) {
|
|
var x = 0, y = 0;
|
|
|
|
var offsetParent = elm;
|
|
while (offsetParent && offsetParent.nodeType) {
|
|
x += offsetParent.offsetLeft || 0;
|
|
y += offsetParent.offsetTop || 0;
|
|
offsetParent = offsetParent.offsetParent;
|
|
}
|
|
|
|
return { x: x, y: y };
|
|
};
|
|
|
|
var fireScrollIntoViewEvent = function (editor, elm, alignToTop) {
|
|
var scrollEvent = { elm: elm, alignToTop: alignToTop };
|
|
editor.fire('scrollIntoView', scrollEvent);
|
|
return scrollEvent.isDefaultPrevented();
|
|
};
|
|
|
|
var scrollIntoView = function (editor, elm, alignToTop) {
|
|
var y, viewPort, dom = editor.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0;
|
|
|
|
if (fireScrollIntoViewEvent(editor, elm, alignToTop)) {
|
|
return;
|
|
}
|
|
|
|
if (!NodeType.isElement(elm)) {
|
|
return;
|
|
}
|
|
|
|
if (alignToTop === false) {
|
|
offsetY = elm.offsetHeight;
|
|
}
|
|
|
|
if (root.nodeName !== 'BODY') {
|
|
var scrollContainer = editor.selection.getScrollContainer();
|
|
if (scrollContainer) {
|
|
y = getPos(elm).y - getPos(scrollContainer).y + offsetY;
|
|
viewPortH = scrollContainer.clientHeight;
|
|
viewPortY = scrollContainer.scrollTop;
|
|
if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
|
|
scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
viewPort = dom.getViewPort(editor.getWin());
|
|
y = dom.getPos(elm).y + offsetY;
|
|
viewPortY = viewPort.y;
|
|
viewPortH = viewPort.h;
|
|
if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
|
|
editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
|
|
}
|
|
};
|
|
|
|
return {
|
|
scrollIntoView: scrollIntoView
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* TridentSelection.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
|
|
*/
|
|
|
|
/**
|
|
* Selection class for old explorer versions. This one fakes the
|
|
* native selection object available on modern browsers.
|
|
*
|
|
* @private
|
|
* @class tinymce.dom.TridentSelection
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.TridentSelection',
|
|
[
|
|
],
|
|
function () {
|
|
function Selection(selection) {
|
|
var self = this, dom = selection.dom, FALSE = false;
|
|
|
|
function getPosition(rng, start) {
|
|
var checkRng, startIndex = 0, endIndex, inside,
|
|
children, child, offset, index, position = -1, parent;
|
|
|
|
// Setup test range, collapse it and get the parent
|
|
checkRng = rng.duplicate();
|
|
checkRng.collapse(start);
|
|
parent = checkRng.parentElement();
|
|
|
|
// Check if the selection is within the right document
|
|
if (parent.ownerDocument !== selection.dom.doc) {
|
|
return;
|
|
}
|
|
|
|
// IE will report non editable elements as it's parent so look for an editable one
|
|
while (parent.contentEditable === "false") {
|
|
parent = parent.parentNode;
|
|
}
|
|
|
|
// If parent doesn't have any children then return that we are inside the element
|
|
if (!parent.hasChildNodes()) {
|
|
return { node: parent, inside: 1 };
|
|
}
|
|
|
|
// Setup node list and endIndex
|
|
children = parent.children;
|
|
endIndex = children.length - 1;
|
|
|
|
// Perform a binary search for the position
|
|
while (startIndex <= endIndex) {
|
|
index = Math.floor((startIndex + endIndex) / 2);
|
|
|
|
// Move selection to node and compare the ranges
|
|
child = children[index];
|
|
checkRng.moveToElementText(child);
|
|
position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
|
|
|
|
// Before/after or an exact match
|
|
if (position > 0) {
|
|
endIndex = index - 1;
|
|
} else if (position < 0) {
|
|
startIndex = index + 1;
|
|
} else {
|
|
return { node: child };
|
|
}
|
|
}
|
|
|
|
// Check if child position is before or we didn't find a position
|
|
if (position < 0) {
|
|
// No element child was found use the parent element and the offset inside that
|
|
if (!child) {
|
|
checkRng.moveToElementText(parent);
|
|
checkRng.collapse(true);
|
|
child = parent;
|
|
inside = true;
|
|
} else {
|
|
checkRng.collapse(false);
|
|
}
|
|
|
|
// Walk character by character in text node until we hit the selected range endpoint,
|
|
// hit the end of document or parent isn't the right one
|
|
// We need to walk char by char since rng.text or rng.htmlText will trim line endings
|
|
offset = 0;
|
|
while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
|
|
if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) {
|
|
break;
|
|
}
|
|
|
|
offset++;
|
|
}
|
|
} else {
|
|
// Child position is after the selection endpoint
|
|
checkRng.collapse(true);
|
|
|
|
// Walk character by character in text node until we hit the selected range endpoint, hit
|
|
// the end of document or parent isn't the right one
|
|
offset = 0;
|
|
while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) {
|
|
if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) {
|
|
break;
|
|
}
|
|
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
return { node: child, position: position, offset: offset, inside: inside };
|
|
}
|
|
|
|
// Returns a W3C DOM compatible range object by using the IE Range API
|
|
function getRange() {
|
|
var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark;
|
|
|
|
// If selection is outside the current document just return an empty range
|
|
element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
|
|
if (element.ownerDocument != dom.doc) {
|
|
return domRange;
|
|
}
|
|
|
|
collapsed = selection.isCollapsed();
|
|
|
|
// Handle control selection
|
|
if (ieRange.item) {
|
|
domRange.setStart(element.parentNode, dom.nodeIndex(element));
|
|
domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
|
|
|
|
return domRange;
|
|
}
|
|
|
|
function findEndPoint(start) {
|
|
var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue;
|
|
|
|
container = endPoint.node;
|
|
offset = endPoint.offset;
|
|
|
|
if (endPoint.inside && !container.hasChildNodes()) {
|
|
domRange[start ? 'setStart' : 'setEnd'](container, 0);
|
|
return;
|
|
}
|
|
|
|
if (offset === undef) {
|
|
domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
|
|
return;
|
|
}
|
|
|
|
if (endPoint.position < 0) {
|
|
sibling = endPoint.inside ? container.firstChild : container.nextSibling;
|
|
|
|
if (!sibling) {
|
|
domRange[start ? 'setStartAfter' : 'setEndAfter'](container);
|
|
return;
|
|
}
|
|
|
|
if (!offset) {
|
|
if (sibling.nodeType == 3) {
|
|
domRange[start ? 'setStart' : 'setEnd'](sibling, 0);
|
|
} else {
|
|
domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Find the text node and offset
|
|
while (sibling) {
|
|
if (sibling.nodeType == 3) {
|
|
nodeValue = sibling.nodeValue;
|
|
textNodeOffset += nodeValue.length;
|
|
|
|
// We are at or passed the position we where looking for
|
|
if (textNodeOffset >= offset) {
|
|
container = sibling;
|
|
textNodeOffset -= offset;
|
|
textNodeOffset = nodeValue.length - textNodeOffset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
sibling = sibling.nextSibling;
|
|
}
|
|
} else {
|
|
// Find the text node and offset
|
|
sibling = container.previousSibling;
|
|
|
|
if (!sibling) {
|
|
return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
|
|
}
|
|
|
|
// If there isn't any text to loop then use the first position
|
|
if (!offset) {
|
|
if (container.nodeType == 3) {
|
|
domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length);
|
|
} else {
|
|
domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
while (sibling) {
|
|
if (sibling.nodeType == 3) {
|
|
textNodeOffset += sibling.nodeValue.length;
|
|
|
|
// We are at or passed the position we where looking for
|
|
if (textNodeOffset >= offset) {
|
|
container = sibling;
|
|
textNodeOffset -= offset;
|
|
break;
|
|
}
|
|
}
|
|
|
|
sibling = sibling.previousSibling;
|
|
}
|
|
}
|
|
|
|
domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
|
|
}
|
|
|
|
try {
|
|
// Find start point
|
|
findEndPoint(true);
|
|
|
|
// Find end point if needed
|
|
if (!collapsed) {
|
|
findEndPoint();
|
|
}
|
|
} catch (ex) {
|
|
// IE has a nasty bug where text nodes might throw "invalid argument" when you
|
|
// access the nodeValue or other properties of text nodes. This seems to happen when
|
|
// text nodes are split into two nodes by a delete/backspace call.
|
|
// So let us detect and try to fix it.
|
|
if (ex.number == -2147024809) {
|
|
// Get the current selection
|
|
bookmark = self.getBookmark(2);
|
|
|
|
// Get start element
|
|
tmpRange = ieRange.duplicate();
|
|
tmpRange.collapse(true);
|
|
element = tmpRange.parentElement();
|
|
|
|
// Get end element
|
|
if (!collapsed) {
|
|
tmpRange = ieRange.duplicate();
|
|
tmpRange.collapse(false);
|
|
element2 = tmpRange.parentElement();
|
|
element2.innerHTML = element2.innerHTML;
|
|
}
|
|
|
|
// Remove the broken elements
|
|
element.innerHTML = element.innerHTML;
|
|
|
|
// Restore the selection
|
|
self.moveToBookmark(bookmark);
|
|
|
|
// Since the range has moved we need to re-get it
|
|
ieRange = selection.getRng();
|
|
|
|
// Find start point
|
|
findEndPoint(true);
|
|
|
|
// Find end point if needed
|
|
if (!collapsed) {
|
|
findEndPoint();
|
|
}
|
|
} else {
|
|
throw ex; // Throw other errors
|
|
}
|
|
}
|
|
|
|
return domRange;
|
|
}
|
|
|
|
this.getBookmark = function (type) {
|
|
var rng = selection.getRng(), bookmark = {};
|
|
|
|
function getIndexes(node) {
|
|
var parent, root, children, i, indexes = [];
|
|
|
|
parent = node.parentNode;
|
|
root = dom.getRoot().parentNode;
|
|
|
|
while (parent != root && parent.nodeType !== 9) {
|
|
children = parent.children;
|
|
|
|
i = children.length;
|
|
while (i--) {
|
|
if (node === children[i]) {
|
|
indexes.push(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
node = parent;
|
|
parent = parent.parentNode;
|
|
}
|
|
|
|
return indexes;
|
|
}
|
|
|
|
function getBookmarkEndPoint(start) {
|
|
var position;
|
|
|
|
position = getPosition(rng, start);
|
|
if (position) {
|
|
return {
|
|
position: position.position,
|
|
offset: position.offset,
|
|
indexes: getIndexes(position.node),
|
|
inside: position.inside
|
|
};
|
|
}
|
|
}
|
|
|
|
// Non ubstructive bookmark
|
|
if (type === 2) {
|
|
// Handle text selection
|
|
if (!rng.item) {
|
|
bookmark.start = getBookmarkEndPoint(true);
|
|
|
|
if (!selection.isCollapsed()) {
|
|
bookmark.end = getBookmarkEndPoint();
|
|
}
|
|
} else {
|
|
bookmark.start = { ctrl: true, indexes: getIndexes(rng.item(0)) };
|
|
}
|
|
}
|
|
|
|
return bookmark;
|
|
};
|
|
|
|
this.moveToBookmark = function (bookmark) {
|
|
var rng, body = dom.doc.body;
|
|
|
|
function resolveIndexes(indexes) {
|
|
var node, i, idx, children;
|
|
|
|
node = dom.getRoot();
|
|
for (i = indexes.length - 1; i >= 0; i--) {
|
|
children = node.children;
|
|
idx = indexes[i];
|
|
|
|
if (idx <= children.length - 1) {
|
|
node = children[idx];
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function setBookmarkEndPoint(start) {
|
|
var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
|
|
|
|
if (endPoint) {
|
|
moveLeft = endPoint.position > 0;
|
|
|
|
moveRng = body.createTextRange();
|
|
moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
|
|
|
|
offset = endPoint.offset;
|
|
if (offset !== undef) {
|
|
moveRng.collapse(endPoint.inside || moveLeft);
|
|
moveRng.moveStart('character', moveLeft ? -offset : offset);
|
|
} else {
|
|
moveRng.collapse(start);
|
|
}
|
|
|
|
rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
|
|
|
|
if (start) {
|
|
rng.collapse(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bookmark.start) {
|
|
if (bookmark.start.ctrl) {
|
|
rng = body.createControlRange();
|
|
rng.addElement(resolveIndexes(bookmark.start.indexes));
|
|
rng.select();
|
|
} else {
|
|
rng = body.createTextRange();
|
|
setBookmarkEndPoint(true);
|
|
setBookmarkEndPoint();
|
|
rng.select();
|
|
}
|
|
}
|
|
};
|
|
|
|
this.addRange = function (rng) {
|
|
var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling,
|
|
doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm;
|
|
|
|
function setEndPoint(start) {
|
|
var container, offset, marker, tmpRng, nodes;
|
|
|
|
marker = dom.create('a');
|
|
container = start ? startContainer : endContainer;
|
|
offset = start ? startOffset : endOffset;
|
|
tmpRng = ieRng.duplicate();
|
|
|
|
if (container == doc || container == doc.documentElement) {
|
|
container = body;
|
|
offset = 0;
|
|
}
|
|
|
|
if (container.nodeType == 3) {
|
|
container.parentNode.insertBefore(marker, container);
|
|
tmpRng.moveToElementText(marker);
|
|
tmpRng.moveStart('character', offset);
|
|
dom.remove(marker);
|
|
ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
|
|
} else {
|
|
nodes = container.childNodes;
|
|
|
|
if (nodes.length) {
|
|
if (offset >= nodes.length) {
|
|
dom.insertAfter(marker, nodes[nodes.length - 1]);
|
|
} else {
|
|
container.insertBefore(marker, nodes[offset]);
|
|
}
|
|
|
|
tmpRng.moveToElementText(marker);
|
|
} else if (container.canHaveHTML) {
|
|
// Empty node selection for example <div>|</div>
|
|
// Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
|
|
container.innerHTML = '<span></span>';
|
|
marker = container.firstChild;
|
|
tmpRng.moveToElementText(marker);
|
|
tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
|
|
}
|
|
|
|
ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
|
|
dom.remove(marker);
|
|
}
|
|
}
|
|
|
|
// Setup some shorter versions
|
|
startContainer = rng.startContainer;
|
|
startOffset = rng.startOffset;
|
|
endContainer = rng.endContainer;
|
|
endOffset = rng.endOffset;
|
|
ieRng = body.createTextRange();
|
|
|
|
// If single element selection then try making a control selection out of it
|
|
if (startContainer == endContainer && startContainer.nodeType == 1) {
|
|
// Trick to place the caret inside an empty block element like <p></p>
|
|
if (startOffset == endOffset && !startContainer.hasChildNodes()) {
|
|
if (startContainer.canHaveHTML) {
|
|
// Check if previous sibling is an empty block if it is then we need to render it
|
|
// IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236
|
|
// Example this: <p></p><p>|</p> would become this: <p>|</p><p></p>
|
|
sibling = startContainer.previousSibling;
|
|
if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
|
|
sibling.innerHTML = '';
|
|
} else {
|
|
sibling = null;
|
|
}
|
|
|
|
startContainer.innerHTML = '<span></span><span></span>';
|
|
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 = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
|
|
tmpElm.removeChild(tmpElm.firstChild);
|
|
} else {
|
|
tmpElm.innerHTML = rng.toString();
|
|
}
|
|
|
|
// Keep whitespace before and after
|
|
if (/^\s/.test(tmpElm.innerHTML)) {
|
|
whiteSpaceBefore = ' ';
|
|
}
|
|
|
|
if (/\s+$/.test(tmpElm.innerHTML)) {
|
|
whiteSpaceAfter = ' ';
|
|
}
|
|
|
|
args.getInner = true;
|
|
|
|
args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
|
|
self.editor.fire('GetContent', args);
|
|
|
|
return args.content;
|
|
},
|
|
|
|
/**
|
|
* Sets the current selection to the specified content. If any contents is selected it will be replaced
|
|
* with the contents passed in to this function. If there is no selection the contents will be inserted
|
|
* where the caret is placed in the editor/page.
|
|
*
|
|
* @method setContent
|
|
* @param {String} content HTML contents to set could also be other formats depending on settings.
|
|
* @param {Object} args Optional settings object with for example data format.
|
|
* @example
|
|
* // Inserts some HTML contents at the current selection
|
|
* tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
|
|
*/
|
|
setContent: function (content, args) {
|
|
var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
|
|
|
|
args = args || { format: 'html' };
|
|
args.set = true;
|
|
args.selection = true;
|
|
args.content = content;
|
|
|
|
// Dispatch before set content event
|
|
if (!args.no_events) {
|
|
self.editor.fire('BeforeSetContent', args);
|
|
}
|
|
|
|
content = args.content;
|
|
|
|
if (rng.insertNode) {
|
|
// Make caret marker since insertNode places the caret in the beginning of text after insert
|
|
content += '<span id="__caret">_</span>';
|
|
|
|
// Delete and insert new node
|
|
if (rng.startContainer == doc && rng.endContainer == doc) {
|
|
// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
|
|
doc.body.innerHTML = content;
|
|
} else {
|
|
rng.deleteContents();
|
|
|
|
if (doc.body.childNodes.length === 0) {
|
|
doc.body.innerHTML = content;
|
|
} else {
|
|
// createContextualFragment doesn't exists in IE 9 DOMRanges
|
|
if (rng.createContextualFragment) {
|
|
rng.insertNode(rng.createContextualFragment(content));
|
|
} else {
|
|
// Fake createContextualFragment call in IE 9
|
|
frag = doc.createDocumentFragment();
|
|
temp = doc.createElement('div');
|
|
|
|
frag.appendChild(temp);
|
|
temp.outerHTML = content;
|
|
|
|
rng.insertNode(frag);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move to caret marker
|
|
caretNode = self.dom.get('__caret');
|
|
|
|
// Make sure we wrap it compleatly, Opera fails with a simple select call
|
|
rng = doc.createRange();
|
|
rng.setStartBefore(caretNode);
|
|
rng.setEndBefore(caretNode);
|
|
self.setRng(rng);
|
|
|
|
// Remove the caret position
|
|
self.dom.remove('__caret');
|
|
|
|
try {
|
|
self.setRng(rng);
|
|
} catch (ex) {
|
|
// Might fail on Opera for some odd reason
|
|
}
|
|
} else {
|
|
if (rng.item) {
|
|
// Delete content and get caret text selection
|
|
doc.execCommand('Delete', false, null);
|
|
rng = self.getRng();
|
|
}
|
|
|
|
// Explorer removes spaces from the beginning of pasted contents
|
|
if (/^\s+/.test(content)) {
|
|
rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
|
|
self.dom.remove('__mce_tmp');
|
|
} else {
|
|
rng.pasteHTML(content);
|
|
}
|
|
}
|
|
|
|
// Dispatch set content event
|
|
if (!args.no_events) {
|
|
self.editor.fire('SetContent', args);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the start element of a selection range. If the start is in a text
|
|
* node the parent element will be returned.
|
|
*
|
|
* @method getStart
|
|
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
|
|
* @return {Element} Start element of selection range.
|
|
*/
|
|
getStart: function (real) {
|
|
var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
|
|
|
|
if (rng.duplicate || rng.item) {
|
|
// Control selection, return first item
|
|
if (rng.item) {
|
|
return rng.item(0);
|
|
}
|
|
|
|
// Get start element
|
|
checkRng = rng.duplicate();
|
|
checkRng.collapse(1);
|
|
startElement = checkRng.parentElement();
|
|
if (startElement.ownerDocument !== self.dom.doc) {
|
|
startElement = self.dom.getRoot();
|
|
}
|
|
|
|
// Check if range parent is inside the start element, then return the inner parent element
|
|
// This will fix issues when a single element is selected, IE would otherwise return the wrong start element
|
|
parentElement = node = rng.parentElement();
|
|
while ((node = node.parentNode)) {
|
|
if (node == startElement) {
|
|
startElement = parentElement;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return startElement;
|
|
}
|
|
|
|
startElement = rng.startContainer;
|
|
|
|
if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
|
|
if (!real || !rng.collapsed) {
|
|
startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
|
|
}
|
|
}
|
|
|
|
if (startElement && startElement.nodeType == 3) {
|
|
return startElement.parentNode;
|
|
}
|
|
|
|
return startElement;
|
|
},
|
|
|
|
/**
|
|
* Returns the end element of a selection range. If the end is in a text
|
|
* node the parent element will be returned.
|
|
*
|
|
* @method getEnd
|
|
* @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element.
|
|
* @return {Element} End element of selection range.
|
|
*/
|
|
getEnd: function (real) {
|
|
var self = this, rng = self.getRng(), endElement, endOffset;
|
|
|
|
if (rng.duplicate || rng.item) {
|
|
if (rng.item) {
|
|
return rng.item(0);
|
|
}
|
|
|
|
rng = rng.duplicate();
|
|
rng.collapse(0);
|
|
endElement = rng.parentElement();
|
|
if (endElement.ownerDocument !== self.dom.doc) {
|
|
endElement = self.dom.getRoot();
|
|
}
|
|
|
|
if (endElement && endElement.nodeName == 'BODY') {
|
|
return endElement.lastChild || endElement;
|
|
}
|
|
|
|
return endElement;
|
|
}
|
|
|
|
endElement = rng.endContainer;
|
|
endOffset = rng.endOffset;
|
|
|
|
if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
|
|
if (!real || !rng.collapsed) {
|
|
endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
|
|
}
|
|
}
|
|
|
|
if (endElement && endElement.nodeType == 3) {
|
|
return endElement.parentNode;
|
|
}
|
|
|
|
return endElement;
|
|
},
|
|
|
|
/**
|
|
* Returns a bookmark location for the current selection. This bookmark object
|
|
* can then be used to restore the selection after some content modification to the document.
|
|
*
|
|
* @method getBookmark
|
|
* @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
|
|
* @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
|
|
* @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
|
|
* @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);
|
|
*/
|
|
getBookmark: function (type, normalized) {
|
|
return this.bookmarkManager.getBookmark(type, normalized);
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
*/
|
|
moveToBookmark: function (bookmark) {
|
|
return this.bookmarkManager.moveToBookmark(bookmark);
|
|
},
|
|
|
|
/**
|
|
* Selects the specified element. This will place the start and end of the selection range around the element.
|
|
*
|
|
* @method select
|
|
* @param {Element} node HTML DOM element to select.
|
|
* @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
|
|
* @return {Element} Selected element the same element as the one that got passed in.
|
|
* @example
|
|
* // Select the first paragraph in the active editor
|
|
* tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
|
|
*/
|
|
select: function (node, content) {
|
|
var self = this, dom = self.dom, rng = dom.createRng(), idx;
|
|
|
|
// Clear stored range set by FocusManager
|
|
self.lastFocusBookmark = null;
|
|
|
|
if (node) {
|
|
if (!content && self.controlSelection.controlSelect(node)) {
|
|
return;
|
|
}
|
|
|
|
idx = dom.nodeIndex(node);
|
|
rng.setStart(node.parentNode, idx);
|
|
rng.setEnd(node.parentNode, idx + 1);
|
|
|
|
// Find first/last text node or BR element
|
|
if (content) {
|
|
self._moveEndPoint(rng, node, true);
|
|
self._moveEndPoint(rng, node);
|
|
}
|
|
|
|
self.setRng(rng);
|
|
}
|
|
|
|
return node;
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
|
|
*
|
|
* @method isCollapsed
|
|
* @return {Boolean} true/false state if the selection range is collapsed or not.
|
|
* Collapsed means if it's a caret or a larger selection.
|
|
*/
|
|
isCollapsed: function () {
|
|
var self = this, rng = self.getRng(), sel = self.getSel();
|
|
|
|
if (!rng || rng.item) {
|
|
return false;
|
|
}
|
|
|
|
if (rng.compareEndPoints) {
|
|
return rng.compareEndPoints('StartToEnd', rng) === 0;
|
|
}
|
|
|
|
return !sel || rng.collapsed;
|
|
},
|
|
|
|
/**
|
|
* Collapse the selection to start or end of range.
|
|
*
|
|
* @method collapse
|
|
* @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false.
|
|
*/
|
|
collapse: function (toStart) {
|
|
var self = this, rng = self.getRng(), node;
|
|
|
|
// Control range on IE
|
|
if (rng.item) {
|
|
node = rng.item(0);
|
|
rng = self.win.document.body.createTextRange();
|
|
rng.moveToElementText(node);
|
|
}
|
|
|
|
rng.collapse(!!toStart);
|
|
self.setRng(rng);
|
|
},
|
|
|
|
/**
|
|
* Returns the browsers internal selection object.
|
|
*
|
|
* @method getSel
|
|
* @return {Selection} Internal browser selection object.
|
|
*/
|
|
getSel: function () {
|
|
var win = this.win;
|
|
|
|
return win.getSelection ? win.getSelection() : win.document.selection;
|
|
},
|
|
|
|
/**
|
|
* Returns the browsers internal range object.
|
|
*
|
|
* @method getRng
|
|
* @param {Boolean} w3c Forces a compatible W3C range on IE.
|
|
* @return {Range} Internal browser range object.
|
|
* @see http://www.quirksmode.org/dom/range_intro.html
|
|
* @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
|
|
*/
|
|
getRng: function (w3c) {
|
|
var self = this, selection, rng, elm, doc, ieRng, evt;
|
|
|
|
function tryCompareBoundaryPoints(how, sourceRange, destinationRange) {
|
|
try {
|
|
return sourceRange.compareBoundaryPoints(how, destinationRange);
|
|
} catch (ex) {
|
|
// Gecko throws wrong document exception if the range points
|
|
// to nodes that where removed from the dom #6690
|
|
// Browsers should mutate existing DOMRange instances so that they always point
|
|
// to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
|
|
// For performance reasons just return -1
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!self.win) {
|
|
return null;
|
|
}
|
|
|
|
doc = self.win.document;
|
|
|
|
if (typeof doc === 'undefined' || doc === null) {
|
|
return null;
|
|
}
|
|
|
|
// Use last rng passed from FocusManager if it's available this enables
|
|
// calls to editor.selection.getStart() to work when caret focus is lost on IE
|
|
if (!w3c && self.lastFocusBookmark) {
|
|
var bookmark = self.lastFocusBookmark;
|
|
|
|
// Convert bookmark to range IE 11 fix
|
|
if (bookmark.startContainer) {
|
|
rng = doc.createRange();
|
|
rng.setStart(bookmark.startContainer, bookmark.startOffset);
|
|
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
|
|
} else {
|
|
rng = bookmark;
|
|
}
|
|
|
|
return rng;
|
|
}
|
|
|
|
// Found tridentSel object then we need to use that one
|
|
if (w3c && self.tridentSel) {
|
|
return self.tridentSel.getRangeAt(0);
|
|
}
|
|
|
|
try {
|
|
if ((selection = self.getSel())) {
|
|
if (selection.rangeCount > 0) {
|
|
rng = selection.getRangeAt(0);
|
|
} else {
|
|
rng = selection.createRange ? selection.createRange() : doc.createRange();
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
|
|
}
|
|
|
|
evt = self.editor.fire('GetSelectionRange', { range: rng });
|
|
if (evt.range !== rng) {
|
|
return evt.range;
|
|
}
|
|
|
|
// We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
|
|
// IE 11 doesn't support the selection object so we check for that as well
|
|
if (isIE && rng && rng.setStart && doc.selection) {
|
|
try {
|
|
// IE will sometimes throw an exception here
|
|
ieRng = doc.selection.createRange();
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
|
|
if (ieRng && ieRng.item) {
|
|
elm = ieRng.item(0);
|
|
rng = doc.createRange();
|
|
rng.setStartBefore(elm);
|
|
rng.setEndAfter(elm);
|
|
}
|
|
}
|
|
|
|
// No range found then create an empty one
|
|
// This can occur when the editor is placed in a hidden container element on Gecko
|
|
// Or on IE when there was an exception
|
|
if (!rng) {
|
|
rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
|
|
}
|
|
|
|
// If range is at start of document then move it to start of body
|
|
if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
|
|
elm = self.dom.getRoot();
|
|
rng.setStart(elm, 0);
|
|
rng.setEnd(elm, 0);
|
|
}
|
|
|
|
if (self.selectedRange && self.explicitRange) {
|
|
if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
|
|
tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
|
|
// Safari, Opera and Chrome only ever select text which causes the range to change.
|
|
// This lets us use the originally set range if the selection hasn't been changed by the user.
|
|
rng = self.explicitRange;
|
|
} else {
|
|
self.selectedRange = null;
|
|
self.explicitRange = null;
|
|
}
|
|
}
|
|
|
|
return rng;
|
|
},
|
|
|
|
/**
|
|
* Changes the selection to the specified DOM range.
|
|
*
|
|
* @method setRng
|
|
* @param {Range} rng Range to select.
|
|
* @param {Boolean} forward Optional boolean if the selection is forwards or backwards.
|
|
*/
|
|
setRng: function (rng, forward) {
|
|
var self = this, sel, node, evt;
|
|
|
|
if (!rng) {
|
|
return;
|
|
}
|
|
|
|
// Is IE specific range
|
|
if (rng.select) {
|
|
self.explicitRange = null;
|
|
|
|
try {
|
|
rng.select();
|
|
} catch (ex) {
|
|
// Needed for some odd IE bug #1843306
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!self.tridentSel) {
|
|
sel = self.getSel();
|
|
|
|
evt = self.editor.fire('SetSelectionRange', { range: rng });
|
|
rng = evt.range;
|
|
|
|
if (sel) {
|
|
self.explicitRange = rng;
|
|
|
|
try {
|
|
sel.removeAllRanges();
|
|
sel.addRange(rng);
|
|
} catch (ex) {
|
|
// IE might throw errors here if the editor is within a hidden container and selection is changed
|
|
}
|
|
|
|
// Forward is set to false and we have an extend function
|
|
if (forward === false && sel.extend) {
|
|
sel.collapse(rng.endContainer, rng.endOffset);
|
|
sel.extend(rng.startContainer, rng.startOffset);
|
|
}
|
|
|
|
// adding range isn't always successful so we need to check range count otherwise an exception can occur
|
|
self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
}
|
|
|
|
// WebKit egde case selecting images works better using setBaseAndExtent when the image is floated
|
|
if (!rng.collapsed && rng.startContainer === rng.endContainer && sel.setBaseAndExtent && !Env.ie) {
|
|
if (rng.endOffset - rng.startOffset < 2) {
|
|
if (rng.startContainer.hasChildNodes()) {
|
|
node = rng.startContainer.childNodes[rng.startOffset];
|
|
if (node && node.tagName === 'IMG') {
|
|
sel.setBaseAndExtent(
|
|
rng.startContainer,
|
|
rng.startOffset,
|
|
rng.endContainer,
|
|
rng.endOffset
|
|
);
|
|
|
|
// Since the setBaseAndExtent is fixed in more recent Blink versions we
|
|
// need to detect if it's doing the wrong thing and falling back to the
|
|
// crazy incorrect behavior api call since that seems to be the only way
|
|
// to get it to work on Safari WebKit as of 2017-02-23
|
|
if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) {
|
|
sel.setBaseAndExtent(node, 0, node, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.editor.fire('AfterSetSelectionRange', { range: rng });
|
|
} else {
|
|
// Is W3C Range fake range on IE
|
|
if (rng.cloneRange) {
|
|
try {
|
|
self.tridentSel.addRange(rng);
|
|
} catch (ex) {
|
|
//IE9 throws an error here if called before selection is placed in the editor
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the current selection to the specified DOM element.
|
|
*
|
|
* @method setNode
|
|
* @param {Element} elm Element to set as the contents of the selection.
|
|
* @return {Element} Returns the element that got passed in.
|
|
* @example
|
|
* // Inserts a DOM node at current selection/caret location
|
|
* tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
|
|
*/
|
|
setNode: function (elm) {
|
|
var self = this;
|
|
|
|
self.setContent(self.dom.getOuterHTML(elm));
|
|
|
|
return elm;
|
|
},
|
|
|
|
/**
|
|
* Returns the currently selected element or the common ancestor element for both start and end of the selection.
|
|
*
|
|
* @method getNode
|
|
* @return {Element} Currently selected element or common ancestor element.
|
|
* @example
|
|
* // Alerts the currently selected elements node name
|
|
* alert(tinymce.activeEditor.selection.getNode().nodeName);
|
|
*/
|
|
getNode: function () {
|
|
var self = this, rng = self.getRng(), elm;
|
|
var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot();
|
|
|
|
function skipEmptyTextNodes(node, forwards) {
|
|
var orig = node;
|
|
|
|
while (node && node.nodeType === 3 && node.length === 0) {
|
|
node = forwards ? node.nextSibling : node.previousSibling;
|
|
}
|
|
|
|
return node || orig;
|
|
}
|
|
|
|
// Range maybe lost after the editor is made visible again
|
|
if (!rng) {
|
|
return root;
|
|
}
|
|
|
|
startContainer = rng.startContainer;
|
|
endContainer = rng.endContainer;
|
|
startOffset = rng.startOffset;
|
|
endOffset = rng.endOffset;
|
|
|
|
if (rng.setStart) {
|
|
elm = rng.commonAncestorContainer;
|
|
|
|
// Handle selection a image or other control like element such as anchors
|
|
if (!rng.collapsed) {
|
|
if (startContainer == endContainer) {
|
|
if (endOffset - startOffset < 2) {
|
|
if (startContainer.hasChildNodes()) {
|
|
elm = startContainer.childNodes[startOffset];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the anchor node is a element instead of a text node then return this element
|
|
//if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
|
|
// return sel.anchorNode.childNodes[sel.anchorOffset];
|
|
|
|
// Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
|
|
// This happens when you double click an underlined word in FireFox.
|
|
if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
|
|
if (startContainer.length === startOffset) {
|
|
startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
|
|
} else {
|
|
startContainer = startContainer.parentNode;
|
|
}
|
|
|
|
if (endOffset === 0) {
|
|
endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
|
|
} else {
|
|
endContainer = endContainer.parentNode;
|
|
}
|
|
|
|
if (startContainer && startContainer === endContainer) {
|
|
return startContainer;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (elm && elm.nodeType == 3) {
|
|
return elm.parentNode;
|
|
}
|
|
|
|
return elm;
|
|
}
|
|
|
|
elm = rng.item ? rng.item(0) : rng.parentElement();
|
|
|
|
// IE 7 might return elements outside the iframe
|
|
if (elm.ownerDocument !== self.win.document) {
|
|
elm = root;
|
|
}
|
|
|
|
return elm;
|
|
},
|
|
|
|
getSelectedBlocks: function (startElm, endElm) {
|
|
var self = this, dom = self.dom, node, root, selectedBlocks = [];
|
|
|
|
root = dom.getRoot();
|
|
startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
|
|
endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
|
|
|
|
if (startElm && startElm != root) {
|
|
selectedBlocks.push(startElm);
|
|
}
|
|
|
|
if (startElm && endElm && startElm != endElm) {
|
|
node = startElm;
|
|
|
|
var walker = new TreeWalker(startElm, root);
|
|
while ((node = walker.next()) && node != endElm) {
|
|
if (dom.isBlock(node)) {
|
|
selectedBlocks.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (endElm && startElm != endElm && endElm != root) {
|
|
selectedBlocks.push(endElm);
|
|
}
|
|
|
|
return selectedBlocks;
|
|
},
|
|
|
|
isForward: function () {
|
|
var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
|
|
|
|
// No support for selection direction then always return true
|
|
if (!sel || !sel.anchorNode || !sel.focusNode) {
|
|
return true;
|
|
}
|
|
|
|
anchorRange = dom.createRng();
|
|
anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
|
|
anchorRange.collapse(true);
|
|
|
|
focusRange = dom.createRng();
|
|
focusRange.setStart(sel.focusNode, sel.focusOffset);
|
|
focusRange.collapse(true);
|
|
|
|
return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
|
|
},
|
|
|
|
normalize: function () {
|
|
var self = this, rng = self.getRng();
|
|
|
|
if (Env.range && new RangeUtils(self.dom).normalize(rng)) {
|
|
self.setRng(rng, self.isForward());
|
|
}
|
|
|
|
return rng;
|
|
},
|
|
|
|
/**
|
|
* Executes callback when the current selection starts/stops matching the specified selector. The current
|
|
* state will be passed to the callback as it's first argument.
|
|
*
|
|
* @method selectorChanged
|
|
* @param {String} selector CSS selector to check for.
|
|
* @param {function} callback Callback with state and args when the selector is matches or not.
|
|
*/
|
|
selectorChanged: function (selector, callback) {
|
|
var self = this, currentSelectors;
|
|
|
|
if (!self.selectorChangedData) {
|
|
self.selectorChangedData = {};
|
|
currentSelectors = {};
|
|
|
|
self.editor.on('NodeChange', function (e) {
|
|
var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
|
|
|
|
// Check for new matching selectors
|
|
each(self.selectorChangedData, function (callbacks, selector) {
|
|
each(parents, function (node) {
|
|
if (dom.is(node, selector)) {
|
|
if (!currentSelectors[selector]) {
|
|
// Execute callbacks
|
|
each(callbacks, function (callback) {
|
|
callback(true, { node: node, selector: selector, parents: parents });
|
|
});
|
|
|
|
currentSelectors[selector] = callbacks;
|
|
}
|
|
|
|
matchedSelectors[selector] = callbacks;
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Check if current selectors still match
|
|
each(currentSelectors, function (callbacks, selector) {
|
|
if (!matchedSelectors[selector]) {
|
|
delete currentSelectors[selector];
|
|
|
|
each(callbacks, function (callback) {
|
|
callback(false, { node: node, selector: selector, parents: parents });
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Add selector listeners
|
|
if (!self.selectorChangedData[selector]) {
|
|
self.selectorChangedData[selector] = [];
|
|
}
|
|
|
|
self.selectorChangedData[selector].push(callback);
|
|
|
|
return self;
|
|
},
|
|
|
|
getScrollContainer: function () {
|
|
var scrollContainer, node = this.dom.getRoot();
|
|
|
|
while (node && node.nodeName != 'BODY') {
|
|
if (node.scrollHeight > node.clientHeight) {
|
|
scrollContainer = node;
|
|
break;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return scrollContainer;
|
|
},
|
|
|
|
scrollIntoView: function (elm, alignToTop) {
|
|
ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop);
|
|
},
|
|
|
|
placeCaretAt: function (clientX, clientY) {
|
|
this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc()));
|
|
},
|
|
|
|
_moveEndPoint: function (rng, node, start) {
|
|
var root = node, walker = new TreeWalker(node, root);
|
|
var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
|
|
|
|
do {
|
|
// Text node
|
|
if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
|
|
if (start) {
|
|
rng.setStart(node, 0);
|
|
} else {
|
|
rng.setEnd(node, node.nodeValue.length);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// BR/IMG/INPUT elements but not table cells
|
|
if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) {
|
|
if (start) {
|
|
rng.setStartBefore(node);
|
|
} else {
|
|
if (node.nodeName == 'BR') {
|
|
rng.setEndBefore(node);
|
|
} else {
|
|
rng.setEndAfter(node);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Found empty text block old IE can place the selection inside those
|
|
if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
|
|
if (start) {
|
|
rng.setStart(node, 0);
|
|
} else {
|
|
rng.setEnd(node, 0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
} while ((node = (start ? walker.next() : walker.prev())));
|
|
|
|
// Failed to find any text node or other suitable location then move to the root of body
|
|
if (root.nodeName == 'BODY') {
|
|
if (start) {
|
|
rng.setStart(root, 0);
|
|
} else {
|
|
rng.setEnd(root, root.childNodes.length);
|
|
}
|
|
}
|
|
},
|
|
|
|
getBoundingClientRect: function () {
|
|
var rng = this.getRng();
|
|
return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
|
|
},
|
|
|
|
destroy: function () {
|
|
this.win = null;
|
|
this.controlSelection.destroy();
|
|
}
|
|
};
|
|
|
|
return Selection;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ElementUtils.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 various element specific functions.
|
|
*
|
|
* @private
|
|
* @class tinymce.dom.ElementUtils
|
|
*/
|
|
define(
|
|
'tinymce.core.dom.ElementUtils',
|
|
[
|
|
"tinymce.core.dom.BookmarkManager",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (BookmarkManager, Tools) {
|
|
var each = Tools.each;
|
|
|
|
function ElementUtils(dom) {
|
|
/**
|
|
* Compares two nodes and checks if it's attributes and styles matches.
|
|
* This doesn't compare classes as items since their order is significant.
|
|
*
|
|
* @method compare
|
|
* @param {Node} node1 First node to compare with.
|
|
* @param {Node} node2 Second node to compare with.
|
|
* @return {boolean} True/false if the nodes are the same or not.
|
|
*/
|
|
this.compare = function (node1, node2) {
|
|
// Not the same name
|
|
if (node1.nodeName != node2.nodeName) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns all the nodes attributes excluding internal ones, styles and classes.
|
|
*
|
|
* @private
|
|
* @param {Node} node Node to get attributes from.
|
|
* @return {Object} Name/value object with attributes and attribute values.
|
|
*/
|
|
function getAttribs(node) {
|
|
var attribs = {};
|
|
|
|
each(dom.getAttribs(node), function (attr) {
|
|
var name = attr.nodeName.toLowerCase();
|
|
|
|
// Don't compare internal attributes or style
|
|
if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) {
|
|
attribs[name] = dom.getAttrib(node, name);
|
|
}
|
|
});
|
|
|
|
return attribs;
|
|
}
|
|
|
|
/**
|
|
* Compares two objects checks if it's key + value exists in the other one.
|
|
*
|
|
* @private
|
|
* @param {Object} obj1 First object to compare.
|
|
* @param {Object} obj2 Second object to compare.
|
|
* @return {boolean} True/false if the objects matches or not.
|
|
*/
|
|
function compareObjects(obj1, obj2) {
|
|
var value, name;
|
|
|
|
for (name in obj1) {
|
|
// Obj1 has item obj2 doesn't have
|
|
if (obj1.hasOwnProperty(name)) {
|
|
value = obj2[name];
|
|
|
|
// Obj2 doesn't have obj1 item
|
|
if (typeof value == "undefined") {
|
|
return false;
|
|
}
|
|
|
|
// Obj2 item has a different value
|
|
if (obj1[name] != value) {
|
|
return false;
|
|
}
|
|
|
|
// Delete similar value
|
|
delete obj2[name];
|
|
}
|
|
}
|
|
|
|
// Check if obj 2 has something obj 1 doesn't have
|
|
for (name in obj2) {
|
|
// Obj2 has item obj1 doesn't have
|
|
if (obj2.hasOwnProperty(name)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Attribs are not the same
|
|
if (!compareObjects(getAttribs(node1), getAttribs(node2))) {
|
|
return false;
|
|
}
|
|
|
|
// Styles are not the same
|
|
if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) {
|
|
return false;
|
|
}
|
|
|
|
return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2);
|
|
};
|
|
}
|
|
|
|
return ElementUtils;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Preview.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
|
|
*/
|
|
|
|
/**
|
|
* Internal class for generating previews styles for formats.
|
|
*
|
|
* Example:
|
|
* Preview.getCssText(editor, 'bold');
|
|
*
|
|
* @private
|
|
* @class tinymce.fmt.Preview
|
|
*/
|
|
define(
|
|
'tinymce.core.fmt.Preview',
|
|
[
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.html.Schema"
|
|
],
|
|
function (DOMUtils, Tools, Schema) {
|
|
var each = Tools.each;
|
|
var dom = DOMUtils.DOM;
|
|
|
|
function parsedSelectorToHtml(ancestry, editor) {
|
|
var elm, item, fragment;
|
|
var schema = editor && editor.schema || new Schema({});
|
|
|
|
function decorate(elm, item) {
|
|
if (item.classes.length) {
|
|
dom.addClass(elm, item.classes.join(' '));
|
|
}
|
|
dom.setAttribs(elm, item.attrs);
|
|
}
|
|
|
|
function createElement(sItem) {
|
|
var elm;
|
|
|
|
item = typeof sItem === 'string' ? {
|
|
name: sItem,
|
|
classes: [],
|
|
attrs: {}
|
|
} : sItem;
|
|
|
|
elm = dom.create(item.name);
|
|
decorate(elm, item);
|
|
return elm;
|
|
}
|
|
|
|
function getRequiredParent(elm, candidate) {
|
|
var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm;
|
|
var elmRule = schema.getElementRule(name);
|
|
var parentsRequired = elmRule.parentsRequired;
|
|
|
|
if (parentsRequired && parentsRequired.length) {
|
|
return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function wrapInHtml(elm, ancestry, siblings) {
|
|
var parent, parentCandidate, parentRequired;
|
|
var ancestor = ancestry.length && ancestry[0];
|
|
var ancestorName = ancestor && ancestor.name;
|
|
|
|
parentRequired = getRequiredParent(elm, ancestorName);
|
|
|
|
if (parentRequired) {
|
|
if (ancestorName == parentRequired) {
|
|
parentCandidate = ancestry[0];
|
|
ancestry = ancestry.slice(1);
|
|
} else {
|
|
parentCandidate = parentRequired;
|
|
}
|
|
} else if (ancestor) {
|
|
parentCandidate = ancestry[0];
|
|
ancestry = ancestry.slice(1);
|
|
} else if (!siblings) {
|
|
return elm;
|
|
}
|
|
|
|
if (parentCandidate) {
|
|
parent = createElement(parentCandidate);
|
|
parent.appendChild(elm);
|
|
}
|
|
|
|
if (siblings) {
|
|
if (!parent) {
|
|
// if no more ancestry, wrap in generic div
|
|
parent = dom.create('div');
|
|
parent.appendChild(elm);
|
|
}
|
|
|
|
Tools.each(siblings, function (sibling) {
|
|
var siblingElm = createElement(sibling);
|
|
parent.insertBefore(siblingElm, elm);
|
|
});
|
|
}
|
|
|
|
return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings);
|
|
}
|
|
|
|
if (ancestry && ancestry.length) {
|
|
item = ancestry[0];
|
|
elm = createElement(item);
|
|
fragment = dom.create('div');
|
|
fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings));
|
|
return fragment;
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
|
|
function selectorToHtml(selector, editor) {
|
|
return parsedSelectorToHtml(parseSelector(selector), editor);
|
|
}
|
|
|
|
|
|
function parseSelectorItem(item) {
|
|
var tagName;
|
|
var obj = {
|
|
classes: [],
|
|
attrs: {}
|
|
};
|
|
|
|
item = obj.selector = Tools.trim(item);
|
|
|
|
if (item !== '*') {
|
|
// matching IDs, CLASSes, ATTRIBUTES and PSEUDOs
|
|
tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function ($0, $1, $2, $3, $4) {
|
|
switch ($1) {
|
|
case '#':
|
|
obj.attrs.id = $2;
|
|
break;
|
|
|
|
case '.':
|
|
obj.classes.push($2);
|
|
break;
|
|
|
|
case ':':
|
|
if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) {
|
|
obj.attrs[$2] = $2;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// atribute matched
|
|
if ($3 == '[') {
|
|
var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/);
|
|
if (m) {
|
|
obj.attrs[m[1]] = m[2];
|
|
}
|
|
}
|
|
|
|
return '';
|
|
});
|
|
}
|
|
|
|
obj.name = tagName || 'div';
|
|
return obj;
|
|
}
|
|
|
|
|
|
function parseSelector(selector) {
|
|
if (!selector || typeof selector !== 'string') {
|
|
return [];
|
|
}
|
|
|
|
// take into account only first one
|
|
selector = selector.split(/\s*,\s*/)[0];
|
|
|
|
// tighten
|
|
selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1');
|
|
|
|
// split either on > or on space, but not the one inside brackets
|
|
return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function (item) {
|
|
// process each sibling selector separately
|
|
var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem);
|
|
var obj = siblings.pop(); // the last one is our real target
|
|
|
|
if (siblings.length) {
|
|
obj.siblings = siblings;
|
|
}
|
|
return obj;
|
|
}).reverse();
|
|
}
|
|
|
|
|
|
function getCssText(editor, format) {
|
|
var name, previewFrag, previewElm, items;
|
|
var previewCss = '', parentFontSize, previewStyles;
|
|
|
|
previewStyles = editor.settings.preview_styles;
|
|
|
|
// No preview forced
|
|
if (previewStyles === false) {
|
|
return '';
|
|
}
|
|
|
|
// Default preview
|
|
if (typeof previewStyles !== 'string') {
|
|
previewStyles = 'font-family font-size font-weight font-style text-decoration ' +
|
|
'text-transform color background-color border border-radius outline text-shadow';
|
|
}
|
|
|
|
// Removes any variables since these can't be previewed
|
|
function removeVars(val) {
|
|
return val.replace(/%(\w+)/g, '');
|
|
}
|
|
|
|
// Create block/inline element to use for preview
|
|
if (typeof format == "string") {
|
|
format = editor.formatter.get(format);
|
|
if (!format) {
|
|
return;
|
|
}
|
|
|
|
format = format[0];
|
|
}
|
|
|
|
// Format specific preview override
|
|
// TODO: This should probably be further reduced by the previewStyles option
|
|
if ('preview' in format) {
|
|
previewStyles = format.preview;
|
|
if (previewStyles === false) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
name = format.block || format.inline || 'span';
|
|
|
|
items = parseSelector(format.selector);
|
|
if (items.length) {
|
|
if (!items[0].name) { // e.g. something like ul > .someClass was provided
|
|
items[0].name = name;
|
|
}
|
|
name = format.selector;
|
|
previewFrag = parsedSelectorToHtml(items, editor);
|
|
} else {
|
|
previewFrag = parsedSelectorToHtml([name], editor);
|
|
}
|
|
|
|
previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild;
|
|
|
|
// Add format styles to preview element
|
|
each(format.styles, function (value, name) {
|
|
value = removeVars(value);
|
|
|
|
if (value) {
|
|
dom.setStyle(previewElm, name, value);
|
|
}
|
|
});
|
|
|
|
// Add attributes to preview element
|
|
each(format.attributes, function (value, name) {
|
|
value = removeVars(value);
|
|
|
|
if (value) {
|
|
dom.setAttrib(previewElm, name, value);
|
|
}
|
|
});
|
|
|
|
// Add classes to preview element
|
|
each(format.classes, function (value) {
|
|
value = removeVars(value);
|
|
|
|
if (!dom.hasClass(previewElm, value)) {
|
|
dom.addClass(previewElm, value);
|
|
}
|
|
});
|
|
|
|
editor.fire('PreviewFormats');
|
|
|
|
// Add the previewElm outside the visual area
|
|
dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF });
|
|
editor.getBody().appendChild(previewFrag);
|
|
|
|
// Get parent container font size so we can compute px values out of em/% for older IE:s
|
|
parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true);
|
|
parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0;
|
|
|
|
each(previewStyles.split(' '), function (name) {
|
|
var value = dom.getStyle(previewElm, name, true);
|
|
|
|
// If background is transparent then check if the body has a background color we can use
|
|
if (name == 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) {
|
|
value = dom.getStyle(editor.getBody(), name, true);
|
|
|
|
// Ignore white since it's the default color, not the nicest fix
|
|
// TODO: Fix this by detecting runtime style
|
|
if (dom.toHex(value).toLowerCase() == '#ffffff') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (name == 'color') {
|
|
// Ignore black since it's the default color, not the nicest fix
|
|
// TODO: Fix this by detecting runtime style
|
|
if (dom.toHex(value).toLowerCase() == '#000000') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Old IE won't calculate the font size so we need to do that manually
|
|
if (name == 'font-size') {
|
|
if (/em|%$/.test(value)) {
|
|
if (parentFontSize === 0) {
|
|
return;
|
|
}
|
|
|
|
// Convert font size from em/% to px
|
|
value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1);
|
|
value = (value * parentFontSize) + 'px';
|
|
}
|
|
}
|
|
|
|
if (name == "border" && value) {
|
|
previewCss += 'padding:0 2px;';
|
|
}
|
|
|
|
previewCss += name + ':' + value + ';';
|
|
});
|
|
|
|
editor.fire('AfterPreviewFormats');
|
|
|
|
//previewCss += 'line-height:normal';
|
|
|
|
dom.remove(previewFrag);
|
|
|
|
return previewCss;
|
|
}
|
|
|
|
return {
|
|
getCssText: getCssText,
|
|
parseSelector: parseSelector,
|
|
selectorToHtml: selectorToHtml
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Hooks.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
|
|
*/
|
|
|
|
/**
|
|
* Internal class for overriding formatting.
|
|
*
|
|
* @private
|
|
* @class tinymce.fmt.Hooks
|
|
*/
|
|
define(
|
|
'tinymce.core.fmt.Hooks',
|
|
[
|
|
"tinymce.core.util.Arr",
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.dom.DomQuery"
|
|
],
|
|
function (Arr, NodeType, $) {
|
|
var postProcessHooks = {}, filter = Arr.filter, each = Arr.each;
|
|
|
|
function addPostProcessHook(name, hook) {
|
|
var hooks = postProcessHooks[name];
|
|
|
|
if (!hooks) {
|
|
postProcessHooks[name] = hooks = [];
|
|
}
|
|
|
|
postProcessHooks[name].push(hook);
|
|
}
|
|
|
|
function postProcess(name, editor) {
|
|
each(postProcessHooks[name], function (hook) {
|
|
hook(editor);
|
|
});
|
|
}
|
|
|
|
addPostProcessHook("pre", function (editor) {
|
|
var rng = editor.selection.getRng(), isPre, blocks;
|
|
|
|
function hasPreSibling(pre) {
|
|
return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) != -1;
|
|
}
|
|
|
|
function joinPre(pre1, pre2) {
|
|
$(pre2).remove();
|
|
$(pre1).append('<br><br>').append(pre2.childNodes);
|
|
}
|
|
|
|
isPre = NodeType.matchNodeNames('pre');
|
|
|
|
if (!rng.collapsed) {
|
|
blocks = editor.selection.getSelectedBlocks();
|
|
|
|
each(filter(filter(blocks, isPre), hasPreSibling), function (pre) {
|
|
joinPre(pre.previousSibling, pre);
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
postProcess: postProcess
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Formatter.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
|
|
*/
|
|
|
|
/**
|
|
* Text formatter engine class. This class is used to apply formats like bold, italic, font size
|
|
* etc to the current selection or specific nodes. This engine was built to replace the browser's
|
|
* default formatting logic for execCommand due to its inconsistent and buggy behavior.
|
|
*
|
|
* @class tinymce.Formatter
|
|
* @example
|
|
* tinymce.activeEditor.formatter.register('mycustomformat', {
|
|
* inline: 'span',
|
|
* styles: {color: '#ff0000'}
|
|
* });
|
|
*
|
|
* tinymce.activeEditor.formatter.apply('mycustomformat');
|
|
*/
|
|
define(
|
|
'tinymce.core.Formatter',
|
|
[
|
|
"tinymce.core.dom.TreeWalker",
|
|
"tinymce.core.dom.RangeUtils",
|
|
"tinymce.core.dom.BookmarkManager",
|
|
"tinymce.core.dom.ElementUtils",
|
|
"tinymce.core.dom.NodeType",
|
|
"tinymce.core.util.Fun",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.fmt.Preview",
|
|
"tinymce.core.fmt.Hooks"
|
|
],
|
|
function (TreeWalker, RangeUtils, BookmarkManager, ElementUtils, NodeType, Fun, Tools, Preview, Hooks) {
|
|
/**
|
|
* Constructs a new formatter instance.
|
|
*
|
|
* @constructor Formatter
|
|
* @param {tinymce.Editor} ed Editor instance to construct the formatter engine to.
|
|
*/
|
|
return function (ed) {
|
|
var formats = {},
|
|
dom = ed.dom,
|
|
selection = ed.selection,
|
|
rangeUtils = new RangeUtils(dom),
|
|
isValid = ed.schema.isValidChild,
|
|
isBlock = dom.isBlock,
|
|
forcedRootBlock = ed.settings.forced_root_block,
|
|
nodeIndex = dom.nodeIndex,
|
|
INVISIBLE_CHAR = '\uFEFF',
|
|
MCE_ATTR_RE = /^(src|href|style)$/,
|
|
FALSE = false,
|
|
TRUE = true,
|
|
formatChangeData,
|
|
undef,
|
|
getContentEditable = dom.getContentEditable,
|
|
disableCaretContainer,
|
|
markCaretContainersBogus,
|
|
isBookmarkNode = BookmarkManager.isBookmarkNode;
|
|
|
|
var each = Tools.each,
|
|
grep = Tools.grep,
|
|
walk = Tools.walk,
|
|
extend = Tools.extend;
|
|
|
|
function isTextBlock(name) {
|
|
if (name.nodeType) {
|
|
name = name.nodeName;
|
|
}
|
|
|
|
return !!ed.schema.getTextBlockElements()[name.toLowerCase()];
|
|
}
|
|
|
|
function isTableCell(node) {
|
|
return /^(TH|TD)$/.test(node.nodeName);
|
|
}
|
|
|
|
function isInlineBlock(node) {
|
|
return node && /^(IMG)$/.test(node.nodeName);
|
|
}
|
|
|
|
function getParents(node, selector) {
|
|
return dom.getParents(node, selector, dom.getRoot());
|
|
}
|
|
|
|
function isCaretNode(node) {
|
|
return node.nodeType === 1 && node.id === '_mce_caret';
|
|
}
|
|
|
|
function defaultFormats() {
|
|
register({
|
|
valigntop: [
|
|
{ selector: 'td,th', styles: { 'verticalAlign': 'top' } }
|
|
],
|
|
|
|
valignmiddle: [
|
|
{ selector: 'td,th', styles: { 'verticalAlign': 'middle' } }
|
|
],
|
|
|
|
valignbottom: [
|
|
{ selector: 'td,th', styles: { 'verticalAlign': 'bottom' } }
|
|
],
|
|
|
|
alignleft: [
|
|
{
|
|
selector: 'figure.image',
|
|
collapsed: false,
|
|
classes: 'align-left',
|
|
ceFalseOverride: true,
|
|
preview: 'font-family font-size'
|
|
},
|
|
{
|
|
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
|
|
styles: {
|
|
textAlign: 'left'
|
|
},
|
|
inherit: false,
|
|
preview: false,
|
|
defaultBlock: 'div'
|
|
},
|
|
{ selector: 'img,table', collapsed: false, styles: { 'float': 'left' }, preview: 'font-family font-size' }
|
|
],
|
|
|
|
aligncenter: [
|
|
{
|
|
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
|
|
styles: {
|
|
textAlign: 'center'
|
|
},
|
|
inherit: false,
|
|
preview: false,
|
|
defaultBlock: 'div'
|
|
},
|
|
{
|
|
selector: 'figure.image',
|
|
collapsed: false,
|
|
classes: 'align-center',
|
|
ceFalseOverride: true,
|
|
preview: 'font-family font-size'
|
|
},
|
|
{
|
|
selector: 'img',
|
|
collapsed: false,
|
|
styles: {
|
|
display: 'block',
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto'
|
|
},
|
|
preview: false
|
|
},
|
|
{
|
|
selector: 'table',
|
|
collapsed: false,
|
|
styles: {
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto'
|
|
},
|
|
preview: 'font-family font-size'
|
|
}
|
|
],
|
|
|
|
alignright: [
|
|
{
|
|
selector: 'figure.image',
|
|
collapsed: false,
|
|
classes: 'align-right',
|
|
ceFalseOverride: true,
|
|
preview: 'font-family font-size'
|
|
},
|
|
{
|
|
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
|
|
styles: {
|
|
textAlign: 'right'
|
|
},
|
|
inherit: false,
|
|
preview: 'font-family font-size',
|
|
defaultBlock: 'div'
|
|
},
|
|
{
|
|
selector: 'img,table',
|
|
collapsed: false,
|
|
styles: {
|
|
'float': 'right'
|
|
},
|
|
preview: 'font-family font-size'
|
|
}
|
|
],
|
|
|
|
alignjustify: [
|
|
{
|
|
selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li',
|
|
styles: {
|
|
textAlign: 'justify'
|
|
},
|
|
inherit: false,
|
|
defaultBlock: 'div',
|
|
preview: 'font-family font-size'
|
|
}
|
|
],
|
|
|
|
bold: [
|
|
{ inline: 'strong', remove: 'all' },
|
|
{ inline: 'span', styles: { fontWeight: 'bold' } },
|
|
{ inline: 'b', remove: 'all' }
|
|
],
|
|
|
|
italic: [
|
|
{ inline: 'em', remove: 'all' },
|
|
{ inline: 'span', styles: { fontStyle: 'italic' } },
|
|
{ inline: 'i', remove: 'all' }
|
|
],
|
|
|
|
underline: [
|
|
{ inline: 'span', styles: { textDecoration: 'underline' }, exact: true },
|
|
{ inline: 'u', remove: 'all' }
|
|
],
|
|
|
|
strikethrough: [
|
|
{ inline: 'span', styles: { textDecoration: 'line-through' }, exact: true },
|
|
{ inline: 'strike', remove: 'all' }
|
|
],
|
|
|
|
forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true },
|
|
hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true },
|
|
fontname: { inline: 'span', styles: { fontFamily: '%value' } },
|
|
fontsize: { inline: 'span', styles: { fontSize: '%value' } },
|
|
fontsize_class: { inline: 'span', attributes: { 'class': '%value' } },
|
|
blockquote: { block: 'blockquote', wrapper: 1, remove: 'all' },
|
|
subscript: { inline: 'sub' },
|
|
superscript: { inline: 'sup' },
|
|
code: { inline: 'code' },
|
|
|
|
link: {
|
|
inline: 'a', selector: 'a', remove: 'all', split: true, deep: true,
|
|
onmatch: function () {
|
|
return true;
|
|
},
|
|
|
|
onformat: function (elm, fmt, vars) {
|
|
each(vars, function (value, key) {
|
|
dom.setAttrib(elm, key, value);
|
|
});
|
|
}
|
|
},
|
|
|
|
removeformat: [
|
|
{
|
|
selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins',
|
|
remove: 'all',
|
|
split: true,
|
|
expand: false,
|
|
block_expand: true,
|
|
deep: true
|
|
},
|
|
{ selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true },
|
|
{ selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true }
|
|
]
|
|
});
|
|
|
|
// Register default block formats
|
|
each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function (name) {
|
|
register(name, { block: name, remove: 'all' });
|
|
});
|
|
|
|
// Register user defined formats
|
|
register(ed.settings.formats);
|
|
}
|
|
|
|
function addKeyboardShortcuts() {
|
|
// Add some inline shortcuts
|
|
ed.addShortcut('meta+b', 'bold_desc', 'Bold');
|
|
ed.addShortcut('meta+i', 'italic_desc', 'Italic');
|
|
ed.addShortcut('meta+u', 'underline_desc', 'Underline');
|
|
|
|
// BlockFormat shortcuts keys
|
|
for (var i = 1; i <= 6; i++) {
|
|
ed.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]);
|
|
}
|
|
|
|
ed.addShortcut('access+7', '', ['FormatBlock', false, 'p']);
|
|
ed.addShortcut('access+8', '', ['FormatBlock', false, 'div']);
|
|
ed.addShortcut('access+9', '', ['FormatBlock', false, 'address']);
|
|
}
|
|
|
|
// Public functions
|
|
|
|
/**
|
|
* Returns the format by name or all formats if no name is specified.
|
|
*
|
|
* @method get
|
|
* @param {String} name Optional name to retrieve by.
|
|
* @return {Array/Object} Array/Object with all registered formats or a specific format.
|
|
*/
|
|
function get(name) {
|
|
return name ? formats[name] : formats;
|
|
}
|
|
|
|
/**
|
|
* Registers a specific format by name.
|
|
*
|
|
* @method register
|
|
* @param {Object/String} name Name of the format for example "bold".
|
|
* @param {Object/Array} format Optional format object or array of format variants
|
|
* can only be omitted if the first arg is an object.
|
|
*/
|
|
function register(name, format) {
|
|
if (name) {
|
|
if (typeof name !== 'string') {
|
|
each(name, function (format, name) {
|
|
register(name, format);
|
|
});
|
|
} else {
|
|
// Force format into array and add it to internal collection
|
|
format = format.length ? format : [format];
|
|
|
|
each(format, function (format) {
|
|
// Set deep to false by default on selector formats this to avoid removing
|
|
// alignment on images inside paragraphs when alignment is changed on paragraphs
|
|
if (format.deep === undef) {
|
|
format.deep = !format.selector;
|
|
}
|
|
|
|
// Default to true
|
|
if (format.split === undef) {
|
|
format.split = !format.selector || format.inline;
|
|
}
|
|
|
|
// Default to true
|
|
if (format.remove === undef && format.selector && !format.inline) {
|
|
format.remove = 'none';
|
|
}
|
|
|
|
// Mark format as a mixed format inline + block level
|
|
if (format.selector && format.inline) {
|
|
format.mixed = true;
|
|
format.block_expand = true;
|
|
}
|
|
|
|
// Split classes if needed
|
|
if (typeof format.classes === 'string') {
|
|
format.classes = format.classes.split(/\s+/);
|
|
}
|
|
});
|
|
|
|
formats[name] = format;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister a specific format by name.
|
|
*
|
|
* @method unregister
|
|
* @param {String} name Name of the format for example "bold".
|
|
*/
|
|
function unregister(name) {
|
|
if (name && formats[name]) {
|
|
delete formats[name];
|
|
}
|
|
|
|
return formats;
|
|
}
|
|
|
|
function matchesUnInheritedFormatSelector(node, name) {
|
|
var formatList = get(name);
|
|
|
|
if (formatList) {
|
|
for (var i = 0; i < formatList.length; i++) {
|
|
if (formatList[i].inherit === false && dom.is(node, formatList[i].selector)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getTextDecoration(node) {
|
|
var decoration;
|
|
|
|
ed.dom.getParent(node, function (n) {
|
|
decoration = ed.dom.getStyle(n, 'text-decoration');
|
|
return decoration && decoration !== 'none';
|
|
});
|
|
|
|
return decoration;
|
|
}
|
|
|
|
function processUnderlineAndColor(node) {
|
|
var textDecoration;
|
|
if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) {
|
|
textDecoration = getTextDecoration(node.parentNode);
|
|
if (ed.dom.getStyle(node, 'color') && textDecoration) {
|
|
ed.dom.setStyle(node, 'text-decoration', textDecoration);
|
|
} else if (ed.dom.getStyle(node, 'text-decoration') === textDecoration) {
|
|
ed.dom.setStyle(node, 'text-decoration', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies the specified format to the current selection or specified node.
|
|
*
|
|
* @method apply
|
|
* @param {String} name Name of format to apply.
|
|
* @param {Object} vars Optional list of variables to replace within format before applying it.
|
|
* @param {Node} node Optional node to apply the format to defaults to current selection.
|
|
*/
|
|
function apply(name, vars, node) {
|
|
var formatList = get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && selection.isCollapsed();
|
|
|
|
function setElementFormat(elm, fmt) {
|
|
fmt = fmt || format;
|
|
|
|
if (elm) {
|
|
if (fmt.onformat) {
|
|
fmt.onformat(elm, fmt, vars, node);
|
|
}
|
|
|
|
each(fmt.styles, function (value, name) {
|
|
dom.setStyle(elm, name, replaceVars(value, vars));
|
|
});
|
|
|
|
// Needed for the WebKit span spam bug
|
|
// TODO: Remove this once WebKit/Blink fixes this
|
|
if (fmt.styles) {
|
|
var styleVal = dom.getAttrib(elm, 'style');
|
|
|
|
if (styleVal) {
|
|
elm.setAttribute('data-mce-style', styleVal);
|
|
}
|
|
}
|
|
|
|
each(fmt.attributes, function (value, name) {
|
|
dom.setAttrib(elm, name, replaceVars(value, vars));
|
|
});
|
|
|
|
each(fmt.classes, function (value) {
|
|
value = replaceVars(value, vars);
|
|
|
|
if (!dom.hasClass(elm, value)) {
|
|
dom.addClass(elm, value);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function applyNodeStyle(formatList, node) {
|
|
var found = false;
|
|
|
|
if (!format.selector) {
|
|
return false;
|
|
}
|
|
|
|
// Look for matching formats
|
|
each(formatList, function (format) {
|
|
// Check collapsed state if it exists
|
|
if ('collapsed' in format && format.collapsed !== isCollapsed) {
|
|
return;
|
|
}
|
|
|
|
if (dom.is(node, format.selector) && !isCaretNode(node)) {
|
|
setElementFormat(node, format);
|
|
found = true;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return found;
|
|
}
|
|
|
|
// This converts: <p>[a</p><p>]b</p> -> <p>[a]</p><p>b</p>
|
|
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 <h1></h1> 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: <span style="color:red"><b><span style="color:red; font-size:10px">text</span></b></span>
|
|
// will become: <span style="color:red"><b><span style="font-size:10px">text</span></b></span>
|
|
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 <b>text</b><b>text</b> becomes <b>texttext</b>
|
|
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: "<b>text|</b>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 <a> 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 !: !<p><b><i>*text</i><i>text*</i></b></p>!
|
|
// 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 <b> or <span>
|
|
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<div>text</div>text
|
|
*
|
|
* Output becomes:
|
|
* text<div><br />text<br /></div>text
|
|
*
|
|
* So when the div is removed the result is:
|
|
* text<br />text<br />text
|
|
*
|
|
* @private
|
|
* @param {Node} node Node to remove + apply BR/P elements to.
|
|
* @param {Object} format Format rule.
|
|
* @return {Node} Input node.
|
|
*/
|
|
function removeNode(node, format) {
|
|
var parentNode = node.parentNode, rootBlockElm;
|
|
|
|
function find(node, next, inc) {
|
|
node = getNonWhiteSpaceSibling(node, next, inc);
|
|
|
|
return !node || (node.nodeName == 'BR' || isBlock(node));
|
|
}
|
|
|
|
if (format.block) {
|
|
if (!forcedRootBlock) {
|
|
// Append BR elements if needed before we remove the block
|
|
if (isBlock(node) && !isBlock(parentNode)) {
|
|
if (!find(node, FALSE) && !find(node.firstChild, TRUE, 1)) {
|
|
node.insertBefore(dom.create('br'), node.firstChild);
|
|
}
|
|
|
|
if (!find(node, TRUE) && !find(node.lastChild, FALSE, 1)) {
|
|
node.appendChild(dom.create('br'));
|
|
}
|
|
}
|
|
} else {
|
|
// Wrap the block in a forcedRootBlock if we are at the root of document
|
|
if (parentNode == dom.getRoot()) {
|
|
if (!format.list_block || !isEq(node, format.list_block)) {
|
|
each(grep(node.childNodes), function (node) {
|
|
if (isValid(forcedRootBlock, node.nodeName.toLowerCase())) {
|
|
if (!rootBlockElm) {
|
|
rootBlockElm = wrap(node, forcedRootBlock);
|
|
dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs);
|
|
} else {
|
|
rootBlockElm.appendChild(node);
|
|
}
|
|
} else {
|
|
rootBlockElm = 0;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Never remove nodes that isn't the specified inline element if a selector is specified too
|
|
if (format.selector && format.inline && !isEq(format.inline, node)) {
|
|
return;
|
|
}
|
|
|
|
dom.remove(node, 1);
|
|
}
|
|
|
|
/**
|
|
* Returns the next/previous non whitespace node.
|
|
*
|
|
* @private
|
|
* @param {Node} node Node to start at.
|
|
* @param {boolean} next (Optional) Include next or previous node defaults to previous.
|
|
* @param {boolean} inc (Optional) Include the current node in checking. Defaults to false.
|
|
* @return {Node} Next or previous node or undefined if it wasn't found.
|
|
*/
|
|
function getNonWhiteSpaceSibling(node, next, inc) {
|
|
if (node) {
|
|
next = next ? 'nextSibling' : 'previousSibling';
|
|
|
|
for (node = inc ? node : node[next]; node; node = node[next]) {
|
|
if (node.nodeType == 1 || !isWhiteSpaceNode(node)) {
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges the next/previous sibling element if they match.
|
|
*
|
|
* @private
|
|
* @param {Node} prev Previous node to compare/merge.
|
|
* @param {Node} next Next node to compare/merge.
|
|
* @return {Node} Next node if we didn't merge and prev node if we did.
|
|
*/
|
|
function mergeSiblings(prev, next) {
|
|
var sibling, tmpSibling, elementUtils = new ElementUtils(dom);
|
|
|
|
function findElementSibling(node, siblingName) {
|
|
for (sibling = node; sibling; sibling = sibling[siblingName]) {
|
|
if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) {
|
|
return node;
|
|
}
|
|
|
|
if (sibling.nodeType == 1 && !isBookmarkNode(sibling)) {
|
|
return sibling;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
// Check if next/prev exists and that they are elements
|
|
if (prev && next) {
|
|
// If previous sibling is empty then jump over it
|
|
prev = findElementSibling(prev, 'previousSibling');
|
|
next = findElementSibling(next, 'nextSibling');
|
|
|
|
// Compare next and previous nodes
|
|
if (elementUtils.compare(prev, next)) {
|
|
// Append nodes between
|
|
for (sibling = prev.nextSibling; sibling && sibling != next;) {
|
|
tmpSibling = sibling;
|
|
sibling = sibling.nextSibling;
|
|
prev.appendChild(tmpSibling);
|
|
}
|
|
|
|
// Remove next node
|
|
dom.remove(next);
|
|
|
|
// Move children into prev node
|
|
each(grep(next.childNodes), function (node) {
|
|
prev.appendChild(node);
|
|
});
|
|
|
|
return prev;
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function getContainer(rng, start) {
|
|
var container, offset, lastIdx;
|
|
|
|
container = rng[start ? 'startContainer' : 'endContainer'];
|
|
offset = rng[start ? 'startOffset' : 'endOffset'];
|
|
|
|
if (container.nodeType == 1) {
|
|
lastIdx = container.childNodes.length - 1;
|
|
|
|
if (!start && offset) {
|
|
offset--;
|
|
}
|
|
|
|
container = container.childNodes[offset > lastIdx ? lastIdx : offset];
|
|
}
|
|
|
|
// If start text node is excluded then walk to the next node
|
|
if (container.nodeType === 3 && start && offset >= container.nodeValue.length) {
|
|
container = new TreeWalker(container, ed.getBody()).next() || container;
|
|
}
|
|
|
|
// If end text node is excluded then walk to the previous node
|
|
if (container.nodeType === 3 && !start && offset === 0) {
|
|
container = new TreeWalker(container, ed.getBody()).prev() || container;
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
function performCaretAction(type, name, vars, similar) {
|
|
var caretContainerId = '_mce_caret', debug = ed.settings.caret_debug;
|
|
|
|
// Creates a caret container bogus element
|
|
function createCaretContainer(fill) {
|
|
var caretContainer = dom.create('span', { id: caretContainerId, 'data-mce-bogus': true, style: debug ? 'color:red' : '' });
|
|
|
|
if (fill) {
|
|
caretContainer.appendChild(ed.getDoc().createTextNode(INVISIBLE_CHAR));
|
|
}
|
|
|
|
return caretContainer;
|
|
}
|
|
|
|
function isCaretContainerEmpty(node, nodes) {
|
|
while (node) {
|
|
if ((node.nodeType === 3 && node.nodeValue !== INVISIBLE_CHAR) || node.childNodes.length > 1) {
|
|
return false;
|
|
}
|
|
|
|
// Collect nodes
|
|
if (nodes && node.nodeType === 1) {
|
|
nodes.push(node);
|
|
}
|
|
|
|
node = node.firstChild;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Returns any parent caret container element
|
|
function getParentCaretContainer(node) {
|
|
while (node) {
|
|
if (node.id === caretContainerId) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
}
|
|
|
|
// Finds the first text node in the specified node
|
|
function findFirstTextNode(node) {
|
|
var walker;
|
|
|
|
if (node) {
|
|
walker = new TreeWalker(node, node);
|
|
|
|
for (node = walker.current(); node; node = walker.next()) {
|
|
if (node.nodeType === 3) {
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Removes the caret container for the specified node or all on the current document
|
|
function removeCaretContainer(node, moveCaret) {
|
|
var child, rng;
|
|
|
|
if (!node) {
|
|
node = getParentCaretContainer(selection.getStart());
|
|
|
|
if (!node) {
|
|
while ((node = dom.get(caretContainerId))) {
|
|
removeCaretContainer(node, false);
|
|
}
|
|
}
|
|
} else {
|
|
rng = selection.getRng(true);
|
|
|
|
if (isCaretContainerEmpty(node)) {
|
|
if (moveCaret !== false) {
|
|
rng.setStartBefore(node);
|
|
rng.setEndBefore(node);
|
|
}
|
|
|
|
dom.remove(node);
|
|
} else {
|
|
child = findFirstTextNode(node);
|
|
|
|
if (child.nodeValue.charAt(0) === INVISIBLE_CHAR) {
|
|
child.deleteData(0, 1);
|
|
|
|
// Fix for bug #6976
|
|
if (rng.startContainer == child && rng.startOffset > 0) {
|
|
rng.setStart(child, rng.startOffset - 1);
|
|
}
|
|
|
|
if (rng.endContainer == child && rng.endOffset > 0) {
|
|
rng.setEnd(child, rng.endOffset - 1);
|
|
}
|
|
}
|
|
|
|
dom.remove(node, 1);
|
|
}
|
|
|
|
selection.setRng(rng);
|
|
}
|
|
}
|
|
|
|
// Applies formatting to the caret position
|
|
function applyCaretFormat() {
|
|
var rng, caretContainer, textNode, offset, bookmark, container, text;
|
|
|
|
rng = selection.getRng(true);
|
|
offset = rng.startOffset;
|
|
container = rng.startContainer;
|
|
text = container.nodeValue;
|
|
|
|
caretContainer = getParentCaretContainer(selection.getStart());
|
|
if (caretContainer) {
|
|
textNode = findFirstTextNode(caretContainer);
|
|
}
|
|
|
|
// Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character
|
|
var wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/;
|
|
if (text && offset > 0 && offset < text.length &&
|
|
wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) {
|
|
// Get bookmark of caret position
|
|
bookmark = selection.getBookmark();
|
|
|
|
// Collapse bookmark range (WebKit)
|
|
rng.collapse(true);
|
|
|
|
// Expand the range to the closest word and split it at those points
|
|
rng = expandRng(rng, get(name));
|
|
rng = rangeUtils.split(rng);
|
|
|
|
// Apply the format to the range
|
|
apply(name, vars, rng);
|
|
|
|
// Move selection back to caret position
|
|
selection.moveToBookmark(bookmark);
|
|
} else {
|
|
if (!caretContainer || textNode.nodeValue !== INVISIBLE_CHAR) {
|
|
caretContainer = createCaretContainer(true);
|
|
textNode = caretContainer.firstChild;
|
|
|
|
rng.insertNode(caretContainer);
|
|
offset = 1;
|
|
|
|
apply(name, vars, caretContainer);
|
|
} else {
|
|
apply(name, vars, caretContainer);
|
|
}
|
|
|
|
// Move selection to text node
|
|
selection.setCursorLocation(textNode, offset);
|
|
}
|
|
}
|
|
|
|
function removeCaretFormat() {
|
|
var rng = selection.getRng(true), container, offset, bookmark,
|
|
hasContentAfter, node, formatNode, parents = [], i, caretContainer;
|
|
|
|
container = rng.startContainer;
|
|
offset = rng.startOffset;
|
|
node = container;
|
|
|
|
if (container.nodeType == 3) {
|
|
if (offset != container.nodeValue.length) {
|
|
hasContentAfter = true;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
while (node) {
|
|
if (matchNode(node, name, vars, similar)) {
|
|
formatNode = node;
|
|
break;
|
|
}
|
|
|
|
if (node.nextSibling) {
|
|
hasContentAfter = true;
|
|
}
|
|
|
|
parents.push(node);
|
|
node = node.parentNode;
|
|
}
|
|
|
|
// Node doesn't have the specified format
|
|
if (!formatNode) {
|
|
return;
|
|
}
|
|
|
|
// Is there contents after the caret then remove the format on the element
|
|
if (hasContentAfter) {
|
|
// Get bookmark of caret position
|
|
bookmark = selection.getBookmark();
|
|
|
|
// Collapse bookmark range (WebKit)
|
|
rng.collapse(true);
|
|
|
|
// Expand the range to the closest word and split it at those points
|
|
rng = expandRng(rng, get(name), true);
|
|
rng = rangeUtils.split(rng);
|
|
|
|
// Remove the format from the range
|
|
remove(name, vars, rng);
|
|
|
|
// Move selection back to caret position
|
|
selection.moveToBookmark(bookmark);
|
|
} else {
|
|
caretContainer = createCaretContainer();
|
|
|
|
node = caretContainer;
|
|
for (i = parents.length - 1; i >= 0; i--) {
|
|
node.appendChild(dom.clone(parents[i], false));
|
|
node = node.firstChild;
|
|
}
|
|
|
|
// Insert invisible character into inner most format element
|
|
node.appendChild(dom.doc.createTextNode(INVISIBLE_CHAR));
|
|
node = node.firstChild;
|
|
|
|
var block = dom.getParent(formatNode, isTextBlock);
|
|
|
|
if (block && dom.isEmpty(block)) {
|
|
// Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
|
|
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, isAtEndOfText,
|
|
walker, node, nodes, tmpNode;
|
|
|
|
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;
|
|
isAtEndOfText = true;
|
|
}
|
|
|
|
// Move startContainer/startOffset in to a suitable node
|
|
if (container.nodeType == 1) {
|
|
nodes = container.childNodes;
|
|
container = nodes[Math.min(offset, nodes.length - 1)];
|
|
walker = new TreeWalker(container, dom.getParent(container, dom.isBlock));
|
|
|
|
// If offset is at end of the parent node walk to the next one
|
|
if (offset > nodes.length - 1 || isAtEndOfText) {
|
|
walker.next();
|
|
}
|
|
|
|
for (node = walker.current(); node; node = walker.next()) {
|
|
if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
|
|
// IE has a "neat" feature where it moves the start node into the closest element
|
|
// we can avoid this by inserting an element before it and then remove it after we set the selection
|
|
tmpNode = dom.create('a', { 'data-mce-bogus': 'all' }, INVISIBLE_CHAR);
|
|
node.parentNode.insertBefore(tmpNode, node);
|
|
|
|
// Set selection and remove tmpNode
|
|
rng.setStart(node, 0);
|
|
selection.setRng(rng);
|
|
dom.remove(tmpNode);
|
|
|
|
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 '<!--' + elm.data + '-->';
|
|
}
|
|
|
|
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('</iframe>') !== -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 <p>|<br></p><p>a</p> -> <p><br></p><p>|a</p>
|
|
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);
|
|
} 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 (fromBlock, toBlock, toPosition) {
|
|
var children = Traverse.children(fromBlock);
|
|
|
|
if (NodeType.isBr(toPosition.getNode())) {
|
|
Remove.remove(Element.fromDom(toPosition.getNode()));
|
|
}
|
|
|
|
Arr.each(children, function (node) {
|
|
Insert.append(toBlock, node);
|
|
});
|
|
|
|
if (Empty.isEmpty(fromBlock)) {
|
|
Remove.remove(fromBlock);
|
|
}
|
|
|
|
return children.length > 0 ? Option.some(toPosition) : Option.none();
|
|
};
|
|
|
|
var mergeBlocks = function (forward, block1, block2) {
|
|
if (forward) {
|
|
return CaretFinder.positionIn(false, block1.dom()).bind(function (toPosition) {
|
|
return mergeBlocksAndReposition(block2, block1, toPosition);
|
|
});
|
|
} else {
|
|
return CaretFinder.positionIn(false, block2.dom()).bind(function (toPosition) {
|
|
return mergeBlocksAndReposition(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',
|
|
'tinymce.core.caret.CaretFinder',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.caret.CaretUtils',
|
|
'tinymce.core.dom.NodeType'
|
|
],
|
|
function (Adt, Option, CaretFinder, CaretPosition, CaretUtils, 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 findCefPosition = function (rootNode, forward, from) {
|
|
return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) {
|
|
if (forward && NodeType.isContentEditableFalse(to.getNode())) {
|
|
return Option.some(DeleteAction.moveToElement(to.getNode()));
|
|
} else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) {
|
|
return Option.some(DeleteAction.moveToElement(to.getNode(true)));
|
|
} 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 findCaretPosition = function (forward, rootElement, elm) {
|
|
if (CaretCandidate.isCaretCandidate(elm.previousSibling)) {
|
|
return Option.some(afterOrEndOf(elm.previousSibling));
|
|
} else if (CaretCandidate.isCaretCandidate(elm.nextSibling)) {
|
|
return Option.some(beforeOrStartOf(elm));
|
|
} else {
|
|
return InlineUtils.findCaretPosition(rootElement, forward, CaretPosition.before(elm)).fold(
|
|
function () {
|
|
return InlineUtils.findCaretPosition(rootElement, !forward, CaretPosition.after(elm));
|
|
},
|
|
Option.some
|
|
);
|
|
}
|
|
};
|
|
|
|
var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) {
|
|
return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm));
|
|
};
|
|
|
|
var setSelection = function (editor, pos) {
|
|
pos.fold(
|
|
function () {
|
|
editor.focus();
|
|
},
|
|
function (pos) {
|
|
editor.selection.setRng(pos.toRange());
|
|
}
|
|
);
|
|
};
|
|
|
|
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('<br data-mce-bogus="1">');
|
|
Remove.empty(elm);
|
|
Insert.append(elm, br);
|
|
return Option.some(CaretPosition.before(br.dom()));
|
|
} else {
|
|
return Option.none();
|
|
}
|
|
};
|
|
|
|
var deleteElement = function (editor, forward, elm) {
|
|
var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom());
|
|
var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody()));
|
|
|
|
Remove.remove(elm);
|
|
|
|
parentBlock.bind(paddEmptyBlock).fold(
|
|
function () {
|
|
setSelection(editor, afterDeletePos);
|
|
},
|
|
function (paddPos) {
|
|
setSelection(editor, Option.some(paddPos));
|
|
}
|
|
);
|
|
};
|
|
|
|
return {
|
|
deleteElement: deleteElement
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CefDelete.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.CefDelete',
|
|
[
|
|
'ephox.sugar.api.node.Element',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.caret.CaretUtils',
|
|
'tinymce.core.delete.BlockBoundary',
|
|
'tinymce.core.delete.CefDeleteAction',
|
|
'tinymce.core.delete.DeleteElement',
|
|
'tinymce.core.delete.MergeBlocks',
|
|
'tinymce.core.dom.NodeType'
|
|
],
|
|
function (Element, CaretPosition, CaretUtils, BlockBoundary, CefDeleteAction, DeleteElement, MergeBlocks, NodeType) {
|
|
var deleteElement = function (editor, forward) {
|
|
return function (element) {
|
|
DeleteElement.deleteElement(editor, forward, Element.fromDom(element));
|
|
return true;
|
|
};
|
|
};
|
|
|
|
var moveToElement = function (editor, forward) {
|
|
return function (element) {
|
|
var pos = forward ? CaretPosition.before(element) : CaretPosition.after(element);
|
|
editor.selection.setRng(pos.toRange());
|
|
return true;
|
|
};
|
|
};
|
|
|
|
var moveToPosition = function (editor) {
|
|
return function (pos) {
|
|
editor.selection.setRng(pos.toRange());
|
|
return true;
|
|
};
|
|
};
|
|
|
|
var backspaceDeleteCaret = function (editor, forward) {
|
|
var result = CefDeleteAction.read(editor.getBody(), forward, editor.selection.getRng()).map(function (deleteAction) {
|
|
return deleteAction.fold(
|
|
deleteElement(editor, forward),
|
|
moveToElement(editor, forward),
|
|
moveToPosition(editor)
|
|
);
|
|
});
|
|
|
|
return result.getOr(false);
|
|
};
|
|
|
|
var backspaceDeleteRange = function (editor, forward) {
|
|
var selectedElement = editor.selection.getNode();
|
|
if (NodeType.isContentEditableFalse(selectedElement)) {
|
|
DeleteElement.deleteElement(editor, forward, Element.fromDom(editor.selection.getNode()));
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
var getContentEditableRoot = function (root, node) {
|
|
while (node && node !== root) {
|
|
if (NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node)) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
var paddEmptyElement = function (editor) {
|
|
var br, ceRoot = getContentEditableRoot(editor.getBody(), editor.selection.getNode());
|
|
|
|
if (NodeType.isContentEditableTrue(ceRoot) && editor.dom.isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) {
|
|
br = editor.dom.create('br', { "data-mce-bogus": "1" });
|
|
editor.dom.setHTML(ceRoot, '');
|
|
ceRoot.appendChild(br);
|
|
editor.selection.setRng(CaretPosition.before(br).toRange());
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
var backspaceDelete = function (editor, forward) {
|
|
if (editor.selection.isCollapsed()) {
|
|
return backspaceDeleteCaret(editor, forward);
|
|
} else {
|
|
return backspaceDeleteRange(editor, forward);
|
|
}
|
|
};
|
|
|
|
return {
|
|
backspaceDelete: backspaceDelete,
|
|
paddEmptyElement: paddEmptyElement
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* CaretContainerInline.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.CaretContainerInline',
|
|
[
|
|
'ephox.katamari.api.Fun',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.text.Zwsp'
|
|
],
|
|
function (Fun, NodeType, Zwsp) {
|
|
var isText = NodeType.isText;
|
|
|
|
var startsWithCaretContainer = function (node) {
|
|
return isText(node) && node.data[0] === Zwsp.ZWSP;
|
|
};
|
|
|
|
var endsWithCaretContainer = function (node) {
|
|
return isText(node) && node.data[node.data.length - 1] === Zwsp.ZWSP;
|
|
};
|
|
|
|
var createZwsp = function (node) {
|
|
return node.ownerDocument.createTextNode(Zwsp.ZWSP);
|
|
};
|
|
|
|
var insertBefore = function (node) {
|
|
if (isText(node.previousSibling)) {
|
|
if (endsWithCaretContainer(node.previousSibling)) {
|
|
return node.previousSibling;
|
|
} else {
|
|
node.previousSibling.appendData(Zwsp.ZWSP);
|
|
return node.previousSibling;
|
|
}
|
|
} else if (isText(node)) {
|
|
if (startsWithCaretContainer(node)) {
|
|
return node;
|
|
} else {
|
|
node.insertData(0, Zwsp.ZWSP);
|
|
return node;
|
|
}
|
|
} else {
|
|
var newNode = createZwsp(node);
|
|
node.parentNode.insertBefore(newNode, node);
|
|
return newNode;
|
|
}
|
|
};
|
|
|
|
var insertAfter = function (node) {
|
|
if (isText(node.nextSibling)) {
|
|
if (startsWithCaretContainer(node.nextSibling)) {
|
|
return node.nextSibling;
|
|
} else {
|
|
node.nextSibling.insertData(0, Zwsp.ZWSP);
|
|
return node.nextSibling;
|
|
}
|
|
} else if (isText(node)) {
|
|
if (endsWithCaretContainer(node)) {
|
|
return node;
|
|
} else {
|
|
node.appendData(Zwsp.ZWSP);
|
|
return node;
|
|
}
|
|
} else {
|
|
var newNode = createZwsp(node);
|
|
if (node.nextSibling) {
|
|
node.parentNode.insertBefore(newNode, node.nextSibling);
|
|
} else {
|
|
node.parentNode.appendChild(newNode);
|
|
}
|
|
return newNode;
|
|
}
|
|
};
|
|
|
|
var insertInline = function (before, node) {
|
|
return before ? insertBefore(node) : insertAfter(node);
|
|
};
|
|
|
|
return {
|
|
insertInline: insertInline,
|
|
insertInlineBefore: Fun.curry(insertInline, true),
|
|
insertInlineAfter: Fun.curry(insertInline, false)
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CaretContainerRemove.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.CaretContainerRemove',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.text.Zwsp',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (Arr, CaretContainer, CaretPosition, NodeType, Zwsp, Tools) {
|
|
var isElement = NodeType.isElement;
|
|
var isText = NodeType.isText;
|
|
|
|
var removeNode = function (node) {
|
|
var parentNode = node.parentNode;
|
|
if (parentNode) {
|
|
parentNode.removeChild(node);
|
|
}
|
|
};
|
|
|
|
var getNodeValue = function (node) {
|
|
try {
|
|
return node.nodeValue;
|
|
} catch (ex) {
|
|
// IE sometimes produces "Invalid argument" on nodes
|
|
return "";
|
|
}
|
|
};
|
|
|
|
var setNodeValue = function (node, text) {
|
|
if (text.length === 0) {
|
|
removeNode(node);
|
|
} else {
|
|
node.nodeValue = text;
|
|
}
|
|
};
|
|
|
|
var trimCount = function (text) {
|
|
var trimmedText = Zwsp.trim(text);
|
|
return { count: text.length - trimmedText.length, text: trimmedText };
|
|
};
|
|
|
|
var removeUnchanged = function (caretContainer, pos) {
|
|
remove(caretContainer);
|
|
return pos;
|
|
};
|
|
|
|
var removeTextAndReposition = function (caretContainer, pos) {
|
|
var before = trimCount(caretContainer.data.substr(0, pos.offset()));
|
|
var after = trimCount(caretContainer.data.substr(pos.offset()));
|
|
var text = before.text + after.text;
|
|
|
|
if (text.length > 0) {
|
|
setNodeValue(caretContainer, text);
|
|
return new CaretPosition(caretContainer, pos.offset() - before.count);
|
|
} else {
|
|
return pos;
|
|
}
|
|
};
|
|
|
|
var removeElementAndReposition = function (caretContainer, pos) {
|
|
var parentNode = pos.container();
|
|
var newPosition = Arr.indexOf(parentNode.childNodes, caretContainer).map(function (index) {
|
|
return index < pos.offset() ? new CaretPosition(parentNode, pos.offset() - 1) : pos;
|
|
}).getOr(pos);
|
|
remove(caretContainer);
|
|
return newPosition;
|
|
};
|
|
|
|
var removeTextCaretContainer = function (caretContainer, pos) {
|
|
return pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
|
|
};
|
|
|
|
var removeElementCaretContainer = function (caretContainer, pos) {
|
|
return pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos);
|
|
};
|
|
|
|
var removeAndReposition = function (container, pos) {
|
|
return CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos);
|
|
};
|
|
|
|
var remove = function (caretContainerNode) {
|
|
if (isElement(caretContainerNode) && CaretContainer.isCaretContainer(caretContainerNode)) {
|
|
if (CaretContainer.hasContent(caretContainerNode)) {
|
|
caretContainerNode.removeAttribute('data-mce-caret');
|
|
} else {
|
|
removeNode(caretContainerNode);
|
|
}
|
|
}
|
|
|
|
if (isText(caretContainerNode)) {
|
|
var text = Zwsp.trim(getNodeValue(caretContainerNode));
|
|
setNodeValue(caretContainerNode, text);
|
|
}
|
|
};
|
|
|
|
return {
|
|
removeAndReposition: removeAndReposition,
|
|
remove: remove
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* BoundaryCaret.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.BoundaryCaret',
|
|
[
|
|
'ephox.katamari.api.Option',
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.caret.CaretContainerInline',
|
|
'tinymce.core.caret.CaretContainerRemove',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.keyboard.InlineUtils'
|
|
],
|
|
function (Option, CaretContainer, CaretContainerInline, CaretContainerRemove, CaretPosition, NodeType, InlineUtils) {
|
|
var insertInlinePos = function (pos, before) {
|
|
if (NodeType.isText(pos.container())) {
|
|
return CaretContainerInline.insertInline(before, pos.container());
|
|
} else {
|
|
return CaretContainerInline.insertInline(before, pos.getNode());
|
|
}
|
|
};
|
|
|
|
var renderCaret = function (caret, location) {
|
|
return location.fold(
|
|
function (element) { // Before
|
|
CaretContainerRemove.remove(caret.get());
|
|
var text = CaretContainerInline.insertInlineBefore(element);
|
|
caret.set(text);
|
|
return Option.some(new CaretPosition(text, text.length - 1));
|
|
},
|
|
function (element) { // Start
|
|
return InlineUtils.findCaretPositionIn(element, true).map(function (pos) {
|
|
CaretContainerRemove.remove(caret.get());
|
|
var text = insertInlinePos(pos, true);
|
|
caret.set(text);
|
|
return new CaretPosition(text, 1);
|
|
});
|
|
},
|
|
function (element) { // End
|
|
return InlineUtils.findCaretPositionIn(element, false).map(function (pos) {
|
|
CaretContainerRemove.remove(caret.get());
|
|
var text = insertInlinePos(pos, false);
|
|
caret.set(text);
|
|
return new CaretPosition(text, text.length - 1);
|
|
});
|
|
},
|
|
function (element) { // After
|
|
CaretContainerRemove.remove(caret.get());
|
|
var text = CaretContainerInline.insertInlineAfter(element);
|
|
caret.set(text);
|
|
return Option.some(new CaretPosition(text, 1));
|
|
}
|
|
);
|
|
};
|
|
|
|
return {
|
|
renderCaret: renderCaret
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* LazyEvaluator.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.util.LazyEvaluator',
|
|
[
|
|
'ephox.katamari.api.Option'
|
|
],
|
|
function (Option) {
|
|
var evaluateUntil = function (fns, args) {
|
|
for (var i = 0; i < fns.length; i++) {
|
|
var result = fns[i].apply(null, args);
|
|
if (result.isSome()) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return Option.none();
|
|
};
|
|
|
|
return {
|
|
evaluateUntil: evaluateUntil
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* BoundaryLocation.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.BoundaryLocation',
|
|
[
|
|
'ephox.katamari.api.Adt',
|
|
'ephox.katamari.api.Fun',
|
|
'ephox.katamari.api.Option',
|
|
'ephox.katamari.api.Options',
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.caret.CaretUtils',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.keyboard.InlineUtils',
|
|
'tinymce.core.util.LazyEvaluator'
|
|
],
|
|
function (Adt, Fun, Option, Options, CaretContainer, CaretPosition, CaretUtils, NodeType, InlineUtils, LazyEvaluator) {
|
|
var Location = Adt.generate([
|
|
{ before: [ 'element' ] },
|
|
{ start: [ 'element' ] },
|
|
{ end: [ 'element' ] },
|
|
{ after: [ 'element' ] }
|
|
]);
|
|
|
|
var before = function (rootNode, pos) {
|
|
var nPos = InlineUtils.normalizeForwards(pos);
|
|
return InlineUtils.findInline(rootNode, nPos).fold(
|
|
function () {
|
|
return InlineUtils.findCaretPosition(rootNode, true, nPos)
|
|
.bind(Fun.curry(InlineUtils.findInline, rootNode))
|
|
.map(function (inline) {
|
|
return Location.before(inline);
|
|
});
|
|
},
|
|
Option.none
|
|
);
|
|
};
|
|
|
|
var start = function (rootNode, pos) {
|
|
var nPos = InlineUtils.normalizeBackwards(pos);
|
|
return InlineUtils.findInline(rootNode, nPos).bind(function (inline) {
|
|
var prevPos = InlineUtils.findCaretPosition(inline, false, nPos);
|
|
return prevPos.isNone() ? Option.some(Location.start(inline)) : Option.none();
|
|
});
|
|
};
|
|
|
|
var end = function (rootNode, pos) {
|
|
var nPos = InlineUtils.normalizeForwards(pos);
|
|
return InlineUtils.findInline(rootNode, nPos).bind(function (inline) {
|
|
var nextPos = InlineUtils.findCaretPosition(inline, true, nPos);
|
|
return nextPos.isNone() ? Option.some(Location.end(inline)) : Option.none();
|
|
});
|
|
};
|
|
|
|
var after = function (rootNode, pos) {
|
|
var nPos = InlineUtils.normalizeBackwards(pos);
|
|
return InlineUtils.findInline(rootNode, nPos).fold(
|
|
function () {
|
|
return InlineUtils.findCaretPosition(rootNode, false, nPos)
|
|
.bind(Fun.curry(InlineUtils.findInline, rootNode))
|
|
.map(function (inline) {
|
|
return Location.after(inline);
|
|
});
|
|
},
|
|
Option.none
|
|
);
|
|
};
|
|
|
|
var isValidLocation = function (location) {
|
|
return InlineUtils.isRtl(getElement(location)) === false;
|
|
};
|
|
|
|
var readLocation = function (rootNode, pos) {
|
|
var location = LazyEvaluator.evaluateUntil([
|
|
before,
|
|
start,
|
|
end,
|
|
after
|
|
], [rootNode, pos]);
|
|
|
|
return location.filter(isValidLocation);
|
|
};
|
|
|
|
var getElement = function (location) {
|
|
return location.fold(
|
|
Fun.identity, // Before
|
|
Fun.identity, // Start
|
|
Fun.identity, // End
|
|
Fun.identity // After
|
|
);
|
|
};
|
|
|
|
var outside = function (location) {
|
|
return location.fold(
|
|
Location.before, // Before
|
|
Location.before, // Start
|
|
Location.after, // End
|
|
Location.after // After
|
|
);
|
|
};
|
|
|
|
var inside = function (location) {
|
|
return location.fold(
|
|
Location.start, // Before
|
|
Location.start, // Start
|
|
Location.end, // End
|
|
Location.end // After
|
|
);
|
|
};
|
|
|
|
var isInside = function (location) {
|
|
return location.fold(
|
|
Fun.constant(false), // Before
|
|
Fun.constant(true), // Start
|
|
Fun.constant(true), // End
|
|
Fun.constant(false) // After
|
|
);
|
|
};
|
|
|
|
var betweenInlines = function (forward, rootNode, from, to, location) {
|
|
return Options.liftN([
|
|
InlineUtils.findInline(rootNode, from),
|
|
InlineUtils.findInline(rootNode, to)
|
|
], function (fromInline, toInline) {
|
|
if (fromInline !== toInline && InlineUtils.hasSameParentBlock(rootNode, fromInline, toInline)) {
|
|
// Force after since some browsers normalize and lean left into the closest inline
|
|
return Location.after(forward ? fromInline : toInline);
|
|
} else {
|
|
return location;
|
|
}
|
|
}).getOr(location);
|
|
};
|
|
|
|
var isFirstPositionInBlock = function (rootBlock, pos) {
|
|
return InlineUtils.findCaretPosition(rootBlock, false, pos).isNone();
|
|
};
|
|
|
|
var isLastPositionInBlock = function (rootBlock, pos) {
|
|
return InlineUtils.findCaretPosition(rootBlock, true, pos).bind(function (nextPos) {
|
|
if (NodeType.isBr(nextPos.getNode())) {
|
|
return InlineUtils.findCaretPosition(rootBlock, true, CaretPosition.after(nextPos.getNode()));
|
|
} else {
|
|
return Option.some(nextPos);
|
|
}
|
|
}).isNone();
|
|
};
|
|
|
|
var isEndPositionInBlock = function (forward, rootBlock, pos) {
|
|
return forward ? isLastPositionInBlock(rootBlock, pos) : isFirstPositionInBlock(rootBlock, pos);
|
|
};
|
|
|
|
var onlyOutside = function (location) {
|
|
if (isInside(location)) {
|
|
return Option.some(outside(location));
|
|
} else {
|
|
return Option.none();
|
|
}
|
|
};
|
|
|
|
var findFirstOrLastLocationInBlock = function (rootNode, forward, toBlock) {
|
|
return InlineUtils.findCaretPositionIn(toBlock, forward).bind(function (lastPosition) {
|
|
return readLocation(toBlock, lastPosition).map(outside);
|
|
});
|
|
};
|
|
|
|
var betweenBlocks = function (forward, rootNode, from, to, location) {
|
|
var fromBlock = CaretUtils.getParentBlock(from.container(), rootNode);
|
|
if (isEndPositionInBlock(forward, fromBlock, to) && isInside(location) === false) {
|
|
return readLocation(rootNode, from).bind(onlyOutside);
|
|
} else if (isEndPositionInBlock(forward, fromBlock, from)) {
|
|
return readLocation(rootNode, from)
|
|
.bind(onlyOutside)
|
|
.orThunk(function () {
|
|
return Option.from(CaretUtils.getParentBlock(to.container(), rootNode)).bind(function (toBlock) {
|
|
return findFirstOrLastLocationInBlock(rootNode, forward, toBlock);
|
|
});
|
|
});
|
|
} else {
|
|
return Option.some(location);
|
|
}
|
|
};
|
|
|
|
var findLocation = function (forward, rootNode, pos) {
|
|
var from = InlineUtils.normalizePosition(forward, pos);
|
|
var to = InlineUtils.findCaretPosition(rootNode, forward, from).map(Fun.curry(InlineUtils.normalizePosition, forward));
|
|
var location = to.fold(
|
|
function () {
|
|
return readLocation(rootNode, from).map(outside);
|
|
},
|
|
function (to) {
|
|
return readLocation(rootNode, to)
|
|
.bind(Fun.curry(betweenBlocks, forward, rootNode, from, to))
|
|
.map(Fun.curry(betweenInlines, forward, rootNode, from, to));
|
|
}
|
|
);
|
|
|
|
return location.filter(isValidLocation);
|
|
};
|
|
|
|
return {
|
|
readLocation: readLocation,
|
|
prevLocation: Fun.curry(findLocation, false),
|
|
nextLocation: Fun.curry(findLocation, true),
|
|
getElement: getElement,
|
|
outside: outside,
|
|
inside: inside
|
|
};
|
|
}
|
|
);
|
|
define(
|
|
'ephox.katamari.api.Cell',
|
|
|
|
[
|
|
],
|
|
|
|
function () {
|
|
var Cell = function (initial) {
|
|
var value = initial;
|
|
|
|
var get = function () {
|
|
return value;
|
|
};
|
|
|
|
var set = function (v) {
|
|
value = v;
|
|
};
|
|
|
|
var clone = function () {
|
|
return Cell(get());
|
|
};
|
|
|
|
return {
|
|
get: get,
|
|
set: set,
|
|
clone: clone
|
|
};
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* BoundarySelection.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.BoundarySelection',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Cell',
|
|
'ephox.katamari.api.Fun',
|
|
'tinymce.core.caret.CaretContainerRemove',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.keyboard.BoundaryCaret',
|
|
'tinymce.core.keyboard.BoundaryLocation',
|
|
'tinymce.core.keyboard.InlineUtils'
|
|
],
|
|
function (Arr, Cell, Fun, CaretContainerRemove, CaretPosition, BoundaryCaret, BoundaryLocation, InlineUtils) {
|
|
var setCaretPosition = function (editor, pos) {
|
|
var rng = editor.dom.createRng();
|
|
rng.setStart(pos.container(), pos.offset());
|
|
rng.setEnd(pos.container(), pos.offset());
|
|
editor.selection.setRng(rng);
|
|
};
|
|
|
|
var isFeatureEnabled = function (editor) {
|
|
return editor.settings.inline_boundaries !== false;
|
|
};
|
|
|
|
var setSelected = function (state, elm) {
|
|
if (state) {
|
|
elm.setAttribute('data-mce-selected', '1');
|
|
} else {
|
|
elm.removeAttribute('data-mce-selected', '1');
|
|
}
|
|
};
|
|
|
|
var renderCaretLocation = function (editor, caret, location) {
|
|
return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
|
|
setCaretPosition(editor, pos);
|
|
return location;
|
|
});
|
|
};
|
|
|
|
var findLocation = function (editor, caret, forward) {
|
|
var rootNode = editor.getBody();
|
|
var from = CaretPosition.fromRangeStart(editor.selection.getRng());
|
|
var location = forward ? BoundaryLocation.nextLocation(rootNode, from) : BoundaryLocation.prevLocation(rootNode, from);
|
|
return location.bind(function (location) {
|
|
return renderCaretLocation(editor, caret, location);
|
|
});
|
|
};
|
|
|
|
var toggleInlines = function (dom, elms) {
|
|
var selectedInlines = dom.select('a[href][data-mce-selected],code[data-mce-selected]');
|
|
var targetInlines = Arr.filter(elms, InlineUtils.isInlineTarget);
|
|
Arr.each(Arr.difference(selectedInlines, targetInlines), Fun.curry(setSelected, false));
|
|
Arr.each(Arr.difference(targetInlines, selectedInlines), Fun.curry(setSelected, true));
|
|
};
|
|
|
|
var safeRemoveCaretContainer = function (editor, caret) {
|
|
if (editor.selection.isCollapsed() && editor.composing !== true && caret.get()) {
|
|
var pos = CaretPosition.fromRangeStart(editor.selection.getRng());
|
|
if (CaretPosition.isTextPosition(pos) && InlineUtils.isAtZwsp(pos) === false) {
|
|
setCaretPosition(editor, CaretContainerRemove.removeAndReposition(caret.get(), pos));
|
|
caret.set(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
var renderInsideInlineCaret = function (editor, caret, elms) {
|
|
if (editor.selection.isCollapsed()) {
|
|
var inlines = Arr.filter(elms, InlineUtils.isInlineTarget);
|
|
Arr.each(inlines, function (inline) {
|
|
var pos = CaretPosition.fromRangeStart(editor.selection.getRng());
|
|
BoundaryLocation.readLocation(editor.getBody(), pos).bind(function (location) {
|
|
return renderCaretLocation(editor, caret, location);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
var move = function (editor, caret, forward) {
|
|
return function () {
|
|
return isFeatureEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false;
|
|
};
|
|
};
|
|
|
|
var setupSelectedState = function (editor) {
|
|
var caret = new Cell(null);
|
|
|
|
editor.on('NodeChange', function (e) {
|
|
if (isFeatureEnabled(editor)) {
|
|
toggleInlines(editor.dom, e.parents);
|
|
safeRemoveCaretContainer(editor, caret);
|
|
renderInsideInlineCaret(editor, caret, e.parents);
|
|
}
|
|
});
|
|
|
|
return caret;
|
|
};
|
|
|
|
return {
|
|
move: move,
|
|
setupSelectedState: setupSelectedState,
|
|
setCaretPosition: setCaretPosition
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* InlineBoundaryDelete.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.InlineBoundaryDelete',
|
|
[
|
|
'ephox.katamari.api.Fun',
|
|
'ephox.katamari.api.Option',
|
|
'ephox.katamari.api.Options',
|
|
'ephox.sugar.api.node.Element',
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.delete.DeleteElement',
|
|
'tinymce.core.keyboard.BoundaryCaret',
|
|
'tinymce.core.keyboard.BoundaryLocation',
|
|
'tinymce.core.keyboard.BoundarySelection',
|
|
'tinymce.core.keyboard.InlineUtils'
|
|
],
|
|
function (Fun, Option, Options, Element, CaretContainer, CaretPosition, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection, InlineUtils) {
|
|
var rangeFromPositions = function (from, to) {
|
|
var range = document.createRange();
|
|
|
|
range.setStart(from.container(), from.offset());
|
|
range.setEnd(to.container(), to.offset());
|
|
|
|
return range;
|
|
};
|
|
|
|
var setCaretLocation = function (editor, caret) {
|
|
return function (location) {
|
|
return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
|
|
BoundarySelection.setCaretPosition(editor, pos);
|
|
return true;
|
|
}).getOr(false);
|
|
};
|
|
};
|
|
|
|
var deleteFromTo = function (editor, caret, from, to) {
|
|
var rootNode = editor.getBody();
|
|
|
|
editor.undoManager.ignore(function () {
|
|
editor.selection.setRng(rangeFromPositions(from, to));
|
|
editor.execCommand('Delete');
|
|
|
|
BoundaryLocation.readLocation(rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
|
|
.map(BoundaryLocation.inside)
|
|
.map(setCaretLocation(editor, caret));
|
|
});
|
|
|
|
editor.nodeChanged();
|
|
};
|
|
|
|
var backspaceDeleteCollapsed = function (editor, caret, forward, from) {
|
|
var rootNode = editor.getBody();
|
|
var fromLocation = BoundaryLocation.readLocation(rootNode, from);
|
|
|
|
return fromLocation.bind(function (location) {
|
|
if (forward) {
|
|
return location.fold(
|
|
Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
|
|
Option.none, // Start
|
|
Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
|
|
Option.none // After
|
|
);
|
|
} else {
|
|
return location.fold(
|
|
Option.none, // Before
|
|
Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
|
|
Option.none, // End
|
|
Fun.constant(Option.some(BoundaryLocation.inside(location))) // After
|
|
);
|
|
}
|
|
})
|
|
.map(setCaretLocation(editor, caret))
|
|
.getOrThunk(function () {
|
|
var toPosition = InlineUtils.findCaretPosition(rootNode, forward, from);
|
|
var toLocation = toPosition.bind(function (pos) {
|
|
return BoundaryLocation.readLocation(rootNode, pos);
|
|
});
|
|
|
|
if (fromLocation.isSome() && toLocation.isSome()) {
|
|
InlineUtils.findInline(rootNode, from).bind(function (elm) {
|
|
return DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
|
|
});
|
|
return true;
|
|
} else {
|
|
return toLocation.map(function (_) {
|
|
toPosition.map(function (to) {
|
|
if (forward) {
|
|
deleteFromTo(editor, caret, from, to);
|
|
} else {
|
|
deleteFromTo(editor, caret, to, from);
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}).getOr(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
var backspaceDelete = function (editor, caret, forward) {
|
|
if (editor.selection.isCollapsed()) {
|
|
var from = CaretPosition.fromRangeStart(editor.selection.getRng());
|
|
return backspaceDeleteCollapsed(editor, caret, forward, from);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
return {
|
|
backspaceDelete: backspaceDelete
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Commands.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.DeleteCommands',
|
|
[
|
|
'tinymce.core.delete.BlockBoundaryDelete',
|
|
'tinymce.core.delete.BlockRangeDelete',
|
|
'tinymce.core.delete.CefDelete',
|
|
'tinymce.core.delete.InlineBoundaryDelete'
|
|
],
|
|
function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, BoundaryDelete) {
|
|
var nativeCommand = function (editor, command) {
|
|
editor.getDoc().execCommand(command, false, null);
|
|
};
|
|
|
|
var paddEmptyBody = function (editor) {
|
|
var dom = editor.dom;
|
|
|
|
// Check if body is empty after the delete call if so then set the contents
|
|
// to an empty string and move the caret to any block produced by that operation
|
|
// this fixes the issue with root blocks not being properly produced after a delete call on IE
|
|
var body = editor.getBody();
|
|
|
|
if (dom.isEmpty(body)) {
|
|
editor.setContent('');
|
|
|
|
if (body.firstChild && dom.isBlock(body.firstChild)) {
|
|
editor.selection.setCursorLocation(body.firstChild, 0);
|
|
} else {
|
|
editor.selection.setCursorLocation(body, 0);
|
|
}
|
|
}
|
|
};
|
|
|
|
var deleteCommand = function (editor) {
|
|
if (CefDelete.backspaceDelete(editor, false)) {
|
|
return;
|
|
} else if (BoundaryDelete.backspaceDelete(editor, false)) {
|
|
return;
|
|
} else if (BlockBoundaryDelete.backspaceDelete(editor, false)) {
|
|
return;
|
|
} else if (BlockRangeDelete.backspaceDelete(editor, false)) {
|
|
return;
|
|
} else {
|
|
nativeCommand(editor, 'Delete');
|
|
paddEmptyBody(editor);
|
|
}
|
|
};
|
|
|
|
var forwardDeleteCommand = function (editor) {
|
|
if (CefDelete.backspaceDelete(editor, true)) {
|
|
return;
|
|
} else if (BoundaryDelete.backspaceDelete(editor, true)) {
|
|
return;
|
|
} else if (BlockBoundaryDelete.backspaceDelete(editor, true)) {
|
|
return;
|
|
} else if (BlockRangeDelete.backspaceDelete(editor, true)) {
|
|
return;
|
|
} else {
|
|
nativeCommand(editor, 'ForwardDelete');
|
|
}
|
|
};
|
|
|
|
return {
|
|
deleteCommand: deleteCommand,
|
|
forwardDeleteCommand: forwardDeleteCommand
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* RangeNormalizer.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.RangeNormalizer',
|
|
[
|
|
'tinymce.core.caret.CaretFinder',
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.caret.CaretUtils',
|
|
'tinymce.core.dom.NodeType'
|
|
],
|
|
function (CaretFinder, CaretPosition, CaretUtils, NodeType) {
|
|
var isTextBlock = function (elm) {
|
|
return NodeType.isElement(elm) && /^(P|H[1-6]|DIV)$/.test(elm.nodeName);
|
|
};
|
|
|
|
var matchEndContainer = function (rng, predicate) {
|
|
return predicate(rng.endContainer);
|
|
};
|
|
|
|
var createRange = function (sc, so, ec, eo) {
|
|
var rng = document.createRange();
|
|
rng.setStart(sc, so);
|
|
rng.setEnd(ec, eo);
|
|
return rng;
|
|
};
|
|
|
|
// If you tripple click a paragraph in this case:
|
|
// <blockquote><p>a</p></blockquote><p>b</p>
|
|
// It would become this range in webkit:
|
|
// <blockquote><p>[a</p></blockquote><p>]b</p>
|
|
// We would want it to be:
|
|
// <blockquote><p>[a]</p></blockquote><p>b</p>
|
|
// 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 <meta><ul>..</ul>
|
|
if (firstChild && firstChild.name === 'meta') {
|
|
firstChild = firstChild.next;
|
|
}
|
|
|
|
// Skip mce_marker since it's likely <ul>..</ul><span id="mce_marker"></span>
|
|
if (lastChild && lastChild.attr('id') === 'mce_marker') {
|
|
lastChild = lastChild.prev;
|
|
}
|
|
|
|
if (!firstChild || firstChild !== lastChild) {
|
|
return false;
|
|
}
|
|
|
|
return firstChild.name === 'ul' || firstChild.name === 'ol';
|
|
};
|
|
|
|
var cleanupDomFragment = function (domFragment) {
|
|
var firstChild = domFragment.firstChild;
|
|
var lastChild = domFragment.lastChild;
|
|
|
|
// TODO: remove the meta tag from paste logic
|
|
if (firstChild && firstChild.nodeName === 'META') {
|
|
firstChild.parentNode.removeChild(firstChild);
|
|
}
|
|
|
|
if (lastChild && lastChild.id === 'mce_marker') {
|
|
lastChild.parentNode.removeChild(lastChild);
|
|
}
|
|
|
|
return domFragment;
|
|
};
|
|
|
|
var toDomFragment = function (dom, serializer, fragment) {
|
|
var html = serializer.serialize(fragment);
|
|
var domFragment = dom.createFragment(html);
|
|
|
|
return cleanupDomFragment(domFragment);
|
|
};
|
|
|
|
var listItems = function (elm) {
|
|
return Tools.grep(elm.childNodes, function (child) {
|
|
return child.nodeName === 'LI';
|
|
});
|
|
};
|
|
|
|
var isEmpty = function (elm) {
|
|
return !elm.firstChild;
|
|
};
|
|
|
|
var trimListItems = function (elms) {
|
|
return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms;
|
|
};
|
|
|
|
var getParentLi = function (dom, node) {
|
|
var parentBlock = dom.getParent(node, dom.isBlock);
|
|
return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null;
|
|
};
|
|
|
|
var isParentBlockLi = function (dom, node) {
|
|
return !!getParentLi(dom, node);
|
|
};
|
|
|
|
var getSplit = function (parentNode, rng) {
|
|
var beforeRng = rng.cloneRange();
|
|
var afterRng = rng.cloneRange();
|
|
|
|
beforeRng.setStartBefore(parentNode);
|
|
afterRng.setEndAfter(parentNode);
|
|
|
|
return [
|
|
beforeRng.cloneContents(),
|
|
afterRng.cloneContents()
|
|
];
|
|
};
|
|
|
|
var findFirstIn = function (node, rootNode) {
|
|
var caretPos = CaretPosition.before(node);
|
|
var caretWalker = new CaretWalker(rootNode);
|
|
var newCaretPos = caretWalker.next(caretPos);
|
|
|
|
return newCaretPos ? newCaretPos.toRange() : null;
|
|
};
|
|
|
|
var findLastOf = function (node, rootNode) {
|
|
var caretPos = CaretPosition.after(node);
|
|
var caretWalker = new CaretWalker(rootNode);
|
|
var newCaretPos = caretWalker.prev(caretPos);
|
|
|
|
return newCaretPos ? newCaretPos.toRange() : null;
|
|
};
|
|
|
|
var insertMiddle = function (target, elms, rootNode, rng) {
|
|
var parts = getSplit(target, rng);
|
|
var parentElm = target.parentNode;
|
|
|
|
parentElm.insertBefore(parts[0], target);
|
|
Tools.each(elms, function (li) {
|
|
parentElm.insertBefore(li, target);
|
|
});
|
|
parentElm.insertBefore(parts[1], target);
|
|
parentElm.removeChild(target);
|
|
|
|
return findLastOf(elms[elms.length - 1], rootNode);
|
|
};
|
|
|
|
var insertBefore = function (target, elms, rootNode) {
|
|
var parentElm = target.parentNode;
|
|
|
|
Tools.each(elms, function (elm) {
|
|
parentElm.insertBefore(elm, target);
|
|
});
|
|
|
|
return findFirstIn(target, rootNode);
|
|
};
|
|
|
|
var insertAfter = function (target, elms, rootNode, dom) {
|
|
dom.insertAfter(elms.reverse(), target);
|
|
return findLastOf(elms[0], rootNode);
|
|
};
|
|
|
|
var insertAtCaret = function (serializer, dom, rng, fragment) {
|
|
var domFragment = toDomFragment(dom, serializer, fragment);
|
|
var liTarget = getParentLi(dom, rng.startContainer);
|
|
var liElms = trimListItems(listItems(domFragment.firstChild));
|
|
var BEGINNING = 1, END = 2;
|
|
var rootNode = dom.getRoot();
|
|
|
|
var isAt = function (location) {
|
|
var caretPos = CaretPosition.fromRangeStart(rng);
|
|
var caretWalker = new CaretWalker(dom.getRoot());
|
|
var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos);
|
|
|
|
return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true;
|
|
};
|
|
|
|
if (isAt(BEGINNING)) {
|
|
return insertBefore(liTarget, liElms, rootNode);
|
|
} else if (isAt(END)) {
|
|
return insertAfter(liTarget, liElms, rootNode, dom);
|
|
}
|
|
|
|
return insertMiddle(liTarget, liElms, rootNode, rng);
|
|
};
|
|
|
|
return {
|
|
isListFragment: isListFragment,
|
|
insertAtCaret: insertAtCaret,
|
|
isParentBlockLi: isParentBlockLi,
|
|
trimListItems: trimListItems,
|
|
listItems: listItems
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* InsertContent.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 contents into the editor instance.
|
|
*
|
|
* @class tinymce.InsertContent
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.InsertContent',
|
|
[
|
|
'tinymce.core.caret.CaretPosition',
|
|
'tinymce.core.caret.CaretWalker',
|
|
'tinymce.core.dom.ElementUtils',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.dom.RangeNormalizer',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.html.Serializer',
|
|
'tinymce.core.InsertList',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (CaretPosition, CaretWalker, ElementUtils, NodeType, RangeNormalizer, Env, Serializer, InsertList, Tools) {
|
|
var isTableCell = NodeType.matchNodeNames('td th');
|
|
|
|
var validInsertion = function (editor, value, parentNode) {
|
|
// Should never insert content into bogus elements, since these can
|
|
// be resize handles or similar
|
|
if (parentNode.getAttribute('data-mce-bogus') === 'all') {
|
|
parentNode.parentNode.insertBefore(editor.dom.createFragment(value), parentNode);
|
|
} else {
|
|
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
|
|
var node = parentNode.firstChild;
|
|
var node2 = parentNode.lastChild;
|
|
if (!node || (node === node2 && node.nodeName === 'BR')) {///
|
|
editor.dom.setHTML(parentNode, value);
|
|
} else {
|
|
editor.selection.setContent(value);
|
|
}
|
|
}
|
|
};
|
|
|
|
var insertHtmlAtCaret = function (editor, value, details) {
|
|
var parser, serializer, parentNode, rootNode, fragment, args;
|
|
var marker, rng, node, node2, bookmarkHtml, merge;
|
|
var textInlineElements = editor.schema.getTextInlineElements();
|
|
var selection = editor.selection, dom = editor.dom;
|
|
|
|
function trimOrPaddLeftRight(html) {
|
|
var rng, container, offset;
|
|
|
|
rng = selection.getRng(true);
|
|
container = rng.startContainer;
|
|
offset = rng.startOffset;
|
|
|
|
function hasSiblingText(siblingName) {
|
|
return container[siblingName] && container[siblingName].nodeType == 3;
|
|
}
|
|
|
|
if (container.nodeType == 3) {
|
|
if (offset > 0) {
|
|
html = html.replace(/^ /, ' ');
|
|
} else if (!hasSiblingText('previousSibling')) {
|
|
html = html.replace(/^ /, ' ');
|
|
}
|
|
|
|
if (offset < container.length) {
|
|
html = html.replace(/ (<br>|)$/, ' ');
|
|
} else if (!hasSiblingText('nextSibling')) {
|
|
html = html.replace(/( | )(<br>|)$/, ' ');
|
|
}
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// Removes from a [b] c -> a c -> a c
|
|
function trimNbspAfterDeleteAndPaddValue() {
|
|
var rng, container, offset;
|
|
|
|
rng = selection.getRng(true);
|
|
container = rng.startContainer;
|
|
offset = rng.startOffset;
|
|
|
|
if (container.nodeType == 3 && rng.collapsed) {
|
|
if (container.data[offset] === '\u00a0') {
|
|
container.deleteData(offset, 1);
|
|
|
|
if (!/[\u00a0| ]$/.test(value)) {
|
|
value += ' ';
|
|
}
|
|
} else if (container.data[offset - 1] === '\u00a0') {
|
|
container.deleteData(offset - 1, 1);
|
|
|
|
if (!/[\u00a0| ]$/.test(value)) {
|
|
value = ' ' + value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function reduceInlineTextElements() {
|
|
if (merge) {
|
|
var root = editor.getBody(), elementUtils = new ElementUtils(dom);
|
|
|
|
Tools.each(dom.select('*[data-mce-fragment]'), function (node) {
|
|
for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) {
|
|
if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) {
|
|
dom.remove(node, true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function markFragmentElements(fragment) {
|
|
var node = fragment;
|
|
|
|
while ((node = node.walk())) {
|
|
if (node.type === 1) {
|
|
node.attr('data-mce-fragment', '1');
|
|
}
|
|
}
|
|
}
|
|
|
|
function umarkFragmentElements(elm) {
|
|
Tools.each(elm.getElementsByTagName('*'), function (elm) {
|
|
elm.removeAttribute('data-mce-fragment');
|
|
});
|
|
}
|
|
|
|
function isPartOfFragment(node) {
|
|
return !!node.getAttribute('data-mce-fragment');
|
|
}
|
|
|
|
function canHaveChildren(node) {
|
|
return node && !editor.schema.getShortEndedElements()[node.nodeName];
|
|
}
|
|
|
|
function moveSelectionToMarker(marker) {
|
|
var parentEditableFalseElm, parentBlock, nextRng;
|
|
|
|
function getContentEditableFalseParent(node) {
|
|
var root = editor.getBody();
|
|
|
|
for (; node && node !== root; node = node.parentNode) {
|
|
if (editor.dom.getContentEditable(node) === 'false') {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!marker) {
|
|
return;
|
|
}
|
|
|
|
selection.scrollIntoView(marker);
|
|
|
|
// If marker is in cE=false then move selection to that element instead
|
|
parentEditableFalseElm = getContentEditableFalseParent(marker);
|
|
if (parentEditableFalseElm) {
|
|
dom.remove(marker);
|
|
selection.select(parentEditableFalseElm);
|
|
return;
|
|
}
|
|
|
|
// Move selection before marker and remove it
|
|
rng = dom.createRng();
|
|
|
|
// If previous sibling is a text node set the selection to the end of that node
|
|
node = marker.previousSibling;
|
|
if (node && node.nodeType == 3) {
|
|
rng.setStart(node, node.nodeValue.length);
|
|
|
|
// TODO: Why can't we normalize on IE
|
|
if (!Env.ie) {
|
|
node2 = marker.nextSibling;
|
|
if (node2 && node2.nodeType == 3) {
|
|
node.appendData(node2.data);
|
|
node2.parentNode.removeChild(node2);
|
|
}
|
|
}
|
|
} else {
|
|
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
|
|
rng.setStartBefore(marker);
|
|
rng.setEndBefore(marker);
|
|
}
|
|
|
|
function findNextCaretRng(rng) {
|
|
var caretPos = CaretPosition.fromRangeStart(rng);
|
|
var caretWalker = new CaretWalker(editor.getBody());
|
|
|
|
caretPos = caretWalker.next(caretPos);
|
|
if (caretPos) {
|
|
return caretPos.toRange();
|
|
}
|
|
}
|
|
|
|
// Remove the marker node and set the new range
|
|
parentBlock = dom.getParent(marker, dom.isBlock);
|
|
dom.remove(marker);
|
|
|
|
if (parentBlock && dom.isEmpty(parentBlock)) {
|
|
editor.$(parentBlock).empty();
|
|
|
|
rng.setStart(parentBlock, 0);
|
|
rng.setEnd(parentBlock, 0);
|
|
|
|
if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) {
|
|
rng = nextRng;
|
|
dom.remove(parentBlock);
|
|
} else {
|
|
dom.add(parentBlock, dom.create('br', { 'data-mce-bogus': '1' }));
|
|
}
|
|
}
|
|
|
|
selection.setRng(rng);
|
|
}
|
|
|
|
// Check for whitespace before/after value
|
|
if (/^ | $/.test(value)) {
|
|
value = trimOrPaddLeftRight(value);
|
|
}
|
|
|
|
// Setup parser and serializer
|
|
parser = editor.parser;
|
|
merge = details.merge;
|
|
|
|
serializer = new Serializer({
|
|
validate: editor.settings.validate
|
|
}, editor.schema);
|
|
bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">​</span>';
|
|
|
|
// Run beforeSetContent handlers on the HTML to be inserted
|
|
args = { content: value, format: 'html', selection: true };
|
|
editor.fire('BeforeSetContent', args);
|
|
value = args.content;
|
|
|
|
// Add caret at end of contents if it's missing
|
|
if (value.indexOf('{$caret}') == -1) {
|
|
value += '{$caret}';
|
|
}
|
|
|
|
// Replace the caret marker with a span bookmark element
|
|
value = value.replace(/\{\$caret\}/, bookmarkHtml);
|
|
|
|
// If selection is at <body>|<p></p> then move it into <body><p>|</p>
|
|
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(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/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, '<hr />');
|
|
},
|
|
|
|
mceToggleVisualAid: function () {
|
|
editor.hasVisual = !editor.hasVisual;
|
|
editor.addVisual();
|
|
},
|
|
|
|
mceReplaceContent: function (command, ui, value) {
|
|
editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({ format: 'text' })));
|
|
},
|
|
|
|
mceInsertLink: function (command, ui, value) {
|
|
var anchor;
|
|
|
|
if (typeof value == 'string') {
|
|
value = { href: value };
|
|
}
|
|
|
|
anchor = dom.getParent(selection.getNode(), 'a');
|
|
|
|
// Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
|
|
value.href = value.href.replace(' ', '%20');
|
|
|
|
// Remove existing links if there could be child links or that the href isn't specified
|
|
if (!anchor || !value.href) {
|
|
formatter.remove('link');
|
|
}
|
|
|
|
// Apply new link to selection
|
|
if (value.href) {
|
|
formatter.apply('link', value, anchor);
|
|
}
|
|
},
|
|
|
|
selectAll: function () {
|
|
var root = dom.getRoot(), rng;
|
|
|
|
if (selection.getRng().setStart) {
|
|
var editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue);
|
|
if (editingHost) {
|
|
rng = dom.createRng();
|
|
rng.selectNodeContents(editingHost);
|
|
selection.setRng(rng);
|
|
}
|
|
} else {
|
|
// IE will render it's own root level block elements and sometimes
|
|
// even put font elements in them when the user starts typing. So we need to
|
|
// move the selection to a more suitable element from this:
|
|
// <body>|<p></p></body> to this: <body><p>|</p></body>
|
|
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
|
|
* <a href="/docs/advanced/events">event reference</a> 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
|
|
* <a href="/docs/advanced/events">event reference</a> 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
|
|
* <a href="/docs/advanced/events">event reference</a> 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
|
|
* <a href="/docs/advanced/events">event reference</a> 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:<state>
|
|
* element:not(<expression>)
|
|
* element:first
|
|
* element:last
|
|
* element:odd
|
|
* element:even
|
|
* element element
|
|
* element > element
|
|
*
|
|
* @class tinymce.ui.Selector
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Selector',
|
|
[
|
|
"tinymce.core.util.Class"
|
|
],
|
|
function (Class) {
|
|
"use strict";
|
|
|
|
/**
|
|
* Produces an array with a unique set of objects. It will not compare the values
|
|
* but the references of the objects.
|
|
*
|
|
* @private
|
|
* @method unqiue
|
|
* @param {Array} array Array to make into an array with unique items.
|
|
* @return {Array} Array with unique items.
|
|
*/
|
|
function unique(array) {
|
|
var uniqueItems = [], i = array.length, item;
|
|
|
|
while (i--) {
|
|
item = array[i];
|
|
|
|
if (!item.__checked) {
|
|
uniqueItems.push(item);
|
|
item.__checked = 1;
|
|
}
|
|
}
|
|
|
|
i = uniqueItems.length;
|
|
while (i--) {
|
|
delete uniqueItems[i].__checked;
|
|
}
|
|
|
|
return uniqueItems;
|
|
}
|
|
|
|
var expression = /^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i;
|
|
|
|
/*jshint maxlen:255 */
|
|
/*eslint max-len:0 */
|
|
var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
|
|
whiteSpace = /^\s*|\s*$/g,
|
|
Collection;
|
|
|
|
var Selector = Class.extend({
|
|
/**
|
|
* Constructs a new Selector instance.
|
|
*
|
|
* @constructor
|
|
* @method init
|
|
* @param {String} selector CSS like selector expression.
|
|
*/
|
|
init: function (selector) {
|
|
var match = this.match;
|
|
|
|
function compileNameFilter(name) {
|
|
if (name) {
|
|
name = name.toLowerCase();
|
|
|
|
return function (item) {
|
|
return name === '*' || item.type === name;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileIdFilter(id) {
|
|
if (id) {
|
|
return function (item) {
|
|
return item._name === id;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileClassesFilter(classes) {
|
|
if (classes) {
|
|
classes = classes.split('.');
|
|
|
|
return function (item) {
|
|
var i = classes.length;
|
|
|
|
while (i--) {
|
|
if (!item.classes.contains(classes[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileAttrFilter(name, cmp, check) {
|
|
if (name) {
|
|
return function (item) {
|
|
var value = item[name] ? item[name]() : '';
|
|
|
|
return !cmp ? !!check :
|
|
cmp === "=" ? value === check :
|
|
cmp === "*=" ? value.indexOf(check) >= 0 :
|
|
cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 :
|
|
cmp === "!=" ? value != check :
|
|
cmp === "^=" ? value.indexOf(check) === 0 :
|
|
cmp === "$=" ? value.substr(value.length - check.length) === check :
|
|
false;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compilePsuedoFilter(name) {
|
|
var notSelectors;
|
|
|
|
if (name) {
|
|
name = /(?:not\((.+)\))|(.+)/i.exec(name);
|
|
|
|
if (!name[1]) {
|
|
name = name[2];
|
|
|
|
return function (item, index, length) {
|
|
return name === 'first' ? index === 0 :
|
|
name === 'last' ? index === length - 1 :
|
|
name === 'even' ? index % 2 === 0 :
|
|
name === 'odd' ? index % 2 === 1 :
|
|
item[name] ? item[name]() :
|
|
false;
|
|
};
|
|
}
|
|
|
|
// Compile not expression
|
|
notSelectors = parseChunks(name[1], []);
|
|
|
|
return function (item) {
|
|
return !match(item, notSelectors);
|
|
};
|
|
}
|
|
}
|
|
|
|
function compile(selector, filters, direct) {
|
|
var parts;
|
|
|
|
function add(filter) {
|
|
if (filter) {
|
|
filters.push(filter);
|
|
}
|
|
}
|
|
|
|
// Parse expression into parts
|
|
parts = expression.exec(selector.replace(whiteSpace, ''));
|
|
|
|
add(compileNameFilter(parts[1]));
|
|
add(compileIdFilter(parts[2]));
|
|
add(compileClassesFilter(parts[3]));
|
|
add(compileAttrFilter(parts[4], parts[5], parts[6]));
|
|
add(compilePsuedoFilter(parts[7]));
|
|
|
|
// Mark the filter with pseudo for performance
|
|
filters.pseudo = !!parts[7];
|
|
filters.direct = direct;
|
|
|
|
return filters;
|
|
}
|
|
|
|
// Parser logic based on Sizzle by John Resig
|
|
function parseChunks(selector, selectors) {
|
|
var parts = [], extra, matches, i;
|
|
|
|
do {
|
|
chunker.exec("");
|
|
matches = chunker.exec(selector);
|
|
|
|
if (matches) {
|
|
selector = matches[3];
|
|
parts.push(matches[1]);
|
|
|
|
if (matches[2]) {
|
|
extra = matches[3];
|
|
break;
|
|
}
|
|
}
|
|
} while (matches);
|
|
|
|
if (extra) {
|
|
parseChunks(extra, selectors);
|
|
}
|
|
|
|
selector = [];
|
|
for (i = 0; i < parts.length; i++) {
|
|
if (parts[i] != '>') {
|
|
selector.push(compile(parts[i], [], parts[i - 1] === '>'));
|
|
}
|
|
}
|
|
|
|
selectors.push(selector);
|
|
|
|
return selectors;
|
|
}
|
|
|
|
this._selectors = parseChunks(selector, []);
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the selector matches the specified control.
|
|
*
|
|
* @method match
|
|
* @param {tinymce.ui.Control} control Control to match against the selector.
|
|
* @param {Array} selectors Optional array of selectors, mostly used internally.
|
|
* @return {Boolean} true/false state if the control matches or not.
|
|
*/
|
|
match: function (control, selectors) {
|
|
var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item;
|
|
|
|
selectors = selectors || this._selectors;
|
|
for (i = 0, l = selectors.length; i < l; i++) {
|
|
selector = selectors[i];
|
|
sl = selector.length;
|
|
item = control;
|
|
count = 0;
|
|
|
|
for (si = sl - 1; si >= 0; si--) {
|
|
filters = selector[si];
|
|
|
|
while (item) {
|
|
// Find the index and length since a pseudo filter like :first needs it
|
|
if (filters.pseudo) {
|
|
siblings = item.parent().items();
|
|
index = length = siblings.length;
|
|
while (index--) {
|
|
if (siblings[index] === item) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (fi = 0, fl = filters.length; fi < fl; fi++) {
|
|
if (!filters[fi](item, index, length)) {
|
|
fi = fl + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fi === fl) {
|
|
count++;
|
|
break;
|
|
} else {
|
|
// If it didn't match the right most expression then
|
|
// break since it's no point looking at the parents
|
|
if (si === sl - 1) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
item = item.parent();
|
|
}
|
|
}
|
|
|
|
// If we found all selectors then return true otherwise continue looking
|
|
if (count === sl) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container.
|
|
*
|
|
* @method find
|
|
* @param {tinymce.ui.Control} container Container to look for items in.
|
|
* @return {tinymce.ui.Collection} Collection with matched elements.
|
|
*/
|
|
find: function (container) {
|
|
var matches = [], i, l, selectors = this._selectors;
|
|
|
|
function collect(items, selector, index) {
|
|
var i, l, fi, fl, item, filters = selector[index];
|
|
|
|
for (i = 0, l = items.length; i < l; i++) {
|
|
item = items[i];
|
|
|
|
// Run each filter against the item
|
|
for (fi = 0, fl = filters.length; fi < fl; fi++) {
|
|
if (!filters[fi](item, i, l)) {
|
|
fi = fl + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// All filters matched the item
|
|
if (fi === fl) {
|
|
// Matched item is on the last expression like: panel toolbar [button]
|
|
if (index == selector.length - 1) {
|
|
matches.push(item);
|
|
} else {
|
|
// Collect next expression type
|
|
if (item.items) {
|
|
collect(item.items(), selector, index + 1);
|
|
}
|
|
}
|
|
} else if (filters.direct) {
|
|
return;
|
|
}
|
|
|
|
// Collect child items
|
|
if (item.items) {
|
|
collect(item.items(), selector, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (container.items) {
|
|
for (i = 0, l = selectors.length; i < l; i++) {
|
|
collect(container.items(), selectors[i], 0);
|
|
}
|
|
|
|
// Unique the matches if needed
|
|
if (l > 1) {
|
|
matches = unique(matches);
|
|
}
|
|
}
|
|
|
|
// Fix for circular reference
|
|
if (!Collection) {
|
|
// TODO: Fix me!
|
|
Collection = Selector.Collection;
|
|
}
|
|
|
|
return new Collection(matches);
|
|
}
|
|
});
|
|
|
|
return Selector;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Collection.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
|
|
*/
|
|
|
|
/**
|
|
* Control collection, this class contains control instances and it enables you to
|
|
* perform actions on all the contained items. This is very similar to how jQuery works.
|
|
*
|
|
* @example
|
|
* someCollection.show().disabled(true);
|
|
*
|
|
* @class tinymce.ui.Collection
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Collection',
|
|
[
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.ui.Selector",
|
|
"tinymce.core.util.Class"
|
|
],
|
|
function (Tools, Selector, Class) {
|
|
"use strict";
|
|
|
|
var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice;
|
|
|
|
proto = {
|
|
/**
|
|
* Current number of contained control instances.
|
|
*
|
|
* @field length
|
|
* @type Number
|
|
*/
|
|
length: 0,
|
|
|
|
/**
|
|
* Constructor for the collection.
|
|
*
|
|
* @constructor
|
|
* @method init
|
|
* @param {Array} items Optional array with items to add.
|
|
*/
|
|
init: function (items) {
|
|
if (items) {
|
|
this.add(items);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds new items to the control collection.
|
|
*
|
|
* @method add
|
|
* @param {Array} items Array if items to add to collection.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
add: function (items) {
|
|
var self = this;
|
|
|
|
// Force single item into array
|
|
if (!Tools.isArray(items)) {
|
|
if (items instanceof Collection) {
|
|
self.add(items.toArray());
|
|
} else {
|
|
push.call(self, items);
|
|
}
|
|
} else {
|
|
push.apply(self, items);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Sets the contents of the collection. This will remove any existing items
|
|
* and replace them with the ones specified in the input array.
|
|
*
|
|
* @method set
|
|
* @param {Array} items Array with items to set into the Collection.
|
|
* @return {tinymce.ui.Collection} Collection instance.
|
|
*/
|
|
set: function (items) {
|
|
var self = this, len = self.length, i;
|
|
|
|
self.length = 0;
|
|
self.add(items);
|
|
|
|
// Remove old entries
|
|
for (i = self.length; i < len; i++) {
|
|
delete self[i];
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Filters the collection item based on the specified selector expression or selector function.
|
|
*
|
|
* @method filter
|
|
* @param {String} selector Selector expression to filter items by.
|
|
* @return {tinymce.ui.Collection} Collection containing the filtered items.
|
|
*/
|
|
filter: function (selector) {
|
|
var self = this, i, l, matches = [], item, match;
|
|
|
|
// Compile string into selector expression
|
|
if (typeof selector === "string") {
|
|
selector = new Selector(selector);
|
|
|
|
match = function (item) {
|
|
return selector.match(item);
|
|
};
|
|
} else {
|
|
// Use selector as matching function
|
|
match = selector;
|
|
}
|
|
|
|
for (i = 0, l = self.length; i < l; i++) {
|
|
item = self[i];
|
|
|
|
if (match(item)) {
|
|
matches.push(item);
|
|
}
|
|
}
|
|
|
|
return new Collection(matches);
|
|
},
|
|
|
|
/**
|
|
* Slices the items within the collection.
|
|
*
|
|
* @method slice
|
|
* @param {Number} index Index to slice at.
|
|
* @param {Number} len Optional length to slice.
|
|
* @return {tinymce.ui.Collection} Current collection.
|
|
*/
|
|
slice: function () {
|
|
return new Collection(slice.apply(this, arguments));
|
|
},
|
|
|
|
/**
|
|
* Makes the current collection equal to the specified index.
|
|
*
|
|
* @method eq
|
|
* @param {Number} index Index of the item to set the collection to.
|
|
* @return {tinymce.ui.Collection} Current collection.
|
|
*/
|
|
eq: function (index) {
|
|
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
|
|
},
|
|
|
|
/**
|
|
* Executes the specified callback on each item in collection.
|
|
*
|
|
* @method each
|
|
* @param {function} callback Callback to execute for each item in collection.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
each: function (callback) {
|
|
Tools.each(this, callback);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns an JavaScript array object of the contents inside the collection.
|
|
*
|
|
* @method toArray
|
|
* @return {Array} Array with all items from collection.
|
|
*/
|
|
toArray: function () {
|
|
return Tools.toArray(this);
|
|
},
|
|
|
|
/**
|
|
* Finds the index of the specified control or return -1 if it isn't in the collection.
|
|
*
|
|
* @method indexOf
|
|
* @param {Control} ctrl Control instance to look for.
|
|
* @return {Number} Index of the specified control or -1.
|
|
*/
|
|
indexOf: function (ctrl) {
|
|
var self = this, i = self.length;
|
|
|
|
while (i--) {
|
|
if (self[i] === ctrl) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return i;
|
|
},
|
|
|
|
/**
|
|
* Returns a new collection of the contents in reverse order.
|
|
*
|
|
* @method reverse
|
|
* @return {tinymce.ui.Collection} Collection instance with reversed items.
|
|
*/
|
|
reverse: function () {
|
|
return new Collection(Tools.toArray(this).reverse());
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the class exists or not.
|
|
*
|
|
* @method hasClass
|
|
* @param {String} cls Class to check for.
|
|
* @return {Boolean} true/false state if the class exists or not.
|
|
*/
|
|
hasClass: function (cls) {
|
|
return this[0] ? this[0].classes.contains(cls) : false;
|
|
},
|
|
|
|
/**
|
|
* Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>);
|
|
*
|
|
* @method prop
|
|
* @param {String} name Property name to get/set.
|
|
* @param {Object} value Optional object value to set.
|
|
* @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation.
|
|
*/
|
|
prop: function (name, value) {
|
|
var self = this, undef, item;
|
|
|
|
if (value !== undef) {
|
|
self.each(function (item) {
|
|
if (item[name]) {
|
|
item[name](value);
|
|
}
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
item = self[0];
|
|
|
|
if (item && item[name]) {
|
|
return item[name]();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Executes the specific function name with optional arguments an all items in collection if it exists.
|
|
*
|
|
* @example collection.exec("myMethod", arg1, arg2, arg3);
|
|
* @method exec
|
|
* @param {String} name Name of the function to execute.
|
|
* @param {Object} ... Multiple arguments to pass to each function.
|
|
* @return {tinymce.ui.Collection} Current collection.
|
|
*/
|
|
exec: function (name) {
|
|
var self = this, args = Tools.toArray(arguments).slice(1);
|
|
|
|
self.each(function (item) {
|
|
if (item[name]) {
|
|
item[name].apply(item, args);
|
|
}
|
|
});
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Remove all items from collection and DOM.
|
|
*
|
|
* @method remove
|
|
* @return {tinymce.ui.Collection} Current collection.
|
|
*/
|
|
remove: function () {
|
|
var i = this.length;
|
|
|
|
while (i--) {
|
|
this[i].remove();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Adds a class to all items in the collection.
|
|
*
|
|
* @method addClass
|
|
* @param {String} cls Class to add to each item.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
addClass: function (cls) {
|
|
return this.each(function (item) {
|
|
item.classes.add(cls);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Removes the specified class from all items in collection.
|
|
*
|
|
* @method removeClass
|
|
* @param {String} cls Class to remove from each item.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
removeClass: function (cls) {
|
|
return this.each(function (item) {
|
|
item.classes.remove(cls);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 Optional arguments to pass to the event.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
// fire: function(event, args) {}, -- Generated by code below
|
|
|
|
/**
|
|
* 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 have two parameters the first one being the control that received the event
|
|
* the second one will be the event object either the browsers native event object or a custom JS object.
|
|
*
|
|
* @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.Collection} Current collection instance.
|
|
*/
|
|
// on: function(name, callback) {}, -- Generated by code below
|
|
|
|
/**
|
|
* 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 Optional name for the event to unbind.
|
|
* @param {function} callback Optional callback function to unbind.
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
// off: function(name, callback) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Shows the items in the current collection.
|
|
*
|
|
* @method show
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
// show: function() {}, -- Generated by code below
|
|
|
|
/**
|
|
* Hides the items in the current collection.
|
|
*
|
|
* @method hide
|
|
* @return {tinymce.ui.Collection} Current collection instance.
|
|
*/
|
|
// hide: function() {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the text contents of the items in the current collection.
|
|
*
|
|
* @method text
|
|
* @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation.
|
|
*/
|
|
// text: function(value) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the name contents of the items in the current collection.
|
|
*
|
|
* @method name
|
|
* @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation.
|
|
*/
|
|
// name: function(value) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the disabled state on the items in the current collection.
|
|
*
|
|
* @method disabled
|
|
* @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation.
|
|
*/
|
|
// disabled: function(state) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the active state on the items in the current collection.
|
|
*
|
|
* @method active
|
|
* @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation.
|
|
*/
|
|
// active: function(state) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the selected state on the items in the current collection.
|
|
*
|
|
* @method selected
|
|
* @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation.
|
|
*/
|
|
// selected: function(state) {}, -- Generated by code below
|
|
|
|
/**
|
|
* Sets/gets the selected state on the items in the current collection.
|
|
*
|
|
* @method visible
|
|
* @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation.
|
|
*/
|
|
// visible: function(state) {}, -- Generated by code below
|
|
};
|
|
|
|
// Extend tinymce.ui.Collection prototype with some generated control specific methods
|
|
Tools.each('fire on off show hide append prepend before after reflow'.split(' '), function (name) {
|
|
proto[name] = function () {
|
|
var args = Tools.toArray(arguments);
|
|
|
|
this.each(function (ctrl) {
|
|
if (name in ctrl) {
|
|
ctrl[name].apply(ctrl, args);
|
|
}
|
|
});
|
|
|
|
return this;
|
|
};
|
|
});
|
|
|
|
// Extend tinymce.ui.Collection prototype with some property methods
|
|
Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function (name) {
|
|
proto[name] = function (value) {
|
|
return this.prop(name, value);
|
|
};
|
|
});
|
|
|
|
// Create class based on the new prototype
|
|
Collection = Class.extend(proto);
|
|
|
|
// Stick Collection into Selector to prevent circual references
|
|
Selector.Collection = Collection;
|
|
|
|
return Collection;
|
|
}
|
|
);
|
|
/**
|
|
* DomUtils.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
|
|
*/
|
|
|
|
/**
|
|
* Private UI DomUtils proxy.
|
|
*
|
|
* @private
|
|
* @class tinymce.ui.DomUtils
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.DomUtils',
|
|
[
|
|
"tinymce.core.Env",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.dom.DOMUtils"
|
|
],
|
|
function (Env, Tools, DOMUtils) {
|
|
"use strict";
|
|
|
|
var count = 0;
|
|
|
|
var funcs = {
|
|
id: function () {
|
|
return 'mceu_' + (count++);
|
|
},
|
|
|
|
create: function (name, attrs, children) {
|
|
var elm = document.createElement(name);
|
|
|
|
DOMUtils.DOM.setAttribs(elm, attrs);
|
|
|
|
if (typeof children === 'string') {
|
|
elm.innerHTML = children;
|
|
} else {
|
|
Tools.each(children, function (child) {
|
|
if (child.nodeType) {
|
|
elm.appendChild(child);
|
|
}
|
|
});
|
|
}
|
|
|
|
return elm;
|
|
},
|
|
|
|
createFragment: function (html) {
|
|
return DOMUtils.DOM.createFragment(html);
|
|
},
|
|
|
|
getWindowSize: function () {
|
|
return DOMUtils.DOM.getViewPort();
|
|
},
|
|
|
|
getSize: function (elm) {
|
|
var width, height;
|
|
|
|
if (elm.getBoundingClientRect) {
|
|
var rect = elm.getBoundingClientRect();
|
|
|
|
width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth);
|
|
height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight);
|
|
} else {
|
|
width = elm.offsetWidth;
|
|
height = elm.offsetHeight;
|
|
}
|
|
|
|
return { width: width, height: height };
|
|
},
|
|
|
|
getPos: function (elm, root) {
|
|
return DOMUtils.DOM.getPos(elm, root || funcs.getContainer());
|
|
},
|
|
|
|
getContainer: function () {
|
|
return Env.container ? Env.container : document.body;
|
|
},
|
|
|
|
getViewPort: function (win) {
|
|
return DOMUtils.DOM.getViewPort(win);
|
|
},
|
|
|
|
get: function (id) {
|
|
return document.getElementById(id);
|
|
},
|
|
|
|
addClass: function (elm, cls) {
|
|
return DOMUtils.DOM.addClass(elm, cls);
|
|
},
|
|
|
|
removeClass: function (elm, cls) {
|
|
return DOMUtils.DOM.removeClass(elm, cls);
|
|
},
|
|
|
|
hasClass: function (elm, cls) {
|
|
return DOMUtils.DOM.hasClass(elm, cls);
|
|
},
|
|
|
|
toggleClass: function (elm, cls, state) {
|
|
return DOMUtils.DOM.toggleClass(elm, cls, state);
|
|
},
|
|
|
|
css: function (elm, name, value) {
|
|
return DOMUtils.DOM.setStyle(elm, name, value);
|
|
},
|
|
|
|
getRuntimeStyle: function (elm, name) {
|
|
return DOMUtils.DOM.getStyle(elm, name, true);
|
|
},
|
|
|
|
on: function (target, name, callback, scope) {
|
|
return DOMUtils.DOM.bind(target, name, callback, scope);
|
|
},
|
|
|
|
off: function (target, name, callback) {
|
|
return DOMUtils.DOM.unbind(target, name, callback);
|
|
},
|
|
|
|
fire: function (target, name, args) {
|
|
return DOMUtils.DOM.fire(target, name, args);
|
|
},
|
|
|
|
innerHtml: function (elm, html) {
|
|
// Workaround for <div> in <p> 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 '<div id="' + this._id + '" class="' + this.classes + '"></div>';
|
|
},
|
|
|
|
/**
|
|
* 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 on<event> settings
|
|
for (name in settings) {
|
|
if (name.indexOf("on") === 0) {
|
|
self.on(name.substr(2), settings[name]);
|
|
}
|
|
}
|
|
|
|
if (self._eventsRoot) {
|
|
for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) {
|
|
parentEventsRoot = parent._eventsRoot;
|
|
}
|
|
|
|
if (parentEventsRoot) {
|
|
for (name in parentEventsRoot._nativeEvents) {
|
|
self._nativeEvents[name] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bindPendingEvents(self);
|
|
|
|
if (settings.style) {
|
|
elm = self.getEl();
|
|
if (elm) {
|
|
elm.setAttribute('style', settings.style);
|
|
elm.style.cssText = settings.style;
|
|
}
|
|
}
|
|
|
|
if (self.settings.border) {
|
|
box = self.borderBox;
|
|
self.$el.css({
|
|
'border-top-width': box.top,
|
|
'border-right-width': box.right,
|
|
'border-bottom-width': box.bottom,
|
|
'border-left-width': box.left
|
|
});
|
|
}
|
|
|
|
// Add instance to lookup
|
|
var root = self.getRoot();
|
|
if (!root.controlIdLookup) {
|
|
root.controlIdLookup = {};
|
|
}
|
|
|
|
root.controlIdLookup[self._id] = self;
|
|
|
|
for (var key in self._aria) {
|
|
self.aria(key, self._aria[key]);
|
|
}
|
|
|
|
if (self.state.get('visible') === false) {
|
|
self.getEl().style.display = 'none';
|
|
}
|
|
|
|
self.bindStates();
|
|
|
|
self.state.on('change:visible', function (e) {
|
|
var state = e.value, parentCtrl;
|
|
|
|
if (self.state.get('rendered')) {
|
|
self.getEl().style.display = state === false ? 'none' : '';
|
|
|
|
// Need to force a reflow here on IE 8
|
|
self.getEl().getBoundingClientRect();
|
|
}
|
|
|
|
// Parent container needs to reflow
|
|
parentCtrl = self.parent();
|
|
if (parentCtrl) {
|
|
parentCtrl._lastRect = null;
|
|
}
|
|
|
|
self.fire(state ? 'show' : 'hide');
|
|
|
|
ReflowQueue.add(self);
|
|
});
|
|
|
|
self.fire('postrender', {}, false);
|
|
},
|
|
|
|
bindStates: function () {
|
|
},
|
|
|
|
/**
|
|
* Scrolls the current control into view.
|
|
*
|
|
* @method scrollIntoView
|
|
* @param {String} align Alignment in view top|center|bottom.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
scrollIntoView: function (align) {
|
|
function getOffset(elm, rootElm) {
|
|
var x, y, parent = elm;
|
|
|
|
x = y = 0;
|
|
while (parent && parent != rootElm && parent.nodeType) {
|
|
x += parent.offsetLeft || 0;
|
|
y += parent.offsetTop || 0;
|
|
parent = parent.offsetParent;
|
|
}
|
|
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
var elm = this.getEl(), parentElm = elm.parentNode;
|
|
var x, y, width, height, parentWidth, parentHeight;
|
|
var pos = getOffset(elm, parentElm);
|
|
|
|
x = pos.x;
|
|
y = pos.y;
|
|
width = elm.offsetWidth;
|
|
height = elm.offsetHeight;
|
|
parentWidth = parentElm.clientWidth;
|
|
parentHeight = parentElm.clientHeight;
|
|
|
|
if (align == "end") {
|
|
x -= parentWidth - width;
|
|
y -= parentHeight - height;
|
|
} else if (align == "center") {
|
|
x -= (parentWidth / 2) - (width / 2);
|
|
y -= (parentHeight / 2) - (height / 2);
|
|
}
|
|
|
|
parentElm.scrollLeft = x;
|
|
parentElm.scrollTop = y;
|
|
|
|
return this;
|
|
},
|
|
|
|
getRoot: function () {
|
|
var ctrl = this, rootControl, parents = [];
|
|
|
|
while (ctrl) {
|
|
if (ctrl.rootControl) {
|
|
rootControl = ctrl.rootControl;
|
|
break;
|
|
}
|
|
|
|
parents.push(ctrl);
|
|
rootControl = ctrl;
|
|
ctrl = ctrl.parent();
|
|
}
|
|
|
|
if (!rootControl) {
|
|
rootControl = this;
|
|
}
|
|
|
|
var i = parents.length;
|
|
while (i--) {
|
|
parents[i].rootControl = rootControl;
|
|
}
|
|
|
|
return rootControl;
|
|
},
|
|
|
|
/**
|
|
* Reflows the current control and it's parents.
|
|
* This should be used after you for example append children to the current control so
|
|
* that the layout managers know that they need to reposition everything.
|
|
*
|
|
* @example
|
|
* container.append({type: 'button', text: 'My button'}).reflow();
|
|
*
|
|
* @method reflow
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
reflow: function () {
|
|
ReflowQueue.remove(this);
|
|
|
|
var parent = this.parent();
|
|
if (parent && parent._layout && !parent._layout.isNative()) {
|
|
parent.reflow();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets/gets the parent container for the control.
|
|
*
|
|
* @method parent
|
|
* @param {tinymce.ui.Container} parent Optional parent to set.
|
|
* @return {tinymce.ui.Control} Parent control or the current control on a set action.
|
|
*/
|
|
// parent: function(parent) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the text for the control.
|
|
*
|
|
* @method text
|
|
* @param {String} value Value to set to control.
|
|
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
|
|
*/
|
|
// text: function(value) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the disabled state on the control.
|
|
*
|
|
* @method disabled
|
|
* @param {Boolean} state Value to set to control.
|
|
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
|
|
*/
|
|
// disabled: function(state) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the active for the control.
|
|
*
|
|
* @method active
|
|
* @param {Boolean} state Value to set to control.
|
|
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
|
|
*/
|
|
// active: function(state) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the name for the control.
|
|
*
|
|
* @method name
|
|
* @param {String} value Value to set to control.
|
|
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
|
|
*/
|
|
// name: function(value) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the title for the control.
|
|
*
|
|
* @method title
|
|
* @param {String} value Value to set to control.
|
|
* @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get.
|
|
*/
|
|
// title: function(value) {} -- Generated
|
|
|
|
/**
|
|
* Sets/gets the visible for the control.
|
|
*
|
|
* @method visible
|
|
* @param {Boolean} state Value to set to control.
|
|
* @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get.
|
|
*/
|
|
// visible: function(value) {} -- Generated
|
|
};
|
|
|
|
/**
|
|
* Setup state properties.
|
|
*/
|
|
Tools.each('text title visible disabled active value'.split(' '), function (name) {
|
|
proto[name] = function (value) {
|
|
if (arguments.length === 0) {
|
|
return this.state.get(name);
|
|
}
|
|
|
|
if (typeof value != "undefined") {
|
|
this.state.set(name, value);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
});
|
|
|
|
Control = Class.extend(proto);
|
|
|
|
function getEventDispatcher(obj) {
|
|
if (!obj._eventDispatcher) {
|
|
obj._eventDispatcher = new EventDispatcher({
|
|
scope: obj,
|
|
toggleEvent: function (name, state) {
|
|
if (state && EventDispatcher.isNative(name)) {
|
|
if (!obj._nativeEvents) {
|
|
obj._nativeEvents = {};
|
|
}
|
|
|
|
obj._nativeEvents[name] = true;
|
|
|
|
if (obj.state.get('rendered')) {
|
|
bindPendingEvents(obj);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return obj._eventDispatcher;
|
|
}
|
|
|
|
function bindPendingEvents(eventCtrl) {
|
|
var i, l, parents, eventRootCtrl, nativeEvents, name;
|
|
|
|
function delegate(e) {
|
|
var control = eventCtrl.getParentCtrl(e.target);
|
|
|
|
if (control) {
|
|
control.fire(e.type, e);
|
|
}
|
|
}
|
|
|
|
function mouseLeaveHandler() {
|
|
var ctrl = eventRootCtrl._lastHoverCtrl;
|
|
|
|
if (ctrl) {
|
|
ctrl.fire("mouseleave", { target: ctrl.getEl() });
|
|
|
|
ctrl.parents().each(function (ctrl) {
|
|
ctrl.fire("mouseleave", { target: ctrl.getEl() });
|
|
});
|
|
|
|
eventRootCtrl._lastHoverCtrl = null;
|
|
}
|
|
}
|
|
|
|
function mouseEnterHandler(e) {
|
|
var ctrl = eventCtrl.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents;
|
|
|
|
// Over on a new control
|
|
if (ctrl !== lastCtrl) {
|
|
eventRootCtrl._lastHoverCtrl = ctrl;
|
|
|
|
parents = ctrl.parents().toArray().reverse();
|
|
parents.push(ctrl);
|
|
|
|
if (lastCtrl) {
|
|
lastParents = lastCtrl.parents().toArray().reverse();
|
|
lastParents.push(lastCtrl);
|
|
|
|
for (idx = 0; idx < lastParents.length; idx++) {
|
|
if (parents[idx] !== lastParents[idx]) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (i = lastParents.length - 1; i >= idx; i--) {
|
|
lastCtrl = lastParents[i];
|
|
lastCtrl.fire("mouseleave", {
|
|
target: lastCtrl.getEl()
|
|
});
|
|
}
|
|
}
|
|
|
|
for (i = idx; i < parents.length; i++) {
|
|
ctrl = parents[i];
|
|
ctrl.fire("mouseenter", {
|
|
target: ctrl.getEl()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function fixWheelEvent(e) {
|
|
e.preventDefault();
|
|
|
|
if (e.type == "mousewheel") {
|
|
e.deltaY = -1 / 40 * e.wheelDelta;
|
|
|
|
if (e.wheelDeltaX) {
|
|
e.deltaX = -1 / 40 * e.wheelDeltaX;
|
|
}
|
|
} else {
|
|
e.deltaX = 0;
|
|
e.deltaY = e.detail;
|
|
}
|
|
|
|
e = eventCtrl.fire("wheel", e);
|
|
}
|
|
|
|
nativeEvents = eventCtrl._nativeEvents;
|
|
if (nativeEvents) {
|
|
// Find event root element if it exists
|
|
parents = eventCtrl.parents().toArray();
|
|
parents.unshift(eventCtrl);
|
|
for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) {
|
|
eventRootCtrl = parents[i]._eventsRoot;
|
|
}
|
|
|
|
// Event root wasn't found the use the root control
|
|
if (!eventRootCtrl) {
|
|
eventRootCtrl = parents[parents.length - 1] || eventCtrl;
|
|
}
|
|
|
|
// Set the eventsRoot property on children that didn't have it
|
|
eventCtrl._eventsRoot = eventRootCtrl;
|
|
for (l = i, i = 0; i < l; i++) {
|
|
parents[i]._eventsRoot = eventRootCtrl;
|
|
}
|
|
|
|
var eventRootDelegates = eventRootCtrl._delegates;
|
|
if (!eventRootDelegates) {
|
|
eventRootDelegates = eventRootCtrl._delegates = {};
|
|
}
|
|
|
|
// Bind native event delegates
|
|
for (name in nativeEvents) {
|
|
if (!nativeEvents) {
|
|
return false;
|
|
}
|
|
|
|
if (name === "wheel" && !hasWheelEventSupport) {
|
|
if (hasMouseWheelEventSupport) {
|
|
$(eventCtrl.getEl()).on("mousewheel", fixWheelEvent);
|
|
} else {
|
|
$(eventCtrl.getEl()).on("DOMMouseScroll", fixWheelEvent);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Special treatment for mousenter/mouseleave since these doesn't bubble
|
|
if (name === "mouseenter" || name === "mouseleave") {
|
|
// Fake mousenter/mouseleave
|
|
if (!eventRootCtrl._hasMouseEnter) {
|
|
$(eventRootCtrl.getEl()).on("mouseleave", mouseLeaveHandler).on("mouseover", mouseEnterHandler);
|
|
eventRootCtrl._hasMouseEnter = 1;
|
|
}
|
|
} else if (!eventRootDelegates[name]) {
|
|
$(eventRootCtrl.getEl()).on(name, delegate);
|
|
eventRootDelegates[name] = true;
|
|
}
|
|
|
|
// Remove the event once it's bound
|
|
nativeEvents[name] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Control;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Factory.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 factory for control instances. This enables you
|
|
* to create instances of controls without having to require the UI controls directly.
|
|
*
|
|
* It also allow you to override or add new control types.
|
|
*
|
|
* @class tinymce.ui.Factory
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Factory',
|
|
[
|
|
],
|
|
function () {
|
|
"use strict";
|
|
|
|
var types = {};
|
|
|
|
return {
|
|
/**
|
|
* Adds a new control instance type to the factory.
|
|
*
|
|
* @method add
|
|
* @param {String} type Type name for example "button".
|
|
* @param {function} typeClass Class type function.
|
|
*/
|
|
add: function (type, typeClass) {
|
|
types[type.toLowerCase()] = typeClass;
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the specified type exists or not.
|
|
*
|
|
* @method has
|
|
* @param {String} type Type to look for.
|
|
* @return {Boolean} true/false if the control by name exists.
|
|
*/
|
|
has: function (type) {
|
|
return !!types[type.toLowerCase()];
|
|
},
|
|
|
|
/**
|
|
* Creates a new control instance based on the settings provided. The instance created will be
|
|
* based on the specified type property it can also create whole structures of components out of
|
|
* the specified JSON object.
|
|
*
|
|
* @example
|
|
* tinymce.ui.Factory.create({
|
|
* type: 'button',
|
|
* text: 'Hello world!'
|
|
* });
|
|
*
|
|
* @method create
|
|
* @param {Object/String} settings Name/Value object with items used to create the type.
|
|
* @return {tinymce.ui.Control} Control instance based on the specified type.
|
|
*/
|
|
create: function (type, settings) {
|
|
var ControlType;
|
|
|
|
// If string is specified then use it as the type
|
|
if (typeof type == 'string') {
|
|
settings = settings || {};
|
|
settings.type = type;
|
|
} else {
|
|
settings = type;
|
|
type = settings.type;
|
|
}
|
|
|
|
// Find control type
|
|
type = type.toLowerCase();
|
|
ControlType = types[type];
|
|
|
|
// #if debug
|
|
|
|
if (!ControlType) {
|
|
throw new Error("Could not find control by type: " + type);
|
|
}
|
|
|
|
// #endif
|
|
|
|
ControlType = new ControlType(settings);
|
|
ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine
|
|
|
|
return ControlType;
|
|
}
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* KeyboardNavigation.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 keyboard navigation of controls and elements.
|
|
*
|
|
* @class tinymce.ui.KeyboardNavigation
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.KeyboardNavigation',
|
|
[
|
|
],
|
|
function () {
|
|
"use strict";
|
|
|
|
var hasTabstopData = function (elm) {
|
|
return elm.getAttribute('data-mce-tabstop') ? true : false;
|
|
};
|
|
|
|
/**
|
|
* This class handles all keyboard navigation for WAI-ARIA support. Each root container
|
|
* gets an instance of this class.
|
|
*
|
|
* @constructor
|
|
*/
|
|
return function (settings) {
|
|
var root = settings.root, focusedElement, focusedControl;
|
|
|
|
function isElement(node) {
|
|
return node && node.nodeType === 1;
|
|
}
|
|
|
|
try {
|
|
focusedElement = document.activeElement;
|
|
} catch (ex) {
|
|
// IE sometimes fails to return a proper element
|
|
focusedElement = document.body;
|
|
}
|
|
|
|
focusedControl = root.getParentCtrl(focusedElement);
|
|
|
|
/**
|
|
* Returns the currently focused elements wai aria role of the currently
|
|
* focused element or specified element.
|
|
*
|
|
* @private
|
|
* @param {Element} elm Optional element to get role from.
|
|
* @return {String} Role of specified element.
|
|
*/
|
|
function getRole(elm) {
|
|
elm = elm || focusedElement;
|
|
|
|
if (isElement(elm)) {
|
|
return elm.getAttribute('role');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the wai role of the parent element of the currently
|
|
* focused element or specified element.
|
|
*
|
|
* @private
|
|
* @param {Element} elm Optional element to get parent role from.
|
|
* @return {String} Role of the first parent that has a role.
|
|
*/
|
|
function getParentRole(elm) {
|
|
var role, parent = elm || focusedElement;
|
|
|
|
while ((parent = parent.parentNode)) {
|
|
if ((role = getRole(parent))) {
|
|
return role;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a wai aria property by name for example aria-selected.
|
|
*
|
|
* @private
|
|
* @param {String} name Name of the aria property to get for example "disabled".
|
|
* @return {String} Aria property value.
|
|
*/
|
|
function getAriaProp(name) {
|
|
var elm = focusedElement;
|
|
|
|
if (isElement(elm)) {
|
|
return elm.getAttribute('aria-' + name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is the element a text input element or not.
|
|
*
|
|
* @private
|
|
* @param {Element} elm Element to check if it's an text input element or not.
|
|
* @return {Boolean} True/false if the element is a text element or not.
|
|
*/
|
|
function isTextInputElement(elm) {
|
|
var tagName = elm.tagName.toUpperCase();
|
|
|
|
// Notice: since type can be "email" etc we don't check the type
|
|
// So all input elements gets treated as text input elements
|
|
return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
|
|
}
|
|
|
|
/**
|
|
* Returns true/false if the specified element can be focused or not.
|
|
*
|
|
* @private
|
|
* @param {Element} elm DOM element to check if it can be focused or not.
|
|
* @return {Boolean} True/false if the element can have focus.
|
|
*/
|
|
function canFocus(elm) {
|
|
if (isTextInputElement(elm) && !elm.hidden) {
|
|
return true;
|
|
}
|
|
|
|
if (hasTabstopData(elm)) {
|
|
return true;
|
|
}
|
|
|
|
if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of focusable visible elements within the specified container element.
|
|
*
|
|
* @private
|
|
* @param {Element} elm DOM element to find focusable elements within.
|
|
* @return {Array} Array of focusable elements.
|
|
*/
|
|
function getFocusElements(elm) {
|
|
var elements = [];
|
|
|
|
function collect(elm) {
|
|
if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) {
|
|
return;
|
|
}
|
|
|
|
if (canFocus(elm)) {
|
|
elements.push(elm);
|
|
}
|
|
|
|
for (var i = 0; i < elm.childNodes.length; i++) {
|
|
collect(elm.childNodes[i]);
|
|
}
|
|
}
|
|
|
|
collect(elm || root.getEl());
|
|
|
|
return elements;
|
|
}
|
|
|
|
/**
|
|
* Returns the navigation root control for the specified control. The navigation root
|
|
* is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group.
|
|
* It will look for parents of the specified target control or the currently focused control if this option is omitted.
|
|
*
|
|
* @private
|
|
* @param {tinymce.ui.Control} targetControl Optional target control to find root of.
|
|
* @return {tinymce.ui.Control} Navigation root control.
|
|
*/
|
|
function getNavigationRoot(targetControl) {
|
|
var navigationRoot, controls;
|
|
|
|
targetControl = targetControl || focusedControl;
|
|
controls = targetControl.parents().toArray();
|
|
controls.unshift(targetControl);
|
|
|
|
for (var i = 0; i < controls.length; i++) {
|
|
navigationRoot = controls[i];
|
|
|
|
if (navigationRoot.settings.ariaRoot) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return navigationRoot;
|
|
}
|
|
|
|
/**
|
|
* Focuses the first item in the specified targetControl element or the last aria index if the
|
|
* navigation root has the ariaRemember option enabled.
|
|
*
|
|
* @private
|
|
* @param {tinymce.ui.Control} targetControl Target control to focus the first item in.
|
|
*/
|
|
function focusFirst(targetControl) {
|
|
var navigationRoot = getNavigationRoot(targetControl);
|
|
var focusElements = getFocusElements(navigationRoot.getEl());
|
|
|
|
if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) {
|
|
moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements);
|
|
} else {
|
|
moveFocusToIndex(0, focusElements);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the specified index within the elements list.
|
|
* This will scope the index to the size of the element list if it changed.
|
|
*
|
|
* @private
|
|
* @param {Number} idx Specified index to move to.
|
|
* @param {Array} elements Array with dom elements to move focus within.
|
|
* @return {Number} Input index or a changed index if it was out of range.
|
|
*/
|
|
function moveFocusToIndex(idx, elements) {
|
|
if (idx < 0) {
|
|
idx = elements.length - 1;
|
|
} else if (idx >= elements.length) {
|
|
idx = 0;
|
|
}
|
|
|
|
if (elements[idx]) {
|
|
elements[idx].focus();
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
/**
|
|
* Moves the focus forwards or backwards.
|
|
*
|
|
* @private
|
|
* @param {Number} dir Direction to move in positive means forward, negative means backwards.
|
|
* @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements.
|
|
*/
|
|
function moveFocus(dir, elements) {
|
|
var idx = -1, navigationRoot = getNavigationRoot();
|
|
|
|
elements = elements || getFocusElements(navigationRoot.getEl());
|
|
|
|
for (var i = 0; i < elements.length; i++) {
|
|
if (elements[i] === focusedElement) {
|
|
idx = i;
|
|
}
|
|
}
|
|
|
|
idx += dir;
|
|
navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements);
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the left this is called by the left key.
|
|
*
|
|
* @private
|
|
*/
|
|
function left() {
|
|
var parentRole = getParentRole();
|
|
|
|
if (parentRole == "tablist") {
|
|
moveFocus(-1, getFocusElements(focusedElement.parentNode));
|
|
} else if (focusedControl.parent().submenu) {
|
|
cancel();
|
|
} else {
|
|
moveFocus(-1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the right this is called by the right key.
|
|
*
|
|
* @private
|
|
*/
|
|
function right() {
|
|
var role = getRole(), parentRole = getParentRole();
|
|
|
|
if (parentRole == "tablist") {
|
|
moveFocus(1, getFocusElements(focusedElement.parentNode));
|
|
} else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) {
|
|
enter();
|
|
} else {
|
|
moveFocus(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the up this is called by the up key.
|
|
*
|
|
* @private
|
|
*/
|
|
function up() {
|
|
moveFocus(-1);
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the up this is called by the down key.
|
|
*
|
|
* @private
|
|
*/
|
|
function down() {
|
|
var role = getRole(), parentRole = getParentRole();
|
|
|
|
if (role == "menuitem" && parentRole == "menubar") {
|
|
enter();
|
|
} else if (role == "button" && getAriaProp('haspopup')) {
|
|
enter({ key: 'down' });
|
|
} else {
|
|
moveFocus(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the focus to the next item or previous item depending on shift key.
|
|
*
|
|
* @private
|
|
* @param {DOMEvent} e DOM event object.
|
|
*/
|
|
function tab(e) {
|
|
var parentRole = getParentRole();
|
|
|
|
if (parentRole == "tablist") {
|
|
var elm = getFocusElements(focusedControl.getEl('body'))[0];
|
|
|
|
if (elm) {
|
|
elm.focus();
|
|
}
|
|
} else {
|
|
moveFocus(e.shiftKey ? -1 : 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the cancel event on the currently focused control. This is normally done using the Esc key.
|
|
*
|
|
* @private
|
|
*/
|
|
function cancel() {
|
|
focusedControl.fire('cancel');
|
|
}
|
|
|
|
/**
|
|
* Calls the click event on the currently focused control. This is normally done using the Enter/Space keys.
|
|
*
|
|
* @private
|
|
* @param {Object} aria Optional aria data to pass along with the enter event.
|
|
*/
|
|
function enter(aria) {
|
|
aria = aria || {};
|
|
focusedControl.fire('click', { target: focusedElement, aria: aria });
|
|
}
|
|
|
|
root.on('keydown', function (e) {
|
|
function handleNonTabOrEscEvent(e, handler) {
|
|
// Ignore non tab keys for text elements
|
|
if (isTextInputElement(focusedElement) || hasTabstopData(focusedElement)) {
|
|
return;
|
|
}
|
|
|
|
if (getRole(focusedElement) === 'slider') {
|
|
return;
|
|
}
|
|
|
|
if (handler(e) !== false) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
if (e.isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
switch (e.keyCode) {
|
|
case 37: // DOM_VK_LEFT
|
|
handleNonTabOrEscEvent(e, left);
|
|
break;
|
|
|
|
case 39: // DOM_VK_RIGHT
|
|
handleNonTabOrEscEvent(e, right);
|
|
break;
|
|
|
|
case 38: // DOM_VK_UP
|
|
handleNonTabOrEscEvent(e, up);
|
|
break;
|
|
|
|
case 40: // DOM_VK_DOWN
|
|
handleNonTabOrEscEvent(e, down);
|
|
break;
|
|
|
|
case 27: // DOM_VK_ESCAPE
|
|
cancel();
|
|
break;
|
|
|
|
case 14: // DOM_VK_ENTER
|
|
case 13: // DOM_VK_RETURN
|
|
case 32: // DOM_VK_SPACE
|
|
handleNonTabOrEscEvent(e, enter);
|
|
break;
|
|
|
|
case 9: // DOM_VK_TAB
|
|
if (tab(e) !== false) {
|
|
e.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
root.on('focusin', function (e) {
|
|
focusedElement = e.target;
|
|
focusedControl = e.control;
|
|
});
|
|
|
|
return {
|
|
focusFirst: focusFirst
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Container.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
|
|
*/
|
|
|
|
/**
|
|
* Container control. This is extended by all controls that can have
|
|
* children such as panels etc. You can also use this class directly as an
|
|
* generic container instance. The container doesn't have any specific role or style.
|
|
*
|
|
* @-x-less Container.less
|
|
* @class tinymce.ui.Container
|
|
* @extends tinymce.ui.Control
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Container',
|
|
[
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.ui.Collection",
|
|
"tinymce.core.ui.Selector",
|
|
"tinymce.core.ui.Factory",
|
|
"tinymce.core.ui.KeyboardNavigation",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.ui.ClassList",
|
|
"tinymce.core.ui.ReflowQueue"
|
|
],
|
|
function (Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) {
|
|
"use strict";
|
|
|
|
var selectorCache = {};
|
|
|
|
return Control.extend({
|
|
/**
|
|
* Constructs a new control instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Array} items Items to add to container in JSON format or control instances.
|
|
* @setting {String} layout Layout manager by name to use.
|
|
* @setting {Object} defaults Default settings to apply to all items.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
|
|
if (settings.fixed) {
|
|
self.state.set('fixed', true);
|
|
}
|
|
|
|
self._items = new Collection();
|
|
|
|
if (self.isRtl()) {
|
|
self.classes.add('rtl');
|
|
}
|
|
|
|
self.bodyClasses = new ClassList(function () {
|
|
if (self.state.get('rendered')) {
|
|
self.getEl('body').className = this.toString();
|
|
}
|
|
});
|
|
self.bodyClasses.prefix = self.classPrefix;
|
|
|
|
self.classes.add('container');
|
|
self.bodyClasses.add('container-body');
|
|
|
|
if (settings.containerCls) {
|
|
self.classes.add(settings.containerCls);
|
|
}
|
|
|
|
self._layout = Factory.create((settings.layout || '') + 'layout');
|
|
|
|
if (self.settings.items) {
|
|
self.add(self.settings.items);
|
|
} else {
|
|
self.add(self.render());
|
|
}
|
|
|
|
// TODO: Fix this!
|
|
self._hasBody = true;
|
|
},
|
|
|
|
/**
|
|
* Returns a collection of child items that the container currently have.
|
|
*
|
|
* @method items
|
|
* @return {tinymce.ui.Collection} Control collection direct child controls.
|
|
*/
|
|
items: function () {
|
|
return this._items;
|
|
},
|
|
|
|
/**
|
|
* Find child controls by selector.
|
|
*
|
|
* @method find
|
|
* @param {String} selector Selector CSS pattern to find children by.
|
|
* @return {tinymce.ui.Collection} Control collection with child controls.
|
|
*/
|
|
find: function (selector) {
|
|
selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector);
|
|
|
|
return selector.find(this);
|
|
},
|
|
|
|
/**
|
|
* Adds one or many items to the current container. This will create instances of
|
|
* the object representations if needed.
|
|
*
|
|
* @method add
|
|
* @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container.
|
|
* @return {tinymce.ui.Collection} Current collection control.
|
|
*/
|
|
add: function (items) {
|
|
var self = this;
|
|
|
|
self.items().add(self.create(items)).parent(self);
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Focuses the current container instance. This will look
|
|
* for the first control in the container and focus that.
|
|
*
|
|
* @method focus
|
|
* @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not.
|
|
* @return {tinymce.ui.Collection} Current instance.
|
|
*/
|
|
focus: function (keyboard) {
|
|
var self = this, focusCtrl, keyboardNav, items;
|
|
|
|
if (keyboard) {
|
|
keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav;
|
|
|
|
if (keyboardNav) {
|
|
keyboardNav.focusFirst(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
items = self.find('*');
|
|
|
|
// TODO: Figure out a better way to auto focus alert dialog buttons
|
|
if (self.statusbar) {
|
|
items.add(self.statusbar.items());
|
|
}
|
|
|
|
items.each(function (ctrl) {
|
|
if (ctrl.settings.autofocus) {
|
|
focusCtrl = null;
|
|
return false;
|
|
}
|
|
|
|
if (ctrl.canFocus) {
|
|
focusCtrl = focusCtrl || ctrl;
|
|
}
|
|
});
|
|
|
|
if (focusCtrl) {
|
|
focusCtrl.focus();
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Replaces the specified child control with a new control.
|
|
*
|
|
* @method replace
|
|
* @param {tinymce.ui.Control} oldItem Old item to be replaced.
|
|
* @param {tinymce.ui.Control} newItem New item to be inserted.
|
|
*/
|
|
replace: function (oldItem, newItem) {
|
|
var ctrlElm, items = this.items(), i = items.length;
|
|
|
|
// Replace the item in collection
|
|
while (i--) {
|
|
if (items[i] === oldItem) {
|
|
items[i] = newItem;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i >= 0) {
|
|
// Remove new item from DOM
|
|
ctrlElm = newItem.getEl();
|
|
if (ctrlElm) {
|
|
ctrlElm.parentNode.removeChild(ctrlElm);
|
|
}
|
|
|
|
// Remove old item from DOM
|
|
ctrlElm = oldItem.getEl();
|
|
if (ctrlElm) {
|
|
ctrlElm.parentNode.removeChild(ctrlElm);
|
|
}
|
|
}
|
|
|
|
// Adopt the item
|
|
newItem.parent(this);
|
|
},
|
|
|
|
/**
|
|
* Creates the specified items. If any of the items is plain JSON style objects
|
|
* it will convert these into real tinymce.ui.Control instances.
|
|
*
|
|
* @method create
|
|
* @param {Array} items Array of items to convert into control instances.
|
|
* @return {Array} Array with control instances.
|
|
*/
|
|
create: function (items) {
|
|
var self = this, settings, ctrlItems = [];
|
|
|
|
// Non array structure, then force it into an array
|
|
if (!Tools.isArray(items)) {
|
|
items = [items];
|
|
}
|
|
|
|
// Add default type to each child control
|
|
Tools.each(items, function (item) {
|
|
if (item) {
|
|
// Construct item if needed
|
|
if (!(item instanceof Control)) {
|
|
// Name only then convert it to an object
|
|
if (typeof item == "string") {
|
|
item = { type: item };
|
|
}
|
|
|
|
// Create control instance based on input settings and default settings
|
|
settings = Tools.extend({}, self.settings.defaults, item);
|
|
item.type = settings.type = settings.type || item.type || self.settings.defaultType ||
|
|
(settings.defaults ? settings.defaults.type : null);
|
|
item = Factory.create(settings);
|
|
}
|
|
|
|
ctrlItems.push(item);
|
|
}
|
|
});
|
|
|
|
return ctrlItems;
|
|
},
|
|
|
|
/**
|
|
* Renders new control instances.
|
|
*
|
|
* @private
|
|
*/
|
|
renderNew: function () {
|
|
var self = this;
|
|
|
|
// Render any new items
|
|
self.items().each(function (ctrl, index) {
|
|
var containerElm;
|
|
|
|
ctrl.parent(self);
|
|
|
|
if (!ctrl.state.get('rendered')) {
|
|
containerElm = self.getEl('body');
|
|
|
|
// Insert or append the item
|
|
if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
|
|
$(containerElm.childNodes[index]).before(ctrl.renderHtml());
|
|
} else {
|
|
$(containerElm).append(ctrl.renderHtml());
|
|
}
|
|
|
|
ctrl.postRender();
|
|
ReflowQueue.add(ctrl);
|
|
}
|
|
});
|
|
|
|
self._layout.applyClasses(self.items().filter(':visible'));
|
|
self._lastRect = null;
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Appends new instances to the current container.
|
|
*
|
|
* @method append
|
|
* @param {Array/tinymce.ui.Collection} items Array if controls to append.
|
|
* @return {tinymce.ui.Container} Current container instance.
|
|
*/
|
|
append: function (items) {
|
|
return this.add(items).renderNew();
|
|
},
|
|
|
|
/**
|
|
* Prepends new instances to the current container.
|
|
*
|
|
* @method prepend
|
|
* @param {Array/tinymce.ui.Collection} items Array if controls to prepend.
|
|
* @return {tinymce.ui.Container} Current container instance.
|
|
*/
|
|
prepend: function (items) {
|
|
var self = this;
|
|
|
|
self.items().set(self.create(items).concat(self.items().toArray()));
|
|
|
|
return self.renderNew();
|
|
},
|
|
|
|
/**
|
|
* Inserts an control at a specific index.
|
|
*
|
|
* @method insert
|
|
* @param {Array/tinymce.ui.Collection} items Array if controls to insert.
|
|
* @param {Number} index Index to insert controls at.
|
|
* @param {Boolean} [before=false] Inserts controls before the index.
|
|
*/
|
|
insert: function (items, index, before) {
|
|
var self = this, curItems, beforeItems, afterItems;
|
|
|
|
items = self.create(items);
|
|
curItems = self.items();
|
|
|
|
if (!before && index < curItems.length - 1) {
|
|
index += 1;
|
|
}
|
|
|
|
if (index >= 0 && index < curItems.length) {
|
|
beforeItems = curItems.slice(0, index).toArray();
|
|
afterItems = curItems.slice(index).toArray();
|
|
curItems.set(beforeItems.concat(items, afterItems));
|
|
}
|
|
|
|
return self.renderNew();
|
|
},
|
|
|
|
/**
|
|
* Populates the form fields from the specified JSON data object.
|
|
*
|
|
* Control items in the form that matches the data will have it's value set.
|
|
*
|
|
* @method fromJSON
|
|
* @param {Object} data JSON data object to set control values by.
|
|
* @return {tinymce.ui.Container} Current form instance.
|
|
*/
|
|
fromJSON: function (data) {
|
|
var self = this;
|
|
|
|
for (var name in data) {
|
|
self.find('#' + name).value(data[name]);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Serializes the form into a JSON object by getting all items
|
|
* that has a name and a value.
|
|
*
|
|
* @method toJSON
|
|
* @return {Object} JSON object with form data.
|
|
*/
|
|
toJSON: function () {
|
|
var self = this, data = {};
|
|
|
|
self.find('*').each(function (ctrl) {
|
|
var name = ctrl.name(), value = ctrl.value();
|
|
|
|
if (name && typeof value != "undefined") {
|
|
data[name] = value;
|
|
}
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, role = this.settings.role;
|
|
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' +
|
|
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
|
|
(self.settings.html || '') + layout.renderHtml(self) +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Post render method. Called after the control has been rendered to the target.
|
|
*
|
|
* @method postRender
|
|
* @return {tinymce.ui.Container} Current combobox instance.
|
|
*/
|
|
postRender: function () {
|
|
var self = this, box;
|
|
|
|
self.items().exec('postRender');
|
|
self._super();
|
|
|
|
self._layout.postRender(self);
|
|
self.state.set('rendered', true);
|
|
|
|
if (self.settings.style) {
|
|
self.$el.css(self.settings.style);
|
|
}
|
|
|
|
if (self.settings.border) {
|
|
box = self.borderBox;
|
|
self.$el.css({
|
|
'border-top-width': box.top,
|
|
'border-right-width': box.right,
|
|
'border-bottom-width': box.bottom,
|
|
'border-left-width': box.left
|
|
});
|
|
}
|
|
|
|
if (!self.parent()) {
|
|
self.keyboardNav = new KeyboardNavigation({
|
|
root: self
|
|
});
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Initializes the current controls layout rect.
|
|
* This will be executed by the layout managers to determine the
|
|
* default minWidth/minHeight etc.
|
|
*
|
|
* @method initLayoutRect
|
|
* @return {Object} Layout rect instance.
|
|
*/
|
|
initLayoutRect: function () {
|
|
var self = this, layoutRect = self._super();
|
|
|
|
// Recalc container size by asking layout manager
|
|
self._layout.recalc(self);
|
|
|
|
return layoutRect;
|
|
},
|
|
|
|
/**
|
|
* Recalculates the positions of the controls in the current container.
|
|
* This is invoked by the reflow method and shouldn't be called directly.
|
|
*
|
|
* @method recalc
|
|
*/
|
|
recalc: function () {
|
|
var self = this, rect = self._layoutRect, lastRect = self._lastRect;
|
|
|
|
if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) {
|
|
self._layout.recalc(self);
|
|
rect = self.layoutRect();
|
|
self._lastRect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
return true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reflows the current container and it's children and possible parents.
|
|
* This should be used after you for example append children to the current control so
|
|
* that the layout managers know that they need to reposition everything.
|
|
*
|
|
* @example
|
|
* container.append({type: 'button', text: 'My button'}).reflow();
|
|
*
|
|
* @method reflow
|
|
* @return {tinymce.ui.Container} Current container instance.
|
|
*/
|
|
reflow: function () {
|
|
var i;
|
|
|
|
ReflowQueue.remove(this);
|
|
|
|
if (this.visible()) {
|
|
Control.repaintControls = [];
|
|
Control.repaintControls.map = {};
|
|
|
|
this.recalc();
|
|
i = Control.repaintControls.length;
|
|
|
|
while (i--) {
|
|
Control.repaintControls[i].repaint();
|
|
}
|
|
|
|
// TODO: Fix me!
|
|
if (this.settings.layout !== "flow" && this.settings.layout !== "stack") {
|
|
this.repaint();
|
|
}
|
|
|
|
Control.repaintControls = [];
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* DragHelper.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
|
|
*/
|
|
|
|
/**
|
|
* Drag/drop helper class.
|
|
*
|
|
* @example
|
|
* var dragHelper = new tinymce.ui.DragHelper('mydiv', {
|
|
* start: function(evt) {
|
|
* },
|
|
*
|
|
* drag: function(evt) {
|
|
* },
|
|
*
|
|
* end: function(evt) {
|
|
* }
|
|
* });
|
|
*
|
|
* @class tinymce.ui.DragHelper
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.DragHelper',
|
|
[
|
|
"tinymce.core.dom.DomQuery"
|
|
],
|
|
function ($) {
|
|
"use strict";
|
|
|
|
function getDocumentSize(doc) {
|
|
var documentElement, body, scrollWidth, clientWidth;
|
|
var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max;
|
|
|
|
documentElement = doc.documentElement;
|
|
body = doc.body;
|
|
|
|
scrollWidth = max(documentElement.scrollWidth, body.scrollWidth);
|
|
clientWidth = max(documentElement.clientWidth, body.clientWidth);
|
|
offsetWidth = max(documentElement.offsetWidth, body.offsetWidth);
|
|
|
|
scrollHeight = max(documentElement.scrollHeight, body.scrollHeight);
|
|
clientHeight = max(documentElement.clientHeight, body.clientHeight);
|
|
offsetHeight = max(documentElement.offsetHeight, body.offsetHeight);
|
|
|
|
return {
|
|
width: scrollWidth < offsetWidth ? clientWidth : scrollWidth,
|
|
height: scrollHeight < offsetHeight ? clientHeight : scrollHeight
|
|
};
|
|
}
|
|
|
|
function updateWithTouchData(e) {
|
|
var keys, i;
|
|
|
|
if (e.changedTouches) {
|
|
keys = "screenX screenY pageX pageY clientX clientY".split(' ');
|
|
for (i = 0; i < keys.length; i++) {
|
|
e[keys[i]] = e.changedTouches[0][keys[i]];
|
|
}
|
|
}
|
|
}
|
|
|
|
return function (id, settings) {
|
|
var $eventOverlay, doc = settings.document || document, downButton, start, stop, drag, startX, startY;
|
|
|
|
settings = settings || {};
|
|
|
|
function getHandleElm() {
|
|
return doc.getElementById(settings.handle || id);
|
|
}
|
|
|
|
start = function (e) {
|
|
var docSize = getDocumentSize(doc), handleElm, cursor;
|
|
|
|
updateWithTouchData(e);
|
|
|
|
e.preventDefault();
|
|
downButton = e.button;
|
|
handleElm = getHandleElm();
|
|
startX = e.screenX;
|
|
startY = e.screenY;
|
|
|
|
// Grab cursor from handle so we can place it on overlay
|
|
if (window.getComputedStyle) {
|
|
cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor");
|
|
} else {
|
|
cursor = handleElm.runtimeStyle.cursor;
|
|
}
|
|
|
|
$eventOverlay = $('<div></div>').css({
|
|
position: "absolute",
|
|
top: 0, left: 0,
|
|
width: docSize.width,
|
|
height: docSize.height,
|
|
zIndex: 0x7FFFFFFF,
|
|
opacity: 0.0001,
|
|
cursor: cursor
|
|
}).appendTo(doc.body);
|
|
|
|
$(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop);
|
|
|
|
settings.start(e);
|
|
};
|
|
|
|
drag = function (e) {
|
|
updateWithTouchData(e);
|
|
|
|
if (e.button !== downButton) {
|
|
return stop(e);
|
|
}
|
|
|
|
e.deltaX = e.screenX - startX;
|
|
e.deltaY = e.screenY - startY;
|
|
|
|
e.preventDefault();
|
|
settings.drag(e);
|
|
};
|
|
|
|
stop = function (e) {
|
|
updateWithTouchData(e);
|
|
|
|
$(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop);
|
|
|
|
$eventOverlay.remove();
|
|
|
|
if (settings.stop) {
|
|
settings.stop(e);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Destroys the drag/drop helper instance.
|
|
*
|
|
* @method destroy
|
|
*/
|
|
this.destroy = function () {
|
|
$(getHandleElm()).off();
|
|
};
|
|
|
|
$(getHandleElm()).on('mousedown touchstart', start);
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Scrollable.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 makes controls scrollable using custom scrollbars.
|
|
*
|
|
* @-x-less Scrollable.less
|
|
* @mixin tinymce.ui.Scrollable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Scrollable',
|
|
[
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.ui.DragHelper"
|
|
],
|
|
function ($, DragHelper) {
|
|
"use strict";
|
|
|
|
return {
|
|
init: function () {
|
|
var self = this;
|
|
self.on('repaint', self.renderScroll);
|
|
},
|
|
|
|
renderScroll: function () {
|
|
var self = this, margin = 2;
|
|
|
|
function repaintScroll() {
|
|
var hasScrollH, hasScrollV, bodyElm;
|
|
|
|
function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) {
|
|
var containerElm, scrollBarElm, scrollThumbElm;
|
|
var containerSize, scrollSize, ratio, rect;
|
|
var posNameLower, sizeNameLower;
|
|
|
|
scrollBarElm = self.getEl('scroll' + axisName);
|
|
if (scrollBarElm) {
|
|
posNameLower = posName.toLowerCase();
|
|
sizeNameLower = sizeName.toLowerCase();
|
|
|
|
$(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1);
|
|
|
|
if (!hasScroll) {
|
|
$(scrollBarElm).css('display', 'none');
|
|
return;
|
|
}
|
|
|
|
$(scrollBarElm).css('display', 'block');
|
|
containerElm = self.getEl('body');
|
|
scrollThumbElm = self.getEl('scroll' + axisName + "t");
|
|
containerSize = containerElm["client" + sizeName] - (margin * 2);
|
|
containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0;
|
|
scrollSize = containerElm["scroll" + sizeName];
|
|
ratio = containerSize / scrollSize;
|
|
|
|
rect = {};
|
|
rect[posNameLower] = containerElm["offset" + posName] + margin;
|
|
rect[sizeNameLower] = containerSize;
|
|
$(scrollBarElm).css(rect);
|
|
|
|
rect = {};
|
|
rect[posNameLower] = containerElm["scroll" + posName] * ratio;
|
|
rect[sizeNameLower] = containerSize * ratio;
|
|
$(scrollThumbElm).css(rect);
|
|
}
|
|
}
|
|
|
|
bodyElm = self.getEl('body');
|
|
hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth;
|
|
hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight;
|
|
|
|
repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height");
|
|
repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width");
|
|
}
|
|
|
|
function addScroll() {
|
|
function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) {
|
|
var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix;
|
|
|
|
$(self.getEl()).append(
|
|
'<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' +
|
|
'<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' +
|
|
'</div>'
|
|
);
|
|
|
|
self.draghelper = new DragHelper(axisId + 't', {
|
|
start: function () {
|
|
scrollStart = self.getEl('body')["scroll" + posName];
|
|
$('#' + axisId).addClass(prefix + 'active');
|
|
},
|
|
|
|
drag: function (e) {
|
|
var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect();
|
|
|
|
hasScrollH = layoutRect.contentW > layoutRect.innerW;
|
|
hasScrollV = layoutRect.contentH > layoutRect.innerH;
|
|
containerSize = self.getEl('body')["client" + sizeName] - (margin * 2);
|
|
containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0;
|
|
|
|
ratio = containerSize / self.getEl('body')["scroll" + sizeName];
|
|
self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio);
|
|
},
|
|
|
|
stop: function () {
|
|
$('#' + axisId).removeClass(prefix + 'active');
|
|
}
|
|
});
|
|
}
|
|
|
|
self.classes.add('scroll');
|
|
|
|
addScrollAxis("v", "Top", "Height", "Y", "Width");
|
|
addScrollAxis("h", "Left", "Width", "X", "Height");
|
|
}
|
|
|
|
if (self.settings.autoScroll) {
|
|
if (!self._hasScroll) {
|
|
self._hasScroll = true;
|
|
addScroll();
|
|
|
|
self.on('wheel', function (e) {
|
|
var bodyEl = self.getEl('body');
|
|
|
|
bodyEl.scrollLeft += (e.deltaX || 0) * 10;
|
|
bodyEl.scrollTop += e.deltaY * 10;
|
|
|
|
repaintScroll();
|
|
});
|
|
|
|
$(self.getEl('body')).on("scroll", repaintScroll);
|
|
}
|
|
|
|
repaintScroll();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Panel.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new panel.
|
|
*
|
|
* @-x-less Panel.less
|
|
* @class tinymce.ui.Panel
|
|
* @extends tinymce.ui.Container
|
|
* @mixes tinymce.ui.Scrollable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Panel',
|
|
[
|
|
"tinymce.core.ui.Container",
|
|
"tinymce.core.ui.Scrollable"
|
|
],
|
|
function (Container, Scrollable) {
|
|
"use strict";
|
|
|
|
return Container.extend({
|
|
Defaults: {
|
|
layout: 'fit',
|
|
containerCls: 'panel'
|
|
},
|
|
|
|
Mixins: [Scrollable],
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, innerHtml = self.settings.html;
|
|
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
if (typeof innerHtml == "undefined") {
|
|
innerHtml = (
|
|
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
|
|
layout.renderHtml(self) +
|
|
'</div>'
|
|
);
|
|
} else {
|
|
if (typeof innerHtml == 'function') {
|
|
innerHtml = innerHtml.call(self);
|
|
}
|
|
|
|
self._hasBody = false;
|
|
}
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1" role="group">' +
|
|
(self._preBodyHtml || '') +
|
|
innerHtml +
|
|
'</div>'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Movable.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
|
|
*/
|
|
|
|
/**
|
|
* Movable mixin. Makes controls movable absolute and relative to other elements.
|
|
*
|
|
* @mixin tinymce.ui.Movable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Movable',
|
|
[
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (DomUtils) {
|
|
"use strict";
|
|
|
|
function calculateRelativePosition(ctrl, targetElm, rel) {
|
|
var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
|
|
|
|
viewport = DomUtils.getViewPort();
|
|
|
|
// Get pos of target
|
|
pos = DomUtils.getPos(targetElm);
|
|
x = pos.x;
|
|
y = pos.y;
|
|
|
|
if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') {
|
|
x -= viewport.x;
|
|
y -= viewport.y;
|
|
}
|
|
|
|
// Get size of self
|
|
ctrlElm = ctrl.getEl();
|
|
size = DomUtils.getSize(ctrlElm);
|
|
selfW = size.width;
|
|
selfH = size.height;
|
|
|
|
// Get size of target
|
|
size = DomUtils.getSize(targetElm);
|
|
targetW = size.width;
|
|
targetH = size.height;
|
|
|
|
// Parse align string
|
|
rel = (rel || '').split('');
|
|
|
|
// Target corners
|
|
if (rel[0] === 'b') {
|
|
y += targetH;
|
|
}
|
|
|
|
if (rel[1] === 'r') {
|
|
x += targetW;
|
|
}
|
|
|
|
if (rel[0] === 'c') {
|
|
y += Math.round(targetH / 2);
|
|
}
|
|
|
|
if (rel[1] === 'c') {
|
|
x += Math.round(targetW / 2);
|
|
}
|
|
|
|
// Self corners
|
|
if (rel[3] === 'b') {
|
|
y -= selfH;
|
|
}
|
|
|
|
if (rel[4] === 'r') {
|
|
x -= selfW;
|
|
}
|
|
|
|
if (rel[3] === 'c') {
|
|
y -= Math.round(selfH / 2);
|
|
}
|
|
|
|
if (rel[4] === 'c') {
|
|
x -= Math.round(selfW / 2);
|
|
}
|
|
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
w: selfW,
|
|
h: selfH
|
|
};
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Tests various positions to get the most suitable one.
|
|
*
|
|
* @method testMoveRel
|
|
* @param {DOMElement} elm Element to position against.
|
|
* @param {Array} rels Array with relative positions.
|
|
* @return {String} Best suitable relative position.
|
|
*/
|
|
testMoveRel: function (elm, rels) {
|
|
var viewPortRect = DomUtils.getViewPort();
|
|
|
|
for (var i = 0; i < rels.length; i++) {
|
|
var pos = calculateRelativePosition(this, elm, rels[i]);
|
|
|
|
if (this.state.get('fixed')) {
|
|
if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) {
|
|
return rels[i];
|
|
}
|
|
} else {
|
|
if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x &&
|
|
pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) {
|
|
return rels[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return rels[0];
|
|
},
|
|
|
|
/**
|
|
* Move relative to the specified element.
|
|
*
|
|
* @method moveRel
|
|
* @param {Element} elm Element to move relative to.
|
|
* @param {String} rel Relative mode. For example: br-tl.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
moveRel: function (elm, rel) {
|
|
if (typeof rel != 'string') {
|
|
rel = this.testMoveRel(elm, rel);
|
|
}
|
|
|
|
var pos = calculateRelativePosition(this, elm, rel);
|
|
return this.moveTo(pos.x, pos.y);
|
|
},
|
|
|
|
/**
|
|
* Move by a relative x, y values.
|
|
*
|
|
* @method moveBy
|
|
* @param {Number} dx Relative x position.
|
|
* @param {Number} dy Relative y position.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
moveBy: function (dx, dy) {
|
|
var self = this, rect = self.layoutRect();
|
|
|
|
self.moveTo(rect.x + dx, rect.y + dy);
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Move to absolute position.
|
|
*
|
|
* @method moveTo
|
|
* @param {Number} x Absolute x position.
|
|
* @param {Number} y Absolute y position.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
moveTo: function (x, y) {
|
|
var self = this;
|
|
|
|
// TODO: Move this to some global class
|
|
function constrain(value, max, size) {
|
|
if (value < 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (value + size > max) {
|
|
value = max - size;
|
|
return value < 0 ? 0 : value;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
if (self.settings.constrainToViewport) {
|
|
var viewPortRect = DomUtils.getViewPort(window);
|
|
var layoutRect = self.layoutRect();
|
|
|
|
x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
|
|
y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
|
|
}
|
|
|
|
if (self.state.get('rendered')) {
|
|
self.layoutRect({ x: x, y: y }).repaint();
|
|
} else {
|
|
self.settings.x = x;
|
|
self.settings.y = y;
|
|
}
|
|
|
|
self.fire('move', { x: x, y: y });
|
|
|
|
return self;
|
|
}
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Resizable.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
|
|
*/
|
|
|
|
/**
|
|
* Resizable mixin. Enables controls to be resized.
|
|
*
|
|
* @mixin tinymce.ui.Resizable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Resizable',
|
|
[
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (DomUtils) {
|
|
"use strict";
|
|
|
|
return {
|
|
/**
|
|
* Resizes the control to contents.
|
|
*
|
|
* @method resizeToContent
|
|
*/
|
|
resizeToContent: function () {
|
|
this._layoutRect.autoResize = true;
|
|
this._lastRect = null;
|
|
this.reflow();
|
|
},
|
|
|
|
/**
|
|
* Resizes the control to a specific width/height.
|
|
*
|
|
* @method resizeTo
|
|
* @param {Number} w Control width.
|
|
* @param {Number} h Control height.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
resizeTo: function (w, h) {
|
|
// TODO: Fix hack
|
|
if (w <= 1 || h <= 1) {
|
|
var rect = DomUtils.getWindowSize();
|
|
|
|
w = w <= 1 ? w * rect.w : w;
|
|
h = h <= 1 ? h * rect.h : h;
|
|
}
|
|
|
|
this._layoutRect.autoResize = false;
|
|
return this.layoutRect({ minW: w, minH: h, w: w, h: h }).reflow();
|
|
},
|
|
|
|
/**
|
|
* Resizes the control to a specific relative width/height.
|
|
*
|
|
* @method resizeBy
|
|
* @param {Number} dw Relative control width.
|
|
* @param {Number} dh Relative control height.
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
resizeBy: function (dw, dh) {
|
|
var self = this, rect = self.layoutRect();
|
|
|
|
return self.resizeTo(rect.w + dw, rect.h + dh);
|
|
}
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* FloatPanel.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 creates a floating panel.
|
|
*
|
|
* @-x-less FloatPanel.less
|
|
* @class tinymce.ui.FloatPanel
|
|
* @extends tinymce.ui.Panel
|
|
* @mixes tinymce.ui.Movable
|
|
* @mixes tinymce.ui.Resizable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FloatPanel',
|
|
[
|
|
"tinymce.core.ui.Panel",
|
|
"tinymce.core.ui.Movable",
|
|
"tinymce.core.ui.Resizable",
|
|
"tinymce.core.ui.DomUtils",
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function (Panel, Movable, Resizable, DomUtils, $, Delay) {
|
|
"use strict";
|
|
|
|
var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = [];
|
|
var zOrder = [], hasModal;
|
|
|
|
function isChildOf(ctrl, parent) {
|
|
while (ctrl) {
|
|
if (ctrl == parent) {
|
|
return true;
|
|
}
|
|
|
|
ctrl = ctrl.parent();
|
|
}
|
|
}
|
|
|
|
function skipOrHidePanels(e) {
|
|
// Hide any float panel when a click/focus out is out side that float panel and the
|
|
// float panels direct parent for example a click on a menu button
|
|
var i = visiblePanels.length;
|
|
|
|
while (i--) {
|
|
var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target);
|
|
|
|
if (panel.settings.autohide) {
|
|
if (clickCtrl) {
|
|
if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
e = panel.fire('autohide', { target: e.target });
|
|
if (!e.isDefaultPrevented()) {
|
|
panel.hide();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function bindDocumentClickHandler() {
|
|
|
|
if (!documentClickHandler) {
|
|
documentClickHandler = function (e) {
|
|
// Gecko fires click event and in the wrong order on Mac so lets normalize
|
|
if (e.button == 2) {
|
|
return;
|
|
}
|
|
|
|
skipOrHidePanels(e);
|
|
};
|
|
|
|
$(document).on('click touchstart', documentClickHandler);
|
|
}
|
|
}
|
|
|
|
function bindDocumentScrollHandler() {
|
|
if (!documentScrollHandler) {
|
|
documentScrollHandler = function () {
|
|
var i;
|
|
|
|
i = visiblePanels.length;
|
|
while (i--) {
|
|
repositionPanel(visiblePanels[i]);
|
|
}
|
|
};
|
|
|
|
$(window).on('scroll', documentScrollHandler);
|
|
}
|
|
}
|
|
|
|
function bindWindowResizeHandler() {
|
|
if (!windowResizeHandler) {
|
|
var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight;
|
|
|
|
windowResizeHandler = function () {
|
|
// Workaround for #7065 IE 7 fires resize events event though the window wasn't resized
|
|
if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) {
|
|
clientWidth = docElm.clientWidth;
|
|
clientHeight = docElm.clientHeight;
|
|
FloatPanel.hideAll();
|
|
}
|
|
};
|
|
|
|
$(window).on('resize', windowResizeHandler);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
|
|
* also reposition all child panels of the current panel.
|
|
*/
|
|
function repositionPanel(panel) {
|
|
var scrollY = DomUtils.getViewPort().y;
|
|
|
|
function toggleFixedChildPanels(fixed, deltaY) {
|
|
var parent;
|
|
|
|
for (var i = 0; i < visiblePanels.length; i++) {
|
|
if (visiblePanels[i] != panel) {
|
|
parent = visiblePanels[i].parent();
|
|
|
|
while (parent && (parent = parent.parent())) {
|
|
if (parent == panel) {
|
|
visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (panel.settings.autofix) {
|
|
if (!panel.state.get('fixed')) {
|
|
panel._autoFixY = panel.layoutRect().y;
|
|
|
|
if (panel._autoFixY < scrollY) {
|
|
panel.fixed(true).layoutRect({ y: 0 }).repaint();
|
|
toggleFixedChildPanels(true, scrollY - panel._autoFixY);
|
|
}
|
|
} else {
|
|
if (panel._autoFixY > scrollY) {
|
|
panel.fixed(false).layoutRect({ y: panel._autoFixY }).repaint();
|
|
toggleFixedChildPanels(false, panel._autoFixY - scrollY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function addRemove(add, ctrl) {
|
|
var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
|
|
|
|
if (add) {
|
|
zOrder.push(ctrl);
|
|
} else {
|
|
i = zOrder.length;
|
|
|
|
while (i--) {
|
|
if (zOrder[i] === ctrl) {
|
|
zOrder.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (zOrder.length) {
|
|
for (i = 0; i < zOrder.length; i++) {
|
|
if (zOrder[i].modal) {
|
|
zIndex++;
|
|
topModal = zOrder[i];
|
|
}
|
|
|
|
zOrder[i].getEl().style.zIndex = zIndex;
|
|
zOrder[i].zIndex = zIndex;
|
|
zIndex++;
|
|
}
|
|
}
|
|
|
|
var modalBlockEl = $('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0];
|
|
|
|
if (topModal) {
|
|
$(modalBlockEl).css('z-index', topModal.zIndex - 1);
|
|
} else if (modalBlockEl) {
|
|
modalBlockEl.parentNode.removeChild(modalBlockEl);
|
|
hasModal = false;
|
|
}
|
|
|
|
FloatPanel.currentZIndex = zIndex;
|
|
}
|
|
|
|
var FloatPanel = Panel.extend({
|
|
Mixins: [Movable, Resizable],
|
|
|
|
/**
|
|
* Constructs a new control instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} autohide Automatically hide the panel.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
self._eventsRoot = self;
|
|
|
|
self.classes.add('floatpanel');
|
|
|
|
// Hide floatpanes on click out side the root button
|
|
if (settings.autohide) {
|
|
bindDocumentClickHandler();
|
|
bindWindowResizeHandler();
|
|
visiblePanels.push(self);
|
|
}
|
|
|
|
if (settings.autofix) {
|
|
bindDocumentScrollHandler();
|
|
|
|
self.on('move', function () {
|
|
repositionPanel(this);
|
|
});
|
|
}
|
|
|
|
self.on('postrender show', function (e) {
|
|
if (e.control == self) {
|
|
var $modalBlockEl, prefix = self.classPrefix;
|
|
|
|
if (self.modal && !hasModal) {
|
|
$modalBlockEl = $('#' + prefix + 'modal-block', self.getContainerElm());
|
|
if (!$modalBlockEl[0]) {
|
|
$modalBlockEl = $(
|
|
'<div id="' + prefix + 'modal-block" class="' + prefix + 'reset ' + prefix + 'fade"></div>'
|
|
).appendTo(self.getContainerElm());
|
|
}
|
|
|
|
Delay.setTimeout(function () {
|
|
$modalBlockEl.addClass(prefix + 'in');
|
|
$(self.getEl()).addClass(prefix + 'in');
|
|
});
|
|
|
|
hasModal = true;
|
|
}
|
|
|
|
addRemove(true, self);
|
|
}
|
|
});
|
|
|
|
self.on('show', function () {
|
|
self.parents().each(function (ctrl) {
|
|
if (ctrl.state.get('fixed')) {
|
|
self.fixed(true);
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (settings.popover) {
|
|
self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>';
|
|
self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start');
|
|
}
|
|
|
|
self.aria('label', settings.ariaLabel);
|
|
self.aria('labelledby', self._id);
|
|
self.aria('describedby', self.describedBy || self._id + '-none');
|
|
},
|
|
|
|
fixed: function (state) {
|
|
var self = this;
|
|
|
|
if (self.state.get('fixed') != state) {
|
|
if (self.state.get('rendered')) {
|
|
var viewport = DomUtils.getViewPort();
|
|
|
|
if (state) {
|
|
self.layoutRect().y -= viewport.y;
|
|
} else {
|
|
self.layoutRect().y += viewport.y;
|
|
}
|
|
}
|
|
|
|
self.classes.toggle('fixed', state);
|
|
self.state.set('fixed', state);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Shows the current float panel.
|
|
*
|
|
* @method show
|
|
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
|
|
*/
|
|
show: function () {
|
|
var self = this, i, state = self._super();
|
|
|
|
i = visiblePanels.length;
|
|
while (i--) {
|
|
if (visiblePanels[i] === self) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i === -1) {
|
|
visiblePanels.push(self);
|
|
}
|
|
|
|
return state;
|
|
},
|
|
|
|
/**
|
|
* Hides the current float panel.
|
|
*
|
|
* @method hide
|
|
* @return {tinymce.ui.FloatPanel} Current floatpanel instance.
|
|
*/
|
|
hide: function () {
|
|
removeVisiblePanel(this);
|
|
addRemove(false, this);
|
|
|
|
return this._super();
|
|
},
|
|
|
|
/**
|
|
* Hide all visible float panels with he autohide setting enabled. This is for
|
|
* manually hiding floating menus or panels.
|
|
*
|
|
* @method hideAll
|
|
*/
|
|
hideAll: function () {
|
|
FloatPanel.hideAll();
|
|
},
|
|
|
|
/**
|
|
* Closes the float panel. This will remove the float panel from page and fire the close event.
|
|
*
|
|
* @method close
|
|
*/
|
|
close: function () {
|
|
var self = this;
|
|
|
|
if (!self.fire('close').isDefaultPrevented()) {
|
|
self.remove();
|
|
addRemove(false, self);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Removes the float panel from page.
|
|
*
|
|
* @method remove
|
|
*/
|
|
remove: function () {
|
|
removeVisiblePanel(this);
|
|
this._super();
|
|
},
|
|
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
if (self.settings.bodyRole) {
|
|
this.getEl('body').setAttribute('role', self.settings.bodyRole);
|
|
}
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Hide all visible float panels with he autohide setting enabled. This is for
|
|
* manually hiding floating menus or panels.
|
|
*
|
|
* @static
|
|
* @method hideAll
|
|
*/
|
|
FloatPanel.hideAll = function () {
|
|
var i = visiblePanels.length;
|
|
|
|
while (i--) {
|
|
var panel = visiblePanels[i];
|
|
|
|
if (panel && panel.settings.autohide) {
|
|
panel.hide();
|
|
visiblePanels.splice(i, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
function removeVisiblePanel(panel) {
|
|
var i;
|
|
|
|
i = visiblePanels.length;
|
|
while (i--) {
|
|
if (visiblePanels[i] === panel) {
|
|
visiblePanels.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
i = zOrder.length;
|
|
while (i--) {
|
|
if (zOrder[i] === panel) {
|
|
zOrder.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return FloatPanel;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Window.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new window.
|
|
*
|
|
* @-x-less Window.less
|
|
* @class tinymce.ui.Window
|
|
* @extends tinymce.ui.FloatPanel
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Window',
|
|
[
|
|
"tinymce.core.ui.FloatPanel",
|
|
"tinymce.core.ui.Panel",
|
|
"tinymce.core.ui.DomUtils",
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.ui.DragHelper",
|
|
"tinymce.core.ui.BoxUtils",
|
|
"tinymce.core.Env",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function (FloatPanel, Panel, DomUtils, $, DragHelper, BoxUtils, Env, Delay) {
|
|
"use strict";
|
|
|
|
var windows = [], oldMetaValue = '';
|
|
|
|
function toggleFullScreenState(state) {
|
|
var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0',
|
|
viewport = $("meta[name=viewport]")[0],
|
|
contentValue;
|
|
|
|
if (Env.overrideViewPort === false) {
|
|
return;
|
|
}
|
|
|
|
if (!viewport) {
|
|
viewport = document.createElement('meta');
|
|
viewport.setAttribute('name', 'viewport');
|
|
document.getElementsByTagName('head')[0].appendChild(viewport);
|
|
}
|
|
|
|
contentValue = viewport.getAttribute('content');
|
|
if (contentValue && typeof oldMetaValue != 'undefined') {
|
|
oldMetaValue = contentValue;
|
|
}
|
|
|
|
viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue);
|
|
}
|
|
|
|
function toggleBodyFullScreenClasses(classPrefix, state) {
|
|
if (checkFullscreenWindows() && state === false) {
|
|
$([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen');
|
|
}
|
|
}
|
|
|
|
function checkFullscreenWindows() {
|
|
for (var i = 0; i < windows.length; i++) {
|
|
if (windows[i]._fullscreen) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function handleWindowResize() {
|
|
if (!Env.desktop) {
|
|
var lastSize = {
|
|
w: window.innerWidth,
|
|
h: window.innerHeight
|
|
};
|
|
|
|
Delay.setInterval(function () {
|
|
var w = window.innerWidth,
|
|
h = window.innerHeight;
|
|
|
|
if (lastSize.w != w || lastSize.h != h) {
|
|
lastSize = {
|
|
w: w,
|
|
h: h
|
|
};
|
|
|
|
$(window).trigger('resize');
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
function reposition() {
|
|
var i, rect = DomUtils.getWindowSize(), layoutRect;
|
|
|
|
for (i = 0; i < windows.length; i++) {
|
|
layoutRect = windows[i].layoutRect();
|
|
|
|
windows[i].moveTo(
|
|
windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2),
|
|
windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2)
|
|
);
|
|
}
|
|
}
|
|
|
|
$(window).on('resize', reposition);
|
|
}
|
|
|
|
var Window = FloatPanel.extend({
|
|
modal: true,
|
|
|
|
Defaults: {
|
|
border: 1,
|
|
layout: 'flex',
|
|
containerCls: 'panel',
|
|
role: 'dialog',
|
|
callbacks: {
|
|
submit: function () {
|
|
this.fire('submit', { data: this.toJSON() });
|
|
},
|
|
|
|
close: function () {
|
|
this.close();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
|
|
if (self.isRtl()) {
|
|
self.classes.add('rtl');
|
|
}
|
|
|
|
self.classes.add('window');
|
|
self.bodyClasses.add('window-body');
|
|
self.state.set('fixed', true);
|
|
|
|
// Create statusbar
|
|
if (settings.buttons) {
|
|
self.statusbar = new Panel({
|
|
layout: 'flex',
|
|
border: '1 0 0 0',
|
|
spacing: 3,
|
|
padding: 10,
|
|
align: 'center',
|
|
pack: self.isRtl() ? 'start' : 'end',
|
|
defaults: {
|
|
type: 'button'
|
|
},
|
|
items: settings.buttons
|
|
});
|
|
|
|
self.statusbar.classes.add('foot');
|
|
self.statusbar.parent(self);
|
|
}
|
|
|
|
self.on('click', function (e) {
|
|
var closeClass = self.classPrefix + 'close';
|
|
|
|
if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) {
|
|
self.close();
|
|
}
|
|
});
|
|
|
|
self.on('cancel', function () {
|
|
self.close();
|
|
});
|
|
|
|
self.aria('describedby', self.describedBy || self._id + '-none');
|
|
self.aria('label', settings.title);
|
|
self._fullscreen = false;
|
|
},
|
|
|
|
/**
|
|
* Recalculates the positions of the controls in the current container.
|
|
* This is invoked by the reflow method and shouldn't be called directly.
|
|
*
|
|
* @method recalc
|
|
*/
|
|
recalc: function () {
|
|
var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc;
|
|
|
|
if (self._fullscreen) {
|
|
self.layoutRect(DomUtils.getWindowSize());
|
|
self.layoutRect().contentH = self.layoutRect().innerH;
|
|
}
|
|
|
|
self._super();
|
|
|
|
layoutRect = self.layoutRect();
|
|
|
|
// Resize window based on title width
|
|
if (self.settings.title && !self._fullscreen) {
|
|
width = layoutRect.headerW;
|
|
if (width > layoutRect.w) {
|
|
x = layoutRect.x - Math.max(0, width / 2);
|
|
self.layoutRect({ w: width, x: x });
|
|
needsRecalc = true;
|
|
}
|
|
}
|
|
|
|
// Resize window based on statusbar width
|
|
if (statusbar) {
|
|
statusbar.layoutRect({ w: self.layoutRect().innerW }).recalc();
|
|
|
|
width = statusbar.layoutRect().minW + layoutRect.deltaW;
|
|
if (width > layoutRect.w) {
|
|
x = layoutRect.x - Math.max(0, width - layoutRect.w);
|
|
self.layoutRect({ w: width, x: x });
|
|
needsRecalc = true;
|
|
}
|
|
}
|
|
|
|
// Recalc body and disable auto resize
|
|
if (needsRecalc) {
|
|
self.recalc();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes the current controls layout rect.
|
|
* This will be executed by the layout managers to determine the
|
|
* default minWidth/minHeight etc.
|
|
*
|
|
* @method initLayoutRect
|
|
* @return {Object} Layout rect instance.
|
|
*/
|
|
initLayoutRect: function () {
|
|
var self = this, layoutRect = self._super(), deltaH = 0, headEl;
|
|
|
|
// Reserve vertical space for title
|
|
if (self.settings.title && !self._fullscreen) {
|
|
headEl = self.getEl('head');
|
|
|
|
var size = DomUtils.getSize(headEl);
|
|
|
|
layoutRect.headerW = size.width;
|
|
layoutRect.headerH = size.height;
|
|
|
|
deltaH += layoutRect.headerH;
|
|
}
|
|
|
|
// Reserve vertical space for statusbar
|
|
if (self.statusbar) {
|
|
deltaH += self.statusbar.layoutRect().h;
|
|
}
|
|
|
|
layoutRect.deltaH += deltaH;
|
|
layoutRect.minH += deltaH;
|
|
//layoutRect.innerH -= deltaH;
|
|
layoutRect.h += deltaH;
|
|
|
|
var rect = DomUtils.getWindowSize();
|
|
|
|
layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2);
|
|
layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2);
|
|
|
|
return layoutRect;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix;
|
|
var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html;
|
|
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
if (settings.title) {
|
|
headerHtml = (
|
|
'<div id="' + id + '-head" class="' + prefix + 'window-head">' +
|
|
'<div id="' + id + '-title" class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' +
|
|
'<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' +
|
|
'<button type="button" class="' + prefix + 'close" aria-hidden="true">' +
|
|
'<i class="mce-ico mce-i-remove"></i>' +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
|
|
if (settings.url) {
|
|
html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>';
|
|
}
|
|
|
|
if (typeof html == "undefined") {
|
|
html = layout.renderHtml(self);
|
|
}
|
|
|
|
if (self.statusbar) {
|
|
footerHtml = self.statusbar.renderHtml();
|
|
}
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" hidefocus="1">' +
|
|
'<div class="' + self.classPrefix + 'reset" role="application">' +
|
|
headerHtml +
|
|
'<div id="' + id + '-body" class="' + self.bodyClasses + '">' +
|
|
html +
|
|
'</div>' +
|
|
footerHtml +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Switches the window fullscreen mode.
|
|
*
|
|
* @method fullscreen
|
|
* @param {Boolean} state True/false state.
|
|
* @return {tinymce.ui.Window} Current window instance.
|
|
*/
|
|
fullscreen: function (state) {
|
|
var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect;
|
|
|
|
if (state != self._fullscreen) {
|
|
$(window).on('resize', function () {
|
|
var time;
|
|
|
|
if (self._fullscreen) {
|
|
// Time the layout time if it's to slow use a timeout to not hog the CPU
|
|
if (!slowRendering) {
|
|
time = new Date().getTime();
|
|
|
|
var rect = DomUtils.getWindowSize();
|
|
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
|
|
|
|
if ((new Date().getTime()) - time > 50) {
|
|
slowRendering = true;
|
|
}
|
|
} else {
|
|
if (!self._timer) {
|
|
self._timer = Delay.setTimeout(function () {
|
|
var rect = DomUtils.getWindowSize();
|
|
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
|
|
|
|
self._timer = 0;
|
|
}, 50);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
layoutRect = self.layoutRect();
|
|
self._fullscreen = state;
|
|
|
|
if (!state) {
|
|
self.borderBox = BoxUtils.parseBox(self.settings.border);
|
|
self.getEl('head').style.display = '';
|
|
layoutRect.deltaH += layoutRect.headerH;
|
|
$([documentElement, document.body]).removeClass(prefix + 'fullscreen');
|
|
self.classes.remove('fullscreen');
|
|
self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h);
|
|
} else {
|
|
self._initial = { x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h };
|
|
|
|
self.borderBox = BoxUtils.parseBox('0');
|
|
self.getEl('head').style.display = 'none';
|
|
layoutRect.deltaH -= layoutRect.headerH + 2;
|
|
$([documentElement, document.body]).addClass(prefix + 'fullscreen');
|
|
self.classes.add('fullscreen');
|
|
|
|
var rect = DomUtils.getWindowSize();
|
|
self.moveTo(0, 0).resizeTo(rect.w, rect.h);
|
|
}
|
|
}
|
|
|
|
return self.reflow();
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this, startPos;
|
|
|
|
setTimeout(function () {
|
|
self.classes.add('in');
|
|
self.fire('open');
|
|
}, 0);
|
|
|
|
self._super();
|
|
|
|
if (self.statusbar) {
|
|
self.statusbar.postRender();
|
|
}
|
|
|
|
self.focus();
|
|
|
|
this.dragHelper = new DragHelper(self._id + '-dragh', {
|
|
start: function () {
|
|
startPos = {
|
|
x: self.layoutRect().x,
|
|
y: self.layoutRect().y
|
|
};
|
|
},
|
|
|
|
drag: function (e) {
|
|
self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY);
|
|
}
|
|
});
|
|
|
|
self.on('submit', function (e) {
|
|
if (!e.isDefaultPrevented()) {
|
|
self.close();
|
|
}
|
|
});
|
|
|
|
windows.push(self);
|
|
toggleFullScreenState(true);
|
|
},
|
|
|
|
/**
|
|
* Fires a submit event with the serialized form.
|
|
*
|
|
* @method submit
|
|
* @return {Object} Event arguments object.
|
|
*/
|
|
submit: function () {
|
|
return this.fire('submit', { data: this.toJSON() });
|
|
},
|
|
|
|
/**
|
|
* Removes the current control from DOM and from UI collections.
|
|
*
|
|
* @method remove
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
remove: function () {
|
|
var self = this, i;
|
|
|
|
self.dragHelper.destroy();
|
|
self._super();
|
|
|
|
if (self.statusbar) {
|
|
this.statusbar.remove();
|
|
}
|
|
|
|
toggleBodyFullScreenClasses(self.classPrefix, false);
|
|
|
|
i = windows.length;
|
|
while (i--) {
|
|
if (windows[i] === self) {
|
|
windows.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
toggleFullScreenState(windows.length > 0);
|
|
},
|
|
|
|
/**
|
|
* Returns the contentWindow object of the iframe if it exists.
|
|
*
|
|
* @method getContentWindow
|
|
* @return {Window} window object or null.
|
|
*/
|
|
getContentWindow: function () {
|
|
var ifr = this.getEl().getElementsByTagName('iframe')[0];
|
|
return ifr ? ifr.contentWindow : null;
|
|
}
|
|
});
|
|
|
|
handleWindowResize();
|
|
|
|
return Window;
|
|
}
|
|
);
|
|
/**
|
|
* MessageBox.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 create MessageBoxes like alerts/confirms etc.
|
|
*
|
|
* @class tinymce.ui.MessageBox
|
|
* @extends tinymce.ui.FloatPanel
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.MessageBox',
|
|
[
|
|
"tinymce.core.ui.Window"
|
|
],
|
|
function (Window) {
|
|
"use strict";
|
|
|
|
var MessageBox = Window.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
settings = {
|
|
border: 1,
|
|
padding: 20,
|
|
layout: 'flex',
|
|
pack: "center",
|
|
align: "center",
|
|
containerCls: 'panel',
|
|
autoScroll: true,
|
|
buttons: { type: "button", text: "Ok", action: "ok" },
|
|
items: {
|
|
type: "label",
|
|
multiline: true,
|
|
maxWidth: 500,
|
|
maxHeight: 200
|
|
}
|
|
};
|
|
|
|
this._super(settings);
|
|
},
|
|
|
|
Statics: {
|
|
/**
|
|
* Ok buttons constant.
|
|
*
|
|
* @static
|
|
* @final
|
|
* @field {Number} OK
|
|
*/
|
|
OK: 1,
|
|
|
|
/**
|
|
* Ok/cancel buttons constant.
|
|
*
|
|
* @static
|
|
* @final
|
|
* @field {Number} OK_CANCEL
|
|
*/
|
|
OK_CANCEL: 2,
|
|
|
|
/**
|
|
* yes/no buttons constant.
|
|
*
|
|
* @static
|
|
* @final
|
|
* @field {Number} YES_NO
|
|
*/
|
|
YES_NO: 3,
|
|
|
|
/**
|
|
* yes/no/cancel buttons constant.
|
|
*
|
|
* @static
|
|
* @final
|
|
* @field {Number} YES_NO_CANCEL
|
|
*/
|
|
YES_NO_CANCEL: 4,
|
|
|
|
/**
|
|
* Constructs a new message box and renders it to the body element.
|
|
*
|
|
* @static
|
|
* @method msgBox
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
msgBox: function (settings) {
|
|
var buttons, callback = settings.callback || function () { };
|
|
|
|
function createButton(text, status, primary) {
|
|
return {
|
|
type: "button",
|
|
text: text,
|
|
subtype: primary ? 'primary' : '',
|
|
onClick: function (e) {
|
|
e.control.parents()[1].close();
|
|
callback(status);
|
|
}
|
|
};
|
|
}
|
|
|
|
switch (settings.buttons) {
|
|
case MessageBox.OK_CANCEL:
|
|
buttons = [
|
|
createButton('Ok', true, true),
|
|
createButton('Cancel', false)
|
|
];
|
|
break;
|
|
|
|
case MessageBox.YES_NO:
|
|
case MessageBox.YES_NO_CANCEL:
|
|
buttons = [
|
|
createButton('Yes', 1, true),
|
|
createButton('No', 0)
|
|
];
|
|
|
|
if (settings.buttons == MessageBox.YES_NO_CANCEL) {
|
|
buttons.push(createButton('Cancel', -1));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
buttons = [
|
|
createButton('Ok', true, true)
|
|
];
|
|
break;
|
|
}
|
|
|
|
return new Window({
|
|
padding: 20,
|
|
x: settings.x,
|
|
y: settings.y,
|
|
minWidth: 300,
|
|
minHeight: 100,
|
|
layout: "flex",
|
|
pack: "center",
|
|
align: "center",
|
|
buttons: buttons,
|
|
title: settings.title,
|
|
role: 'alertdialog',
|
|
items: {
|
|
type: "label",
|
|
multiline: true,
|
|
maxWidth: 500,
|
|
maxHeight: 200,
|
|
text: settings.text
|
|
},
|
|
onPostRender: function () {
|
|
this.aria('describedby', this.items()[0]._id);
|
|
},
|
|
onClose: settings.onClose,
|
|
onCancel: function () {
|
|
callback(false);
|
|
}
|
|
}).renderTo(document.body).reflow();
|
|
},
|
|
|
|
/**
|
|
* Creates a new alert dialog.
|
|
*
|
|
* @method alert
|
|
* @param {Object} settings Settings for the alert dialog.
|
|
* @param {function} [callback] Callback to execute when the user makes a choice.
|
|
*/
|
|
alert: function (settings, callback) {
|
|
if (typeof settings == "string") {
|
|
settings = { text: settings };
|
|
}
|
|
|
|
settings.callback = callback;
|
|
return MessageBox.msgBox(settings);
|
|
},
|
|
|
|
/**
|
|
* Creates a new confirm dialog.
|
|
*
|
|
* @method confirm
|
|
* @param {Object} settings Settings for the confirm dialog.
|
|
* @param {function} [callback] Callback to execute when the user makes a choice.
|
|
*/
|
|
confirm: function (settings, callback) {
|
|
if (typeof settings == "string") {
|
|
settings = { text: settings };
|
|
}
|
|
|
|
settings.callback = callback;
|
|
settings.buttons = MessageBox.OK_CANCEL;
|
|
|
|
return MessageBox.msgBox(settings);
|
|
}
|
|
}
|
|
});
|
|
|
|
return MessageBox;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* WindowManager.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 creation of native windows and dialogs. This class can be extended to provide for example inline dialogs.
|
|
*
|
|
* @class tinymce.WindowManager
|
|
* @example
|
|
* // Opens a new dialog with the file.htm file and the size 320x240
|
|
* // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog.
|
|
* tinymce.activeEditor.windowManager.open({
|
|
* url: 'file.htm',
|
|
* width: 320,
|
|
* height: 240
|
|
* }, {
|
|
* custom_param: 1
|
|
* });
|
|
*
|
|
* // Displays an alert box using the active editors window manager instance
|
|
* tinymce.activeEditor.windowManager.alert('Hello world!');
|
|
*
|
|
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
|
|
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
|
|
* if (s)
|
|
* tinymce.activeEditor.windowManager.alert("Ok");
|
|
* else
|
|
* tinymce.activeEditor.windowManager.alert("Cancel");
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.WindowManager',
|
|
[
|
|
"tinymce.core.ui.Window",
|
|
"tinymce.core.ui.MessageBox"
|
|
],
|
|
function (Window, MessageBox) {
|
|
return function (editor) {
|
|
var self = this, windows = [];
|
|
|
|
function getTopMostWindow() {
|
|
if (windows.length) {
|
|
return windows[windows.length - 1];
|
|
}
|
|
}
|
|
|
|
function fireOpenEvent(win) {
|
|
editor.fire('OpenWindow', {
|
|
win: win
|
|
});
|
|
}
|
|
|
|
function fireCloseEvent(win) {
|
|
editor.fire('CloseWindow', {
|
|
win: win
|
|
});
|
|
}
|
|
|
|
self.windows = windows;
|
|
|
|
editor.on('remove', function () {
|
|
var i = windows.length;
|
|
|
|
while (i--) {
|
|
windows[i].close();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Opens a new window.
|
|
*
|
|
* @method open
|
|
* @param {Object} args Optional name/value settings collection contains things like width/height/url etc.
|
|
* @param {Object} params Options like title, file, width, height etc.
|
|
* @option {String} title Window title.
|
|
* @option {String} file URL of the file to open in the window.
|
|
* @option {Number} width Width in pixels.
|
|
* @option {Number} height Height in pixels.
|
|
* @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content
|
|
* larger than the popup size specified).
|
|
*/
|
|
self.open = function (args, params) {
|
|
var win;
|
|
|
|
editor.editorManager.setActive(editor);
|
|
|
|
args.title = args.title || ' ';
|
|
|
|
// Handle URL
|
|
args.url = args.url || args.file; // Legacy
|
|
if (args.url) {
|
|
args.width = parseInt(args.width || 320, 10);
|
|
args.height = parseInt(args.height || 240, 10);
|
|
}
|
|
|
|
// Handle body
|
|
if (args.body) {
|
|
args.items = {
|
|
defaults: args.defaults,
|
|
type: args.bodyType || 'form',
|
|
items: args.body,
|
|
data: args.data,
|
|
callbacks: args.commands
|
|
};
|
|
}
|
|
|
|
if (!args.url && !args.buttons) {
|
|
args.buttons = [
|
|
{
|
|
text: 'Ok', subtype: 'primary', onclick: function () {
|
|
win.find('form')[0].submit();
|
|
}
|
|
},
|
|
|
|
{
|
|
text: 'Cancel', onclick: function () {
|
|
win.close();
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
win = new Window(args);
|
|
windows.push(win);
|
|
|
|
win.on('close', function () {
|
|
var i = windows.length;
|
|
|
|
while (i--) {
|
|
if (windows[i] === win) {
|
|
windows.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
if (!windows.length) {
|
|
editor.focus();
|
|
}
|
|
|
|
fireCloseEvent(win);
|
|
});
|
|
|
|
// Handle data
|
|
if (args.data) {
|
|
win.on('postRender', function () {
|
|
this.find('*').each(function (ctrl) {
|
|
var name = ctrl.name();
|
|
|
|
if (name in args.data) {
|
|
ctrl.value(args.data[name]);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// store args and parameters
|
|
win.features = args || {};
|
|
win.params = params || {};
|
|
|
|
// Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
|
|
if (windows.length === 1) {
|
|
editor.nodeChanged();
|
|
}
|
|
|
|
win = win.renderTo().reflow();
|
|
|
|
fireOpenEvent(win);
|
|
|
|
return win;
|
|
};
|
|
|
|
/**
|
|
* Creates a alert dialog. Please don't use the blocking behavior of this
|
|
* native version use the callback method instead then it can be extended.
|
|
*
|
|
* @method alert
|
|
* @param {String} message Text to display in the new alert dialog.
|
|
* @param {function} callback Callback function to be executed after the user has selected ok.
|
|
* @param {Object} scope Optional scope to execute the callback in.
|
|
* @example
|
|
* // Displays an alert box using the active editors window manager instance
|
|
* tinymce.activeEditor.windowManager.alert('Hello world!');
|
|
*/
|
|
self.alert = function (message, callback, scope) {
|
|
var win;
|
|
|
|
win = MessageBox.alert(message, function () {
|
|
if (callback) {
|
|
callback.call(scope || this);
|
|
} else {
|
|
editor.focus();
|
|
}
|
|
});
|
|
|
|
win.on('close', function () {
|
|
fireCloseEvent(win);
|
|
});
|
|
|
|
fireOpenEvent(win);
|
|
};
|
|
|
|
/**
|
|
* Creates a confirm dialog. Please don't use the blocking behavior of this
|
|
* native version use the callback method instead then it can be extended.
|
|
*
|
|
* @method confirm
|
|
* @param {String} message Text to display in the new confirm dialog.
|
|
* @param {function} callback Callback function to be executed after the user has selected ok or cancel.
|
|
* @param {Object} scope Optional scope to execute the callback in.
|
|
* @example
|
|
* // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm
|
|
* tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) {
|
|
* if (s)
|
|
* tinymce.activeEditor.windowManager.alert("Ok");
|
|
* else
|
|
* tinymce.activeEditor.windowManager.alert("Cancel");
|
|
* });
|
|
*/
|
|
self.confirm = function (message, callback, scope) {
|
|
var win;
|
|
|
|
win = MessageBox.confirm(message, function (state) {
|
|
callback.call(scope || this, state);
|
|
});
|
|
|
|
win.on('close', function () {
|
|
fireCloseEvent(win);
|
|
});
|
|
|
|
fireOpenEvent(win);
|
|
};
|
|
|
|
/**
|
|
* Closes the top most window.
|
|
*
|
|
* @method close
|
|
*/
|
|
self.close = function () {
|
|
if (getTopMostWindow()) {
|
|
getTopMostWindow().close();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the params of the last window open call. This can be used in iframe based
|
|
* dialog to get params passed from the tinymce plugin.
|
|
*
|
|
* @example
|
|
* var dialogArguments = top.tinymce.activeEditor.windowManager.getParams();
|
|
*
|
|
* @method getParams
|
|
* @return {Object} Name/value object with parameters passed from windowManager.open call.
|
|
*/
|
|
self.getParams = function () {
|
|
return getTopMostWindow() ? getTopMostWindow().params : null;
|
|
};
|
|
|
|
/**
|
|
* Sets the params of the last opened window.
|
|
*
|
|
* @method setParams
|
|
* @param {Object} params Params object to set for the last opened window.
|
|
*/
|
|
self.setParams = function (params) {
|
|
if (getTopMostWindow()) {
|
|
getTopMostWindow().params = params;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the currently opened window objects.
|
|
*
|
|
* @method getWindows
|
|
* @return {Array} Array of the currently opened windows.
|
|
*/
|
|
self.getWindows = function () {
|
|
return windows;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Tooltip.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a tooltip instance.
|
|
*
|
|
* @-x-less ToolTip.less
|
|
* @class tinymce.ui.ToolTip
|
|
* @extends tinymce.ui.Control
|
|
* @mixes tinymce.ui.Movable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Tooltip',
|
|
[
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.ui.Movable"
|
|
],
|
|
function (Control, Movable) {
|
|
return Control.extend({
|
|
Mixins: [Movable],
|
|
|
|
Defaults: {
|
|
classes: 'widget tooltip tooltip-n'
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, prefix = self.classPrefix;
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '" role="presentation">' +
|
|
'<div class="' + prefix + 'tooltip-arrow"></div>' +
|
|
'<div class="' + prefix + 'tooltip-inner">' + self.encode(self.state.get('text')) + '</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:text', function (e) {
|
|
self.getEl().lastChild.innerHTML = self.encode(e.value);
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this, style, rect;
|
|
|
|
style = self.getEl().style;
|
|
rect = self._layoutRect;
|
|
|
|
style.left = rect.x + 'px';
|
|
style.top = rect.y + 'px';
|
|
style.zIndex = 0xFFFF + 0xFFFF;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Widget.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
|
|
*/
|
|
|
|
/**
|
|
* Widget base class a widget is a control that has a tooltip and some basic states.
|
|
*
|
|
* @class tinymce.ui.Widget
|
|
* @extends tinymce.ui.Control
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Widget',
|
|
[
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.ui.Tooltip"
|
|
],
|
|
function (Control, Tooltip) {
|
|
"use strict";
|
|
|
|
var tooltip;
|
|
|
|
var Widget = Control.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {String} tooltip Tooltip text to display when hovering.
|
|
* @setting {Boolean} autofocus True if the control should be focused when rendered.
|
|
* @setting {String} text Text to display inside widget.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
self.canFocus = true;
|
|
|
|
if (settings.tooltip && Widget.tooltips !== false) {
|
|
self.on('mouseenter', function (e) {
|
|
var tooltip = self.tooltip().moveTo(-0xFFFF);
|
|
|
|
if (e.control == self) {
|
|
var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
|
|
|
|
tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
|
|
tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
|
|
tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
|
|
|
|
tooltip.moveRel(self.getEl(), rel);
|
|
} else {
|
|
tooltip.hide();
|
|
}
|
|
});
|
|
|
|
self.on('mouseleave mousedown click', function () {
|
|
self.tooltip().hide();
|
|
});
|
|
}
|
|
|
|
self.aria('label', settings.ariaLabel || settings.tooltip);
|
|
},
|
|
|
|
/**
|
|
* Returns the current tooltip instance.
|
|
*
|
|
* @method tooltip
|
|
* @return {tinymce.ui.Tooltip} Tooltip instance.
|
|
*/
|
|
tooltip: function () {
|
|
if (!tooltip) {
|
|
tooltip = new Tooltip({ type: 'tooltip' });
|
|
tooltip.renderTo();
|
|
}
|
|
|
|
return tooltip;
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this, settings = self.settings;
|
|
|
|
self._super();
|
|
|
|
if (!self.parent() && (settings.width || settings.height)) {
|
|
self.initLayoutRect();
|
|
self.repaint();
|
|
}
|
|
|
|
if (settings.autofocus) {
|
|
self.focus();
|
|
}
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
function disable(state) {
|
|
self.aria('disabled', state);
|
|
self.classes.toggle('disabled', state);
|
|
}
|
|
|
|
function active(state) {
|
|
self.aria('pressed', state);
|
|
self.classes.toggle('active', state);
|
|
}
|
|
|
|
self.state.on('change:disabled', function (e) {
|
|
disable(e.value);
|
|
});
|
|
|
|
self.state.on('change:active', function (e) {
|
|
active(e.value);
|
|
});
|
|
|
|
if (self.state.get('disabled')) {
|
|
disable(true);
|
|
}
|
|
|
|
if (self.state.get('active')) {
|
|
active(true);
|
|
}
|
|
|
|
return self._super();
|
|
},
|
|
|
|
/**
|
|
* Removes the current control from DOM and from UI collections.
|
|
*
|
|
* @method remove
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
remove: function () {
|
|
this._super();
|
|
|
|
if (tooltip) {
|
|
tooltip.remove();
|
|
tooltip = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
return Widget;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Progress.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
|
|
*/
|
|
|
|
/**
|
|
* Progress control.
|
|
*
|
|
* @-x-less Progress.less
|
|
* @class tinymce.ui.Progress
|
|
* @extends tinymce.ui.Control
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Progress',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
value: 0
|
|
},
|
|
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
self.classes.add('progress');
|
|
|
|
if (!self.settings.filter) {
|
|
self.settings.filter = function (value) {
|
|
return Math.round(value);
|
|
};
|
|
}
|
|
},
|
|
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = this.classPrefix;
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '">' +
|
|
'<div class="' + prefix + 'bar-container">' +
|
|
'<div class="' + prefix + 'bar"></div>' +
|
|
'</div>' +
|
|
'<div class="' + prefix + 'text">0%</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self._super();
|
|
self.value(self.settings.value);
|
|
|
|
return self;
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
function setValue(value) {
|
|
value = self.settings.filter(value);
|
|
self.getEl().lastChild.innerHTML = value + '%';
|
|
self.getEl().firstChild.firstChild.style.width = value + '%';
|
|
}
|
|
|
|
self.state.on('change:value', function (e) {
|
|
setValue(e.value);
|
|
});
|
|
|
|
setValue(self.state.get('value'));
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Notification.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a notification instance.
|
|
*
|
|
* @-x-less Notification.less
|
|
* @class tinymce.ui.Notification
|
|
* @extends tinymce.ui.Container
|
|
* @mixes tinymce.ui.Movable
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Notification',
|
|
[
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.ui.Movable",
|
|
"tinymce.core.ui.Progress",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function (Control, Movable, Progress, Delay) {
|
|
return Control.extend({
|
|
Mixins: [Movable],
|
|
|
|
Defaults: {
|
|
classes: 'widget notification'
|
|
},
|
|
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
|
|
if (settings.text) {
|
|
self.text(settings.text);
|
|
}
|
|
|
|
if (settings.icon) {
|
|
self.icon = settings.icon;
|
|
}
|
|
|
|
if (settings.color) {
|
|
self.color = settings.color;
|
|
}
|
|
|
|
if (settings.type) {
|
|
self.classes.add('notification-' + settings.type);
|
|
}
|
|
|
|
if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) {
|
|
self.closeButton = false;
|
|
} else {
|
|
self.classes.add('has-close');
|
|
self.closeButton = true;
|
|
}
|
|
|
|
if (settings.progressBar) {
|
|
self.progressBar = new Progress();
|
|
}
|
|
|
|
self.on('click', function (e) {
|
|
if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
|
|
self.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = '';
|
|
|
|
if (self.icon) {
|
|
icon = '<i class="' + prefix + 'ico' + ' ' + prefix + 'i-' + self.icon + '"></i>';
|
|
}
|
|
|
|
if (self.color) {
|
|
notificationStyle = ' style="background-color: ' + self.color + '"';
|
|
}
|
|
|
|
if (self.closeButton) {
|
|
closeButton = '<button type="button" class="' + prefix + 'close" aria-hidden="true">\u00d7</button>';
|
|
}
|
|
|
|
if (self.progressBar) {
|
|
progressBar = self.progressBar.renderHtml();
|
|
}
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '"' + notificationStyle + ' role="presentation">' +
|
|
icon +
|
|
'<div class="' + prefix + 'notification-inner">' + self.state.get('text') + '</div>' +
|
|
progressBar +
|
|
closeButton +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
Delay.setTimeout(function () {
|
|
self.$el.addClass(self.classPrefix + 'in');
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:text', function (e) {
|
|
self.getEl().childNodes[1].innerHTML = e.value;
|
|
});
|
|
if (self.progressBar) {
|
|
self.progressBar.bindStates();
|
|
}
|
|
return self._super();
|
|
},
|
|
|
|
close: function () {
|
|
var self = this;
|
|
|
|
if (!self.fire('close').isDefaultPrevented()) {
|
|
self.remove();
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this, style, rect;
|
|
|
|
style = self.getEl().style;
|
|
rect = self._layoutRect;
|
|
|
|
style.left = rect.x + 'px';
|
|
style.top = rect.y + 'px';
|
|
|
|
// Hardcoded arbitrary z-value because we want the
|
|
// notifications under the other windows
|
|
style.zIndex = 0xFFFF - 1;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* NotificationManager.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 creation of TinyMCE's notifications.
|
|
*
|
|
* @class tinymce.NotificationManager
|
|
* @example
|
|
* // Opens a new notification of type "error" with text "An error occurred."
|
|
* tinymce.activeEditor.notificationManager.open({
|
|
* text: 'An error occurred.',
|
|
* type: 'error'
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.NotificationManager',
|
|
[
|
|
"tinymce.core.ui.Notification",
|
|
"tinymce.core.util.Delay",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Notification, Delay, Tools) {
|
|
return function (editor) {
|
|
var self = this, notifications = [];
|
|
|
|
function getLastNotification() {
|
|
if (notifications.length) {
|
|
return notifications[notifications.length - 1];
|
|
}
|
|
}
|
|
|
|
self.notifications = notifications;
|
|
|
|
function resizeWindowEvent() {
|
|
Delay.requestAnimationFrame(function () {
|
|
prePositionNotifications();
|
|
positionNotifications();
|
|
});
|
|
}
|
|
|
|
// Since the viewport will change based on the present notifications, we need to move them all to the
|
|
// top left of the viewport to give an accurate size measurement so we can position them later.
|
|
function prePositionNotifications() {
|
|
for (var i = 0; i < notifications.length; i++) {
|
|
notifications[i].moveTo(0, 0);
|
|
}
|
|
}
|
|
|
|
function positionNotifications() {
|
|
if (notifications.length > 0) {
|
|
var firstItem = notifications.slice(0, 1)[0];
|
|
var container = editor.inline ? editor.getElement() : editor.getContentAreaContainer();
|
|
firstItem.moveRel(container, 'tc-tc');
|
|
if (notifications.length > 1) {
|
|
for (var i = 1; i < notifications.length; i++) {
|
|
notifications[i].moveRel(notifications[i - 1].getEl(), 'bc-tc');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
editor.on('remove', function () {
|
|
var i = notifications.length;
|
|
|
|
while (i--) {
|
|
notifications[i].close();
|
|
}
|
|
});
|
|
|
|
editor.on('ResizeEditor', positionNotifications);
|
|
editor.on('ResizeWindow', resizeWindowEvent);
|
|
|
|
/**
|
|
* Opens a new notification.
|
|
*
|
|
* @method open
|
|
* @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
|
|
*/
|
|
self.open = function (args) {
|
|
// Never open notification if editor has been removed.
|
|
if (editor.removed) {
|
|
return;
|
|
}
|
|
|
|
var notif;
|
|
|
|
editor.editorManager.setActive(editor);
|
|
|
|
var duplicate = findDuplicateMessage(notifications, args);
|
|
|
|
if (duplicate === null) {
|
|
notif = new Notification(args);
|
|
notifications.push(notif);
|
|
|
|
//If we have a timeout value
|
|
if (args.timeout > 0) {
|
|
notif.timer = setTimeout(function () {
|
|
notif.close();
|
|
}, args.timeout);
|
|
}
|
|
|
|
notif.on('close', function () {
|
|
var i = notifications.length;
|
|
|
|
if (notif.timer) {
|
|
editor.getWin().clearTimeout(notif.timer);
|
|
}
|
|
|
|
while (i--) {
|
|
if (notifications[i] === notif) {
|
|
notifications.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
positionNotifications();
|
|
});
|
|
|
|
notif.renderTo();
|
|
|
|
positionNotifications();
|
|
} else {
|
|
notif = duplicate;
|
|
}
|
|
|
|
return notif;
|
|
};
|
|
|
|
/**
|
|
* Closes the top most notification.
|
|
*
|
|
* @method close
|
|
*/
|
|
self.close = function () {
|
|
if (getLastNotification()) {
|
|
getLastNotification().close();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the currently opened notification objects.
|
|
*
|
|
* @method getNotifications
|
|
* @return {Array} Array of the currently opened notifications.
|
|
*/
|
|
self.getNotifications = function () {
|
|
return notifications;
|
|
};
|
|
|
|
editor.on('SkinLoaded', function () {
|
|
var serviceMessage = editor.settings.service_message;
|
|
|
|
if (serviceMessage) {
|
|
editor.notificationManager.open({
|
|
text: serviceMessage,
|
|
type: 'warning',
|
|
timeout: 0,
|
|
icon: ''
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Finds any existing notification with the same properties as the new one.
|
|
* Returns either the found notification or null.
|
|
*
|
|
* @param {Notification[]} notificationArray - Array of current notifications
|
|
* @param {type: string, } newNotification - New notification object
|
|
* @returns {?Notification}
|
|
*/
|
|
function findDuplicateMessage(notificationArray, newNotification) {
|
|
if (!isPlainTextNotification(newNotification)) {
|
|
return null;
|
|
}
|
|
|
|
var filteredNotifications = Tools.grep(notificationArray, function (notification) {
|
|
return isSameNotification(newNotification, notification);
|
|
});
|
|
|
|
return filteredNotifications.length === 0 ? null : filteredNotifications[0];
|
|
}
|
|
|
|
/**
|
|
* Checks if the passed in args object has the same
|
|
* type and text properties as the sent in notification.
|
|
*
|
|
* @param {type: string, text: string} a - New notification args object
|
|
* @param {Notification} b - Old notification
|
|
* @returns {boolean}
|
|
*/
|
|
function isSameNotification(a, b) {
|
|
return a.type === b.settings.type && a.text === b.settings.text;
|
|
}
|
|
|
|
/**
|
|
* Checks that the notification does not have a progressBar
|
|
* or timeour property.
|
|
*
|
|
* @param {Notification} notification - Notification to check
|
|
* @returns {boolean}
|
|
*/
|
|
function isPlainTextNotification(notification) {
|
|
return !notification.progressBar && !notification.timeout;
|
|
}
|
|
|
|
//self.positionNotifications = positionNotifications;
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* EditorObservable.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 contains the event logic for the tinymce.Editor class.
|
|
*
|
|
* @mixin tinymce.EditorObservable
|
|
* @extends tinymce.util.Observable
|
|
*/
|
|
define(
|
|
'tinymce.core.EditorObservable',
|
|
[
|
|
"tinymce.core.util.Observable",
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Observable, DOMUtils, Tools) {
|
|
var DOM = DOMUtils.DOM, customEventRootDelegates;
|
|
|
|
/**
|
|
* Returns the event target so for the specified event. Some events fire
|
|
* only on document, some fire on documentElement etc. This also handles the
|
|
* custom event root setting where it returns that element instead of the body.
|
|
*
|
|
* @private
|
|
* @param {tinymce.Editor} editor Editor instance to get event target from.
|
|
* @param {String} eventName Name of the event for example "click".
|
|
* @return {Element/Document} HTML Element or document target to bind on.
|
|
*/
|
|
function getEventTarget(editor, eventName) {
|
|
if (eventName == 'selectionchange') {
|
|
return editor.getDoc();
|
|
}
|
|
|
|
// Need to bind mousedown/mouseup etc to document not body in iframe mode
|
|
// Since the user might click on the HTML element not the BODY
|
|
if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) {
|
|
return editor.getDoc().documentElement;
|
|
}
|
|
|
|
// Bind to event root instead of body if it's defined
|
|
if (editor.settings.event_root) {
|
|
if (!editor.eventRoot) {
|
|
editor.eventRoot = DOM.select(editor.settings.event_root)[0];
|
|
}
|
|
|
|
return editor.eventRoot;
|
|
}
|
|
|
|
return editor.getBody();
|
|
}
|
|
|
|
/**
|
|
* Binds a event delegate for the specified name this delegate will fire
|
|
* the event to the editor dispatcher.
|
|
*
|
|
* @private
|
|
* @param {tinymce.Editor} editor Editor instance to get event target from.
|
|
* @param {String} eventName Name of the event for example "click".
|
|
*/
|
|
function bindEventDelegate(editor, eventName) {
|
|
var eventRootElm = getEventTarget(editor, eventName), delegate;
|
|
|
|
function isListening(editor) {
|
|
return !editor.hidden && !editor.readonly;
|
|
}
|
|
|
|
if (!editor.delegates) {
|
|
editor.delegates = {};
|
|
}
|
|
|
|
if (editor.delegates[eventName]) {
|
|
return;
|
|
}
|
|
|
|
if (editor.settings.event_root) {
|
|
if (!customEventRootDelegates) {
|
|
customEventRootDelegates = {};
|
|
editor.editorManager.on('removeEditor', function () {
|
|
var name;
|
|
|
|
if (!editor.editorManager.activeEditor) {
|
|
if (customEventRootDelegates) {
|
|
for (name in customEventRootDelegates) {
|
|
editor.dom.unbind(getEventTarget(editor, name));
|
|
}
|
|
|
|
customEventRootDelegates = null;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (customEventRootDelegates[eventName]) {
|
|
return;
|
|
}
|
|
|
|
delegate = function (e) {
|
|
var target = e.target, editors = editor.editorManager.editors, i = editors.length;
|
|
|
|
while (i--) {
|
|
var body = editors[i].getBody();
|
|
|
|
if (body === target || DOM.isChildOf(target, body)) {
|
|
if (isListening(editors[i])) {
|
|
editors[i].fire(eventName, e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
customEventRootDelegates[eventName] = delegate;
|
|
DOM.bind(eventRootElm, eventName, delegate);
|
|
} else {
|
|
delegate = function (e) {
|
|
if (isListening(editor)) {
|
|
editor.fire(eventName, e);
|
|
}
|
|
};
|
|
|
|
DOM.bind(eventRootElm, eventName, delegate);
|
|
editor.delegates[eventName] = delegate;
|
|
}
|
|
}
|
|
|
|
var EditorObservable = {
|
|
/**
|
|
* Bind any pending event delegates. This gets executed after the target body/document is created.
|
|
*
|
|
* @private
|
|
*/
|
|
bindPendingEventDelegates: function () {
|
|
var self = this;
|
|
|
|
Tools.each(self._pendingNativeEvents, function (name) {
|
|
bindEventDelegate(self, name);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Toggles a native event on/off this is called by the EventDispatcher when
|
|
* the first native event handler is added and when the last native event handler is removed.
|
|
*
|
|
* @private
|
|
*/
|
|
toggleNativeEvent: function (name, state) {
|
|
var self = this;
|
|
|
|
// Never bind focus/blur since the FocusManager fakes those
|
|
if (name == "focus" || name == "blur") {
|
|
return;
|
|
}
|
|
|
|
if (state) {
|
|
if (self.initialized) {
|
|
bindEventDelegate(self, name);
|
|
} else {
|
|
if (!self._pendingNativeEvents) {
|
|
self._pendingNativeEvents = [name];
|
|
} else {
|
|
self._pendingNativeEvents.push(name);
|
|
}
|
|
}
|
|
} else if (self.initialized) {
|
|
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
|
|
delete self.delegates[name];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unbinds all native event handlers that means delegates, custom events bound using the Events API etc.
|
|
*
|
|
* @private
|
|
*/
|
|
unbindAllNativeEvents: function () {
|
|
var self = this, name;
|
|
|
|
if (self.delegates) {
|
|
for (name in self.delegates) {
|
|
self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]);
|
|
}
|
|
|
|
delete self.delegates;
|
|
}
|
|
|
|
if (!self.inline) {
|
|
self.getBody().onload = null;
|
|
self.dom.unbind(self.getWin());
|
|
self.dom.unbind(self.getDoc());
|
|
}
|
|
|
|
self.dom.unbind(self.getBody());
|
|
self.dom.unbind(self.getContainer());
|
|
}
|
|
};
|
|
|
|
EditorObservable = Tools.extend({}, Observable, EditorObservable);
|
|
|
|
return EditorObservable;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Shortcuts.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 all logic for handling of keyboard shortcuts.
|
|
*
|
|
* @class tinymce.Shortcuts
|
|
* @example
|
|
* editor.shortcuts.add('ctrl+a', function() {});
|
|
* editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC
|
|
* editor.shortcuts.add('ctrl+alt+a', function() {});
|
|
* editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC
|
|
*/
|
|
define(
|
|
'tinymce.core.Shortcuts',
|
|
[
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.Env"
|
|
],
|
|
function (Tools, Env) {
|
|
var each = Tools.each, explode = Tools.explode;
|
|
|
|
var keyCodeLookup = {
|
|
"f9": 120,
|
|
"f10": 121,
|
|
"f11": 122
|
|
};
|
|
|
|
var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
|
|
|
|
return function (editor) {
|
|
var self = this, shortcuts = {}, pendingPatterns = [];
|
|
|
|
function parseShortcut(pattern) {
|
|
var id, key, shortcut = {};
|
|
|
|
// Parse modifiers and keys ctrl+alt+b for example
|
|
each(explode(pattern, '+'), function (value) {
|
|
if (value in modifierNames) {
|
|
shortcut[value] = true;
|
|
} else {
|
|
// Allow numeric keycodes like ctrl+219 for ctrl+[
|
|
if (/^[0-9]{2,}$/.test(value)) {
|
|
shortcut.keyCode = parseInt(value, 10);
|
|
} else {
|
|
shortcut.charCode = value.charCodeAt(0);
|
|
shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Generate unique id for modifier combination and set default state for unused modifiers
|
|
id = [shortcut.keyCode];
|
|
for (key in modifierNames) {
|
|
if (shortcut[key]) {
|
|
id.push(key);
|
|
} else {
|
|
shortcut[key] = false;
|
|
}
|
|
}
|
|
shortcut.id = id.join(',');
|
|
|
|
// Handle special access modifier differently depending on Mac/Win
|
|
if (shortcut.access) {
|
|
shortcut.alt = true;
|
|
|
|
if (Env.mac) {
|
|
shortcut.ctrl = true;
|
|
} else {
|
|
shortcut.shift = true;
|
|
}
|
|
}
|
|
|
|
// Handle special meta modifier differently depending on Mac/Win
|
|
if (shortcut.meta) {
|
|
if (Env.mac) {
|
|
shortcut.meta = true;
|
|
} else {
|
|
shortcut.ctrl = true;
|
|
shortcut.meta = false;
|
|
}
|
|
}
|
|
|
|
return shortcut;
|
|
}
|
|
|
|
function createShortcut(pattern, desc, cmdFunc, scope) {
|
|
var shortcuts;
|
|
|
|
shortcuts = Tools.map(explode(pattern, '>'), parseShortcut);
|
|
shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], {
|
|
func: cmdFunc,
|
|
scope: scope || editor
|
|
});
|
|
|
|
return Tools.extend(shortcuts[0], {
|
|
desc: editor.translate(desc),
|
|
subpatterns: shortcuts.slice(1)
|
|
});
|
|
}
|
|
|
|
function hasModifier(e) {
|
|
return e.altKey || e.ctrlKey || e.metaKey;
|
|
}
|
|
|
|
function isFunctionKey(e) {
|
|
return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123;
|
|
}
|
|
|
|
function matchShortcut(e, shortcut) {
|
|
if (!shortcut) {
|
|
return false;
|
|
}
|
|
|
|
if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) {
|
|
return false;
|
|
}
|
|
|
|
if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
|
|
return false;
|
|
}
|
|
|
|
if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
|
|
e.preventDefault();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function executeShortcutAction(shortcut) {
|
|
return shortcut.func ? shortcut.func.call(shortcut.scope) : null;
|
|
}
|
|
|
|
editor.on('keyup keypress keydown', function (e) {
|
|
if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) {
|
|
each(shortcuts, function (shortcut) {
|
|
if (matchShortcut(e, shortcut)) {
|
|
pendingPatterns = shortcut.subpatterns.slice(0);
|
|
|
|
if (e.type == "keydown") {
|
|
executeShortcutAction(shortcut);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (matchShortcut(e, pendingPatterns[0])) {
|
|
if (pendingPatterns.length === 1) {
|
|
if (e.type == "keydown") {
|
|
executeShortcutAction(pendingPatterns[0]);
|
|
}
|
|
}
|
|
|
|
pendingPatterns.shift();
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Adds a keyboard shortcut for some command or function.
|
|
*
|
|
* @method add
|
|
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
|
|
* @param {String} desc Text description for the command.
|
|
* @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed.
|
|
* @param {Object} scope Optional scope to execute the function in.
|
|
* @return {Boolean} true/false state if the shortcut was added or not.
|
|
*/
|
|
self.add = function (pattern, desc, cmdFunc, scope) {
|
|
var cmd;
|
|
|
|
cmd = cmdFunc;
|
|
|
|
if (typeof cmdFunc === 'string') {
|
|
cmdFunc = function () {
|
|
editor.execCommand(cmd, false, null);
|
|
};
|
|
} else if (Tools.isArray(cmd)) {
|
|
cmdFunc = function () {
|
|
editor.execCommand(cmd[0], cmd[1], cmd[2]);
|
|
};
|
|
}
|
|
|
|
each(explode(Tools.trim(pattern.toLowerCase())), function (pattern) {
|
|
var shortcut = createShortcut(pattern, desc, cmdFunc, scope);
|
|
shortcuts[shortcut.id] = shortcut;
|
|
});
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Remove a keyboard shortcut by pattern.
|
|
*
|
|
* @method remove
|
|
* @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o.
|
|
* @return {Boolean} true/false state if the shortcut was removed or not.
|
|
*/
|
|
self.remove = function (pattern) {
|
|
var shortcut = createShortcut(pattern);
|
|
|
|
if (shortcuts[shortcut.id]) {
|
|
delete shortcuts[shortcut.id];
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
defineGlobal("global!window", window);
|
|
/**
|
|
* ErrorReporter.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
|
|
*/
|
|
|
|
/**
|
|
* Various error reporting helper functions.
|
|
*
|
|
* @class tinymce.ErrorReporter
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.ErrorReporter',
|
|
[
|
|
"tinymce.core.AddOnManager"
|
|
],
|
|
function (AddOnManager) {
|
|
var PluginManager = AddOnManager.PluginManager;
|
|
|
|
var resolvePluginName = function (targetUrl, suffix) {
|
|
for (var name in PluginManager.urls) {
|
|
var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js';
|
|
if (matchUrl === targetUrl) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
var pluginUrlToMessage = function (editor, url) {
|
|
var plugin = resolvePluginName(url, editor.suffix);
|
|
return plugin ?
|
|
'Failed to load plugin: ' + plugin + ' from url ' + url :
|
|
'Failed to load plugin url: ' + url;
|
|
};
|
|
|
|
var displayNotification = function (editor, message) {
|
|
editor.notificationManager.open({
|
|
type: 'error',
|
|
text: message
|
|
});
|
|
};
|
|
|
|
var displayError = function (editor, message) {
|
|
if (editor._skinLoaded) {
|
|
displayNotification(editor, message);
|
|
} else {
|
|
editor.on('SkinLoaded', function () {
|
|
displayNotification(editor, message);
|
|
});
|
|
}
|
|
};
|
|
|
|
var uploadError = function (editor, message) {
|
|
displayError(editor, 'Failed to upload image: ' + message);
|
|
};
|
|
|
|
var pluginLoadError = function (editor, url) {
|
|
displayError(editor, pluginUrlToMessage(editor, url));
|
|
};
|
|
|
|
var contentCssError = function (editor, urls) {
|
|
displayError(editor, 'Failed to load content css: ' + urls[0]);
|
|
};
|
|
|
|
var initError = function (message) {
|
|
var console = window.console;
|
|
if (console && !window.test) { // Skip test env
|
|
if (console.error) {
|
|
console.error.apply(console, arguments);
|
|
} else {
|
|
console.log.apply(console, arguments);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
pluginLoadError: pluginLoadError,
|
|
uploadError: uploadError,
|
|
displayError: displayError,
|
|
contentCssError: contentCssError,
|
|
initError: initError
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* CaretContainerInput.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 shows the invisble block that the caret is currently in when contents is added to that block.
|
|
*/
|
|
define(
|
|
'tinymce.core.caret.CaretContainerInput',
|
|
[
|
|
'ephox.katamari.api.Fun',
|
|
'tinymce.core.caret.CaretContainer'
|
|
],
|
|
function (Fun, CaretContainer) {
|
|
var findBlockCaretContainer = function (editor) {
|
|
return editor.dom.select('*[data-mce-caret]')[0];
|
|
};
|
|
|
|
var removeIeControlRect = function (editor) {
|
|
editor.selection.setRng(editor.selection.getRng());
|
|
};
|
|
|
|
var showBlockCaretContainer = function (editor, blockCaretContainer) {
|
|
if (blockCaretContainer.hasAttribute('data-mce-caret')) {
|
|
CaretContainer.showCaretContainerBlock(blockCaretContainer);
|
|
removeIeControlRect(editor);
|
|
editor.selection.scrollIntoView(blockCaretContainer);
|
|
}
|
|
};
|
|
|
|
var handleBlockContainer = function (editor, e) {
|
|
var blockCaretContainer = findBlockCaretContainer(editor);
|
|
|
|
if (!blockCaretContainer) {
|
|
return;
|
|
}
|
|
|
|
if (e.type === 'compositionstart') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
showBlockCaretContainer(blockCaretContainer);
|
|
return;
|
|
}
|
|
|
|
if (CaretContainer.hasContent(blockCaretContainer)) {
|
|
showBlockCaretContainer(editor, blockCaretContainer);
|
|
}
|
|
};
|
|
|
|
var setup = function (editor) {
|
|
editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor));
|
|
};
|
|
|
|
return {
|
|
setup: setup
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Uploader.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
|
|
*/
|
|
|
|
/**
|
|
* Upload blobs or blob infos to the specified URL or handler.
|
|
*
|
|
* @private
|
|
* @class tinymce.file.Uploader
|
|
* @example
|
|
* var uploader = new Uploader({
|
|
* url: '/upload.php',
|
|
* basePath: '/base/path',
|
|
* credentials: true,
|
|
* handler: function(data, success, failure) {
|
|
* ...
|
|
* }
|
|
* });
|
|
*
|
|
* uploader.upload(blobInfos).then(function(result) {
|
|
* ...
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.file.Uploader',
|
|
[
|
|
"tinymce.core.util.Promise",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.util.Fun"
|
|
],
|
|
function (Promise, Tools, Fun) {
|
|
return function (uploadStatus, settings) {
|
|
var pendingPromises = {};
|
|
|
|
function pathJoin(path1, path2) {
|
|
if (path1) {
|
|
return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
|
|
}
|
|
|
|
return path2;
|
|
}
|
|
|
|
function defaultHandler(blobInfo, success, failure, progress) {
|
|
var xhr, formData;
|
|
|
|
xhr = new XMLHttpRequest();
|
|
xhr.open('POST', settings.url);
|
|
xhr.withCredentials = settings.credentials;
|
|
|
|
xhr.upload.onprogress = function (e) {
|
|
progress(e.loaded / e.total * 100);
|
|
};
|
|
|
|
xhr.onerror = function () {
|
|
failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status);
|
|
};
|
|
|
|
xhr.onload = function () {
|
|
var json;
|
|
|
|
if (xhr.status != 200) {
|
|
failure("HTTP Error: " + xhr.status);
|
|
return;
|
|
}
|
|
|
|
json = JSON.parse(xhr.responseText);
|
|
|
|
if (!json || typeof json.location != "string") {
|
|
failure("Invalid JSON: " + xhr.responseText);
|
|
return;
|
|
}
|
|
|
|
success(pathJoin(settings.basePath, json.location));
|
|
};
|
|
|
|
formData = new FormData();
|
|
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
|
|
|
xhr.send(formData);
|
|
}
|
|
|
|
function noUpload() {
|
|
return new Promise(function (resolve) {
|
|
resolve([]);
|
|
});
|
|
}
|
|
|
|
function handlerSuccess(blobInfo, url) {
|
|
return {
|
|
url: url,
|
|
blobInfo: blobInfo,
|
|
status: true
|
|
};
|
|
}
|
|
|
|
function handlerFailure(blobInfo, error) {
|
|
return {
|
|
url: '',
|
|
blobInfo: blobInfo,
|
|
status: false,
|
|
error: error
|
|
};
|
|
}
|
|
|
|
function resolvePending(blobUri, result) {
|
|
Tools.each(pendingPromises[blobUri], function (resolve) {
|
|
resolve(result);
|
|
});
|
|
|
|
delete pendingPromises[blobUri];
|
|
}
|
|
|
|
function uploadBlobInfo(blobInfo, handler, openNotification) {
|
|
uploadStatus.markPending(blobInfo.blobUri());
|
|
|
|
return new Promise(function (resolve) {
|
|
var notification, progress;
|
|
|
|
var noop = function () {
|
|
};
|
|
|
|
try {
|
|
var closeNotification = function () {
|
|
if (notification) {
|
|
notification.close();
|
|
progress = noop; // Once it's closed it's closed
|
|
}
|
|
};
|
|
|
|
var success = function (url) {
|
|
closeNotification();
|
|
uploadStatus.markUploaded(blobInfo.blobUri(), url);
|
|
resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url));
|
|
resolve(handlerSuccess(blobInfo, url));
|
|
};
|
|
|
|
var failure = function (error) {
|
|
closeNotification();
|
|
uploadStatus.removeFailed(blobInfo.blobUri());
|
|
resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error));
|
|
resolve(handlerFailure(blobInfo, error));
|
|
};
|
|
|
|
progress = function (percent) {
|
|
if (percent < 0 || percent > 100) {
|
|
return;
|
|
}
|
|
|
|
if (!notification) {
|
|
notification = openNotification();
|
|
}
|
|
|
|
notification.progressBar.value(percent);
|
|
};
|
|
|
|
handler(blobInfo, success, failure, progress);
|
|
} catch (ex) {
|
|
resolve(handlerFailure(blobInfo, ex.message));
|
|
}
|
|
});
|
|
}
|
|
|
|
function isDefaultHandler(handler) {
|
|
return handler === defaultHandler;
|
|
}
|
|
|
|
function pendingUploadBlobInfo(blobInfo) {
|
|
var blobUri = blobInfo.blobUri();
|
|
|
|
return new Promise(function (resolve) {
|
|
pendingPromises[blobUri] = pendingPromises[blobUri] || [];
|
|
pendingPromises[blobUri].push(resolve);
|
|
});
|
|
}
|
|
|
|
function uploadBlobs(blobInfos, openNotification) {
|
|
blobInfos = Tools.grep(blobInfos, function (blobInfo) {
|
|
return !uploadStatus.isUploaded(blobInfo.blobUri());
|
|
});
|
|
|
|
return Promise.all(Tools.map(blobInfos, function (blobInfo) {
|
|
return uploadStatus.isPending(blobInfo.blobUri()) ?
|
|
pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification);
|
|
}));
|
|
}
|
|
|
|
function upload(blobInfos, openNotification) {
|
|
return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification);
|
|
}
|
|
|
|
settings = Tools.extend({
|
|
credentials: false,
|
|
// We are adding a notify argument to this (at the moment, until it doesn't work)
|
|
handler: defaultHandler
|
|
}, settings);
|
|
|
|
return {
|
|
upload: upload
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Conversions.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
|
|
*/
|
|
|
|
/**
|
|
* Converts blob/uris back and forth.
|
|
*
|
|
* @private
|
|
* @class tinymce.file.Conversions
|
|
*/
|
|
define(
|
|
'tinymce.core.file.Conversions',
|
|
[
|
|
"tinymce.core.util.Promise"
|
|
],
|
|
function (Promise) {
|
|
function blobUriToBlob(url) {
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
var rejectWithError = function () {
|
|
reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible.");
|
|
};
|
|
|
|
try {
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
xhr.open('GET', url, true);
|
|
xhr.responseType = 'blob';
|
|
|
|
xhr.onload = function () {
|
|
if (this.status == 200) {
|
|
resolve(this.response);
|
|
} else {
|
|
// IE11 makes it into onload but responds with status 500
|
|
rejectWithError();
|
|
}
|
|
};
|
|
|
|
// Chrome fires an error event instead of the exception
|
|
// Also there seems to be no way to intercept the message that is logged to the console
|
|
xhr.onerror = rejectWithError;
|
|
|
|
xhr.send();
|
|
} catch (ex) {
|
|
rejectWithError();
|
|
}
|
|
});
|
|
}
|
|
|
|
function parseDataUri(uri) {
|
|
var type, matches;
|
|
|
|
uri = decodeURIComponent(uri).split(',');
|
|
|
|
matches = /data:([^;]+)/.exec(uri[0]);
|
|
if (matches) {
|
|
type = matches[1];
|
|
}
|
|
|
|
return {
|
|
type: type,
|
|
data: uri[1]
|
|
};
|
|
}
|
|
|
|
function dataUriToBlob(uri) {
|
|
return new Promise(function (resolve) {
|
|
var str, arr, i;
|
|
|
|
uri = parseDataUri(uri);
|
|
|
|
// Might throw error if data isn't proper base64
|
|
try {
|
|
str = atob(uri.data);
|
|
} catch (e) {
|
|
resolve(new Blob([]));
|
|
return;
|
|
}
|
|
|
|
arr = new Uint8Array(str.length);
|
|
|
|
for (i = 0; i < arr.length; i++) {
|
|
arr[i] = str.charCodeAt(i);
|
|
}
|
|
|
|
resolve(new Blob([arr], { type: uri.type }));
|
|
});
|
|
}
|
|
|
|
function uriToBlob(url) {
|
|
if (url.indexOf('blob:') === 0) {
|
|
return blobUriToBlob(url);
|
|
}
|
|
|
|
if (url.indexOf('data:') === 0) {
|
|
return dataUriToBlob(url);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function blobToDataUri(blob) {
|
|
return new Promise(function (resolve) {
|
|
var reader = new FileReader();
|
|
|
|
reader.onloadend = function () {
|
|
resolve(reader.result);
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
return {
|
|
uriToBlob: uriToBlob,
|
|
blobToDataUri: blobToDataUri,
|
|
parseDataUri: parseDataUri
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* ImageScanner.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
|
|
*/
|
|
|
|
/**
|
|
* Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris.
|
|
*
|
|
* @private
|
|
* @class tinymce.file.ImageScanner
|
|
*/
|
|
define(
|
|
'tinymce.core.file.ImageScanner',
|
|
[
|
|
"tinymce.core.util.Promise",
|
|
"tinymce.core.util.Arr",
|
|
"tinymce.core.util.Fun",
|
|
"tinymce.core.file.Conversions",
|
|
"tinymce.core.Env"
|
|
],
|
|
function (Promise, Arr, Fun, Conversions, Env) {
|
|
var count = 0;
|
|
|
|
var uniqueId = function (prefix) {
|
|
return (prefix || 'blobid') + (count++);
|
|
};
|
|
|
|
return function (uploadStatus, blobCache) {
|
|
var cachedPromises = {};
|
|
|
|
function findAll(elm, predicate) {
|
|
var images, promises;
|
|
|
|
function imageToBlobInfo(img, resolve, reject) {
|
|
var base64, blobInfo;
|
|
|
|
if (img.src.indexOf('blob:') === 0) {
|
|
blobInfo = blobCache.getByUri(img.src);
|
|
|
|
if (blobInfo) {
|
|
resolve({
|
|
image: img,
|
|
blobInfo: blobInfo
|
|
});
|
|
} else {
|
|
Conversions.uriToBlob(img.src).then(function (blob) {
|
|
Conversions.blobToDataUri(blob).then(function (dataUri) {
|
|
base64 = Conversions.parseDataUri(dataUri).data;
|
|
blobInfo = blobCache.create(uniqueId(), blob, base64);
|
|
blobCache.add(blobInfo);
|
|
|
|
resolve({
|
|
image: img,
|
|
blobInfo: blobInfo
|
|
});
|
|
});
|
|
}, function (err) {
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
base64 = Conversions.parseDataUri(img.src).data;
|
|
blobInfo = blobCache.findFirst(function (cachedBlobInfo) {
|
|
return cachedBlobInfo.base64() === base64;
|
|
});
|
|
|
|
if (blobInfo) {
|
|
resolve({
|
|
image: img,
|
|
blobInfo: blobInfo
|
|
});
|
|
} else {
|
|
Conversions.uriToBlob(img.src).then(function (blob) {
|
|
blobInfo = blobCache.create(uniqueId(), blob, base64);
|
|
blobCache.add(blobInfo);
|
|
|
|
resolve({
|
|
image: img,
|
|
blobInfo: blobInfo
|
|
});
|
|
}, function (err) {
|
|
reject(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!predicate) {
|
|
predicate = Fun.constant(true);
|
|
}
|
|
|
|
images = Arr.filter(elm.getElementsByTagName('img'), function (img) {
|
|
var src = img.src;
|
|
|
|
if (!Env.fileApi) {
|
|
return false;
|
|
}
|
|
|
|
if (img.hasAttribute('data-mce-bogus')) {
|
|
return false;
|
|
}
|
|
|
|
if (img.hasAttribute('data-mce-placeholder')) {
|
|
return false;
|
|
}
|
|
|
|
if (!src || src == Env.transparentSrc) {
|
|
return false;
|
|
}
|
|
|
|
if (src.indexOf('blob:') === 0) {
|
|
return !uploadStatus.isUploaded(src);
|
|
}
|
|
|
|
if (src.indexOf('data:') === 0) {
|
|
return predicate(img);
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
promises = Arr.map(images, function (img) {
|
|
var newPromise;
|
|
|
|
if (cachedPromises[img.src]) {
|
|
// Since the cached promise will return the cached image
|
|
// We need to wrap it and resolve with the actual image
|
|
return new Promise(function (resolve) {
|
|
cachedPromises[img.src].then(function (imageInfo) {
|
|
if (typeof imageInfo === 'string') { // error apparently
|
|
return imageInfo;
|
|
}
|
|
resolve({
|
|
image: img,
|
|
blobInfo: imageInfo.blobInfo
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
newPromise = new Promise(function (resolve, reject) {
|
|
imageToBlobInfo(img, resolve, reject);
|
|
}).then(function (result) {
|
|
delete cachedPromises[result.image.src];
|
|
return result;
|
|
})['catch'](function (error) {
|
|
delete cachedPromises[img.src];
|
|
return error;
|
|
});
|
|
|
|
cachedPromises[img.src] = newPromise;
|
|
|
|
return newPromise;
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
return {
|
|
findAll: findAll
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Uuid.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
|
|
*/
|
|
|
|
/**
|
|
* Generates unique ids.
|
|
*
|
|
* @class tinymce.util.Uuid
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.util.Uuid',
|
|
[
|
|
],
|
|
function () {
|
|
var count = 0;
|
|
|
|
var seed = function () {
|
|
var rnd = function () {
|
|
return Math.round(Math.random() * 0xFFFFFFFF).toString(36);
|
|
};
|
|
|
|
var now = new Date().getTime();
|
|
return 's' + now.toString(36) + rnd() + rnd() + rnd();
|
|
};
|
|
|
|
var uuid = function (prefix) {
|
|
return prefix + (count++) + seed();
|
|
};
|
|
|
|
return {
|
|
uuid: uuid
|
|
};
|
|
}
|
|
);
|
|
|
|
defineGlobal("global!URL", URL);
|
|
/**
|
|
* BlobCache.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
|
|
*/
|
|
|
|
/**
|
|
* Hold blob info objects where a blob has extra internal information.
|
|
*
|
|
* @private
|
|
* @class tinymce.file.BlobCache
|
|
*/
|
|
define(
|
|
'tinymce.core.file.BlobCache',
|
|
[
|
|
'tinymce.core.util.Arr',
|
|
'tinymce.core.util.Fun',
|
|
'tinymce.core.util.Uuid',
|
|
'global!URL'
|
|
],
|
|
function (Arr, Fun, Uuid, URL) {
|
|
return function () {
|
|
var cache = [], constant = Fun.constant;
|
|
|
|
function mimeToExt(mime) {
|
|
var mimes = {
|
|
'image/jpeg': 'jpg',
|
|
'image/jpg': 'jpg',
|
|
'image/gif': 'gif',
|
|
'image/png': 'png'
|
|
};
|
|
|
|
return mimes[mime.toLowerCase()] || 'dat';
|
|
}
|
|
|
|
function create(o, blob, base64, filename) {
|
|
return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({
|
|
id: o,
|
|
name: filename,
|
|
blob: blob,
|
|
base64: base64
|
|
});
|
|
}
|
|
|
|
function toBlobInfo(o) {
|
|
var id, name;
|
|
|
|
if (!o.blob || !o.base64) {
|
|
throw "blob and base64 representations of the image are required for BlobInfo to be created";
|
|
}
|
|
|
|
id = o.id || Uuid.uuid('blobid');
|
|
name = o.name || id;
|
|
|
|
return {
|
|
id: constant(id),
|
|
name: constant(name),
|
|
filename: constant(name + '.' + mimeToExt(o.blob.type)),
|
|
blob: constant(o.blob),
|
|
base64: constant(o.base64),
|
|
blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)),
|
|
uri: constant(o.uri)
|
|
};
|
|
}
|
|
|
|
function add(blobInfo) {
|
|
if (!get(blobInfo.id())) {
|
|
cache.push(blobInfo);
|
|
}
|
|
}
|
|
|
|
function get(id) {
|
|
return findFirst(function (cachedBlobInfo) {
|
|
return cachedBlobInfo.id() === id;
|
|
});
|
|
}
|
|
|
|
function findFirst(predicate) {
|
|
return Arr.filter(cache, predicate)[0];
|
|
}
|
|
|
|
function getByUri(blobUri) {
|
|
return findFirst(function (blobInfo) {
|
|
return blobInfo.blobUri() == blobUri;
|
|
});
|
|
}
|
|
|
|
function removeByUri(blobUri) {
|
|
cache = Arr.filter(cache, function (blobInfo) {
|
|
if (blobInfo.blobUri() === blobUri) {
|
|
URL.revokeObjectURL(blobInfo.blobUri());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function destroy() {
|
|
Arr.each(cache, function (cachedBlobInfo) {
|
|
URL.revokeObjectURL(cachedBlobInfo.blobUri());
|
|
});
|
|
|
|
cache = [];
|
|
}
|
|
|
|
return {
|
|
create: create,
|
|
add: add,
|
|
get: get,
|
|
getByUri: getByUri,
|
|
findFirst: findFirst,
|
|
removeByUri: removeByUri,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* UploadStatus.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
|
|
*/
|
|
|
|
/**
|
|
* Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was.
|
|
*
|
|
* @private
|
|
* @class tinymce.file.UploadStatus
|
|
*/
|
|
define(
|
|
'tinymce.core.file.UploadStatus',
|
|
[
|
|
],
|
|
function () {
|
|
return function () {
|
|
var PENDING = 1, UPLOADED = 2;
|
|
var blobUriStatuses = {};
|
|
|
|
function createStatus(status, resultUri) {
|
|
return {
|
|
status: status,
|
|
resultUri: resultUri
|
|
};
|
|
}
|
|
|
|
function hasBlobUri(blobUri) {
|
|
return blobUri in blobUriStatuses;
|
|
}
|
|
|
|
function getResultUri(blobUri) {
|
|
var result = blobUriStatuses[blobUri];
|
|
|
|
return result ? result.resultUri : null;
|
|
}
|
|
|
|
function isPending(blobUri) {
|
|
return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false;
|
|
}
|
|
|
|
function isUploaded(blobUri) {
|
|
return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false;
|
|
}
|
|
|
|
function markPending(blobUri) {
|
|
blobUriStatuses[blobUri] = createStatus(PENDING, null);
|
|
}
|
|
|
|
function markUploaded(blobUri, resultUri) {
|
|
blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri);
|
|
}
|
|
|
|
function removeFailed(blobUri) {
|
|
delete blobUriStatuses[blobUri];
|
|
}
|
|
|
|
function destroy() {
|
|
blobUriStatuses = {};
|
|
}
|
|
|
|
return {
|
|
hasBlobUri: hasBlobUri,
|
|
getResultUri: getResultUri,
|
|
isPending: isPending,
|
|
isUploaded: isUploaded,
|
|
markPending: markPending,
|
|
markUploaded: markUploaded,
|
|
removeFailed: removeFailed,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* EditorUpload.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 image uploads, updates undo stack and patches over various internal functions.
|
|
*
|
|
* @private
|
|
* @class tinymce.EditorUpload
|
|
*/
|
|
define(
|
|
'tinymce.core.EditorUpload',
|
|
[
|
|
"tinymce.core.util.Arr",
|
|
"tinymce.core.file.Uploader",
|
|
"tinymce.core.file.ImageScanner",
|
|
"tinymce.core.file.BlobCache",
|
|
"tinymce.core.file.UploadStatus",
|
|
"tinymce.core.ErrorReporter"
|
|
],
|
|
function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) {
|
|
return function (editor) {
|
|
var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings;
|
|
var uploadStatus = new UploadStatus();
|
|
|
|
function aliveGuard(callback) {
|
|
return function (result) {
|
|
if (editor.selection) {
|
|
return callback(result);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
}
|
|
|
|
function cacheInvalidator() {
|
|
return '?' + (new Date()).getTime();
|
|
}
|
|
|
|
// Replaces strings without regexps to avoid FF regexp to big issue
|
|
function replaceString(content, search, replace) {
|
|
var index = 0;
|
|
|
|
do {
|
|
index = content.indexOf(search, index);
|
|
|
|
if (index !== -1) {
|
|
content = content.substring(0, index) + replace + content.substr(index + search.length);
|
|
index += replace.length - search.length + 1;
|
|
}
|
|
} while (index !== -1);
|
|
|
|
return content;
|
|
}
|
|
|
|
function replaceImageUrl(content, targetUrl, replacementUrl) {
|
|
content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
|
|
content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
|
|
|
|
return content;
|
|
}
|
|
|
|
function replaceUrlInUndoStack(targetUrl, replacementUrl) {
|
|
Arr.each(editor.undoManager.data, function (level) {
|
|
if (level.type === 'fragmented') {
|
|
level.fragments = Arr.map(level.fragments, function (fragment) {
|
|
return replaceImageUrl(fragment, targetUrl, replacementUrl);
|
|
});
|
|
} else {
|
|
level.content = replaceImageUrl(level.content, targetUrl, replacementUrl);
|
|
}
|
|
});
|
|
}
|
|
|
|
function openNotification() {
|
|
return editor.notificationManager.open({
|
|
text: editor.translate('Image uploading...'),
|
|
type: 'info',
|
|
timeout: -1,
|
|
progressBar: true
|
|
});
|
|
}
|
|
|
|
function replaceImageUri(image, resultUri) {
|
|
blobCache.removeByUri(image.src);
|
|
replaceUrlInUndoStack(image.src, resultUri);
|
|
|
|
editor.$(image).attr({
|
|
src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri,
|
|
'data-mce-src': editor.convertURL(resultUri, 'src')
|
|
});
|
|
}
|
|
|
|
function uploadImages(callback) {
|
|
if (!uploader) {
|
|
uploader = new Uploader(uploadStatus, {
|
|
url: settings.images_upload_url,
|
|
basePath: settings.images_upload_base_path,
|
|
credentials: settings.images_upload_credentials,
|
|
handler: settings.images_upload_handler
|
|
});
|
|
}
|
|
|
|
return scanForImages().then(aliveGuard(function (imageInfos) {
|
|
var blobInfos;
|
|
|
|
blobInfos = Arr.map(imageInfos, function (imageInfo) {
|
|
return imageInfo.blobInfo;
|
|
});
|
|
|
|
return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) {
|
|
var filteredResult = Arr.map(result, function (uploadInfo, index) {
|
|
var image = imageInfos[index].image;
|
|
|
|
if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) {
|
|
replaceImageUri(image, uploadInfo.url);
|
|
} else if (uploadInfo.error) {
|
|
ErrorReporter.uploadError(editor, uploadInfo.error);
|
|
}
|
|
|
|
return {
|
|
element: image,
|
|
status: uploadInfo.status
|
|
};
|
|
});
|
|
|
|
if (callback) {
|
|
callback(filteredResult);
|
|
}
|
|
|
|
return filteredResult;
|
|
}));
|
|
}));
|
|
}
|
|
|
|
function uploadImagesAuto(callback) {
|
|
if (settings.automatic_uploads !== false) {
|
|
return uploadImages(callback);
|
|
}
|
|
}
|
|
|
|
function isValidDataUriImage(imgElm) {
|
|
return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
|
|
}
|
|
|
|
function scanForImages() {
|
|
if (!imageScanner) {
|
|
imageScanner = new ImageScanner(uploadStatus, blobCache);
|
|
}
|
|
|
|
return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function (result) {
|
|
result = Arr.filter(result, function (resultItem) {
|
|
// ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible.
|
|
// In such case resultItem will contain appropriate text error message, instead of image data.
|
|
if (typeof resultItem === 'string') {
|
|
ErrorReporter.displayError(editor, resultItem);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
Arr.each(result, function (resultItem) {
|
|
replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri());
|
|
resultItem.image.src = resultItem.blobInfo.blobUri();
|
|
resultItem.image.removeAttribute('data-mce-src');
|
|
});
|
|
|
|
return result;
|
|
}));
|
|
}
|
|
|
|
function destroy() {
|
|
blobCache.destroy();
|
|
uploadStatus.destroy();
|
|
imageScanner = uploader = null;
|
|
}
|
|
|
|
function replaceBlobUris(content) {
|
|
return content.replace(/src="(blob:[^"]+)"/g, function (match, blobUri) {
|
|
var resultUri = uploadStatus.getResultUri(blobUri);
|
|
|
|
if (resultUri) {
|
|
return 'src="' + resultUri + '"';
|
|
}
|
|
|
|
var blobInfo = blobCache.getByUri(blobUri);
|
|
|
|
if (!blobInfo) {
|
|
blobInfo = Arr.reduce(editor.editorManager.editors, function (result, editor) {
|
|
return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri);
|
|
}, null);
|
|
}
|
|
|
|
if (blobInfo) {
|
|
return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"';
|
|
}
|
|
|
|
return match;
|
|
});
|
|
}
|
|
|
|
editor.on('setContent', function () {
|
|
if (editor.settings.automatic_uploads !== false) {
|
|
uploadImagesAuto();
|
|
} else {
|
|
scanForImages();
|
|
}
|
|
});
|
|
|
|
editor.on('RawSaveContent', function (e) {
|
|
e.content = replaceBlobUris(e.content);
|
|
});
|
|
|
|
editor.on('getContent', function (e) {
|
|
if (e.source_view || e.format == 'raw') {
|
|
return;
|
|
}
|
|
|
|
e.content = replaceBlobUris(e.content);
|
|
});
|
|
|
|
editor.on('PostRender', function () {
|
|
editor.parser.addNodeFilter('img', function (images) {
|
|
Arr.each(images, function (img) {
|
|
var src = img.attr('src');
|
|
|
|
if (blobCache.getByUri(src)) {
|
|
return;
|
|
}
|
|
|
|
var resultUri = uploadStatus.getResultUri(src);
|
|
if (resultUri) {
|
|
img.attr('src', resultUri);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
return {
|
|
blobCache: blobCache,
|
|
uploadImages: uploadImages,
|
|
uploadImagesAuto: uploadImagesAuto,
|
|
scanForImages: scanForImages,
|
|
destroy: destroy
|
|
};
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* ForceBlocks.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
|
|
*/
|
|
|
|
/**
|
|
* Makes sure that everything gets wrapped in paragraphs.
|
|
*
|
|
* @private
|
|
* @class tinymce.ForceBlocks
|
|
*/
|
|
define(
|
|
'tinymce.core.ForceBlocks',
|
|
[
|
|
'ephox.katamari.api.Fun'
|
|
],
|
|
function (Fun) {
|
|
var addRootBlocks = function (editor) {
|
|
var settings = editor.settings, dom = editor.dom, selection = editor.selection;
|
|
var schema = editor.schema, blockElements = schema.getBlockElements();
|
|
var node = selection.getStart(), rootNode = editor.getBody(), rng;
|
|
var startContainer, startOffset, endContainer, endOffset, rootBlockNode;
|
|
var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection;
|
|
var tmpRng, rootNodeName, forcedRootBlock;
|
|
|
|
forcedRootBlock = settings.forced_root_block;
|
|
|
|
if (!node || node.nodeType !== 1 || !forcedRootBlock) {
|
|
return;
|
|
}
|
|
|
|
// Check if node is wrapped in block
|
|
while (node && node !== rootNode) {
|
|
if (blockElements[node.nodeName]) {
|
|
return;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
// Get current selection
|
|
rng = selection.getRng();
|
|
if (rng.setStart) {
|
|
startContainer = rng.startContainer;
|
|
startOffset = rng.startOffset;
|
|
endContainer = rng.endContainer;
|
|
endOffset = rng.endOffset;
|
|
|
|
try {
|
|
restoreSelection = editor.getDoc().activeElement === rootNode;
|
|
} catch (ex) {
|
|
// IE throws unspecified error here sometimes
|
|
}
|
|
} else {
|
|
// Force control range into text range
|
|
if (rng.item) {
|
|
node = rng.item(0);
|
|
rng = editor.getDoc().body.createTextRange();
|
|
rng.moveToElementText(node);
|
|
}
|
|
|
|
restoreSelection = rng.parentElement().ownerDocument === editor.getDoc();
|
|
tmpRng = rng.duplicate();
|
|
tmpRng.collapse(true);
|
|
startOffset = tmpRng.move('character', offset) * -1;
|
|
|
|
if (!tmpRng.collapsed) {
|
|
tmpRng = rng.duplicate();
|
|
tmpRng.collapse(false);
|
|
endOffset = (tmpRng.move('character', offset) * -1) - startOffset;
|
|
}
|
|
}
|
|
|
|
// Wrap non block elements and text nodes
|
|
node = rootNode.firstChild;
|
|
rootNodeName = rootNode.nodeName.toLowerCase();
|
|
while (node) {
|
|
// TODO: Break this up, too complex
|
|
if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) &&
|
|
schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) {
|
|
// Remove empty text nodes
|
|
if (node.nodeType === 3 && node.nodeValue.length === 0) {
|
|
tempNode = node;
|
|
node = node.nextSibling;
|
|
dom.remove(tempNode);
|
|
continue;
|
|
}
|
|
|
|
if (!rootBlockNode) {
|
|
rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs);
|
|
node.parentNode.insertBefore(rootBlockNode, node);
|
|
wrapped = true;
|
|
}
|
|
|
|
tempNode = node;
|
|
node = node.nextSibling;
|
|
rootBlockNode.appendChild(tempNode);
|
|
} else {
|
|
rootBlockNode = null;
|
|
node = node.nextSibling;
|
|
}
|
|
}
|
|
|
|
if (wrapped && restoreSelection) {
|
|
if (rng.setStart) {
|
|
rng.setStart(startContainer, startOffset);
|
|
rng.setEnd(endContainer, endOffset);
|
|
selection.setRng(rng);
|
|
} else {
|
|
// Only select if the previous selection was inside the document to prevent auto focus in quirks mode
|
|
try {
|
|
rng = editor.getDoc().body.createTextRange();
|
|
rng.moveToElementText(rootNode);
|
|
rng.collapse(true);
|
|
rng.moveStart('character', startOffset);
|
|
|
|
if (endOffset > 0) {
|
|
rng.moveEnd('character', endOffset);
|
|
}
|
|
|
|
rng.select();
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
editor.nodeChanged();
|
|
}
|
|
};
|
|
|
|
var setup = function (editor) {
|
|
if (editor.settings.forced_root_block) {
|
|
editor.on('NodeChange', Fun.curry(addRootBlocks, editor));
|
|
}
|
|
};
|
|
|
|
return {
|
|
setup: setup
|
|
};
|
|
}
|
|
);
|
|
define(
|
|
'ephox.katamari.api.Merger',
|
|
|
|
[
|
|
'ephox.katamari.api.Type',
|
|
'global!Array',
|
|
'global!Error'
|
|
],
|
|
|
|
function (Type, Array, Error) {
|
|
|
|
var shallow = function (old, nu) {
|
|
return nu;
|
|
};
|
|
|
|
var deep = function (old, nu) {
|
|
var bothObjects = Type.isObject(old) && Type.isObject(nu);
|
|
return bothObjects ? deepMerge(old, nu) : nu;
|
|
};
|
|
|
|
var baseMerge = function (merger) {
|
|
return function() {
|
|
// Don't use array slice(arguments), makes the whole function unoptimisable on Chrome
|
|
var objects = new Array(arguments.length);
|
|
for (var i = 0; i < objects.length; i++) objects[i] = arguments[i];
|
|
|
|
if (objects.length === 0) throw new Error('Can\'t merge zero objects');
|
|
|
|
var ret = {};
|
|
for (var j = 0; j < objects.length; j++) {
|
|
var curObject = objects[j];
|
|
for (var key in curObject) if (curObject.hasOwnProperty(key)) {
|
|
ret[key] = merger(ret[key], curObject[key]);
|
|
}
|
|
}
|
|
return ret;
|
|
};
|
|
};
|
|
|
|
var deepMerge = baseMerge(deep);
|
|
var merge = baseMerge(shallow);
|
|
|
|
return {
|
|
deepMerge: deepMerge,
|
|
merge: merge
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* MatchKeys.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.MatchKeys',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Fun',
|
|
'ephox.katamari.api.Merger'
|
|
],
|
|
function (Arr, Fun, Merger) {
|
|
var defaultPatterns = function (patterns) {
|
|
return Arr.map(patterns, function (pattern) {
|
|
return Merger.merge({
|
|
shiftKey: false,
|
|
altKey: false,
|
|
ctrlKey: false,
|
|
metaKey: false,
|
|
keyCode: 0,
|
|
action: Fun.noop
|
|
}, pattern);
|
|
});
|
|
};
|
|
|
|
var matchesEvent = function (pattern, evt) {
|
|
return (
|
|
evt.keyCode === pattern.keyCode &&
|
|
evt.shiftKey === pattern.shiftKey &&
|
|
evt.altKey === pattern.altKey &&
|
|
evt.ctrlKey === pattern.ctrlKey &&
|
|
evt.metaKey === pattern.metaKey
|
|
);
|
|
};
|
|
|
|
var match = function (patterns, evt) {
|
|
return Arr.bind(defaultPatterns(patterns), function (pattern) {
|
|
return matchesEvent(pattern, evt) ? [pattern] : [ ];
|
|
});
|
|
};
|
|
|
|
var action = function (f) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return function () {
|
|
return f.apply(null, args);
|
|
};
|
|
};
|
|
|
|
return {
|
|
match: match,
|
|
action: action
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* ArrowKeys.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.ArrowKeys',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'ephox.katamari.api.Cell',
|
|
'tinymce.core.keyboard.BoundarySelection',
|
|
'tinymce.core.keyboard.MatchKeys',
|
|
'tinymce.core.util.VK'
|
|
],
|
|
function (Arr, Cell, BoundarySelection, MatchKeys, VK) {
|
|
var setup = function (editor, caret) {
|
|
editor.on('keydown', function (evt) {
|
|
var matches = MatchKeys.match([
|
|
{ keyCode: VK.RIGHT, action: BoundarySelection.move(editor, caret, true) },
|
|
{ keyCode: VK.LEFT, action: BoundarySelection.move(editor, caret, false) }
|
|
], evt);
|
|
|
|
Arr.find(matches, function (pattern) {
|
|
return pattern.action();
|
|
}).each(function (_) {
|
|
evt.preventDefault();
|
|
});
|
|
});
|
|
};
|
|
|
|
return {
|
|
setup: setup
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* DeleteBackspaceKeys.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.DeleteBackspaceKeys',
|
|
[
|
|
'ephox.katamari.api.Arr',
|
|
'tinymce.core.delete.BlockBoundaryDelete',
|
|
'tinymce.core.delete.BlockRangeDelete',
|
|
'tinymce.core.delete.CefDelete',
|
|
'tinymce.core.delete.InlineBoundaryDelete',
|
|
'tinymce.core.keyboard.MatchKeys',
|
|
'tinymce.core.util.VK'
|
|
],
|
|
function (Arr, BlockBoundaryDelete, BlockRangeDelete, CefDelete, BoundaryDelete, MatchKeys, VK) {
|
|
var setupKeyDownHandler = function (editor, caret) {
|
|
editor.on('keydown', function (evt) {
|
|
var matches = MatchKeys.match([
|
|
{ keyCode: VK.BACKSPACE, action: MatchKeys.action(BoundaryDelete.backspaceDelete, editor, caret, false) },
|
|
{ keyCode: VK.DELETE, action: MatchKeys.action(BoundaryDelete.backspaceDelete, editor, caret, true) },
|
|
{ keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, false) },
|
|
{ keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, true) },
|
|
{ keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, false) },
|
|
{ keyCode: VK.DELETE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, true) },
|
|
{ keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, false) },
|
|
{ keyCode: VK.DELETE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, true) }
|
|
], evt);
|
|
|
|
Arr.find(matches, function (pattern) {
|
|
return pattern.action();
|
|
}).each(function (_) {
|
|
evt.preventDefault();
|
|
});
|
|
});
|
|
};
|
|
|
|
var setupKeyUpHandler = function (editor) {
|
|
editor.on('keyup', function (evt) {
|
|
var matches = MatchKeys.match([
|
|
{ keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) },
|
|
{ keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }
|
|
], evt);
|
|
|
|
Arr.find(matches, function (pattern) {
|
|
return pattern.action();
|
|
});
|
|
});
|
|
};
|
|
|
|
var setup = function (editor, caret) {
|
|
setupKeyDownHandler(editor, caret);
|
|
setupKeyUpHandler(editor);
|
|
};
|
|
|
|
return {
|
|
setup: setup
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* EnterKey.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 logic for handling the enter key to split/generate block elements.
|
|
*/
|
|
define(
|
|
'tinymce.core.keyboard.EnterKey',
|
|
[
|
|
'tinymce.core.caret.CaretContainer',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.dom.RangeUtils',
|
|
'tinymce.core.dom.TreeWalker',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.text.Zwsp',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (CaretContainer, NodeType, RangeUtils, TreeWalker, Env, Zwsp, Tools) {
|
|
var isIE = Env.ie && Env.ie < 11;
|
|
|
|
var isEmptyAnchor = function (elm) {
|
|
return elm && elm.nodeName === "A" && Tools.trim(Zwsp.trim(elm.innerText || elm.textContent)).length === 0;
|
|
};
|
|
|
|
var isTableCell = function (node) {
|
|
return node && /^(TD|TH|CAPTION)$/.test(node.nodeName);
|
|
};
|
|
|
|
var emptyBlock = function (elm) {
|
|
// BR is needed in empty blocks on non IE browsers
|
|
elm.innerHTML = !isIE ? '<br data-mce-bogus="1">' : '';
|
|
};
|
|
|
|
// Returns true if the block can be split into two blocks or not
|
|
var canSplitBlock = function (dom, node) {
|
|
return node &&
|
|
dom.isBlock(node) &&
|
|
!/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
|
|
!/^(fixed|absolute)/i.test(node.style.position) &&
|
|
dom.getContentEditable(node) !== "true";
|
|
};
|
|
|
|
// Renders empty block on IE
|
|
var renderBlockOnIE = function (dom, selection, block) {
|
|
var oldRng;
|
|
|
|
if (dom.isBlock(block)) {
|
|
oldRng = selection.getRng();
|
|
block.appendChild(dom.create('span', null, '\u00a0'));
|
|
selection.select(block);
|
|
block.lastChild.outerHTML = '';
|
|
selection.setRng(oldRng);
|
|
}
|
|
};
|
|
|
|
// Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
|
|
var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) {
|
|
var node = block, firstChilds = [], i;
|
|
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
// Find inner most first child ex: <p><i><b>*</b></i></p>
|
|
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 example <p><br></p> wont be rendered correctly in a contentEditable area
|
|
// until you remove the br producing <p></p>
|
|
if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) {
|
|
if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') {
|
|
dom.remove(parentBlock.firstChild);
|
|
}
|
|
}
|
|
|
|
if (/^(LI|DT|DD)$/.test(root.nodeName)) {
|
|
var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild);
|
|
|
|
if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) {
|
|
root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild);
|
|
}
|
|
}
|
|
|
|
rng = dom.createRng();
|
|
|
|
// Normalize whitespace to remove empty text nodes. Fix for: #6904
|
|
// Gecko will be able to place the caret in empty text nodes but it won't render propery
|
|
// Older IE versions will sometimes crash so for now ignore all IE versions
|
|
if (!Env.ie) {
|
|
root.normalize();
|
|
}
|
|
|
|
if (root.hasChildNodes()) {
|
|
walker = new TreeWalker(root, root);
|
|
|
|
while ((node = walker.current())) {
|
|
if (node.nodeType == 3) {
|
|
rng.setStart(node, 0);
|
|
rng.setEnd(node, 0);
|
|
break;
|
|
}
|
|
|
|
if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) {
|
|
rng.setStartBefore(node);
|
|
rng.setEndBefore(node);
|
|
break;
|
|
}
|
|
|
|
lastNode = node;
|
|
node = walker.next();
|
|
}
|
|
|
|
if (!node) {
|
|
rng.setStart(lastNode, 0);
|
|
rng.setEnd(lastNode, 0);
|
|
}
|
|
} else {
|
|
if (root.nodeName == 'BR') {
|
|
if (root.nextSibling && dom.isBlock(root.nextSibling)) {
|
|
// Trick on older IE versions to render the caret before the BR between two lists
|
|
if (!documentMode || documentMode < 9) {
|
|
tempElm = dom.create('br');
|
|
root.parentNode.insertBefore(tempElm, root);
|
|
}
|
|
|
|
rng.setStartBefore(root);
|
|
rng.setEndBefore(root);
|
|
} else {
|
|
rng.setStartAfter(root);
|
|
rng.setEndAfter(root);
|
|
}
|
|
} else {
|
|
rng.setStart(root, 0);
|
|
rng.setEnd(root, 0);
|
|
}
|
|
}
|
|
|
|
selection.setRng(rng);
|
|
|
|
// Remove tempElm created for old IE:s
|
|
dom.remove(tempElm);
|
|
selection.scrollIntoView(root);
|
|
}
|
|
|
|
function setForcedBlockAttrs(node) {
|
|
var forcedRootBlockName = settings.forced_root_block;
|
|
|
|
if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
|
|
dom.setAttribs(node, settings.forced_root_block_attrs);
|
|
}
|
|
}
|
|
|
|
// Creates a new block element by cloning the current one or creating a new one if the name is specified
|
|
// This function will also copy any text formatting from the parent block and add it to the new one
|
|
function createNewBlock(name) {
|
|
var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements();
|
|
|
|
if (name || parentBlockName == "TABLE") {
|
|
block = dom.create(name || newBlockName);
|
|
setForcedBlockAttrs(block);
|
|
} else {
|
|
block = parentBlock.cloneNode(false);
|
|
}
|
|
|
|
caretNode = block;
|
|
|
|
if (settings.keep_styles === false) {
|
|
dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block
|
|
dom.setAttrib(block, 'class', null);
|
|
} else {
|
|
// Clone any parent styles
|
|
do {
|
|
if (textInlineElements[node.nodeName]) {
|
|
// Never clone a caret containers
|
|
if (node.id == '_mce_caret') {
|
|
continue;
|
|
}
|
|
|
|
clonedNode = node.cloneNode(false);
|
|
dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique
|
|
|
|
if (block.hasChildNodes()) {
|
|
clonedNode.appendChild(block.firstChild);
|
|
block.appendChild(clonedNode);
|
|
} else {
|
|
caretNode = clonedNode;
|
|
block.appendChild(clonedNode);
|
|
}
|
|
}
|
|
} while ((node = node.parentNode) && node != editableRoot);
|
|
}
|
|
|
|
// BR is needed in empty blocks on non IE browsers
|
|
if (!isIE) {
|
|
caretNode.innerHTML = '<br data-mce-bogus="1">';
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
// Returns true/false if the caret is at the start/end of the parent block element
|
|
function isCaretAtStartOrEndOfBlock(start) {
|
|
var walker, node, name, normalizedOffset;
|
|
|
|
normalizedOffset = normalizeZwspOffset(start, container, offset);
|
|
|
|
// Caret is in the middle of a text node like "a|b"
|
|
if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) {
|
|
return false;
|
|
}
|
|
|
|
// If after the last element in block node edge case for #5091
|
|
if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) {
|
|
return true;
|
|
}
|
|
|
|
// If the caret if before the first element in parentBlock
|
|
if (start && container.nodeType == 1 && container == parentBlock.firstChild) {
|
|
return true;
|
|
}
|
|
|
|
// Caret can be before/after a table
|
|
if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
|
|
return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start);
|
|
}
|
|
|
|
// Walk the DOM and look for text nodes or non empty elements
|
|
walker = new TreeWalker(container, parentBlock);
|
|
|
|
// If caret is in beginning or end of a text block then jump to the next/previous node
|
|
if (container.nodeType == 3) {
|
|
if (start && normalizedOffset === 0) {
|
|
walker.prev();
|
|
} else if (!start && normalizedOffset == container.nodeValue.length) {
|
|
walker.next();
|
|
}
|
|
}
|
|
|
|
while ((node = walker.current())) {
|
|
if (node.nodeType === 1) {
|
|
// Ignore bogus elements
|
|
if (!node.getAttribute('data-mce-bogus')) {
|
|
// Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p>
|
|
name = node.nodeName.toLowerCase();
|
|
if (nonEmptyElementsMap[name] && name !== 'br') {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) {
|
|
return false;
|
|
}
|
|
|
|
if (start) {
|
|
walker.prev();
|
|
} else {
|
|
walker.next();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Wraps any text nodes or inline elements in the specified forced root block name
|
|
function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
|
|
var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
|
|
|
|
// Not in a block element or in a table cell or caption
|
|
parentBlock = dom.getParent(container, dom.isBlock);
|
|
if (!parentBlock || !canSplitBlock(dom, parentBlock)) {
|
|
parentBlock = parentBlock || editableRoot;
|
|
|
|
if (parentBlock == editor.getBody() || isTableCell(parentBlock)) {
|
|
rootBlockName = parentBlock.nodeName.toLowerCase();
|
|
} else {
|
|
rootBlockName = parentBlock.parentNode.nodeName.toLowerCase();
|
|
}
|
|
|
|
if (!parentBlock.hasChildNodes()) {
|
|
newBlock = dom.create(blockName);
|
|
setForcedBlockAttrs(newBlock);
|
|
parentBlock.appendChild(newBlock);
|
|
rng.setStart(newBlock, 0);
|
|
rng.setEnd(newBlock, 0);
|
|
return newBlock;
|
|
}
|
|
|
|
// Find parent that is the first child of parentBlock
|
|
node = container;
|
|
while (node.parentNode != parentBlock) {
|
|
node = node.parentNode;
|
|
}
|
|
|
|
// Loop left to find start node start wrapping at
|
|
while (node && !dom.isBlock(node)) {
|
|
startNode = node;
|
|
node = node.previousSibling;
|
|
}
|
|
|
|
if (startNode && schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
|
|
newBlock = dom.create(blockName);
|
|
setForcedBlockAttrs(newBlock);
|
|
startNode.parentNode.insertBefore(newBlock, startNode);
|
|
|
|
// Start wrapping until we hit a block
|
|
node = startNode;
|
|
while (node && !dom.isBlock(node)) {
|
|
next = node.nextSibling;
|
|
newBlock.appendChild(node);
|
|
node = next;
|
|
}
|
|
|
|
// Restore range to it's past location
|
|
rng.setStart(container, offset);
|
|
rng.setEnd(container, offset);
|
|
}
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
// Inserts a block or br before/after or in the middle of a split list of the LI is empty
|
|
function handleEmptyListItem() {
|
|
function isFirstOrLastLi(first) {
|
|
var node = containerBlock[first ? 'firstChild' : 'lastChild'];
|
|
|
|
// Find first/last element since there might be whitespace there
|
|
while (node) {
|
|
if (node.nodeType == 1) {
|
|
break;
|
|
}
|
|
|
|
node = node[first ? 'nextSibling' : 'previousSibling'];
|
|
}
|
|
|
|
return node === parentBlock;
|
|
}
|
|
|
|
function getContainerBlock() {
|
|
var containerBlockParent = containerBlock.parentNode;
|
|
|
|
if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) {
|
|
return containerBlockParent;
|
|
}
|
|
|
|
return containerBlock;
|
|
}
|
|
|
|
if (containerBlock == editor.getBody()) {
|
|
return;
|
|
}
|
|
|
|
// Check if we are in an nested list
|
|
var containerBlockParentName = containerBlock.parentNode.nodeName;
|
|
if (/^(OL|UL|LI)$/.test(containerBlockParentName)) {
|
|
newBlockName = 'LI';
|
|
}
|
|
|
|
newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
|
|
|
|
if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
|
|
if (containerBlockParentName == 'LI') {
|
|
// Nested list is inside a LI
|
|
dom.insertAfter(newBlock, getContainerBlock());
|
|
} else {
|
|
// Is first and last list item then replace the OL/UL with a text block
|
|
dom.replace(newBlock, containerBlock);
|
|
}
|
|
} else if (isFirstOrLastLi(true)) {
|
|
if (containerBlockParentName == 'LI') {
|
|
// List nested in an LI then move the list to a new sibling LI
|
|
dom.insertAfter(newBlock, getContainerBlock());
|
|
newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed
|
|
newBlock.appendChild(containerBlock);
|
|
} else {
|
|
// First LI in list then remove LI and add text block before list
|
|
containerBlock.parentNode.insertBefore(newBlock, containerBlock);
|
|
}
|
|
} else if (isFirstOrLastLi()) {
|
|
// Last LI in list then remove LI and add text block after list
|
|
dom.insertAfter(newBlock, getContainerBlock());
|
|
renderBlockOnIE(dom, selection, newBlock);
|
|
} else {
|
|
// Middle LI in list the split the list and insert a text block in the middle
|
|
// Extract after fragment and insert it after the current block
|
|
containerBlock = getContainerBlock();
|
|
tmpRng = rng.cloneRange();
|
|
tmpRng.setStartAfter(parentBlock);
|
|
tmpRng.setEndAfter(containerBlock);
|
|
fragment = tmpRng.extractContents();
|
|
|
|
if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') {
|
|
newBlock = fragment.firstChild;
|
|
dom.insertAfter(fragment, containerBlock);
|
|
} else {
|
|
dom.insertAfter(fragment, containerBlock);
|
|
dom.insertAfter(newBlock, containerBlock);
|
|
}
|
|
}
|
|
|
|
dom.remove(parentBlock);
|
|
moveToCaretPosition(newBlock);
|
|
undoManager.add();
|
|
}
|
|
|
|
// Inserts a BR element if the forced_root_block option is set to false or empty string
|
|
function insertBr() {
|
|
editor.execCommand("InsertLineBreak", false, evt);
|
|
}
|
|
|
|
// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
|
|
function trimLeadingLineBreaks(node) {
|
|
do {
|
|
if (node.nodeType === 3) {
|
|
node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
|
|
}
|
|
|
|
node = node.firstChild;
|
|
} while (node);
|
|
}
|
|
|
|
function getEditableRoot(node) {
|
|
var root = dom.getRoot(), parent, editableRoot;
|
|
|
|
// Get all parents until we hit a non editable parent or the root
|
|
parent = node;
|
|
while (parent !== root && dom.getContentEditable(parent) !== "false") {
|
|
if (dom.getContentEditable(parent) === "true") {
|
|
editableRoot = parent;
|
|
}
|
|
|
|
parent = parent.parentNode;
|
|
}
|
|
|
|
return parent !== root ? editableRoot : root;
|
|
}
|
|
|
|
// Adds a BR at the end of blocks that only contains an IMG or INPUT since
|
|
// these might be floated and then they won't expand the block
|
|
function addBrToBlockIfNeeded(block) {
|
|
var lastChild;
|
|
|
|
// IE will render the blocks correctly other browsers needs a BR
|
|
if (!isIE) {
|
|
block.normalize(); // Remove empty text nodes that got left behind by the extract
|
|
|
|
// Check if the block is empty or contains a floated last child
|
|
lastChild = block.lastChild;
|
|
if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) {
|
|
dom.add(block, 'br');
|
|
}
|
|
}
|
|
}
|
|
|
|
function insertNewBlockAfter() {
|
|
// If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup
|
|
if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') {
|
|
newBlock = createNewBlock(newBlockName);
|
|
} else {
|
|
newBlock = createNewBlock();
|
|
}
|
|
|
|
// Split the current container block element if enter is pressed inside an empty inner block element
|
|
if (settings.end_container_on_empty_block && canSplitBlock(dom, containerBlock) && dom.isEmpty(parentBlock)) {
|
|
// Split container block for example a BLOCKQUOTE at the current blockParent location for example a P
|
|
newBlock = dom.split(containerBlock, parentBlock);
|
|
} else {
|
|
dom.insertAfter(newBlock, parentBlock);
|
|
}
|
|
|
|
moveToCaretPosition(newBlock);
|
|
}
|
|
|
|
rng = selection.getRng(true);
|
|
|
|
// Event is blocked by some other handler for example the lists plugin
|
|
if (evt.isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
// Delete any selected contents
|
|
if (!rng.collapsed) {
|
|
editor.execCommand('Delete');
|
|
return;
|
|
}
|
|
|
|
// Setup range items and newBlockName
|
|
new RangeUtils(dom).normalize(rng);
|
|
container = rng.startContainer;
|
|
offset = rng.startOffset;
|
|
newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
|
|
newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
|
|
documentMode = dom.doc.documentMode;
|
|
shiftKey = evt.shiftKey;
|
|
|
|
// Resolve node index
|
|
if (container.nodeType == 1 && container.hasChildNodes()) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Get editable root node, normally the body element but sometimes a div or span
|
|
editableRoot = getEditableRoot(container);
|
|
|
|
// If there is no editable root then enter is done inside a contentEditable false element
|
|
if (!editableRoot) {
|
|
return;
|
|
}
|
|
|
|
undoManager.beforeChange();
|
|
|
|
// If editable root isn't block nor the root of the editor
|
|
if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
|
|
if (!newBlockName || shiftKey) {
|
|
insertBr();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Wrap the current node and it's sibling in a default block if it's needed.
|
|
// for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
|
|
// This won't happen if root blocks are disabled or the shiftKey is pressed
|
|
if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
|
|
container = wrapSelfAndSiblingsInDefaultBlock(container, offset);
|
|
}
|
|
|
|
// Find parent block and setup empty block paddings
|
|
parentBlock = dom.getParent(container, dom.isBlock);
|
|
containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
|
|
|
|
// Setup block names
|
|
parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
|
|
containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
|
|
|
|
// Enter inside block contained within a LI then split or insert before/after LI
|
|
if (containerBlockName == 'LI' && !evt.ctrlKey) {
|
|
parentBlock = containerBlock;
|
|
parentBlockName = containerBlockName;
|
|
}
|
|
|
|
if (editor.undoManager.typing) {
|
|
editor.undoManager.typing = false;
|
|
editor.undoManager.add();
|
|
}
|
|
|
|
// Handle enter in list item
|
|
if (/^(LI|DT|DD)$/.test(parentBlockName)) {
|
|
if (!newBlockName && shiftKey) {
|
|
insertBr();
|
|
return;
|
|
}
|
|
|
|
// Handle enter inside an empty list item
|
|
if (dom.isEmpty(parentBlock)) {
|
|
handleEmptyListItem();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Don't split PRE tags but insert a BR instead easier when writing code samples etc
|
|
if (parentBlockName == 'PRE' && settings.br_in_pre !== false) {
|
|
if (!shiftKey) {
|
|
insertBr();
|
|
return;
|
|
}
|
|
} else {
|
|
// If no root block is configured then insert a BR by default or if the shiftKey is pressed
|
|
if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) {
|
|
insertBr();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If parent block is root then never insert new blocks
|
|
if (newBlockName && parentBlock === editor.getBody()) {
|
|
return;
|
|
}
|
|
|
|
// Default block name if it's not configured
|
|
newBlockName = newBlockName || 'P';
|
|
|
|
// Insert new block before/after the parent block depending on caret location
|
|
if (CaretContainer.isCaretContainerBlock(parentBlock)) {
|
|
newBlock = CaretContainer.showCaretContainerBlock(parentBlock);
|
|
if (dom.isEmpty(parentBlock)) {
|
|
emptyBlock(parentBlock);
|
|
}
|
|
moveToCaretPosition(newBlock);
|
|
} else if (isCaretAtStartOrEndOfBlock()) {
|
|
insertNewBlockAfter();
|
|
} else if (isCaretAtStartOrEndOfBlock(true)) {
|
|
// Insert new block before
|
|
newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
|
|
renderBlockOnIE(dom, selection, newBlock);
|
|
moveToCaretPosition(parentBlock);
|
|
} else {
|
|
// Extract after fragment and insert it after the current block
|
|
tmpRng = includeZwspInRange(rng).cloneRange();
|
|
tmpRng.setEndAfter(parentBlock);
|
|
fragment = tmpRng.extractContents();
|
|
trimLeadingLineBreaks(fragment);
|
|
newBlock = fragment.firstChild;
|
|
dom.insertAfter(fragment, parentBlock);
|
|
trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock);
|
|
addBrToBlockIfNeeded(parentBlock);
|
|
|
|
if (dom.isEmpty(parentBlock)) {
|
|
emptyBlock(parentBlock);
|
|
}
|
|
|
|
newBlock.normalize();
|
|
|
|
// New block might become empty if it's <p><b>a |</b></p>
|
|
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('<div class="mce-visual-caret" data-mce-bogus="all"></div>').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 BR <p>abc|<br></p>
|
|
if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) {
|
|
return true;
|
|
}
|
|
|
|
return inSameBlock;
|
|
}
|
|
|
|
function getNormalizedRangeEndPoint(direction, range) {
|
|
range = CaretUtils.normalizeRange(direction, rootNode, range);
|
|
|
|
if (direction == -1) {
|
|
return CaretPosition.fromRangeStart(range);
|
|
}
|
|
|
|
return CaretPosition.fromRangeEnd(range);
|
|
}
|
|
|
|
function isRangeInCaretContainerBlock(range) {
|
|
return CaretContainer.isCaretContainerBlock(range.startContainer);
|
|
}
|
|
|
|
function moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
|
|
var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;
|
|
|
|
if (!range.collapsed) {
|
|
node = getSelectedNode(range);
|
|
if (isContentEditableFalse(node)) {
|
|
return showCaret(direction, node, direction == -1);
|
|
}
|
|
}
|
|
|
|
rangeIsInContainerBlock = isRangeInCaretContainerBlock(range);
|
|
caretPosition = getNormalizedRangeEndPoint(direction, range);
|
|
|
|
if (isBeforeContentEditableFalseFn(caretPosition)) {
|
|
return selectNode(caretPosition.getNode(direction == -1));
|
|
}
|
|
|
|
caretPosition = getNextPosFn(caretPosition);
|
|
if (!caretPosition) {
|
|
if (rangeIsInContainerBlock) {
|
|
return range;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (isBeforeContentEditableFalseFn(caretPosition)) {
|
|
return showCaret(direction, caretPosition.getNode(direction == -1), direction == 1);
|
|
}
|
|
|
|
// Peek ahead for handling of ab|c<span cE=false> -> abc|<span cE=false>
|
|
peekCaretPosition = getNextPosFn(caretPosition);
|
|
if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
|
|
if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
|
|
return showCaret(direction, peekCaretPosition.getNode(direction == -1), direction == 1);
|
|
}
|
|
}
|
|
|
|
if (rangeIsInContainerBlock) {
|
|
return renderRangeCaret(caretPosition.toRange());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function moveToCeFalseVertically(direction, walkerFn, range) {
|
|
var caretPosition, linePositions, nextLinePositions,
|
|
closestNextLineRect, caretClientRect, clientX,
|
|
dist1, dist2, contentEditableFalseNode;
|
|
|
|
contentEditableFalseNode = getSelectedNode(range);
|
|
caretPosition = getNormalizedRangeEndPoint(direction, range);
|
|
linePositions = walkerFn(rootNode, LineWalker.isAboveLine(1), caretPosition);
|
|
nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
|
|
caretClientRect = Arr.last(caretPosition.getClientRects());
|
|
|
|
if (isBeforeContentEditableFalse(caretPosition)) {
|
|
contentEditableFalseNode = caretPosition.getNode();
|
|
}
|
|
|
|
if (isAfterContentEditableFalse(caretPosition)) {
|
|
contentEditableFalseNode = caretPosition.getNode(true);
|
|
}
|
|
|
|
if (!caretClientRect) {
|
|
return null;
|
|
}
|
|
|
|
clientX = caretClientRect.left;
|
|
|
|
closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
|
|
if (closestNextLineRect) {
|
|
if (isContentEditableFalse(closestNextLineRect.node)) {
|
|
dist1 = Math.abs(clientX - closestNextLineRect.left);
|
|
dist2 = Math.abs(clientX - closestNextLineRect.right);
|
|
|
|
return showCaret(direction, closestNextLineRect.node, dist1 < dist2);
|
|
}
|
|
}
|
|
|
|
if (contentEditableFalseNode) {
|
|
var caretPositions = LineWalker.positionsUntil(direction, rootNode, LineWalker.isAboveLine(1), contentEditableFalseNode);
|
|
|
|
closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
|
|
if (closestNextLineRect) {
|
|
return renderRangeCaret(closestNextLineRect.position.toRange());
|
|
}
|
|
|
|
closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
|
|
if (closestNextLineRect) {
|
|
return renderRangeCaret(closestNextLineRect.position.toRange());
|
|
}
|
|
}
|
|
}
|
|
|
|
function exitPreBlock(direction, range) {
|
|
var pre, caretPos, newBlock;
|
|
|
|
function createTextBlock() {
|
|
var textBlock = editor.dom.create(editor.settings.forced_root_block);
|
|
|
|
if (!Env.ie || Env.ie >= 11) {
|
|
textBlock.innerHTML = '<br data-mce-bogus="1">';
|
|
}
|
|
|
|
return textBlock;
|
|
}
|
|
|
|
if (range.collapsed && editor.settings.forced_root_block) {
|
|
pre = editor.dom.getParent(range.startContainer, 'PRE');
|
|
if (!pre) {
|
|
return;
|
|
}
|
|
|
|
if (direction == 1) {
|
|
caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
|
|
} else {
|
|
caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
|
|
}
|
|
|
|
if (!caretPos) {
|
|
newBlock = createTextBlock();
|
|
|
|
if (direction == 1) {
|
|
editor.$(pre).after(newBlock);
|
|
} else {
|
|
editor.$(pre).before(newBlock);
|
|
}
|
|
|
|
editor.selection.select(newBlock, true);
|
|
editor.selection.collapse();
|
|
}
|
|
}
|
|
}
|
|
|
|
function moveH(direction, getNextPosFn, isBeforeContentEditableFalseFn, range) {
|
|
var newRange;
|
|
|
|
newRange = moveToCeFalseHorizontally(direction, getNextPosFn, isBeforeContentEditableFalseFn, range);
|
|
if (newRange) {
|
|
return newRange;
|
|
}
|
|
|
|
newRange = exitPreBlock(direction, range);
|
|
if (newRange) {
|
|
return newRange;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function moveV(direction, walkerFn, range) {
|
|
var newRange;
|
|
|
|
newRange = moveToCeFalseVertically(direction, walkerFn, range);
|
|
if (newRange) {
|
|
return newRange;
|
|
}
|
|
|
|
newRange = exitPreBlock(direction, range);
|
|
if (newRange) {
|
|
return newRange;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function showBlockCaretContainer(blockCaretContainer) {
|
|
if (blockCaretContainer.hasAttribute('data-mce-caret')) {
|
|
CaretContainer.showCaretContainerBlock(blockCaretContainer);
|
|
setRange(getRange()); // Removes control rect on IE
|
|
scrollIntoView(blockCaretContainer[0]);
|
|
}
|
|
}
|
|
|
|
function renderCaretAtRange(range) {
|
|
var caretPosition, ceRoot;
|
|
|
|
range = CaretUtils.normalizeRange(1, rootNode, range);
|
|
caretPosition = CaretPosition.fromRangeStart(range);
|
|
|
|
if (isContentEditableFalse(caretPosition.getNode())) {
|
|
return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
|
|
}
|
|
|
|
if (isContentEditableFalse(caretPosition.getNode(true))) {
|
|
return showCaret(1, caretPosition.getNode(true), false);
|
|
}
|
|
|
|
// TODO: Should render caret before/after depending on where you click on the page forces after now
|
|
ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue));
|
|
if (isContentEditableFalse(ceRoot)) {
|
|
return showCaret(1, ceRoot, false);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function renderRangeCaret(range) {
|
|
var caretRange;
|
|
|
|
if (!range || !range.collapsed) {
|
|
return range;
|
|
}
|
|
|
|
caretRange = renderCaretAtRange(range);
|
|
if (caretRange) {
|
|
return caretRange;
|
|
}
|
|
|
|
return range;
|
|
}
|
|
|
|
function registerEvents() {
|
|
var right = curry(moveH, 1, getNextVisualCaretPosition, isBeforeContentEditableFalse);
|
|
var left = curry(moveH, -1, getPrevVisualCaretPosition, isAfterContentEditableFalse);
|
|
var up = curry(moveV, -1, LineWalker.upUntil);
|
|
var down = curry(moveV, 1, LineWalker.downUntil);
|
|
|
|
function override(evt, moveFn) {
|
|
if (evt.isDefaultPrevented() === false) {
|
|
var range = moveFn(getRange());
|
|
|
|
if (range) {
|
|
evt.preventDefault();
|
|
setRange(range);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getContentEditableRoot(node) {
|
|
var root = editor.getBody();
|
|
|
|
while (node && node != root) {
|
|
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
|
|
return node;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function isXYWithinRange(clientX, clientY, range) {
|
|
if (range.collapsed) {
|
|
return false;
|
|
}
|
|
|
|
return Arr.reduce(range.getClientRects(), function (state, rect) {
|
|
return state || ClientRect.containsXY(rect, clientX, clientY);
|
|
}, false);
|
|
}
|
|
|
|
// Some browsers (Chrome) lets you place the caret after a cE=false
|
|
// Make sure we render the caret container in this case
|
|
editor.on('mouseup', function () {
|
|
var range = getRange();
|
|
|
|
if (range.collapsed) {
|
|
setRange(renderCaretAtRange(range));
|
|
}
|
|
});
|
|
|
|
editor.on('click', function (e) {
|
|
var contentEditableRoot;
|
|
|
|
contentEditableRoot = getContentEditableRoot(e.target);
|
|
if (contentEditableRoot) {
|
|
// Prevent clicks on links in a cE=false element
|
|
if (isContentEditableFalse(contentEditableRoot)) {
|
|
e.preventDefault();
|
|
editor.focus();
|
|
}
|
|
|
|
// Removes fake selection if a cE=true is clicked within a cE=false like the toc title
|
|
if (isContentEditableTrue(contentEditableRoot)) {
|
|
if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) {
|
|
removeContentEditableSelection();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
editor.on('blur NewBlock', function () {
|
|
removeContentEditableSelection();
|
|
hideFakeCaret();
|
|
});
|
|
|
|
function handleTouchSelect(editor) {
|
|
var moved = false;
|
|
|
|
editor.on('touchstart', function () {
|
|
moved = false;
|
|
});
|
|
|
|
editor.on('touchmove', function () {
|
|
moved = true;
|
|
});
|
|
|
|
editor.on('touchend', function (e) {
|
|
var contentEditableRoot = getContentEditableRoot(e.target);
|
|
|
|
if (isContentEditableFalse(contentEditableRoot)) {
|
|
if (!moved) {
|
|
e.preventDefault();
|
|
setContentEditableSelection(selectNode(contentEditableRoot));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var hasNormalCaretPosition = function (elm) {
|
|
var caretWalker = new CaretWalker(elm);
|
|
|
|
if (!elm.firstChild) {
|
|
return false;
|
|
}
|
|
|
|
var startPos = CaretPosition.before(elm.firstChild);
|
|
var newPos = caretWalker.next(startPos);
|
|
|
|
return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
|
|
};
|
|
|
|
var isInSameBlock = function (node1, node2) {
|
|
var block1 = editor.dom.getParent(node1, editor.dom.isBlock);
|
|
var block2 = editor.dom.getParent(node2, editor.dom.isBlock);
|
|
return block1 === block2;
|
|
};
|
|
|
|
var isContentKey = function (e) {
|
|
if (e.keyCode >= 112 && e.keyCode <= 123) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
// Checks if the target node is in a block and if that block has a caret position better than the
|
|
// suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if
|
|
// they are adjacent on the vertical axis
|
|
var hasBetterMouseTarget = function (targetNode, caretNode) {
|
|
var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
|
|
var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);
|
|
|
|
return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
|
|
};
|
|
|
|
handleTouchSelect(editor);
|
|
|
|
editor.on('mousedown', function (e) {
|
|
var contentEditableRoot;
|
|
|
|
contentEditableRoot = getContentEditableRoot(e.target);
|
|
if (contentEditableRoot) {
|
|
if (isContentEditableFalse(contentEditableRoot)) {
|
|
e.preventDefault();
|
|
setContentEditableSelection(selectNode(contentEditableRoot));
|
|
} else {
|
|
if (!isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) {
|
|
editor.selection.placeCaretAt(e.clientX, e.clientY);
|
|
}
|
|
}
|
|
} else {
|
|
// Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng
|
|
// and therefore not fire the AfterSetSelectionRange event.
|
|
removeContentEditableSelection();
|
|
hideFakeCaret();
|
|
|
|
var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
|
|
if (caretInfo) {
|
|
if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
|
|
e.preventDefault();
|
|
editor.getBody().focus();
|
|
setRange(showCaret(1, caretInfo.node, caretInfo.before));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
editor.on('keydown', function (e) {
|
|
if (VK.modifierPressed(e)) {
|
|
return;
|
|
}
|
|
|
|
switch (e.keyCode) {
|
|
case VK.RIGHT:
|
|
override(e, right);
|
|
break;
|
|
|
|
case VK.DOWN:
|
|
override(e, down);
|
|
break;
|
|
|
|
case VK.LEFT:
|
|
override(e, left);
|
|
break;
|
|
|
|
case VK.UP:
|
|
override(e, up);
|
|
break;
|
|
|
|
default:
|
|
if (isContentEditableFalse(editor.selection.getNode()) && isContentKey(e)) {
|
|
e.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
editor.on('getSelectionRange', function (e) {
|
|
var rng = e.range;
|
|
|
|
if (selectedContentEditableNode) {
|
|
if (!selectedContentEditableNode.parentNode) {
|
|
selectedContentEditableNode = null;
|
|
return;
|
|
}
|
|
|
|
rng = rng.cloneRange();
|
|
rng.selectNode(selectedContentEditableNode);
|
|
e.range = rng;
|
|
}
|
|
});
|
|
|
|
editor.on('setSelectionRange', function (e) {
|
|
var rng;
|
|
|
|
rng = setContentEditableSelection(e.range);
|
|
if (rng) {
|
|
e.range = rng;
|
|
}
|
|
});
|
|
|
|
editor.on('AfterSetSelectionRange', function (e) {
|
|
var rng = e.range;
|
|
|
|
if (!isRangeInCaretContainer(rng)) {
|
|
hideFakeCaret();
|
|
}
|
|
|
|
if (!isFakeSelectionElement(rng.startContainer.parentNode)) {
|
|
removeContentEditableSelection();
|
|
}
|
|
});
|
|
|
|
editor.on('focus', function () {
|
|
// Make sure we have a proper fake caret on focus
|
|
Delay.setEditorTimeout(editor, function () {
|
|
editor.selection.setRng(renderRangeCaret(editor.selection.getRng()));
|
|
}, 0);
|
|
});
|
|
|
|
editor.on('copy', function (e) {
|
|
var clipboardData = e.clipboardData;
|
|
|
|
// Make sure we get proper html/text for the fake cE=false selection
|
|
// Doesn't work at all on Edge since it doesn't have proper clipboardData support
|
|
if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) {
|
|
var realSelectionElement = getRealSelectionElement();
|
|
if (realSelectionElement) {
|
|
e.preventDefault();
|
|
clipboardData.clearData();
|
|
clipboardData.setData('text/html', realSelectionElement.outerHTML);
|
|
clipboardData.setData('text/plain', realSelectionElement.outerText);
|
|
}
|
|
}
|
|
});
|
|
|
|
DragDropOverrides.init(editor);
|
|
}
|
|
|
|
function addCss() {
|
|
var styles = editor.contentStyles, rootClass = '.mce-content-body';
|
|
|
|
styles.push(fakeCaret.getCss());
|
|
styles.push(
|
|
rootClass + ' .mce-offscreen-selection {' +
|
|
'position: absolute;' +
|
|
'left: -9999999999px;' +
|
|
'max-width: 1000000px;' +
|
|
'}' +
|
|
rootClass + ' *[contentEditable=false] {' +
|
|
'cursor: default;' +
|
|
'}' +
|
|
rootClass + ' *[contentEditable=true] {' +
|
|
'cursor: text;' +
|
|
'}'
|
|
);
|
|
}
|
|
|
|
function isWithinCaretContainer(node) {
|
|
return (
|
|
CaretContainer.isCaretContainer(node) ||
|
|
CaretContainer.startsWithCaretContainer(node) ||
|
|
CaretContainer.endsWithCaretContainer(node)
|
|
);
|
|
}
|
|
|
|
function isRangeInCaretContainer(rng) {
|
|
return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer);
|
|
}
|
|
|
|
function setContentEditableSelection(range) {
|
|
var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel,
|
|
startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone;
|
|
|
|
if (!range) {
|
|
return null;
|
|
}
|
|
|
|
if (range.collapsed) {
|
|
if (!isRangeInCaretContainer(range)) {
|
|
caretPosition = getNormalizedRangeEndPoint(1, range);
|
|
|
|
if (isContentEditableFalse(caretPosition.getNode())) {
|
|
return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd());
|
|
}
|
|
|
|
if (isContentEditableFalse(caretPosition.getNode(true))) {
|
|
return showCaret(1, caretPosition.getNode(true), false);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
startContainer = range.startContainer;
|
|
startOffset = range.startOffset;
|
|
endOffset = range.endOffset;
|
|
|
|
// Normalizes <span cE=false>[</span>] to [<span cE=false></span>]
|
|
if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) {
|
|
startContainer = startContainer.parentNode;
|
|
startOffset = dom.nodeIndex(startContainer);
|
|
startContainer = startContainer.parentNode;
|
|
}
|
|
|
|
if (startContainer.nodeType != 1) {
|
|
return null;
|
|
}
|
|
|
|
if (endOffset == startOffset + 1) {
|
|
node = startContainer.childNodes[startOffset];
|
|
}
|
|
|
|
if (!isContentEditableFalse(node)) {
|
|
return null;
|
|
}
|
|
|
|
targetClone = origTargetClone = node.cloneNode(true);
|
|
e = editor.fire('ObjectSelected', { target: node, targetClone: targetClone });
|
|
if (e.isDefaultPrevented()) {
|
|
return null;
|
|
}
|
|
|
|
targetClone = e.targetClone;
|
|
$realSelectionContainer = $('#' + realSelectionId);
|
|
if ($realSelectionContainer.length === 0) {
|
|
$realSelectionContainer = $(
|
|
'<div data-mce-bogus="all" class="mce-offscreen-selection"></div>'
|
|
).attr('id', realSelectionId);
|
|
|
|
$realSelectionContainer.appendTo(editor.getBody());
|
|
}
|
|
|
|
range = editor.dom.createRng();
|
|
|
|
// WHY is IE making things so hard! Copy on <i contentEditable="false">x</i> produces: <em>x</em>
|
|
// This is a ridiculous hack where we place the selection from a block over the inline element
|
|
// so that just the inline element is copied as is and not converted.
|
|
if (targetClone === origTargetClone && Env.ie) {
|
|
$realSelectionContainer.empty().append('<p style="font-size: 0" data-mce-bogus="all">\u00a0</p>').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:
|
|
* <p><b>|</b></p>
|
|
*
|
|
* Or:
|
|
* <h1>|</h1>
|
|
*
|
|
* Or:
|
|
* [<h1></h1>]
|
|
*/
|
|
function emptyEditorWhenDeleting() {
|
|
function serializeRng(rng) {
|
|
var body = dom.create("body");
|
|
var contents = rng.cloneContents();
|
|
body.appendChild(contents);
|
|
return selection.serializer.serialize(body, { format: 'html' });
|
|
}
|
|
|
|
function allContentsSelected(rng) {
|
|
if (!rng.setStart) {
|
|
if (rng.item) {
|
|
return false;
|
|
}
|
|
|
|
var bodyRng = rng.duplicate();
|
|
bodyRng.moveToElementText(editor.getBody());
|
|
return RangeUtils.compareRanges(rng, bodyRng);
|
|
}
|
|
|
|
var selection = serializeRng(rng);
|
|
|
|
var allRng = dom.createRng();
|
|
allRng.selectNode(editor.getBody());
|
|
|
|
var allSelection = serializeRng(allRng);
|
|
return selection === allSelection;
|
|
}
|
|
|
|
editor.on('keydown', function (e) {
|
|
var keyCode = e.keyCode, isCollapsed, body;
|
|
|
|
// Empty the editor if it's needed for example backspace at <p><b>|</b></p>
|
|
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 <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> 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:
|
|
* <p>bla[ck</p><p style="color:red">r]ed</p>
|
|
*
|
|
* Would become:
|
|
* <p>bla|ed</p>
|
|
*
|
|
* Instead of:
|
|
* <p style="color:red">bla|ed</p>
|
|
*/
|
|
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:
|
|
* <blockquote><p>|x</p></blockquote>
|
|
*
|
|
* Becomes:
|
|
* <p>|x</p>
|
|
*/
|
|
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:
|
|
* <p><b><a href="#">x</a></b></p>
|
|
*
|
|
* Becomes this:
|
|
* <p><b><a href="#">x</a></b><br></p>
|
|
*/
|
|
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 <b>a</b>|<b>b</b>
|
|
* this fix will lean the caret right into the closest inline element.
|
|
*/
|
|
function normalizeSelection() {
|
|
// Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> 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: <form><button></form>
|
|
*/
|
|
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 '<!--mce:protected ' + escape(str) + '-->';
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
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(/^(<p[^>]*>( | |\s|\u00a0|<br \/>|)<\/p>[\r\n]*|<br \/>[\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 + '<html><head>';
|
|
|
|
// We only need to override paths if we have to
|
|
// IE has a bug where it remove site absolute urls to relative ones if this is specified
|
|
if (settings.document_base_url != editor.documentBaseUrl) {
|
|
editor.iframeHTML += '<base href="' + editor.documentBaseURI.getURI() + '" />';
|
|
}
|
|
|
|
// IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode.
|
|
if (!Env.caretAfter && settings.ie7_compat) {
|
|
editor.iframeHTML += '<meta http-equiv="X-UA-Compatible" content="IE=7" />';
|
|
}
|
|
|
|
editor.iframeHTML += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
|
|
|
|
bodyId = settings.body_id || 'tinymce';
|
|
if (bodyId.indexOf('=') != -1) {
|
|
bodyId = editor.getParam('body_id', '', 'hash');
|
|
bodyId = bodyId[editor.id] || bodyId;
|
|
}
|
|
|
|
bodyClass = settings.body_class || '';
|
|
if (bodyClass.indexOf('=') != -1) {
|
|
bodyClass = editor.getParam('body_class', '', 'hash');
|
|
bodyClass = bodyClass[editor.id] || '';
|
|
}
|
|
|
|
if (settings.content_security_policy) {
|
|
editor.iframeHTML += '<meta http-equiv="Content-Security-Policy" content="' + settings.content_security_policy + '" />';
|
|
}
|
|
|
|
editor.iframeHTML += '</head><body id="' + bodyId +
|
|
'" class="mce-content-body ' + bodyClass +
|
|
'" data-id="' + editor.id + '"><br></body></html>';
|
|
|
|
var bodyUuid = Uuid.uuid('mce');
|
|
|
|
editor[bodyUuid] = function () {
|
|
InitContentBody.initContentBody(editor);
|
|
};
|
|
|
|
/*eslint no-script-url:0 */
|
|
var domainRelaxUrl = 'javascript:(function(){' +
|
|
'document.open();document.domain="' + document.domain + '";' +
|
|
'var ed = window.parent.tinymce.get("' + editor.id + '");document.write(ed.iframeHTML);' +
|
|
'document.close();ed.' + bodyUuid + '(true);})()';
|
|
|
|
// Domain relaxing is required since the user has messed around with document.domain
|
|
if (document.domain != window.location.hostname) {
|
|
// Edge seems to be able to handle domain relaxing
|
|
if (Env.ie && Env.ie < 12) {
|
|
url = domainRelaxUrl;
|
|
}
|
|
}
|
|
|
|
// Create iframe
|
|
// TODO: ACC add the appropriate description on this.
|
|
var ifr = DOM.create('iframe', {
|
|
id: editor.id + "_ifr",
|
|
//src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
|
|
frameBorder: '0',
|
|
allowTransparency: "true",
|
|
title: editor.editorManager.translate(
|
|
"Rich Text Area. Press ALT-F9 for menu. " +
|
|
"Press ALT-F10 for toolbar. Press ALT-0 for help"
|
|
),
|
|
style: {
|
|
width: '100%',
|
|
height: o.height,
|
|
display: 'block' // Important for Gecko to render the iframe correctly
|
|
}
|
|
});
|
|
|
|
ifr.onload = function () {
|
|
ifr.onload = null;
|
|
editor.fire("load");
|
|
};
|
|
|
|
DOM.setAttrib(ifr, "src", url || 'javascript:""');
|
|
|
|
editor.contentAreaContainer = o.iframeContainer;
|
|
editor.iframeElement = ifr;
|
|
|
|
DOM.add(o.iframeContainer, ifr);
|
|
|
|
// Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
|
|
// Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
|
|
if (Env.ie) {
|
|
try {
|
|
editor.getDoc();
|
|
} catch (e) {
|
|
ifr.src = url = domainRelaxUrl;
|
|
}
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
var init = function (editor) {
|
|
var settings = editor.settings, elm = editor.getElement();
|
|
var boxInfo, url;
|
|
|
|
editor.rtl = settings.rtl_ui || editor.editorManager.i18n.rtl;
|
|
editor.editorManager.i18n.setCode(settings.language);
|
|
settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', editor.getLang('aria.rich_text_area'));
|
|
|
|
editor.fire('ScriptsLoaded');
|
|
|
|
initTheme(editor);
|
|
initPlugins(editor);
|
|
boxInfo = measueBox(editor);
|
|
|
|
// Load specified content CSS last
|
|
if (settings.content_css) {
|
|
Tools.each(Tools.explode(settings.content_css), function (u) {
|
|
editor.contentCSS.push(editor.documentBaseURI.toAbsolute(u));
|
|
});
|
|
}
|
|
|
|
// Load specified content CSS last
|
|
if (settings.content_style) {
|
|
editor.contentStyles.push(settings.content_style);
|
|
}
|
|
|
|
// Content editable mode ends here
|
|
if (settings.content_editable) {
|
|
return InitContentBody.initContentBody(editor);
|
|
}
|
|
|
|
url = createIframe(editor, boxInfo);
|
|
|
|
if (boxInfo.editorContainer) {
|
|
DOM.get(boxInfo.editorContainer).style.display = editor.orgDisplay;
|
|
editor.hidden = DOM.isHidden(boxInfo.editorContainer);
|
|
}
|
|
|
|
editor.getElement().style.display = 'none';
|
|
DOM.setAttrib(editor.id, 'aria-hidden', true);
|
|
|
|
if (!url) {
|
|
InitContentBody.initContentBody(editor);
|
|
}
|
|
};
|
|
|
|
return {
|
|
init: init
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Render.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.Render',
|
|
[
|
|
'global!window',
|
|
'tinymce.core.dom.DOMUtils',
|
|
'tinymce.core.dom.EventUtils',
|
|
'tinymce.core.dom.ScriptLoader',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.ErrorReporter',
|
|
'tinymce.core.init.Init',
|
|
'tinymce.core.NotificationManager',
|
|
'tinymce.core.PluginManager',
|
|
'tinymce.core.ThemeManager',
|
|
'tinymce.core.util.Tools',
|
|
'tinymce.core.WindowManager'
|
|
],
|
|
function (window, DOMUtils, EventUtils, ScriptLoader, Env, ErrorReporter, Init, NotificationManager, PluginManager, ThemeManager, Tools, WindowManager) {
|
|
var DOM = DOMUtils.DOM;
|
|
|
|
var loadScripts = function (editor, suffix) {
|
|
var settings = editor.settings, scriptLoader = ScriptLoader.ScriptLoader;
|
|
|
|
if (settings.language && settings.language != 'en' && !settings.language_url) {
|
|
settings.language_url = editor.editorManager.baseURL + '/langs/' + settings.language + '.js';
|
|
}
|
|
|
|
if (settings.language_url) {
|
|
scriptLoader.add(settings.language_url);
|
|
}
|
|
|
|
if (settings.theme && typeof settings.theme != "function" &&
|
|
settings.theme.charAt(0) != '-' && !ThemeManager.urls[settings.theme]) {
|
|
var themeUrl = settings.theme_url;
|
|
|
|
if (themeUrl) {
|
|
themeUrl = editor.documentBaseURI.toAbsolute(themeUrl);
|
|
} else {
|
|
themeUrl = 'themes/' + settings.theme + '/theme' + suffix + '.js';
|
|
}
|
|
|
|
ThemeManager.load(settings.theme, themeUrl);
|
|
}
|
|
|
|
if (Tools.isArray(settings.plugins)) {
|
|
settings.plugins = settings.plugins.join(' ');
|
|
}
|
|
|
|
Tools.each(settings.external_plugins, function (url, name) {
|
|
PluginManager.load(name, url);
|
|
settings.plugins += ' ' + name;
|
|
});
|
|
|
|
Tools.each(settings.plugins.split(/[ ,]/), function (plugin) {
|
|
plugin = Tools.trim(plugin);
|
|
|
|
if (plugin && !PluginManager.urls[plugin]) {
|
|
if (plugin.charAt(0) === '-') {
|
|
plugin = plugin.substr(1, plugin.length);
|
|
|
|
var dependencies = PluginManager.dependencies(plugin);
|
|
|
|
Tools.each(dependencies, function (dep) {
|
|
var defaultSettings = {
|
|
prefix: 'plugins/',
|
|
resource: dep,
|
|
suffix: '/plugin' + suffix + '.js'
|
|
};
|
|
|
|
dep = PluginManager.createUrl(defaultSettings, dep);
|
|
PluginManager.load(dep.resource, dep);
|
|
});
|
|
} else {
|
|
PluginManager.load(plugin, {
|
|
prefix: 'plugins/',
|
|
resource: plugin,
|
|
suffix: '/plugin' + suffix + '.js'
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
scriptLoader.loadQueue(function () {
|
|
if (!editor.removed) {
|
|
Init.init(editor);
|
|
}
|
|
}, editor, function (urls) {
|
|
ErrorReporter.pluginLoadError(editor, urls[0]);
|
|
|
|
if (!editor.removed) {
|
|
Init.init(editor);
|
|
}
|
|
});
|
|
};
|
|
|
|
var render = function (editor) {
|
|
var settings = editor.settings, id = editor.id;
|
|
|
|
function readyHandler() {
|
|
DOM.unbind(window, 'ready', readyHandler);
|
|
editor.render();
|
|
}
|
|
|
|
// Page is not loaded yet, wait for it
|
|
if (!EventUtils.Event.domLoaded) {
|
|
DOM.bind(window, 'ready', readyHandler);
|
|
return;
|
|
}
|
|
|
|
// Element not found, then skip initialization
|
|
if (!editor.getElement()) {
|
|
return;
|
|
}
|
|
|
|
// No editable support old iOS versions etc
|
|
if (!Env.contentEditable) {
|
|
return;
|
|
}
|
|
|
|
// Hide target element early to prevent content flashing
|
|
if (!settings.inline) {
|
|
editor.orgVisibility = editor.getElement().style.visibility;
|
|
editor.getElement().style.visibility = 'hidden';
|
|
} else {
|
|
editor.inline = true;
|
|
}
|
|
|
|
var form = editor.getElement().form || DOM.getParent(id, 'form');
|
|
if (form) {
|
|
editor.formElement = form;
|
|
|
|
// Add hidden input for non input elements inside form elements
|
|
if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(editor.getElement().nodeName)) {
|
|
DOM.insertAfter(DOM.create('input', { type: 'hidden', name: id }), id);
|
|
editor.hasHiddenInput = true;
|
|
}
|
|
|
|
// Pass submit/reset from form to editor instance
|
|
editor.formEventDelegate = function (e) {
|
|
editor.fire(e.type, e);
|
|
};
|
|
|
|
DOM.bind(form, 'submit reset', editor.formEventDelegate);
|
|
|
|
// Reset contents in editor when the form is reset
|
|
editor.on('reset', function () {
|
|
editor.setContent(editor.startContent, { format: 'raw' });
|
|
});
|
|
|
|
// Check page uses id="submit" or name="submit" for it's submit button
|
|
if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) {
|
|
form._mceOldSubmit = form.submit;
|
|
form.submit = function () {
|
|
editor.editorManager.triggerSave();
|
|
editor.setDirty(false);
|
|
|
|
return form._mceOldSubmit(form);
|
|
};
|
|
}
|
|
}
|
|
|
|
editor.windowManager = new WindowManager(editor);
|
|
editor.notificationManager = new NotificationManager(editor);
|
|
|
|
if (settings.encoding === 'xml') {
|
|
editor.on('GetContent', function (e) {
|
|
if (e.save) {
|
|
e.content = DOM.encode(e.content);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (settings.add_form_submit_trigger) {
|
|
editor.on('submit', function () {
|
|
if (editor.initialized) {
|
|
editor.save();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (settings.add_unload_trigger) {
|
|
editor._beforeUnload = function () {
|
|
if (editor.initialized && !editor.destroyed && !editor.isHidden()) {
|
|
editor.save({ format: 'raw', no_events: true, set_dirty: false });
|
|
}
|
|
};
|
|
|
|
editor.editorManager.on('BeforeUnload', editor._beforeUnload);
|
|
}
|
|
|
|
editor.editorManager.add(editor);
|
|
loadScripts(editor, editor.suffix);
|
|
};
|
|
|
|
return {
|
|
render: render
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Mode.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
|
|
*/
|
|
|
|
/**
|
|
* Mode switcher logic.
|
|
*
|
|
* @private
|
|
* @class tinymce.Mode
|
|
*/
|
|
define(
|
|
'tinymce.core.Mode',
|
|
[
|
|
],
|
|
function () {
|
|
function setEditorCommandState(editor, cmd, state) {
|
|
try {
|
|
editor.getDoc().execCommand(cmd, false, state);
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
function clickBlocker(editor) {
|
|
var target, handler;
|
|
|
|
target = editor.getBody();
|
|
|
|
handler = function (e) {
|
|
if (editor.dom.getParents(e.target, 'a').length > 0) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
editor.dom.bind(target, 'click', handler);
|
|
|
|
return {
|
|
unbind: function () {
|
|
editor.dom.unbind(target, 'click', handler);
|
|
}
|
|
};
|
|
}
|
|
|
|
function toggleReadOnly(editor, state) {
|
|
if (editor._clickBlocker) {
|
|
editor._clickBlocker.unbind();
|
|
editor._clickBlocker = null;
|
|
}
|
|
|
|
if (state) {
|
|
editor._clickBlocker = clickBlocker(editor);
|
|
editor.selection.controlSelection.hideResizeRect();
|
|
editor.readonly = true;
|
|
editor.getBody().contentEditable = false;
|
|
} else {
|
|
editor.readonly = false;
|
|
editor.getBody().contentEditable = true;
|
|
setEditorCommandState(editor, "StyleWithCSS", false);
|
|
setEditorCommandState(editor, "enableInlineTableEditing", false);
|
|
setEditorCommandState(editor, "enableObjectResizing", false);
|
|
editor.focus();
|
|
editor.nodeChanged();
|
|
}
|
|
}
|
|
|
|
function setMode(editor, mode) {
|
|
var currentMode = editor.readonly ? 'readonly' : 'design';
|
|
|
|
if (mode == currentMode) {
|
|
return;
|
|
}
|
|
|
|
if (editor.initialized) {
|
|
toggleReadOnly(editor, mode == 'readonly');
|
|
} else {
|
|
editor.on('init', function () {
|
|
toggleReadOnly(editor, mode == 'readonly');
|
|
});
|
|
}
|
|
|
|
// Event is NOT preventable
|
|
editor.fire('SwitchMode', { mode: mode });
|
|
}
|
|
|
|
return {
|
|
setMode: setMode
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* Sidebar.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 handle sidebar instances for the editor.
|
|
*
|
|
* @class tinymce.ui.Sidebar
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Sidebar',
|
|
[
|
|
],
|
|
function (
|
|
) {
|
|
var add = function (editor, name, settings) {
|
|
var sidebars = editor.sidebars ? editor.sidebars : [];
|
|
sidebars.push({ name: name, settings: settings });
|
|
editor.sidebars = sidebars;
|
|
};
|
|
|
|
return {
|
|
add: add
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Editor.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 scripturl:true */
|
|
|
|
/**
|
|
* Include the base event class documentation.
|
|
*
|
|
* @include ../../../../../tools/docs/tinymce.Event.js
|
|
*/
|
|
|
|
/**
|
|
* This class contains the core logic for a TinyMCE editor.
|
|
*
|
|
* @class tinymce.Editor
|
|
* @mixes tinymce.util.Observable
|
|
* @example
|
|
* // Add a class to all paragraphs in the editor.
|
|
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass');
|
|
*
|
|
* // Gets the current editors selection as text
|
|
* tinymce.activeEditor.selection.getContent({format: 'text'});
|
|
*
|
|
* // Creates a new editor instance
|
|
* var ed = new tinymce.Editor('textareaid', {
|
|
* some_setting: 1
|
|
* }, tinymce.EditorManager);
|
|
*
|
|
* ed.render();
|
|
*/
|
|
define(
|
|
'tinymce.core.Editor',
|
|
[
|
|
'tinymce.core.AddOnManager',
|
|
'tinymce.core.dom.DomQuery',
|
|
'tinymce.core.dom.DOMUtils',
|
|
'tinymce.core.EditorCommands',
|
|
'tinymce.core.EditorObservable',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.html.Serializer',
|
|
'tinymce.core.init.Render',
|
|
'tinymce.core.Mode',
|
|
'tinymce.core.Shortcuts',
|
|
'tinymce.core.ui.Sidebar',
|
|
'tinymce.core.util.Tools',
|
|
'tinymce.core.util.URI',
|
|
'tinymce.core.util.Uuid'
|
|
],
|
|
function (
|
|
AddOnManager, DomQuery, DOMUtils, EditorCommands, EditorObservable, Env, Serializer, Render, Mode,
|
|
Shortcuts, Sidebar, Tools, URI, Uuid
|
|
) {
|
|
// Shorten these names
|
|
var DOM = DOMUtils.DOM;
|
|
var extend = Tools.extend, each = Tools.each;
|
|
var trim = Tools.trim, resolve = Tools.resolve;
|
|
var isGecko = Env.gecko, ie = Env.ie;
|
|
|
|
/**
|
|
* Include Editor API docs.
|
|
*
|
|
* @include ../../../../../tools/docs/tinymce.Editor.js
|
|
*/
|
|
|
|
/**
|
|
* Constructs a editor instance by id.
|
|
*
|
|
* @constructor
|
|
* @method Editor
|
|
* @param {String} id Unique id for the editor.
|
|
* @param {Object} settings Settings for the editor.
|
|
* @param {tinymce.EditorManager} editorManager EditorManager instance.
|
|
*/
|
|
function Editor(id, settings, editorManager) {
|
|
var self = this, documentBaseUrl, baseUri, defaultSettings;
|
|
|
|
documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL;
|
|
baseUri = editorManager.baseURI;
|
|
defaultSettings = editorManager.defaultSettings;
|
|
|
|
/**
|
|
* Name/value collection with editor settings.
|
|
*
|
|
* @property settings
|
|
* @type Object
|
|
* @example
|
|
* // Get the value of the theme setting
|
|
* tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme");
|
|
*/
|
|
settings = extend({
|
|
id: id,
|
|
theme: 'modern',
|
|
delta_width: 0,
|
|
delta_height: 0,
|
|
popup_css: '',
|
|
plugins: '',
|
|
document_base_url: documentBaseUrl,
|
|
add_form_submit_trigger: true,
|
|
submit_patch: true,
|
|
add_unload_trigger: true,
|
|
convert_urls: true,
|
|
relative_urls: true,
|
|
remove_script_host: true,
|
|
object_resizing: true,
|
|
doctype: '<!DOCTYPE html>',
|
|
visual: true,
|
|
font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large',
|
|
|
|
// See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size
|
|
font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%',
|
|
forced_root_block: 'p',
|
|
hidden_input: true,
|
|
padd_empty_editor: true,
|
|
render_ui: true,
|
|
indentation: '30px',
|
|
inline_styles: true,
|
|
convert_fonts_to_spans: true,
|
|
indent: 'simple',
|
|
indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
|
|
'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
|
|
indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' +
|
|
'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
|
|
validate: true,
|
|
entity_encoding: 'named',
|
|
url_converter: self.convertURL,
|
|
url_converter_scope: self,
|
|
ie7_compat: true
|
|
}, defaultSettings, settings);
|
|
|
|
// Merge external_plugins
|
|
if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) {
|
|
settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins);
|
|
}
|
|
|
|
self.settings = settings;
|
|
AddOnManager.language = settings.language || 'en';
|
|
AddOnManager.languageLoad = settings.language_load;
|
|
AddOnManager.baseURL = editorManager.baseURL;
|
|
|
|
/**
|
|
* Editor instance id, normally the same as the div/textarea that was replaced.
|
|
*
|
|
* @property id
|
|
* @type String
|
|
*/
|
|
self.id = settings.id = id;
|
|
|
|
/**
|
|
* State to force the editor to return false on a isDirty call.
|
|
*
|
|
* @property isNotDirty
|
|
* @type Boolean
|
|
* @deprecated Use editor.setDirty instead.
|
|
*/
|
|
self.setDirty(false);
|
|
|
|
/**
|
|
* Name/Value object containing plugin instances.
|
|
*
|
|
* @property plugins
|
|
* @type Object
|
|
* @example
|
|
* // Execute a method inside a plugin directly
|
|
* tinymce.activeEditor.plugins.someplugin.someMethod();
|
|
*/
|
|
self.plugins = {};
|
|
|
|
/**
|
|
* URI object to document configured for the TinyMCE instance.
|
|
*
|
|
* @property documentBaseURI
|
|
* @type tinymce.util.URI
|
|
* @example
|
|
* // Get relative URL from the location of document_base_url
|
|
* tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm');
|
|
*
|
|
* // Get absolute URL from the location of document_base_url
|
|
* tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm');
|
|
*/
|
|
self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, {
|
|
base_uri: baseUri
|
|
});
|
|
|
|
/**
|
|
* URI object to current document that holds the TinyMCE editor instance.
|
|
*
|
|
* @property baseURI
|
|
* @type tinymce.util.URI
|
|
* @example
|
|
* // Get relative URL from the location of the API
|
|
* tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm');
|
|
*
|
|
* // Get absolute URL from the location of the API
|
|
* tinymce.activeEditor.baseURI.toAbsolute('somefile.htm');
|
|
*/
|
|
self.baseURI = baseUri;
|
|
|
|
/**
|
|
* Array with CSS files to load into the iframe.
|
|
*
|
|
* @property contentCSS
|
|
* @type Array
|
|
*/
|
|
self.contentCSS = [];
|
|
|
|
/**
|
|
* Array of CSS styles to add to head of document when the editor loads.
|
|
*
|
|
* @property contentStyles
|
|
* @type Array
|
|
*/
|
|
self.contentStyles = [];
|
|
|
|
// Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic
|
|
self.shortcuts = new Shortcuts(self);
|
|
self.loadedCSS = {};
|
|
self.editorCommands = new EditorCommands(self);
|
|
self.suffix = editorManager.suffix;
|
|
self.editorManager = editorManager;
|
|
self.inline = settings.inline;
|
|
self.settings.content_editable = self.inline;
|
|
|
|
if (settings.cache_suffix) {
|
|
Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, '');
|
|
}
|
|
|
|
if (settings.override_viewport === false) {
|
|
Env.overrideViewPort = false;
|
|
}
|
|
|
|
// Call setup
|
|
editorManager.fire('SetupEditor', self);
|
|
self.execCallback('setup', self);
|
|
|
|
/**
|
|
* Dom query instance with default scope to the editor document and default element is the body of the editor.
|
|
*
|
|
* @property $
|
|
* @type tinymce.dom.DomQuery
|
|
* @example
|
|
* tinymce.activeEditor.$('p').css('color', 'red');
|
|
* tinymce.activeEditor.$().append('<p>new</p>');
|
|
*/
|
|
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('<span>some</span> html');
|
|
*
|
|
* // Sets the raw contents of the activeEditor editor
|
|
* tinymce.activeEditor.setContent('<span>some</span> 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 ? '' : '<br data-mce-bogus="1">';
|
|
|
|
// Todo: There is a lot more root elements that need special padding
|
|
// so separate this and add all of them at some point.
|
|
if (body.nodeName == 'TABLE') {
|
|
content = '<tr><td>' + padd + '</td></tr>';
|
|
} else if (/^(UL|OL)$/.test(body.nodeName)) {
|
|
content = '<li>' + padd + '</li>';
|
|
}
|
|
|
|
forcedRootBlockName = self.settings.forced_root_block;
|
|
|
|
// Check if forcedRootBlock is configured and that the block is a valid child of the body
|
|
if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) {
|
|
// Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly
|
|
content = padd;
|
|
content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content);
|
|
} else if (!ie && !content) {
|
|
// We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
|
|
content = '<br data-mce-bogus="1">';
|
|
}
|
|
|
|
self.dom.setHTML(body, content);
|
|
|
|
self.fire('SetContent', args);
|
|
} else {
|
|
// Parse and serialize the html
|
|
if (args.format !== 'raw') {
|
|
content = new Serializer({
|
|
validate: self.validate
|
|
}, self.schema).serialize(
|
|
self.parser.parse(content, { isRootContent: true })
|
|
);
|
|
}
|
|
|
|
// Set the new cleaned contents to the editor
|
|
args.content = trim(content);
|
|
self.dom.setHTML(body, args.content);
|
|
|
|
// Do post processing
|
|
if (!args.no_events) {
|
|
self.fire('SetContent', args);
|
|
}
|
|
|
|
// Don't normalize selection if the focused element isn't the body in
|
|
// content editable mode since it will steal focus otherwise
|
|
/*if (!self.settings.content_editable || document.activeElement === self.getBody()) {
|
|
self.selection.normalize();
|
|
}*/
|
|
}
|
|
|
|
return args.content;
|
|
},
|
|
|
|
/**
|
|
* Gets the content from the editor instance, this will cleanup the content before it gets returned using
|
|
* the different cleanup rules options.
|
|
*
|
|
* @method getContent
|
|
* @param {Object} args Optional content object, this gets passed around through the whole get process.
|
|
* @return {String} Cleaned content string, normally HTML contents.
|
|
* @example
|
|
* // Get the HTML contents of the currently active editor
|
|
* console.debug(tinymce.activeEditor.getContent());
|
|
*
|
|
* // Get the raw contents of the currently active editor
|
|
* tinymce.activeEditor.getContent({format: 'raw'});
|
|
*
|
|
* // Get content of a specific editor:
|
|
* tinymce.get('content id').getContent()
|
|
*/
|
|
getContent: function (args) {
|
|
var self = this, content, body = self.getBody();
|
|
|
|
// Setup args object
|
|
args = args || {};
|
|
args.format = args.format || 'html';
|
|
args.get = true;
|
|
args.getInner = true;
|
|
|
|
// Do preprocessing
|
|
if (!args.no_events) {
|
|
self.fire('BeforeGetContent', args);
|
|
}
|
|
|
|
// Get raw contents or by default the cleaned contents
|
|
if (args.format == 'raw') {
|
|
content = Tools.trim(self.serializer.getTrimmedContent());
|
|
} else if (args.format == 'text') {
|
|
content = body.innerText || body.textContent;
|
|
} else {
|
|
content = self.serializer.serialize(body, args);
|
|
}
|
|
|
|
// Trim whitespace in beginning/end of HTML
|
|
if (args.format != 'text') {
|
|
args.content = trim(content);
|
|
} else {
|
|
args.content = content;
|
|
}
|
|
|
|
// Do post processing
|
|
if (!args.no_events) {
|
|
self.fire('GetContent', args);
|
|
}
|
|
|
|
return args.content;
|
|
},
|
|
|
|
/**
|
|
* Inserts content at caret position.
|
|
*
|
|
* @method insertContent
|
|
* @param {String} content Content to insert.
|
|
* @param {Object} args Optional args to pass to insert call.
|
|
*/
|
|
insertContent: function (content, args) {
|
|
if (args) {
|
|
content = extend({ content: content }, args);
|
|
}
|
|
|
|
this.execCommand('mceInsertContent', false, content);
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
|
|
*
|
|
* The dirty state is automatically set to true if you do modifications to the content in other
|
|
* words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set
|
|
* to false if you call editor.save().
|
|
*
|
|
* @method isDirty
|
|
* @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents.
|
|
* @example
|
|
* if (tinymce.activeEditor.isDirty())
|
|
* alert("You must save your contents.");
|
|
*/
|
|
isDirty: function () {
|
|
return !this.isNotDirty;
|
|
},
|
|
|
|
/**
|
|
* Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true
|
|
* by invoking this method.
|
|
*
|
|
* @method setDirty
|
|
* @param {Boolean} state True/false if the editor is considered dirty.
|
|
* @example
|
|
* function ajaxSave() {
|
|
* var editor = tinymce.get('elm1');
|
|
*
|
|
* // Save contents using some XHR call
|
|
* alert(editor.getContent());
|
|
*
|
|
* editor.setDirty(false); // Force not dirty state
|
|
* }
|
|
*/
|
|
setDirty: function (state) {
|
|
var oldState = !this.isNotDirty;
|
|
|
|
this.isNotDirty = !state;
|
|
|
|
if (state && state != oldState) {
|
|
this.fire('dirty');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the editor mode. Mode can be for example "design", "code" or "readonly".
|
|
*
|
|
* @method setMode
|
|
* @param {String} mode Mode to set the editor in.
|
|
*/
|
|
setMode: function (mode) {
|
|
Mode.setMode(this, mode);
|
|
},
|
|
|
|
/**
|
|
* Returns the editors container element. The container element wrappes in
|
|
* all the elements added to the page for the editor. Such as UI, iframe etc.
|
|
*
|
|
* @method getContainer
|
|
* @return {Element} HTML DOM element for the editor container.
|
|
*/
|
|
getContainer: function () {
|
|
var self = this;
|
|
|
|
if (!self.container) {
|
|
self.container = DOM.get(self.editorContainer || self.id + '_parent');
|
|
}
|
|
|
|
return self.container;
|
|
},
|
|
|
|
/**
|
|
* Returns the editors content area container element. The this element is the one who
|
|
* holds the iframe or the editable element.
|
|
*
|
|
* @method getContentAreaContainer
|
|
* @return {Element} HTML DOM element for the editor area container.
|
|
*/
|
|
getContentAreaContainer: function () {
|
|
return this.contentAreaContainer;
|
|
},
|
|
|
|
/**
|
|
* Returns the target element/textarea that got replaced with a TinyMCE editor instance.
|
|
*
|
|
* @method getElement
|
|
* @return {Element} HTML DOM element for the replaced element.
|
|
*/
|
|
getElement: function () {
|
|
if (!this.targetElm) {
|
|
this.targetElm = DOM.get(this.id);
|
|
}
|
|
|
|
return this.targetElm;
|
|
},
|
|
|
|
/**
|
|
* Returns the iframes window object.
|
|
*
|
|
* @method getWin
|
|
* @return {Window} Iframe DOM window object.
|
|
*/
|
|
getWin: function () {
|
|
var self = this, elm;
|
|
|
|
if (!self.contentWindow) {
|
|
elm = self.iframeElement;
|
|
|
|
if (elm) {
|
|
self.contentWindow = elm.contentWindow;
|
|
}
|
|
}
|
|
|
|
return self.contentWindow;
|
|
},
|
|
|
|
/**
|
|
* Returns the iframes document object.
|
|
*
|
|
* @method getDoc
|
|
* @return {Document} Iframe DOM document object.
|
|
*/
|
|
getDoc: function () {
|
|
var self = this, win;
|
|
|
|
if (!self.contentDocument) {
|
|
win = self.getWin();
|
|
|
|
if (win) {
|
|
self.contentDocument = win.document;
|
|
}
|
|
}
|
|
|
|
return self.contentDocument;
|
|
},
|
|
|
|
/**
|
|
* Returns the root element of the editable area.
|
|
* For a non-inline iframe-based editor, returns the iframe's body element.
|
|
*
|
|
* @method getBody
|
|
* @return {Element} The root element of the editable area.
|
|
*/
|
|
getBody: function () {
|
|
var doc = this.getDoc();
|
|
return this.bodyElement || (doc ? doc.body : null);
|
|
},
|
|
|
|
/**
|
|
* URL converter function this gets executed each time a user adds an img, a or
|
|
* any other element that has a URL in it. This will be called both by the DOM and HTML
|
|
* manipulation functions.
|
|
*
|
|
* @method convertURL
|
|
* @param {string} url URL to convert.
|
|
* @param {string} name Attribute name src, href etc.
|
|
* @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert.
|
|
* @return {string} Converted URL string.
|
|
*/
|
|
convertURL: function (url, name, elm) {
|
|
var self = this, settings = self.settings;
|
|
|
|
// Use callback instead
|
|
if (settings.urlconverter_callback) {
|
|
return self.execCallback('urlconverter_callback', url, elm, true, name);
|
|
}
|
|
|
|
// Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
|
|
if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) {
|
|
return url;
|
|
}
|
|
|
|
// Convert to relative
|
|
if (settings.relative_urls) {
|
|
return self.documentBaseURI.toRelative(url);
|
|
}
|
|
|
|
// Convert to absolute
|
|
url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host);
|
|
|
|
return url;
|
|
},
|
|
|
|
/**
|
|
* Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor.
|
|
*
|
|
* @method addVisual
|
|
* @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid.
|
|
*/
|
|
addVisual: function (elm) {
|
|
var self = this, settings = self.settings, dom = self.dom, cls;
|
|
|
|
elm = elm || self.getBody();
|
|
|
|
if (self.hasVisual === undefined) {
|
|
self.hasVisual = settings.visual;
|
|
}
|
|
|
|
each(dom.select('table,a', elm), function (elm) {
|
|
var value;
|
|
|
|
switch (elm.nodeName) {
|
|
case 'TABLE':
|
|
cls = settings.visual_table_class || 'mce-item-table';
|
|
value = dom.getAttrib(elm, 'border');
|
|
|
|
if ((!value || value == '0') && self.hasVisual) {
|
|
dom.addClass(elm, cls);
|
|
} else {
|
|
dom.removeClass(elm, cls);
|
|
}
|
|
|
|
return;
|
|
|
|
case 'A':
|
|
if (!dom.getAttrib(elm, 'href', false)) {
|
|
value = dom.getAttrib(elm, 'name') || elm.id;
|
|
cls = settings.visual_anchor_class || 'mce-item-anchor';
|
|
|
|
if (value && self.hasVisual) {
|
|
dom.addClass(elm, cls);
|
|
} else {
|
|
dom.removeClass(elm, cls);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
});
|
|
|
|
self.fire('VisualAid', { element: elm, hasVisual: self.hasVisual });
|
|
},
|
|
|
|
/**
|
|
* Removes the editor from the dom and tinymce collection.
|
|
*
|
|
* @method remove
|
|
*/
|
|
remove: function () {
|
|
var self = this;
|
|
|
|
if (!self.removed) {
|
|
self.save();
|
|
self.removed = 1;
|
|
self.unbindAllNativeEvents();
|
|
|
|
// Remove any hidden input
|
|
if (self.hasHiddenInput) {
|
|
DOM.remove(self.getElement().nextSibling);
|
|
}
|
|
|
|
if (!self.inline) {
|
|
// IE 9 has a bug where the selection stops working if you place the
|
|
// caret inside the editor then remove the iframe
|
|
if (ie && ie < 10) {
|
|
self.getDoc().execCommand('SelectAll', false, null);
|
|
}
|
|
|
|
DOM.setStyle(self.id, 'display', self.orgDisplay);
|
|
self.getBody().onload = null; // Prevent #6816
|
|
}
|
|
|
|
self.fire('remove');
|
|
|
|
self.editorManager.remove(self);
|
|
DOM.remove(self.getContainer());
|
|
self._selectionOverrides.destroy();
|
|
self.editorUpload.destroy();
|
|
self.destroy();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Destroys the editor instance by removing all events, element references or other resources
|
|
* that could leak memory. This method will be called automatically when the page is unloaded
|
|
* but you can also call it directly if you know what you are doing.
|
|
*
|
|
* @method destroy
|
|
* @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one.
|
|
*/
|
|
destroy: function (automatic) {
|
|
var self = this, form;
|
|
|
|
// One time is enough
|
|
if (self.destroyed) {
|
|
return;
|
|
}
|
|
|
|
// If user manually calls destroy and not remove
|
|
// Users seems to have logic that calls destroy instead of remove
|
|
if (!automatic && !self.removed) {
|
|
self.remove();
|
|
return;
|
|
}
|
|
|
|
if (!automatic) {
|
|
self.editorManager.off('beforeunload', self._beforeUnload);
|
|
|
|
// Manual destroy
|
|
if (self.theme && self.theme.destroy) {
|
|
self.theme.destroy();
|
|
}
|
|
|
|
// Destroy controls, selection and dom
|
|
self.selection.destroy();
|
|
self.dom.destroy();
|
|
}
|
|
|
|
form = self.formElement;
|
|
if (form) {
|
|
if (form._mceOldSubmit) {
|
|
form.submit = form._mceOldSubmit;
|
|
form._mceOldSubmit = null;
|
|
}
|
|
|
|
DOM.unbind(form, 'submit reset', self.formEventDelegate);
|
|
}
|
|
|
|
self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
|
|
self.bodyElement = self.contentDocument = self.contentWindow = null;
|
|
self.iframeElement = self.targetElm = null;
|
|
|
|
if (self.selection) {
|
|
self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
|
|
}
|
|
|
|
self.destroyed = 1;
|
|
},
|
|
|
|
/**
|
|
* Uploads all data uri/blob uri images in the editor contents to server.
|
|
*
|
|
* @method uploadImages
|
|
* @param {function} callback Optional callback with images and status for each image.
|
|
* @return {tinymce.util.Promise} Promise instance.
|
|
*/
|
|
uploadImages: function (callback) {
|
|
return this.editorUpload.uploadImages(callback);
|
|
},
|
|
|
|
// Internal functions
|
|
|
|
_scanForImages: function () {
|
|
return this.editorUpload.scanForImages();
|
|
}
|
|
};
|
|
|
|
extend(Editor.prototype, EditorObservable);
|
|
|
|
return Editor;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* I18n.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
|
|
*/
|
|
|
|
/**
|
|
* I18n class that handles translation of TinyMCE UI.
|
|
* Uses po style with csharp style parameters.
|
|
*
|
|
* @class tinymce.util.I18n
|
|
*/
|
|
define(
|
|
'tinymce.core.util.I18n',
|
|
[
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Tools) {
|
|
"use strict";
|
|
|
|
var data = {}, code = "en";
|
|
|
|
return {
|
|
/**
|
|
* Sets the current language code.
|
|
*
|
|
* @method setCode
|
|
* @param {String} newCode Current language code.
|
|
*/
|
|
setCode: function (newCode) {
|
|
if (newCode) {
|
|
code = newCode;
|
|
this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns the current language code.
|
|
*
|
|
* @method getCode
|
|
* @return {String} Current language code.
|
|
*/
|
|
getCode: function () {
|
|
return code;
|
|
},
|
|
|
|
/**
|
|
* Property gets set to true if a RTL language pack was loaded.
|
|
*
|
|
* @property rtl
|
|
* @type Boolean
|
|
*/
|
|
rtl: false,
|
|
|
|
/**
|
|
* Adds translations for a specific language code.
|
|
*
|
|
* @method add
|
|
* @param {String} code Language code like sv_SE.
|
|
* @param {Array} items Name/value array with English en_US to sv_SE.
|
|
*/
|
|
add: function (code, items) {
|
|
var langData = data[code];
|
|
|
|
if (!langData) {
|
|
data[code] = langData = {};
|
|
}
|
|
|
|
for (var name in items) {
|
|
langData[name] = items[name];
|
|
}
|
|
|
|
this.setCode(code);
|
|
},
|
|
|
|
/**
|
|
* Translates the specified text.
|
|
*
|
|
* It has a few formats:
|
|
* I18n.translate("Text");
|
|
* I18n.translate(["Text {0}/{1}", 0, 1]);
|
|
* I18n.translate({raw: "Raw string"});
|
|
*
|
|
* @method translate
|
|
* @param {String/Object/Array} text Text to translate.
|
|
* @return {String} String that got translated.
|
|
*/
|
|
translate: function (text) {
|
|
var langData = data[code] || {};
|
|
|
|
/**
|
|
* number - string
|
|
* null, undefined and empty string - empty string
|
|
* array - comma-delimited string
|
|
* object - in [object Object]
|
|
* function - in [object Function]
|
|
*
|
|
* @param obj
|
|
* @returns {string}
|
|
*/
|
|
function toString(obj) {
|
|
if (Tools.is(obj, 'function')) {
|
|
return Object.prototype.toString.call(obj);
|
|
}
|
|
return !isEmpty(obj) ? '' + obj : '';
|
|
}
|
|
|
|
function isEmpty(text) {
|
|
return text === '' || text === null || Tools.is(text, 'undefined');
|
|
}
|
|
|
|
function getLangData(text) {
|
|
// make sure we work on a string and return a string
|
|
text = toString(text);
|
|
return Tools.hasOwn(langData, text) ? toString(langData[text]) : text;
|
|
}
|
|
|
|
|
|
if (isEmpty(text)) {
|
|
return '';
|
|
}
|
|
|
|
if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) {
|
|
return toString(text.raw);
|
|
}
|
|
|
|
if (Tools.is(text, 'array')) {
|
|
var values = text.slice(1);
|
|
text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function ($1, $2) {
|
|
return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1;
|
|
});
|
|
}
|
|
|
|
return getLangData(text).replace(/{context:\w+}$/, '');
|
|
},
|
|
|
|
data: data
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* FocusManager.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 manages the focus/blur state of the editor. This class is needed since some
|
|
* browsers fire false focus/blur states when the selection is moved to a UI dialog or similar.
|
|
*
|
|
* This class will fire two events focus and blur on the editor instances that got affected.
|
|
* It will also handle the restore of selection when the focus is lost and returned.
|
|
*
|
|
* @class tinymce.FocusManager
|
|
*/
|
|
define(
|
|
'tinymce.core.FocusManager',
|
|
[
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.util.Delay",
|
|
"tinymce.core.Env"
|
|
],
|
|
function (DOMUtils, Delay, Env) {
|
|
var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM;
|
|
|
|
var isUIElement = function (editor, elm) {
|
|
var customSelector = editor ? editor.settings.custom_ui_selector : '';
|
|
var parent = DOM.getParent(elm, function (elm) {
|
|
return (
|
|
FocusManager.isEditorUIElement(elm) ||
|
|
(customSelector ? editor.dom.is(elm, customSelector) : false)
|
|
);
|
|
});
|
|
return parent !== null;
|
|
};
|
|
|
|
var isInlineEditor = function (editor) {
|
|
return editor.inline === true;
|
|
};
|
|
|
|
var isElementOursideInlineEditor = function (editor, target) {
|
|
return isInlineEditor(editor) === false || editor.dom.isChildOf(target, editor.getBody()) === false;
|
|
};
|
|
|
|
/**
|
|
* Constructs a new focus manager instance.
|
|
*
|
|
* @constructor FocusManager
|
|
* @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for.
|
|
*/
|
|
function FocusManager(editorManager) {
|
|
function getActiveElement() {
|
|
try {
|
|
return document.activeElement;
|
|
} catch (ex) {
|
|
// IE sometimes fails to get the activeElement when resizing table
|
|
// TODO: Investigate this
|
|
return document.body;
|
|
}
|
|
}
|
|
|
|
// We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
|
|
// TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
|
|
function createBookmark(dom, rng) {
|
|
if (rng && rng.startContainer) {
|
|
// Verify that the range is within the root of the editor
|
|
if (!dom.isChildOf(rng.startContainer, dom.getRoot()) || !dom.isChildOf(rng.endContainer, dom.getRoot())) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
startContainer: rng.startContainer,
|
|
startOffset: rng.startOffset,
|
|
endContainer: rng.endContainer,
|
|
endOffset: rng.endOffset
|
|
};
|
|
}
|
|
|
|
return rng;
|
|
}
|
|
|
|
function bookmarkToRng(editor, bookmark) {
|
|
var rng;
|
|
|
|
if (bookmark.startContainer) {
|
|
rng = editor.getDoc().createRange();
|
|
rng.setStart(bookmark.startContainer, bookmark.startOffset);
|
|
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
|
|
} else {
|
|
rng = bookmark;
|
|
}
|
|
|
|
return rng;
|
|
}
|
|
|
|
function registerEvents(e) {
|
|
var editor = e.editor;
|
|
|
|
editor.on('init', function () {
|
|
// Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
|
|
if (editor.inline || Env.ie) {
|
|
// Use the onbeforedeactivate event when available since it works better see #7023
|
|
if ("onbeforedeactivate" in document && Env.ie < 9) {
|
|
editor.dom.bind(editor.getBody(), 'beforedeactivate', function (e) {
|
|
if (e.target != editor.getBody()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
editor.lastRng = editor.selection.getRng();
|
|
} catch (ex) {
|
|
// IE throws "Unexcpected call to method or property access" some times so lets ignore it
|
|
}
|
|
});
|
|
} else {
|
|
// On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
|
|
editor.on('nodechange mouseup keyup', function (e) {
|
|
var node = getActiveElement();
|
|
|
|
// Only act on manual nodechanges
|
|
if (e.type == 'nodechange' && e.selectionChange) {
|
|
return;
|
|
}
|
|
|
|
// IE 11 reports active element as iframe not body of iframe
|
|
if (node && node.id == editor.id + '_ifr') {
|
|
node = editor.getBody();
|
|
}
|
|
|
|
if (editor.dom.isChildOf(node, editor.getBody())) {
|
|
editor.lastRng = editor.selection.getRng();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handles the issue with WebKit not retaining selection within inline document
|
|
// If the user releases the mouse out side the body since a mouse up event wont occur on the body
|
|
if (Env.webkit && !selectionChangeHandler) {
|
|
selectionChangeHandler = function () {
|
|
var activeEditor = editorManager.activeEditor;
|
|
|
|
if (activeEditor && activeEditor.selection) {
|
|
var rng = activeEditor.selection.getRng();
|
|
|
|
// Store when it's non collapsed
|
|
if (rng && !rng.collapsed) {
|
|
editor.lastRng = rng;
|
|
}
|
|
}
|
|
};
|
|
|
|
DOM.bind(document, 'selectionchange', selectionChangeHandler);
|
|
}
|
|
}
|
|
});
|
|
|
|
editor.on('setcontent', function () {
|
|
editor.lastRng = null;
|
|
});
|
|
|
|
// Remove last selection bookmark on mousedown see #6305
|
|
editor.on('mousedown', function () {
|
|
editor.selection.lastFocusBookmark = null;
|
|
});
|
|
|
|
editor.on('focusin', function () {
|
|
var focusedEditor = editorManager.focusedEditor, lastRng;
|
|
|
|
if (editor.selection.lastFocusBookmark) {
|
|
lastRng = bookmarkToRng(editor, editor.selection.lastFocusBookmark);
|
|
editor.selection.lastFocusBookmark = null;
|
|
editor.selection.setRng(lastRng);
|
|
}
|
|
|
|
if (focusedEditor != editor) {
|
|
if (focusedEditor) {
|
|
focusedEditor.fire('blur', { focusedEditor: editor });
|
|
}
|
|
|
|
editorManager.setActive(editor);
|
|
editorManager.focusedEditor = editor;
|
|
editor.fire('focus', { blurredEditor: focusedEditor });
|
|
editor.focus(true);
|
|
}
|
|
|
|
editor.lastRng = null;
|
|
});
|
|
|
|
editor.on('focusout', function () {
|
|
Delay.setEditorTimeout(editor, function () {
|
|
var focusedEditor = editorManager.focusedEditor;
|
|
|
|
// Still the same editor the blur was outside any editor UI
|
|
if (!isUIElement(editor, getActiveElement()) && focusedEditor == editor) {
|
|
editor.fire('blur', { focusedEditor: null });
|
|
editorManager.focusedEditor = null;
|
|
|
|
// Make sure selection is valid could be invalid if the editor is blured and removed before the timeout occurs
|
|
if (editor.selection) {
|
|
editor.selection.lastFocusBookmark = null;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Check if focus is moved to an element outside the active editor by checking if the target node
|
|
// isn't within the body of the activeEditor nor a UI element such as a dialog child control
|
|
if (!documentFocusInHandler) {
|
|
documentFocusInHandler = function (e) {
|
|
var activeEditor = editorManager.activeEditor, target;
|
|
|
|
target = e.target;
|
|
|
|
if (activeEditor && target.ownerDocument === document) {
|
|
// Check to make sure we have a valid selection don't update the bookmark if it's
|
|
// a focusin to the body of the editor see #7025
|
|
if (activeEditor.selection && target !== activeEditor.getBody() && isElementOursideInlineEditor(editor, target)) {
|
|
activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
|
|
}
|
|
|
|
// Fire a blur event if the element isn't a UI element
|
|
if (target !== document.body && !isUIElement(activeEditor, target) && editorManager.focusedEditor === activeEditor) {
|
|
activeEditor.fire('blur', { focusedEditor: null });
|
|
editorManager.focusedEditor = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
DOM.bind(document, 'focusin', documentFocusInHandler);
|
|
}
|
|
|
|
// Handle edge case when user starts the selection inside the editor and releases
|
|
// the mouse outside the editor producing a new selection. This weird workaround is needed since
|
|
// Gecko doesn't have the "selectionchange" event we need to do this. Fixes: #6843
|
|
if (editor.inline && !documentMouseUpHandler) {
|
|
documentMouseUpHandler = function (e) {
|
|
var activeEditor = editorManager.activeEditor, dom = activeEditor.dom;
|
|
|
|
if (activeEditor.inline && dom && !dom.isChildOf(e.target, activeEditor.getBody())) {
|
|
var rng = activeEditor.selection.getRng();
|
|
|
|
if (!rng.collapsed) {
|
|
activeEditor.lastRng = rng;
|
|
}
|
|
}
|
|
};
|
|
|
|
DOM.bind(document, 'mouseup', documentMouseUpHandler);
|
|
}
|
|
}
|
|
|
|
function unregisterDocumentEvents(e) {
|
|
if (editorManager.focusedEditor == e.editor) {
|
|
editorManager.focusedEditor = null;
|
|
}
|
|
|
|
if (!editorManager.activeEditor) {
|
|
DOM.unbind(document, 'selectionchange', selectionChangeHandler);
|
|
DOM.unbind(document, 'focusin', documentFocusInHandler);
|
|
DOM.unbind(document, 'mouseup', documentMouseUpHandler);
|
|
selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null;
|
|
}
|
|
}
|
|
|
|
editorManager.on('AddEditor', registerEvents);
|
|
editorManager.on('RemoveEditor', unregisterDocumentEvents);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the specified element is part of the UI for example an button or text input.
|
|
*
|
|
* @method isEditorUIElement
|
|
* @param {Element} elm Element to check if it's part of the UI or not.
|
|
* @return {Boolean} True/false state if the element is part of the UI or not.
|
|
*/
|
|
FocusManager.isEditorUIElement = function (elm) {
|
|
// Needs to be converted to string since svg can have focus: #6776
|
|
return elm.className.toString().indexOf('mce-') !== -1;
|
|
};
|
|
|
|
FocusManager._isUIElement = isUIElement;
|
|
|
|
return FocusManager;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* LegacyInput.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
|
|
*/
|
|
|
|
/**
|
|
* Converts legacy input to modern HTML.
|
|
*
|
|
* @class tinymce.LegacyInput
|
|
* @private
|
|
*/
|
|
define(
|
|
'tinymce.core.LegacyInput',
|
|
[
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Tools) {
|
|
var each = Tools.each, explode = Tools.explode;
|
|
|
|
var register = function (EditorManager) {
|
|
EditorManager.on('AddEditor', function (e) {
|
|
var editor = e.editor;
|
|
|
|
editor.on('preInit', function () {
|
|
var filters, fontSizes, dom, settings = editor.settings;
|
|
|
|
function replaceWithSpan(node, styles) {
|
|
each(styles, function (value, name) {
|
|
if (value) {
|
|
dom.setStyle(node, name, value);
|
|
}
|
|
});
|
|
|
|
dom.rename(node, 'span');
|
|
}
|
|
|
|
function convert(e) {
|
|
dom = editor.dom;
|
|
|
|
if (settings.convert_fonts_to_spans) {
|
|
each(dom.select('font,u,strike', e.node), function (node) {
|
|
filters[node.nodeName.toLowerCase()](dom, node);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (settings.inline_styles) {
|
|
fontSizes = explode(settings.font_size_legacy_values);
|
|
|
|
filters = {
|
|
font: function (dom, node) {
|
|
replaceWithSpan(node, {
|
|
backgroundColor: node.style.backgroundColor,
|
|
color: node.color,
|
|
fontFamily: node.face,
|
|
fontSize: fontSizes[parseInt(node.size, 10) - 1]
|
|
});
|
|
},
|
|
|
|
u: function (dom, node) {
|
|
// HTML5 allows U element
|
|
if (editor.settings.schema === "html4") {
|
|
replaceWithSpan(node, {
|
|
textDecoration: 'underline'
|
|
});
|
|
}
|
|
},
|
|
|
|
strike: function (dom, node) {
|
|
replaceWithSpan(node, {
|
|
textDecoration: 'line-through'
|
|
});
|
|
}
|
|
};
|
|
|
|
editor.on('PreProcess SetContent', convert);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
return {
|
|
register: register
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* EditorManager.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 used as a factory for manager for tinymce.Editor instances.
|
|
*
|
|
* @example
|
|
* tinymce.EditorManager.init({});
|
|
*
|
|
* @class tinymce.EditorManager
|
|
* @mixes tinymce.util.Observable
|
|
* @static
|
|
*/
|
|
define(
|
|
'tinymce.core.EditorManager',
|
|
[
|
|
'tinymce.core.AddOnManager',
|
|
'tinymce.core.dom.DomQuery',
|
|
'tinymce.core.dom.DOMUtils',
|
|
'tinymce.core.Editor',
|
|
'tinymce.core.Env',
|
|
'tinymce.core.ErrorReporter',
|
|
'tinymce.core.FocusManager',
|
|
'tinymce.core.LegacyInput',
|
|
'tinymce.core.util.I18n',
|
|
'tinymce.core.util.Observable',
|
|
'tinymce.core.util.Promise',
|
|
'tinymce.core.util.Tools',
|
|
'tinymce.core.util.URI'
|
|
],
|
|
function (AddOnManager, DomQuery, DOMUtils, Editor, Env, ErrorReporter, FocusManager, LegacyInput, I18n, Observable, Promise, Tools, URI) {
|
|
var DOM = DOMUtils.DOM;
|
|
var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
|
|
var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
|
|
|
|
function globalEventDelegate(e) {
|
|
each(EditorManager.editors, function (editor) {
|
|
if (e.type === 'scroll') {
|
|
editor.fire('ScrollWindow', e);
|
|
} else {
|
|
editor.fire('ResizeWindow', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleGlobalEvents(editors, state) {
|
|
if (state !== boundGlobalEvents) {
|
|
if (state) {
|
|
DomQuery(window).on('resize scroll', globalEventDelegate);
|
|
} else {
|
|
DomQuery(window).off('resize scroll', globalEventDelegate);
|
|
}
|
|
|
|
boundGlobalEvents = state;
|
|
}
|
|
}
|
|
|
|
function removeEditorFromList(editor) {
|
|
var editors = EditorManager.editors, removedFromList;
|
|
|
|
delete editors[editor.id];
|
|
|
|
for (var i = 0; i < editors.length; i++) {
|
|
if (editors[i] == editor) {
|
|
editors.splice(i, 1);
|
|
removedFromList = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Select another editor since the active one was removed
|
|
if (EditorManager.activeEditor == editor) {
|
|
EditorManager.activeEditor = editors[0];
|
|
}
|
|
|
|
// Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
|
|
if (EditorManager.focusedEditor == editor) {
|
|
EditorManager.focusedEditor = null;
|
|
}
|
|
|
|
return removedFromList;
|
|
}
|
|
|
|
function purgeDestroyedEditor(editor) {
|
|
// User has manually destroyed the editor lets clean up the mess
|
|
if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
|
|
removeEditorFromList(editor);
|
|
editor.unbindAllNativeEvents();
|
|
editor.destroy(true);
|
|
editor.removed = true;
|
|
editor = null;
|
|
}
|
|
|
|
return editor;
|
|
}
|
|
|
|
EditorManager = {
|
|
/**
|
|
* Dom query instance.
|
|
*
|
|
* @property $
|
|
* @type tinymce.dom.DomQuery
|
|
*/
|
|
$: DomQuery,
|
|
|
|
/**
|
|
* Major version of TinyMCE build.
|
|
*
|
|
* @property majorVersion
|
|
* @type String
|
|
*/
|
|
majorVersion: '4',
|
|
|
|
/**
|
|
* Minor version of TinyMCE build.
|
|
*
|
|
* @property minorVersion
|
|
* @type String
|
|
*/
|
|
minorVersion: '6.0',
|
|
|
|
/**
|
|
* Release date of TinyMCE build.
|
|
*
|
|
* @property releaseDate
|
|
* @type String
|
|
*/
|
|
releaseDate: '2017-05-04',
|
|
|
|
/**
|
|
* Collection of editor instances.
|
|
*
|
|
* @property editors
|
|
* @type Object
|
|
* @example
|
|
* for (edId in tinymce.editors)
|
|
* tinymce.editors[edId].save();
|
|
*/
|
|
editors: [],
|
|
|
|
/**
|
|
* Collection of language pack data.
|
|
*
|
|
* @property i18n
|
|
* @type Object
|
|
*/
|
|
i18n: I18n,
|
|
|
|
/**
|
|
* Currently active editor instance.
|
|
*
|
|
* @property activeEditor
|
|
* @type tinymce.Editor
|
|
* @example
|
|
* tinyMCE.activeEditor.selection.getContent();
|
|
* tinymce.EditorManager.activeEditor.selection.getContent();
|
|
*/
|
|
activeEditor: null,
|
|
|
|
setup: function () {
|
|
var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
|
|
|
|
// Get base URL for the current document
|
|
documentBaseURL = URI.getDocumentBaseUrl(document.location);
|
|
|
|
// Check if the URL is a document based format like: http://site/dir/file and file:///
|
|
// leave other formats like applewebdata://... intact
|
|
if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
|
|
documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
|
|
|
|
if (!/[\/\\]$/.test(documentBaseURL)) {
|
|
documentBaseURL += '/';
|
|
}
|
|
}
|
|
|
|
// If tinymce is defined and has a base use that or use the old tinyMCEPreInit
|
|
preInit = window.tinymce || window.tinyMCEPreInit;
|
|
if (preInit) {
|
|
baseURL = preInit.base || preInit.baseURL;
|
|
suffix = preInit.suffix;
|
|
} else {
|
|
// Get base where the tinymce script is located
|
|
var scripts = document.getElementsByTagName('script');
|
|
for (var i = 0; i < scripts.length; i++) {
|
|
src = scripts[i].src;
|
|
|
|
// Script types supported:
|
|
// tinymce.js tinymce.min.js tinymce.dev.js
|
|
// tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
|
|
// tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
|
|
var srcScript = src.substring(src.lastIndexOf('/'));
|
|
if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
|
|
if (srcScript.indexOf('.min') != -1) {
|
|
suffix = '.min';
|
|
}
|
|
|
|
baseURL = src.substring(0, src.lastIndexOf('/'));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We didn't find any baseURL by looking at the script elements
|
|
// Try to use the document.currentScript as a fallback
|
|
if (!baseURL && document.currentScript) {
|
|
src = document.currentScript.src;
|
|
|
|
if (src.indexOf('.min') != -1) {
|
|
suffix = '.min';
|
|
}
|
|
|
|
baseURL = src.substring(0, src.lastIndexOf('/'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base URL where the root directory if TinyMCE is located.
|
|
*
|
|
* @property baseURL
|
|
* @type String
|
|
*/
|
|
self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
|
|
|
|
/**
|
|
* Document base URL where the current document is located.
|
|
*
|
|
* @property documentBaseURL
|
|
* @type String
|
|
*/
|
|
self.documentBaseURL = documentBaseURL;
|
|
|
|
/**
|
|
* Absolute baseURI for the installation path of TinyMCE.
|
|
*
|
|
* @property baseURI
|
|
* @type tinymce.util.URI
|
|
*/
|
|
self.baseURI = new URI(self.baseURL);
|
|
|
|
/**
|
|
* Current suffix to add to each plugin/theme that gets loaded for example ".min".
|
|
*
|
|
* @property suffix
|
|
* @type String
|
|
*/
|
|
self.suffix = suffix;
|
|
|
|
self.focusManager = new FocusManager(self);
|
|
},
|
|
|
|
/**
|
|
* Overrides the default settings for editor instances.
|
|
*
|
|
* @method overrideDefaults
|
|
* @param {Object} defaultSettings Defaults settings object.
|
|
*/
|
|
overrideDefaults: function (defaultSettings) {
|
|
var baseUrl, suffix;
|
|
|
|
baseUrl = defaultSettings.base_url;
|
|
if (baseUrl) {
|
|
this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
|
|
this.baseURI = new URI(this.baseURL);
|
|
}
|
|
|
|
suffix = defaultSettings.suffix;
|
|
if (defaultSettings.suffix) {
|
|
this.suffix = suffix;
|
|
}
|
|
|
|
this.defaultSettings = defaultSettings;
|
|
|
|
var pluginBaseUrls = defaultSettings.plugin_base_urls;
|
|
for (var name in pluginBaseUrls) {
|
|
AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes a set of editors. This method will create editors based on various settings.
|
|
*
|
|
* @method init
|
|
* @param {Object} settings Settings object to be passed to each editor instance.
|
|
* @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
|
|
* @example
|
|
* // Initializes a editor using the longer method
|
|
* tinymce.EditorManager.init({
|
|
* some_settings : 'some value'
|
|
* });
|
|
*
|
|
* // Initializes a editor instance using the shorter version and with a promise
|
|
* tinymce.init({
|
|
* some_settings : 'some value'
|
|
* }).then(function(editors) {
|
|
* ...
|
|
* });
|
|
*/
|
|
init: function (settings) {
|
|
var self = this, result, invalidInlineTargets;
|
|
|
|
invalidInlineTargets = Tools.makeMap(
|
|
'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
|
|
'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
|
|
' '
|
|
);
|
|
|
|
function isInvalidInlineTarget(settings, elm) {
|
|
return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
|
|
}
|
|
|
|
function createId(elm) {
|
|
var id = elm.id;
|
|
|
|
// Use element id, or unique name or generate a unique id
|
|
if (!id) {
|
|
id = elm.name;
|
|
|
|
if (id && !DOM.get(id)) {
|
|
id = elm.name;
|
|
} else {
|
|
// Generate unique name
|
|
id = DOM.uniqueId();
|
|
}
|
|
|
|
elm.setAttribute('id', id);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
function execCallback(name) {
|
|
var callback = settings[name];
|
|
|
|
if (!callback) {
|
|
return;
|
|
}
|
|
|
|
return callback.apply(self, Array.prototype.slice.call(arguments, 2));
|
|
}
|
|
|
|
function hasClass(elm, className) {
|
|
return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
|
|
}
|
|
|
|
function findTargets(settings) {
|
|
var l, targets = [];
|
|
|
|
if (Env.ie && Env.ie < 11) {
|
|
ErrorReporter.initError(
|
|
'TinyMCE does not support the browser you are using. For a list of supported' +
|
|
' browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/'
|
|
);
|
|
return [];
|
|
}
|
|
|
|
if (settings.types) {
|
|
each(settings.types, function (type) {
|
|
targets = targets.concat(DOM.select(type.selector));
|
|
});
|
|
|
|
return targets;
|
|
} else if (settings.selector) {
|
|
return DOM.select(settings.selector);
|
|
} else if (settings.target) {
|
|
return [settings.target];
|
|
}
|
|
|
|
// Fallback to old setting
|
|
switch (settings.mode) {
|
|
case "exact":
|
|
l = settings.elements || '';
|
|
|
|
if (l.length > 0) {
|
|
each(explode(l), function (id) {
|
|
var elm;
|
|
|
|
if ((elm = DOM.get(id))) {
|
|
targets.push(elm);
|
|
} else {
|
|
each(document.forms, function (f) {
|
|
each(f.elements, function (e) {
|
|
if (e.name === id) {
|
|
id = 'mce_editor_' + instanceCounter++;
|
|
DOM.setAttrib(e, 'id', id);
|
|
targets.push(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
|
|
case "textareas":
|
|
case "specific_textareas":
|
|
each(DOM.select('textarea'), function (elm) {
|
|
if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
|
|
return;
|
|
}
|
|
|
|
if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
|
|
targets.push(elm);
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
var provideResults = function (editors) {
|
|
result = editors;
|
|
};
|
|
|
|
function initEditors() {
|
|
var initCount = 0, editors = [], targets;
|
|
|
|
function createEditor(id, settings, targetElm) {
|
|
var editor = new Editor(id, settings, self);
|
|
|
|
editors.push(editor);
|
|
|
|
editor.on('init', function () {
|
|
if (++initCount === targets.length) {
|
|
provideResults(editors);
|
|
}
|
|
});
|
|
|
|
editor.targetElm = editor.targetElm || targetElm;
|
|
editor.render();
|
|
}
|
|
|
|
DOM.unbind(window, 'ready', initEditors);
|
|
execCallback('onpageload');
|
|
|
|
targets = DomQuery.unique(findTargets(settings));
|
|
|
|
// TODO: Deprecate this one
|
|
if (settings.types) {
|
|
each(settings.types, function (type) {
|
|
Tools.each(targets, function (elm) {
|
|
if (DOM.is(elm, type.selector)) {
|
|
createEditor(createId(elm), extend({}, settings, type), elm);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
Tools.each(targets, function (elm) {
|
|
purgeDestroyedEditor(self.get(elm.id));
|
|
});
|
|
|
|
targets = Tools.grep(targets, function (elm) {
|
|
return !self.get(elm.id);
|
|
});
|
|
|
|
if (targets.length === 0) {
|
|
provideResults([]);
|
|
} else {
|
|
each(targets, function (elm) {
|
|
if (isInvalidInlineTarget(settings, elm)) {
|
|
ErrorReporter.initError('Could not initialize inline editor on invalid inline target element', elm);
|
|
} else {
|
|
createEditor(createId(elm), settings, elm);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
self.settings = settings;
|
|
DOM.bind(window, 'ready', initEditors);
|
|
|
|
return new Promise(function (resolve) {
|
|
if (result) {
|
|
resolve(result);
|
|
} else {
|
|
provideResults = function (editors) {
|
|
resolve(editors);
|
|
};
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns a editor instance by id.
|
|
*
|
|
* @method get
|
|
* @param {String/Number} id Editor instance id or index to return.
|
|
* @return {tinymce.Editor} Editor instance to return.
|
|
* @example
|
|
* // Adds an onclick event to an editor by id (shorter version)
|
|
* tinymce.get('mytextbox').on('click', function(e) {
|
|
* ed.windowManager.alert('Hello world!');
|
|
* });
|
|
*
|
|
* // Adds an onclick event to an editor by id (longer version)
|
|
* tinymce.EditorManager.get('mytextbox').on('click', function(e) {
|
|
* ed.windowManager.alert('Hello world!');
|
|
* });
|
|
*/
|
|
get: function (id) {
|
|
if (!arguments.length) {
|
|
return this.editors;
|
|
}
|
|
|
|
return id in this.editors ? this.editors[id] : null;
|
|
},
|
|
|
|
/**
|
|
* Adds an editor instance to the editor collection. This will also set it as the active editor.
|
|
*
|
|
* @method add
|
|
* @param {tinymce.Editor} editor Editor instance to add to the collection.
|
|
* @return {tinymce.Editor} The same instance that got passed in.
|
|
*/
|
|
add: function (editor) {
|
|
var self = this, editors = self.editors;
|
|
|
|
// Add named and index editor instance
|
|
editors[editor.id] = editor;
|
|
editors.push(editor);
|
|
|
|
toggleGlobalEvents(editors, true);
|
|
|
|
// Doesn't call setActive method since we don't want
|
|
// to fire a bunch of activate/deactivate calls while initializing
|
|
self.activeEditor = editor;
|
|
|
|
self.fire('AddEditor', { editor: editor });
|
|
|
|
if (!beforeUnloadDelegate) {
|
|
beforeUnloadDelegate = function () {
|
|
self.fire('BeforeUnload');
|
|
};
|
|
|
|
DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
|
|
}
|
|
|
|
return editor;
|
|
},
|
|
|
|
/**
|
|
* Creates an editor instance and adds it to the EditorManager collection.
|
|
*
|
|
* @method createEditor
|
|
* @param {String} id Instance id to use for editor.
|
|
* @param {Object} settings Editor instance settings.
|
|
* @return {tinymce.Editor} Editor instance that got created.
|
|
*/
|
|
createEditor: function (id, settings) {
|
|
return this.add(new Editor(id, settings, this));
|
|
},
|
|
|
|
/**
|
|
* Removes a editor or editors form page.
|
|
*
|
|
* @example
|
|
* // Remove all editors bound to divs
|
|
* tinymce.remove('div');
|
|
*
|
|
* // Remove all editors bound to textareas
|
|
* tinymce.remove('textarea');
|
|
*
|
|
* // Remove all editors
|
|
* tinymce.remove();
|
|
*
|
|
* // Remove specific instance by id
|
|
* tinymce.remove('#id');
|
|
*
|
|
* @method remove
|
|
* @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove.
|
|
* @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null.
|
|
*/
|
|
remove: function (selector) {
|
|
var self = this, i, editors = self.editors, editor;
|
|
|
|
// Remove all editors
|
|
if (!selector) {
|
|
for (i = editors.length - 1; i >= 0; i--) {
|
|
self.remove(editors[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Remove editors by selector
|
|
if (typeof selector == "string") {
|
|
selector = selector.selector || selector;
|
|
|
|
each(DOM.select(selector), function (elm) {
|
|
editor = editors[elm.id];
|
|
|
|
if (editor) {
|
|
self.remove(editor);
|
|
}
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Remove specific editor
|
|
editor = selector;
|
|
|
|
// Not in the collection
|
|
if (!editors[editor.id]) {
|
|
return null;
|
|
}
|
|
|
|
if (removeEditorFromList(editor)) {
|
|
self.fire('RemoveEditor', { editor: editor });
|
|
}
|
|
|
|
if (!editors.length) {
|
|
DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
|
|
}
|
|
|
|
editor.remove();
|
|
|
|
toggleGlobalEvents(editors, editors.length > 0);
|
|
|
|
return editor;
|
|
},
|
|
|
|
/**
|
|
* Executes a specific command on the currently active editor.
|
|
*
|
|
* @method execCommand
|
|
* @param {String} cmd Command to perform for example Bold.
|
|
* @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
|
|
* @param {String} value Optional value parameter like for example an URL to a link.
|
|
* @return {Boolean} true/false if the command was executed or not.
|
|
*/
|
|
execCommand: function (cmd, ui, value) {
|
|
var self = this, editor = self.get(value);
|
|
|
|
// Manager commands
|
|
switch (cmd) {
|
|
case "mceAddEditor":
|
|
if (!self.get(value)) {
|
|
new Editor(value, self.settings, self).render();
|
|
}
|
|
|
|
return true;
|
|
|
|
case "mceRemoveEditor":
|
|
if (editor) {
|
|
editor.remove();
|
|
}
|
|
|
|
return true;
|
|
|
|
case 'mceToggleEditor':
|
|
if (!editor) {
|
|
self.execCommand('mceAddEditor', 0, value);
|
|
return true;
|
|
}
|
|
|
|
if (editor.isHidden()) {
|
|
editor.show();
|
|
} else {
|
|
editor.hide();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Run command on active editor
|
|
if (self.activeEditor) {
|
|
return self.activeEditor.execCommand(cmd, ui, value);
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted.
|
|
*
|
|
* @method triggerSave
|
|
* @example
|
|
* // Saves all contents
|
|
* tinyMCE.triggerSave();
|
|
*/
|
|
triggerSave: function () {
|
|
each(this.editors, function (editor) {
|
|
editor.save();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Adds a language pack, this gets called by the loaded language files like en.js.
|
|
*
|
|
* @method addI18n
|
|
* @param {String} code Optional language code.
|
|
* @param {Object} items Name/value object with translations.
|
|
*/
|
|
addI18n: function (code, items) {
|
|
I18n.add(code, items);
|
|
},
|
|
|
|
/**
|
|
* Translates the specified string using the language pack items.
|
|
*
|
|
* @method translate
|
|
* @param {String/Array/Object} text String to translate
|
|
* @return {String} Translated string.
|
|
*/
|
|
translate: function (text) {
|
|
return I18n.translate(text);
|
|
},
|
|
|
|
/**
|
|
* Sets the active editor instance and fires the deactivate/activate events.
|
|
*
|
|
* @method setActive
|
|
* @param {tinymce.Editor} editor Editor instance to set as the active instance.
|
|
*/
|
|
setActive: function (editor) {
|
|
var activeEditor = this.activeEditor;
|
|
|
|
if (this.activeEditor != editor) {
|
|
if (activeEditor) {
|
|
activeEditor.fire('deactivate', { relatedTarget: editor });
|
|
}
|
|
|
|
editor.fire('activate', { relatedTarget: activeEditor });
|
|
}
|
|
|
|
this.activeEditor = editor;
|
|
}
|
|
};
|
|
|
|
extend(EditorManager, Observable);
|
|
|
|
EditorManager.setup();
|
|
LegacyInput.register(EditorManager);
|
|
|
|
return EditorManager;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* XHR.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 send XMLHTTPRequests cross browser.
|
|
* @class tinymce.util.XHR
|
|
* @mixes tinymce.util.Observable
|
|
* @static
|
|
* @example
|
|
* // Sends a low level Ajax request
|
|
* tinymce.util.XHR.send({
|
|
* url: 'someurl',
|
|
* success: function(text) {
|
|
* console.debug(text);
|
|
* }
|
|
* });
|
|
*
|
|
* // Add custom header to XHR request
|
|
* tinymce.util.XHR.on('beforeSend', function(e) {
|
|
* e.xhr.setRequestHeader('X-Requested-With', 'Something');
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.util.XHR',
|
|
[
|
|
"tinymce.core.util.Observable",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Observable, Tools) {
|
|
var XHR = {
|
|
/**
|
|
* Sends a XMLHTTPRequest.
|
|
* Consult the Wiki for details on what settings this method takes.
|
|
*
|
|
* @method send
|
|
* @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
|
|
*/
|
|
send: function (settings) {
|
|
var xhr, count = 0;
|
|
|
|
function ready() {
|
|
if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
|
|
if (settings.success && count < 10000 && xhr.status == 200) {
|
|
settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
|
|
} else if (settings.error) {
|
|
settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
|
|
}
|
|
|
|
xhr = null;
|
|
} else {
|
|
setTimeout(ready, 10);
|
|
}
|
|
}
|
|
|
|
// Default settings
|
|
settings.scope = settings.scope || this;
|
|
settings.success_scope = settings.success_scope || settings.scope;
|
|
settings.error_scope = settings.error_scope || settings.scope;
|
|
settings.async = settings.async === false ? false : true;
|
|
settings.data = settings.data || '';
|
|
|
|
XHR.fire('beforeInitialize', { settings: settings });
|
|
|
|
xhr = new XMLHttpRequest();
|
|
|
|
if (xhr) {
|
|
if (xhr.overrideMimeType) {
|
|
xhr.overrideMimeType(settings.content_type);
|
|
}
|
|
|
|
xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
|
|
|
|
if (settings.crossDomain) {
|
|
xhr.withCredentials = true;
|
|
}
|
|
|
|
if (settings.content_type) {
|
|
xhr.setRequestHeader('Content-Type', settings.content_type);
|
|
}
|
|
|
|
if (settings.requestheaders) {
|
|
Tools.each(settings.requestheaders, function (header) {
|
|
xhr.setRequestHeader(header.key, header.value);
|
|
});
|
|
}
|
|
|
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
xhr = XHR.fire('beforeSend', { xhr: xhr, settings: settings }).xhr;
|
|
xhr.send(settings.data);
|
|
|
|
// Syncronous request
|
|
if (!settings.async) {
|
|
return ready();
|
|
}
|
|
|
|
// Wait for response, onReadyStateChange can not be used since it leaks memory in IE
|
|
setTimeout(ready, 10);
|
|
}
|
|
}
|
|
};
|
|
|
|
Tools.extend(XHR, Observable);
|
|
|
|
return XHR;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* JSON.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
|
|
*/
|
|
|
|
/**
|
|
* JSON parser and serializer class.
|
|
*
|
|
* @class tinymce.util.JSON
|
|
* @static
|
|
* @example
|
|
* // JSON parse a string into an object
|
|
* var obj = tinymce.util.JSON.parse(somestring);
|
|
*
|
|
* // JSON serialize a object into an string
|
|
* var str = tinymce.util.JSON.serialize(obj);
|
|
*/
|
|
define(
|
|
'tinymce.core.util.JSON',
|
|
[
|
|
],
|
|
function () {
|
|
function serialize(o, quote) {
|
|
var i, v, t, name;
|
|
|
|
quote = quote || '"';
|
|
|
|
if (o === null) {
|
|
return 'null';
|
|
}
|
|
|
|
t = typeof o;
|
|
|
|
if (t == 'string') {
|
|
v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
|
|
|
|
/*eslint no-control-regex:0 */
|
|
return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function (a, b) {
|
|
// Make sure single quotes never get encoded inside double quotes for JSON compatibility
|
|
if (quote === '"' && a === "'") {
|
|
return a;
|
|
}
|
|
|
|
i = v.indexOf(b);
|
|
|
|
if (i + 1) {
|
|
return '\\' + v.charAt(i + 1);
|
|
}
|
|
|
|
a = b.charCodeAt().toString(16);
|
|
|
|
return '\\u' + '0000'.substring(a.length) + a;
|
|
}) + quote;
|
|
}
|
|
|
|
if (t == 'object') {
|
|
if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
|
|
for (i = 0, v = '['; i < o.length; i++) {
|
|
v += (i > 0 ? ',' : '') + serialize(o[i], quote);
|
|
}
|
|
|
|
return v + ']';
|
|
}
|
|
|
|
v = '{';
|
|
|
|
for (name in o) {
|
|
if (o.hasOwnProperty(name)) {
|
|
v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
|
|
quote + ':' + serialize(o[name], quote) : '';
|
|
}
|
|
}
|
|
|
|
return v + '}';
|
|
}
|
|
|
|
return '' + o;
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Serializes the specified object as a JSON string.
|
|
*
|
|
* @method serialize
|
|
* @param {Object} obj Object to serialize as a JSON string.
|
|
* @param {String} quote Optional quote string defaults to ".
|
|
* @return {string} JSON string serialized from input.
|
|
*/
|
|
serialize: serialize,
|
|
|
|
/**
|
|
* Unserializes/parses the specified JSON string into a object.
|
|
*
|
|
* @method parse
|
|
* @param {string} s JSON String to parse into a JavaScript object.
|
|
* @return {Object} Object from input JSON string or undefined if it failed.
|
|
*/
|
|
parse: function (text) {
|
|
try {
|
|
// Trick uglify JS
|
|
return window[String.fromCharCode(101) + 'val']('(' + text + ')');
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
/**#@-*/
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* JSONRequest.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 use JSON-RPC to call backend methods.
|
|
*
|
|
* @class tinymce.util.JSONRequest
|
|
* @example
|
|
* var json = new tinymce.util.JSONRequest({
|
|
* url: 'somebackend.php'
|
|
* });
|
|
*
|
|
* // Send RPC call 1
|
|
* json.send({
|
|
* method: 'someMethod1',
|
|
* params: ['a', 'b'],
|
|
* success: function(result) {
|
|
* console.dir(result);
|
|
* }
|
|
* });
|
|
*
|
|
* // Send RPC call 2
|
|
* json.send({
|
|
* method: 'someMethod2',
|
|
* params: ['a', 'b'],
|
|
* success: function(result) {
|
|
* console.dir(result);
|
|
* }
|
|
* });
|
|
*/
|
|
define(
|
|
'tinymce.core.util.JSONRequest',
|
|
[
|
|
"tinymce.core.util.JSON",
|
|
"tinymce.core.util.XHR",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (JSON, XHR, Tools) {
|
|
var extend = Tools.extend;
|
|
|
|
function JSONRequest(settings) {
|
|
this.settings = extend({}, settings);
|
|
this.count = 0;
|
|
}
|
|
|
|
/**
|
|
* Simple helper function to send a JSON-RPC request without the need to initialize an object.
|
|
* Consult the Wiki API documentation for more details on what you can pass to this function.
|
|
*
|
|
* @method sendRPC
|
|
* @static
|
|
* @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
|
|
*/
|
|
JSONRequest.sendRPC = function (o) {
|
|
return new JSONRequest().send(o);
|
|
};
|
|
|
|
JSONRequest.prototype = {
|
|
/**
|
|
* Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
|
|
*
|
|
* @method send
|
|
* @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
|
|
*/
|
|
send: function (args) {
|
|
var ecb = args.error, scb = args.success;
|
|
|
|
args = extend(this.settings, args);
|
|
|
|
args.success = function (c, x) {
|
|
c = JSON.parse(c);
|
|
|
|
if (typeof c == 'undefined') {
|
|
c = {
|
|
error: 'JSON Parse error.'
|
|
};
|
|
}
|
|
|
|
if (c.error) {
|
|
ecb.call(args.error_scope || args.scope, c.error, x);
|
|
} else {
|
|
scb.call(args.success_scope || args.scope, c.result);
|
|
}
|
|
};
|
|
|
|
args.error = function (ty, x) {
|
|
if (ecb) {
|
|
ecb.call(args.error_scope || args.scope, ty, x);
|
|
}
|
|
};
|
|
|
|
args.data = JSON.serialize({
|
|
id: args.id || 'c' + (this.count++),
|
|
method: args.method,
|
|
params: args.params
|
|
});
|
|
|
|
// JSON content type for Ruby on rails. Bug: #1883287
|
|
args.content_type = 'application/json';
|
|
|
|
XHR.send(args);
|
|
}
|
|
};
|
|
|
|
return JSONRequest;
|
|
}
|
|
);
|
|
/**
|
|
* JSONP.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.util.JSONP',
|
|
[
|
|
"tinymce.core.dom.DOMUtils"
|
|
],
|
|
function (DOMUtils) {
|
|
return {
|
|
callbacks: {},
|
|
count: 0,
|
|
|
|
send: function (settings) {
|
|
var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
|
|
var id = 'tinymce_jsonp_' + count;
|
|
|
|
self.callbacks[count] = function (json) {
|
|
dom.remove(id);
|
|
delete self.callbacks[count];
|
|
|
|
settings.callback(json);
|
|
};
|
|
|
|
dom.add(dom.doc.body, 'script', {
|
|
id: id,
|
|
src: settings.url,
|
|
type: 'text/javascript'
|
|
});
|
|
|
|
self.count++;
|
|
}
|
|
};
|
|
}
|
|
);
|
|
/**
|
|
* LocalStorage.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 simulate LocalStorage on IE 7 and return the native version on modern browsers.
|
|
* Storage is done using userData on IE 7 and a special serialization format. The format is designed
|
|
* to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
|
|
* makes it possible to store for example HTML data.
|
|
*
|
|
* Storage format for userData:
|
|
* <base 32 key length>,<key string>,<base 32 value length>,<value>,...
|
|
*
|
|
* For example this data key1=value1,key2=value2 would be:
|
|
* 4,key1,6,value1,4,key2,6,value2
|
|
*
|
|
* @class tinymce.util.LocalStorage
|
|
* @static
|
|
* @version 4.0
|
|
* @example
|
|
* tinymce.util.LocalStorage.setItem('key', 'value');
|
|
* var value = tinymce.util.LocalStorage.getItem('key');
|
|
*/
|
|
define(
|
|
'tinymce.core.util.LocalStorage',
|
|
[
|
|
],
|
|
function () {
|
|
var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
|
|
|
|
// Check for native support
|
|
try {
|
|
if (window.localStorage) {
|
|
return localStorage;
|
|
}
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
|
|
userDataKey = "tinymce";
|
|
storageElm = document.documentElement;
|
|
hasOldIEDataSupport = !!storageElm.addBehavior;
|
|
|
|
if (hasOldIEDataSupport) {
|
|
storageElm.addBehavior('#default#userData');
|
|
}
|
|
|
|
/**
|
|
* Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
|
|
*/
|
|
function updateKeys() {
|
|
keys = [];
|
|
|
|
for (var key in items) {
|
|
keys.push(key);
|
|
}
|
|
|
|
LocalStorage.length = keys.length;
|
|
}
|
|
|
|
/**
|
|
* Loads the userData string and parses it into the items structure.
|
|
*/
|
|
function load() {
|
|
var key, data, value, pos = 0;
|
|
|
|
items = {};
|
|
|
|
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
|
|
if (!hasOldIEDataSupport) {
|
|
return;
|
|
}
|
|
|
|
function next(end) {
|
|
var value, nextPos;
|
|
|
|
nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
|
|
if (nextPos === -1 || nextPos > data.length) {
|
|
return null;
|
|
}
|
|
|
|
value = data.substring(pos, nextPos);
|
|
pos = nextPos + 1;
|
|
|
|
return value;
|
|
}
|
|
|
|
storageElm.load(userDataKey);
|
|
data = storageElm.getAttribute(userDataKey) || '';
|
|
|
|
do {
|
|
var offset = next();
|
|
if (offset === null) {
|
|
break;
|
|
}
|
|
|
|
key = next(parseInt(offset, 32) || 0);
|
|
if (key !== null) {
|
|
offset = next();
|
|
if (offset === null) {
|
|
break;
|
|
}
|
|
|
|
value = next(parseInt(offset, 32) || 0);
|
|
|
|
if (key) {
|
|
items[key] = value;
|
|
}
|
|
}
|
|
} while (key !== null);
|
|
|
|
updateKeys();
|
|
}
|
|
|
|
/**
|
|
* Saves the items structure into a the userData format.
|
|
*/
|
|
function save() {
|
|
var value, data = '';
|
|
|
|
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
|
|
if (!hasOldIEDataSupport) {
|
|
return;
|
|
}
|
|
|
|
for (var key in items) {
|
|
value = items[key];
|
|
data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
|
|
}
|
|
|
|
storageElm.setAttribute(userDataKey, data);
|
|
|
|
try {
|
|
storageElm.save(userDataKey);
|
|
} catch (ex) {
|
|
// Ignore disk full
|
|
}
|
|
|
|
updateKeys();
|
|
}
|
|
|
|
LocalStorage = {
|
|
/**
|
|
* Length of the number of items in storage.
|
|
*
|
|
* @property length
|
|
* @type Number
|
|
* @return {Number} Number of items in storage.
|
|
*/
|
|
//length:0,
|
|
|
|
/**
|
|
* Returns the key name by index.
|
|
*
|
|
* @method key
|
|
* @param {Number} index Index of key to return.
|
|
* @return {String} Key value or null if it wasn't found.
|
|
*/
|
|
key: function (index) {
|
|
return keys[index];
|
|
},
|
|
|
|
/**
|
|
* Returns the value if the specified key or null if it wasn't found.
|
|
*
|
|
* @method getItem
|
|
* @param {String} key Key of item to retrieve.
|
|
* @return {String} Value of the specified item or null if it wasn't found.
|
|
*/
|
|
getItem: function (key) {
|
|
return key in items ? items[key] : null;
|
|
},
|
|
|
|
/**
|
|
* Sets the value of the specified item by it's key.
|
|
*
|
|
* @method setItem
|
|
* @param {String} key Key of the item to set.
|
|
* @param {String} value Value of the item to set.
|
|
*/
|
|
setItem: function (key, value) {
|
|
items[key] = "" + value;
|
|
save();
|
|
},
|
|
|
|
/**
|
|
* Removes the specified item by key.
|
|
*
|
|
* @method removeItem
|
|
* @param {String} key Key of item to remove.
|
|
*/
|
|
removeItem: function (key) {
|
|
delete items[key];
|
|
save();
|
|
},
|
|
|
|
/**
|
|
* Removes all items.
|
|
*
|
|
* @method clear
|
|
*/
|
|
clear: function () {
|
|
items = {};
|
|
save();
|
|
}
|
|
};
|
|
|
|
load();
|
|
|
|
return LocalStorage;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Compat.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
|
|
*/
|
|
|
|
/**
|
|
* TinyMCE core class.
|
|
*
|
|
* @static
|
|
* @class tinymce
|
|
* @borrow-members tinymce.EditorManager
|
|
* @borrow-members tinymce.util.Tools
|
|
*/
|
|
define(
|
|
'tinymce.core.api.Compat',
|
|
[
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.dom.EventUtils",
|
|
"tinymce.core.dom.ScriptLoader",
|
|
"tinymce.core.AddOnManager",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.Env"
|
|
],
|
|
function (DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
|
|
var register = function (tinymce) {
|
|
/**
|
|
* @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
|
|
* @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
|
|
* @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
|
|
* @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
|
|
*/
|
|
tinymce.DOM = DOMUtils.DOM;
|
|
tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
|
|
tinymce.PluginManager = AddOnManager.PluginManager;
|
|
tinymce.ThemeManager = AddOnManager.ThemeManager;
|
|
|
|
tinymce.dom = tinymce.dom || {};
|
|
tinymce.dom.Event = EventUtils.Event;
|
|
|
|
Tools.each(
|
|
'trim isArray is toArray makeMap each map grep inArray extend create walk createNS resolve explode _addCacheSuffix'.split(' '),
|
|
function (key) {
|
|
tinymce[key] = Tools[key];
|
|
}
|
|
);
|
|
|
|
Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function (name) {
|
|
tinymce[name] = Env[name.substr(2).toLowerCase()];
|
|
});
|
|
};
|
|
|
|
return {
|
|
register: register
|
|
};
|
|
}
|
|
);
|
|
|
|
// Describe the different namespaces
|
|
|
|
/**
|
|
* Root level namespace this contains classes directly related to the TinyMCE editor.
|
|
*
|
|
* @namespace tinymce
|
|
*/
|
|
|
|
/**
|
|
* Contains classes for handling the browsers DOM.
|
|
*
|
|
* @namespace tinymce.dom
|
|
*/
|
|
|
|
/**
|
|
* Contains html parser and serializer logic.
|
|
*
|
|
* @namespace tinymce.html
|
|
*/
|
|
|
|
/**
|
|
* Contains the different UI types such as buttons, listboxes etc.
|
|
*
|
|
* @namespace tinymce.ui
|
|
*/
|
|
|
|
/**
|
|
* Contains various utility classes such as json parser, cookies etc.
|
|
*
|
|
* @namespace tinymce.util
|
|
*/
|
|
|
|
/**
|
|
* Contains modules to handle data binding.
|
|
*
|
|
* @namespace tinymce.data
|
|
*/
|
|
|
|
/**
|
|
* Color.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 parse/serialize colors and convert rgb/hsb.
|
|
*
|
|
* @class tinymce.util.Color
|
|
* @example
|
|
* var white = new tinymce.util.Color({r: 255, g: 255, b: 255});
|
|
* var red = new tinymce.util.Color('#FF0000');
|
|
*
|
|
* console.log(white.toHex(), red.toHsv());
|
|
*/
|
|
define(
|
|
'tinymce.core.util.Color',
|
|
[
|
|
],
|
|
function () {
|
|
var min = Math.min, max = Math.max, round = Math.round;
|
|
|
|
/**
|
|
* Constructs a new color instance.
|
|
*
|
|
* @constructor
|
|
* @method Color
|
|
* @param {String} value Optional initial value to parse.
|
|
*/
|
|
function Color(value) {
|
|
var self = this, r = 0, g = 0, b = 0;
|
|
|
|
function rgb2hsv(r, g, b) {
|
|
var h, s, v, d, minRGB, maxRGB;
|
|
|
|
h = 0;
|
|
s = 0;
|
|
v = 0;
|
|
r = r / 255;
|
|
g = g / 255;
|
|
b = b / 255;
|
|
|
|
minRGB = min(r, min(g, b));
|
|
maxRGB = max(r, max(g, b));
|
|
|
|
if (minRGB == maxRGB) {
|
|
v = minRGB;
|
|
|
|
return {
|
|
h: 0,
|
|
s: 0,
|
|
v: v * 100
|
|
};
|
|
}
|
|
|
|
/*eslint no-nested-ternary:0 */
|
|
d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r);
|
|
h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5);
|
|
h = 60 * (h - d / (maxRGB - minRGB));
|
|
s = (maxRGB - minRGB) / maxRGB;
|
|
v = maxRGB;
|
|
|
|
return {
|
|
h: round(h),
|
|
s: round(s * 100),
|
|
v: round(v * 100)
|
|
};
|
|
}
|
|
|
|
function hsvToRgb(hue, saturation, brightness) {
|
|
var side, chroma, x, match;
|
|
|
|
hue = (parseInt(hue, 10) || 0) % 360;
|
|
saturation = parseInt(saturation, 10) / 100;
|
|
brightness = parseInt(brightness, 10) / 100;
|
|
saturation = max(0, min(saturation, 1));
|
|
brightness = max(0, min(brightness, 1));
|
|
|
|
if (saturation === 0) {
|
|
r = g = b = round(255 * brightness);
|
|
return;
|
|
}
|
|
|
|
side = hue / 60;
|
|
chroma = brightness * saturation;
|
|
x = chroma * (1 - Math.abs(side % 2 - 1));
|
|
match = brightness - chroma;
|
|
|
|
switch (Math.floor(side)) {
|
|
case 0:
|
|
r = chroma;
|
|
g = x;
|
|
b = 0;
|
|
break;
|
|
|
|
case 1:
|
|
r = x;
|
|
g = chroma;
|
|
b = 0;
|
|
break;
|
|
|
|
case 2:
|
|
r = 0;
|
|
g = chroma;
|
|
b = x;
|
|
break;
|
|
|
|
case 3:
|
|
r = 0;
|
|
g = x;
|
|
b = chroma;
|
|
break;
|
|
|
|
case 4:
|
|
r = x;
|
|
g = 0;
|
|
b = chroma;
|
|
break;
|
|
|
|
case 5:
|
|
r = chroma;
|
|
g = 0;
|
|
b = x;
|
|
break;
|
|
|
|
default:
|
|
r = g = b = 0;
|
|
}
|
|
|
|
r = round(255 * (r + match));
|
|
g = round(255 * (g + match));
|
|
b = round(255 * (b + match));
|
|
}
|
|
|
|
/**
|
|
* Returns the hex string of the current color. For example: #ff00ff
|
|
*
|
|
* @method toHex
|
|
* @return {String} Hex string of current color.
|
|
*/
|
|
function toHex() {
|
|
function hex(val) {
|
|
val = parseInt(val, 10).toString(16);
|
|
|
|
return val.length > 1 ? val : '0' + val;
|
|
}
|
|
|
|
return '#' + hex(r) + hex(g) + hex(b);
|
|
}
|
|
|
|
/**
|
|
* Returns the r, g, b values of the color. Each channel has a range from 0-255.
|
|
*
|
|
* @method toRgb
|
|
* @return {Object} Object with r, g, b fields.
|
|
*/
|
|
function toRgb() {
|
|
return {
|
|
r: r,
|
|
g: g,
|
|
b: b
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100.
|
|
*
|
|
* @method toHsv
|
|
* @return {Object} Object with h, s, v fields.
|
|
*/
|
|
function toHsv() {
|
|
return rgb2hsv(r, g, b);
|
|
}
|
|
|
|
/**
|
|
* Parses the specified value and populates the color instance.
|
|
*
|
|
* Supported format examples:
|
|
* * rbg(255,0,0)
|
|
* * #ff0000
|
|
* * #fff
|
|
* * {r: 255, g: 0, b: 0}
|
|
* * {h: 360, s: 100, v: 100}
|
|
*
|
|
* @method parse
|
|
* @param {Object/String} value Color value to parse.
|
|
* @return {tinymce.util.Color} Current color instance.
|
|
*/
|
|
function parse(value) {
|
|
var matches;
|
|
|
|
if (typeof value == 'object') {
|
|
if ("r" in value) {
|
|
r = value.r;
|
|
g = value.g;
|
|
b = value.b;
|
|
} else if ("v" in value) {
|
|
hsvToRgb(value.h, value.s, value.v);
|
|
}
|
|
} else {
|
|
if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) {
|
|
r = parseInt(matches[1], 10);
|
|
g = parseInt(matches[2], 10);
|
|
b = parseInt(matches[3], 10);
|
|
} else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) {
|
|
r = parseInt(matches[1], 16);
|
|
g = parseInt(matches[2], 16);
|
|
b = parseInt(matches[3], 16);
|
|
} else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) {
|
|
r = parseInt(matches[1] + matches[1], 16);
|
|
g = parseInt(matches[2] + matches[2], 16);
|
|
b = parseInt(matches[3] + matches[3], 16);
|
|
}
|
|
}
|
|
|
|
r = r < 0 ? 0 : (r > 255 ? 255 : r);
|
|
g = g < 0 ? 0 : (g > 255 ? 255 : g);
|
|
b = b < 0 ? 0 : (b > 255 ? 255 : b);
|
|
|
|
return self;
|
|
}
|
|
|
|
if (value) {
|
|
parse(value);
|
|
}
|
|
|
|
self.toRgb = toRgb;
|
|
self.toHsv = toHsv;
|
|
self.toHex = toHex;
|
|
self.parse = parse;
|
|
}
|
|
|
|
return Color;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Layout.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
|
|
*/
|
|
|
|
/**
|
|
* Base layout manager class.
|
|
*
|
|
* @class tinymce.ui.Layout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Layout',
|
|
[
|
|
"tinymce.core.util.Class",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Class, Tools) {
|
|
"use strict";
|
|
|
|
return Class.extend({
|
|
Defaults: {
|
|
firstControlClass: 'first',
|
|
lastControlClass: 'last'
|
|
},
|
|
|
|
/**
|
|
* Constructs a layout instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
this.settings = Tools.extend({}, this.Defaults, settings);
|
|
},
|
|
|
|
/**
|
|
* This method gets invoked before the layout renders the controls.
|
|
*
|
|
* @method preRender
|
|
* @param {tinymce.ui.Container} container Container instance to preRender.
|
|
*/
|
|
preRender: function (container) {
|
|
container.bodyClasses.add(this.settings.containerClass);
|
|
},
|
|
|
|
/**
|
|
* Applies layout classes to the container.
|
|
*
|
|
* @private
|
|
*/
|
|
applyClasses: function (items) {
|
|
var self = this, settings = self.settings, firstClass, lastClass, firstItem, lastItem;
|
|
|
|
firstClass = settings.firstControlClass;
|
|
lastClass = settings.lastControlClass;
|
|
|
|
items.each(function (item) {
|
|
item.classes.remove(firstClass).remove(lastClass).add(settings.controlClass);
|
|
|
|
if (item.visible()) {
|
|
if (!firstItem) {
|
|
firstItem = item;
|
|
}
|
|
|
|
lastItem = item;
|
|
}
|
|
});
|
|
|
|
if (firstItem) {
|
|
firstItem.classes.add(firstClass);
|
|
}
|
|
|
|
if (lastItem) {
|
|
lastItem.classes.add(lastClass);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders the specified container and any layout specific HTML.
|
|
*
|
|
* @method renderHtml
|
|
* @param {tinymce.ui.Container} container Container to render HTML for.
|
|
*/
|
|
renderHtml: function (container) {
|
|
var self = this, html = '';
|
|
|
|
self.applyClasses(container.items());
|
|
|
|
container.items().each(function (item) {
|
|
html += item.renderHtml();
|
|
});
|
|
|
|
return html;
|
|
},
|
|
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function () {
|
|
},
|
|
|
|
/**
|
|
* This method gets invoked after the layout renders the controls.
|
|
*
|
|
* @method postRender
|
|
* @param {tinymce.ui.Container} container Container instance to postRender.
|
|
*/
|
|
postRender: function () {
|
|
},
|
|
|
|
isNative: function () {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* AbsoluteLayout.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
|
|
*/
|
|
|
|
/**
|
|
* LayoutManager for absolute positioning. This layout manager is more of
|
|
* a base class for other layouts but can be created and used directly.
|
|
*
|
|
* @-x-less AbsoluteLayout.less
|
|
* @class tinymce.ui.AbsoluteLayout
|
|
* @extends tinymce.ui.Layout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.AbsoluteLayout',
|
|
[
|
|
"tinymce.core.ui.Layout"
|
|
],
|
|
function (Layout) {
|
|
"use strict";
|
|
|
|
return Layout.extend({
|
|
Defaults: {
|
|
containerClass: 'abs-layout',
|
|
controlClass: 'abs-layout-item'
|
|
},
|
|
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function (container) {
|
|
container.items().filter(':visible').each(function (ctrl) {
|
|
var settings = ctrl.settings;
|
|
|
|
ctrl.layoutRect({
|
|
x: settings.x,
|
|
y: settings.y,
|
|
w: settings.w,
|
|
h: settings.h
|
|
});
|
|
|
|
if (ctrl.recalc) {
|
|
ctrl.recalc();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders the specified container and any layout specific HTML.
|
|
*
|
|
* @method renderHtml
|
|
* @param {tinymce.ui.Container} container Container to render HTML for.
|
|
*/
|
|
renderHtml: function (container) {
|
|
return '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Button.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 create buttons. You can create them directly or through the Factory.
|
|
*
|
|
* @example
|
|
* // Create and render a button to the body element
|
|
* tinymce.ui.Factory.create({
|
|
* type: 'button',
|
|
* text: 'My button'
|
|
* }).renderTo(document.body);
|
|
*
|
|
* @-x-less Button.less
|
|
* @class tinymce.ui.Button
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Button',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
classes: "widget btn",
|
|
role: "button"
|
|
},
|
|
|
|
/**
|
|
* Constructs a new button instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {String} size Size of the button small|medium|large.
|
|
* @setting {String} image Image to use for icon.
|
|
* @setting {String} icon Icon to use for button.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this, size;
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
|
|
size = self.settings.size;
|
|
|
|
self.on('click mousedown', function (e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
self.on('touchstart', function (e) {
|
|
self.fire('click', e);
|
|
e.preventDefault();
|
|
});
|
|
|
|
if (settings.subtype) {
|
|
self.classes.add(settings.subtype);
|
|
}
|
|
|
|
if (size) {
|
|
self.classes.add('btn-' + size);
|
|
}
|
|
|
|
if (settings.icon) {
|
|
self.icon(settings.icon);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets/gets the current button icon.
|
|
*
|
|
* @method icon
|
|
* @param {String} [icon] New icon identifier.
|
|
* @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance.
|
|
*/
|
|
icon: function (icon) {
|
|
if (!arguments.length) {
|
|
return this.state.get('icon');
|
|
}
|
|
|
|
this.state.set('icon', icon);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Repaints the button for example after it's been resizes by a layout engine.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var btnElm = this.getEl().firstChild,
|
|
btnStyle;
|
|
|
|
if (btnElm) {
|
|
btnStyle = btnElm.style;
|
|
btnStyle.width = btnStyle.height = "100%";
|
|
}
|
|
|
|
this._super();
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix;
|
|
var icon = self.state.get('icon'), image, text = self.state.get('text'), textHtml = '';
|
|
|
|
image = self.settings.image;
|
|
if (image) {
|
|
icon = 'none';
|
|
|
|
// Support for [high dpi, low dpi] image sources
|
|
if (typeof image != "string") {
|
|
image = window.getSelection ? image[0] : image[1];
|
|
}
|
|
|
|
image = ' style="background-image: url(\'' + image + '\')"';
|
|
} else {
|
|
image = '';
|
|
}
|
|
|
|
if (text) {
|
|
self.classes.add('btn-has-text');
|
|
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
|
|
}
|
|
|
|
icon = icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1">' +
|
|
'<button role="presentation" type="button" tabindex="-1">' +
|
|
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
|
textHtml +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this, $ = self.$, textCls = self.classPrefix + 'txt';
|
|
|
|
function setButtonText(text) {
|
|
var $span = $('span.' + textCls, self.getEl());
|
|
|
|
if (text) {
|
|
if (!$span[0]) {
|
|
$('button:first', self.getEl()).append('<span class="' + textCls + '"></span>');
|
|
$span = $('span.' + textCls, self.getEl());
|
|
}
|
|
|
|
$span.html(self.encode(text));
|
|
} else {
|
|
$span.remove();
|
|
}
|
|
|
|
self.classes.toggle('btn-has-text', !!text);
|
|
}
|
|
|
|
self.state.on('change:text', function (e) {
|
|
setButtonText(e.value);
|
|
});
|
|
|
|
self.state.on('change:icon', function (e) {
|
|
var icon = e.value, prefix = self.classPrefix;
|
|
|
|
self.settings.icon = icon;
|
|
icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
|
|
|
var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
|
|
|
|
if (icon) {
|
|
if (!iconElm || iconElm != btnElm.firstChild) {
|
|
iconElm = document.createElement('i');
|
|
btnElm.insertBefore(iconElm, btnElm.firstChild);
|
|
}
|
|
|
|
iconElm.className = icon;
|
|
} else if (iconElm) {
|
|
btnElm.removeChild(iconElm);
|
|
}
|
|
|
|
setButtonText(self.state.get('text'));
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ButtonGroup.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 control enables you to put multiple buttons into a group. This is
|
|
* useful when you want to combine similar toolbar buttons into a group.
|
|
*
|
|
* @example
|
|
* // Create and render a buttongroup with two buttons to the body element
|
|
* tinymce.ui.Factory.create({
|
|
* type: 'buttongroup',
|
|
* items: [
|
|
* {text: 'Button A'},
|
|
* {text: 'Button B'}
|
|
* ]
|
|
* }).renderTo(document.body);
|
|
*
|
|
* @-x-less ButtonGroup.less
|
|
* @class tinymce.ui.ButtonGroup
|
|
* @extends tinymce.ui.Container
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ButtonGroup',
|
|
[
|
|
"tinymce.core.ui.Container"
|
|
],
|
|
function (Container) {
|
|
"use strict";
|
|
|
|
return Container.extend({
|
|
Defaults: {
|
|
defaultType: 'button',
|
|
role: 'group'
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout;
|
|
|
|
self.classes.add('btn-group');
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '">' +
|
|
'<div id="' + self._id + '-body">' +
|
|
(self.settings.html || '') + layout.renderHtml(self) +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Checkbox.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 control creates a custom checkbox.
|
|
*
|
|
* @example
|
|
* // Create and render a checkbox to the body element
|
|
* tinymce.ui.Factory.create({
|
|
* type: 'checkbox',
|
|
* checked: true,
|
|
* text: 'My checkbox'
|
|
* }).renderTo(document.body);
|
|
*
|
|
* @-x-less Checkbox.less
|
|
* @class tinymce.ui.Checkbox
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Checkbox',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
classes: "checkbox",
|
|
role: "checkbox",
|
|
checked: false
|
|
},
|
|
|
|
/**
|
|
* Constructs a new Checkbox instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} checked True if the checkbox should be checked by default.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
|
|
self.on('click mousedown', function (e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
self.on('click', function (e) {
|
|
e.preventDefault();
|
|
|
|
if (!self.disabled()) {
|
|
self.checked(!self.checked());
|
|
}
|
|
});
|
|
|
|
self.checked(self.settings.checked);
|
|
},
|
|
|
|
/**
|
|
* Getter/setter function for the checked state.
|
|
*
|
|
* @method checked
|
|
* @param {Boolean} [state] State to be set.
|
|
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
|
|
*/
|
|
checked: function (state) {
|
|
if (!arguments.length) {
|
|
return this.state.get('checked');
|
|
}
|
|
|
|
this.state.set('checked', state);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Getter/setter function for the value state.
|
|
*
|
|
* @method value
|
|
* @param {Boolean} [state] State to be set.
|
|
* @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation.
|
|
*/
|
|
value: function (state) {
|
|
if (!arguments.length) {
|
|
return this.checked();
|
|
}
|
|
|
|
return this.checked(state);
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix;
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" unselectable="on" aria-labelledby="' + id + '-al" tabindex="-1">' +
|
|
'<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' +
|
|
'<span id="' + id + '-al" class="' + prefix + 'label">' + self.encode(self.state.get('text')) + '</span>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
function checked(state) {
|
|
self.classes.toggle("checked", state);
|
|
self.aria('checked', state);
|
|
}
|
|
|
|
self.state.on('change:text', function (e) {
|
|
self.getEl('al').firstChild.data = self.translate(e.value);
|
|
});
|
|
|
|
self.state.on('change:checked change:value', function (e) {
|
|
self.fire('change');
|
|
checked(e.value);
|
|
});
|
|
|
|
self.state.on('change:icon', function (e) {
|
|
var icon = e.value, prefix = self.classPrefix;
|
|
|
|
if (typeof icon == 'undefined') {
|
|
return self.settings.icon;
|
|
}
|
|
|
|
self.settings.icon = icon;
|
|
icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
|
|
|
var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0];
|
|
|
|
if (icon) {
|
|
if (!iconElm || iconElm != btnElm.firstChild) {
|
|
iconElm = document.createElement('i');
|
|
btnElm.insertBefore(iconElm, btnElm.firstChild);
|
|
}
|
|
|
|
iconElm.className = icon;
|
|
} else if (iconElm) {
|
|
btnElm.removeChild(iconElm);
|
|
}
|
|
});
|
|
|
|
if (self.state.get('checked')) {
|
|
checked(true);
|
|
}
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* ComboBox.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 creates a combobox control. Select box that you select a value from or
|
|
* type a value into.
|
|
*
|
|
* @-x-less ComboBox.less
|
|
* @class tinymce.ui.ComboBox
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ComboBox',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.Factory",
|
|
"tinymce.core.ui.DomUtils",
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.util.VK",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Widget, Factory, DomUtils, $, VK, Tools) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Constructs a new control instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {String} placeholder Placeholder text to display.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
|
|
self.classes.add('combobox');
|
|
self.subinput = true;
|
|
self.ariaTarget = 'inp'; // TODO: Figure out a better way
|
|
|
|
settings.menu = settings.menu || settings.values;
|
|
|
|
if (settings.menu) {
|
|
settings.icon = 'caret';
|
|
}
|
|
|
|
self.on('click', function (e) {
|
|
var elm = e.target, root = self.getEl();
|
|
|
|
if (!$.contains(root, elm) && elm != root) {
|
|
return;
|
|
}
|
|
|
|
while (elm && elm != root) {
|
|
if (elm.id && elm.id.indexOf('-open') != -1) {
|
|
self.fire('action');
|
|
|
|
if (settings.menu) {
|
|
self.showMenu();
|
|
|
|
if (e.aria) {
|
|
self.menu.items()[0].focus();
|
|
}
|
|
}
|
|
}
|
|
|
|
elm = elm.parentNode;
|
|
}
|
|
});
|
|
|
|
// TODO: Rework this
|
|
self.on('keydown', function (e) {
|
|
var rootControl;
|
|
|
|
if (e.keyCode == 13 && e.target.nodeName === 'INPUT') {
|
|
e.preventDefault();
|
|
|
|
// Find root control that we can do toJSON on
|
|
self.parents().reverse().each(function (ctrl) {
|
|
if (ctrl.toJSON) {
|
|
rootControl = ctrl;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Fire event on current text box with the serialized data of the whole form
|
|
self.fire('submit', { data: rootControl.toJSON() });
|
|
}
|
|
});
|
|
|
|
self.on('keyup', function (e) {
|
|
if (e.target.nodeName == "INPUT") {
|
|
var oldValue = self.state.get('value');
|
|
var newValue = e.target.value;
|
|
|
|
if (newValue !== oldValue) {
|
|
self.state.set('value', newValue);
|
|
self.fire('autocomplete', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
self.on('mouseover', function (e) {
|
|
var tooltip = self.tooltip().moveTo(-0xFFFF);
|
|
|
|
if (self.statusLevel() && e.target.className.indexOf(self.classPrefix + 'status') !== -1) {
|
|
var statusMessage = self.statusMessage() || 'Ok';
|
|
var rel = tooltip.text(statusMessage).show().testMoveRel(e.target, ['bc-tc', 'bc-tl', 'bc-tr']);
|
|
|
|
tooltip.classes.toggle('tooltip-n', rel == 'bc-tc');
|
|
tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl');
|
|
tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr');
|
|
|
|
tooltip.moveRel(e.target, rel);
|
|
}
|
|
});
|
|
},
|
|
|
|
statusLevel: function (value) {
|
|
if (arguments.length > 0) {
|
|
this.state.set('statusLevel', value);
|
|
}
|
|
|
|
return this.state.get('statusLevel');
|
|
},
|
|
|
|
statusMessage: function (value) {
|
|
if (arguments.length > 0) {
|
|
this.state.set('statusMessage', value);
|
|
}
|
|
|
|
return this.state.get('statusMessage');
|
|
},
|
|
|
|
showMenu: function () {
|
|
var self = this, settings = self.settings, menu;
|
|
|
|
if (!self.menu) {
|
|
menu = settings.menu || [];
|
|
|
|
// Is menu array then auto constuct menu control
|
|
if (menu.length) {
|
|
menu = {
|
|
type: 'menu',
|
|
items: menu
|
|
};
|
|
} else {
|
|
menu.type = menu.type || 'menu';
|
|
}
|
|
|
|
self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm());
|
|
self.fire('createmenu');
|
|
self.menu.reflow();
|
|
self.menu.on('cancel', function (e) {
|
|
if (e.control === self.menu) {
|
|
self.focus();
|
|
}
|
|
});
|
|
|
|
self.menu.on('show hide', function (e) {
|
|
e.control.items().each(function (ctrl) {
|
|
ctrl.active(ctrl.value() == self.value());
|
|
});
|
|
}).fire('show');
|
|
|
|
self.menu.on('select', function (e) {
|
|
self.value(e.control.value());
|
|
});
|
|
|
|
self.on('focusin', function (e) {
|
|
if (e.target.tagName.toUpperCase() == 'INPUT') {
|
|
self.menu.hide();
|
|
}
|
|
});
|
|
|
|
self.aria('expanded', true);
|
|
}
|
|
|
|
self.menu.show();
|
|
self.menu.layoutRect({ w: self.layoutRect().w });
|
|
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
|
|
},
|
|
|
|
/**
|
|
* Focuses the input area of the control.
|
|
*
|
|
* @method focus
|
|
*/
|
|
focus: function () {
|
|
this.getEl('inp').focus();
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect();
|
|
var width, lineHeight, innerPadding = 0, inputElm = elm.firstChild;
|
|
|
|
if (self.statusLevel() && self.statusLevel() !== 'none') {
|
|
innerPadding = (
|
|
parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-right'), 10) -
|
|
parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-left'), 10)
|
|
);
|
|
}
|
|
|
|
if (openElm) {
|
|
width = rect.w - DomUtils.getSize(openElm).width - 10;
|
|
} else {
|
|
width = rect.w - 10;
|
|
}
|
|
|
|
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
|
|
var doc = document;
|
|
if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
|
|
lineHeight = (self.layoutRect().h - 2) + 'px';
|
|
}
|
|
|
|
$(inputElm).css({
|
|
width: width - innerPadding,
|
|
lineHeight: lineHeight
|
|
});
|
|
|
|
self._super();
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Post render method. Called after the control has been rendered to the target.
|
|
*
|
|
* @method postRender
|
|
* @return {tinymce.ui.ComboBox} Current combobox instance.
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
$(this.getEl('inp')).on('change', function (e) {
|
|
self.state.set('value', e.target.value);
|
|
self.fire('change', e);
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix;
|
|
var value = self.state.get('value') || '';
|
|
var icon, text, openBtnHtml = '', extraAttrs = '', statusHtml = '';
|
|
|
|
if ("spellcheck" in settings) {
|
|
extraAttrs += ' spellcheck="' + settings.spellcheck + '"';
|
|
}
|
|
|
|
if (settings.maxLength) {
|
|
extraAttrs += ' maxlength="' + settings.maxLength + '"';
|
|
}
|
|
|
|
if (settings.size) {
|
|
extraAttrs += ' size="' + settings.size + '"';
|
|
}
|
|
|
|
if (settings.subtype) {
|
|
extraAttrs += ' type="' + settings.subtype + '"';
|
|
}
|
|
|
|
statusHtml = '<i id="' + id + '-status" class="mce-status mce-ico" style="display: none"></i>';
|
|
|
|
if (self.disabled()) {
|
|
extraAttrs += ' disabled="disabled"';
|
|
}
|
|
|
|
icon = settings.icon;
|
|
if (icon && icon != 'caret') {
|
|
icon = prefix + 'ico ' + prefix + 'i-' + settings.icon;
|
|
}
|
|
|
|
text = self.state.get('text');
|
|
|
|
if (icon || text) {
|
|
openBtnHtml = (
|
|
'<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1" role="button">' +
|
|
'<button id="' + id + '-action" type="button" hidefocus="1" tabindex="-1">' +
|
|
(icon != 'caret' ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') +
|
|
(text ? (icon ? ' ' : '') + text : '') +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
|
|
self.classes.add('has-open');
|
|
}
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '">' +
|
|
'<input id="' + id + '-inp" class="' + prefix + 'textbox" value="' +
|
|
self.encode(value, false) + '" hidefocus="1"' + extraAttrs + ' placeholder="' +
|
|
self.encode(settings.placeholder) + '" />' +
|
|
statusHtml +
|
|
openBtnHtml +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
value: function (value) {
|
|
if (arguments.length) {
|
|
this.state.set('value', value);
|
|
return this;
|
|
}
|
|
|
|
// Make sure the real state is in sync
|
|
if (this.state.get('rendered')) {
|
|
this.state.set('value', this.getEl('inp').value);
|
|
}
|
|
|
|
return this.state.get('value');
|
|
},
|
|
|
|
showAutoComplete: function (items, term) {
|
|
var self = this;
|
|
|
|
if (items.length === 0) {
|
|
self.hideMenu();
|
|
return;
|
|
}
|
|
|
|
var insert = function (value, title) {
|
|
return function () {
|
|
self.fire('selectitem', {
|
|
title: title,
|
|
value: value
|
|
});
|
|
};
|
|
};
|
|
|
|
if (self.menu) {
|
|
self.menu.items().remove();
|
|
} else {
|
|
self.menu = Factory.create({
|
|
type: 'menu',
|
|
classes: 'combobox-menu',
|
|
layout: 'flow'
|
|
}).parent(self).renderTo();
|
|
}
|
|
|
|
Tools.each(items, function (item) {
|
|
self.menu.add({
|
|
text: item.title,
|
|
url: item.previewUrl,
|
|
match: term,
|
|
classes: 'menu-item-ellipsis',
|
|
onclick: insert(item.value, item.title)
|
|
});
|
|
});
|
|
|
|
self.menu.renderNew();
|
|
self.hideMenu();
|
|
|
|
self.menu.on('cancel', function (e) {
|
|
if (e.control.parent() === self.menu) {
|
|
e.stopPropagation();
|
|
self.focus();
|
|
self.hideMenu();
|
|
}
|
|
});
|
|
|
|
self.menu.on('select', function () {
|
|
self.focus();
|
|
});
|
|
|
|
var maxW = self.layoutRect().w;
|
|
self.menu.layoutRect({ w: maxW, minW: 0, maxW: maxW });
|
|
self.menu.reflow();
|
|
self.menu.show();
|
|
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
|
|
},
|
|
|
|
hideMenu: function () {
|
|
if (this.menu) {
|
|
this.menu.hide();
|
|
}
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:value', function (e) {
|
|
if (self.getEl('inp').value != e.value) {
|
|
self.getEl('inp').value = e.value;
|
|
}
|
|
});
|
|
|
|
self.state.on('change:disabled', function (e) {
|
|
self.getEl('inp').disabled = e.value;
|
|
});
|
|
|
|
self.state.on('change:statusLevel', function (e) {
|
|
var statusIconElm = self.getEl('status');
|
|
var prefix = self.classPrefix, value = e.value;
|
|
|
|
DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : '');
|
|
DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok');
|
|
DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn');
|
|
DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error');
|
|
self.classes.toggle('has-status', value !== 'none');
|
|
self.repaint();
|
|
});
|
|
|
|
DomUtils.on(self.getEl('status'), 'mouseleave', function () {
|
|
self.tooltip().hide();
|
|
});
|
|
|
|
self.on('cancel', function (e) {
|
|
if (self.menu && self.menu.visible()) {
|
|
e.stopPropagation();
|
|
self.hideMenu();
|
|
}
|
|
});
|
|
|
|
var focusIdx = function (idx, menu) {
|
|
if (menu && menu.items().length > 0) {
|
|
menu.items().eq(idx)[0].focus();
|
|
}
|
|
};
|
|
|
|
self.on('keydown', function (e) {
|
|
var keyCode = e.keyCode;
|
|
|
|
if (e.target.nodeName === 'INPUT') {
|
|
if (keyCode === VK.DOWN) {
|
|
e.preventDefault();
|
|
self.fire('autocomplete');
|
|
focusIdx(0, self.menu);
|
|
} else if (keyCode === VK.UP) {
|
|
e.preventDefault();
|
|
focusIdx(-1, self.menu);
|
|
}
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
remove: function () {
|
|
$(this.getEl('inp')).off();
|
|
|
|
if (this.menu) {
|
|
this.menu.remove();
|
|
}
|
|
|
|
this._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* ColorBox.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 widget lets you enter colors and browse for colors by pressing the color button. It also displays
|
|
* a preview of the current color.
|
|
*
|
|
* @-x-less ColorBox.less
|
|
* @class tinymce.ui.ColorBox
|
|
* @extends tinymce.ui.ComboBox
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ColorBox',
|
|
[
|
|
"tinymce.core.ui.ComboBox"
|
|
],
|
|
function (ComboBox) {
|
|
"use strict";
|
|
|
|
return ComboBox.extend({
|
|
/**
|
|
* Constructs a new control instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
settings.spellcheck = false;
|
|
|
|
if (settings.onaction) {
|
|
settings.icon = 'none';
|
|
}
|
|
|
|
self._super(settings);
|
|
|
|
self.classes.add('colorbox');
|
|
self.on('change keyup postrender', function () {
|
|
self.repaintColor(self.value());
|
|
});
|
|
},
|
|
|
|
repaintColor: function (value) {
|
|
var openElm = this.getEl('open');
|
|
var elm = openElm ? openElm.getElementsByTagName('i')[0] : null;
|
|
|
|
if (elm) {
|
|
try {
|
|
elm.style.background = value;
|
|
} catch (ex) {
|
|
// Ignore
|
|
}
|
|
}
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:value', function (e) {
|
|
if (self.state.get('rendered')) {
|
|
self.repaintColor(e.value);
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* PanelButton.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new panel button.
|
|
*
|
|
* @class tinymce.ui.PanelButton
|
|
* @extends tinymce.ui.Button
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.PanelButton',
|
|
[
|
|
"tinymce.core.ui.Button",
|
|
"tinymce.core.ui.FloatPanel"
|
|
],
|
|
function (Button, FloatPanel) {
|
|
"use strict";
|
|
|
|
return Button.extend({
|
|
/**
|
|
* Shows the panel for the button.
|
|
*
|
|
* @method showPanel
|
|
*/
|
|
showPanel: function () {
|
|
var self = this, settings = self.settings;
|
|
|
|
self.active(true);
|
|
|
|
if (!self.panel) {
|
|
var panelSettings = settings.panel;
|
|
|
|
// Wrap panel in grid layout if type if specified
|
|
// This makes it possible to add forms or other containers directly in the panel option
|
|
if (panelSettings.type) {
|
|
panelSettings = {
|
|
layout: 'grid',
|
|
items: panelSettings
|
|
};
|
|
}
|
|
|
|
panelSettings.role = panelSettings.role || 'dialog';
|
|
panelSettings.popover = true;
|
|
panelSettings.autohide = true;
|
|
panelSettings.ariaRoot = true;
|
|
|
|
self.panel = new FloatPanel(panelSettings).on('hide', function () {
|
|
self.active(false);
|
|
}).on('cancel', function (e) {
|
|
e.stopPropagation();
|
|
self.focus();
|
|
self.hidePanel();
|
|
}).parent(self).renderTo(self.getContainerElm());
|
|
|
|
self.panel.fire('show');
|
|
self.panel.reflow();
|
|
} else {
|
|
self.panel.show();
|
|
}
|
|
|
|
self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
|
|
},
|
|
|
|
/**
|
|
* Hides the panel for the button.
|
|
*
|
|
* @method hidePanel
|
|
*/
|
|
hidePanel: function () {
|
|
var self = this;
|
|
|
|
if (self.panel) {
|
|
self.panel.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self.aria('haspopup', true);
|
|
|
|
self.on('click', function (e) {
|
|
if (e.control === self) {
|
|
if (self.panel && self.panel.visible()) {
|
|
self.hidePanel();
|
|
} else {
|
|
self.showPanel();
|
|
self.panel.focus(!!e.aria);
|
|
}
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
remove: function () {
|
|
if (this.panel) {
|
|
this.panel.remove();
|
|
this.panel = null;
|
|
}
|
|
|
|
return this._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* ColorButton.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 creates a color button control. This is a split button in which the main
|
|
* button has a visual representation of the currently selected color. When clicked
|
|
* the caret button displays a color picker, allowing the user to select a new color.
|
|
*
|
|
* @-x-less ColorButton.less
|
|
* @class tinymce.ui.ColorButton
|
|
* @extends tinymce.ui.PanelButton
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ColorButton',
|
|
[
|
|
"tinymce.core.ui.PanelButton",
|
|
"tinymce.core.dom.DOMUtils"
|
|
],
|
|
function (PanelButton, DomUtils) {
|
|
"use strict";
|
|
|
|
var DOM = DomUtils.DOM;
|
|
|
|
return PanelButton.extend({
|
|
/**
|
|
* Constructs a new ColorButton instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
this._super(settings);
|
|
this.classes.add('colorbutton');
|
|
},
|
|
|
|
/**
|
|
* Getter/setter for the current color.
|
|
*
|
|
* @method color
|
|
* @param {String} [color] Color to set.
|
|
* @return {String|tinymce.ui.ColorButton} Current color or current instance.
|
|
*/
|
|
color: function (color) {
|
|
if (color) {
|
|
this._color = color;
|
|
this.getEl('preview').style.backgroundColor = color;
|
|
return this;
|
|
}
|
|
|
|
return this._color;
|
|
},
|
|
|
|
/**
|
|
* Resets the current color.
|
|
*
|
|
* @method resetColor
|
|
* @return {tinymce.ui.ColorButton} Current instance.
|
|
*/
|
|
resetColor: function () {
|
|
this._color = null;
|
|
this.getEl('preview').style.backgroundColor = null;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text');
|
|
var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
|
var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '',
|
|
textHtml = '';
|
|
|
|
if (text) {
|
|
self.classes.add('btn-has-text');
|
|
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
|
|
}
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1" aria-haspopup="true">' +
|
|
'<button role="presentation" hidefocus="1" type="button" tabindex="-1">' +
|
|
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
|
'<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' +
|
|
textHtml +
|
|
'</button>' +
|
|
'<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
|
|
' <i class="' + prefix + 'caret"></i>' +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this, onClickHandler = self.settings.onclick;
|
|
|
|
self.on('click', function (e) {
|
|
if (e.aria && e.aria.key == 'down') {
|
|
return;
|
|
}
|
|
|
|
if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
|
|
e.stopImmediatePropagation();
|
|
onClickHandler.call(self, e);
|
|
}
|
|
});
|
|
|
|
delete self.settings.onclick;
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ColorPicker.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
|
|
*/
|
|
|
|
/**
|
|
* Color picker widget lets you select colors.
|
|
*
|
|
* @-x-less ColorPicker.less
|
|
* @class tinymce.ui.ColorPicker
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ColorPicker',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.DragHelper",
|
|
"tinymce.core.ui.DomUtils",
|
|
"tinymce.core.util.Color"
|
|
],
|
|
function (Widget, DragHelper, DomUtils, Color) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
classes: "widget colorpicker"
|
|
},
|
|
|
|
/**
|
|
* Constructs a new colorpicker instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {String} color Initial color value.
|
|
*/
|
|
init: function (settings) {
|
|
this._super(settings);
|
|
},
|
|
|
|
postRender: function () {
|
|
var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm;
|
|
|
|
hueRootElm = self.getEl('h');
|
|
huePointElm = self.getEl('hp');
|
|
svRootElm = self.getEl('sv');
|
|
svPointElm = self.getEl('svp');
|
|
|
|
function getPos(elm, event) {
|
|
var pos = DomUtils.getPos(elm), x, y;
|
|
|
|
x = event.pageX - pos.x;
|
|
y = event.pageY - pos.y;
|
|
|
|
x = Math.max(0, Math.min(x / elm.clientWidth, 1));
|
|
y = Math.max(0, Math.min(y / elm.clientHeight, 1));
|
|
|
|
return {
|
|
x: x,
|
|
y: y
|
|
};
|
|
}
|
|
|
|
function updateColor(hsv, hueUpdate) {
|
|
var hue = (360 - hsv.h) / 360;
|
|
|
|
DomUtils.css(huePointElm, {
|
|
top: (hue * 100) + '%'
|
|
});
|
|
|
|
if (!hueUpdate) {
|
|
DomUtils.css(svPointElm, {
|
|
left: hsv.s + '%',
|
|
top: (100 - hsv.v) + '%'
|
|
});
|
|
}
|
|
|
|
svRootElm.style.background = new Color({ s: 100, v: 100, h: hsv.h }).toHex();
|
|
self.color().parse({ s: hsv.s, v: hsv.v, h: hsv.h });
|
|
}
|
|
|
|
function updateSaturationAndValue(e) {
|
|
var pos;
|
|
|
|
pos = getPos(svRootElm, e);
|
|
hsv.s = pos.x * 100;
|
|
hsv.v = (1 - pos.y) * 100;
|
|
|
|
updateColor(hsv);
|
|
self.fire('change');
|
|
}
|
|
|
|
function updateHue(e) {
|
|
var pos;
|
|
|
|
pos = getPos(hueRootElm, e);
|
|
hsv = color.toHsv();
|
|
hsv.h = (1 - pos.y) * 360;
|
|
updateColor(hsv, true);
|
|
self.fire('change');
|
|
}
|
|
|
|
self._repaint = function () {
|
|
hsv = color.toHsv();
|
|
updateColor(hsv);
|
|
};
|
|
|
|
self._super();
|
|
|
|
self._svdraghelper = new DragHelper(self._id + '-sv', {
|
|
start: updateSaturationAndValue,
|
|
drag: updateSaturationAndValue
|
|
});
|
|
|
|
self._hdraghelper = new DragHelper(self._id + '-h', {
|
|
start: updateHue,
|
|
drag: updateHue
|
|
});
|
|
|
|
self._repaint();
|
|
},
|
|
|
|
rgb: function () {
|
|
return this.color().toRgb();
|
|
},
|
|
|
|
value: function (value) {
|
|
var self = this;
|
|
|
|
if (arguments.length) {
|
|
self.color().parse(value);
|
|
|
|
if (self._rendered) {
|
|
self._repaint();
|
|
}
|
|
} else {
|
|
return self.color().toHex();
|
|
}
|
|
},
|
|
|
|
color: function () {
|
|
if (!this._color) {
|
|
this._color = new Color();
|
|
}
|
|
|
|
return this._color;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix, hueHtml;
|
|
var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000';
|
|
|
|
function getOldIeFallbackHtml() {
|
|
var i, l, html = '', gradientPrefix, stopsList;
|
|
|
|
gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=';
|
|
stopsList = stops.split(',');
|
|
for (i = 0, l = stopsList.length - 1; i < l; i++) {
|
|
html += (
|
|
'<div class="' + prefix + 'colorpicker-h-chunk" style="' +
|
|
'height:' + (100 / l) + '%;' +
|
|
gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ');' +
|
|
'-ms-' + gradientPrefix + stopsList[i] + ',endColorstr=' + stopsList[i + 1] + ')' +
|
|
'"></div>'
|
|
);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
var gradientCssText = (
|
|
'background: -ms-linear-gradient(top,' + stops + ');' +
|
|
'background: linear-gradient(to bottom,' + stops + ');'
|
|
);
|
|
|
|
hueHtml = (
|
|
'<div id="' + id + '-h" class="' + prefix + 'colorpicker-h" style="' + gradientCssText + '">' +
|
|
getOldIeFallbackHtml() +
|
|
'<div id="' + id + '-hp" class="' + prefix + 'colorpicker-h-marker"></div>' +
|
|
'</div>'
|
|
);
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '">' +
|
|
'<div id="' + id + '-sv" class="' + prefix + 'colorpicker-sv">' +
|
|
'<div class="' + prefix + 'colorpicker-overlay1">' +
|
|
'<div class="' + prefix + 'colorpicker-overlay2">' +
|
|
'<div id="' + id + '-svp" class="' + prefix + 'colorpicker-selector1">' +
|
|
'<div class="' + prefix + 'colorpicker-selector2"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
hueHtml +
|
|
'</div>'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Path.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new path control.
|
|
*
|
|
* @-x-less Path.less
|
|
* @class tinymce.ui.Path
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Path',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {String} delimiter Delimiter to display between row in path.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
if (!settings.delimiter) {
|
|
settings.delimiter = '\u00BB';
|
|
}
|
|
|
|
self._super(settings);
|
|
self.classes.add('path');
|
|
self.canFocus = true;
|
|
|
|
self.on('click', function (e) {
|
|
var index, target = e.target;
|
|
|
|
if ((index = target.getAttribute('data-index'))) {
|
|
self.fire('select', { value: self.row()[index], index: index });
|
|
}
|
|
});
|
|
|
|
self.row(self.settings.row);
|
|
},
|
|
|
|
/**
|
|
* Focuses the current control.
|
|
*
|
|
* @method focus
|
|
* @return {tinymce.ui.Control} Current control instance.
|
|
*/
|
|
focus: function () {
|
|
var self = this;
|
|
|
|
self.getEl().firstChild.focus();
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Sets/gets the data to be used for the path.
|
|
*
|
|
* @method row
|
|
* @param {Array} row Array with row name is rendered to path.
|
|
*/
|
|
row: function (row) {
|
|
if (!arguments.length) {
|
|
return this.state.get('row');
|
|
}
|
|
|
|
this.state.set('row', row);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this;
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '">' +
|
|
self._getDataPathHtml(self.state.get('row')) +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:row', function (e) {
|
|
self.innerHtml(self._getDataPathHtml(e.value));
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
_getDataPathHtml: function (data) {
|
|
var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix;
|
|
|
|
for (i = 0, l = parts.length; i < l; i++) {
|
|
html += (
|
|
(i > 0 ? '<div class="' + prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') +
|
|
'<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' +
|
|
i + '" tabindex="-1" id="' + self._id + '-' + i + '" aria-level="' + (i + 1) + '">' + parts[i].name + '</div>'
|
|
);
|
|
}
|
|
|
|
if (!html) {
|
|
html = '<div class="' + prefix + 'path-item">\u00a0</div>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ElementPath.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 control creates an path for the current selections parent elements in TinyMCE.
|
|
*
|
|
* @class tinymce.ui.ElementPath
|
|
* @extends tinymce.ui.Path
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ElementPath',
|
|
[
|
|
"tinymce.core.ui.Path"
|
|
],
|
|
function (Path) {
|
|
return Path.extend({
|
|
/**
|
|
* Post render method. Called after the control has been rendered to the target.
|
|
*
|
|
* @method postRender
|
|
* @return {tinymce.ui.ElementPath} Current combobox instance.
|
|
*/
|
|
postRender: function () {
|
|
var self = this, editor = self.settings.editor;
|
|
|
|
function isHidden(elm) {
|
|
if (elm.nodeType === 1) {
|
|
if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) {
|
|
return true;
|
|
}
|
|
|
|
if (elm.getAttribute('data-mce-type') === 'bookmark') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (editor.settings.elementpath !== false) {
|
|
self.on('select', function (e) {
|
|
editor.focus();
|
|
editor.selection.select(this.row()[e.index].element);
|
|
editor.nodeChanged();
|
|
});
|
|
|
|
editor.on('nodeChange', function (e) {
|
|
var outParents = [], parents = e.parents, i = parents.length;
|
|
|
|
while (i--) {
|
|
if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
|
|
var args = editor.fire('ResolveName', {
|
|
name: parents[i].nodeName.toLowerCase(),
|
|
target: parents[i]
|
|
});
|
|
|
|
if (!args.isDefaultPrevented()) {
|
|
outParents.push({ name: args.name, element: parents[i] });
|
|
}
|
|
|
|
if (args.isPropagationStopped()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.row(outParents);
|
|
});
|
|
}
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FormItem.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 container created by the form element with
|
|
* a label and control item.
|
|
*
|
|
* @class tinymce.ui.FormItem
|
|
* @extends tinymce.ui.Container
|
|
* @setting {String} label Label to display for the form item.
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FormItem',
|
|
[
|
|
"tinymce.core.ui.Container"
|
|
],
|
|
function (Container) {
|
|
"use strict";
|
|
|
|
return Container.extend({
|
|
Defaults: {
|
|
layout: 'flex',
|
|
align: 'center',
|
|
defaults: {
|
|
flex: 1
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, prefix = self.classPrefix;
|
|
|
|
self.classes.add('formitem');
|
|
layout.preRender(self);
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
|
|
(self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' +
|
|
self.settings.title + '</div>') : '') +
|
|
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
|
|
(self.settings.html || '') + layout.renderHtml(self) +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Form.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 creates a form container. A form container has the ability
|
|
* to automatically wrap items in tinymce.ui.FormItem instances.
|
|
*
|
|
* Each FormItem instance is a container for the label and the item.
|
|
*
|
|
* @example
|
|
* tinymce.ui.Factory.create({
|
|
* type: 'form',
|
|
* items: [
|
|
* {type: 'textbox', label: 'My text box'}
|
|
* ]
|
|
* }).renderTo(document.body);
|
|
*
|
|
* @class tinymce.ui.Form
|
|
* @extends tinymce.ui.Container
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Form',
|
|
[
|
|
"tinymce.core.ui.Container",
|
|
"tinymce.core.ui.FormItem",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (Container, FormItem, Tools) {
|
|
"use strict";
|
|
|
|
return Container.extend({
|
|
Defaults: {
|
|
containerCls: 'form',
|
|
layout: 'flex',
|
|
direction: 'column',
|
|
align: 'stretch',
|
|
flex: 1,
|
|
padding: 20,
|
|
labelGap: 30,
|
|
spacing: 10,
|
|
callbacks: {
|
|
submit: function () {
|
|
this.submit();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This method gets invoked before the control is rendered.
|
|
*
|
|
* @method preRender
|
|
*/
|
|
preRender: function () {
|
|
var self = this, items = self.items();
|
|
|
|
if (!self.settings.formItemDefaults) {
|
|
self.settings.formItemDefaults = {
|
|
layout: 'flex',
|
|
autoResize: "overflow",
|
|
defaults: { flex: 1 }
|
|
};
|
|
}
|
|
|
|
// Wrap any labeled items in FormItems
|
|
items.each(function (ctrl) {
|
|
var formItem, label = ctrl.settings.label;
|
|
|
|
if (label) {
|
|
formItem = new FormItem(Tools.extend({
|
|
items: {
|
|
type: 'label',
|
|
id: ctrl._id + '-l',
|
|
text: label,
|
|
flex: 0,
|
|
forId: ctrl._id,
|
|
disabled: ctrl.disabled()
|
|
}
|
|
}, self.settings.formItemDefaults));
|
|
|
|
formItem.type = 'formitem';
|
|
ctrl.aria('labelledby', ctrl._id + '-l');
|
|
|
|
if (typeof ctrl.settings.flex == "undefined") {
|
|
ctrl.settings.flex = 1;
|
|
}
|
|
|
|
self.replace(ctrl, formItem);
|
|
formItem.add(ctrl);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Fires a submit event with the serialized form.
|
|
*
|
|
* @method submit
|
|
* @return {Object} Event arguments object.
|
|
*/
|
|
submit: function () {
|
|
return this.fire('submit', { data: this.toJSON() });
|
|
},
|
|
|
|
/**
|
|
* Post render method. Called after the control has been rendered to the target.
|
|
*
|
|
* @method postRender
|
|
* @return {tinymce.ui.ComboBox} Current combobox instance.
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self._super();
|
|
self.fromJSON(self.settings.data);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self._super();
|
|
|
|
function recalcLabels() {
|
|
var maxLabelWidth = 0, labels = [], i, labelGap, items;
|
|
|
|
if (self.settings.labelGapCalc === false) {
|
|
return;
|
|
}
|
|
|
|
if (self.settings.labelGapCalc == "children") {
|
|
items = self.find('formitem');
|
|
} else {
|
|
items = self.items();
|
|
}
|
|
|
|
items.filter('formitem').each(function (item) {
|
|
var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth;
|
|
|
|
maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth;
|
|
labels.push(labelCtrl);
|
|
});
|
|
|
|
labelGap = self.settings.labelGap || 0;
|
|
|
|
i = labels.length;
|
|
while (i--) {
|
|
labels[i].settings.minWidth = maxLabelWidth + labelGap;
|
|
}
|
|
}
|
|
|
|
self.on('show', recalcLabels);
|
|
recalcLabels();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FieldSet.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 creates fieldset containers.
|
|
*
|
|
* @-x-less FieldSet.less
|
|
* @class tinymce.ui.FieldSet
|
|
* @extends tinymce.ui.Form
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FieldSet',
|
|
[
|
|
"tinymce.core.ui.Form"
|
|
],
|
|
function (Form) {
|
|
"use strict";
|
|
|
|
return Form.extend({
|
|
Defaults: {
|
|
containerCls: 'fieldset',
|
|
layout: 'flex',
|
|
direction: 'column',
|
|
align: 'stretch',
|
|
flex: 1,
|
|
padding: "25 15 5 15",
|
|
labelGap: 30,
|
|
spacing: 10,
|
|
border: 1
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, prefix = self.classPrefix;
|
|
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
return (
|
|
'<fieldset id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
|
|
(self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' +
|
|
self.settings.title + '</legend>') : '') +
|
|
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
|
|
(self.settings.html || '') + layout.renderHtml(self) +
|
|
'</div>' +
|
|
'</fieldset>'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* LinkTargets.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 is enables you to get anything that you can link to in a element.
|
|
*
|
|
* @private
|
|
* @class tinymce.content.LinkTargets
|
|
*/
|
|
define(
|
|
'tinymce.core.content.LinkTargets',
|
|
[
|
|
'tinymce.core.dom.DOMUtils',
|
|
'tinymce.core.dom.NodeType',
|
|
'tinymce.core.util.Arr',
|
|
'tinymce.core.util.Fun',
|
|
'tinymce.core.util.Tools',
|
|
'tinymce.core.util.Uuid'
|
|
],
|
|
function (DOMUtils, NodeType, Arr, Fun, Tools, Uuid) {
|
|
var trim = Tools.trim;
|
|
|
|
var create = function (type, title, url, level, attach) {
|
|
return {
|
|
type: type,
|
|
title: title,
|
|
url: url,
|
|
level: level,
|
|
attach: attach
|
|
};
|
|
};
|
|
|
|
var isChildOfContentEditableTrue = function (node) {
|
|
while ((node = node.parentNode)) {
|
|
var value = node.contentEditable;
|
|
if (value && value !== 'inherit') {
|
|
return NodeType.isContentEditableTrue(node);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
var select = function (selector, root) {
|
|
return DOMUtils.DOM.select(selector, root);
|
|
};
|
|
|
|
var getElementText = function (elm) {
|
|
return elm.innerText || elm.textContent;
|
|
};
|
|
|
|
var getOrGenerateId = function (elm) {
|
|
return elm.id ? elm.id : Uuid.uuid('h');
|
|
};
|
|
|
|
var isAnchor = function (elm) {
|
|
return elm && elm.nodeName === 'A' && (elm.id || elm.name);
|
|
};
|
|
|
|
var isValidAnchor = function (elm) {
|
|
return isAnchor(elm) && isEditable(elm);
|
|
};
|
|
|
|
var isHeader = function (elm) {
|
|
return elm && /^(H[1-6])$/.test(elm.nodeName);
|
|
};
|
|
|
|
var isEditable = function (elm) {
|
|
return isChildOfContentEditableTrue(elm) && !NodeType.isContentEditableFalse(elm);
|
|
};
|
|
|
|
var isValidHeader = function (elm) {
|
|
return isHeader(elm) && isEditable(elm);
|
|
};
|
|
|
|
var getLevel = function (elm) {
|
|
return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0;
|
|
};
|
|
|
|
var headerTarget = function (elm) {
|
|
var headerId = getOrGenerateId(elm);
|
|
|
|
var attach = function () {
|
|
elm.id = headerId;
|
|
};
|
|
|
|
return create('header', getElementText(elm), '#' + headerId, getLevel(elm), attach);
|
|
};
|
|
|
|
var anchorTarget = function (elm) {
|
|
var anchorId = elm.id || elm.name;
|
|
var anchorText = getElementText(elm);
|
|
|
|
return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, Fun.noop);
|
|
};
|
|
|
|
var getHeaderTargets = function (elms) {
|
|
return Arr.map(Arr.filter(elms, isValidHeader), headerTarget);
|
|
};
|
|
|
|
var getAnchorTargets = function (elms) {
|
|
return Arr.map(Arr.filter(elms, isValidAnchor), anchorTarget);
|
|
};
|
|
|
|
var getTargetElements = function (elm) {
|
|
var elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm);
|
|
return elms;
|
|
};
|
|
|
|
var hasTitle = function (target) {
|
|
return trim(target.title).length > 0;
|
|
};
|
|
|
|
var find = function (elm) {
|
|
var elms = getTargetElements(elm);
|
|
return Arr.filter(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle);
|
|
};
|
|
|
|
return {
|
|
find: find
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* FilePicker.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 creates a file picker control.
|
|
*
|
|
* @class tinymce.ui.FilePicker
|
|
* @extends tinymce.ui.ComboBox
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FilePicker',
|
|
[
|
|
'global!window',
|
|
'tinymce.core.content.LinkTargets',
|
|
'tinymce.core.EditorManager',
|
|
'tinymce.core.ui.ComboBox',
|
|
'tinymce.core.util.Arr',
|
|
'tinymce.core.util.Fun',
|
|
'tinymce.core.util.Tools'
|
|
],
|
|
function (window, LinkTargets, EditorManager, ComboBox, Arr, Fun, Tools) {
|
|
"use strict";
|
|
|
|
var getActiveEditor = function () {
|
|
return window.tinymce ? window.tinymce.activeEditor : EditorManager.activeEditor;
|
|
};
|
|
|
|
var history = {};
|
|
var HISTORY_LENGTH = 5;
|
|
|
|
var toMenuItem = function (target) {
|
|
return {
|
|
title: target.title,
|
|
value: {
|
|
title: { raw: target.title },
|
|
url: target.url,
|
|
attach: target.attach
|
|
}
|
|
};
|
|
};
|
|
|
|
var toMenuItems = function (targets) {
|
|
return Tools.map(targets, toMenuItem);
|
|
};
|
|
|
|
var staticMenuItem = function (title, url) {
|
|
return {
|
|
title: title,
|
|
value: {
|
|
title: title,
|
|
url: url,
|
|
attach: Fun.noop
|
|
}
|
|
};
|
|
};
|
|
|
|
var isUniqueUrl = function (url, targets) {
|
|
var foundTarget = Arr.find(targets, function (target) {
|
|
return target.url === url;
|
|
});
|
|
|
|
return !foundTarget;
|
|
};
|
|
|
|
var getSetting = function (editorSettings, name, defaultValue) {
|
|
var value = name in editorSettings ? editorSettings[name] : defaultValue;
|
|
return value === false ? null : value;
|
|
};
|
|
|
|
var createMenuItems = function (term, targets, fileType, editorSettings) {
|
|
var separator = { title: '-' };
|
|
|
|
var fromHistoryMenuItems = function (history) {
|
|
var uniqueHistory = Arr.filter(history[fileType], function (url) {
|
|
return isUniqueUrl(url, targets);
|
|
});
|
|
|
|
return Tools.map(uniqueHistory, function (url) {
|
|
return {
|
|
title: url,
|
|
value: {
|
|
title: url,
|
|
url: url,
|
|
attach: Fun.noop
|
|
}
|
|
};
|
|
});
|
|
};
|
|
|
|
var fromMenuItems = function (type) {
|
|
var filteredTargets = Arr.filter(targets, function (target) {
|
|
return target.type == type;
|
|
});
|
|
|
|
return toMenuItems(filteredTargets);
|
|
};
|
|
|
|
var anchorMenuItems = function () {
|
|
var anchorMenuItems = fromMenuItems('anchor');
|
|
var topAnchor = getSetting(editorSettings, 'anchor_top', '#top');
|
|
var bottomAchor = getSetting(editorSettings, 'anchor_bottom', '#bottom');
|
|
|
|
if (topAnchor !== null) {
|
|
anchorMenuItems.unshift(staticMenuItem('<top>', topAnchor));
|
|
}
|
|
|
|
if (bottomAchor !== null) {
|
|
anchorMenuItems.push(staticMenuItem('<bottom>', bottomAchor));
|
|
}
|
|
|
|
return anchorMenuItems;
|
|
};
|
|
|
|
var join = function (items) {
|
|
return Arr.reduce(items, function (a, b) {
|
|
var bothEmpty = a.length === 0 || b.length === 0;
|
|
return bothEmpty ? a.concat(b) : a.concat(separator, b);
|
|
}, []);
|
|
};
|
|
|
|
if (editorSettings.typeahead_urls === false) {
|
|
return [];
|
|
}
|
|
|
|
return fileType === 'file' ? join([
|
|
filterByQuery(term, fromHistoryMenuItems(history)),
|
|
filterByQuery(term, fromMenuItems('header')),
|
|
filterByQuery(term, anchorMenuItems())
|
|
]) : filterByQuery(term, fromHistoryMenuItems(history));
|
|
};
|
|
|
|
var addToHistory = function (url, fileType) {
|
|
var items = history[fileType];
|
|
|
|
if (!/^https?/.test(url)) {
|
|
return;
|
|
}
|
|
|
|
if (items) {
|
|
if (Arr.indexOf(items, url) === -1) {
|
|
history[fileType] = items.slice(0, HISTORY_LENGTH).concat(url);
|
|
}
|
|
} else {
|
|
history[fileType] = [url];
|
|
}
|
|
};
|
|
|
|
var filterByQuery = function (term, menuItems) {
|
|
var lowerCaseTerm = term.toLowerCase();
|
|
var result = Tools.grep(menuItems, function (item) {
|
|
return item.title.toLowerCase().indexOf(lowerCaseTerm) !== -1;
|
|
});
|
|
|
|
return result.length === 1 && result[0].title === term ? [] : result;
|
|
};
|
|
|
|
var getTitle = function (linkDetails) {
|
|
var title = linkDetails.title;
|
|
return title.raw ? title.raw : title;
|
|
};
|
|
|
|
var setupAutoCompleteHandler = function (ctrl, editorSettings, bodyElm, fileType) {
|
|
var autocomplete = function (term) {
|
|
var linkTargets = LinkTargets.find(bodyElm);
|
|
var menuItems = createMenuItems(term, linkTargets, fileType, editorSettings);
|
|
ctrl.showAutoComplete(menuItems, term);
|
|
};
|
|
|
|
ctrl.on('autocomplete', function () {
|
|
autocomplete(ctrl.value());
|
|
});
|
|
|
|
ctrl.on('selectitem', function (e) {
|
|
var linkDetails = e.value;
|
|
|
|
ctrl.value(linkDetails.url);
|
|
var title = getTitle(linkDetails);
|
|
|
|
if (fileType === 'image') {
|
|
ctrl.fire('change', { meta: { alt: title, attach: linkDetails.attach } });
|
|
} else {
|
|
ctrl.fire('change', { meta: { text: title, attach: linkDetails.attach } });
|
|
}
|
|
|
|
ctrl.focus();
|
|
});
|
|
|
|
ctrl.on('click', function (e) {
|
|
if (ctrl.value().length === 0 && e.target.nodeName === 'INPUT') {
|
|
autocomplete('');
|
|
}
|
|
});
|
|
|
|
ctrl.on('PostRender', function () {
|
|
ctrl.getRoot().on('submit', function (e) {
|
|
if (!e.isDefaultPrevented()) {
|
|
addToHistory(ctrl.value(), fileType);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
var statusToUiState = function (result) {
|
|
var status = result.status, message = result.message;
|
|
|
|
if (status === 'valid') {
|
|
return { status: 'ok', message: message };
|
|
} else if (status === 'unknown') {
|
|
return { status: 'warn', message: message };
|
|
} else if (status === 'invalid') {
|
|
return { status: 'warn', message: message };
|
|
} else {
|
|
return { status: 'none', message: '' };
|
|
}
|
|
};
|
|
|
|
var setupLinkValidatorHandler = function (ctrl, editorSettings, fileType) {
|
|
var validatorHandler = editorSettings.filepicker_validator_handler;
|
|
if (validatorHandler) {
|
|
var validateUrl = function (url) {
|
|
if (url.length === 0) {
|
|
ctrl.statusLevel('none');
|
|
return;
|
|
}
|
|
|
|
validatorHandler({
|
|
url: url,
|
|
type: fileType
|
|
}, function (result) {
|
|
var uiState = statusToUiState(result);
|
|
|
|
ctrl.statusMessage(uiState.message);
|
|
ctrl.statusLevel(uiState.status);
|
|
});
|
|
};
|
|
|
|
ctrl.state.on('change:value', function (e) {
|
|
validateUrl(e.value);
|
|
});
|
|
}
|
|
};
|
|
|
|
return ComboBox.extend({
|
|
/**
|
|
* Constructs a new control instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this, editor = getActiveEditor(), editorSettings = editor.settings;
|
|
var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes;
|
|
var fileType = settings.filetype;
|
|
|
|
settings.spellcheck = false;
|
|
|
|
fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types;
|
|
if (fileBrowserCallbackTypes) {
|
|
fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/);
|
|
}
|
|
|
|
if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType]) {
|
|
fileBrowserCallback = editorSettings.file_picker_callback;
|
|
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
|
|
actionCallback = function () {
|
|
var meta = self.fire('beforecall').meta;
|
|
|
|
meta = Tools.extend({ filetype: fileType }, meta);
|
|
|
|
// file_picker_callback(callback, currentValue, metaData)
|
|
fileBrowserCallback.call(
|
|
editor,
|
|
function (value, meta) {
|
|
self.value(value).fire('change', { meta: meta });
|
|
},
|
|
self.value(),
|
|
meta
|
|
);
|
|
};
|
|
} else {
|
|
// Legacy callback: file_picker_callback(id, currentValue, filetype, window)
|
|
fileBrowserCallback = editorSettings.file_browser_callback;
|
|
if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) {
|
|
actionCallback = function () {
|
|
fileBrowserCallback(
|
|
self.getEl('inp').id,
|
|
self.value(),
|
|
fileType,
|
|
window
|
|
);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (actionCallback) {
|
|
settings.icon = 'browse';
|
|
settings.onaction = actionCallback;
|
|
}
|
|
|
|
self._super(settings);
|
|
|
|
setupAutoCompleteHandler(self, editorSettings, editor.getBody(), fileType);
|
|
setupLinkValidatorHandler(self, editorSettings, fileType);
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FitLayout.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 layout manager will resize the control to be the size of it's parent container.
|
|
* In other words width: 100% and height: 100%.
|
|
*
|
|
* @-x-less FitLayout.less
|
|
* @class tinymce.ui.FitLayout
|
|
* @extends tinymce.ui.AbsoluteLayout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FitLayout',
|
|
[
|
|
"tinymce.core.ui.AbsoluteLayout"
|
|
],
|
|
function (AbsoluteLayout) {
|
|
"use strict";
|
|
|
|
return AbsoluteLayout.extend({
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function (container) {
|
|
var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox;
|
|
|
|
container.items().filter(':visible').each(function (ctrl) {
|
|
ctrl.layoutRect({
|
|
x: paddingBox.left,
|
|
y: paddingBox.top,
|
|
w: contLayoutRect.innerW - paddingBox.right - paddingBox.left,
|
|
h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom
|
|
});
|
|
|
|
if (ctrl.recalc) {
|
|
ctrl.recalc();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FlexLayout.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 layout manager works similar to the CSS flex box.
|
|
*
|
|
* @setting {String} direction row|row-reverse|column|column-reverse
|
|
* @setting {Number} flex A positive-number to flex by.
|
|
* @setting {String} align start|end|center|stretch
|
|
* @setting {String} pack start|end|justify
|
|
*
|
|
* @class tinymce.ui.FlexLayout
|
|
* @extends tinymce.ui.AbsoluteLayout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FlexLayout',
|
|
[
|
|
"tinymce.core.ui.AbsoluteLayout"
|
|
],
|
|
function (AbsoluteLayout) {
|
|
"use strict";
|
|
|
|
return AbsoluteLayout.extend({
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function (container) {
|
|
// A ton of variables, needs to be in the same scope for performance
|
|
var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction;
|
|
var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos;
|
|
var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName;
|
|
var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName;
|
|
var alignDeltaSizeName, alignContentSizeName;
|
|
var max = Math.max, min = Math.min;
|
|
|
|
// Get container items, properties and settings
|
|
items = container.items().filter(':visible');
|
|
contLayoutRect = container.layoutRect();
|
|
contPaddingBox = container.paddingBox;
|
|
contSettings = container.settings;
|
|
direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction;
|
|
align = contSettings.align;
|
|
pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack;
|
|
spacing = contSettings.spacing || 0;
|
|
|
|
if (direction == "row-reversed" || direction == "column-reverse") {
|
|
items = items.set(items.toArray().reverse());
|
|
direction = direction.split('-')[0];
|
|
}
|
|
|
|
// Setup axis variable name for row/column direction since the calculations is the same
|
|
if (direction == "column") {
|
|
posName = "y";
|
|
sizeName = "h";
|
|
minSizeName = "minH";
|
|
maxSizeName = "maxH";
|
|
innerSizeName = "innerH";
|
|
beforeName = 'top';
|
|
deltaSizeName = "deltaH";
|
|
contentSizeName = "contentH";
|
|
|
|
alignBeforeName = "left";
|
|
alignSizeName = "w";
|
|
alignAxisName = "x";
|
|
alignInnerSizeName = "innerW";
|
|
alignMinSizeName = "minW";
|
|
alignAfterName = "right";
|
|
alignDeltaSizeName = "deltaW";
|
|
alignContentSizeName = "contentW";
|
|
} else {
|
|
posName = "x";
|
|
sizeName = "w";
|
|
minSizeName = "minW";
|
|
maxSizeName = "maxW";
|
|
innerSizeName = "innerW";
|
|
beforeName = 'left';
|
|
deltaSizeName = "deltaW";
|
|
contentSizeName = "contentW";
|
|
|
|
alignBeforeName = "top";
|
|
alignSizeName = "h";
|
|
alignAxisName = "y";
|
|
alignInnerSizeName = "innerH";
|
|
alignMinSizeName = "minH";
|
|
alignAfterName = "bottom";
|
|
alignDeltaSizeName = "deltaH";
|
|
alignContentSizeName = "contentH";
|
|
}
|
|
|
|
// Figure out total flex, availableSpace and collect any max size elements
|
|
availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName];
|
|
maxAlignEndPos = totalFlex = 0;
|
|
for (i = 0, l = items.length; i < l; i++) {
|
|
ctrl = items[i];
|
|
ctrlLayoutRect = ctrl.layoutRect();
|
|
ctrlSettings = ctrl.settings;
|
|
flex = ctrlSettings.flex;
|
|
availableSpace -= (i < l - 1 ? spacing : 0);
|
|
|
|
if (flex > 0) {
|
|
totalFlex += flex;
|
|
|
|
// Flexed item has a max size then we need to check if we will hit that size
|
|
if (ctrlLayoutRect[maxSizeName]) {
|
|
maxSizeItems.push(ctrl);
|
|
}
|
|
|
|
ctrlLayoutRect.flex = flex;
|
|
}
|
|
|
|
availableSpace -= ctrlLayoutRect[minSizeName];
|
|
|
|
// Calculate the align end position to be used to check for overflow/underflow
|
|
size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName];
|
|
if (size > maxAlignEndPos) {
|
|
maxAlignEndPos = size;
|
|
}
|
|
}
|
|
|
|
// Calculate minW/minH
|
|
rect = {};
|
|
if (availableSpace < 0) {
|
|
rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName];
|
|
} else {
|
|
rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName];
|
|
}
|
|
|
|
rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName];
|
|
|
|
rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace;
|
|
rect[alignContentSizeName] = maxAlignEndPos;
|
|
rect.minW = min(rect.minW, contLayoutRect.maxW);
|
|
rect.minH = min(rect.minH, contLayoutRect.maxH);
|
|
rect.minW = max(rect.minW, contLayoutRect.startMinWidth);
|
|
rect.minH = max(rect.minH, contLayoutRect.startMinHeight);
|
|
|
|
// Resize container container if minSize was changed
|
|
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
|
|
rect.w = rect.minW;
|
|
rect.h = rect.minH;
|
|
|
|
container.layoutRect(rect);
|
|
this.recalc(container);
|
|
|
|
// Forced recalc for example if items are hidden/shown
|
|
if (container._lastRect === null) {
|
|
var parentCtrl = container.parent();
|
|
if (parentCtrl) {
|
|
parentCtrl._lastRect = null;
|
|
parentCtrl.recalc();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Handle max size elements, check if they will become to wide with current options
|
|
ratio = availableSpace / totalFlex;
|
|
for (i = 0, l = maxSizeItems.length; i < l; i++) {
|
|
ctrl = maxSizeItems[i];
|
|
ctrlLayoutRect = ctrl.layoutRect();
|
|
maxSize = ctrlLayoutRect[maxSizeName];
|
|
size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio;
|
|
|
|
if (size > maxSize) {
|
|
availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]);
|
|
totalFlex -= ctrlLayoutRect.flex;
|
|
ctrlLayoutRect.flex = 0;
|
|
ctrlLayoutRect.maxFlexSize = maxSize;
|
|
} else {
|
|
ctrlLayoutRect.maxFlexSize = 0;
|
|
}
|
|
}
|
|
|
|
// Setup new ratio, target layout rect, start position
|
|
ratio = availableSpace / totalFlex;
|
|
pos = contPaddingBox[beforeName];
|
|
rect = {};
|
|
|
|
// Handle pack setting moves the start position to end, center
|
|
if (totalFlex === 0) {
|
|
if (pack == "end") {
|
|
pos = availableSpace + contPaddingBox[beforeName];
|
|
} else if (pack == "center") {
|
|
pos = Math.round(
|
|
(contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2)
|
|
) + contPaddingBox[beforeName];
|
|
|
|
if (pos < 0) {
|
|
pos = contPaddingBox[beforeName];
|
|
}
|
|
} else if (pack == "justify") {
|
|
pos = contPaddingBox[beforeName];
|
|
spacing = Math.floor(availableSpace / (items.length - 1));
|
|
}
|
|
}
|
|
|
|
// Default aligning (start) the other ones needs to be calculated while doing the layout
|
|
rect[alignAxisName] = contPaddingBox[alignBeforeName];
|
|
|
|
// Start laying out controls
|
|
for (i = 0, l = items.length; i < l; i++) {
|
|
ctrl = items[i];
|
|
ctrlLayoutRect = ctrl.layoutRect();
|
|
size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName];
|
|
|
|
// Align the control on the other axis
|
|
if (align === "center") {
|
|
rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2));
|
|
} else if (align === "stretch") {
|
|
rect[alignSizeName] = max(
|
|
ctrlLayoutRect[alignMinSizeName] || 0,
|
|
contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName]
|
|
);
|
|
rect[alignAxisName] = contPaddingBox[alignBeforeName];
|
|
} else if (align === "end") {
|
|
rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top;
|
|
}
|
|
|
|
// Calculate new size based on flex
|
|
if (ctrlLayoutRect.flex > 0) {
|
|
size += ctrlLayoutRect.flex * ratio;
|
|
}
|
|
|
|
rect[sizeName] = size;
|
|
rect[posName] = pos;
|
|
ctrl.layoutRect(rect);
|
|
|
|
// Recalculate containers
|
|
if (ctrl.recalc) {
|
|
ctrl.recalc();
|
|
}
|
|
|
|
// Move x/y position
|
|
pos += size + spacing;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FlowLayout.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 layout manager will place the controls by using the browsers native layout.
|
|
*
|
|
* @-x-less FlowLayout.less
|
|
* @class tinymce.ui.FlowLayout
|
|
* @extends tinymce.ui.Layout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FlowLayout',
|
|
[
|
|
"tinymce.core.ui.Layout"
|
|
],
|
|
function (Layout) {
|
|
return Layout.extend({
|
|
Defaults: {
|
|
containerClass: 'flow-layout',
|
|
controlClass: 'flow-layout-item',
|
|
endClass: 'break'
|
|
},
|
|
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function (container) {
|
|
container.items().filter(':visible').each(function (ctrl) {
|
|
if (ctrl.recalc) {
|
|
ctrl.recalc();
|
|
}
|
|
});
|
|
},
|
|
|
|
isNative: function () {
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* FontInfo.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
|
|
*/
|
|
|
|
/**
|
|
* Internal class for computing font size for elements.
|
|
*
|
|
* @private
|
|
* @class tinymce.fmt.FontInfo
|
|
*/
|
|
define(
|
|
'tinymce.core.fmt.FontInfo',
|
|
[
|
|
"tinymce.core.dom.DOMUtils"
|
|
],
|
|
function (DOMUtils) {
|
|
var getSpecifiedFontProp = function (propName, rootElm, elm) {
|
|
while (elm !== rootElm) {
|
|
if (elm.style[propName]) {
|
|
return elm.style[propName];
|
|
}
|
|
|
|
elm = elm.parentNode;
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
var toPt = function (fontSize) {
|
|
if (/[0-9.]+px$/.test(fontSize)) {
|
|
return Math.round(parseInt(fontSize, 10) * 72 / 96) + 'pt';
|
|
}
|
|
|
|
return fontSize;
|
|
};
|
|
|
|
var normalizeFontFamily = function (fontFamily) {
|
|
// 'Font name', Font -> Font name,Font
|
|
return fontFamily.replace(/[\'\"]/g, '').replace(/,\s+/g, ',');
|
|
};
|
|
|
|
var getComputedFontProp = function (propName, elm) {
|
|
return DOMUtils.DOM.getStyle(elm, propName, true);
|
|
};
|
|
|
|
var getFontSize = function (rootElm, elm) {
|
|
var specifiedFontSize = getSpecifiedFontProp('fontSize', rootElm, elm);
|
|
return specifiedFontSize !== '' ? specifiedFontSize : getComputedFontProp('fontSize', elm);
|
|
};
|
|
|
|
var getFontFamily = function (rootElm, elm) {
|
|
var specifiedFontSize = getSpecifiedFontProp('fontFamily', rootElm, elm);
|
|
var fontValue = specifiedFontSize !== '' ? specifiedFontSize : getComputedFontProp('fontFamily', elm);
|
|
|
|
return fontValue !== undefined ? normalizeFontFamily(fontValue) : '';
|
|
};
|
|
|
|
return {
|
|
getFontSize: getFontSize,
|
|
getFontFamily: getFontFamily,
|
|
toPt: toPt
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* FormatControls.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
|
|
*/
|
|
|
|
/**
|
|
* Internal class containing all TinyMCE specific control types such as
|
|
* format listboxes, fontlist boxes, toolbar buttons etc.
|
|
*
|
|
* @class tinymce.ui.FormatControls
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.FormatControls',
|
|
[
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.FloatPanel",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.util.Arr",
|
|
"tinymce.core.dom.DOMUtils",
|
|
"tinymce.core.EditorManager",
|
|
"tinymce.core.Env",
|
|
"tinymce.core.fmt.FontInfo"
|
|
],
|
|
function (Control, Widget, FloatPanel, Tools, Arr, DOMUtils, EditorManager, Env, FontInfo) {
|
|
var each = Tools.each;
|
|
|
|
var flatten = function (ar) {
|
|
return Arr.reduce(ar, function (result, item) {
|
|
return result.concat(item);
|
|
}, []);
|
|
};
|
|
|
|
EditorManager.on('AddEditor', function (e) {
|
|
var editor = e.editor;
|
|
|
|
setupRtlMode(editor);
|
|
registerControls(editor);
|
|
setupContainer(editor);
|
|
});
|
|
|
|
Control.translate = function (text) {
|
|
return EditorManager.translate(text);
|
|
};
|
|
|
|
Widget.tooltips = !Env.iOS;
|
|
|
|
function setupContainer(editor) {
|
|
if (editor.settings.ui_container) {
|
|
Env.container = DOMUtils.DOM.select(editor.settings.ui_container)[0];
|
|
}
|
|
}
|
|
|
|
function setupRtlMode(editor) {
|
|
editor.on('ScriptsLoaded', function () {
|
|
if (editor.rtl) {
|
|
Control.rtl = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
function registerControls(editor) {
|
|
var formatMenu;
|
|
|
|
function createListBoxChangeHandler(items, formatName) {
|
|
return function () {
|
|
var self = this;
|
|
|
|
editor.on('nodeChange', function (e) {
|
|
var formatter = editor.formatter;
|
|
var value = null;
|
|
|
|
each(e.parents, function (node) {
|
|
each(items, function (item) {
|
|
if (formatName) {
|
|
if (formatter.matchNode(node, formatName, { value: item.value })) {
|
|
value = item.value;
|
|
}
|
|
} else {
|
|
if (formatter.matchNode(node, item.value)) {
|
|
value = item.value;
|
|
}
|
|
}
|
|
|
|
if (value) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (value) {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
self.value(value);
|
|
});
|
|
};
|
|
}
|
|
|
|
function createFontNameListBoxChangeHandler(items) {
|
|
return function () {
|
|
var self = this;
|
|
|
|
var getFirstFont = function (fontFamily) {
|
|
return fontFamily ? fontFamily.split(',')[0] : '';
|
|
};
|
|
|
|
editor.on('nodeChange', function (e) {
|
|
var fontFamily, value = null;
|
|
|
|
fontFamily = FontInfo.getFontFamily(editor.getBody(), e.element);
|
|
|
|
each(items, function (item) {
|
|
if (item.value.toLowerCase() === fontFamily.toLowerCase()) {
|
|
value = item.value;
|
|
}
|
|
});
|
|
|
|
each(items, function (item) {
|
|
if (!value && getFirstFont(item.value).toLowerCase() === getFirstFont(fontFamily).toLowerCase()) {
|
|
value = item.value;
|
|
}
|
|
});
|
|
|
|
self.value(value);
|
|
|
|
if (!value && fontFamily) {
|
|
self.text(getFirstFont(fontFamily));
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
function createFontSizeListBoxChangeHandler(items) {
|
|
return function () {
|
|
var self = this;
|
|
|
|
editor.on('nodeChange', function (e) {
|
|
var px, pt, value = null;
|
|
|
|
px = FontInfo.getFontSize(editor.getBody(), e.element);
|
|
pt = FontInfo.toPt(px);
|
|
|
|
each(items, function (item) {
|
|
if (item.value === px) {
|
|
value = px;
|
|
} else if (item.value === pt) {
|
|
value = pt;
|
|
}
|
|
});
|
|
|
|
self.value(value);
|
|
|
|
if (!value) {
|
|
self.text(pt);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
function createFormats(formats) {
|
|
formats = formats.replace(/;$/, '').split(';');
|
|
|
|
var i = formats.length;
|
|
while (i--) {
|
|
formats[i] = formats[i].split('=');
|
|
}
|
|
|
|
return formats;
|
|
}
|
|
|
|
function createFormatMenu() {
|
|
var count = 0, newFormats = [];
|
|
|
|
var defaultStyleFormats = [
|
|
{
|
|
title: 'Headings', items: [
|
|
{ title: 'Heading 1', format: 'h1' },
|
|
{ title: 'Heading 2', format: 'h2' },
|
|
{ title: 'Heading 3', format: 'h3' },
|
|
{ title: 'Heading 4', format: 'h4' },
|
|
{ title: 'Heading 5', format: 'h5' },
|
|
{ title: 'Heading 6', format: 'h6' }
|
|
]
|
|
},
|
|
|
|
{
|
|
title: 'Inline', items: [
|
|
{ title: 'Bold', icon: 'bold', format: 'bold' },
|
|
{ title: 'Italic', icon: 'italic', format: 'italic' },
|
|
{ title: 'Underline', icon: 'underline', format: 'underline' },
|
|
{ title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough' },
|
|
{ title: 'Superscript', icon: 'superscript', format: 'superscript' },
|
|
{ title: 'Subscript', icon: 'subscript', format: 'subscript' },
|
|
{ title: 'Code', icon: 'code', format: 'code' }
|
|
]
|
|
},
|
|
|
|
{
|
|
title: 'Blocks', items: [
|
|
{ title: 'Paragraph', format: 'p' },
|
|
{ title: 'Blockquote', format: 'blockquote' },
|
|
{ title: 'Div', format: 'div' },
|
|
{ title: 'Pre', format: 'pre' }
|
|
]
|
|
},
|
|
|
|
{
|
|
title: 'Alignment', items: [
|
|
{ title: 'Left', icon: 'alignleft', format: 'alignleft' },
|
|
{ title: 'Center', icon: 'aligncenter', format: 'aligncenter' },
|
|
{ title: 'Right', icon: 'alignright', format: 'alignright' },
|
|
{ title: 'Justify', icon: 'alignjustify', format: 'alignjustify' }
|
|
]
|
|
}
|
|
];
|
|
|
|
function createMenu(formats) {
|
|
var menu = [];
|
|
|
|
if (!formats) {
|
|
return;
|
|
}
|
|
|
|
each(formats, function (format) {
|
|
var menuItem = {
|
|
text: format.title,
|
|
icon: format.icon
|
|
};
|
|
|
|
if (format.items) {
|
|
menuItem.menu = createMenu(format.items);
|
|
} else {
|
|
var formatName = format.format || "custom" + count++;
|
|
|
|
if (!format.format) {
|
|
format.name = formatName;
|
|
newFormats.push(format);
|
|
}
|
|
|
|
menuItem.format = formatName;
|
|
menuItem.cmd = format.cmd;
|
|
}
|
|
|
|
menu.push(menuItem);
|
|
});
|
|
|
|
return menu;
|
|
}
|
|
|
|
function createStylesMenu() {
|
|
var menu;
|
|
|
|
if (editor.settings.style_formats_merge) {
|
|
if (editor.settings.style_formats) {
|
|
menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats));
|
|
} else {
|
|
menu = createMenu(defaultStyleFormats);
|
|
}
|
|
} else {
|
|
menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
|
|
}
|
|
|
|
return menu;
|
|
}
|
|
|
|
editor.on('init', function () {
|
|
each(newFormats, function (format) {
|
|
editor.formatter.register(format.name, format);
|
|
});
|
|
});
|
|
|
|
return {
|
|
type: 'menu',
|
|
items: createStylesMenu(),
|
|
onPostRender: function (e) {
|
|
editor.fire('renderFormatsMenu', { control: e.control });
|
|
},
|
|
itemDefaults: {
|
|
preview: true,
|
|
|
|
textStyle: function () {
|
|
if (this.settings.format) {
|
|
return editor.formatter.getCssText(this.settings.format);
|
|
}
|
|
},
|
|
|
|
onPostRender: function () {
|
|
var self = this;
|
|
|
|
self.parent().on('show', function () {
|
|
var formatName, command;
|
|
|
|
formatName = self.settings.format;
|
|
if (formatName) {
|
|
self.disabled(!editor.formatter.canApply(formatName));
|
|
self.active(editor.formatter.match(formatName));
|
|
}
|
|
|
|
command = self.settings.cmd;
|
|
if (command) {
|
|
self.active(editor.queryCommandState(command));
|
|
}
|
|
});
|
|
},
|
|
|
|
onclick: function () {
|
|
if (this.settings.format) {
|
|
toggleFormat(this.settings.format);
|
|
}
|
|
|
|
if (this.settings.cmd) {
|
|
editor.execCommand(this.settings.cmd);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
formatMenu = createFormatMenu();
|
|
|
|
function initOnPostRender(name) {
|
|
return function () {
|
|
var self = this;
|
|
|
|
// TODO: Fix this
|
|
if (editor.formatter) {
|
|
editor.formatter.formatChanged(name, function (state) {
|
|
self.active(state);
|
|
});
|
|
} else {
|
|
editor.on('init', function () {
|
|
editor.formatter.formatChanged(name, function (state) {
|
|
self.active(state);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
// Simple format controls <control/format>:<UI text>
|
|
each({
|
|
bold: 'Bold',
|
|
italic: 'Italic',
|
|
underline: 'Underline',
|
|
strikethrough: 'Strikethrough',
|
|
subscript: 'Subscript',
|
|
superscript: 'Superscript'
|
|
}, function (text, name) {
|
|
editor.addButton(name, {
|
|
tooltip: text,
|
|
onPostRender: initOnPostRender(name),
|
|
onclick: function () {
|
|
toggleFormat(name);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Simple command controls <control>:[<UI text>,<Command>]
|
|
each({
|
|
outdent: ['Decrease indent', 'Outdent'],
|
|
indent: ['Increase indent', 'Indent'],
|
|
cut: ['Cut', 'Cut'],
|
|
copy: ['Copy', 'Copy'],
|
|
paste: ['Paste', 'Paste'],
|
|
help: ['Help', 'mceHelp'],
|
|
selectall: ['Select all', 'SelectAll'],
|
|
removeformat: ['Clear formatting', 'RemoveFormat'],
|
|
visualaid: ['Visual aids', 'mceToggleVisualAid'],
|
|
newdocument: ['New document', 'mceNewDocument']
|
|
}, function (item, name) {
|
|
editor.addButton(name, {
|
|
tooltip: item[0],
|
|
cmd: item[1]
|
|
});
|
|
});
|
|
|
|
// Simple command controls with format state
|
|
each({
|
|
blockquote: ['Blockquote', 'mceBlockQuote'],
|
|
subscript: ['Subscript', 'Subscript'],
|
|
superscript: ['Superscript', 'Superscript'],
|
|
alignleft: ['Align left', 'JustifyLeft'],
|
|
aligncenter: ['Align center', 'JustifyCenter'],
|
|
alignright: ['Align right', 'JustifyRight'],
|
|
alignjustify: ['Justify', 'JustifyFull'],
|
|
alignnone: ['No alignment', 'JustifyNone']
|
|
}, function (item, name) {
|
|
editor.addButton(name, {
|
|
tooltip: item[0],
|
|
cmd: item[1],
|
|
onPostRender: initOnPostRender(name)
|
|
});
|
|
});
|
|
|
|
function toggleUndoRedoState(type) {
|
|
return function () {
|
|
var self = this;
|
|
|
|
function checkState() {
|
|
var typeFn = type == 'redo' ? 'hasRedo' : 'hasUndo';
|
|
return editor.undoManager ? editor.undoManager[typeFn]() : false;
|
|
}
|
|
|
|
self.disabled(!checkState());
|
|
editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function () {
|
|
self.disabled(editor.readonly || !checkState());
|
|
});
|
|
};
|
|
}
|
|
|
|
function toggleVisualAidState() {
|
|
var self = this;
|
|
|
|
editor.on('VisualAid', function (e) {
|
|
self.active(e.hasVisual);
|
|
});
|
|
|
|
self.active(editor.hasVisual);
|
|
}
|
|
|
|
var trimMenuItems = function (menuItems) {
|
|
var outputMenuItems = menuItems;
|
|
|
|
if (outputMenuItems.length > 0 && outputMenuItems[0].text === '-') {
|
|
outputMenuItems = outputMenuItems.slice(1);
|
|
}
|
|
|
|
if (outputMenuItems.length > 0 && outputMenuItems[outputMenuItems.length - 1].text === '-') {
|
|
outputMenuItems = outputMenuItems.slice(0, outputMenuItems.length - 1);
|
|
}
|
|
|
|
return outputMenuItems;
|
|
};
|
|
|
|
var createCustomMenuItems = function (names) {
|
|
var items, nameList;
|
|
|
|
if (typeof names === 'string') {
|
|
nameList = names.split(' ');
|
|
} else if (Tools.isArray(names)) {
|
|
return flatten(Tools.map(names, createCustomMenuItems));
|
|
}
|
|
|
|
items = Tools.grep(nameList, function (name) {
|
|
return name === '|' || name in editor.menuItems;
|
|
});
|
|
|
|
return Tools.map(items, function (name) {
|
|
return name === '|' ? { text: '-' } : editor.menuItems[name];
|
|
});
|
|
};
|
|
|
|
var createContextMenuItems = function (context) {
|
|
var outputMenuItems = [{ text: '-' }];
|
|
var menuItems = Tools.grep(editor.menuItems, function (menuItem) {
|
|
return menuItem.context === context;
|
|
});
|
|
|
|
Tools.each(menuItems, function (menuItem) {
|
|
if (menuItem.separator == 'before') {
|
|
outputMenuItems.push({ text: '|' });
|
|
}
|
|
|
|
if (menuItem.prependToContext) {
|
|
outputMenuItems.unshift(menuItem);
|
|
} else {
|
|
outputMenuItems.push(menuItem);
|
|
}
|
|
|
|
if (menuItem.separator == 'after') {
|
|
outputMenuItems.push({ text: '|' });
|
|
}
|
|
});
|
|
|
|
return outputMenuItems;
|
|
};
|
|
|
|
var createInsertMenu = function (editorSettings) {
|
|
if (editorSettings.insert_button_items) {
|
|
return trimMenuItems(createCustomMenuItems(editorSettings.insert_button_items));
|
|
} else {
|
|
return trimMenuItems(createContextMenuItems('insert'));
|
|
}
|
|
};
|
|
|
|
editor.addButton('undo', {
|
|
tooltip: 'Undo',
|
|
onPostRender: toggleUndoRedoState('undo'),
|
|
cmd: 'undo'
|
|
});
|
|
|
|
editor.addButton('redo', {
|
|
tooltip: 'Redo',
|
|
onPostRender: toggleUndoRedoState('redo'),
|
|
cmd: 'redo'
|
|
});
|
|
|
|
editor.addMenuItem('newdocument', {
|
|
text: 'New document',
|
|
icon: 'newdocument',
|
|
cmd: 'mceNewDocument'
|
|
});
|
|
|
|
editor.addMenuItem('undo', {
|
|
text: 'Undo',
|
|
icon: 'undo',
|
|
shortcut: 'Meta+Z',
|
|
onPostRender: toggleUndoRedoState('undo'),
|
|
cmd: 'undo'
|
|
});
|
|
|
|
editor.addMenuItem('redo', {
|
|
text: 'Redo',
|
|
icon: 'redo',
|
|
shortcut: 'Meta+Y',
|
|
onPostRender: toggleUndoRedoState('redo'),
|
|
cmd: 'redo'
|
|
});
|
|
|
|
editor.addMenuItem('visualaid', {
|
|
text: 'Visual aids',
|
|
selectable: true,
|
|
onPostRender: toggleVisualAidState,
|
|
cmd: 'mceToggleVisualAid'
|
|
});
|
|
|
|
editor.addButton('remove', {
|
|
tooltip: 'Remove',
|
|
icon: 'remove',
|
|
cmd: 'Delete'
|
|
});
|
|
|
|
editor.addButton('insert', {
|
|
type: 'menubutton',
|
|
icon: 'insert',
|
|
menu: [],
|
|
oncreatemenu: function () {
|
|
this.menu.add(createInsertMenu(editor.settings));
|
|
this.menu.renderNew();
|
|
}
|
|
});
|
|
|
|
each({
|
|
cut: ['Cut', 'Cut', 'Meta+X'],
|
|
copy: ['Copy', 'Copy', 'Meta+C'],
|
|
paste: ['Paste', 'Paste', 'Meta+V'],
|
|
selectall: ['Select all', 'SelectAll', 'Meta+A'],
|
|
bold: ['Bold', 'Bold', 'Meta+B'],
|
|
italic: ['Italic', 'Italic', 'Meta+I'],
|
|
underline: ['Underline', 'Underline', 'Meta+U'],
|
|
strikethrough: ['Strikethrough', 'Strikethrough'],
|
|
subscript: ['Subscript', 'Subscript'],
|
|
superscript: ['Superscript', 'Superscript'],
|
|
removeformat: ['Clear formatting', 'RemoveFormat']
|
|
}, function (item, name) {
|
|
editor.addMenuItem(name, {
|
|
text: item[0],
|
|
icon: name,
|
|
shortcut: item[2],
|
|
cmd: item[1]
|
|
});
|
|
});
|
|
|
|
editor.on('mousedown', function () {
|
|
FloatPanel.hideAll();
|
|
});
|
|
|
|
function toggleFormat(fmt) {
|
|
if (fmt.control) {
|
|
fmt = fmt.control.value();
|
|
}
|
|
|
|
if (fmt) {
|
|
editor.execCommand('mceToggleFormat', false, fmt);
|
|
}
|
|
}
|
|
|
|
function hideMenuObjects(menu) {
|
|
var count = menu.length;
|
|
|
|
Tools.each(menu, function (item) {
|
|
if (item.menu) {
|
|
item.hidden = hideMenuObjects(item.menu) === 0;
|
|
}
|
|
|
|
var formatName = item.format;
|
|
if (formatName) {
|
|
item.hidden = !editor.formatter.canApply(formatName);
|
|
}
|
|
|
|
if (item.hidden) {
|
|
count--;
|
|
}
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
function hideFormatMenuItems(menu) {
|
|
var count = menu.items().length;
|
|
|
|
menu.items().each(function (item) {
|
|
if (item.menu) {
|
|
item.visible(hideFormatMenuItems(item.menu) > 0);
|
|
}
|
|
|
|
if (!item.menu && item.settings.menu) {
|
|
item.visible(hideMenuObjects(item.settings.menu) > 0);
|
|
}
|
|
|
|
var formatName = item.settings.format;
|
|
if (formatName) {
|
|
item.visible(editor.formatter.canApply(formatName));
|
|
}
|
|
|
|
if (!item.visible()) {
|
|
count--;
|
|
}
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
editor.addButton('styleselect', {
|
|
type: 'menubutton',
|
|
text: 'Formats',
|
|
menu: formatMenu,
|
|
onShowMenu: function () {
|
|
if (editor.settings.style_formats_autohide) {
|
|
hideFormatMenuItems(this.menu);
|
|
}
|
|
}
|
|
});
|
|
|
|
editor.addButton('formatselect', function () {
|
|
var items = [], blocks = createFormats(editor.settings.block_formats ||
|
|
'Paragraph=p;' +
|
|
'Heading 1=h1;' +
|
|
'Heading 2=h2;' +
|
|
'Heading 3=h3;' +
|
|
'Heading 4=h4;' +
|
|
'Heading 5=h5;' +
|
|
'Heading 6=h6;' +
|
|
'Preformatted=pre'
|
|
);
|
|
|
|
each(blocks, function (block) {
|
|
items.push({
|
|
text: block[0],
|
|
value: block[1],
|
|
textStyle: function () {
|
|
return editor.formatter.getCssText(block[1]);
|
|
}
|
|
});
|
|
});
|
|
|
|
return {
|
|
type: 'listbox',
|
|
text: blocks[0][0],
|
|
values: items,
|
|
fixedWidth: true,
|
|
onselect: toggleFormat,
|
|
onPostRender: createListBoxChangeHandler(items)
|
|
};
|
|
});
|
|
|
|
editor.addButton('fontselect', function () {
|
|
var defaultFontsFormats =
|
|
'Andale Mono=andale mono,monospace;' +
|
|
'Arial=arial,helvetica,sans-serif;' +
|
|
'Arial Black=arial black,sans-serif;' +
|
|
'Book Antiqua=book antiqua,palatino,serif;' +
|
|
'Comic Sans MS=comic sans ms,sans-serif;' +
|
|
'Courier New=courier new,courier,monospace;' +
|
|
'Georgia=georgia,palatino,serif;' +
|
|
'Helvetica=helvetica,arial,sans-serif;' +
|
|
'Impact=impact,sans-serif;' +
|
|
'Symbol=symbol;' +
|
|
'Tahoma=tahoma,arial,helvetica,sans-serif;' +
|
|
'Terminal=terminal,monaco,monospace;' +
|
|
'Times New Roman=times new roman,times,serif;' +
|
|
'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
|
|
'Verdana=verdana,geneva,sans-serif;' +
|
|
'Webdings=webdings;' +
|
|
'Wingdings=wingdings,zapf dingbats';
|
|
|
|
var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats);
|
|
|
|
each(fonts, function (font) {
|
|
items.push({
|
|
text: { raw: font[0] },
|
|
value: font[1],
|
|
textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : ''
|
|
});
|
|
});
|
|
|
|
return {
|
|
type: 'listbox',
|
|
text: 'Font Family',
|
|
tooltip: 'Font Family',
|
|
values: items,
|
|
fixedWidth: true,
|
|
onPostRender: createFontNameListBoxChangeHandler(items),
|
|
onselect: function (e) {
|
|
if (e.control.settings.value) {
|
|
editor.execCommand('FontName', false, e.control.settings.value);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
editor.addButton('fontsizeselect', function () {
|
|
var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt';
|
|
var fontsizeFormats = editor.settings.fontsizeFormats || defaultFontsizeFormats;
|
|
|
|
each(fontsizeFormats.split(' '), function (item) {
|
|
var text = item, value = item;
|
|
// Allow text=value font sizes.
|
|
var values = item.split('=');
|
|
if (values.length > 1) {
|
|
text = values[0];
|
|
value = values[1];
|
|
}
|
|
items.push({ text: text, value: value });
|
|
});
|
|
|
|
return {
|
|
type: 'listbox',
|
|
text: 'Font Sizes',
|
|
tooltip: 'Font Sizes',
|
|
values: items,
|
|
fixedWidth: true,
|
|
onPostRender: createFontSizeListBoxChangeHandler(items),
|
|
onclick: function (e) {
|
|
if (e.control.settings.value) {
|
|
editor.execCommand('FontSize', false, e.control.settings.value);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
editor.addMenuItem('formats', {
|
|
text: 'Formats',
|
|
menu: formatMenu
|
|
});
|
|
}
|
|
|
|
return {};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* GridLayout.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 layout manager places controls in a grid.
|
|
*
|
|
* @setting {Number} spacing Spacing between controls.
|
|
* @setting {Number} spacingH Horizontal spacing between controls.
|
|
* @setting {Number} spacingV Vertical spacing between controls.
|
|
* @setting {Number} columns Number of columns to use.
|
|
* @setting {String/Array} alignH start|end|center|stretch or array of values for each column.
|
|
* @setting {String/Array} alignV start|end|center|stretch or array of values for each column.
|
|
* @setting {String} pack start|end
|
|
*
|
|
* @class tinymce.ui.GridLayout
|
|
* @extends tinymce.ui.AbsoluteLayout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.GridLayout',
|
|
[
|
|
"tinymce.core.ui.AbsoluteLayout"
|
|
],
|
|
function (AbsoluteLayout) {
|
|
"use strict";
|
|
|
|
return AbsoluteLayout.extend({
|
|
/**
|
|
* Recalculates the positions of the controls in the specified container.
|
|
*
|
|
* @method recalc
|
|
* @param {tinymce.ui.Container} container Container instance to recalc.
|
|
*/
|
|
recalc: function (container) {
|
|
var settings, rows, cols, items, contLayoutRect, width, height, rect,
|
|
ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY,
|
|
colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx;
|
|
|
|
// Get layout settings
|
|
settings = container.settings;
|
|
items = container.items().filter(':visible');
|
|
contLayoutRect = container.layoutRect();
|
|
cols = settings.columns || Math.ceil(Math.sqrt(items.length));
|
|
rows = Math.ceil(items.length / cols);
|
|
spacingH = settings.spacingH || settings.spacing || 0;
|
|
spacingV = settings.spacingV || settings.spacing || 0;
|
|
alignH = settings.alignH || settings.align;
|
|
alignV = settings.alignV || settings.align;
|
|
contPaddingBox = container.paddingBox;
|
|
reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl();
|
|
|
|
if (alignH && typeof alignH == "string") {
|
|
alignH = [alignH];
|
|
}
|
|
|
|
if (alignV && typeof alignV == "string") {
|
|
alignV = [alignV];
|
|
}
|
|
|
|
// Zero padd columnWidths
|
|
for (x = 0; x < cols; x++) {
|
|
colWidths.push(0);
|
|
}
|
|
|
|
// Zero padd rowHeights
|
|
for (y = 0; y < rows; y++) {
|
|
rowHeights.push(0);
|
|
}
|
|
|
|
// Calculate columnWidths and rowHeights
|
|
for (y = 0; y < rows; y++) {
|
|
for (x = 0; x < cols; x++) {
|
|
ctrl = items[y * cols + x];
|
|
|
|
// Out of bounds
|
|
if (!ctrl) {
|
|
break;
|
|
}
|
|
|
|
ctrlLayoutRect = ctrl.layoutRect();
|
|
ctrlMinWidth = ctrlLayoutRect.minW;
|
|
ctrlMinHeight = ctrlLayoutRect.minH;
|
|
|
|
colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x];
|
|
rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y];
|
|
}
|
|
}
|
|
|
|
// Calculate maxX
|
|
availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right;
|
|
for (maxX = 0, x = 0; x < cols; x++) {
|
|
maxX += colWidths[x] + (x > 0 ? spacingH : 0);
|
|
availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x];
|
|
}
|
|
|
|
// Calculate maxY
|
|
availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom;
|
|
for (maxY = 0, y = 0; y < rows; y++) {
|
|
maxY += rowHeights[y] + (y > 0 ? spacingV : 0);
|
|
availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y];
|
|
}
|
|
|
|
maxX += contPaddingBox.left + contPaddingBox.right;
|
|
maxY += contPaddingBox.top + contPaddingBox.bottom;
|
|
|
|
// Calculate minW/minH
|
|
rect = {};
|
|
rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW);
|
|
rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH);
|
|
|
|
rect.contentW = rect.minW - contLayoutRect.deltaW;
|
|
rect.contentH = rect.minH - contLayoutRect.deltaH;
|
|
rect.minW = Math.min(rect.minW, contLayoutRect.maxW);
|
|
rect.minH = Math.min(rect.minH, contLayoutRect.maxH);
|
|
rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth);
|
|
rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight);
|
|
|
|
// Resize container container if minSize was changed
|
|
if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) {
|
|
rect.w = rect.minW;
|
|
rect.h = rect.minH;
|
|
|
|
container.layoutRect(rect);
|
|
this.recalc(container);
|
|
|
|
// Forced recalc for example if items are hidden/shown
|
|
if (container._lastRect === null) {
|
|
var parentCtrl = container.parent();
|
|
if (parentCtrl) {
|
|
parentCtrl._lastRect = null;
|
|
parentCtrl.recalc();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Update contentW/contentH so absEnd moves correctly
|
|
if (contLayoutRect.autoResize) {
|
|
rect = container.layoutRect(rect);
|
|
rect.contentW = rect.minW - contLayoutRect.deltaW;
|
|
rect.contentH = rect.minH - contLayoutRect.deltaH;
|
|
}
|
|
|
|
var flexV;
|
|
|
|
if (settings.packV == 'start') {
|
|
flexV = 0;
|
|
} else {
|
|
flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0;
|
|
}
|
|
|
|
// Calculate totalFlex
|
|
var totalFlex = 0;
|
|
var flexWidths = settings.flexWidths;
|
|
if (flexWidths) {
|
|
for (x = 0; x < flexWidths.length; x++) {
|
|
totalFlex += flexWidths[x];
|
|
}
|
|
} else {
|
|
totalFlex = cols;
|
|
}
|
|
|
|
// Calculate new column widths based on flex values
|
|
var ratio = availableWidth / totalFlex;
|
|
for (x = 0; x < cols; x++) {
|
|
colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio;
|
|
}
|
|
|
|
// Move/resize controls
|
|
posY = contPaddingBox.top;
|
|
for (y = 0; y < rows; y++) {
|
|
posX = contPaddingBox.left;
|
|
height = rowHeights[y] + flexV;
|
|
|
|
for (x = 0; x < cols; x++) {
|
|
if (reverseRows) {
|
|
idx = y * cols + cols - 1 - x;
|
|
} else {
|
|
idx = y * cols + x;
|
|
}
|
|
|
|
ctrl = items[idx];
|
|
|
|
// No more controls to render then break
|
|
if (!ctrl) {
|
|
break;
|
|
}
|
|
|
|
// Get control settings and calculate x, y
|
|
ctrlSettings = ctrl.settings;
|
|
ctrlLayoutRect = ctrl.layoutRect();
|
|
width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth);
|
|
ctrlLayoutRect.x = posX;
|
|
ctrlLayoutRect.y = posY;
|
|
|
|
// Align control horizontal
|
|
align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null);
|
|
if (align == "center") {
|
|
ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2);
|
|
} else if (align == "right") {
|
|
ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w;
|
|
} else if (align == "stretch") {
|
|
ctrlLayoutRect.w = width;
|
|
}
|
|
|
|
// Align control vertical
|
|
align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null);
|
|
if (align == "center") {
|
|
ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2);
|
|
} else if (align == "bottom") {
|
|
ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h;
|
|
} else if (align == "stretch") {
|
|
ctrlLayoutRect.h = height;
|
|
}
|
|
|
|
ctrl.layoutRect(ctrlLayoutRect);
|
|
|
|
posX += width + spacingH;
|
|
|
|
if (ctrl.recalc) {
|
|
ctrl.recalc();
|
|
}
|
|
}
|
|
|
|
posY += height + spacingV;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Iframe.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 scripturl:true */
|
|
|
|
/**
|
|
* This class creates an iframe.
|
|
*
|
|
* @setting {String} url Url to open in the iframe.
|
|
*
|
|
* @-x-less Iframe.less
|
|
* @class tinymce.ui.Iframe
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Iframe',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function (Widget, Delay) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this;
|
|
|
|
self.classes.add('iframe');
|
|
self.canFocus = false;
|
|
|
|
/*eslint no-script-url:0 */
|
|
return (
|
|
'<iframe id="' + self._id + '" class="' + self.classes + '" tabindex="-1" src="' +
|
|
(self.settings.url || "javascript:''") + '" frameborder="0"></iframe>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Setter for the iframe source.
|
|
*
|
|
* @method src
|
|
* @param {String} src Source URL for iframe.
|
|
*/
|
|
src: function (src) {
|
|
this.getEl().src = src;
|
|
},
|
|
|
|
/**
|
|
* Inner HTML for the iframe.
|
|
*
|
|
* @method html
|
|
* @param {String} html HTML string to set as HTML inside the iframe.
|
|
* @param {function} callback Optional callback to execute when the iframe body is filled with contents.
|
|
* @return {tinymce.ui.Iframe} Current iframe control.
|
|
*/
|
|
html: function (html, callback) {
|
|
var self = this, body = this.getEl().contentWindow.document.body;
|
|
|
|
// Wait for iframe to initialize IE 10 takes time
|
|
if (!body) {
|
|
Delay.setTimeout(function () {
|
|
self.html(html);
|
|
});
|
|
} else {
|
|
body.innerHTML = html;
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* InfoBox.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
|
|
*/
|
|
|
|
/**
|
|
* ....
|
|
*
|
|
* @-x-less InfoBox.less
|
|
* @class tinymce.ui.InfoBox
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.InfoBox',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} multiline Multiline label.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
self.classes.add('widget').add('infobox');
|
|
self.canFocus = false;
|
|
},
|
|
|
|
severity: function (level) {
|
|
this.classes.remove('error');
|
|
this.classes.remove('warning');
|
|
this.classes.remove('success');
|
|
this.classes.add(level);
|
|
},
|
|
|
|
help: function (state) {
|
|
this.state.set('help', state);
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, prefix = self.classPrefix;
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '">' +
|
|
'<div id="' + self._id + '-body">' +
|
|
self.encode(self.state.get('text')) +
|
|
'<button role="button" tabindex="-1">' +
|
|
'<i class="' + prefix + 'ico ' + prefix + 'i-help"></i>' +
|
|
'</button>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:text', function (e) {
|
|
self.getEl('body').firstChild.data = self.encode(e.value);
|
|
|
|
if (self.state.get('rendered')) {
|
|
self.updateLayoutRect();
|
|
}
|
|
});
|
|
|
|
self.state.on('change:help', function (e) {
|
|
self.classes.toggle('has-help', e.value);
|
|
|
|
if (self.state.get('rendered')) {
|
|
self.updateLayoutRect();
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Label.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 creates a label element. A label is a simple text control
|
|
* that can be bound to other controls.
|
|
*
|
|
* @-x-less Label.less
|
|
* @class tinymce.ui.Label
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Label',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (Widget, DomUtils) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} multiline Multiline label.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
self.classes.add('widget').add('label');
|
|
self.canFocus = false;
|
|
|
|
if (settings.multiline) {
|
|
self.classes.add('autoscroll');
|
|
}
|
|
|
|
if (settings.strong) {
|
|
self.classes.add('strong');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes the current controls layout rect.
|
|
* This will be executed by the layout managers to determine the
|
|
* default minWidth/minHeight etc.
|
|
*
|
|
* @method initLayoutRect
|
|
* @return {Object} Layout rect instance.
|
|
*/
|
|
initLayoutRect: function () {
|
|
var self = this, layoutRect = self._super();
|
|
|
|
if (self.settings.multiline) {
|
|
var size = DomUtils.getSize(self.getEl());
|
|
|
|
// Check if the text fits within maxW if not then try word wrapping it
|
|
if (size.width > layoutRect.maxW) {
|
|
layoutRect.minW = layoutRect.maxW;
|
|
self.classes.add('multiline');
|
|
}
|
|
|
|
self.getEl().style.width = layoutRect.minW + 'px';
|
|
layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height);
|
|
}
|
|
|
|
return layoutRect;
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this;
|
|
|
|
if (!self.settings.multiline) {
|
|
self.getEl().style.lineHeight = self.layoutRect().h + 'px';
|
|
}
|
|
|
|
return self._super();
|
|
},
|
|
|
|
severity: function (level) {
|
|
this.classes.remove('error');
|
|
this.classes.remove('warning');
|
|
this.classes.remove('success');
|
|
this.classes.add(level);
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, targetCtrl, forName, forId = self.settings.forId;
|
|
var text = self.settings.html ? self.settings.html : self.encode(self.state.get('text'));
|
|
|
|
if (!forId && (forName = self.settings.forName)) {
|
|
targetCtrl = self.getRoot().find('#' + forName)[0];
|
|
|
|
if (targetCtrl) {
|
|
forId = targetCtrl._id;
|
|
}
|
|
}
|
|
|
|
if (forId) {
|
|
return (
|
|
'<label id="' + self._id + '" class="' + self.classes + '"' + (forId ? ' for="' + forId + '"' : '') + '>' +
|
|
text +
|
|
'</label>'
|
|
);
|
|
}
|
|
|
|
return (
|
|
'<span id="' + self._id + '" class="' + self.classes + '">' +
|
|
text +
|
|
'</span>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:text', function (e) {
|
|
self.innerHtml(self.encode(e.value));
|
|
|
|
if (self.state.get('rendered')) {
|
|
self.updateLayoutRect();
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Toolbar.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new toolbar.
|
|
*
|
|
* @class tinymce.ui.Toolbar
|
|
* @extends tinymce.ui.Container
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Toolbar',
|
|
[
|
|
"tinymce.core.ui.Container"
|
|
],
|
|
function (Container) {
|
|
"use strict";
|
|
|
|
return Container.extend({
|
|
Defaults: {
|
|
role: 'toolbar',
|
|
layout: 'flow'
|
|
},
|
|
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
self.classes.add('toolbar');
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self.items().each(function (ctrl) {
|
|
ctrl.classes.add('toolbar-item');
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* MenuBar.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new menubar.
|
|
*
|
|
* @-x-less MenuBar.less
|
|
* @class tinymce.ui.MenuBar
|
|
* @extends tinymce.ui.Container
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.MenuBar',
|
|
[
|
|
"tinymce.core.ui.Toolbar"
|
|
],
|
|
function (Toolbar) {
|
|
"use strict";
|
|
|
|
return Toolbar.extend({
|
|
Defaults: {
|
|
role: 'menubar',
|
|
containerCls: 'menubar',
|
|
ariaRoot: true,
|
|
defaults: {
|
|
type: 'menubutton'
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* MenuButton.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new menu button.
|
|
*
|
|
* @-x-less MenuButton.less
|
|
* @class tinymce.ui.MenuButton
|
|
* @extends tinymce.ui.Button
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.MenuButton',
|
|
[
|
|
"tinymce.core.ui.Button",
|
|
"tinymce.core.ui.Factory",
|
|
"tinymce.core.ui.MenuBar"
|
|
],
|
|
function (Button, Factory, MenuBar) {
|
|
"use strict";
|
|
|
|
// TODO: Maybe add as some global function
|
|
function isChildOf(node, parent) {
|
|
while (node) {
|
|
if (parent === node) {
|
|
return true;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
var MenuButton = Button.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._renderOpen = true;
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
|
|
self.classes.add('menubtn');
|
|
|
|
if (settings.fixedWidth) {
|
|
self.classes.add('fixed-width');
|
|
}
|
|
|
|
self.aria('haspopup', true);
|
|
|
|
self.state.set('menu', settings.menu || self.render());
|
|
},
|
|
|
|
/**
|
|
* Shows the menu for the button.
|
|
*
|
|
* @method showMenu
|
|
*/
|
|
showMenu: function (toggle) {
|
|
var self = this, menu;
|
|
|
|
if (self.menu && self.menu.visible() && toggle !== false) {
|
|
return self.hideMenu();
|
|
}
|
|
|
|
if (!self.menu) {
|
|
menu = self.state.get('menu') || [];
|
|
|
|
// Is menu array then auto constuct menu control
|
|
if (menu.length) {
|
|
menu = {
|
|
type: 'menu',
|
|
items: menu
|
|
};
|
|
} else {
|
|
menu.type = menu.type || 'menu';
|
|
}
|
|
|
|
if (!menu.renderTo) {
|
|
self.menu = Factory.create(menu).parent(self).renderTo();
|
|
} else {
|
|
self.menu = menu.parent(self).show().renderTo();
|
|
}
|
|
|
|
self.fire('createmenu');
|
|
self.menu.reflow();
|
|
self.menu.on('cancel', function (e) {
|
|
if (e.control.parent() === self.menu) {
|
|
e.stopPropagation();
|
|
self.focus();
|
|
self.hideMenu();
|
|
}
|
|
});
|
|
|
|
// Move focus to button when a menu item is selected/clicked
|
|
self.menu.on('select', function () {
|
|
self.focus();
|
|
});
|
|
|
|
self.menu.on('show hide', function (e) {
|
|
if (e.control == self.menu) {
|
|
self.activeMenu(e.type == 'show');
|
|
}
|
|
|
|
self.aria('expanded', e.type == 'show');
|
|
}).fire('show');
|
|
}
|
|
|
|
self.menu.show();
|
|
self.menu.layoutRect({ w: self.layoutRect().w });
|
|
self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']);
|
|
self.fire('showmenu');
|
|
},
|
|
|
|
/**
|
|
* Hides the menu for the button.
|
|
*
|
|
* @method hideMenu
|
|
*/
|
|
hideMenu: function () {
|
|
var self = this;
|
|
|
|
if (self.menu) {
|
|
self.menu.items().each(function (item) {
|
|
if (item.hideMenu) {
|
|
item.hideMenu();
|
|
}
|
|
});
|
|
|
|
self.menu.hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the active menu state.
|
|
*
|
|
* @private
|
|
*/
|
|
activeMenu: function (state) {
|
|
this.classes.toggle('active', state);
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix;
|
|
var icon = self.settings.icon, image, text = self.state.get('text'),
|
|
textHtml = '';
|
|
|
|
image = self.settings.image;
|
|
if (image) {
|
|
icon = 'none';
|
|
|
|
// Support for [high dpi, low dpi] image sources
|
|
if (typeof image != "string") {
|
|
image = window.getSelection ? image[0] : image[1];
|
|
}
|
|
|
|
image = ' style="background-image: url(\'' + image + '\')"';
|
|
} else {
|
|
image = '';
|
|
}
|
|
|
|
if (text) {
|
|
self.classes.add('btn-has-text');
|
|
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
|
|
}
|
|
|
|
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
|
|
|
|
self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1" aria-labelledby="' + id + '">' +
|
|
'<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' +
|
|
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
|
textHtml +
|
|
' <i class="' + prefix + 'caret"></i>' +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Gets invoked after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self.on('click', function (e) {
|
|
if (e.control === self && isChildOf(e.target, self.getEl())) {
|
|
self.focus();
|
|
self.showMenu(!e.aria);
|
|
|
|
if (e.aria) {
|
|
self.menu.items().filter(':visible')[0].focus();
|
|
}
|
|
}
|
|
});
|
|
|
|
self.on('mouseenter', function (e) {
|
|
var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu;
|
|
|
|
if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) {
|
|
parent.items().filter('MenuButton').each(function (ctrl) {
|
|
if (ctrl.hideMenu && ctrl != overCtrl) {
|
|
if (ctrl.menu && ctrl.menu.visible()) {
|
|
hasVisibleSiblingMenu = true;
|
|
}
|
|
|
|
ctrl.hideMenu();
|
|
}
|
|
});
|
|
|
|
if (hasVisibleSiblingMenu) {
|
|
overCtrl.focus(); // Fix for: #5887
|
|
overCtrl.showMenu();
|
|
}
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:menu', function () {
|
|
if (self.menu) {
|
|
self.menu.remove();
|
|
}
|
|
|
|
self.menu = null;
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
/**
|
|
* Removes the control and it's menus.
|
|
*
|
|
* @method remove
|
|
*/
|
|
remove: function () {
|
|
this._super();
|
|
|
|
if (this.menu) {
|
|
this.menu.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
return MenuButton;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* MenuItem.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new menu item.
|
|
*
|
|
* @-x-less MenuItem.less
|
|
* @class tinymce.ui.MenuItem
|
|
* @extends tinymce.ui.Control
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.MenuItem',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.Factory",
|
|
"tinymce.core.Env",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function (Widget, Factory, Env, Delay) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
border: 0,
|
|
role: 'menuitem'
|
|
},
|
|
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} selectable Selectable menu.
|
|
* @setting {Array} menu Submenu array with items.
|
|
* @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X
|
|
*/
|
|
init: function (settings) {
|
|
var self = this, text;
|
|
|
|
self._super(settings);
|
|
|
|
settings = self.settings;
|
|
|
|
self.classes.add('menu-item');
|
|
|
|
if (settings.menu) {
|
|
self.classes.add('menu-item-expand');
|
|
}
|
|
|
|
if (settings.preview) {
|
|
self.classes.add('menu-item-preview');
|
|
}
|
|
|
|
text = self.state.get('text');
|
|
if (text === '-' || text === '|') {
|
|
self.classes.add('menu-item-sep');
|
|
self.aria('role', 'separator');
|
|
self.state.set('text', '-');
|
|
}
|
|
|
|
if (settings.selectable) {
|
|
self.aria('role', 'menuitemcheckbox');
|
|
self.classes.add('menu-item-checkbox');
|
|
settings.icon = 'selected';
|
|
}
|
|
|
|
if (!settings.preview && !settings.selectable) {
|
|
self.classes.add('menu-item-normal');
|
|
}
|
|
|
|
self.on('mousedown', function (e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
if (settings.menu && !settings.ariaHideMenu) {
|
|
self.aria('haspopup', true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the menuitem has sub menu.
|
|
*
|
|
* @method hasMenus
|
|
* @return {Boolean} True/false state if it has submenu.
|
|
*/
|
|
hasMenus: function () {
|
|
return !!this.settings.menu;
|
|
},
|
|
|
|
/**
|
|
* Shows the menu for the menu item.
|
|
*
|
|
* @method showMenu
|
|
*/
|
|
showMenu: function () {
|
|
var self = this, settings = self.settings, menu, parent = self.parent();
|
|
|
|
parent.items().each(function (ctrl) {
|
|
if (ctrl !== self) {
|
|
ctrl.hideMenu();
|
|
}
|
|
});
|
|
|
|
if (settings.menu) {
|
|
menu = self.menu;
|
|
|
|
if (!menu) {
|
|
menu = settings.menu;
|
|
|
|
// Is menu array then auto constuct menu control
|
|
if (menu.length) {
|
|
menu = {
|
|
type: 'menu',
|
|
items: menu
|
|
};
|
|
} else {
|
|
menu.type = menu.type || 'menu';
|
|
}
|
|
|
|
if (parent.settings.itemDefaults) {
|
|
menu.itemDefaults = parent.settings.itemDefaults;
|
|
}
|
|
|
|
menu = self.menu = Factory.create(menu).parent(self).renderTo();
|
|
menu.reflow();
|
|
menu.on('cancel', function (e) {
|
|
e.stopPropagation();
|
|
self.focus();
|
|
menu.hide();
|
|
});
|
|
menu.on('show hide', function (e) {
|
|
if (e.control.items) {
|
|
e.control.items().each(function (ctrl) {
|
|
ctrl.active(ctrl.settings.selected);
|
|
});
|
|
}
|
|
}).fire('show');
|
|
|
|
menu.on('hide', function (e) {
|
|
if (e.control === menu) {
|
|
self.classes.remove('selected');
|
|
}
|
|
});
|
|
|
|
menu.submenu = true;
|
|
} else {
|
|
menu.show();
|
|
}
|
|
|
|
menu._parentMenu = parent;
|
|
|
|
menu.classes.add('menu-sub');
|
|
|
|
var rel = menu.testMoveRel(
|
|
self.getEl(),
|
|
self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br']
|
|
);
|
|
|
|
menu.moveRel(self.getEl(), rel);
|
|
menu.rel = rel;
|
|
|
|
rel = 'menu-sub-' + rel;
|
|
menu.classes.remove(menu._lastRel).add(rel);
|
|
menu._lastRel = rel;
|
|
|
|
self.classes.add('selected');
|
|
self.aria('expanded', true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Hides the menu for the menu item.
|
|
*
|
|
* @method hideMenu
|
|
*/
|
|
hideMenu: function () {
|
|
var self = this;
|
|
|
|
if (self.menu) {
|
|
self.menu.items().each(function (item) {
|
|
if (item.hideMenu) {
|
|
item.hideMenu();
|
|
}
|
|
});
|
|
|
|
self.menu.hide();
|
|
self.aria('expanded', false);
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.state.get('text');
|
|
var icon = self.settings.icon, image = '', shortcut = settings.shortcut;
|
|
var url = self.encode(settings.url), iconHtml = '';
|
|
|
|
// Converts shortcut format to Mac/PC variants
|
|
function convertShortcut(shortcut) {
|
|
var i, value, replace = {};
|
|
|
|
if (Env.mac) {
|
|
replace = {
|
|
alt: '⌥',
|
|
ctrl: '⌘',
|
|
shift: '⇧',
|
|
meta: '⌘'
|
|
};
|
|
} else {
|
|
replace = {
|
|
meta: 'Ctrl'
|
|
};
|
|
}
|
|
|
|
shortcut = shortcut.split('+');
|
|
|
|
for (i = 0; i < shortcut.length; i++) {
|
|
value = replace[shortcut[i].toLowerCase()];
|
|
|
|
if (value) {
|
|
shortcut[i] = value;
|
|
}
|
|
}
|
|
|
|
return shortcut.join('+');
|
|
}
|
|
|
|
function escapeRegExp(str) {
|
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function markMatches(text) {
|
|
var match = settings.match || '';
|
|
|
|
return match ? text.replace(new RegExp(escapeRegExp(match), 'gi'), function (match) {
|
|
return '!mce~match[' + match + ']mce~match!';
|
|
}) : text;
|
|
}
|
|
|
|
function boldMatches(text) {
|
|
return text.
|
|
replace(new RegExp(escapeRegExp('!mce~match['), 'g'), '<b>').
|
|
replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), '</b>');
|
|
}
|
|
|
|
if (icon) {
|
|
self.parent().classes.add('menu-has-icons');
|
|
}
|
|
|
|
if (settings.image) {
|
|
image = ' style="background-image: url(\'' + settings.image + '\')"';
|
|
}
|
|
|
|
if (shortcut) {
|
|
shortcut = convertShortcut(shortcut);
|
|
}
|
|
|
|
icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
|
|
iconHtml = (text !== '-' ? '<i class="' + icon + '"' + image + '></i>\u00a0' : '');
|
|
|
|
text = boldMatches(self.encode(markMatches(text)));
|
|
url = boldMatches(self.encode(markMatches(url)));
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" tabindex="-1">' +
|
|
iconHtml +
|
|
(text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') +
|
|
(shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' + shortcut + '</div>' : '') +
|
|
(settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') +
|
|
(url ? '<div class="' + prefix + 'menu-item-link">' + url + '</div>' : '') +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Gets invoked after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this, settings = self.settings;
|
|
|
|
var textStyle = settings.textStyle;
|
|
if (typeof textStyle == "function") {
|
|
textStyle = textStyle.call(this);
|
|
}
|
|
|
|
if (textStyle) {
|
|
var textElm = self.getEl('text');
|
|
if (textElm) {
|
|
textElm.setAttribute('style', textStyle);
|
|
}
|
|
}
|
|
|
|
self.on('mouseenter click', function (e) {
|
|
if (e.control === self) {
|
|
if (!settings.menu && e.type === 'click') {
|
|
self.fire('select');
|
|
|
|
// Edge will crash if you stress it see #2660
|
|
Delay.requestAnimationFrame(function () {
|
|
self.parent().hideAll();
|
|
});
|
|
} else {
|
|
self.showMenu();
|
|
|
|
if (e.aria) {
|
|
self.menu.focus(true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
self._super();
|
|
|
|
return self;
|
|
},
|
|
|
|
hover: function () {
|
|
var self = this;
|
|
|
|
self.parent().items().each(function (ctrl) {
|
|
ctrl.classes.remove('selected');
|
|
});
|
|
|
|
self.classes.toggle('selected', true);
|
|
|
|
return self;
|
|
},
|
|
|
|
active: function (state) {
|
|
if (typeof state != "undefined") {
|
|
this.aria('checked', state);
|
|
}
|
|
|
|
return this._super(state);
|
|
},
|
|
|
|
/**
|
|
* Removes the control and it's menus.
|
|
*
|
|
* @method remove
|
|
*/
|
|
remove: function () {
|
|
this._super();
|
|
|
|
if (this.menu) {
|
|
this.menu.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Throbber.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 display a Throbber for any element.
|
|
*
|
|
* @-x-less Throbber.less
|
|
* @class tinymce.ui.Throbber
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Throbber',
|
|
[
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.ui.Control",
|
|
"tinymce.core.util.Delay"
|
|
],
|
|
function ($, Control, Delay) {
|
|
"use strict";
|
|
|
|
/**
|
|
* Constructs a new throbber.
|
|
*
|
|
* @constructor
|
|
* @param {Element} elm DOM Html element to display throbber in.
|
|
* @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll.
|
|
*/
|
|
return function (elm, inline) {
|
|
var self = this, state, classPrefix = Control.classPrefix, timer;
|
|
|
|
/**
|
|
* Shows the throbber.
|
|
*
|
|
* @method show
|
|
* @param {Number} [time] Time to wait before showing.
|
|
* @param {function} [callback] Optional callback to execute when the throbber is shown.
|
|
* @return {tinymce.ui.Throbber} Current throbber instance.
|
|
*/
|
|
self.show = function (time, callback) {
|
|
function render() {
|
|
if (state) {
|
|
$(elm).append(
|
|
'<div class="' + classPrefix + 'throbber' + (inline ? ' ' + classPrefix + 'throbber-inline' : '') + '"></div>'
|
|
);
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
}
|
|
|
|
self.hide();
|
|
|
|
state = true;
|
|
|
|
if (time) {
|
|
timer = Delay.setTimeout(render, time);
|
|
} else {
|
|
render();
|
|
}
|
|
|
|
return self;
|
|
};
|
|
|
|
/**
|
|
* Hides the throbber.
|
|
*
|
|
* @method hide
|
|
* @return {tinymce.ui.Throbber} Current throbber instance.
|
|
*/
|
|
self.hide = function () {
|
|
var child = elm.lastChild;
|
|
|
|
Delay.clearTimeout(timer);
|
|
|
|
if (child && child.className.indexOf('throbber') != -1) {
|
|
child.parentNode.removeChild(child);
|
|
}
|
|
|
|
state = false;
|
|
|
|
return self;
|
|
};
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Menu.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new menu.
|
|
*
|
|
* @-x-less Menu.less
|
|
* @class tinymce.ui.Menu
|
|
* @extends tinymce.ui.FloatPanel
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Menu',
|
|
[
|
|
"tinymce.core.ui.FloatPanel",
|
|
"tinymce.core.ui.MenuItem",
|
|
"tinymce.core.ui.Throbber",
|
|
"tinymce.core.util.Tools"
|
|
],
|
|
function (FloatPanel, MenuItem, Throbber, Tools) {
|
|
"use strict";
|
|
|
|
return FloatPanel.extend({
|
|
Defaults: {
|
|
defaultType: 'menuitem',
|
|
border: 1,
|
|
layout: 'stack',
|
|
role: 'application',
|
|
bodyRole: 'menu',
|
|
ariaRoot: true
|
|
},
|
|
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
settings.autohide = true;
|
|
settings.constrainToViewport = true;
|
|
|
|
if (typeof settings.items === 'function') {
|
|
settings.itemsFactory = settings.items;
|
|
settings.items = [];
|
|
}
|
|
|
|
if (settings.itemDefaults) {
|
|
var items = settings.items, i = items.length;
|
|
|
|
while (i--) {
|
|
items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
|
|
}
|
|
}
|
|
|
|
self._super(settings);
|
|
self.classes.add('menu');
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
this.classes.toggle('menu-align', true);
|
|
|
|
this._super();
|
|
|
|
this.getEl().style.height = '';
|
|
this.getEl('body').style.height = '';
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Hides/closes the menu.
|
|
*
|
|
* @method cancel
|
|
*/
|
|
cancel: function () {
|
|
var self = this;
|
|
|
|
self.hideAll();
|
|
self.fire('select');
|
|
},
|
|
|
|
/**
|
|
* Loads new items from the factory items function.
|
|
*
|
|
* @method load
|
|
*/
|
|
load: function () {
|
|
var self = this, time, factory;
|
|
|
|
function hideThrobber() {
|
|
if (self.throbber) {
|
|
self.throbber.hide();
|
|
self.throbber = null;
|
|
}
|
|
}
|
|
|
|
factory = self.settings.itemsFactory;
|
|
if (!factory) {
|
|
return;
|
|
}
|
|
|
|
if (!self.throbber) {
|
|
self.throbber = new Throbber(self.getEl('body'), true);
|
|
|
|
if (self.items().length === 0) {
|
|
self.throbber.show();
|
|
self.fire('loading');
|
|
} else {
|
|
self.throbber.show(100, function () {
|
|
self.items().remove();
|
|
self.fire('loading');
|
|
});
|
|
}
|
|
|
|
self.on('hide close', hideThrobber);
|
|
}
|
|
|
|
self.requestTime = time = new Date().getTime();
|
|
|
|
self.settings.itemsFactory(function (items) {
|
|
if (items.length === 0) {
|
|
self.hide();
|
|
return;
|
|
}
|
|
|
|
if (self.requestTime !== time) {
|
|
return;
|
|
}
|
|
|
|
self.getEl().style.width = '';
|
|
self.getEl('body').style.width = '';
|
|
|
|
hideThrobber();
|
|
self.items().remove();
|
|
self.getEl('body').innerHTML = '';
|
|
|
|
self.add(items);
|
|
self.renderNew();
|
|
self.fire('loaded');
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Hide menu and all sub menus.
|
|
*
|
|
* @method hideAll
|
|
*/
|
|
hideAll: function () {
|
|
var self = this;
|
|
|
|
this.find('menuitem').exec('hideMenu');
|
|
|
|
return self._super();
|
|
},
|
|
|
|
/**
|
|
* Invoked before the menu is rendered.
|
|
*
|
|
* @method preRender
|
|
*/
|
|
preRender: function () {
|
|
var self = this;
|
|
|
|
self.items().each(function (ctrl) {
|
|
var settings = ctrl.settings;
|
|
|
|
if (settings.icon || settings.image || settings.selectable) {
|
|
self._hasIcons = true;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (self.settings.itemsFactory) {
|
|
self.on('postrender', function () {
|
|
if (self.settings.itemsFactory) {
|
|
self.load();
|
|
}
|
|
});
|
|
}
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* ListBox.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new list box control.
|
|
*
|
|
* @-x-less ListBox.less
|
|
* @class tinymce.ui.ListBox
|
|
* @extends tinymce.ui.MenuButton
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ListBox',
|
|
[
|
|
"tinymce.core.ui.MenuButton",
|
|
"tinymce.core.ui.Menu"
|
|
],
|
|
function (MenuButton, Menu) {
|
|
"use strict";
|
|
|
|
return MenuButton.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Array} values Array with values to add to list box.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this, values, selected, selectedText, lastItemCtrl;
|
|
|
|
function setSelected(menuValues) {
|
|
// Try to find a selected value
|
|
for (var i = 0; i < menuValues.length; i++) {
|
|
selected = menuValues[i].selected || settings.value === menuValues[i].value;
|
|
|
|
if (selected) {
|
|
selectedText = selectedText || menuValues[i].text;
|
|
self.state.set('value', menuValues[i].value);
|
|
return true;
|
|
}
|
|
|
|
// If the value has a submenu, try to find the selected values in that menu
|
|
if (menuValues[i].menu) {
|
|
if (setSelected(menuValues[i].menu)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self._super(settings);
|
|
settings = self.settings;
|
|
|
|
self._values = values = settings.values;
|
|
if (values) {
|
|
if (typeof settings.value != "undefined") {
|
|
setSelected(values);
|
|
}
|
|
|
|
// Default with first item
|
|
if (!selected && values.length > 0) {
|
|
selectedText = values[0].text;
|
|
self.state.set('value', values[0].value);
|
|
}
|
|
|
|
self.state.set('menu', values);
|
|
}
|
|
|
|
self.state.set('text', settings.text || selectedText);
|
|
|
|
self.classes.add('listbox');
|
|
|
|
self.on('select', function (e) {
|
|
var ctrl = e.control;
|
|
|
|
if (lastItemCtrl) {
|
|
e.lastControl = lastItemCtrl;
|
|
}
|
|
|
|
if (settings.multiple) {
|
|
ctrl.active(!ctrl.active());
|
|
} else {
|
|
self.value(e.control.value());
|
|
}
|
|
|
|
lastItemCtrl = ctrl;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Getter/setter function for the control value.
|
|
*
|
|
* @method value
|
|
* @param {String} [value] Value to be set.
|
|
* @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation.
|
|
*/
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
function activateMenuItemsByValue(menu, value) {
|
|
if (menu instanceof Menu) {
|
|
menu.items().each(function (ctrl) {
|
|
if (!ctrl.hasMenus()) {
|
|
ctrl.active(ctrl.value() === value);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function getSelectedItem(menuValues, value) {
|
|
var selectedItem;
|
|
|
|
if (!menuValues) {
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < menuValues.length; i++) {
|
|
if (menuValues[i].value === value) {
|
|
return menuValues[i];
|
|
}
|
|
|
|
if (menuValues[i].menu) {
|
|
selectedItem = getSelectedItem(menuValues[i].menu, value);
|
|
if (selectedItem) {
|
|
return selectedItem;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.on('show', function (e) {
|
|
activateMenuItemsByValue(e.control, self.value());
|
|
});
|
|
|
|
self.state.on('change:value', function (e) {
|
|
var selectedItem = getSelectedItem(self.state.get('menu'), e.value);
|
|
|
|
if (selectedItem) {
|
|
self.text(selectedItem.text);
|
|
} else {
|
|
self.text(self.settings.text);
|
|
}
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Radio.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new radio button.
|
|
*
|
|
* @-x-less Radio.less
|
|
* @class tinymce.ui.Radio
|
|
* @extends tinymce.ui.Checkbox
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Radio',
|
|
[
|
|
"tinymce.core.ui.Checkbox"
|
|
],
|
|
function (Checkbox) {
|
|
"use strict";
|
|
|
|
return Checkbox.extend({
|
|
Defaults: {
|
|
classes: "radio",
|
|
role: "radio"
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* ResizeHandle.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
|
|
*/
|
|
|
|
/**
|
|
* Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events.
|
|
*
|
|
* @-x-less ResizeHandle.less
|
|
* @class tinymce.ui.ResizeHandle
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.ResizeHandle',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.DragHelper"
|
|
],
|
|
function (Widget, DragHelper) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, prefix = self.classPrefix;
|
|
|
|
self.classes.add('resizehandle');
|
|
|
|
if (self.settings.direction == "both") {
|
|
self.classes.add('resizehandle-both');
|
|
}
|
|
|
|
self.canFocus = false;
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '">' +
|
|
'<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self._super();
|
|
|
|
self.resizeDragHelper = new DragHelper(this._id, {
|
|
start: function () {
|
|
self.fire('ResizeStart');
|
|
},
|
|
|
|
drag: function (e) {
|
|
if (self.settings.direction != "both") {
|
|
e.deltaX = 0;
|
|
}
|
|
|
|
self.fire('Resize', e);
|
|
},
|
|
|
|
stop: function () {
|
|
self.fire('ResizeEnd');
|
|
}
|
|
});
|
|
},
|
|
|
|
remove: function () {
|
|
if (this.resizeDragHelper) {
|
|
this.resizeDragHelper.destroy();
|
|
}
|
|
|
|
return this._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* SelectBox.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new select box control.
|
|
*
|
|
* @-x-less SelectBox.less
|
|
* @class tinymce.ui.SelectBox
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.SelectBox',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
function createOptions(options) {
|
|
var strOptions = '';
|
|
if (options) {
|
|
for (var i = 0; i < options.length; i++) {
|
|
strOptions += '<option value="' + options[i] + '">' + options[i] + '</option>';
|
|
}
|
|
}
|
|
return strOptions;
|
|
}
|
|
|
|
return Widget.extend({
|
|
Defaults: {
|
|
classes: "selectbox",
|
|
role: "selectbox",
|
|
options: []
|
|
},
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Array} options Array with options to add to the select box.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
|
|
if (self.settings.size) {
|
|
self.size = self.settings.size;
|
|
}
|
|
|
|
if (self.settings.options) {
|
|
self._options = self.settings.options;
|
|
}
|
|
|
|
self.on('keydown', function (e) {
|
|
var rootControl;
|
|
|
|
if (e.keyCode == 13) {
|
|
e.preventDefault();
|
|
|
|
// Find root control that we can do toJSON on
|
|
self.parents().reverse().each(function (ctrl) {
|
|
if (ctrl.toJSON) {
|
|
rootControl = ctrl;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Fire event on current text box with the serialized data of the whole form
|
|
self.fire('submit', { data: rootControl.toJSON() });
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Getter/setter function for the options state.
|
|
*
|
|
* @method options
|
|
* @param {Array} [state] State to be set.
|
|
* @return {Array|tinymce.ui.SelectBox} Array of string options.
|
|
*/
|
|
options: function (state) {
|
|
if (!arguments.length) {
|
|
return this.state.get('options');
|
|
}
|
|
|
|
this.state.set('options', state);
|
|
|
|
return this;
|
|
},
|
|
|
|
renderHtml: function () {
|
|
var self = this, options, size = '';
|
|
|
|
options = createOptions(self._options);
|
|
|
|
if (self.size) {
|
|
size = ' size = "' + self.size + '"';
|
|
}
|
|
|
|
return (
|
|
'<select id="' + self._id + '" class="' + self.classes + '"' + size + '>' +
|
|
options +
|
|
'</select>'
|
|
);
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:options', function (e) {
|
|
self.getEl().innerHTML = createOptions(e.value);
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Slider.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
|
|
*/
|
|
|
|
/**
|
|
* Slider control.
|
|
*
|
|
* @-x-less Slider.less
|
|
* @class tinymce.ui.Slider
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Slider',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.ui.DragHelper",
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (Widget, DragHelper, DomUtils) {
|
|
"use strict";
|
|
|
|
function constrain(value, minVal, maxVal) {
|
|
if (value < minVal) {
|
|
value = minVal;
|
|
}
|
|
|
|
if (value > maxVal) {
|
|
value = maxVal;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function setAriaProp(el, name, value) {
|
|
el.setAttribute('aria-' + name, value);
|
|
}
|
|
|
|
function updateSliderHandle(ctrl, value) {
|
|
var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl;
|
|
|
|
if (ctrl.settings.orientation == "v") {
|
|
stylePosName = "top";
|
|
sizeName = "height";
|
|
shortSizeName = "h";
|
|
} else {
|
|
stylePosName = "left";
|
|
sizeName = "width";
|
|
shortSizeName = "w";
|
|
}
|
|
|
|
handleEl = ctrl.getEl('handle');
|
|
maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
|
|
|
|
styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px';
|
|
handleEl.style[stylePosName] = styleValue;
|
|
handleEl.style.height = ctrl.layoutRect().h + 'px';
|
|
|
|
setAriaProp(handleEl, 'valuenow', value);
|
|
setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value));
|
|
setAriaProp(handleEl, 'valuemin', ctrl._minValue);
|
|
setAriaProp(handleEl, 'valuemax', ctrl._maxValue);
|
|
}
|
|
|
|
return Widget.extend({
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
if (!settings.previewFilter) {
|
|
settings.previewFilter = function (value) {
|
|
return Math.round(value * 100) / 100.0;
|
|
};
|
|
}
|
|
|
|
self._super(settings);
|
|
self.classes.add('slider');
|
|
|
|
if (settings.orientation == "v") {
|
|
self.classes.add('vertical');
|
|
}
|
|
|
|
self._minValue = settings.minValue || 0;
|
|
self._maxValue = settings.maxValue || 100;
|
|
self._initValue = self.state.get('value');
|
|
},
|
|
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix;
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '">' +
|
|
'<div id="' + id + '-handle" class="' + prefix + 'slider-handle" role="slider" tabindex="-1"></div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
reset: function () {
|
|
this.value(this._initValue).repaint();
|
|
},
|
|
|
|
postRender: function () {
|
|
var self = this, minValue, maxValue, screenCordName,
|
|
stylePosName, sizeName, shortSizeName;
|
|
|
|
function toFraction(min, max, val) {
|
|
return (val + min) / (max - min);
|
|
}
|
|
|
|
function fromFraction(min, max, val) {
|
|
return (val * (max - min)) - min;
|
|
}
|
|
|
|
function handleKeyboard(minValue, maxValue) {
|
|
function alter(delta) {
|
|
var value;
|
|
|
|
value = self.value();
|
|
value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05));
|
|
value = constrain(value, minValue, maxValue);
|
|
|
|
self.value(value);
|
|
|
|
self.fire('dragstart', { value: value });
|
|
self.fire('drag', { value: value });
|
|
self.fire('dragend', { value: value });
|
|
}
|
|
|
|
self.on('keydown', function (e) {
|
|
switch (e.keyCode) {
|
|
case 37:
|
|
case 38:
|
|
alter(-1);
|
|
break;
|
|
|
|
case 39:
|
|
case 40:
|
|
alter(1);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleDrag(minValue, maxValue, handleEl) {
|
|
var startPos, startHandlePos, maxHandlePos, handlePos, value;
|
|
|
|
self._dragHelper = new DragHelper(self._id, {
|
|
handle: self._id + "-handle",
|
|
|
|
start: function (e) {
|
|
startPos = e[screenCordName];
|
|
startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10);
|
|
maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName];
|
|
self.fire('dragstart', { value: value });
|
|
},
|
|
|
|
drag: function (e) {
|
|
var delta = e[screenCordName] - startPos;
|
|
|
|
handlePos = constrain(startHandlePos + delta, 0, maxHandlePos);
|
|
handleEl.style[stylePosName] = handlePos + 'px';
|
|
|
|
value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue);
|
|
self.value(value);
|
|
|
|
self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc');
|
|
|
|
self.fire('drag', { value: value });
|
|
},
|
|
|
|
stop: function () {
|
|
self.tooltip().hide();
|
|
self.fire('dragend', { value: value });
|
|
}
|
|
});
|
|
}
|
|
|
|
minValue = self._minValue;
|
|
maxValue = self._maxValue;
|
|
|
|
if (self.settings.orientation == "v") {
|
|
screenCordName = "screenY";
|
|
stylePosName = "top";
|
|
sizeName = "height";
|
|
shortSizeName = "h";
|
|
} else {
|
|
screenCordName = "screenX";
|
|
stylePosName = "left";
|
|
sizeName = "width";
|
|
shortSizeName = "w";
|
|
}
|
|
|
|
self._super();
|
|
|
|
handleKeyboard(minValue, maxValue, self.getEl('handle'));
|
|
handleDrag(minValue, maxValue, self.getEl('handle'));
|
|
},
|
|
|
|
repaint: function () {
|
|
this._super();
|
|
updateSliderHandle(this, this.value());
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:value', function (e) {
|
|
updateSliderHandle(self, e.value);
|
|
});
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* Spacer.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a spacer. This control is used in flex layouts for example.
|
|
*
|
|
* @-x-less Spacer.less
|
|
* @class tinymce.ui.Spacer
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.Spacer',
|
|
[
|
|
"tinymce.core.ui.Widget"
|
|
],
|
|
function (Widget) {
|
|
"use strict";
|
|
|
|
return Widget.extend({
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this;
|
|
|
|
self.classes.add('spacer');
|
|
self.canFocus = false;
|
|
|
|
return '<div id="' + self._id + '" class="' + self.classes + '"></div>';
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* SplitButton.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a split button.
|
|
*
|
|
* @-x-less SplitButton.less
|
|
* @class tinymce.ui.SplitButton
|
|
* @extends tinymce.ui.Button
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.SplitButton',
|
|
[
|
|
"tinymce.core.ui.MenuButton",
|
|
"tinymce.core.ui.DomUtils",
|
|
"tinymce.core.dom.DomQuery"
|
|
],
|
|
function (MenuButton, DomUtils, $) {
|
|
return MenuButton.extend({
|
|
Defaults: {
|
|
classes: "widget btn splitbtn",
|
|
role: "button"
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm;
|
|
|
|
self._super();
|
|
|
|
mainButtonElm = elm.firstChild;
|
|
menuButtonElm = elm.lastChild;
|
|
|
|
$(mainButtonElm).css({
|
|
width: rect.w - DomUtils.getSize(menuButtonElm).width,
|
|
height: rect.h - 2
|
|
});
|
|
|
|
$(menuButtonElm).css({
|
|
height: rect.h - 2
|
|
});
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Sets the active menu state.
|
|
*
|
|
* @private
|
|
*/
|
|
activeMenu: function (state) {
|
|
var self = this;
|
|
|
|
$(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state);
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, id = self._id, prefix = self.classPrefix, image;
|
|
var icon = self.state.get('icon'), text = self.state.get('text'),
|
|
textHtml = '';
|
|
|
|
image = self.settings.image;
|
|
if (image) {
|
|
icon = 'none';
|
|
|
|
// Support for [high dpi, low dpi] image sources
|
|
if (typeof image != "string") {
|
|
image = window.getSelection ? image[0] : image[1];
|
|
}
|
|
|
|
image = ' style="background-image: url(\'' + image + '\')"';
|
|
} else {
|
|
image = '';
|
|
}
|
|
|
|
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
|
|
|
|
if (text) {
|
|
self.classes.add('btn-has-text');
|
|
textHtml = '<span class="' + prefix + 'txt">' + self.encode(text) + '</span>';
|
|
}
|
|
|
|
return (
|
|
'<div id="' + id + '" class="' + self.classes + '" role="button" tabindex="-1">' +
|
|
'<button type="button" hidefocus="1" tabindex="-1">' +
|
|
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
|
textHtml +
|
|
'</button>' +
|
|
'<button type="button" class="' + prefix + 'open" hidefocus="1" tabindex="-1">' +
|
|
//(icon ? '<i class="' + icon + '"></i>' : '') +
|
|
(self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') +
|
|
' <i class="' + prefix + 'caret"></i>' +
|
|
'</button>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this, onClickHandler = self.settings.onclick;
|
|
|
|
self.on('click', function (e) {
|
|
var node = e.target;
|
|
|
|
if (e.control == this) {
|
|
// Find clicks that is on the main button
|
|
while (node) {
|
|
if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) {
|
|
e.stopImmediatePropagation();
|
|
|
|
if (onClickHandler) {
|
|
onClickHandler.call(this, e);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
}
|
|
});
|
|
|
|
delete self.settings.onclick;
|
|
|
|
return self._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* StackLayout.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 layout uses the browsers layout when the items are blocks.
|
|
*
|
|
* @-x-less StackLayout.less
|
|
* @class tinymce.ui.StackLayout
|
|
* @extends tinymce.ui.FlowLayout
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.StackLayout',
|
|
[
|
|
"tinymce.core.ui.FlowLayout"
|
|
],
|
|
function (FlowLayout) {
|
|
"use strict";
|
|
|
|
return FlowLayout.extend({
|
|
Defaults: {
|
|
containerClass: 'stack-layout',
|
|
controlClass: 'stack-layout-item',
|
|
endClass: 'break'
|
|
},
|
|
|
|
isNative: function () {
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
/**
|
|
* TabPanel.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a tab panel control.
|
|
*
|
|
* @-x-less TabPanel.less
|
|
* @class tinymce.ui.TabPanel
|
|
* @extends tinymce.ui.Panel
|
|
*
|
|
* @setting {Number} activeTab Active tab index.
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.TabPanel',
|
|
[
|
|
"tinymce.core.ui.Panel",
|
|
"tinymce.core.dom.DomQuery",
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (Panel, $, DomUtils) {
|
|
"use strict";
|
|
|
|
return Panel.extend({
|
|
Defaults: {
|
|
layout: 'absolute',
|
|
defaults: {
|
|
type: 'panel'
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Activates the specified tab by index.
|
|
*
|
|
* @method activateTab
|
|
* @param {Number} idx Index of the tab to activate.
|
|
*/
|
|
activateTab: function (idx) {
|
|
var activeTabElm;
|
|
|
|
if (this.activeTabId) {
|
|
activeTabElm = this.getEl(this.activeTabId);
|
|
$(activeTabElm).removeClass(this.classPrefix + 'active');
|
|
activeTabElm.setAttribute('aria-selected', "false");
|
|
}
|
|
|
|
this.activeTabId = 't' + idx;
|
|
|
|
activeTabElm = this.getEl('t' + idx);
|
|
activeTabElm.setAttribute('aria-selected', "true");
|
|
$(activeTabElm).addClass(this.classPrefix + 'active');
|
|
|
|
this.items()[idx].show().fire('showtab');
|
|
this.reflow();
|
|
|
|
this.items().each(function (item, i) {
|
|
if (idx != i) {
|
|
item.hide();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix;
|
|
|
|
self.preRender();
|
|
layout.preRender(self);
|
|
|
|
self.items().each(function (ctrl, i) {
|
|
var id = self._id + '-t' + i;
|
|
|
|
ctrl.aria('role', 'tabpanel');
|
|
ctrl.aria('labelledby', id);
|
|
|
|
tabsHtml += (
|
|
'<div id="' + id + '" class="' + prefix + 'tab" ' +
|
|
'unselectable="on" role="tab" aria-controls="' + ctrl._id + '" aria-selected="false" tabIndex="-1">' +
|
|
self.encode(ctrl.settings.title) +
|
|
'</div>'
|
|
);
|
|
});
|
|
|
|
return (
|
|
'<div id="' + self._id + '" class="' + self.classes + '" hidefocus="1" tabindex="-1">' +
|
|
'<div id="' + self._id + '-head" class="' + prefix + 'tabs" role="tablist">' +
|
|
tabsHtml +
|
|
'</div>' +
|
|
'<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
|
|
layout.renderHtml(self) +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self._super();
|
|
|
|
self.settings.activeTab = self.settings.activeTab || 0;
|
|
self.activateTab(self.settings.activeTab);
|
|
|
|
this.on('click', function (e) {
|
|
var targetParent = e.target.parentNode;
|
|
|
|
if (targetParent && targetParent.id == self._id + '-head') {
|
|
var i = targetParent.childNodes.length;
|
|
|
|
while (i--) {
|
|
if (targetParent.childNodes[i] == e.target) {
|
|
self.activateTab(i);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initializes the current controls layout rect.
|
|
* This will be executed by the layout managers to determine the
|
|
* default minWidth/minHeight etc.
|
|
*
|
|
* @method initLayoutRect
|
|
* @return {Object} Layout rect instance.
|
|
*/
|
|
initLayoutRect: function () {
|
|
var self = this, rect, minW, minH;
|
|
|
|
minW = DomUtils.getSize(self.getEl('head')).width;
|
|
minW = minW < 0 ? 0 : minW;
|
|
minH = 0;
|
|
|
|
self.items().each(function (item) {
|
|
minW = Math.max(minW, item.layoutRect().minW);
|
|
minH = Math.max(minH, item.layoutRect().minH);
|
|
});
|
|
|
|
self.items().each(function (ctrl) {
|
|
ctrl.settings.x = 0;
|
|
ctrl.settings.y = 0;
|
|
ctrl.settings.w = minW;
|
|
ctrl.settings.h = minH;
|
|
|
|
ctrl.layoutRect({
|
|
x: 0,
|
|
y: 0,
|
|
w: minW,
|
|
h: minH
|
|
});
|
|
});
|
|
|
|
var headH = DomUtils.getSize(self.getEl('head')).height;
|
|
|
|
self.settings.minWidth = minW;
|
|
self.settings.minHeight = minH + headH;
|
|
|
|
rect = self._super();
|
|
rect.deltaH += headH;
|
|
rect.innerH = rect.h - rect.deltaH;
|
|
|
|
return rect;
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* TextBox.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
|
|
*/
|
|
|
|
/**
|
|
* Creates a new textbox.
|
|
*
|
|
* @-x-less TextBox.less
|
|
* @class tinymce.ui.TextBox
|
|
* @extends tinymce.ui.Widget
|
|
*/
|
|
define(
|
|
'tinymce.core.ui.TextBox',
|
|
[
|
|
"tinymce.core.ui.Widget",
|
|
"tinymce.core.util.Tools",
|
|
"tinymce.core.ui.DomUtils"
|
|
],
|
|
function (Widget, Tools, DomUtils) {
|
|
return Widget.extend({
|
|
/**
|
|
* Constructs a instance with the specified settings.
|
|
*
|
|
* @constructor
|
|
* @param {Object} settings Name/value object with settings.
|
|
* @setting {Boolean} multiline True if the textbox is a multiline control.
|
|
* @setting {Number} maxLength Max length for the textbox.
|
|
* @setting {Number} size Size of the textbox in characters.
|
|
*/
|
|
init: function (settings) {
|
|
var self = this;
|
|
|
|
self._super(settings);
|
|
|
|
self.classes.add('textbox');
|
|
|
|
if (settings.multiline) {
|
|
self.classes.add('multiline');
|
|
} else {
|
|
self.on('keydown', function (e) {
|
|
var rootControl;
|
|
|
|
if (e.keyCode == 13) {
|
|
e.preventDefault();
|
|
|
|
// Find root control that we can do toJSON on
|
|
self.parents().reverse().each(function (ctrl) {
|
|
if (ctrl.toJSON) {
|
|
rootControl = ctrl;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Fire event on current text box with the serialized data of the whole form
|
|
self.fire('submit', { data: rootControl.toJSON() });
|
|
}
|
|
});
|
|
|
|
self.on('keyup', function (e) {
|
|
self.state.set('value', e.target.value);
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Repaints the control after a layout operation.
|
|
*
|
|
* @method repaint
|
|
*/
|
|
repaint: function () {
|
|
var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect;
|
|
|
|
style = self.getEl().style;
|
|
rect = self._layoutRect;
|
|
lastRepaintRect = self._lastRepaintRect || {};
|
|
|
|
// Detect old IE 7+8 add lineHeight to align caret vertically in the middle
|
|
var doc = document;
|
|
if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) {
|
|
style.lineHeight = (rect.h - borderH) + 'px';
|
|
}
|
|
|
|
borderBox = self.borderBox;
|
|
borderW = borderBox.left + borderBox.right + 8;
|
|
borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
|
|
|
|
if (rect.x !== lastRepaintRect.x) {
|
|
style.left = rect.x + 'px';
|
|
lastRepaintRect.x = rect.x;
|
|
}
|
|
|
|
if (rect.y !== lastRepaintRect.y) {
|
|
style.top = rect.y + 'px';
|
|
lastRepaintRect.y = rect.y;
|
|
}
|
|
|
|
if (rect.w !== lastRepaintRect.w) {
|
|
style.width = (rect.w - borderW) + 'px';
|
|
lastRepaintRect.w = rect.w;
|
|
}
|
|
|
|
if (rect.h !== lastRepaintRect.h) {
|
|
style.height = (rect.h - borderH) + 'px';
|
|
lastRepaintRect.h = rect.h;
|
|
}
|
|
|
|
self._lastRepaintRect = lastRepaintRect;
|
|
self.fire('repaint', {}, false);
|
|
|
|
return self;
|
|
},
|
|
|
|
/**
|
|
* Renders the control as a HTML string.
|
|
*
|
|
* @method renderHtml
|
|
* @return {String} HTML representing the control.
|
|
*/
|
|
renderHtml: function () {
|
|
var self = this, settings = self.settings, attrs, elm;
|
|
|
|
attrs = {
|
|
id: self._id,
|
|
hidefocus: '1'
|
|
};
|
|
|
|
Tools.each([
|
|
'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min',
|
|
'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple'
|
|
], function (name) {
|
|
attrs[name] = settings[name];
|
|
});
|
|
|
|
if (self.disabled()) {
|
|
attrs.disabled = 'disabled';
|
|
}
|
|
|
|
if (settings.subtype) {
|
|
attrs.type = settings.subtype;
|
|
}
|
|
|
|
elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs);
|
|
elm.value = self.state.get('value');
|
|
elm.className = self.classes;
|
|
|
|
return elm.outerHTML;
|
|
},
|
|
|
|
value: function (value) {
|
|
if (arguments.length) {
|
|
this.state.set('value', value);
|
|
return this;
|
|
}
|
|
|
|
// Make sure the real state is in sync
|
|
if (this.state.get('rendered')) {
|
|
this.state.set('value', this.getEl().value);
|
|
}
|
|
|
|
return this.state.get('value');
|
|
},
|
|
|
|
/**
|
|
* Called after the control has been rendered.
|
|
*
|
|
* @method postRender
|
|
*/
|
|
postRender: function () {
|
|
var self = this;
|
|
|
|
self.getEl().value = self.state.get('value');
|
|
self._super();
|
|
|
|
self.$el.on('change', function (e) {
|
|
self.state.set('value', e.target.value);
|
|
self.fire('change', e);
|
|
});
|
|
},
|
|
|
|
bindStates: function () {
|
|
var self = this;
|
|
|
|
self.state.on('change:value', function (e) {
|
|
if (self.getEl().value != e.value) {
|
|
self.getEl().value = e.value;
|
|
}
|
|
});
|
|
|
|
self.state.on('change:disabled', function (e) {
|
|
self.getEl().disabled = e.value;
|
|
});
|
|
|
|
return self._super();
|
|
},
|
|
|
|
remove: function () {
|
|
this.$el.off();
|
|
this._super();
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Api.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.ui.Api',
|
|
[
|
|
'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.Window',
|
|
'tinymce.core.ui.MessageBox',
|
|
'tinymce.core.ui.Tooltip',
|
|
'tinymce.core.ui.Widget',
|
|
'tinymce.core.ui.Progress',
|
|
'tinymce.core.ui.Notification',
|
|
'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'
|
|
],
|
|
function (
|
|
Selector, Collection, ReflowQueue, Control, Factory, KeyboardNavigation, Container, DragHelper, Scrollable, Panel, Movable,
|
|
Resizable, FloatPanel, Window, MessageBox, Tooltip, Widget, Progress, Notification, Layout, AbsoluteLayout, Button,
|
|
ButtonGroup, Checkbox, ComboBox, ColorBox, PanelButton, ColorButton, ColorPicker, Path, ElementPath, FormItem, Form,
|
|
FieldSet, FilePicker, FitLayout, FlexLayout, FlowLayout, FormatControls, GridLayout, Iframe, InfoBox, Label, Toolbar,
|
|
MenuBar, MenuButton, MenuItem, Throbber, Menu, ListBox, Radio, ResizeHandle, SelectBox, Slider, Spacer, SplitButton,
|
|
StackLayout, TabPanel, TextBox
|
|
) {
|
|
"use strict";
|
|
|
|
var registerToFactory = function (id, ref) {
|
|
Factory.add(id.split('.').pop(), ref);
|
|
};
|
|
|
|
var expose = function (target, id, ref) {
|
|
var i, fragments;
|
|
|
|
fragments = id.split(/[.\/]/);
|
|
for (i = 0; i < fragments.length - 1; ++i) {
|
|
if (target[fragments[i]] === undefined) {
|
|
target[fragments[i]] = {};
|
|
}
|
|
|
|
target = target[fragments[i]];
|
|
}
|
|
|
|
target[fragments[fragments.length - 1]] = ref;
|
|
|
|
registerToFactory(id, ref);
|
|
};
|
|
|
|
var appendTo = function (target) {
|
|
expose(target, 'ui.Selector', Selector);
|
|
expose(target, 'ui.Collection', Collection);
|
|
expose(target, 'ui.ReflowQueue', ReflowQueue);
|
|
expose(target, 'ui.Control', Control);
|
|
expose(target, 'ui.Factory', Factory);
|
|
expose(target, 'ui.KeyboardNavigation', KeyboardNavigation);
|
|
expose(target, 'ui.Container', Container);
|
|
expose(target, 'ui.DragHelper', DragHelper);
|
|
expose(target, 'ui.Scrollable', Scrollable);
|
|
expose(target, 'ui.Panel', Panel);
|
|
expose(target, 'ui.Movable', Movable);
|
|
expose(target, 'ui.Resizable', Resizable);
|
|
expose(target, 'ui.FloatPanel', FloatPanel);
|
|
expose(target, 'ui.Window', Window);
|
|
expose(target, 'ui.MessageBox', MessageBox);
|
|
expose(target, 'ui.Tooltip', Tooltip);
|
|
expose(target, 'ui.Widget', Widget);
|
|
expose(target, 'ui.Progress', Progress);
|
|
expose(target, 'ui.Notification', Notification);
|
|
expose(target, 'ui.Layout', Layout);
|
|
expose(target, 'ui.AbsoluteLayout', AbsoluteLayout);
|
|
expose(target, 'ui.Button', Button);
|
|
expose(target, 'ui.ButtonGroup', ButtonGroup);
|
|
expose(target, 'ui.Checkbox', Checkbox);
|
|
expose(target, 'ui.ComboBox', ComboBox);
|
|
expose(target, 'ui.ColorBox', ColorBox);
|
|
expose(target, 'ui.PanelButton', PanelButton);
|
|
expose(target, 'ui.ColorButton', ColorButton);
|
|
expose(target, 'ui.ColorPicker', ColorPicker);
|
|
expose(target, 'ui.Path', Path);
|
|
expose(target, 'ui.ElementPath', ElementPath);
|
|
expose(target, 'ui.FormItem', FormItem);
|
|
expose(target, 'ui.Form', Form);
|
|
expose(target, 'ui.FieldSet', FieldSet);
|
|
expose(target, 'ui.FilePicker', FilePicker);
|
|
expose(target, 'ui.FitLayout', FitLayout);
|
|
expose(target, 'ui.FlexLayout', FlexLayout);
|
|
expose(target, 'ui.FlowLayout', FlowLayout);
|
|
expose(target, 'ui.FormatControls', FormatControls);
|
|
expose(target, 'ui.GridLayout', GridLayout);
|
|
expose(target, 'ui.Iframe', Iframe);
|
|
expose(target, 'ui.InfoBox', InfoBox);
|
|
expose(target, 'ui.Label', Label);
|
|
expose(target, 'ui.Toolbar', Toolbar);
|
|
expose(target, 'ui.MenuBar', MenuBar);
|
|
expose(target, 'ui.MenuButton', MenuButton);
|
|
expose(target, 'ui.MenuItem', MenuItem);
|
|
expose(target, 'ui.Throbber', Throbber);
|
|
expose(target, 'ui.Menu', Menu);
|
|
expose(target, 'ui.ListBox', ListBox);
|
|
expose(target, 'ui.Radio', Radio);
|
|
expose(target, 'ui.ResizeHandle', ResizeHandle);
|
|
expose(target, 'ui.SelectBox', SelectBox);
|
|
expose(target, 'ui.Slider', Slider);
|
|
expose(target, 'ui.Spacer', Spacer);
|
|
expose(target, 'ui.SplitButton', SplitButton);
|
|
expose(target, 'ui.StackLayout', StackLayout);
|
|
expose(target, 'ui.TabPanel', TabPanel);
|
|
expose(target, 'ui.TextBox', TextBox);
|
|
expose(target, 'ui.Api', Api);
|
|
};
|
|
|
|
var Api = {
|
|
appendTo: appendTo
|
|
};
|
|
|
|
return Api;
|
|
}
|
|
);
|
|
/**
|
|
* Tinymce.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.api.Tinymce',
|
|
[
|
|
'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'
|
|
],
|
|
function (
|
|
Rect, Promise, Delay, Env, EventUtils, Sizzle, Tools, DomQuery, Styles, TreeWalker, Entities, DOMUtils, ScriptLoader, AddOnManager,
|
|
RangeUtils, Node, Schema, SaxParser, DomParser, Writer, HtmlSerializer, DomSerializer, VK, ControlSelection, BookmarkManager, Selection,
|
|
Formatter, UndoManager, EditorCommands, URI, Class, EventDispatcher, Observable, WindowManager,
|
|
NotificationManager, EditorObservable, Shortcuts, Editor, I18n, FocusManager, EditorManager,
|
|
XHR, JSON, JSONRequest, JSONP, LocalStorage, Compat, Color, Api
|
|
) {
|
|
var tinymce = EditorManager;
|
|
|
|
var expose = function (target, id, ref) {
|
|
var i, fragments;
|
|
|
|
fragments = id.split(/[.\/]/);
|
|
for (i = 0; i < fragments.length - 1; ++i) {
|
|
if (target[fragments[i]] === undefined) {
|
|
target[fragments[i]] = {};
|
|
}
|
|
|
|
target = target[fragments[i]];
|
|
}
|
|
|
|
target[fragments[fragments.length - 1]] = ref;
|
|
};
|
|
|
|
expose(tinymce, 'geom.Rect', Rect);
|
|
expose(tinymce, 'util.Promise', Promise);
|
|
expose(tinymce, 'util.Delay', Delay);
|
|
expose(tinymce, 'Env', Env);
|
|
expose(tinymce, 'dom.EventUtils', EventUtils);
|
|
expose(tinymce, 'dom.Sizzle', Sizzle);
|
|
expose(tinymce, 'util.Tools', Tools);
|
|
expose(tinymce, 'dom.DomQuery', DomQuery);
|
|
expose(tinymce, 'html.Styles', Styles);
|
|
expose(tinymce, 'dom.TreeWalker', TreeWalker);
|
|
expose(tinymce, 'html.Entities', Entities);
|
|
expose(tinymce, 'dom.DOMUtils', DOMUtils);
|
|
expose(tinymce, 'dom.ScriptLoader', ScriptLoader);
|
|
expose(tinymce, 'AddOnManager', AddOnManager);
|
|
expose(tinymce, 'dom.RangeUtils', RangeUtils);
|
|
expose(tinymce, 'html.Node', Node);
|
|
expose(tinymce, 'html.Schema', Schema);
|
|
expose(tinymce, 'html.SaxParser', SaxParser);
|
|
expose(tinymce, 'html.DomParser', DomParser);
|
|
expose(tinymce, 'html.Writer', Writer);
|
|
expose(tinymce, 'html.Serializer', HtmlSerializer);
|
|
expose(tinymce, 'dom.Serializer', DomSerializer);
|
|
expose(tinymce, 'util.VK', VK);
|
|
expose(tinymce, 'dom.ControlSelection', ControlSelection);
|
|
expose(tinymce, 'dom.BookmarkManager', BookmarkManager);
|
|
expose(tinymce, 'dom.Selection', Selection);
|
|
expose(tinymce, 'Formatter', Formatter);
|
|
expose(tinymce, 'UndoManager', UndoManager);
|
|
expose(tinymce, 'EditorCommands', EditorCommands);
|
|
expose(tinymce, 'util.URI', URI);
|
|
expose(tinymce, 'util.Class', Class);
|
|
expose(tinymce, 'util.EventDispatcher', EventDispatcher);
|
|
expose(tinymce, 'util.Observable', Observable);
|
|
expose(tinymce, 'WindowManager', WindowManager);
|
|
expose(tinymce, 'NotificationManager', NotificationManager);
|
|
expose(tinymce, 'EditorObservable', EditorObservable);
|
|
expose(tinymce, 'Shortcuts', Shortcuts);
|
|
expose(tinymce, 'Editor', Editor);
|
|
expose(tinymce, 'util.I18n', I18n);
|
|
expose(tinymce, 'FocusManager', FocusManager);
|
|
expose(tinymce, 'EditorManager', EditorManager);
|
|
expose(tinymce, 'util.XHR', XHR);
|
|
expose(tinymce, 'util.JSON', JSON);
|
|
expose(tinymce, 'util.JSONRequest', JSONRequest);
|
|
expose(tinymce, 'util.JSONP', JSONP);
|
|
expose(tinymce, 'util.LocalStorage', LocalStorage);
|
|
expose(tinymce, 'Compat', Compat);
|
|
expose(tinymce, 'util.Color', Color);
|
|
|
|
Api.appendTo(tinymce);
|
|
|
|
Compat.register(tinymce);
|
|
|
|
return tinymce;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Register.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 registers tinymce in common module loaders.
|
|
*
|
|
* @private
|
|
* @class tinymce.Register
|
|
*/
|
|
define(
|
|
'tinymce.core.Register',
|
|
[
|
|
],
|
|
function () {
|
|
/*eslint consistent-this: 0 */
|
|
var context = this || window;
|
|
|
|
var exposeToModuleLoaders = function (tinymce) {
|
|
if (typeof context.define === "function") {
|
|
// Bolt
|
|
if (!context.define.amd) {
|
|
context.define("ephox/tinymce", [], function () {
|
|
return tinymce;
|
|
});
|
|
|
|
context.define("tinymce.core.EditorManager", [], function () {
|
|
return tinymce;
|
|
});
|
|
}
|
|
}
|
|
|
|
if (typeof module === 'object') {
|
|
/* global module */
|
|
module.exports = tinymce;
|
|
}
|
|
};
|
|
|
|
return {
|
|
exposeToModuleLoaders: exposeToModuleLoaders
|
|
};
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Main.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.api.Main',
|
|
[
|
|
'tinymce.core.api.Tinymce',
|
|
'tinymce.core.Register'
|
|
],
|
|
function (tinymce, Register) {
|
|
return function () {
|
|
window.tinymce = tinymce;
|
|
window.tinyMCE = tinymce;
|
|
Register.exposeToModuleLoaders(tinymce);
|
|
return tinymce;
|
|
};
|
|
}
|
|
);
|
|
|
|
dem('tinymce.core.api.Main')();
|
|
})();
|