window.console && console.log('Use tinymce.min.js instead of tinymce.jquery.min.js.'); // 4.6.4 (2017-06-13) (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","ephox.katamari.api.Fun","tinymce.core.dom.RangePoint","tinymce.core.caret.CaretBookmark","tinymce.core.caret.CaretPosition","ephox.sugar.api.dom.Compare","ephox.sugar.api.node.Element","tinymce.core.dom.ScrollIntoView","tinymce.core.dom.TridentSelection","tinymce.core.selection.FragmentReader","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.EditorSettings","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","global!Array","global!Error","ephox.katamari.api.Future","ephox.katamari.api.Futures","ephox.katamari.api.Result","tinymce.core.geom.ClientRect","tinymce.core.caret.CaretCandidate","tinymce.core.text.ExtendingChar","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","ephox.sugar.api.search.Selectors","global!console","ephox.sugar.api.dom.Insert","ephox.sugar.api.dom.Replication","ephox.sugar.api.node.Fragment","ephox.sugar.api.node.Node","tinymce.core.dom.ElementType","tinymce.core.dom.Parents","tinymce.core.selection.SelectionUtils","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!String","ephox.katamari.api.LazyValue","ephox.katamari.async.Bounce","ephox.katamari.async.AsyncValues","ephox.sand.util.Global","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","global!navigator","ephox.sugar.api.node.NodeTypes","ephox.sugar.api.search.Traverse","ephox.sugar.api.properties.Attr","ephox.sugar.api.dom.InsertAll","ephox.sugar.api.dom.Remove","ephox.katamari.api.Options","tinymce.core.undo.Diff","tinymce.core.delete.BlockBoundary","tinymce.core.delete.MergeBlocks","tinymce.core.delete.DeleteUtils","tinymce.core.caret.CaretUtils","tinymce.core.delete.CefDeleteAction","tinymce.core.delete.DeleteElement","tinymce.core.caret.CaretFinder","tinymce.core.keyboard.BoundaryCaret","tinymce.core.keyboard.BoundaryLocation","tinymce.core.keyboard.BoundarySelection","tinymce.core.keyboard.InlineUtils","tinymce.core.data.Binding","tinymce.core.init.InitContentBody","global!Object","global!setTimeout","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.katamari.api.Type","ephox.katamari.api.Struct","ephox.sugar.alien.Recurse","ephox.katamari.api.Obj","ephox.sugar.api.search.PredicateFind","tinymce.core.dom.Empty","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.api.Global","ephox.sand.detect.Version","ephox.katamari.api.Strings","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sugar.api.node.Body","ephox.sugar.impl.ClosestOrAncestor","ephox.sugar.api.search.SelectorExists","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.LineUtils","tinymce.core.DragDropOverrides","tinymce.core.EditorView","tinymce.core.keyboard.CefUtils","tinymce.core.dom.NodePath","global!Number","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts","ephox.katamari.util.BagUtils","ephox.sugar.api.search.SelectorFind","tinymce.core.file.Conversions","global!URL","tinymce.core.keyboard.CefNavigation","tinymce.core.keyboard.MatchKeys","tinymce.core.keyboard.InsertSpace","tinymce.core.dom.Dimensions","tinymce.core.dom.MousePosition","ephox.sugar.api.properties.Css","tinymce.core.caret.LineWalker","ephox.katamari.api.Merger","ephox.sugar.impl.Style"] 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 = { '\"': '&quot;', // Needs to be escaped since the YUI compressor would otherwise break the code "'": '&#39;', '<': '&lt;', '>': '&gt;', '&': '&amp;', '\u0060': '&#96;' }; // Reverse lookup table for raw entities reverseEntities = { '&lt;': '<', '&gt;': '>', '&amp;': '&', '&quot;': '"', '&apos;': "'" }; // 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) { if (baseEntities[chr] !== undefined) { return baseEntities[chr]; } if (entities[chr] !== undefined) { return entities[chr]; } // Convert multi-byte sequences to a single entity. if (chr.length > 1) { return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; } return '&#' + chr.charCodeAt(0) + ';'; }); } 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 noframes noembed title style textarea xmp').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 &aring;. * * @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; }; var trimComments = function (text) { return text.replace(/<!--|-->/g, ''); }; /** * 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, processHtml; 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; } } // Block data or event attributes on elements marked as internal if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) { 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(); processHtml = html + '>'; while ((matches = tokenRegExp.exec(processHtml))) { // 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(trimComments(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>&nbsp;</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); } } } }); } self.addAttributeFilter('href', function (nodes) { var i = nodes.length, node; var appendRel = function (rel) { var parts = rel.split(' ').filter(function (p) { return p.length > 0; }); return parts.concat(['noopener']).sort().join(' '); }; var addNoOpener = function (rel) { var newRel = rel ? Tools.trim(rel) : ''; if (!/\b(noopener)\b/g.test(newRel)) { return appendRel(newRel); } else { return newRel; } }; if (!settings.allow_unsafe_link_target) { while (i--) { node = nodes[i]; if (node.name === 'a' && node.attr('target') === '_blank') { node.attr('rel', addNoOpener(node.attr('rel'))); } } } }); // 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); } }; } ); /** * 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 }; } ); /** * RangePoint.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.RangePoint', [ 'ephox.katamari.api.Arr', 'tinymce.core.geom.ClientRect' ], function (Arr, ClientRect) { var isXYWithinRange = function (clientX, clientY, range) { if (range.collapsed) { return false; } return Arr.foldl(range.getClientRects(), function (state, rect) { return state || ClientRect.containsXY(rect, clientX, clientY); }, false); }; return { isXYWithinRange: isXYWithinRange }; } ); /** * 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', [ 'ephox.katamari.api.Fun', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangePoint', 'tinymce.core.Env', 'tinymce.core.util.Delay', 'tinymce.core.util.Tools', 'tinymce.core.util.VK' ], function (Fun, NodeType, RangePoint, Env, Delay, Tools, VK) { 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; } var isImage = function (elm) { return elm && elm.nodeName === 'IMG'; }; var isEventOnImageOutsideRange = function (evt, range) { return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range); }; var contextMenuSelectImage = function (editor, evt) { var target = evt.target; if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) { evt.preventDefault(); editor.selection.select(target); } }; 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 + ' &times; ' + 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 + ' &times; ' + 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)) { if (e.button !== 2) { 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); editor.on('contextmenu', Fun.curry(contextMenuSelectImage, editor)); // 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 }; } ); /** * 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 = '&#xFEFF;', 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; } ); define( 'ephox.katamari.api.Global', [ ], function () { // Use window object as the global if it's available since CSP will block script evals if (typeof window !== 'undefined') { return window; } else { 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 }; } ); /** * 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>&#xFEFF;</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 = '&#xFEFF;'; } else { sibling = null; } startContainer.innerHTML = '<span>&#xFEFF;</span><span>&#xFEFF;</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; } ); 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.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.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.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 }; } ); 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.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.properties.Attr', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', 'ephox.sugar.api.node.Node', 'global!Error', 'global!console' ], /* * Direct attribute manipulation has been around since IE8, but * was apparently unstable until IE10. */ function (Type, Arr, Obj, Node, Error, console) { var rawSet = function (dom, key, value) { /* * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. * * We fail on those invalid cases, only allowing numbers and booleans. */ if (Type.isString(value) || Type.isBoolean(value) || Type.isNumber(value)) { dom.setAttribute(key, value + ''); } else { console.error('Invalid call to Attr.set. Key ', key, ':: Value ', value, ':: Element ', dom); throw new Error('Attribute value was not simple'); } }; var set = function (element, key, value) { rawSet(element.dom(), key, value); }; var setAll = function (element, attrs) { var dom = element.dom(); Obj.each(attrs, function (v, k) { rawSet(dom, k, v); }); }; var get = function (element, key) { var v = element.dom().getAttribute(key); // undefined is the more appropriate value for JS, and this matches JQuery return v === null ? undefined : v; }; var has = function (element, key) { var dom = element.dom(); // return false for non-element nodes, no point in throwing an error return dom && dom.hasAttribute ? dom.hasAttribute(key) : false; }; var remove = function (element, key) { element.dom().removeAttribute(key); }; var hasNone = function (element) { var attrs = element.dom().attributes; return attrs === undefined || attrs === null || attrs.length === 0; }; var clone = function (element) { return Arr.foldl(element.dom().attributes, function (acc, attr) { acc[attr.name] = attr.value; return acc; }, {}); }; var transferOne = function (source, destination, attr) { // NOTE: We don't want to clobber any existing attributes if (has(source, attr) && !has(destination, attr)) set(destination, attr, get(source, attr)); }; // Transfer attributes(attrs) from source to destination, unless they are already present var transfer = function (source, destination, attrs) { if (!Node.isElement(source) || !Node.isElement(destination)) return; Arr.each(attrs, function (attr) { transferOne(source, destination, attr); }); }; return { clone: clone, set: set, setAll: setAll, get: get, has: has, remove: remove, hasNone: hasNone, transfer: transfer }; } ); 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 }; } ); define( 'ephox.sugar.api.dom.Replication', [ 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.InsertAll', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.search.Traverse' ], function (Attr, Element, Insert, InsertAll, Remove, Traverse) { var clone = function (original, deep) { return Element.fromDom(original.dom().cloneNode(deep)); }; /** Shallow clone - just the tag, no children */ var shallow = function (original) { return clone(original, false); }; /** Deep clone - everything copied including children */ var deep = function (original) { return clone(original, true); }; /** Shallow clone, with a new tag */ var shallowAs = function (original, tag) { var nu = Element.fromTag(tag); var attributes = Attr.clone(original); Attr.setAll(nu, attributes); return nu; }; /** Deep clone, with a new tag */ var copy = function (original, tag) { var nu = shallowAs(original, tag); // NOTE // previously this used serialisation: // nu.dom().innerHTML = original.dom().innerHTML; // // Clone should be equivalent (and faster), but if TD <-> TH toggle breaks, put it back. var cloneChildren = Traverse.children(deep(original)); InsertAll.append(nu, cloneChildren); return nu; }; /** Change the tag name, but keep all children */ var mutate = function (original, tag) { var nu = shallowAs(original, tag); Insert.before(original, nu); var children = Traverse.children(original); InsertAll.append(nu, children); Remove.remove(original); return nu; }; return { shallow: shallow, shallowAs: shallowAs, deep: deep, copy: copy, mutate: mutate }; } ); define( 'ephox.sugar.api.node.Fragment', [ 'ephox.katamari.api.Arr', 'ephox.sugar.api.node.Element', 'global!document' ], function (Arr, Element, document) { var fromElements = function (elements, scope) { var doc = scope || document; var fragment = doc.createDocumentFragment(); Arr.each(elements, function (element) { fragment.appendChild(element.dom()); }); return Element.fromDom(fragment); }; return { fromElements: fromElements }; } ); /** * ElementType.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.ElementType', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Node' ], function (Arr, Fun, Node) { var blocks = [ 'article', 'aside', 'details', 'div', 'dt', 'figcaption', 'footer', 'form', 'fieldset', 'header', 'hgroup', 'html', 'main', 'nav', 'section', 'summary', 'body', 'p', 'dl', 'multicol', 'dd', 'figure', 'address', 'center', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'listing', 'xmp', 'pre', 'plaintext', 'menu', 'dir', 'ul', 'ol', 'li', 'hr', 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'caption' ]; var voids = [ 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param', 'embed', 'source', 'wbr', 'track' ]; var tableCells = ['td', 'th']; var textBlocks = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', 'section', 'hgroup', 'aside', 'nav', 'figure' ]; var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; var lazyLookup = function (items) { var lookup; return function (node) { lookup = lookup ? lookup : Arr.mapToObject(items, Fun.constant(true)); return lookup.hasOwnProperty(Node.name(node)); }; }; var isHeading = lazyLookup(headings); var isBlock = lazyLookup(blocks); var isInline = function (node) { return Node.isElement(node) && !isBlock(node); }; return { isBlock: isBlock, isInline: isInline, isHeading: isHeading, isTextBlock: lazyLookup(textBlocks), isVoid: lazyLookup(voids), isTableCell: lazyLookup(tableCells) }; } ); /** * Parents.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.Parents', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.search.Traverse' ], function (Fun, Compare, Traverse) { var dropLast = function (xs) { return xs.slice(0, -1); }; var parentsUntil = function (startNode, rootElm, predicate) { if (Compare.contains(rootElm, startNode)) { return dropLast(Traverse.parents(startNode, function (elm) { return predicate(elm) || Compare.eq(elm, rootElm); })); } else { return []; } }; var parents = function (startNode, rootElm) { return parentsUntil(startNode, rootElm, Fun.constant(false)); }; var parentsAndSelf = function (startNode, rootElm) { return [startNode].concat(parents(startNode, rootElm)); }; return { parentsUntil: parentsUntil, parents: parents, parentsAndSelf: parentsAndSelf }; } ); 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 }; } ); /** * SelectionUtils.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.selection.SelectionUtils', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.api.search.Traverse', 'tinymce.core.dom.NodeType' ], function (Arr, Fun, Option, Options, Compare, Element, Node, Traverse, NodeType) { var getStartNode = function (rng) { var sc = rng.startContainer, so = rng.startOffset; if (NodeType.isText(sc)) { return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none(); } else { return Option.from(sc.childNodes[so]).map(Element.fromDom); } }; var getEndNode = function (rng) { var ec = rng.endContainer, eo = rng.endOffset; if (NodeType.isText(ec)) { return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none(); } else { return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom); } }; var getFirstChildren = function (node) { return Traverse.firstChild(node).fold( Fun.constant([node]), function (child) { return [node].concat(getFirstChildren(child)); } ); }; var getLastChildren = function (node) { return Traverse.lastChild(node).fold( Fun.constant([node]), function (child) { if (Node.name(child) === 'br') { return Traverse.prevSibling(child).map(function (sibling) { return [node].concat(getLastChildren(sibling)); }).getOr([]); } else { return [node].concat(getLastChildren(child)); } } ); }; var hasAllContentsSelected = function (elm, rng) { return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) { var start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode)); var end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode)); return start.isSome() && end.isSome(); }).getOr(false); }; return { hasAllContentsSelected: hasAllContentsSelected }; } ); /** * FragmentReader.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.selection.FragmentReader', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Replication', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Fragment', 'ephox.sugar.api.node.Node', 'tinymce.core.dom.ElementType', 'tinymce.core.dom.Parents', 'tinymce.core.selection.SelectionUtils' ], function (Arr, Fun, Insert, Replication, Element, Fragment, Node, ElementType, Parents, SelectionUtils) { var findParentListContainer = function (parents) { return Arr.find(parents, function (elm) { return Node.name(elm) === 'ul' || Node.name(elm) === 'ol'; }); }; var getFullySelectedListWrappers = function (parents, rng) { return Arr.find(parents, function (elm) { return Node.name(elm) === 'li' && SelectionUtils.hasAllContentsSelected(elm, rng); }).fold( Fun.constant([]), function (li) { return findParentListContainer(parents).map(function (listCont) { return [ Element.fromTag('li'), Element.fromTag(Node.name(listCont)) ]; }).getOr([]); } ); }; var wrap = function (innerElm, elms) { var wrapped = Arr.foldl(elms, function (acc, elm) { Insert.append(elm, acc); return elm; }, innerElm); return elms.length > 0 ? Fragment.fromElements([wrapped]) : wrapped; }; var getWrapElements = function (rootNode, rng) { var parents = Parents.parentsAndSelf(Element.fromDom(rng.commonAncestorContainer), Element.fromDom(rootNode)); var wrapElements = Arr.filter(parents, function (elm) { return ElementType.isInline(elm) || ElementType.isHeading(elm); }); var fullWrappers = getFullySelectedListWrappers(parents, rng); return Arr.map(wrapElements.concat(fullWrappers), Replication.shallow); }; var getFragmentFromRange = function (rootNode, rng) { return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); }; var read = function (rootNode, rng) { return rng.collapsed ? Fragment.fromElements([]) : getFragmentFromRange(rootNode, rng); }; return { read: read }; } ); /** * 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', [ 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', '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.selection.FragmentReader', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], function ( Compare, Element, CaretPosition, BookmarkManager, ControlSelection, NodeType, RangeUtils, ScrollIntoView, TreeWalker, TridentSelection, Env, FragmentReader, Zwsp, Tools ) { var each = Tools.each, trim = Tools.trim; var isIE = Env.ie; var isAttachedToDom = function (node) { return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node)); }; var isValidRange = function (rng) { if (!rng) { return false; } else if (rng.select) { // Native IE range still produced by placeCaretAt return true; } else { return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer); } }; /** * 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 = args.contextual ? FragmentReader.read(self.editor.getBody(), rng).dom() : 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 (!isValidRange(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, forward: forward }); 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, forward: forward }); } 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 && 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 > 0 && 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, clear_child_styles: true }, hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, fontname: { inline: 'span', styles: { fontFamily: '%value' }, clear_child_styles: true }, fontsize: { inline: 'span', styles: { fontSize: '%value' }, clear_child_styles: true }, 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); } var clearChildStyles = function (format, node) { if (format.clear_child_styles) { each(dom.select('*', node), function (node) { each(format.styles, function (value, name) { dom.setStyle(node, name, ''); }); }); } }; 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); }); clearChildStyles(format, node); }); // 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, walker, node, nodes; if (rng.startContainer == rng.endContainer) { if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { return; } } // Convert text node into index if possible if (container.nodeType == 3 && offset >= container.nodeValue.length) { // Get the parent container location and walk from there offset = nodeIndex(container); container = container.parentNode; } // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1) { nodes = container.childNodes; if (offset < nodes.length) { container = nodes[offset]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); } else { container = nodes[nodes.length - 1]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); walker.next(true); } for (node = walker.current(); node; node = walker.next()) { if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { rng.setStart(node, 0); selection.setRng(rng); return; } } } } }; } ); /** * Diff.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. * * @class tinymce.undo.Diff * @private */ define( 'tinymce.core.undo.Diff', [ ], function () { var KEEP = 0, INSERT = 1, DELETE = 2; var diff = function (left, right) { var size = left.length + right.length + 2; var vDown = new Array(size); var vUp = new Array(size); var snake = function (start, end, diag) { return { start: start, end: end, diag: diag }; }; var buildScript = function (start1, end1, start2, end2, script) { var middle = getMiddleSnake(start1, end1, start2, end2); if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || middle.end === start1 && middle.diag === start1 - start2) { var i = start1; var j = start2; while (i < end1 || j < end2) { if (i < end1 && j < end2 && left[i] === right[j]) { script.push([KEEP, left[i]]); ++i; ++j; } else { if (end1 - start1 > end2 - start2) { script.push([DELETE, left[i]]); ++i; } else { script.push([INSERT, right[j]]); ++j; } } } } else { buildScript(start1, middle.start, start2, middle.start - middle.diag, script); for (var i2 = middle.start; i2 < middle.end; ++i2) { script.push([KEEP, left[i2]]); } buildScript(middle.end, end1, middle.end - middle.diag, end2, script); } }; var buildSnake = function (start, diag, end1, end2) { var end = start; while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { ++end; } return snake(start, end, diag); }; var getMiddleSnake = function (start1, end1, start2, end2) { // Myers Algorithm // Initialisations var m = end1 - start1; var n = end2 - start2; if (m === 0 || n === 0) { return null; } var delta = m - n; var sum = n + m; var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; vDown[1 + offset] = start1; vUp[1 + offset] = end1 + 1; for (var d = 0; d <= offset; ++d) { // Down for (var k = -d; k <= d; k += 2) { // First step var i = k + offset; if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { vDown[i] = vDown[i + 1]; } else { vDown[i] = vDown[i - 1] + 1; } var x = vDown[i]; var y = x - start1 + start2 - k; while (x < end1 && y < end2 && left[x] === right[y]) { vDown[i] = ++x; ++y; } // Second step if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { if (vUp[i - delta] <= vDown[i]) { return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); } } } // Up for (k = delta - d; k <= delta + d; k += 2) { // First step i = k + offset - delta; if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { vUp[i] = vUp[i + 1] - 1; } else { vUp[i] = vUp[i - 1]; } x = vUp[i] - 1; y = x - start1 + start2 - k; while (x >= start1 && y >= start2 && left[x] === right[y]) { vUp[i] = x--; y--; } // Second step if (delta % 2 === 0 && -d <= k && k <= d) { if (vUp[i] <= vDown[i + delta]) { return buildSnake(vUp[i], k + start1 - start2, end1, end2); } } } } }; var script = []; buildScript(0, left.length, 0, right.length, script); return script; }; return { KEEP: KEEP, DELETE: DELETE, INSERT: INSERT, diff: diff }; } ); /** * Fragments.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** * This module reads and applies html fragments from/to dom nodes. * * @class tinymce.undo.Fragments * @private */ define( 'tinymce.core.undo.Fragments', [ "tinymce.core.util.Arr", "tinymce.core.html.Entities", "tinymce.core.undo.Diff" ], function (Arr, Entities, Diff) { var getOuterHtml = function (elm) { if (elm.nodeType === 1) { return elm.outerHTML; } else if (elm.nodeType === 3) { return Entities.encodeRaw(elm.data, false); } else if (elm.nodeType === 8) { return '<!--' + 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.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) { return item.length > 0; }); }; 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.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 }; } ); /** * 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 list-item'), 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); if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) { return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition; } } else { caretPosition = startCaretPosition; } container = caretPosition.container(); offset = caretPosition.offset(); if (isText(container)) { if (isBackwards(direction) && offset > 0) { return CaretPosition(container, --offset); } if (isForwards(direction) && offset < container.length) { return CaretPosition(container, ++offset); } node = container; } else { if (isBackwards(direction) && offset > 0) { nextNode = nodeAtIndex(container, offset - 1); if (isCaretCandidate(nextNode)) { if (!isAtomic(nextNode)) { innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, innerNode.data.length); } return CaretPosition.after(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, nextNode.data.length); } return CaretPosition.before(nextNode); } } if (isForwards(direction) && offset < container.childNodes.length) { nextNode = nodeAtIndex(container, offset); if (isCaretCandidate(nextNode)) { if (isBrBeforeBlock(nextNode, rootNode)) { return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode); } if (!isAtomic(nextNode)) { innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); if (innerNode) { if (isText(innerNode)) { return CaretPosition(innerNode, 0); } return CaretPosition.before(innerNode); } } if (isText(nextNode)) { return CaretPosition(nextNode, 0); } return CaretPosition.after(nextNode); } } node = caretPosition.getNode(); } if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) { node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true); if (isEditableCaretCandidate(node)) { return getCaretCandidatePosition(direction, node); } } nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse)); if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { if (isForwards(direction)) { caretPosition = CaretPosition.after(rootContentEditableFalseElm); } else { caretPosition = CaretPosition.before(rootContentEditableFalseElm); } return caretPosition; } if (nextNode) { return getCaretCandidatePosition(direction, nextNode); } return null; } return function (rootNode) { return { /** * Returns the next logical caret position from the specificed input * caretPoisiton or null if there isn't any more positions left for example * at the end specified root element. * * @method next * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ next: function (caretPosition) { return findCaretPosition(1, caretPosition, rootNode); }, /** * Returns the previous logical caret position from the specificed input * caretPoisiton or null if there isn't any more positions left for example * at the end specified root element. * * @method prev * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. */ prev: function (caretPosition) { return findCaretPosition(-1, caretPosition, rootNode); } }; }; } ); /** * CaretFinder.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.caret.CaretFinder', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.dom.NodeType' ], function (Fun, Option, CaretCandidate, CaretPosition, CaretUtils, CaretWalker, NodeType) { var walkToPositionIn = function (forward, rootNode, startNode) { var position = forward ? CaretPosition.before(startNode) : CaretPosition.after(startNode); return fromPosition(forward, rootNode, position); }; var afterElement = function (node) { return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node); }; var isBeforeOrStart = function (position) { if (CaretPosition.isTextPosition(position)) { return position.offset() === 0; } else { return CaretCandidate.isCaretCandidate(position.getNode()); } }; var isAfterOrEnd = function (position) { if (CaretPosition.isTextPosition(position)) { return position.offset() === position.container().data.length; } else { return CaretCandidate.isCaretCandidate(position.getNode(true)); } }; var isBeforeAfterSameElement = function (from, to) { return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); }; var isAtBr = function (position) { return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode()); }; var shouldSkipPosition = function (forward, from, to) { if (forward) { return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to); } else { return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to); } }; // Finds: <p>a|<b>b</b></p> -> <p>a<b>|b</b></p> var fromPosition = function (forward, rootNode, position) { var walker = new CaretWalker(rootNode); return Option.from(forward ? walker.next(position) : walker.prev(position)); }; // Finds: <p>a|<b>b</b></p> -> <p>a<b>b|</b></p> var navigate = function (forward, rootNode, from) { return fromPosition(forward, rootNode, from).bind(function (to) { if (CaretUtils.isInSameBlock(from, to, rootNode) && shouldSkipPosition(forward, from, to)) { return fromPosition(forward, rootNode, to); } else { return Option.some(to); } }); }; var positionIn = function (forward, element) { var startNode = forward ? element.firstChild : element.lastChild; if (NodeType.isText(startNode)) { return Option.some(new CaretPosition(startNode, forward ? 0 : startNode.data.length)); } else if (startNode) { if (CaretCandidate.isCaretCandidate(startNode)) { return Option.some(forward ? CaretPosition.before(startNode) : afterElement(startNode)); } else { return walkToPositionIn(forward, element, startNode); } } else { return Option.none(); } }; return { fromPosition: fromPosition, navigate: navigate, 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 }; } ); /** * MergeBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.MergeBlocks', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Option', 'ephox.sugar.api.dom.Insert', 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Arr, Option, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, Empty, NodeType) { var mergeBlocksAndReposition = function (forward, fromBlock, toBlock, toPosition) { var children = Traverse.children(fromBlock); if (NodeType.isBr(toPosition.getNode())) { Remove.remove(Element.fromDom(toPosition.getNode())); toPosition = CaretFinder.positionIn(false, toBlock.dom()).getOr(toPosition); } if (Empty.isEmpty(fromBlock) === false) { Arr.each(children, function (node) { Insert.append(toBlock, node); }); } if (Empty.isEmpty(fromBlock)) { Remove.remove(fromBlock); } return children.length > 0 ? Option.from(toPosition) : Option.none(); }; var mergeBlocks = function (forward, block1, block2) { if (forward) { if (Empty.isEmpty(block1)) { Remove.remove(block1); return CaretFinder.positionIn(true, block2.dom()); } else { return CaretFinder.positionIn(false, block1.dom()).bind(function (toPosition) { return mergeBlocksAndReposition(forward, block2, block1, toPosition); }); } } else { if (Empty.isEmpty(block2)) { Remove.remove(block2); return CaretFinder.positionIn(true, block1.dom()); } else { return CaretFinder.positionIn(false, block2.dom()).bind(function (toPosition) { return mergeBlocksAndReposition(forward, block1, block2, toPosition); }); } } }; return { mergeBlocks: mergeBlocks }; } ); /** * BlockBoundaryDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockBoundaryDelete', [ 'tinymce.core.delete.BlockBoundary', 'tinymce.core.delete.MergeBlocks' ], function (BlockBoundary, MergeBlocks) { var backspaceDelete = function (editor, forward) { var position; position = BlockBoundary.read(editor.getBody(), forward, editor.selection.getRng()).bind(function (blockBoundary) { return MergeBlocks.mergeBlocks(forward, blockBoundary.from().block(), blockBoundary.to().block()); }); position.each(function (pos) { editor.selection.setRng(pos.toRange()); }); return position.isSome(); }; return { backspaceDelete: backspaceDelete }; } ); /** * BlockRangeDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.BlockRangeDelete', [ 'ephox.katamari.api.Options', 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.delete.MergeBlocks' ], function (Options, Compare, Element, DeleteUtils, MergeBlocks) { var deleteRange = function (rootNode, selection) { var rng = selection.getRng(); return Options.liftN([ DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.startContainer)), DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.endContainer)) ], function (block1, block2) { if (Compare.eq(block1, block2) === false) { rng.deleteContents(); MergeBlocks.mergeBlocks(true, block1, block2).each(function (pos) { selection.setRng(pos.toRange()); }); return true; } else { return false; } }).getOr(false); }; var backspaceDelete = function (editor, forward) { var rootNode = Element.fromDom(editor.getBody()); if (editor.selection.isCollapsed() === false) { return deleteRange(rootNode, editor.selection); } else { return false; } }; return { backspaceDelete: backspaceDelete }; } ); define( 'ephox.katamari.api.Adt', [ 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Type', 'global!Array', 'global!Error', 'global!console' ], function (Arr, Obj, Type, Array, Error, console) { /* * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) * For syntax and use, look at the test code. */ var generate = function (cases) { // validation if (!Type.isArray(cases)) { throw new Error('cases must be an array'); } if (cases.length === 0) { throw new Error('there must be at least one case'); } var constructors = [ ]; // adt is mutated to add the individual cases var adt = {}; Arr.each(cases, function (acase, count) { var keys = Obj.keys(acase); // validation if (keys.length !== 1) { throw new Error('one and only one name per case'); } var key = keys[0]; var value = acase[key]; // validation if (adt[key] !== undefined) { throw new Error('duplicate key detected:' + key); } else if (key === 'cata') { throw new Error('cannot have a case named cata (sorry)'); } else if (!Type.isArray(value)) { // this implicitly checks if acase is an object throw new Error('case arguments must be an array'); } constructors.push(key); // // constructor for key // adt[key] = function () { var argLength = arguments.length; // validation if (argLength !== value.length) { throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); } // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome var args = new Array(argLength); for (var i = 0; i < args.length; i++) args[i] = arguments[i]; var match = function (branches) { var branchKeys = Obj.keys(branches); if (constructors.length !== branchKeys.length) { throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); } var allReqd = Arr.forall(constructors, function (reqKey) { return Arr.contains(branchKeys, reqKey); }); if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); return branches[key].apply(null, args); }; // // the fold function for key // return { fold: function (/* arguments */) { // runtime validation if (arguments.length !== cases.length) { throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + arguments.length); } var target = arguments[count]; return target.apply(null, args); }, match: match, // NOTE: Only for debugging. log: function (label) { console.log(label, { constructors: constructors, constructor: key, params: args }); } }; }; }); return adt; }; return { generate: generate }; } ); /** * CefDeleteAction.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.delete.CefDeleteAction', [ 'ephox.katamari.api.Adt', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.delete.DeleteUtils', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType' ], function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) { var DeleteAction = Adt.generate([ { remove: [ 'element' ] }, { moveToElement: [ 'element' ] }, { moveToPosition: [ 'position' ] } ]); var isAtContentEditableBlockCaret = function (forward, from) { var elm = from.getNode(forward === false); var caretLocation = forward ? 'after' : 'before'; return NodeType.isElement(elm) && elm.getAttribute('data-mce-caret') === caretLocation; }; var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { var toCefElm = to.getNode(forward === false); return DeleteUtils.getParentTextBlock(Element.fromDom(rootNode), Element.fromDom(from.getNode())).map(function (blockElm) { return Empty.isEmpty(blockElm) ? DeleteAction.remove(blockElm.dom()) : DeleteAction.moveToElement(toCefElm); }).orThunk(function () { return Option.some(DeleteAction.moveToElement(toCefElm)); }); }; var findCefPosition = function (rootNode, forward, from) { return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) { if (forward && NodeType.isContentEditableFalse(to.getNode())) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) { return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); } else if (forward && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else if (forward === false && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.moveToPosition(to)); } else { return Option.none(); } }); }; var getContentEditableBlockAction = function (forward, elm) { if (forward && NodeType.isContentEditableFalse(elm.nextSibling)) { return Option.some(DeleteAction.moveToElement(elm.nextSibling)); } else if (forward === false && NodeType.isContentEditableFalse(elm.previousSibling)) { return Option.some(DeleteAction.moveToElement(elm.previousSibling)); } else { return Option.none(); } }; var getContentEditableAction = function (rootNode, forward, from) { if (isAtContentEditableBlockCaret(forward, from)) { return getContentEditableBlockAction(forward, from.getNode(forward === false)) .fold( function () { return findCefPosition(rootNode, forward, from); }, Option.some ); } else { return findCefPosition(rootNode, forward, from); } }; var read = function (rootNode, forward, rng) { var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); var from = CaretPosition.fromRangeStart(normalizedRange); if (forward === false && CaretUtils.isAfterContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode(true))); } else if (forward && CaretUtils.isBeforeContentEditableFalse(from)) { return Option.some(DeleteAction.remove(from.getNode())); } else { return getContentEditableAction(rootNode, forward, from); } }; return { read: read }; } ); /** * Bidi.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.text.Bidi', [ ], function () { var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; var hasStrongRtl = function (text) { return strongRtl.test(text); }; return { hasStrongRtl: hasStrongRtl }; } ); /** * InlineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.keyboard.InlineUtils', [ 'ephox.katamari.api.Arr', '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.dom.NodeType', 'tinymce.core.text.Bidi' ], function (Arr, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, 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 findInlineParents = function (rootNode, pos) { return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); }; var findInline = function (rootNode, pos) { var parents = findInlineParents(rootNode, pos); return Option.from(parents[0]); }; var findRootInline = function (rootNode, pos) { var parents = findInlineParents(rootNode, pos); return Option.from(parents[parents.length - 1]); }; 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 ? findRootInline(rootNode, pos).isSome() : false; }; var isAtInlineEndPoint = function (rootNode, pos) { return findRootInline(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) { if (CaretContainer.isCaretContainerInline(container)) { if (NodeType.isText(container.nextSibling)) { return new CaretPosition(container.nextSibling, 0); } else { return CaretPosition.after(container); } } else { return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos; } } else { if (CaretContainer.isCaretContainerInline(container)) { if (NodeType.isText(container.previousSibling)) { return new CaretPosition(container.previousSibling, container.previousSibling.data.length); } else { return CaretPosition.before(container); } } 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, findRootInline: findRootInline, 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.katamari.api.Options', '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', 'ephox.sugar.api.search.Traverse', 'tinymce.core.caret.CaretCandidate', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.Empty', 'tinymce.core.dom.NodeType', 'tinymce.core.keyboard.InlineUtils' ], function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretPosition, Empty, NodeType, InlineUtils) { var needsReposition = function (pos, elm) { var container = pos.container(); var offset = pos.offset(); return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); }; var reposition = function (elm, pos) { return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; }; var beforeOrStartOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); }; var afterOrEndOf = function (node) { return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); }; var getPreviousSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { return Option.some(afterOrEndOf(elm.previousSibling)); } else { return elm.previousSibling ? InlineUtils.findCaretPositionIn(elm.previousSibling, false) : Option.none(); } }; var getNextSiblingCaretPosition = function (elm) { if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { return Option.some(beforeOrStartOf(elm.nextSibling)); } else { return elm.nextSibling ? InlineUtils.findCaretPositionIn(elm.nextSibling, true) : Option.none(); } }; var findCaretPositionBackwardsFromElm = function (rootElement, elm) { var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); return InlineUtils.findCaretPosition(rootElement, false, startPosition).fold( function () { return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)); }, Option.some ); }; var findCaretPositionForwardsFromElm = function (rootElement, elm) { return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)).fold( function () { return InlineUtils.findCaretPosition(rootElement, false, CaretPosition.before(elm)); }, Option.some ); }; var findCaretPositionBackwards = function (rootElement, elm) { return getPreviousSiblingCaretPosition(elm).orThunk(function () { return getNextSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionBackwardsFromElm(rootElement, elm); }); }; var findCaretPositionForward = function (rootElement, elm) { return getNextSiblingCaretPosition(elm).orThunk(function () { return getPreviousSiblingCaretPosition(elm); }).orThunk(function () { return findCaretPositionForwardsFromElm(rootElement, elm); }); }; var findCaretPosition = function (forward, rootElement, elm) { return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); }; var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); }; var setSelection = function (editor, forward, pos) { pos.fold( function () { editor.focus(); }, function (pos) { editor.selection.setRng(pos.toRange(), forward); } ); }; var eqRawNode = function (rawNode) { return function (elm) { return elm.dom() === rawNode; }; }; var isBlock = function (editor, elm) { return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); }; var paddEmptyBlock = function (elm) { if (Empty.isEmpty(elm)) { var br = Element.fromHtml('<br data-mce-bogus="1">'); Remove.empty(elm); Insert.append(elm, br); return Option.some(CaretPosition.before(br.dom())); } else { return Option.none(); } }; // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes var deleteNormalized = function (elm, afterDeletePosOpt) { return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) { var offset, prevNode = prev.dom(), nextNode = next.dom(); if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) { offset = prevNode.data.length; prevNode.appendData(nextNode.data); Remove.remove(next); Remove.remove(elm); if (afterDeletePos.container() === nextNode) { return new CaretPosition(prevNode, offset); } else { return afterDeletePos; } } else { Remove.remove(elm); return afterDeletePos; } }).orThunk(function () { Remove.remove(elm); return afterDeletePosOpt; }); }; 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())); var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos); parentBlock.bind(paddEmptyBlock).fold( function () { setSelection(editor, forward, normalizedAfterDeletePos); }, function (paddPos) { setSelection(editor, forward, 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 isPosCaretContainer = function (pos, caret) { var caretNode = caret.get(); return caretNode && pos.container() === caretNode && CaretContainer.isCaretContainerInline(caretNode); }; 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) { if (!isPosCaretContainer(pos, caret)) { CaretContainerRemove.remove(caret.get()); var text = insertInlinePos(pos, true); caret.set(text); return new CaretPosition(text, 1); } else { return new CaretPosition(caret.get(), 1); } }); }, function (element) { // End return InlineUtils.findCaretPositionIn(element, false).map(function (pos) { if (!isPosCaretContainer(pos, caret)) { CaretContainerRemove.remove(caret.get()); var text = insertInlinePos(pos, false); caret.set(text); return new CaretPosition(text, text.length - 1); } else { return new CaretPosition(caret.get(), caret.get().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 rescope = function (rootNode, node) { var parentBlock = CaretUtils.getParentBlock(node, rootNode); return parentBlock ? parentBlock : rootNode; }; var before = function (rootNode, pos) { var nPos = InlineUtils.normalizeForwards(pos); var scope = rescope(rootNode, nPos.container()); return InlineUtils.findRootInline(scope, nPos).fold( function () { return InlineUtils.findCaretPosition(scope, true, nPos) .bind(Fun.curry(InlineUtils.findRootInline, scope)) .map(function (inline) { return Location.before(inline); }); }, Option.none ); }; var start = function (rootNode, pos) { var nPos = InlineUtils.normalizeBackwards(pos); return InlineUtils.findRootInline(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.findRootInline(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); var scope = rescope(rootNode, nPos.container()); return InlineUtils.findRootInline(scope, nPos).fold( function () { return InlineUtils.findCaretPosition(scope, false, nPos) .bind(Fun.curry(InlineUtils.findRootInline, scope)) .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 getName = function (location) { return location.fold( Fun.constant('before'), // Before Fun.constant('start'), // Start Fun.constant('end'), // End Fun.constant('after') // 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 isEq = function (location1, location2) { return getName(location1) === getName(location2) && getElement(location1) === getElement(location2); }; var betweenInlines = function (forward, rootNode, from, to, location) { return Options.liftN([ InlineUtils.findRootInline(rootNode, from), InlineUtils.findRootInline(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 skipNoMovement = function (fromLocation, toLocation) { return fromLocation.fold( Fun.constant(true), function (fromLocation) { return !isEq(fromLocation, toLocation); } ); }; var findLocationTraverse = function (forward, rootNode, fromLocation, 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 fromLocation.map(outside); }, function (to) { return readLocation(rootNode, to) .map(Fun.curry(betweenInlines, forward, rootNode, from, to)) .filter(Fun.curry(skipNoMovement, fromLocation)); } ); return location.filter(isValidLocation); }; var findLocationSimple = function (forward, location) { if (forward) { return location.fold( Fun.compose(Option.some, Location.start), // Before -> Start Option.none, Fun.compose(Option.some, Location.after), // End -> After Option.none ); } else { return location.fold( Option.none, Fun.compose(Option.some, Location.before), // Before <- Start Option.none, Fun.compose(Option.some, Location.end) // End <- After ); } }; var findLocation = function (forward, rootNode, pos) { var from = InlineUtils.normalizePosition(forward, pos); var fromLocation = readLocation(rootNode, from); return readLocation(rootNode, from).bind(Fun.curry(findLocationSimple, forward)).orThunk(function () { return findLocationTraverse(forward, rootNode, fromLocation, pos); }); }; 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.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', '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, CaretFinder, CaretPosition, CaretUtils, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection, InlineUtils ) { var isFeatureEnabled = function (editor) { return editor.settings.inline_boundaries !== false; }; var rangeFromPositions = function (from, to) { var range = document.createRange(); range.setStart(from.container(), from.offset()); range.setEnd(to.container(), to.offset()); return range; }; // Checks for delete at <code>|a</code> when there is only one item left except the zwsp caret container nodes var hasOnlyTwoOrLessPositionsLeft = function (elm) { return Options.liftN([ InlineUtils.findCaretPositionIn(elm, true), InlineUtils.findCaretPositionIn(elm, false) ], function (firstPos, lastPos) { var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos); var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos); return InlineUtils.findCaretPosition(elm, true, normalizedFirstPos).map(function (pos) { return pos.isEqual(normalizedLastPos); }).getOr(true); }).getOr(true); }; 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 rescope = function (rootNode, node) { var parentBlock = CaretUtils.getParentBlock(node, rootNode); return parentBlock ? parentBlock : rootNode; }; var backspaceDeleteCollapsed = function (editor, caret, forward, from) { var rootNode = rescope(editor.getBody(), from.container()); 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 = CaretFinder.navigate(forward, rootNode, from); var toLocation = toPosition.bind(function (pos) { return BoundaryLocation.readLocation(rootNode, pos); }); if (fromLocation.isSome() && toLocation.isSome()) { return InlineUtils.findRootInline(rootNode, from).map(function (elm) { if (hasOnlyTwoOrLessPositionsLeft(elm)) { DeleteElement.deleteElement(editor, forward, Element.fromDom(elm)); return true; } else { return false; } }).getOr(false); } else { return toLocation.bind(function (_) { return 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() && isFeatureEnabled(editor)) { 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(/^&nbsp;/, ' '); } else if (!hasSiblingText('previousSibling')) { html = html.replace(/^ /, '&nbsp;'); } if (offset < container.length) { html = html.replace(/&nbsp;(<br>|)$/, ' '); } else if (!hasSiblingText('nextSibling')) { html = html.replace(/(&nbsp;| )(<br>|)$/, '&nbsp;'); } } return html; } // Removes &nbsp; from a [b] c -> a &nbsp;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">&#xFEFF;&#x200B;</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 (editor.removed) { return; } 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; if (editor.quirks.isHidden() || editor.removed) { 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; if (editor.quirks.isHidden() || editor.removed) { 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', {}, '&nbsp;'); 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, delegate; function isListening(editor) { return !editor.hidden && !editor.readonly; } if (!editor.delegates) { editor.delegates = {}; } if (editor.delegates[eventName] || editor.removed) { return; } eventRootElm = getEventTarget(editor, eventName); 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 logic for handling keyboard shortcuts. * * @class tinymce.Shortcuts * @example * editor.shortcuts.add('ctrl+a', "description of the shortcut", function() {}); * editor.shortcuts.add('meta+a', "description of the shortcut", function() {}); // "meta" maps to Command on Mac and Ctrl on PC * editor.shortcuts.add('ctrl+alt+a', "description of the shortcut", function() {}); * editor.shortcuts.add('access+a', "description of the shortcut", 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; }; }; } ); /** * DefaultSettings.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.EditorSettings', [ 'tinymce.core.util.Tools' ], function (Tools) { var getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) { settings = Tools.extend( // Default settings { 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', entity_encoding: 'named', url_converter: editor.convertURL, url_converter_scope: editor, ie7_compat: true }, // tinymce.overrideDefaults settings defaultOverrideSettings, // User settings settings, // Forced settings { validate: true, content_editable: settings.inline } ); // Merge external_plugins if (defaultOverrideSettings && defaultOverrideSettings.external_plugins && settings.external_plugins) { settings.external_plugins = Tools.extend({}, defaultOverrideSettings.external_plugins, settings.external_plugins); } return settings; }; return { getEditorSettings: getEditorSettings }; } ); 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 || xhr.status >= 300) { 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++); }; var imageToBlobInfo = function (blobCache, 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); }); } }; var getAllImages = function (elm) { return elm ? elm.getElementsByTagName('img') : []; }; return function (uploadStatus, blobCache) { var cachedPromises = {}; function findAll(elm, predicate) { var images, promises; if (!predicate) { predicate = Fun.constant(true); } images = Arr.filter(getAllImages(elm), 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(blobCache, 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 }; } ); /** * 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 }; } ); /** * 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 }; } ); /** * 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) }; } ); /** * CefUtils.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.CefUtils', [ 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.util.Fun' ], function (CaretPosition, CaretUtils, NodeType, Fun) { var isContentEditableTrue = NodeType.isContentEditableTrue; var isContentEditableFalse = NodeType.isContentEditableFalse; var showCaret = function (direction, editor, node, before) { // TODO: Figure out a better way to handle this dependency return editor._selectionOverrides.showCaret(direction, node, before); }; var getNodeRange = function (node) { var rng = node.ownerDocument.createRange(); rng.selectNode(node); return rng; }; var selectNode = function (editor, node) { var e; e = editor.fire('BeforeObjectSelected', { target: node }); if (e.isDefaultPrevented()) { return null; } return getNodeRange(node); }; var renderCaretAtRange = function (editor, range) { var caretPosition, ceRoot; range = CaretUtils.normalizeRange(1, editor.getBody(), range); caretPosition = CaretPosition.fromRangeStart(range); if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd()); } if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(1, editor, 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, editor, ceRoot, false); } return null; }; var renderRangeCaret = function (editor, range) { var caretRange; if (!range || !range.collapsed) { return range; } caretRange = renderCaretAtRange(editor, range); if (caretRange) { return caretRange; } return range; }; return { showCaret: showCaret, selectNode: selectNode, renderCaretAtRange: renderCaretAtRange, renderRangeCaret: renderRangeCaret }; } ); /** * CefNavigation.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.CefNavigation', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.LineUtils', 'tinymce.core.caret.LineWalker', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.Env', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun' ], function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) { var isContentEditableFalse = NodeType.isContentEditableFalse; var getSelectedNode = RangeUtils.getSelectedNode; var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse; var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; var getVisualCaretPosition = function (walkFn, caretPosition) { while ((caretPosition = walkFn(caretPosition))) { if (caretPosition.isVisible()) { return caretPosition; } } return caretPosition; }; var isMoveInsideSameBlock = function (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; }; var isRangeInCaretContainerBlock = function (range) { return CaretContainer.isCaretContainerBlock(range.startContainer); }; var getNormalizedRangeEndPoint = function (direction, rootNode, range) { range = CaretUtils.normalizeRange(direction, rootNode, range); if (direction === -1) { return CaretPosition.fromRangeStart(range); } return CaretPosition.fromRangeEnd(range); }; var moveToCeFalseHorizontally = function (direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range) { var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock; if (!range.collapsed) { node = getSelectedNode(range); if (isContentEditableFalse(node)) { return CefUtils.showCaret(direction, editor, node, direction === -1); } } rangeIsInContainerBlock = isRangeInCaretContainerBlock(range); caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); if (isBeforeContentEditableFalseFn(caretPosition)) { return CefUtils.selectNode(editor, caretPosition.getNode(direction === -1)); } caretPosition = getNextPosFn(caretPosition); if (!caretPosition) { if (rangeIsInContainerBlock) { return range; } return null; } if (isBeforeContentEditableFalseFn(caretPosition)) { return CefUtils.showCaret(direction, editor, 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 CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(direction === -1), direction === 1); } } if (rangeIsInContainerBlock) { return CefUtils.renderRangeCaret(editor, caretPosition.toRange()); } return null; }; var moveToCeFalseVertically = function (direction, editor, walkerFn, range) { var caretPosition, linePositions, nextLinePositions, closestNextLineRect, caretClientRect, clientX, dist1, dist2, contentEditableFalseNode; contentEditableFalseNode = getSelectedNode(range); caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); linePositions = walkerFn(editor.getBody(), 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 CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2); } } if (contentEditableFalseNode) { var caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode); closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX); if (closestNextLineRect) { return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); } closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0))); if (closestNextLineRect) { return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); } } }; var createTextBlock = function (editor) { 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; }; var exitPreBlock = function (editor, direction, range) { var pre, caretPos, newBlock; var caretWalker = new CaretWalker(editor.getBody()); var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); 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(editor); if (direction === 1) { editor.$(pre).after(newBlock); } else { editor.$(pre).before(newBlock); } editor.selection.select(newBlock, true); editor.selection.collapse(); } } }; var getHorizontalRange = function (editor, forward) { var caretWalker = new CaretWalker(editor.getBody()); var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); var newRange, direction = forward ? 1 : -1; var getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition; var isBeforeContentEditableFalseFn = forward ? isBeforeContentEditableFalse : isAfterContentEditableFalse; var range = editor.selection.getRng(); newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range); if (newRange) { return newRange; } newRange = exitPreBlock(editor, direction, range); if (newRange) { return newRange; } return null; }; var getVerticalRange = function (editor, down) { var newRange, direction = down ? 1 : -1; var walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil; var range = editor.selection.getRng(); newRange = moveToCeFalseVertically(direction, editor, walkerFn, range); if (newRange) { return newRange; } newRange = exitPreBlock(editor, direction, range); if (newRange) { return newRange; } return null; }; var moveH = function (editor, forward) { return function () { var newRng = getHorizontalRange(editor, forward); if (newRng) { editor.selection.setRng(newRng); return true; } else { return false; } }; }; var moveV = function (editor, down) { return function () { var newRng = getVerticalRange(editor, down); if (newRng) { editor.selection.setRng(newRng); return true; } else { return false; } }; }; return { moveH: moveH, moveV: moveV }; } ); 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.CefNavigation', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], function (Arr, Cell, BoundarySelection, CefNavigation, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { var matches = MatchKeys.match([ { keyCode: VK.RIGHT, action: CefNavigation.moveH(editor, true) }, { keyCode: VK.LEFT, action: CefNavigation.moveH(editor, false) }, { keyCode: VK.UP, action: CefNavigation.moveV(editor, false) }, { keyCode: VK.DOWN, action: CefNavigation.moveV(editor, true) }, { 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(); }); }; var setup = function (editor, caret) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, caret, evt); } }); }; 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, InlineBoundaryDelete, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { var matches = MatchKeys.match([ { 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(InlineBoundaryDelete.backspaceDelete, editor, caret, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, 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 executeKeyupOverride = function (editor, 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) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, caret, evt); } }); editor.on('keyup', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeyupOverride(editor, evt); } }); }; 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">' : ''; }; var containerAndSiblingName = function (container, nodeName) { return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName); }; // 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" || parentBlockName == "HR") { 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 or a hr if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { 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); // Adjust caret position if HR containerAndSiblingName(parentBlock, 'HR') ? moveToCaretPosition(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 executeKeydownOverride = function (editor, 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) { editor.on('keydown', function (evt) { if (evt.isDefaultPrevented() === false) { executeKeydownOverride(editor, evt); } }); }; 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; var isTableCell = function (node) { return node && /^(TD|TH)$/i.test(node.nodeName); }; 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 (isTableCell(node)) { return null; } 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 }; }; } ); /** * 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 }; } ); define( 'ephox.sugar.impl.Style', [ ], function () { // some elements, such as mathml, don't have style attributes var isSupported = function (dom) { return dom.style !== undefined; }; return { isSupported: isSupported }; } ); define( 'ephox.sugar.api.properties.Css', [ 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', 'ephox.katamari.api.Option', 'ephox.sugar.api.properties.Attr', 'ephox.sugar.api.node.Body', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'ephox.sugar.impl.Style', 'ephox.katamari.api.Strings', 'global!Error', 'global!console', 'global!window' ], function (Type, Arr, Obj, Option, Attr, Body, Element, Node, Style, Strings, Error, console, window) { var internalSet = function (dom, property, value) { // This is going to hurt. Apologies. // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through. // we're going to be explicit; strings only. if (!Type.isString(value)) { console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom); throw new Error('CSS value must be a string: ' + value); } // removed: support for dom().style[property] where prop is camel case instead of normal property name if (Style.isSupported(dom)) dom.style.setProperty(property, value); }; var internalRemove = function (dom, property) { /* * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims * * http://help.dottoro.com/ljopsjck.php * http://stackoverflow.com/a/7901886/7546 */ if (Style.isSupported(dom)) dom.style.removeProperty(property); }; var set = function (element, property, value) { var dom = element.dom(); internalSet(dom, property, value); }; var setAll = function (element, css) { var dom = element.dom(); Obj.each(css, function (v, k) { internalSet(dom, k, v); }); }; var setOptions = function(element, css) { var dom = element.dom(); Obj.each(css, function (v, k) { v.fold(function () { internalRemove(dom, k); }, function (value) { internalSet(dom, k, value); }); }); }; /* * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle). * Blame CSS 2.0. * * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value */ var get = function (element, property) { var dom = element.dom(); /* * IE9 and above per * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle * * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous. * * JQuery has some magic here for IE popups, but we don't really need that. * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6. */ var styles = window.getComputedStyle(dom); var r = styles.getPropertyValue(property); // jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value. // Turns out we do this a lot. var v = (r === '' && !Body.inBody(element)) ? getUnsafeProperty(dom, property) : r; // undefined is the more appropriate value for JS. JQuery coerces to an empty string, but screw that! return v === null ? undefined : v; }; var getUnsafeProperty = function (dom, property) { // removed: support for dom().style[property] where prop is camel case instead of normal property name // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists. return Style.isSupported(dom) ? dom.style.getPropertyValue(property) : ''; }; /* * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM: * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value * * Returns NONE if the property isn't set, or the value is an empty string. */ var getRaw = function (element, property) { var dom = element.dom(); var raw = getUnsafeProperty(dom, property); return Option.from(raw).filter(function (r) { return r.length > 0; }); }; var isValidValue = function (tag, property, value) { var element = Element.fromTag(tag); set(element, property, value); var style = getRaw(element, property); return style.isSome(); }; var remove = function (element, property) { var dom = element.dom(); internalRemove(dom, property); if (Attr.has(element, 'style') && Strings.trim(Attr.get(element, 'style')) === '') { // No more styles left, remove the style attribute as well Attr.remove(element, 'style'); } }; var preserve = function (element, f) { var oldStyles = Attr.get(element, 'style'); var result = f(element); var restore = oldStyles === undefined ? Attr.remove : Attr.set; restore(element, 'style', oldStyles); return result; }; var copy = function (source, target) { var sourceDom = source.dom(); var targetDom = target.dom(); if (Style.isSupported(sourceDom) && Style.isSupported(targetDom)) { targetDom.style.cssText = sourceDom.style.cssText; } }; var reflow = function (e) { /* NOTE: * do not rely on this return value. * It's here so the closure compiler doesn't optimise the property access away. */ return e.dom().offsetWidth; }; var transferOne = function (source, destination, style) { getRaw(source, style).each(function (value) { // NOTE: We don't want to clobber any existing inline styles. if (getRaw(destination, style).isNone()) set(destination, style, value); }); }; var transfer = function (source, destination, styles) { if (!Node.isElement(source) || !Node.isElement(destination)) return; Arr.each(styles, function (style) { transferOne(source, destination, style); }); }; return { copy: copy, set: set, preserve: preserve, setAll: setAll, setOptions: setOptions, remove: remove, get: get, getRaw: getRaw, isValidValue: isValidValue, reflow: reflow, transfer: transfer }; } ); /** * EditorView.js * * Released under LGPL License. * Copyright (c) 1999-2016 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( 'tinymce.core.EditorView', [ 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.properties.Css', 'ephox.sugar.api.search.Traverse' ], function (Fun, Element, Css, Traverse) { var getProp = function (propName, elm) { var rawElm = elm.dom(); return rawElm[propName]; }; var getComputedSizeProp = function (propName, elm) { return parseInt(Css.get(elm, propName), 10); }; var getClientWidth = Fun.curry(getProp, 'clientWidth'); var getClientHeight = Fun.curry(getProp, 'clientHeight'); var getMarginTop = Fun.curry(getComputedSizeProp, 'margin-top'); var getMarginLeft = Fun.curry(getComputedSizeProp, 'margin-left'); var getBoundingClientRect = function (elm) { return elm.dom().getBoundingClientRect(); }; var isInsideElementContentArea = function (bodyElm, clientX, clientY) { var clientWidth = getClientWidth(bodyElm); var clientHeight = getClientHeight(bodyElm); return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight; }; var transpose = function (inline, elm, clientX, clientY) { var clientRect = getBoundingClientRect(elm); var deltaX = inline ? clientRect.left + elm.dom().clientLeft + getMarginLeft(elm) : 0; var deltaY = inline ? clientRect.top + elm.dom().clientTop + getMarginTop(elm) : 0; var x = clientX - deltaX; var y = clientY - deltaY; return { x: x, y: y }; }; // Checks if the specified coordinate is within the visual content area excluding the scrollbars var isXYInContentArea = function (editor, clientX, clientY) { var bodyElm = Element.fromDom(editor.getBody()); var targetElm = editor.inline ? bodyElm : Traverse.documentElement(bodyElm); var transposedPoint = transpose(editor.inline, targetElm, clientX, clientY); return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y); }; return { isXYInContentArea: isXYInContentArea }; } ); /** * 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.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.FakeCaret', 'tinymce.core.caret.LineUtils', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangePoint', 'tinymce.core.DragDropOverrides', 'tinymce.core.EditorView', 'tinymce.core.Env', 'tinymce.core.keyboard.CefUtils', 'tinymce.core.util.Delay', 'tinymce.core.util.VK' ], function ( CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret, LineUtils, NodeType, RangePoint, DragDropOverrides, EditorView, Env, CefUtils, Delay, VK ) { var isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse, isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; function SelectionOverrides(editor) { var rootNode = editor.getBody(); var 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 getNormalizedRangeEndPoint(direction, range) { range = CaretUtils.normalizeRange(direction, rootNode, range); if (direction == -1) { return CaretPosition.fromRangeStart(range); } return CaretPosition.fromRangeEnd(range); } function showBlockCaretContainer(blockCaretContainer) { if (blockCaretContainer.hasAttribute('data-mce-caret')) { CaretContainer.showCaretContainerBlock(blockCaretContainer); setRange(getRange()); // Removes control rect on IE scrollIntoView(blockCaretContainer[0]); } } function registerEvents() { function getContentEditableRoot(node) { var root = editor.getBody(); while (node && node != root) { if (isContentEditableTrue(node) || isContentEditableFalse(node)) { return node; } node = node.parentNode; } return null; } // 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 (e) { var range = getRange(); if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) { setRange(CefUtils.renderCaretAtRange(editor, 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(CefUtils.selectNode(editor, 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; }; // 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; if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) { return; } contentEditableRoot = getContentEditableRoot(e.target); if (contentEditableRoot) { if (isContentEditableFalse(contentEditableRoot)) { e.preventDefault(); setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); } else { removeContentEditableSelection(); // Check that we're not attempting a shift + click select within a contenteditable='true' element if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !RangePoint.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('keypress', function (e) { if (VK.modifierPressed(e)) { return; } switch (e.keyCode) { default: if (isContentEditableFalse(editor.selection.getNode())) { 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, e.forward); 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(CefUtils.renderRangeCaret(editor, 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, forward) { 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)) { if (forward === false) { caretPosition = getNormalizedRangeEndPoint(-1, range); if (isContentEditableFalse(caretPosition.getNode(true))) { return showCaret(-1, caretPosition.getNode(true), false); } if (isContentEditableFalse(caretPosition.getNode())) { return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd()); } } else { 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 { showCaret: showCaret, 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 || editor.removed) { 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 getStyleSheetLoader = function (editor) { return editor.inline ? DOM.styleSheetLoader : editor.dom.styleSheetLoader; }; 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[^>]*>(&nbsp;|&#160;|\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); } getStyleSheetLoader(editor).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 relaxDomain = function (editor, ifr) { // Domain relaxing is required since the user has messed around with document.domain // This only applies to IE 11 other browsers including Edge seems to handle document.domain if (document.domain !== window.location.hostname && Env.ie && Env.ie < 12) { 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);})()'; DOM.setAttrib(ifr, 'src', domainRelaxUrl); return true; } return false; }; var createIframe = function (editor, o) { var settings = editor.settings, bodyId, bodyClass; 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>'; // Create iframe // TODO: ACC add the appropriate description on this. var ifr = DOM.create('iframe', { id: editor.id + "_ifr", 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"); }; var isDomainRelaxed = relaxDomain(editor, ifr); editor.contentAreaContainer = o.iframeContainer; editor.iframeElement = ifr; DOM.add(o.iframeContainer, ifr); return isDomainRelaxed; }; var init = function (editor) { var settings = editor.settings, elm = editor.getElement(), boxInfo; 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); } var isDomainRelaxed = 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 (!isDomainRelaxed) { 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.EditorSettings', '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, EditorSettings, 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; documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; baseUri = editorManager.baseURI; /** * 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 = EditorSettings.getEditorSettings(self, id, documentBaseUrl, editorManager.defaultSettings, settings); 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 = 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, { 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; 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 (self.removed) { return; } 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 { // Restore previous selection before focus to prevent Chrome from // jumping to the top of the document in long inline editors self.selection.setRng(self.lastRng); 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) { if (text && Tools.is(text, 'string')) { var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; 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 (self.removed) { return ''; } 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 || self.removed) { 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(); if (self.removed) { return ''; } // 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.4', /** * Release date of TinyMCE build. * * @property releaseDate * @type String */ releaseDate: '2017-06-13', /** * 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(); } var rel = self.panel.testMoveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tc', 'bc-tl', 'bc-tr'] : ['bc-tc', 'bc-tr', 'bc-tl'])); self.panel.classes.toggle('start', rel === 'bc-tl'); self.panel.classes.toggle('end', rel === 'bc-tr'); self.panel.moveRel(self.getEl(), rel); }, /** * 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', [ 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', 'ephox.sugar.api.node.Element', 'ephox.sugar.api.node.Node', 'tinymce.core.dom.DOMUtils' ], function (Fun, Option, Element, Node, DOMUtils) { var getSpecifiedFontProp = function (propName, rootElm, elm) { while (elm !== rootElm) { if (elm.style[propName]) { var foundStyle = elm.style[propName]; return foundStyle !== '' ? Option.some(foundStyle) : Option.none(); } elm = elm.parentNode; } return Option.none(); }; 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 Option.from(DOMUtils.DOM.getStyle(elm, propName, true)); }; var getFontProp = function (propName) { return function (rootElm, elm) { return Option.from(elm) .map(Element.fromDom) .filter(Node.isElement) .bind(function (element) { return getSpecifiedFontProp(propName, rootElm, element.dom()) .or(getComputedFontProp(propName, element.dom())); }) .getOr(''); }; }; return { getFontSize: getFontProp('fontSize'), getFontFamily: Fun.compose(normalizeFontFamily, getFontProp('fontFamily')), 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('init 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('init 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.fontsize_formats || 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: '&#x2325;', ctrl: '&#x2318;', shift: '&#x21E7;', meta: '&#x2318;' }; } 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')(); })();