[3056] Image plugin now in new directory. Also temporarly removes the toolbar button because its not working anyways. Running npm run build inside the plugin root will now compile the plugin into ccm-core.
git-svn-id: https://svn.libreccm.org/ccm/trunk@5740 8810af33-2d31-482b-a856-94f89814c4dfmaster
parent
602095c44f
commit
50ec8b6c4e
|
|
@ -0,0 +1,36 @@
|
|||
ul {
|
||||
list-style: disc inside;
|
||||
}
|
||||
|
||||
div.image {
|
||||
border: 1px solid #999;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.image span.caption {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.image.left {
|
||||
float: left;
|
||||
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
div.image.right {
|
||||
float: right;
|
||||
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
div.image.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.image.center span.caption {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
tinymce.init({
|
||||
menubar: "tools",
|
||||
plugins: "trunk-images code lists nonbreaking noneditable paste searchreplace table template visualblocks wordcount",
|
||||
plugins: "ccm-cms-images code lists nonbreaking noneditable paste searchreplace table template visualblocks wordcount",
|
||||
selector: ".tinymce",
|
||||
templates: [],
|
||||
toolbar: "code"
|
||||
content_css: ['./editor.css'],
|
||||
toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | numlist bullist | outdent indent"
|
||||
});
|
||||
|
|
@ -75,12 +75,13 @@
|
|||
exports.__esModule = true;
|
||||
var Dialog_1 = __webpack_require__(1);
|
||||
var plugin = function (editor, url) {
|
||||
editor.addButton("trunk-images-button", {
|
||||
editor.addButton("ccm-cms-images-button", {
|
||||
icon: "image",
|
||||
tooltip: "Insert/Edit image",
|
||||
onlick: Dialog_1["default"](editor).open,
|
||||
stateSelector: "div.image"
|
||||
});
|
||||
editor.addMenuItem("trunk-images", {
|
||||
editor.addMenuItem("ccm-cms-images-menu", {
|
||||
icon: "image",
|
||||
text: "Insert Images",
|
||||
onclick: Dialog_1["default"](editor).open,
|
||||
|
|
@ -100,7 +101,26 @@ exports["default"] = plugin;
|
|||
|
||||
exports.__esModule = true;
|
||||
function default_1(editor) {
|
||||
function getImageData(editor) {
|
||||
var elem = editor.selection.getNode();
|
||||
var imgDiv = editor.dom.getParent(elem, "div.image");
|
||||
var img = editor.dom.select("img", imgDiv)[0];
|
||||
if (imgDiv != null) {
|
||||
var imageData = {
|
||||
file: img.getAttribute("src"),
|
||||
width: img.getAttribute("width").slice(0, -2),
|
||||
height: img.getAttribute("height").slice(0, -2),
|
||||
alt: img.getAttribute("alt"),
|
||||
parent: imgDiv
|
||||
};
|
||||
return imageData;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function open() {
|
||||
var imageData = getImageData(editor);
|
||||
var image_name = "";
|
||||
var fileChooseContainer = new tinymce.ui.Container({
|
||||
type: "container",
|
||||
|
|
@ -278,10 +298,33 @@ function default_1(editor) {
|
|||
fancy_box_wrap +
|
||||
span +
|
||||
"</div>";
|
||||
if (imageData != null) {
|
||||
editor.dom.replace(editor.dom.createFragment(img_div + "<br/>"), imageData.parent);
|
||||
}
|
||||
else {
|
||||
editor.insertContent(img_div + "<br/>");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (imageData != null) {
|
||||
win
|
||||
.find("#file")
|
||||
.value(imageData.file)
|
||||
.fire("change");
|
||||
win
|
||||
.find("#alternate")
|
||||
.value(imageData.alt)
|
||||
.fire("change");
|
||||
win
|
||||
.find("#width")
|
||||
.value(imageData.width)
|
||||
.fire("change");
|
||||
win
|
||||
.find("#height")
|
||||
.value(imageData.height)
|
||||
.fire("change");
|
||||
}
|
||||
}
|
||||
return {
|
||||
open: open
|
||||
|
|
@ -298,7 +341,7 @@ exports["default"] = default_1;
|
|||
|
||||
exports.__esModule = true;
|
||||
var plugin_1 = __webpack_require__(0);
|
||||
tinymce.PluginManager.add("trunk-images", plugin_1["default"]);
|
||||
tinymce.PluginManager.add("ccm-cms-images", plugin_1["default"]);
|
||||
|
||||
|
||||
/***/ })
|
||||
|
|
@ -0,0 +1 @@
|
|||
!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=2)}([function(e,t,n){"use strict";t.__esModule=!0;var i=n(1),a=function(e,t){e.addButton("ccm-cms-images-button",{icon:"image",tooltip:"Insert/Edit image",onlick:i.default(e).open,stateSelector:"div.image"}),e.addMenuItem("ccm-cms-images-menu",{icon:"image",text:"Insert Images",onclick:i.default(e).open,stateSelector:"image",context:"insert",prependToContext:!0})};t.default=a},function(e,t,n){"use strict";function i(e){function t(e){var t=e.selection.getNode(),n=e.dom.getParent(t,"div.image"),i=e.dom.select("img",n)[0];if(null!=n){return{file:i.getAttribute("src"),width:i.getAttribute("width").slice(0,-2),height:i.getAttribute("height").slice(0,-2),alt:i.getAttribute("alt"),parent:n}}return null}function n(){var n=t(e),i="",a=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),l=new tinymce.ui.TextBox({name:"file",label:"File:",disabled:!0});a.add(l);var r=new tinymce.ui.Button({name:"browse_images",text:"Browse Images",onclick:function(){var e=window.location.href,t=e.lastIndexOf("/"),n=e.slice(0,t+1)+"image_select.jsp";window.open(n,"_blank","scrollbars=yes,directories=no,toolbar=no,width=800,height=600,status=no,menubar=no");window.openCCM=new Object,window.openCCM.imageSet=function(e){return l.text(e.src),p.find("#file").value(e.src).fire("change"),p.find("#width").value(e.width).fire("change"),p.find("#height").value(e.height).fire("change"),i=e.name,!0}}});a.add(r);var o=new tinymce.ui.TextBox({name:"alternate",label:"Alternate:"}),c=new tinymce.ui.TextBox({name:"title",label:"Title:"}),u=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),d=new tinymce.ui.Label({text:"Alignment:"}),m=new tinymce.ui.ListBox({name:"alignment",values:[{text:"Not set",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]});u.add(d),u.add(m);var s=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),f=new tinymce.ui.Label({text:"Fancy Box:"}),g=new tinymce.ui.ListBox({name:"fancybox",values:[{text:"None",value:""},{text:"Zoom",value:"imageZoom"},{text:"Gallery",value:"imageGallery"}]});s.add(f),s.add(g);var h=new tinymce.ui.Checkbox({label:"Caption:",name:"caption"}),w=new tinymce.ui.Container({label:"Dimension",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),x=new tinymce.ui.TextBox({name:"width",label:"Width"}),v=new tinymce.ui.TextBox({name:"height",label:"Height"});w.add(x),w.add({type:"label",text:"X"}),w.add(v);var p=e.windowManager.open({title:"Insert/Modify Image",width:800,height:600,body:[a,o,c,u,s,h,w],onsubmit:function(){var t=p.find("#file").value(),a=p.find("#alternate").value(),l=p.find("#width").value(),r=p.find("#height").value(),o=p.find("#title").value(),c=p.find("#alignment").value(),u=p.find("#fancybox").value();if(null!=t){var d="<img src="+t+' alt="'+a+'" name="'+i+'" width="'+l+'px" height="'+r+'px" />',m="<a class="+u+'" href="'+t+'" title="'+o+'" data-mce-href="'+t+'"> '+d+"</a>",s="";p.find("#caption").value()&&(s='<span class="caption" style="width: '+l+'px;" data-mce-style="width: '+l+'px;">'+i+"</span>");var f='<div class="image '+c+'">'+m+s+"</div>";null!=n?e.dom.replace(e.dom.createFragment(f+"<br/>"),n.parent):e.insertContent(f+"<br/>")}}});null!=n&&(p.find("#file").value(n.file).fire("change"),p.find("#alternate").value(n.alt).fire("change"),p.find("#width").value(n.width).fire("change"),p.find("#height").value(n.height).fire("change"))}return{open:n}}t.__esModule=!0,t.default=i},function(e,t,n){"use strict";t.__esModule=!0;var i=n(0);tinymce.PluginManager.add("ccm-cms-images",i.default)}]);
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +0,0 @@
|
|||
!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=2)}([function(e,t,n){"use strict";t.__esModule=!0;var i=n(1),a=function(e,t){e.addButton("trunk-images-button",{icon:"image",onlick:i.default(e).open,stateSelector:"div.image"}),e.addMenuItem("trunk-images",{icon:"image",text:"Insert Images",onclick:i.default(e).open,stateSelector:"image",context:"insert",prependToContext:!0})};t.default=a},function(e,t,n){"use strict";function i(e){function t(){var t="",n=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),i=new tinymce.ui.TextBox({name:"file",label:"File:",disabled:!0});n.add(i);var a=new tinymce.ui.Button({name:"browse_images",text:"Browse Images",onclick:function(){var e=window.location.href,n=e.lastIndexOf("/"),a=e.slice(0,n+1)+"image_select.jsp";window.open(a,"_blank","scrollbars=yes,directories=no,toolbar=no,width=800,height=600,status=no,menubar=no");window.openCCM=new Object,window.openCCM.imageSet=function(e){return i.text(e.src),p.find("#file").value(e.src).fire("change"),p.find("#width").value(e.width).fire("change"),p.find("#height").value(e.height).fire("change"),t=e.name,!0}}});n.add(a);var o=new tinymce.ui.TextBox({name:"alternate",label:"Alternate:"}),l=new tinymce.ui.TextBox({name:"title",label:"Title:"}),r=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),u=new tinymce.ui.Label({text:"Alignment:"}),c=new tinymce.ui.ListBox({name:"alignment",values:[{text:"Not set",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]});r.add(u),r.add(c);var d=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),s=new tinymce.ui.Label({text:"Fancy Box:"}),m=new tinymce.ui.ListBox({name:"fancybox",values:[{text:"None",value:""},{text:"Zoom",value:"imageZoom"},{text:"Gallery",value:"imageGallery"}]});d.add(s),d.add(m);var f=new tinymce.ui.Checkbox({label:"Caption:",name:"caption"}),g=new tinymce.ui.Container({label:"Dimension",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),x=new tinymce.ui.TextBox({name:"width",label:"Width"}),w=new tinymce.ui.TextBox({name:"height",label:"Height"});g.add(x),g.add({type:"label",text:"X"}),g.add(w);var p=e.windowManager.open({title:"Insert/Modify Image",width:800,height:600,body:[n,o,l,r,d,f,g],onsubmit:function(){var n=p.find("#file").value(),i=p.find("#alternate").value(),a=p.find("#width").value(),o=p.find("#height").value(),l=p.find("#title").value(),r=p.find("#alignment").value(),u=p.find("#fancybox").value();if(null!=n){var c="<img src="+n+' alt="'+i+'" name="'+t+'" width="'+a+'px" height="'+o+'px" />',d="<a class="+u+'" href="'+n+'" title="'+l+'" data-mce-href="'+n+'"> '+c+"</a>",s="";p.find("#caption").value()&&(s='<span class="caption" style="width: '+a+'px;" data-mce-style="width: '+a+'px;">'+t+"</span>");var m='<div class="image '+r+'">'+d+s+"</div>";e.insertContent(m+"<br/>")}}})}return{open:t}}t.__esModule=!0,t.default=i},function(e,t,n){"use strict";t.__esModule=!0;var i=n(0);tinymce.PluginManager.add("trunk-images",i.default)}]);
|
||||
|
|
@ -1 +0,0 @@
|
|||
!function(t){function e(o){if(n[o])return n[o].exports;var u=n[o]={i:o,l:!1,exports:{}};return t[o].call(u.exports,u,u.exports,e),u.l=!0,u.exports}var n={};e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,n){"use strict";e.__esModule=!0;var o=n(1),u=function(t,e){return console.log("Trunk-Images loaded"),t.addMenuItem("trunk-images",{icon:!1,text:"Insert Trunk-Images",onclick:function(){o.default.open(t)},context:"insert"}),{}};e.default=u},function(t,e,n){"use strict";e.__esModule=!0;var o=function(t){t.addMenuItem("example",{text:"Example plugin",context:"tools",onclick:function(){t.windowManager.open({title:"TinyMCE site",url:"https://www.tinymce.com",width:800,height:600,buttons:[{text:"Close",onclick:"close"}]})}})};e.default={open:o}},function(t,e,n){"use strict";e.__esModule=!0;var o=n(0);tinymce.PluginManager.add("trunk-images",o.default)}]);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
* eol=lf
|
||||
*.jar binary
|
||||
*.gif binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.swf binary
|
||||
*.xap binary
|
||||
*.zip binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.ttf binary
|
||||
*.mov binary
|
||||
*.avi binary
|
||||
*.flv binary
|
||||
*.rm binary
|
||||
*.dcr binary
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
**Do you want to request a *feature* or report a *bug*?**
|
||||
|
||||
**What is the current behavior?**
|
||||
|
||||
**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via [fiddle.tinymce.com](http://fiddle.tinymce.com/) or similar.**
|
||||
|
||||
**What is the expected behavior?**
|
||||
|
||||
**Which versions of TinyMCE, and which browser / OS are affected by this issue? Did this work in previous versions of TinyMCE?**
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
**Before submitting a pull request** please do the following:
|
||||
|
||||
1. Fork [the repository](https://github.com/tinymce/tinymce) and create your branch from `master`
|
||||
2. Have you added some code that should be tested? Write some tests! (Are you unsure how to write the test you want to write, ask us for help!)
|
||||
3. Ensure that the tests pass: `grunt test`
|
||||
4. Ensure that your code passes the linter: `grunt lint`
|
||||
5. Make sure to sign the CLA.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
bin
|
||||
obj
|
||||
tmp
|
||||
.settings
|
||||
.idea
|
||||
.project
|
||||
.vscode
|
||||
.nyc_output
|
||||
*.sublime-*
|
||||
.externalToolBuilders
|
||||
**/bolt/bootstrap*.js
|
||||
*~
|
||||
*.diff
|
||||
*.patch
|
||||
*.bak
|
||||
*.log
|
||||
*.swp
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
.project
|
||||
coverage
|
||||
node_modules
|
||||
js/**/*
|
||||
imagemanager
|
||||
filemanager
|
||||
mcmanager
|
||||
powerpaste
|
||||
tinymcespellchecker
|
||||
a11ychecker
|
||||
codemirror
|
||||
mentions
|
||||
*.min.js
|
||||
*.dev.js
|
||||
*.full.js
|
||||
*.min.css
|
||||
*.dev.less
|
||||
**/dist
|
||||
**/scratch
|
||||
**/lib
|
||||
**/dependency
|
||||
**/instrumented_*
|
||||
.rpt2_cache
|
||||
|
|
@ -0,0 +1,825 @@
|
|||
/*eslint-env node */
|
||||
|
||||
let zipUtils = require('./tools/modules/zip-helper');
|
||||
let gruntUtils = require('./tools/modules/grunt-utils');
|
||||
let gruntWebPack = require('./tools/modules/grunt-webpack');
|
||||
let swag = require('@ephox/swag');
|
||||
let path = require('path');
|
||||
|
||||
let plugins = [
|
||||
'advlist', 'anchor', 'autolink', 'autoresize', 'autosave', 'bbcode', 'charmap', 'code', 'codesample',
|
||||
'colorpicker', /*'compat3x', */ 'contextmenu', 'directionality', 'emoticons', 'help', 'fullpage',
|
||||
'fullscreen', 'hr', 'image', 'imagetools', 'importcss', 'insertdatetime', 'legacyoutput', 'link',
|
||||
'lists', 'media', 'nonbreaking', 'noneditable', 'pagebreak', 'paste', 'preview', 'print', 'save',
|
||||
'searchreplace', 'spellchecker', 'tabfocus', 'table', 'template', 'textcolor', 'textpattern', 'toc',
|
||||
'visualblocks', 'visualchars', 'wordcount',
|
||||
];
|
||||
|
||||
let themes = [
|
||||
'modern', 'mobile', 'inlite'
|
||||
];
|
||||
|
||||
module.exports = function (grunt) {
|
||||
var packageData = grunt.file.readJSON('package.json');
|
||||
var changelogLine = grunt.file.read('changelog.txt').toString().split('\n')[0];
|
||||
var BUILD_VERSION = packageData.version + '-' + (process.env.BUILD_NUMBER ? process.env.BUILD_NUMBER : '0');
|
||||
packageData.date = /^Version [^\(]+\(([^\)]+)\)/.exec(changelogLine)[1];
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: packageData,
|
||||
|
||||
shell: {
|
||||
tsc: { command: 'node ./node_modules/typescript/bin/tsc' }
|
||||
},
|
||||
|
||||
tslint: {
|
||||
options: {
|
||||
configuration: 'tslint.json'
|
||||
},
|
||||
files: { src: [ 'src/**/*.ts' ] }
|
||||
},
|
||||
|
||||
globals: {
|
||||
options: {
|
||||
configFile: 'src/core/main/json/globals.json',
|
||||
outputDir: 'lib/globals',
|
||||
templateFile: 'src/core/main/js/GlobalsTemplate.js'
|
||||
}
|
||||
},
|
||||
|
||||
rollup: Object.assign(
|
||||
{
|
||||
core: {
|
||||
options: {
|
||||
treeshake: true,
|
||||
name: 'tinymce',
|
||||
format: 'iife',
|
||||
banner: '(function () {',
|
||||
footer: '})();',
|
||||
plugins: [
|
||||
swag.nodeResolve({
|
||||
basedir: __dirname,
|
||||
prefixes: {
|
||||
'tinymce/core': 'lib/core/main/ts'
|
||||
}
|
||||
}),
|
||||
swag.remapImports()
|
||||
]
|
||||
},
|
||||
files:[
|
||||
{
|
||||
src: 'lib/core/main/ts/api/Main.js',
|
||||
dest: 'js/tinymce/tinymce.js'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
gruntUtils.generate(plugins, 'plugin', (name) => {
|
||||
return {
|
||||
options: {
|
||||
treeshake: true,
|
||||
name: name,
|
||||
format: 'iife',
|
||||
banner: '(function () {',
|
||||
footer: '})();',
|
||||
plugins: [
|
||||
swag.nodeResolve({
|
||||
basedir: __dirname,
|
||||
prefixes: gruntUtils.prefixes({
|
||||
'tinymce/core': 'lib/globals/tinymce/core'
|
||||
}, [
|
||||
[`tinymce/plugins/${name}`, `lib/plugins/${name}/main/ts`]
|
||||
])
|
||||
}),
|
||||
swag.remapImports()
|
||||
]
|
||||
},
|
||||
files:[ { src: `lib/plugins/${name}/main/ts/Plugin.js`, dest: `js/tinymce/plugins/${name}/plugin.js` } ]
|
||||
};
|
||||
}),
|
||||
gruntUtils.generate(themes, 'theme', (name) => {
|
||||
return {
|
||||
options: {
|
||||
treeshake: true,
|
||||
name: name,
|
||||
format: 'iife',
|
||||
banner: '(function () {',
|
||||
footer: '})();',
|
||||
plugins: [
|
||||
swag.nodeResolve({
|
||||
basedir: __dirname,
|
||||
prefixes: gruntUtils.prefixes({
|
||||
'tinymce/core': 'lib/globals/tinymce/core',
|
||||
'tinymce/ui': 'lib/ui/main/ts'
|
||||
}, [
|
||||
[`tinymce/themes/${name}`, `lib/themes/${name}/main/ts`]
|
||||
])
|
||||
}),
|
||||
swag.remapImports()
|
||||
]
|
||||
},
|
||||
files:[
|
||||
{
|
||||
src: `lib/themes/${name}/main/ts/Theme.js`,
|
||||
dest: `js/tinymce/themes/${name}/theme.js`
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
||||
),
|
||||
|
||||
uglify: Object.assign(
|
||||
{
|
||||
options: {
|
||||
output: {
|
||||
ascii_only: true,
|
||||
},
|
||||
ie8: true
|
||||
},
|
||||
core: {
|
||||
files: [
|
||||
{ src: 'js/tinymce/tinymce.js', dest: 'js/tinymce/tinymce.min.js' },
|
||||
{ src: 'src/core/main/js/JqueryIntegration.js', dest: 'js/tinymce/jquery.tinymce.min.js' }
|
||||
]
|
||||
},
|
||||
'compat3x-plugin': {
|
||||
files: [
|
||||
{
|
||||
src: 'src/plugins/compat3x/main/js/plugin.js',
|
||||
dest: 'js/tinymce/plugins/compat3x/plugin.min.js'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
gruntUtils.generate(plugins, 'plugin', (name) => {
|
||||
return {
|
||||
files: [ { src: `js/tinymce/plugins/${name}/plugin.js`, dest: `js/tinymce/plugins/${name}/plugin.min.js` } ]
|
||||
};
|
||||
}),
|
||||
gruntUtils.generate(themes, 'theme', (name) => {
|
||||
return {
|
||||
files: [ { src: `js/tinymce/themes/${name}/theme.js`, dest: `js/tinymce/themes/${name}/theme.min.js` } ]
|
||||
};
|
||||
})
|
||||
),
|
||||
|
||||
webpack: Object.assign(
|
||||
{core: () => gruntWebPack.create('src/core/demo/ts/demo/Demos.ts', 'tsconfig.json', 'scratch/demos/core', 'demo.js')},
|
||||
{plugins: () => gruntWebPack.allPlugins(plugins)},
|
||||
{themes: () => gruntWebPack.allThemes(themes)},
|
||||
gruntUtils.generate(plugins, 'plugin', (name) => () => gruntWebPack.createPlugin(name) ),
|
||||
gruntUtils.generate(themes, 'theme', (name) => () => gruntWebPack.createTheme(name) )
|
||||
),
|
||||
|
||||
'webpack-dev-server': {
|
||||
options: {
|
||||
webpack: gruntWebPack.all(plugins, themes),
|
||||
publicPath: '/',
|
||||
inline: false,
|
||||
port: grunt.option('webpack-port') !== undefined ? grunt.option('webpack-port') : 3000,
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
before: app => gruntWebPack.generateDemoIndex(grunt, app, plugins, themes)
|
||||
},
|
||||
start: { }
|
||||
},
|
||||
|
||||
less: {
|
||||
desktop: {
|
||||
options: {
|
||||
cleancss: true,
|
||||
strictImports: true,
|
||||
compress: true,
|
||||
yuicompress: true,
|
||||
sourceMap: true,
|
||||
sourceMapRootpath: '.',
|
||||
optimization: 2
|
||||
},
|
||||
files: {
|
||||
'js/tinymce/skins/lightgray/skin.min.css': 'src/skins/lightgray/main/less/desktop/Skin.less'
|
||||
}
|
||||
},
|
||||
mobile: {
|
||||
options: {
|
||||
plugins : [ new (require('less-plugin-autoprefix'))({ browsers : [ 'last 2 versions', /* for phantom */'safari >= 4' ] }) ],
|
||||
compress: true,
|
||||
yuicompress: true,
|
||||
sourceMap: true,
|
||||
sourceMapRootpath: '.',
|
||||
optimization: 2
|
||||
},
|
||||
files: {
|
||||
'js/tinymce/skins/lightgray/skin.mobile.min.css': 'src/skins/lightgray/main/less/mobile/app/mobile-less.less'
|
||||
}
|
||||
},
|
||||
'content-mobile': {
|
||||
options: {
|
||||
cleancss: true,
|
||||
strictImports: true,
|
||||
compress: true
|
||||
},
|
||||
files: {
|
||||
'js/tinymce/skins/lightgray/content.mobile.min.css': 'src/skins/lightgray/main/less/mobile/content.less'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
options: {
|
||||
cleancss: true,
|
||||
strictImports: true,
|
||||
compress: true
|
||||
},
|
||||
files: {
|
||||
'js/tinymce/skins/lightgray/content.min.css': 'src/skins/lightgray/main/less/desktop/Content.less'
|
||||
}
|
||||
},
|
||||
'content-inline': {
|
||||
options: {
|
||||
cleancss: true,
|
||||
strictImports: true,
|
||||
compress: true
|
||||
},
|
||||
files: {
|
||||
'js/tinymce/skins/lightgray/content.inline.min.css': 'src/skins/lightgray/main/less/desktop/Content.Inline.less'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
copy: {
|
||||
core: {
|
||||
options: {
|
||||
process: function (content) {
|
||||
return content.
|
||||
replace('@@majorVersion@@', packageData.version.split('.')[0]).
|
||||
replace('@@minorVersion@@', packageData.version.split('.').slice(1).join('.')).
|
||||
replace('@@releaseDate@@', packageData.date);
|
||||
}
|
||||
},
|
||||
files: [
|
||||
{
|
||||
src: 'js/tinymce/tinymce.js',
|
||||
dest: 'js/tinymce/tinymce.js'
|
||||
},
|
||||
{
|
||||
src: 'js/tinymce/tinymce.min.js',
|
||||
dest: 'js/tinymce/tinymce.min.js'
|
||||
},
|
||||
{
|
||||
src: 'src/core/main/text/readme_lang.md',
|
||||
dest: 'js/tinymce/langs/readme.md'
|
||||
},
|
||||
{
|
||||
src: 'LICENSE.TXT',
|
||||
dest: 'js/tinymce/license.txt'
|
||||
}
|
||||
]
|
||||
},
|
||||
skins: {
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
flatten: true,
|
||||
cwd: 'src/skins/lightgray/main/fonts',
|
||||
src: [
|
||||
'**',
|
||||
'!*.json',
|
||||
'!*.md'
|
||||
],
|
||||
dest: 'js/tinymce/skins/lightgray/fonts'
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
flatten: true,
|
||||
cwd: 'src/skins/lightgray/main/img',
|
||||
src: '**',
|
||||
dest: 'js/tinymce/skins/lightgray/img'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: {
|
||||
files: [
|
||||
{ expand: true, cwd: 'src/plugins/compat3x/main', src: ['img/**'], dest: 'js/tinymce/plugins/compat3x' },
|
||||
{ expand: true, cwd: 'src/plugins/compat3x/main', src: ['css/**'], dest: 'js/tinymce/plugins/compat3x' },
|
||||
{ expand: true, cwd: 'src/plugins/compat3x/main/js', src: ['utils/**', 'plugin.js', 'tiny_mce_popup.js'], dest: 'js/tinymce/plugins/compat3x' },
|
||||
{ src: 'src/plugins/codesample/main/css/prism.css', dest: 'js/tinymce/plugins/codesample/css/prism.css' }
|
||||
]
|
||||
},
|
||||
'emoticons-plugin': {
|
||||
files: [
|
||||
{
|
||||
flatten: true,
|
||||
expand: true,
|
||||
cwd: 'src/plugins/emoticons/main/img',
|
||||
src: '*.gif',
|
||||
dest: 'js/tinymce/plugins/emoticons/img/'
|
||||
}
|
||||
]
|
||||
},
|
||||
'help-plugin': {
|
||||
files: [
|
||||
{ src: 'src/plugins/help/main/img/logo.png', dest: 'js/tinymce/plugins/help/img/logo.png' }
|
||||
]
|
||||
},
|
||||
'visualblocks-plugin': {
|
||||
files: [
|
||||
{ src: 'src/plugins/visualblocks/main/css/visualblocks.css', dest: 'js/tinymce/plugins/visualblocks/css/visualblocks.css' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
moxiezip: {
|
||||
production: {
|
||||
options: {
|
||||
baseDir: 'tinymce',
|
||||
excludes: [
|
||||
'js/**/plugin.js',
|
||||
'js/**/theme.js',
|
||||
'js/**/*.map',
|
||||
'js/tinymce/tinymce.full.min.js',
|
||||
'js/tinymce/plugins/moxiemanager',
|
||||
'js/tinymce/plugins/compat3x',
|
||||
'js/tinymce/plugins/visualblocks/img',
|
||||
'js/tinymce/skins/*/fonts/*.json',
|
||||
'js/tinymce/skins/*/fonts/readme.md',
|
||||
'readme.md'
|
||||
],
|
||||
to: 'tmp/tinymce_<%= pkg.version %>.zip'
|
||||
},
|
||||
src: [
|
||||
'js/tinymce/langs',
|
||||
'js/tinymce/plugins',
|
||||
'js/tinymce/skins',
|
||||
'js/tinymce/themes',
|
||||
'js/tinymce/tinymce.min.js',
|
||||
'js/tinymce/jquery.tinymce.min.js',
|
||||
'js/tinymce/license.txt',
|
||||
'changelog.txt',
|
||||
'LICENSE.TXT',
|
||||
'readme.md'
|
||||
]
|
||||
},
|
||||
|
||||
development: {
|
||||
options: {
|
||||
baseDir: 'tinymce',
|
||||
excludes: [
|
||||
'src/**/dist',
|
||||
'src/**/scratch',
|
||||
'src/**/lib',
|
||||
'src/**/dependency',
|
||||
'js/tinymce/tinymce.full.min.js',
|
||||
'js/tests/.jshintrc'
|
||||
],
|
||||
to: 'tmp/tinymce_<%= pkg.version %>_dev.zip'
|
||||
},
|
||||
src: [
|
||||
'config',
|
||||
'src',
|
||||
'js',
|
||||
'tests',
|
||||
'tools',
|
||||
'changelog.txt',
|
||||
'LICENSE.TXT',
|
||||
'Gruntfile.js',
|
||||
'readme.md',
|
||||
'package.json',
|
||||
'.eslintrc',
|
||||
'.jscsrc',
|
||||
'.jshintrc'
|
||||
]
|
||||
},
|
||||
cdn: {
|
||||
options: {
|
||||
onBeforeSave: function (zip) {
|
||||
zip.addData('dist/version.txt', packageData.version);
|
||||
},
|
||||
pathFilter: function (zipFilePath) {
|
||||
return zipFilePath.replace('js/tinymce/', 'dist/');
|
||||
},
|
||||
excludes: [
|
||||
'js/**/config',
|
||||
'js/**/scratch',
|
||||
'js/**/classes',
|
||||
'js/**/lib',
|
||||
'js/**/dependency',
|
||||
'js/**/src',
|
||||
'js/**/*.less',
|
||||
'js/**/*.dev.js',
|
||||
'js/**/*.dev.svg',
|
||||
'js/**/*.map',
|
||||
'js/tinymce/tinymce.full.min.js',
|
||||
'js/tinymce/plugins/moxiemanager',
|
||||
'js/tinymce/plugins/visualblocks/img',
|
||||
'js/tinymce/skins/*/fonts/*.json',
|
||||
'js/tinymce/skins/*/fonts/*.dev.svg',
|
||||
'js/tinymce/skins/*/fonts/readme.md',
|
||||
'readme.md',
|
||||
'js/tests/.jshintrc'
|
||||
],
|
||||
concat: [
|
||||
{
|
||||
src: [
|
||||
'js/tinymce/tinymce.min.js',
|
||||
'js/tinymce/themes/*/theme.min.js',
|
||||
'js/tinymce/plugins/*/plugin.min.js',
|
||||
'!js/tinymce/plugins/compat3x/plugin.min.js',
|
||||
'!js/tinymce/plugins/example/plugin.min.js',
|
||||
'!js/tinymce/plugins/example_dependency/plugin.min.js'
|
||||
],
|
||||
|
||||
dest: [
|
||||
'js/tinymce/tinymce.min.js'
|
||||
]
|
||||
}
|
||||
],
|
||||
to: 'tmp/tinymce_<%= pkg.version %>_cdn.zip'
|
||||
},
|
||||
src: [
|
||||
'js/tinymce/jquery.tinymce.min.js',
|
||||
'js/tinymce/tinymce.js',
|
||||
'js/tinymce/langs',
|
||||
'js/tinymce/plugins',
|
||||
'js/tinymce/skins',
|
||||
'js/tinymce/themes',
|
||||
'js/tinymce/license.txt'
|
||||
]
|
||||
},
|
||||
|
||||
component: {
|
||||
options: {
|
||||
excludes: [
|
||||
'js/**/config',
|
||||
'js/**/scratch',
|
||||
'js/**/classes',
|
||||
'js/**/lib',
|
||||
'js/**/dependency',
|
||||
'js/**/src',
|
||||
'js/**/*.less',
|
||||
'js/**/*.dev.svg',
|
||||
'js/**/*.dev.js',
|
||||
'js/**/*.map',
|
||||
'js/tinymce/tinymce.full.min.js',
|
||||
'js/tinymce/plugins/moxiemanager',
|
||||
'js/tinymce/plugins/example',
|
||||
'js/tinymce/plugins/example_dependency',
|
||||
'js/tinymce/plugins/compat3x',
|
||||
'js/tinymce/plugins/visualblocks/img',
|
||||
'js/tinymce/skins/*/fonts/*.json',
|
||||
'js/tinymce/skins/*/fonts/readme.md'
|
||||
],
|
||||
pathFilter: function (zipFilePath) {
|
||||
if (zipFilePath.indexOf('js/tinymce/') === 0) {
|
||||
return zipFilePath.substr('js/tinymce/'.length);
|
||||
}
|
||||
|
||||
return zipFilePath;
|
||||
},
|
||||
onBeforeSave: function (zip) {
|
||||
function jsonToBuffer(json) {
|
||||
return new Buffer(JSON.stringify(json, null, '\t'));
|
||||
}
|
||||
|
||||
zip.addData('bower.json', jsonToBuffer({
|
||||
'name': 'tinymce',
|
||||
'description': 'Web based JavaScript HTML WYSIWYG editor control.',
|
||||
'license': 'LGPL-2.1',
|
||||
'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
|
||||
'homepage': 'http://www.tinymce.com',
|
||||
'ignore': ['readme.md', 'composer.json', 'package.json', '.npmignore', 'changelog.txt']
|
||||
}));
|
||||
|
||||
zip.addData('package.json', jsonToBuffer({
|
||||
'name': 'tinymce',
|
||||
'version': packageData.version,
|
||||
'repository': {
|
||||
'type': 'git',
|
||||
'url': 'https://github.com/tinymce/tinymce-dist.git'
|
||||
},
|
||||
'description': 'Web based JavaScript HTML WYSIWYG editor control.',
|
||||
'author': 'Ephox Corporation',
|
||||
'main': 'tinymce.js',
|
||||
'license': 'LGPL-2.1',
|
||||
'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
|
||||
'bugs': { 'url': 'https://github.com/tinymce/tinymce/issues' }
|
||||
}));
|
||||
|
||||
zip.addData('composer.json', jsonToBuffer({
|
||||
'name': 'tinymce/tinymce',
|
||||
'version': packageData.version,
|
||||
'description': 'Web based JavaScript HTML WYSIWYG editor control.',
|
||||
'license': ['LGPL-2.1-only'],
|
||||
'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
|
||||
'homepage': 'http://www.tinymce.com',
|
||||
'type': 'component',
|
||||
'extra': {
|
||||
'component': {
|
||||
'scripts': [
|
||||
'tinymce.js',
|
||||
'plugins/*/plugin.js',
|
||||
'themes/*/theme.js'
|
||||
],
|
||||
'files': [
|
||||
'tinymce.min.js',
|
||||
'plugins/*/plugin.min.js',
|
||||
'themes/*/theme.min.js',
|
||||
'skins/**'
|
||||
]
|
||||
}
|
||||
},
|
||||
'archive': {
|
||||
'exclude': ['readme.md', 'bower.js', 'package.json', '.npmignore', 'changelog.txt']
|
||||
}
|
||||
}));
|
||||
|
||||
zip.addFile(
|
||||
'jquery.tinymce.js',
|
||||
'js/tinymce/jquery.tinymce.min.js'
|
||||
);
|
||||
|
||||
var getDirs = zipUtils.getDirectories(grunt, this.excludes);
|
||||
|
||||
zipUtils.addIndexFiles(
|
||||
zip,
|
||||
getDirs('js/tinymce/plugins'),
|
||||
zipUtils.generateIndex('plugins', 'plugin')
|
||||
);
|
||||
zipUtils.addIndexFiles(
|
||||
zip,
|
||||
getDirs('js/tinymce/themes'),
|
||||
zipUtils.generateIndex('themes', 'theme')
|
||||
);
|
||||
},
|
||||
to: 'tmp/tinymce_<%= pkg.version %>_component.zip'
|
||||
},
|
||||
src: [
|
||||
'js/tinymce/skins',
|
||||
'js/tinymce/plugins',
|
||||
'js/tinymce/themes',
|
||||
'js/tinymce/tinymce.js',
|
||||
'js/tinymce/tinymce.min.js',
|
||||
'js/tinymce/jquery.tinymce.min.js',
|
||||
'js/tinymce/license.txt',
|
||||
'changelog.txt',
|
||||
'readme.md'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
nugetpack: {
|
||||
main: {
|
||||
options: {
|
||||
id: 'TinyMCE',
|
||||
version: packageData.version,
|
||||
authors: 'Ephox Corp',
|
||||
owners: 'Ephox Corp',
|
||||
description: 'The best WYSIWYG editor! TinyMCE is a platform independent web based Javascript HTML WYSIWYG editor ' +
|
||||
'control released as Open Source under LGPL by Ephox Corp. TinyMCE has the ability to convert HTML ' +
|
||||
'TEXTAREA fields or other HTML elements to editor instances. TinyMCE is very easy to integrate ' +
|
||||
'into other Content Management Systems.',
|
||||
releaseNotes: 'Release notes for my package.',
|
||||
summary: 'TinyMCE is a platform independent web based Javascript HTML WYSIWYG editor ' +
|
||||
'control released as Open Source under LGPL by Ephox Corp.',
|
||||
projectUrl: 'http://www.tinymce.com/',
|
||||
iconUrl: 'http://www.tinymce.com/favicon.ico',
|
||||
licenseUrl: 'http://www.tinymce.com/license',
|
||||
requireLicenseAcceptance: true,
|
||||
tags: 'Editor TinyMCE HTML HTMLEditor',
|
||||
excludes: [
|
||||
'js/**/config',
|
||||
'js/**/scratch',
|
||||
'js/**/classes',
|
||||
'js/**/lib',
|
||||
'js/**/dependency',
|
||||
'js/**/src',
|
||||
'js/**/*.less',
|
||||
'js/**/*.dev.svg',
|
||||
'js/**/*.dev.js',
|
||||
'js/**/*.map',
|
||||
'js/tinymce/tinymce.full.min.js'
|
||||
],
|
||||
outputDir: 'tmp'
|
||||
},
|
||||
files: [
|
||||
{ src: 'js/tinymce/langs', dest: '/content/scripts/tinymce/langs' },
|
||||
{ src: 'js/tinymce/plugins', dest: '/content/scripts/tinymce/plugins' },
|
||||
{ src: 'js/tinymce/themes', dest: '/content/scripts/tinymce/themes' },
|
||||
{ src: 'js/tinymce/skins', dest: '/content/scripts/tinymce/skins' },
|
||||
{ src: 'js/tinymce/tinymce.js', dest: '/content/scripts/tinymce/tinymce.js' },
|
||||
{ src: 'js/tinymce/tinymce.min.js', dest: '/content/scripts/tinymce/tinymce.min.js' },
|
||||
{ src: 'js/tinymce/jquery.tinymce.min.js', dest: '/content/scripts/tinymce/jquery.tinymce.min.js' },
|
||||
{ src: 'js/tinymce/license.txt', dest: '/content/scripts/tinymce/license.txt' }
|
||||
]
|
||||
},
|
||||
|
||||
jquery: {
|
||||
options: {
|
||||
id: 'TinyMCE.jQuery',
|
||||
title: 'TinyMCE.jQuery [Deprecated]',
|
||||
version: packageData.version,
|
||||
authors: 'Ephox Corp',
|
||||
owners: 'Ephox Corp',
|
||||
description: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
|
||||
releaseNotes: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
|
||||
summary: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
|
||||
projectUrl: 'http://www.tinymce.com/',
|
||||
iconUrl: 'http://www.tinymce.com/favicon.ico',
|
||||
licenseUrl: 'http://www.tinymce.com/license',
|
||||
requireLicenseAcceptance: true,
|
||||
tags: 'Editor TinyMCE HTML HTMLEditor',
|
||||
excludes: [
|
||||
'js/**/config',
|
||||
'js/**/scratch',
|
||||
'js/**/classes',
|
||||
'js/**/lib',
|
||||
'js/**/dependency',
|
||||
'js/**/src',
|
||||
'js/**/*.less',
|
||||
'js/**/*.dev.svg',
|
||||
'js/**/*.dev.js',
|
||||
'js/**/*.map',
|
||||
'js/tinymce/tinymce.full.min.js'
|
||||
],
|
||||
outputDir: 'tmp'
|
||||
},
|
||||
|
||||
files: [
|
||||
{ src: 'js/tinymce/langs', dest: '/content/scripts/tinymce/langs' },
|
||||
{ src: 'js/tinymce/plugins', dest: '/content/scripts/tinymce/plugins' },
|
||||
{ src: 'js/tinymce/themes', dest: '/content/scripts/tinymce/themes' },
|
||||
{ src: 'js/tinymce/skins', dest: '/content/scripts/tinymce/skins' },
|
||||
{ src: 'js/tinymce/tinymce.js', dest: '/content/scripts/tinymce/tinymce.js' },
|
||||
{ src: 'js/tinymce/tinymce.min.js', dest: '/content/scripts/tinymce/tinymce.min.js' },
|
||||
{ src: 'js/tinymce/jquery.tinymce.min.js', dest: '/content/scripts/tinymce/jquery.tinymce.min.js' },
|
||||
{ src: 'js/tinymce/license.txt', dest: '/content/scripts/tinymce/license.txt' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
bundle: {
|
||||
minified: {
|
||||
options: {
|
||||
themesDir: 'js/tinymce/themes',
|
||||
pluginsDir: 'js/tinymce/plugins',
|
||||
pluginFileName: 'plugin.min.js',
|
||||
themeFileName: 'theme.min.js',
|
||||
outputPath: 'js/tinymce/tinymce.full.min.js'
|
||||
},
|
||||
|
||||
src: [
|
||||
'js/tinymce/tinymce.min.js'
|
||||
]
|
||||
},
|
||||
|
||||
source: {
|
||||
options: {
|
||||
themesDir: 'js/tinymce/themes',
|
||||
pluginsDir: 'js/tinymce/plugins',
|
||||
pluginFileName: 'plugin.js',
|
||||
themeFileName: 'theme.js',
|
||||
outputPath: 'js/tinymce/tinymce.full.js'
|
||||
},
|
||||
|
||||
src: [
|
||||
'js/tinymce/tinymce.js'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
clean: {
|
||||
dist: ['js'],
|
||||
lib: ['lib'],
|
||||
scratch: ['scratch'],
|
||||
release: ['tmp']
|
||||
},
|
||||
|
||||
'bedrock-manual': {
|
||||
core: {
|
||||
config: 'tsconfig.json',
|
||||
projectdir: '.',
|
||||
stopOnFailure: true,
|
||||
testfiles: [
|
||||
'src/**/test/ts/atomic/**/*Test.ts',
|
||||
'src/**/test/ts/browser/**/*Test.ts'
|
||||
],
|
||||
customRoutes: 'src/core/test/json/routes.json'
|
||||
}
|
||||
},
|
||||
|
||||
'bedrock-auto': {
|
||||
phantomjs: {
|
||||
browser: 'phantomjs',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'phantomjs'
|
||||
},
|
||||
'chrome-headless': {
|
||||
browser: 'chrome-headless',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'chrome-headless'
|
||||
},
|
||||
chrome: {
|
||||
browser: 'chrome',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'chrome'
|
||||
},
|
||||
firefox: {
|
||||
browser: 'firefox',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'firefox'
|
||||
},
|
||||
MicrosoftEdge: {
|
||||
browser: 'MicrosoftEdge',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'MicrosoftEdge'
|
||||
},
|
||||
ie: {
|
||||
browser: 'ie',
|
||||
config: 'tsconfig.json',
|
||||
testfiles: ['src/**/test/ts/**/*Test.ts'],
|
||||
stopOnFailure: true,
|
||||
overallTimeout: 600000,
|
||||
singleTimeout: 300000,
|
||||
customRoutes: 'src/core/test/json/routes.json',
|
||||
name: 'ie'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
skins: {
|
||||
files: ['src/skins/lightgray/main/less/**/*'],
|
||||
tasks: ['less', 'copy:skins'],
|
||||
options: {
|
||||
spawn: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
grunt.registerTask('version', 'Creates a version file', function () {
|
||||
grunt.file.write('tmp/version.txt', BUILD_VERSION);
|
||||
});
|
||||
|
||||
grunt.registerTask('build-headers', 'Appends build headers to js files', function () {
|
||||
var header = '// ' + packageData.version + ' (' + packageData.date + ')\n';
|
||||
grunt.file.write('js/tinymce/tinymce.js', header + grunt.file.read('js/tinymce/tinymce.js'));
|
||||
grunt.file.write('js/tinymce/tinymce.min.js', header + grunt.file.read('js/tinymce/tinymce.min.js'));
|
||||
});
|
||||
|
||||
require('load-grunt-tasks')(grunt);
|
||||
grunt.loadTasks('tools/tasks');
|
||||
grunt.loadNpmTasks('@ephox/bedrock');
|
||||
grunt.loadNpmTasks('@ephox/swag');
|
||||
grunt.loadNpmTasks('grunt-tslint');
|
||||
|
||||
grunt.registerTask('prod', [
|
||||
'validateVersion',
|
||||
'shell:tsc',
|
||||
'tslint',
|
||||
'globals',
|
||||
'rollup',
|
||||
'uglify',
|
||||
'less',
|
||||
'copy',
|
||||
'build-headers',
|
||||
'clean:release',
|
||||
'moxiezip',
|
||||
'nugetpack',
|
||||
'version'
|
||||
]);
|
||||
|
||||
grunt.registerTask('dev', [
|
||||
'shell:tsc',
|
||||
'globals',
|
||||
'rollup',
|
||||
'less',
|
||||
'copy'
|
||||
]);
|
||||
|
||||
grunt.registerTask('start', ['webpack-dev-server']);
|
||||
|
||||
grunt.registerTask('default', ['prod']);
|
||||
grunt.registerTask('test', ['bedrock-auto:phantomjs']);
|
||||
};
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"name": "tinymce",
|
||||
"version": "4.8.5",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinymce/tinymce.git"
|
||||
},
|
||||
"description": "TinyMCE rich text editor",
|
||||
"author": "Ephox Corporation",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tinymce/tinymce/issues"
|
||||
},
|
||||
"license": "LGPL-2.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.26"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt bedrock-auto:chrome-headless",
|
||||
"lint": "tslint src/**/*.ts",
|
||||
"postinstall": "rimraf node_modules/@ephox/alloy/node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ephox/alloy": "2.2.3",
|
||||
"@ephox/boulder": "3.2.1",
|
||||
"@ephox/darwin": "latest",
|
||||
"@ephox/dom-globals": "latest",
|
||||
"@ephox/imagetools": "latest",
|
||||
"@ephox/katamari": "latest",
|
||||
"@ephox/robin": "latest",
|
||||
"@ephox/sand": "latest",
|
||||
"@ephox/snooker": "latest",
|
||||
"@ephox/sugar": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ephox/agar": "latest",
|
||||
"@ephox/bedrock": "latest",
|
||||
"@ephox/mcagar": "latest",
|
||||
"@ephox/swag": "latest",
|
||||
"@ephox/wrap-jsverify": "latest",
|
||||
"awesome-typescript-loader": "^5.2.0",
|
||||
"grunt": "~1.0.2",
|
||||
"grunt-contrib-clean": "~1.1.0",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-less": "~1.4.1",
|
||||
"grunt-contrib-uglify": "~3.3.0",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-nuget-pack": "^0.0.6",
|
||||
"grunt-replace": "^1.0.1",
|
||||
"grunt-shell": "^2.1.0",
|
||||
"grunt-tslint": "^5.0.1",
|
||||
"grunt-webpack": "^3.1.2",
|
||||
"less-plugin-autoprefix": "^1.5.1",
|
||||
"load-grunt-tasks": "^4.0.0",
|
||||
"moxie-zip": "~0.0.3",
|
||||
"tslint": "^5.9.1",
|
||||
"ts-loader": "^5.3.0",
|
||||
"typescript": "^3.1.5",
|
||||
"webpack": "^4.8.3",
|
||||
"webpack-dev-server": "^3.1.5",
|
||||
"webpack-livereload-plugin": "^2.1.1",
|
||||
"rimraf": "^2.6.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
TinyMCE - JavaScript Library for Rich Text Editing
|
||||
===================================================
|
||||
|
||||
Building TinyMCE
|
||||
-----------------
|
||||
Install [Node.js](https://nodejs.org/en/) on your system.
|
||||
Clone this repository on your system
|
||||
```
|
||||
$ git clone https://github.com/tinymce/tinymce.git
|
||||
```
|
||||
Open a console and go to the project directory.
|
||||
```
|
||||
$ cd tinymce/
|
||||
```
|
||||
Install `grunt` command line tool globally.
|
||||
```
|
||||
$ npm i -g grunt-cli
|
||||
```
|
||||
Install all package dependencies.
|
||||
```
|
||||
$ npm install
|
||||
```
|
||||
Now, build TinyMCE by using `grunt`.
|
||||
```
|
||||
$ grunt
|
||||
```
|
||||
|
||||
|
||||
Build tasks
|
||||
------------
|
||||
`grunt`
|
||||
Lints, compiles, minifies and creates release packages for TinyMCE. This will produce the production ready packages.
|
||||
|
||||
`grunt start`
|
||||
Starts a webpack-dev-server that compiles the core, themes, plugins and all demos. Go to `localhost:3000` for a list of links to all the demo pages.
|
||||
|
||||
`grunt dev`
|
||||
Runs tsc, webpack and less. This will only produce the bare essentials for a development build and is a lot faster.
|
||||
|
||||
`grunt test`
|
||||
Runs all tests on PhantomJS.
|
||||
|
||||
`grunt bedrock-manual`
|
||||
Runs all tests manually in a browser.
|
||||
|
||||
`grunt bedrock-auto:<browser>`
|
||||
Runs all tests through selenium browsers supported are chrome, firefox, ie, MicrosoftEdge, chrome-headless and phantomjs.
|
||||
|
||||
`grunt webpack:core`
|
||||
Builds the demo js files for the core part of tinymce this is required to get the core demos working.
|
||||
|
||||
`grunt webpack:plugins`
|
||||
Builds the demo js files for the plugins part of tinymce this is required to get the plugins demos working.
|
||||
|
||||
`grunt webpack:themes`
|
||||
Builds the demo js files for the themes part of tinymce this is required to get the themes demos working.
|
||||
|
||||
`grunt webpack:<name>-plugin`
|
||||
Builds the demo js files for the specific plugin.
|
||||
|
||||
`grunt webpack:<name>-theme`
|
||||
Builds the demo js files for the specific theme.
|
||||
|
||||
`grunt --help`
|
||||
Displays the various build tasks.
|
||||
|
||||
Bundle themes and plugins into a single file
|
||||
---------------------------------------------
|
||||
`grunt bundle --themes=modern --plugins=table,paste`
|
||||
|
||||
Minifies the core, adds the modern theme and adds the table and paste plugin into tinymce.min.js.
|
||||
|
||||
Contributing to the TinyMCE project
|
||||
------------------------------------
|
||||
TinyMCE is an open source software project and we encourage developers to contribute patches and code to be included in the main package of TinyMCE.
|
||||
|
||||
__Basic Rules__
|
||||
|
||||
* Contributed code will be licensed under the LGPL license but not limited to LGPL
|
||||
* Copyright notices will be changed to Ephox Corporation, contributors will get credit for their work
|
||||
* All third party code will be reviewed, tested and possibly modified before being released
|
||||
* All contributors will have to have signed the Contributor License Agreement
|
||||
|
||||
These basic rules ensures that the contributed code remains open source and under the LGPL license.
|
||||
|
||||
__How to Contribute to the Code__
|
||||
|
||||
The TinyMCE source code is [hosted on Github](https://github.com/tinymce/tinymce). Through Github you can submit pull requests and log new bugs and feature requests.
|
||||
|
||||
When you submit a pull request, you will get a notice about signing the __Contributors License Agreement (CLA)__.
|
||||
You should have a __valid email address on your GitHub account__, and you will be sent a key to verify your identity and digitally sign the agreement.
|
||||
|
||||
After you signed your pull request will automatically be ready for review & merge.
|
||||
|
||||
__How to Contribute to the Docs__
|
||||
|
||||
Docs are hosted on Github in the [tinymce-docs](https://github.com/tinymce/tinymce-docs) repo.
|
||||
|
||||
[How to contribute](https://www.tinymce.com/docs/advanced/contributing-docs/) to the docs, including a style guide, can be found on the TinyMCE website.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
.mce-content-body *[contentEditable=true], .mce-content-body *[data-mce-contenteditable=true] {
|
||||
background: #adffad;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.mce-content-body *[contentEditable=false], .mce-content-body *[data-mce-contenteditable=false] {
|
||||
background: #ffadad;
|
||||
border: 1px solid #999;
|
||||
margin: 0 1px 0 2px;
|
||||
}
|
||||
|
||||
.mce-content-body div[contentEditable=false], .mce-content-body p[contentEditable=false] {
|
||||
margin: 2px 0 2px 0;
|
||||
}
|
||||
|
||||
.mce-content-body *[contentEditable]:focus {
|
||||
outline: 2px dotted blue;
|
||||
}
|
||||
|
||||
/* Debug overrides */
|
||||
|
||||
.mce-visual-caret {
|
||||
outline: 2px solid red;
|
||||
}
|
||||
|
||||
.mce-visual-caret-before {
|
||||
outline: 2px solid green;
|
||||
}
|
||||
|
||||
.mce-content-body *[data-mce-selected] {
|
||||
outline: 2px solid blue;
|
||||
}
|
||||
|
||||
.mce-content-body .mce-offscreen-selection {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
*[data-mce-caret] {
|
||||
outline: 1px solid green;
|
||||
position: absolute;
|
||||
left: auto;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>annotations demo Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>annotations demo Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea class="tinymce">
|
||||
<p>dsfdsafs<span class="mce-annotation" data-mce-annotation-uid="mce-annotation_94269650211529705226881" data-mce-annotation="alpha" data-mce-comment="dsaf">dfasdf</span></p><p><br data-mce-bogus="1"></p><p><span class="mce-annotation" data-mce-annotation-uid="mce-annotation_94269650211529705226881" data-mce-annotation="alpha" data-mce-comment="dsaf">dsafs</span>adf</p>
|
||||
|
||||
</textarea>
|
||||
</div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.AnnotationsDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>commands demo Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>commands demo Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea class="tinymce"></textarea>
|
||||
</div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.CommandsDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tinymce content editable false Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Tinymce content editable false Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea class="tinymce" rows="15" cols="80" style="width: 80%">
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<p>[cE=true]<span contentEditable="false">[cE=false:1]</span>abc<span contentEditable="false">[cE=false:2]</span><span contentEditable="false">[cE=false:3]</span><span contentEditable="false">[cE=false:4]</span><span contentEditable="false">[cE=false:5]</span></p>
|
||||
<p>Nam nisi elit, cursus in rhoncus sit amet, pulvinar laoreet leo. Nam <a href="#">sed</a> lectus quam, ut sagittis tellus. Quisque dignissim mauris a augue rutrum tempor. Donec vitae purus nec massa vestibulum ornare sit ame<span contentEditable="false">[cE=false]</span>t id tellus. Nunc quam mauris, fermentum nec lacinia eget, sollicitudin nec ante. Aliquam molestie volutpat dapibus. Nunc interdum viverra sodales. Morbi laoreet pulvinar gravida. Quisque ut turpis sagittis nunc accumsan vehicula. Duis elementum congue ultrices. Cras faucibus feugiat arcu quis lacinia. In hac habitasse platea dictumst. Pellentesque fermentum magna sit amet tellus varius ullamcorper. Vestibulum at urna augue, eget varius neque. Fusce facilisis venenatis dapibus. Integer non sem at arcu euismod tempor nec sed nisl. Morbi ultricies, mauris ut ultricies adipiscing, felis odio condimentum massa, et luctus est nunc nec eros.</p>
|
||||
<p><span contentEditable="false">[cE=false<span contentEditable="true">[cE=true]</span>]</span></p>
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<div contentEditable="false" style="width: 100px; height: 100px"><div>[cE=false]</div><div contentEditable="true">[cE=true]</div></div>
|
||||
<div contentEditable="false" style="width: 100px; height: 100px"><div>[cE=false]</div><span contentEditable="true">[cE=true]</span></div>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td style="padding: 10px"><div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div></td>
|
||||
<td style="padding: 10px">aaaa</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bbbb</td>
|
||||
<td style="padding: 10px"><div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px"><div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div></td>
|
||||
<td style="padding: 10px"><div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px">aaa<div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div></td>
|
||||
<td style="padding: 10px"><div contentEditable="false" style="width: 100px; height: 100px">[cE=false]</div>bbb</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div contentEditable="false" style="width: 100px; height: 100px">a<img src="https://www.google.com/logos/google.jpg" width="100">b</div>
|
||||
</textarea>
|
||||
|
||||
<div class="tinymce">
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<p>[cE=true]<span contentEditable="false">[cE=false]</span>abc<span contentEditable="false">[cE=false]</span><span contentEditable="false">[cE=false]</span><span contentEditable="false">[cE=false]</span><span contentEditable="false">[cE=false]</span></p>
|
||||
<p>Nam nisi elit, cursus in rhoncus sit amet, pulvinar laoreet leo. Nam sed lectus quam, ut sagittis tellus. Quisque dignissim mauris a augue rutrum tempor. Donec vitae purus nec massa vestibulum ornare sit ame<span contentEditable="false">[cE=false]</span>t id tellus. Nunc quam mauris, fermentum nec lacinia eget, sollicitudin nec ante. Aliquam molestie volutpat dapibus. Nunc interdum viverra sodales. Morbi laoreet pulvinar gravida. Quisque ut turpis sagittis nunc accumsan vehicula. Duis elementum congue ultrices. Cras faucibus feugiat arcu quis lacinia. In hac habitasse platea dictumst. Pellentesque fermentum magna sit amet tellus varius ullamcorper. Vestibulum at urna augue, eget varius neque. Fusce facilisis venenatis dapibus. Integer non sem at arcu euismod tempor nec sed nisl. Morbi ultricies, mauris ut ultricies adipiscing, felis odio condimentum massa, et luctus est nunc nec eros.</p>
|
||||
<p><span contentEditable="false">[cE=false<span contentEditable="true">[cE=true]</span>]</span></p>
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<p contentEditable="false">[cE=false]</p>
|
||||
<div contentEditable="false" style="width: 100px; height: 100px"><div>[cE=false]</div><span contentEditable="true">[cE=true]</span></div>
|
||||
<div contentEditable="false" style="width: 100px; height: 100px"><div>[cE=false]</div><span contentEditable="true">[cE=true]</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.ContentEditableFalseDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tinymce custom theme Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Tinymce custom theme Page</h2>
|
||||
<div id="ephox-ui"></div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.CustomThemeDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tinymce full featured Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Tinymce full featured Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea></textarea>
|
||||
</div>
|
||||
<div class="tinymce">Inline demo</div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.FullDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>jQuery integration Demo Page</title>
|
||||
<script src="//code.jquery.com/jquery-2.2.4.js"></script>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../js/tinymce/jquery.tinymce.min.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('textarea').tinymce({});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>jQuery integration Demo Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea></textarea>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TinyMCE Source Dump Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>TinyMCE Source Dump Demo Page</h2>
|
||||
<p><textarea id="editor"></textarea></p>
|
||||
<p>
|
||||
<textarea id="source" style="width: 100%; height: 500px"></textarea>
|
||||
<input type="checkbox" id="raw"> Raw html
|
||||
</p>
|
||||
</div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.SourceDumpDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tinymce Demo Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>TinyMCE Demo Page</h2>
|
||||
<div id="ephox-ui"></div>
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.TinyMceDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>ui_container Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="left" style="position: relative; right: 10px; top: 10px; width: 500px; height: 500px; overflow: scroll; margin: 40px; padding: 20px; border: 1px solid black">
|
||||
<h2 style="height: 400px">Left side iframe</h2>
|
||||
<textarea>
|
||||
<table style="border-collapse: collapse; width: 100%;" border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</textarea>
|
||||
|
||||
<h2>Left side inline</h2>
|
||||
<div class="tinymce" style="margin-bottom: 1000px">
|
||||
<table style="border-collapse: collapse; width: 100%;" border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right" style="position: absolute; right: 10px; top: 10px; width: 500px; height: 500px; overflow: scroll; margin: 40px; padding: 20px; border: 1px solid black">
|
||||
<h2 style="height: 400px">Right side iframe</h2>
|
||||
<textarea>
|
||||
<table style="border-collapse: collapse; width: 100%;" border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</textarea>
|
||||
|
||||
<h2>Right side inline</h2>
|
||||
<div class="tinymce" style="margin-bottom: 1000px">
|
||||
<table style="border-collapse: collapse; width: 100%;" border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 50%;"> </td>
|
||||
<td style="width: 50%;"> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../scratch/demos/core/demo.js"></script>
|
||||
<script>demos.UiContainerDemo();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* CommandsDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { prompt, document } from '@ephox/dom-globals';
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = 'Get all annotations';
|
||||
button.addEventListener('click', () => {
|
||||
// tslint:disable no-console
|
||||
console.log('annotations', tinymce.activeEditor.annotator.getAll('alpha'));
|
||||
// tslint:enable no-console
|
||||
});
|
||||
document.body.appendChild(button);
|
||||
|
||||
tinymce.init({
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
selector: 'textarea.tinymce',
|
||||
toolbar: 'annotate-alpha',
|
||||
plugins: [ ],
|
||||
|
||||
content_style: '.mce-annotation { background-color: darkgreen; color: white; }',
|
||||
|
||||
setup: (ed: Editor) => {
|
||||
ed.addButton('annotate-alpha', {
|
||||
text: 'Annotate',
|
||||
onclick: () => {
|
||||
const comment = prompt('Comment with?');
|
||||
ed.annotator.annotate('alpha', {
|
||||
comment
|
||||
});
|
||||
ed.focus();
|
||||
},
|
||||
|
||||
onpostrender: (ctrl) => {
|
||||
const button = ctrl.control;
|
||||
ed.on('init', () => {
|
||||
ed.annotator.annotationChanged('alpha', (state, name, obj) => {
|
||||
if (! state) {
|
||||
button.active(false);
|
||||
} else {
|
||||
button.active(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ed.on('init', () => {
|
||||
ed.annotator.register('alpha', {
|
||||
persistent: true,
|
||||
decorate: (uid, data) => {
|
||||
return {
|
||||
attributes: {
|
||||
'data-mce-comment': data.comment
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
theme: 'modern',
|
||||
menubar: false
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* CommandsDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { Arr } from '@ephox/katamari';
|
||||
import { document } from '@ephox/dom-globals';
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
const cmd = function (command, value?) {
|
||||
return { command, value };
|
||||
};
|
||||
|
||||
const commands = [
|
||||
cmd('Bold'),
|
||||
cmd('Italic'),
|
||||
cmd('Underline'),
|
||||
cmd('Strikethrough'),
|
||||
cmd('Superscript'),
|
||||
cmd('Subscript'),
|
||||
cmd('Cut'),
|
||||
cmd('Copy'),
|
||||
cmd('Paste'),
|
||||
cmd('Unlink'),
|
||||
cmd('JustifyLeft'),
|
||||
cmd('JustifyCenter'),
|
||||
cmd('JustifyRight'),
|
||||
cmd('JustifyFull'),
|
||||
cmd('JustifyNone'),
|
||||
cmd('InsertUnorderedList'),
|
||||
cmd('InsertOrderedList'),
|
||||
cmd('ForeColor', 'red'),
|
||||
cmd('HiliteColor', 'green'),
|
||||
cmd('FontName', 'Arial'),
|
||||
cmd('FontSize', 7),
|
||||
cmd('RemoveFormat'),
|
||||
cmd('mceBlockQuote'),
|
||||
cmd('FormatBlock', 'h1'),
|
||||
cmd('mceInsertContent', 'abc'),
|
||||
cmd('mceToggleFormat', 'bold'),
|
||||
cmd('mceSetContent', 'abc'),
|
||||
cmd('Indent'),
|
||||
cmd('Outdent'),
|
||||
cmd('InsertHorizontalRule'),
|
||||
cmd('mceToggleVisualAid'),
|
||||
cmd('mceInsertLink', 'url'),
|
||||
cmd('selectAll'),
|
||||
cmd('delete'),
|
||||
cmd('mceNewDocument'),
|
||||
cmd('Undo'),
|
||||
cmd('Redo'),
|
||||
cmd('mceAutoResize'),
|
||||
cmd('mceShowCharmap'),
|
||||
cmd('mceCodeEditor'),
|
||||
cmd('mceDirectionLTR'),
|
||||
cmd('mceDirectionRTL'),
|
||||
cmd('mceFullPageProperties'),
|
||||
cmd('mceFullscreen'),
|
||||
cmd('mceImage'),
|
||||
cmd('mceInsertDate'),
|
||||
cmd('mceInsertTime'),
|
||||
cmd('InsertDefinitionList'),
|
||||
cmd('mceNonBreaking'),
|
||||
cmd('mcePageBreak'),
|
||||
cmd('mcePreview'),
|
||||
cmd('mcePrint'),
|
||||
cmd('mceSave'),
|
||||
cmd('SearchReplace'),
|
||||
cmd('mceSpellcheck'),
|
||||
cmd('mceInsertTemplate', '{$user}'),
|
||||
cmd('mceVisualBlocks'),
|
||||
cmd('mceVisualChars'),
|
||||
cmd('mceMedia'),
|
||||
cmd('mceAnchor'),
|
||||
cmd('mceTableSplitCells'),
|
||||
cmd('mceTableMergeCells'),
|
||||
cmd('mceTableInsertRowBefore'),
|
||||
cmd('mceTableInsertRowAfter'),
|
||||
cmd('mceTableInsertColBefore'),
|
||||
cmd('mceTableInsertColAfter'),
|
||||
cmd('mceTableDeleteCol'),
|
||||
cmd('mceTableDeleteRow'),
|
||||
cmd('mceTableCutRow'),
|
||||
cmd('mceTableCopyRow'),
|
||||
cmd('mceTablePasteRowBefore'),
|
||||
cmd('mceTablePasteRowAfter'),
|
||||
cmd('mceTableDelete'),
|
||||
cmd('mceInsertTable'),
|
||||
cmd('mceTableProps'),
|
||||
cmd('mceTableRowProps'),
|
||||
cmd('mceTableCellProps'),
|
||||
cmd('mceEditImage')
|
||||
];
|
||||
|
||||
Arr.each(commands, function (cmd) {
|
||||
const btn = document.createElement('button');
|
||||
btn.innerHTML = cmd.command;
|
||||
btn.onclick = function () {
|
||||
tinymce.activeEditor.execCommand(cmd.command, false, cmd.value);
|
||||
};
|
||||
document.querySelector('#ephox-ui').appendChild(btn);
|
||||
});
|
||||
|
||||
tinymce.init({
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
selector: 'textarea.tinymce',
|
||||
plugins: [
|
||||
'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker toc',
|
||||
'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
|
||||
'save table contextmenu directionality emoticons template paste textcolor importcss colorpicker textpattern codesample'
|
||||
],
|
||||
theme: 'modern',
|
||||
toolbar1: 'bold italic',
|
||||
menubar: false
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* ContentEditableFalseDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
// tslint:disable:no-console
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
declare const window: any;
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
|
||||
const paintClientRect = function (rect, color, id) {
|
||||
const editor: Editor = tinymce.activeEditor;
|
||||
const $ = editor.$;
|
||||
let rectDiv;
|
||||
const viewPort = editor.dom.getViewPort();
|
||||
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
color = color || 'red';
|
||||
id = id || color;
|
||||
rectDiv = $('#' + id);
|
||||
|
||||
if (!rectDiv[0]) {
|
||||
rectDiv = $('<div></div>').appendTo(editor.getBody());
|
||||
}
|
||||
|
||||
rectDiv.attr('id', id).css({
|
||||
position: 'absolute',
|
||||
left: (rect.left + viewPort.x) + 'px',
|
||||
top: (rect.top + viewPort.y) + 'px',
|
||||
width: (rect.width || 1) + 'px',
|
||||
height: rect.height + 'px',
|
||||
background: color,
|
||||
opacity: 0.8
|
||||
});
|
||||
};
|
||||
|
||||
const paintClientRects = function (rects, color) {
|
||||
tinymce.util.Tools.each(rects, function (rect, index) {
|
||||
paintClientRect(rect, color, color + index);
|
||||
});
|
||||
};
|
||||
|
||||
const logPos = function (caretPosition) {
|
||||
const container = caretPosition.container(),
|
||||
offset = caretPosition.offset();
|
||||
|
||||
if (container.nodeType === 3) {
|
||||
if (container.data[offset]) {
|
||||
console.log(container.data[offset]);
|
||||
} else {
|
||||
console.log('<end of text node>');
|
||||
}
|
||||
} else {
|
||||
console.log(container, offset, caretPosition.getNode());
|
||||
}
|
||||
};
|
||||
|
||||
window.paintClientRect = paintClientRect;
|
||||
window.paintClientRects = paintClientRects;
|
||||
window.logPos = logPos;
|
||||
|
||||
tinymce.init({
|
||||
selector: 'textarea.tinymce',
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
add_unload_trigger: false,
|
||||
toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify' +
|
||||
' | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons table codesample',
|
||||
plugins: ['paste'],
|
||||
content_css: '../css/content_editable.css',
|
||||
height: 400
|
||||
});
|
||||
|
||||
tinymce.init({
|
||||
selector: 'div.tinymce',
|
||||
inline: true,
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
add_unload_trigger: false,
|
||||
toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify' +
|
||||
' | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons table codesample',
|
||||
plugins: ['paste'],
|
||||
content_css: '../css/content_editable.css'
|
||||
});
|
||||
|
||||
window.tinymce = tinymce;
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* CustomThemeDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { document } from '@ephox/dom-globals';
|
||||
|
||||
declare const tinymce: any;
|
||||
|
||||
export default function () {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.rows = 20;
|
||||
textarea.cols = 80;
|
||||
textarea.innerHTML = '<p>Bolt</p>';
|
||||
textarea.classList.add('tinymce');
|
||||
document.querySelector('#ephox-ui').appendChild(textarea);
|
||||
|
||||
tinymce.init({
|
||||
selector: 'textarea',
|
||||
theme (editor, target) {
|
||||
const dom = tinymce.DOM;
|
||||
let editorContainer;
|
||||
|
||||
editorContainer = dom.insertAfter(dom.create('div', { style: 'border: 1px solid gray' },
|
||||
'<div>' +
|
||||
'<button data-mce-command="bold">B</button>' +
|
||||
'<button data-mce-command="italic">I</button>' +
|
||||
'<button data-mce-command="mceInsertContent" data-mce-value="Hello">Insert Hello</button>' +
|
||||
'</div>' +
|
||||
'<div style="border-top: 1px solid gray"></div>'
|
||||
), target);
|
||||
|
||||
dom.setStyle(editorContainer, 'width', target.offsetWidth);
|
||||
|
||||
tinymce.each(dom.select('button', editorContainer), function (button) {
|
||||
dom.bind(button, 'click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
editor.execCommand(
|
||||
dom.getAttrib(e.target, 'data-mce-command'),
|
||||
false,
|
||||
dom.getAttrib(e.target, 'data-mce-value')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
editor.on(function () {
|
||||
tinymce.each(dom.select('button', editorContainer), function (button) {
|
||||
editor.formatter.formatChanged(dom.getAttrib(button, 'data-mce-command'), function (state) {
|
||||
button.style.color = state ? 'red' : '';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
editorContainer,
|
||||
iframeContainer: editorContainer.lastChild,
|
||||
iframeHeight: target.offsetHeight - editorContainer.firstChild.offsetHeight
|
||||
};
|
||||
},
|
||||
height: 600
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import CommandsDemo from './CommandsDemo';
|
||||
import ContentEditableFalseDemo from './ContentEditableFalseDemo';
|
||||
import CustomThemeDemo from './CustomThemeDemo';
|
||||
import FullDemo from './FullDemo';
|
||||
import TinyMceDemo from './TinyMceDemo';
|
||||
import UiContainerDemo from './UiContainerDemo';
|
||||
import AnnotationsDemo from './AnnotationsDemo';
|
||||
import SourceDumpDemo from './SourceDumpDemo';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
window.demos = {
|
||||
CommandsDemo,
|
||||
ContentEditableFalseDemo,
|
||||
CustomThemeDemo,
|
||||
FullDemo,
|
||||
TinyMceDemo,
|
||||
UiContainerDemo,
|
||||
AnnotationsDemo,
|
||||
SourceDumpDemo
|
||||
};
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* FullDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { Merger } from '@ephox/katamari';
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
|
||||
const settings = {
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
codesample_content_css: '../../../../js/tinymce/plugins/codesample/css/prism.css',
|
||||
visualblocks_content_css: '../../../../js/tinymce/plugins/visualblocks/css/visualblocks.css',
|
||||
images_upload_url: 'd',
|
||||
selector: 'textarea',
|
||||
// rtl_ui: true,
|
||||
link_list: [
|
||||
{ title: 'My page 1', value: 'http://www.tinymce.com' },
|
||||
{ title: 'My page 2', value: 'http://www.moxiecode.com' }
|
||||
],
|
||||
image_list: [
|
||||
{ title: 'My page 1', value: 'http://www.tinymce.com' },
|
||||
{ title: 'My page 2', value: 'http://www.moxiecode.com' }
|
||||
],
|
||||
image_class_list: [
|
||||
{ title: 'None', value: '' },
|
||||
{ title: 'Some class', value: 'class-name' }
|
||||
],
|
||||
importcss_append: true,
|
||||
height: 400,
|
||||
file_picker_callback (callback, value, meta) {
|
||||
// Provide file and text for the link dialog
|
||||
if (meta.filetype === 'file') {
|
||||
callback('https://www.google.com/logos/google.jpg', { text: 'My text' });
|
||||
}
|
||||
|
||||
// Provide image and alt text for the image dialog
|
||||
if (meta.filetype === 'image') {
|
||||
callback('https://www.google.com/logos/google.jpg', { alt: 'My alt text' });
|
||||
}
|
||||
|
||||
// Provide alternative source and posted for the media dialog
|
||||
if (meta.filetype === 'media') {
|
||||
callback('movie.mp4', { source2: 'alt.ogg', poster: 'https://www.google.com/logos/google.jpg' });
|
||||
}
|
||||
},
|
||||
spellchecker_callback (method, text, success, failure) {
|
||||
const words = text.match(this.getWordCharPattern());
|
||||
|
||||
if (method === 'spellcheck') {
|
||||
const suggestions = {};
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
suggestions[words[i]] = ['First', 'Second'];
|
||||
}
|
||||
|
||||
success(suggestions);
|
||||
}
|
||||
|
||||
if (method === 'addToDictionary') {
|
||||
success();
|
||||
}
|
||||
},
|
||||
templates: [
|
||||
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
|
||||
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
|
||||
],
|
||||
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
|
||||
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
|
||||
image_caption: true,
|
||||
theme: 'modern',
|
||||
mobile: {
|
||||
plugins: [
|
||||
'autosave lists'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'autosave advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker toc',
|
||||
'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
|
||||
'save table contextmenu directionality emoticons template paste textcolor importcss colorpicker textpattern',
|
||||
'codesample help noneditable print'
|
||||
],
|
||||
// rtl_ui: true,
|
||||
add_unload_trigger: false,
|
||||
autosave_ask_before_unload: false,
|
||||
toolbar: 'fontsizeselect fontselect insertfile undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | ' +
|
||||
'bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons table codesample code | ltr rtl'
|
||||
};
|
||||
|
||||
tinymce.init(settings);
|
||||
tinymce.init(Merger.deepMerge(settings, { inline: true, selector: 'div.tinymce' }));
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { document, HTMLTextAreaElement, HTMLInputElement } from '@ephox/dom-globals';
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
tinymce.init({
|
||||
selector: 'textarea#editor',
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
codesample_content_css: '../../../../js/tinymce/plugins/codesample/css/prism.css',
|
||||
visualblocks_content_css: '../../../../js/tinymce/plugins/visualblocks/css/visualblocks.css',
|
||||
templates: [
|
||||
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
|
||||
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
|
||||
],
|
||||
image_caption: true,
|
||||
plugins: [
|
||||
'autosave advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker toc',
|
||||
'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
|
||||
'save table contextmenu directionality emoticons template paste textcolor importcss colorpicker textpattern',
|
||||
'codesample help noneditable print'
|
||||
],
|
||||
add_unload_trigger: false,
|
||||
autosave_ask_before_unload: false,
|
||||
toolbar: 'fontsizeselect fontselect insertfile undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | ' +
|
||||
'bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons table codesample code | ltr rtl',
|
||||
init_instance_callback(editor) {
|
||||
editor.on('init keyup change', () => dumpSource(editor));
|
||||
}
|
||||
});
|
||||
|
||||
const dumpSource = (editor) => {
|
||||
const textArea = document.getElementById('source') as HTMLTextAreaElement;
|
||||
const raw = document.getElementById('raw') as HTMLInputElement;
|
||||
textArea.value = raw.checked ? editor.getBody().innerHTML : editor.getContent();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* TinyMceDemo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { document } from '@ephox/dom-globals';
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = '<p>Bolt</p>';
|
||||
|
||||
textarea.classList.add('tinymce');
|
||||
document.querySelector('#ephox-ui').appendChild(textarea);
|
||||
|
||||
tinymce.init({
|
||||
// imagetools_cors_hosts: ["moxiecode.cachefly.net"],
|
||||
// imagetools_proxy: "proxy.php",
|
||||
// imagetools_api_key: '123',
|
||||
|
||||
// images_upload_url: 'postAcceptor.php',
|
||||
// images_upload_base_path: 'base/path',
|
||||
// images_upload_credentials: true,
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
setup (ed) {
|
||||
ed.addButton('demoButton', {
|
||||
type: 'button',
|
||||
text: 'Demo',
|
||||
onclick () {
|
||||
ed.insertContent('Hello world!');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
selector: 'textarea.tinymce',
|
||||
theme: 'modern',
|
||||
toolbar1: 'demoButton bold italic',
|
||||
menubar: false
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* UiContainer.ts
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { Merger } from '@ephox/katamari';
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
export default function () {
|
||||
const generalSettings = {
|
||||
skin_url: '../../../../js/tinymce/skins/lightgray',
|
||||
codesample_content_css: '../../../../js/tinymce/plugins/codesample/css/prism.css',
|
||||
visualblocks_content_css: '../../../../js/tinymce/plugins/visualblocks/css/visualblocks.css',
|
||||
images_upload_url: 'd',
|
||||
// rtl_ui: true,
|
||||
link_list: [
|
||||
{ title: 'My page 1', value: 'http://www.tinymce.com' },
|
||||
{ title: 'My page 2', value: 'http://www.moxiecode.com' }
|
||||
],
|
||||
image_list: [
|
||||
{ title: 'My page 1', value: 'http://www.tinymce.com' },
|
||||
{ title: 'My page 2', value: 'http://www.moxiecode.com' }
|
||||
],
|
||||
image_class_list: [
|
||||
{ title: 'None', value: '' },
|
||||
{ title: 'Some class', value: 'class-name' }
|
||||
],
|
||||
importcss_append: true,
|
||||
height: 400,
|
||||
file_picker_callback (callback, value, meta) {
|
||||
// Provide file and text for the link dialog
|
||||
if (meta.filetype === 'file') {
|
||||
callback('https://www.google.com/logos/google.jpg', { text: 'My text' });
|
||||
}
|
||||
|
||||
// Provide image and alt text for the image dialog
|
||||
if (meta.filetype === 'image') {
|
||||
callback('https://www.google.com/logos/google.jpg', { alt: 'My alt text' });
|
||||
}
|
||||
|
||||
// Provide alternative source and posted for the media dialog
|
||||
if (meta.filetype === 'media') {
|
||||
callback('movie.mp4', { source2: 'alt.ogg', poster: 'https://www.google.com/logos/google.jpg' });
|
||||
}
|
||||
},
|
||||
spellchecker_callback (method, text, success, failure) {
|
||||
const words = text.match(this.getWordCharPattern());
|
||||
|
||||
if (method === 'spellcheck') {
|
||||
const suggestions = {};
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
suggestions[words[i]] = ['First', 'Second'];
|
||||
}
|
||||
|
||||
success(suggestions);
|
||||
}
|
||||
|
||||
if (method === 'addToDictionary') {
|
||||
success();
|
||||
}
|
||||
},
|
||||
templates: [
|
||||
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
|
||||
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
|
||||
],
|
||||
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
|
||||
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
|
||||
image_caption: true,
|
||||
theme: 'modern',
|
||||
mobile: {
|
||||
plugins: [
|
||||
'autosave lists'
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'autosave advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker toc',
|
||||
'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking',
|
||||
'save table contextmenu directionality emoticons template paste textcolor importcss colorpicker textpattern',
|
||||
'codesample help noneditable print'
|
||||
],
|
||||
// rtl_ui: true,
|
||||
add_unload_trigger: false,
|
||||
autosave_ask_before_unload: false
|
||||
};
|
||||
|
||||
const iframeSettings = Merger.deepMerge(generalSettings, {
|
||||
toolbar: 'fontsizeselect fontselect insertfile undo redo | insert | styleselect | bold italic | alignleft aligncenter alignright alignjustify | ' +
|
||||
'bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons table codesample code | ltr rtl'
|
||||
});
|
||||
|
||||
const inlineSettings = Merger.deepMerge(generalSettings, {
|
||||
inline: true,
|
||||
toolbar: [
|
||||
'fontsizeselect fontselect insertfile undo redo | insert | styleselect',
|
||||
'bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image',
|
||||
'print preview media fullpage | forecolor backcolor emoticons table codesample code | ltr rtl'
|
||||
]
|
||||
});
|
||||
|
||||
tinymce.init(Merger.deepMerge(iframeSettings, {
|
||||
selector: '#left textarea',
|
||||
ui_container: '#left'
|
||||
}));
|
||||
|
||||
tinymce.init(Merger.deepMerge(inlineSettings, {
|
||||
selector: '#left div.tinymce',
|
||||
ui_container: '#left'
|
||||
}));
|
||||
|
||||
tinymce.init(Merger.deepMerge(iframeSettings, {
|
||||
selector: '#right textarea',
|
||||
ui_container: '#right'
|
||||
}));
|
||||
|
||||
tinymce.init(Merger.deepMerge(inlineSettings, {
|
||||
selector: '#right div.tinymce',
|
||||
ui_container: '#right'
|
||||
}));
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
var global = tinymce.util.Tools.resolve('{$globalId}');
|
||||
|
||||
export default global;
|
||||
export var {$globalName} = global;
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
/**
|
||||
* JqueryIntegration.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Jquery integration plugin.
|
||||
*
|
||||
* @class tinymce.core.JqueryIntegration
|
||||
* @private
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var undef, lazyLoading, patchApplied;
|
||||
var delayedInits = [], $, win;
|
||||
|
||||
win = typeof global !== 'undefined' ? global : window;
|
||||
$ = win.jQuery;
|
||||
|
||||
var getTinymce = function () {
|
||||
// Reference to tinymce needs to be lazily evaluated since tinymce
|
||||
// might be loaded through the compressor or other means
|
||||
return win.tinymce;
|
||||
};
|
||||
|
||||
$.fn.tinymce = function (settings) {
|
||||
var self = this, url, base, lang, suffix = "";
|
||||
|
||||
// No match then just ignore the call
|
||||
if (!self.length) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Get editor instance
|
||||
if (!settings) {
|
||||
return getTinymce() ? getTinymce().get(self[0].id) : null;
|
||||
}
|
||||
|
||||
self.css('visibility', 'hidden'); // Hide textarea to avoid flicker
|
||||
|
||||
var init = function () {
|
||||
var editors = [], initCount = 0;
|
||||
|
||||
// Apply patches to the jQuery object, only once
|
||||
if (!patchApplied) {
|
||||
applyPatch();
|
||||
patchApplied = true;
|
||||
}
|
||||
|
||||
// Create an editor instance for each matched node
|
||||
self.each(function (i, node) {
|
||||
var ed, id = node.id, oninit = settings.oninit;
|
||||
|
||||
// Generate unique id for target element if needed
|
||||
if (!id) {
|
||||
node.id = id = getTinymce().DOM.uniqueId();
|
||||
}
|
||||
|
||||
// Only init the editor once
|
||||
if (getTinymce().get(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create editor instance and render it
|
||||
ed = getTinymce().createEditor(id, settings);
|
||||
editors.push(ed);
|
||||
|
||||
ed.on('init', function () {
|
||||
var scope, func = oninit;
|
||||
|
||||
self.css('visibility', '');
|
||||
|
||||
// Run this if the oninit setting is defined
|
||||
// this logic will fire the oninit callback ones each
|
||||
// matched editor instance is initialized
|
||||
if (oninit) {
|
||||
// Fire the oninit event ones each editor instance is initialized
|
||||
if (++initCount == editors.length) {
|
||||
if (typeof func === "string") {
|
||||
scope = (func.indexOf(".") === -1) ? null : getTinymce().resolve(func.replace(/\.\w+$/, ""));
|
||||
func = getTinymce().resolve(func);
|
||||
}
|
||||
|
||||
// Call the oninit function with the object
|
||||
func.apply(scope || getTinymce(), editors);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render the editor instances in a separate loop since we
|
||||
// need to have the full editors array used in the onInit calls
|
||||
$.each(editors, function (i, ed) {
|
||||
ed.render();
|
||||
});
|
||||
};
|
||||
|
||||
// Load TinyMCE on demand, if we need to
|
||||
if (!win.tinymce && !lazyLoading && (url = settings.script_url)) {
|
||||
lazyLoading = 1;
|
||||
base = url.substring(0, url.lastIndexOf("/"));
|
||||
|
||||
// Check if it's a dev/src version they want to load then
|
||||
// make sure that all plugins, themes etc are loaded in source mode as well
|
||||
if (url.indexOf('.min') != -1) {
|
||||
suffix = ".min";
|
||||
}
|
||||
|
||||
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
|
||||
// core script to locate other resources like CSS files, dialogs etc
|
||||
// You can also predefined a tinyMCEPreInit object and then it will use that instead
|
||||
win.tinymce = win.tinyMCEPreInit || {
|
||||
base: base,
|
||||
suffix: suffix
|
||||
};
|
||||
|
||||
// url contains gzip then we assume it's a compressor
|
||||
if (url.indexOf('gzip') != -1) {
|
||||
lang = settings.language || "en";
|
||||
url = url + (/\?/.test(url) ? '&' : '?') + "js=true&core=true&suffix=" + escape(suffix) +
|
||||
"&themes=" + escape(settings.theme || 'modern') + "&plugins=" +
|
||||
escape(settings.plugins || '') + "&languages=" + (lang || '');
|
||||
|
||||
// Check if compressor script is already loaded otherwise setup a basic one
|
||||
if (!win.tinyMCE_GZ) {
|
||||
win.tinyMCE_GZ = {
|
||||
start: function () {
|
||||
var load = function (url) {
|
||||
getTinymce().ScriptLoader.markDone(getTinymce().baseURI.toAbsolute(url));
|
||||
};
|
||||
|
||||
// Add core languages
|
||||
load("langs/" + lang + ".js");
|
||||
|
||||
// Add themes with languages
|
||||
load("themes/" + settings.theme + "/theme" + suffix + ".js");
|
||||
load("themes/" + settings.theme + "/langs/" + lang + ".js");
|
||||
|
||||
// Add plugins with languages
|
||||
$.each(settings.plugins.split(","), function (i, name) {
|
||||
if (name) {
|
||||
load("plugins/" + name + "/plugin" + suffix + ".js");
|
||||
load("plugins/" + name + "/langs/" + lang + ".js");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
end: function () {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.onload = script.onreadystatechange = function (e) {
|
||||
e = e || window.event;
|
||||
|
||||
if (lazyLoading !== 2 && (e.type == 'load' || /complete|loaded/.test(script.readyState))) {
|
||||
getTinymce().dom.Event.domLoaded = 1;
|
||||
lazyLoading = 2;
|
||||
|
||||
// Execute callback after mainscript has been loaded and before the initialization occurs
|
||||
if (settings.script_loaded) {
|
||||
settings.script_loaded();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$.each(delayedInits, function (i, init) {
|
||||
init();
|
||||
});
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
// Delay the init call until tinymce is loaded
|
||||
if (lazyLoading === 1) {
|
||||
delayedInits.push(init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
// Add :tinymce pseudo selector this will select elements that has been converted into editor instances
|
||||
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
|
||||
$.extend($.expr[":"], {
|
||||
tinymce: function (e) {
|
||||
var editor;
|
||||
|
||||
if (e.id && "tinymce" in win) {
|
||||
editor = getTinymce().get(e.id);
|
||||
|
||||
if (editor && editor.editorManager === getTinymce()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// This function patches internal jQuery functions so that if
|
||||
// you for example remove an div element containing an editor it's
|
||||
// automatically destroyed by the TinyMCE API
|
||||
var applyPatch = function () {
|
||||
// Removes any child editor instances by looking for editor wrapper elements
|
||||
var removeEditors = function (name) {
|
||||
// If the function is remove
|
||||
if (name === "remove") {
|
||||
this.each(function (i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ed.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.find("span.mceEditor,div.mceEditor").each(function (i, node) {
|
||||
var ed = getTinymce().get(node.id.replace(/_parent$/, ""));
|
||||
|
||||
if (ed) {
|
||||
ed.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Loads or saves contents from/to textarea if the value
|
||||
// argument is defined it will set the TinyMCE internal contents
|
||||
var loadOrSave = function (value) {
|
||||
var self = this, ed;
|
||||
|
||||
// Handle set value
|
||||
/*jshint eqnull:true */
|
||||
if (value != null) {
|
||||
removeEditors.call(self);
|
||||
|
||||
// Saves the contents before get/set value of textarea/div
|
||||
self.each(function (i, node) {
|
||||
var ed;
|
||||
|
||||
if ((ed = getTinymce().get(node.id))) {
|
||||
ed.setContent(value);
|
||||
}
|
||||
});
|
||||
} else if (self.length > 0) {
|
||||
// Handle get value
|
||||
if ((ed = getTinymce().get(self[0].id))) {
|
||||
return ed.getContent();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Returns tinymce instance for the specified element or null if it wasn't found
|
||||
var tinyMCEInstance = function (element) {
|
||||
var ed = null;
|
||||
|
||||
if (element && element.id && win.tinymce) {
|
||||
ed = getTinymce().get(element.id);
|
||||
}
|
||||
|
||||
return ed;
|
||||
};
|
||||
|
||||
// Checks if the specified set contains tinymce instances
|
||||
var containsTinyMCE = function (matchedSet) {
|
||||
return !!((matchedSet) && (matchedSet.length) && (win.tinymce) && (matchedSet.is(":tinymce")));
|
||||
};
|
||||
|
||||
// Patch various jQuery functions
|
||||
var jQueryFn = {};
|
||||
|
||||
// Patch some setter/getter functions these will
|
||||
// now be able to set/get the contents of editor instances for
|
||||
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
|
||||
$.each(["text", "html", "val"], function (i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name],
|
||||
textProc = (name === "text");
|
||||
|
||||
$.fn[name] = function (value) {
|
||||
var self = this;
|
||||
|
||||
if (!containsTinyMCE(self)) {
|
||||
return origFn.apply(self, arguments);
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
loadOrSave.call(self.filter(":tinymce"), value);
|
||||
origFn.apply(self.not(":tinymce"), arguments);
|
||||
|
||||
return self; // return original set for chaining
|
||||
}
|
||||
|
||||
var ret = "";
|
||||
var args = arguments;
|
||||
|
||||
(textProc ? self : self.eq(0)).each(function (i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ret += textProc ? ed.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, "") : ed.getContent({ save: true });
|
||||
} else {
|
||||
ret += origFn.apply($(node), args);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
|
||||
$.each(["append", "prepend"], function (i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name],
|
||||
prepend = (name === "prepend");
|
||||
|
||||
$.fn[name] = function (value) {
|
||||
var self = this;
|
||||
|
||||
if (!containsTinyMCE(self)) {
|
||||
return origFn.apply(self, arguments);
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
if (typeof value === "string") {
|
||||
self.filter(":tinymce").each(function (i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ed.setContent(prepend ? value + ed.getContent() : ed.getContent() + value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
origFn.apply(self.not(":tinymce"), arguments);
|
||||
|
||||
return self; // return original set for chaining
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
|
||||
$.each(["remove", "replaceWith", "replaceAll", "empty"], function (i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name];
|
||||
|
||||
$.fn[name] = function () {
|
||||
removeEditors.call(this, name);
|
||||
|
||||
return origFn.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
jQueryFn.attr = $.fn.attr;
|
||||
|
||||
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
|
||||
$.fn.attr = function (name, value) {
|
||||
var self = this, args = arguments;
|
||||
|
||||
if ((!name) || (name !== "value") || (!containsTinyMCE(self))) {
|
||||
if (value !== undef) {
|
||||
return jQueryFn.attr.apply(self, args);
|
||||
}
|
||||
|
||||
return jQueryFn.attr.apply(self, args);
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
loadOrSave.call(self.filter(":tinymce"), value);
|
||||
jQueryFn.attr.apply(self.not(":tinymce"), args);
|
||||
|
||||
return self; // return original set for chaining
|
||||
}
|
||||
|
||||
var node = self[0], ed = tinyMCEInstance(node);
|
||||
|
||||
return ed ? ed.getContent({ save: true }) : jQueryFn.attr.apply($(node), args);
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"replace.prefix": {
|
||||
"search": [ "tinymce.core.api", "tinymce.core" ],
|
||||
"replace": "tinymce",
|
||||
"targets": [
|
||||
"tinymce.core.api.AddOnManager",
|
||||
"tinymce.core.api.dom.BookmarkManager",
|
||||
"tinymce.core.api.dom.RangeUtils",
|
||||
"tinymce.core.api.Annotator",
|
||||
"tinymce.core.api.FocusManager",
|
||||
"tinymce.core.api.Formatter",
|
||||
"tinymce.core.api.WindowManager",
|
||||
"tinymce.core.api.dom.ControlSelection",
|
||||
"tinymce.core.api.dom.DomQuery",
|
||||
"tinymce.core.api.dom.DOMUtils",
|
||||
"tinymce.core.api.dom.EventUtils",
|
||||
"tinymce.core.api.dom.ScriptLoader",
|
||||
"tinymce.core.api.dom.Selection",
|
||||
"tinymce.core.api.dom.Serializer",
|
||||
"tinymce.core.api.dom.Sizzle",
|
||||
"tinymce.core.api.dom.TreeWalker",
|
||||
"tinymce.core.api.Editor",
|
||||
"tinymce.core.api.EditorCommands",
|
||||
"tinymce.core.api.EditorManager",
|
||||
"tinymce.core.api.EditorObservable",
|
||||
"tinymce.core.api.Env",
|
||||
"tinymce.core.api.geom.Rect",
|
||||
"tinymce.core.api.html.DomParser",
|
||||
"tinymce.core.api.html.Entities",
|
||||
"tinymce.core.api.html.Node",
|
||||
"tinymce.core.api.html.SaxParser",
|
||||
"tinymce.core.api.html.Schema",
|
||||
"tinymce.core.api.html.Serializer",
|
||||
"tinymce.core.api.html.Styles",
|
||||
"tinymce.core.api.html.Writer",
|
||||
"tinymce.core.api.NotificationManager",
|
||||
"tinymce.core.api.PluginManager",
|
||||
"tinymce.core.api.Shortcuts",
|
||||
"tinymce.core.api.ThemeManager",
|
||||
"tinymce.core.api.ui.Factory",
|
||||
"tinymce.core.api.UndoManager",
|
||||
"tinymce.core.api.util.Class",
|
||||
"tinymce.core.api.util.Color",
|
||||
"tinymce.core.api.util.Delay",
|
||||
"tinymce.core.api.util.EventDispatcher",
|
||||
"tinymce.core.api.util.I18n",
|
||||
"tinymce.core.api.util.JSON",
|
||||
"tinymce.core.api.util.JSONP",
|
||||
"tinymce.core.api.util.JSONRequest",
|
||||
"tinymce.core.api.util.LocalStorage",
|
||||
"tinymce.core.api.util.Observable",
|
||||
"tinymce.core.api.util.Promise",
|
||||
"tinymce.core.api.util.Tools",
|
||||
"tinymce.core.api.util.URI",
|
||||
"tinymce.core.api.util.VK",
|
||||
"tinymce.core.api.util.XHR"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
This is where language files should be placed.
|
||||
|
||||
Please DO NOT translate these directly use this service: https://www.transifex.com/projects/p/tinymce/
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import DOMUtils from './api/dom/DOMUtils';
|
||||
import MousePosition from './dom/MousePosition';
|
||||
import NodeType from './dom/NodeType';
|
||||
import Arr from './util/Arr';
|
||||
import Delay from './api/util/Delay';
|
||||
import Fun from './util/Fun';
|
||||
import { document } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* This module contains logic overriding the drag/drop logic of the editor.
|
||||
*
|
||||
* @private
|
||||
* @class tinymce.DragDropOverrides
|
||||
*/
|
||||
|
||||
const isContentEditableFalse = NodeType.isContentEditableFalse,
|
||||
isContentEditableTrue = NodeType.isContentEditableTrue;
|
||||
|
||||
const isDraggable = function (rootElm, elm) {
|
||||
return isContentEditableFalse(elm) && elm !== rootElm;
|
||||
};
|
||||
|
||||
const isValidDropTarget = function (editor, targetElement, dragElement) {
|
||||
if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isContentEditableFalse(targetElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const cloneElement = function (elm) {
|
||||
const cloneElm = elm.cloneNode(true);
|
||||
cloneElm.removeAttribute('data-mce-selected');
|
||||
return cloneElm;
|
||||
};
|
||||
|
||||
const createGhost = function (editor, elm, width, height) {
|
||||
const clonedElm = elm.cloneNode(true);
|
||||
|
||||
editor.dom.setStyles(clonedElm, { width, height });
|
||||
editor.dom.setAttrib(clonedElm, 'data-mce-selected', null);
|
||||
|
||||
const 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,
|
||||
height
|
||||
});
|
||||
|
||||
editor.dom.setStyles(clonedElm, {
|
||||
margin: 0,
|
||||
boxSizing: 'border-box'
|
||||
});
|
||||
|
||||
ghostElm.appendChild(clonedElm);
|
||||
|
||||
return ghostElm;
|
||||
};
|
||||
|
||||
const appendGhostToBody = function (ghostElm, bodyElm) {
|
||||
if (ghostElm.parentNode !== bodyElm) {
|
||||
bodyElm.appendChild(ghostElm);
|
||||
}
|
||||
};
|
||||
|
||||
const moveGhost = function (ghostElm, position, width, height, maxX, maxY) {
|
||||
let 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';
|
||||
};
|
||||
|
||||
const removeElement = function (elm) {
|
||||
if (elm && elm.parentNode) {
|
||||
elm.parentNode.removeChild(elm);
|
||||
}
|
||||
};
|
||||
|
||||
const isLeftMouseButtonPressed = function (e) {
|
||||
return e.button === 0;
|
||||
};
|
||||
|
||||
const hasDraggableElement = function (state) {
|
||||
return state.element;
|
||||
};
|
||||
|
||||
const applyRelPos = function (state, position) {
|
||||
return {
|
||||
pageX: position.pageX - state.relX,
|
||||
pageY: position.pageY + 5
|
||||
};
|
||||
};
|
||||
|
||||
const start = function (state, editor) {
|
||||
return function (e) {
|
||||
if (isLeftMouseButtonPressed(e)) {
|
||||
const ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue));
|
||||
|
||||
if (isDraggable(editor.getBody(), ceElm)) {
|
||||
const elmPos = editor.dom.getPos(ceElm);
|
||||
const bodyElm = editor.getBody();
|
||||
const 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const move = function (state, editor) {
|
||||
// Reduces laggy drag behavior on Gecko
|
||||
const throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) {
|
||||
editor._selectionOverrides.hideFakeCaret();
|
||||
editor.selection.placeCaretAt(clientX, clientY);
|
||||
}, 0);
|
||||
|
||||
return function (e) {
|
||||
const movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY));
|
||||
|
||||
if (hasDraggableElement(state) && !state.dragging && movement > 10) {
|
||||
const args = editor.fire('dragstart', { target: state.element });
|
||||
if (args.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.dragging = true;
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
if (state.dragging) {
|
||||
const 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
|
||||
const getRawTarget = function (selection) {
|
||||
const rng = selection.getSel().getRangeAt(0);
|
||||
const startContainer = rng.startContainer;
|
||||
return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer;
|
||||
};
|
||||
|
||||
const drop = function (state, editor) {
|
||||
return function (e) {
|
||||
if (state.dragging) {
|
||||
if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) {
|
||||
let targetClone = cloneElement(state.element);
|
||||
|
||||
const args = editor.fire('drop', {
|
||||
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);
|
||||
};
|
||||
};
|
||||
|
||||
const stop = function (state, editor) {
|
||||
return function () {
|
||||
if (state.dragging) {
|
||||
editor.fire('dragend');
|
||||
}
|
||||
removeDragState(state);
|
||||
};
|
||||
};
|
||||
|
||||
const removeDragState = function (state) {
|
||||
state.dragging = false;
|
||||
state.element = null;
|
||||
removeElement(state.ghost);
|
||||
};
|
||||
|
||||
const bindFakeDragEvents = function (editor) {
|
||||
const state = {};
|
||||
let 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);
|
||||
});
|
||||
};
|
||||
|
||||
const 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
|
||||
const realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null;
|
||||
|
||||
if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const init = function (editor) {
|
||||
bindFakeDragEvents(editor);
|
||||
blockIeDrop(editor);
|
||||
};
|
||||
|
||||
export default {
|
||||
init
|
||||
};
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import DOMUtils from 'tinymce/core/api/dom/DOMUtils';
|
||||
import { Option } from '@ephox/katamari';
|
||||
import Events from 'tinymce/core/api/Events';
|
||||
|
||||
const DOM = DOMUtils.DOM;
|
||||
|
||||
const restoreOriginalStyles = (editor: Editor) => {
|
||||
DOM.setStyle(editor.id, 'display', editor.orgDisplay);
|
||||
};
|
||||
|
||||
const safeDestroy = (x: any) => Option.from(x).each((x) => x.destroy());
|
||||
|
||||
const clearDomReferences = (editor: Editor) => {
|
||||
editor.contentAreaContainer = editor.formElement = editor.container = editor.editorContainer = null;
|
||||
editor.bodyElement = editor.contentDocument = editor.contentWindow = null;
|
||||
editor.iframeElement = editor.targetElm = null;
|
||||
|
||||
if (editor.selection) {
|
||||
editor.selection = editor.selection.win = editor.selection.dom = editor.selection.dom.doc = null;
|
||||
}
|
||||
};
|
||||
|
||||
const restoreForm = (editor: Editor) => {
|
||||
const form = editor.formElement as any;
|
||||
if (form) {
|
||||
if (form._mceOldSubmit) {
|
||||
form.submit = form._mceOldSubmit;
|
||||
form._mceOldSubmit = null;
|
||||
}
|
||||
|
||||
DOM.unbind(form, 'submit reset', editor.formEventDelegate);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (editor: Editor): void => {
|
||||
if (!editor.removed) {
|
||||
const { _selectionOverrides, editorUpload } = editor;
|
||||
const body = editor.getBody();
|
||||
const element = editor.getElement();
|
||||
if (body) {
|
||||
editor.save({ is_removing: true });
|
||||
}
|
||||
editor.removed = true;
|
||||
editor.unbindAllNativeEvents();
|
||||
|
||||
// Remove any hidden input
|
||||
if (editor.hasHiddenInput && element) {
|
||||
DOM.remove(element.nextSibling);
|
||||
}
|
||||
|
||||
if (!editor.inline && body) {
|
||||
restoreOriginalStyles(editor);
|
||||
}
|
||||
|
||||
Events.fireRemove(editor);
|
||||
|
||||
editor.editorManager.remove(editor);
|
||||
DOM.remove(editor.getContainer());
|
||||
|
||||
safeDestroy(_selectionOverrides);
|
||||
safeDestroy(editorUpload);
|
||||
|
||||
editor.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = (editor: Editor, automatic?: boolean): void => {
|
||||
const { selection, dom } = editor;
|
||||
|
||||
if (editor.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If user manually calls destroy and not remove
|
||||
// Users seems to have logic that calls destroy instead of remove
|
||||
if (!automatic && !editor.removed) {
|
||||
editor.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!automatic) {
|
||||
editor.editorManager.off('beforeunload', editor._beforeUnload);
|
||||
|
||||
// Manual destroy
|
||||
if (editor.theme && editor.theme.destroy) {
|
||||
editor.theme.destroy();
|
||||
}
|
||||
|
||||
safeDestroy(selection);
|
||||
safeDestroy(dom);
|
||||
}
|
||||
|
||||
restoreForm(editor);
|
||||
clearDomReferences(editor);
|
||||
|
||||
editor.destroyed = true;
|
||||
};
|
||||
|
||||
export {
|
||||
remove,
|
||||
destroy
|
||||
};
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr, Fun, Obj, Option, Strings, Struct, Type } from '@ephox/katamari';
|
||||
import { PlatformDetection } from '@ephox/sand';
|
||||
import Tools from './api/util/Tools';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
export interface ParamTypeMap {
|
||||
'hash': Record<string, string>;
|
||||
'string': string;
|
||||
'number': number;
|
||||
'boolean': boolean;
|
||||
'string[]': string[];
|
||||
'array': any[];
|
||||
}
|
||||
|
||||
const sectionResult = Struct.immutable('sections', 'settings');
|
||||
const detection = PlatformDetection.detect();
|
||||
const isTouch = detection.deviceType.isTouch();
|
||||
const mobilePlugins = [ 'lists', 'autolink', 'autosave' ];
|
||||
const defaultMobileSettings = { theme: 'mobile' };
|
||||
|
||||
const normalizePlugins = function (plugins) {
|
||||
const pluginNames = Type.isArray(plugins) ? plugins.join(' ') : plugins;
|
||||
const trimmedPlugins = Arr.map(Type.isString(pluginNames) ? pluginNames.split(' ') : [ ], Strings.trim);
|
||||
return Arr.filter(trimmedPlugins, function (item) {
|
||||
return item.length > 0;
|
||||
});
|
||||
};
|
||||
|
||||
const filterMobilePlugins = function (plugins) {
|
||||
return Arr.filter(plugins, Fun.curry(Arr.contains, mobilePlugins));
|
||||
};
|
||||
|
||||
const extractSections = function (keys, settings) {
|
||||
const result = Obj.bifilter(settings, function (value, key) {
|
||||
return Arr.contains(keys, key);
|
||||
});
|
||||
|
||||
return sectionResult(result.t, result.f);
|
||||
};
|
||||
|
||||
const getSection = function (sectionResult, name, defaults) {
|
||||
const sections = sectionResult.sections();
|
||||
const sectionSettings = sections.hasOwnProperty(name) ? sections[name] : { };
|
||||
return Tools.extend({}, defaults, sectionSettings);
|
||||
};
|
||||
|
||||
const hasSection = function (sectionResult, name) {
|
||||
return sectionResult.sections().hasOwnProperty(name);
|
||||
};
|
||||
|
||||
const getDefaultSettings = function (id, documentBaseUrl, editor) {
|
||||
return {
|
||||
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,
|
||||
render_ui: true,
|
||||
indentation: '40px',
|
||||
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,summary,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,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist',
|
||||
entity_encoding: 'named',
|
||||
url_converter: editor.convertURL,
|
||||
url_converter_scope: editor,
|
||||
ie7_compat: true
|
||||
};
|
||||
};
|
||||
|
||||
const getExternalPlugins = function (overrideSettings, settings) {
|
||||
const userDefinedExternalPlugins = settings.external_plugins ? settings.external_plugins : { };
|
||||
|
||||
if (overrideSettings && overrideSettings.external_plugins) {
|
||||
return Tools.extend({}, overrideSettings.external_plugins, userDefinedExternalPlugins);
|
||||
} else {
|
||||
return userDefinedExternalPlugins;
|
||||
}
|
||||
};
|
||||
|
||||
const combinePlugins = function (forcedPlugins, plugins) {
|
||||
return [].concat(normalizePlugins(forcedPlugins)).concat(normalizePlugins(plugins));
|
||||
};
|
||||
|
||||
const processPlugins = function (isTouchDevice, sectionResult, defaultOverrideSettings, settings) {
|
||||
const forcedPlugins = normalizePlugins(defaultOverrideSettings.forced_plugins);
|
||||
const plugins = normalizePlugins(settings.plugins);
|
||||
const platformPlugins = isTouchDevice && hasSection(sectionResult, 'mobile') ? filterMobilePlugins(plugins) : plugins;
|
||||
const combinedPlugins = combinePlugins(forcedPlugins, platformPlugins);
|
||||
|
||||
return Tools.extend(settings, {
|
||||
plugins: combinedPlugins.join(' ')
|
||||
});
|
||||
};
|
||||
|
||||
const isOnMobile = function (isTouchDevice, sectionResult) {
|
||||
const isInline = sectionResult.settings().inline; // We don't support mobile inline yet
|
||||
return isTouchDevice && hasSection(sectionResult, 'mobile') && !isInline;
|
||||
};
|
||||
|
||||
const combineSettings = function (isTouchDevice, defaultSettings, defaultOverrideSettings, settings) {
|
||||
const sectionResult = extractSections(['mobile'], settings);
|
||||
const extendedSettings = Tools.extend(
|
||||
// Default settings
|
||||
defaultSettings,
|
||||
|
||||
// tinymce.overrideDefaults settings
|
||||
defaultOverrideSettings,
|
||||
|
||||
// User settings
|
||||
sectionResult.settings(),
|
||||
|
||||
// Sections
|
||||
isOnMobile(isTouchDevice, sectionResult) ? getSection(sectionResult, 'mobile', defaultMobileSettings) : { },
|
||||
|
||||
// Forced settings
|
||||
{
|
||||
validate: true,
|
||||
content_editable: sectionResult.settings().inline,
|
||||
external_plugins: getExternalPlugins(defaultOverrideSettings, sectionResult.settings())
|
||||
}
|
||||
);
|
||||
|
||||
return processPlugins(isTouchDevice, sectionResult, defaultOverrideSettings, extendedSettings);
|
||||
};
|
||||
|
||||
const getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) {
|
||||
const defaultSettings = getDefaultSettings(id, documentBaseUrl, editor);
|
||||
return combineSettings(isTouch, defaultSettings, defaultOverrideSettings, settings);
|
||||
};
|
||||
|
||||
const get = function (editor, name) {
|
||||
return Option.from(editor.settings[name]);
|
||||
};
|
||||
|
||||
const getFiltered = (predicate: (x: any) => boolean, editor, name: string) => Option.from(editor.settings[name]).filter(predicate);
|
||||
|
||||
const getString = Fun.curry(getFiltered, Type.isString);
|
||||
|
||||
const getParamObject = (value: string) => {
|
||||
let output = {};
|
||||
|
||||
if (typeof value === 'string') {
|
||||
Arr.each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function (val: string) {
|
||||
const arr = val.split('=');
|
||||
|
||||
if (arr.length > 1) {
|
||||
output[Tools.trim(arr[0])] = Tools.trim(arr[1]);
|
||||
} else {
|
||||
output[Tools.trim(arr[0])] = Tools.trim(arr);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
output = value;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const isArrayOf = (p: (a: any) => boolean) => (a: any) => Type.isArray(a) && Arr.forall(a, p);
|
||||
|
||||
const getParam = (editor: Editor, name: string, defaultVal?: any, type?: string) => {
|
||||
const value = name in editor.settings ? editor.settings[name] : defaultVal;
|
||||
|
||||
if (type === 'hash') {
|
||||
return getParamObject(value);
|
||||
} else if (type === 'string') {
|
||||
return getFiltered(Type.isString, editor, name).getOr(defaultVal);
|
||||
} else if (type === 'number') {
|
||||
return getFiltered(Type.isNumber, editor, name).getOr(defaultVal);
|
||||
} else if (type === 'boolean') {
|
||||
return getFiltered(Type.isBoolean, editor, name).getOr(defaultVal);
|
||||
} else if (type === 'object') {
|
||||
return getFiltered(Type.isObject, editor, name).getOr(defaultVal);
|
||||
} else if (type === 'array') {
|
||||
return getFiltered(Type.isArray, editor, name).getOr(defaultVal);
|
||||
} else if (type === 'string[]') {
|
||||
return getFiltered(isArrayOf(Type.isString), editor, name).getOr(defaultVal);
|
||||
} else if (type === 'function') {
|
||||
return getFiltered(Type.isFunction, editor, name).getOr(defaultVal);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
getEditorSettings,
|
||||
get,
|
||||
getString,
|
||||
getParam,
|
||||
combineSettings
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Fun, Option } from '@ephox/katamari';
|
||||
import { Compare, Element, Css, Traverse } from '@ephox/sugar';
|
||||
|
||||
const getProp = function (propName, elm) {
|
||||
const rawElm = elm.dom();
|
||||
return rawElm[propName];
|
||||
};
|
||||
|
||||
const getComputedSizeProp = function (propName, elm) {
|
||||
return parseInt(Css.get(elm, propName), 10);
|
||||
};
|
||||
|
||||
const getClientWidth = Fun.curry(getProp, 'clientWidth');
|
||||
const getClientHeight = Fun.curry(getProp, 'clientHeight');
|
||||
const getMarginTop = Fun.curry(getComputedSizeProp, 'margin-top');
|
||||
const getMarginLeft = Fun.curry(getComputedSizeProp, 'margin-left');
|
||||
|
||||
const getBoundingClientRect = function (elm) {
|
||||
return elm.dom().getBoundingClientRect();
|
||||
};
|
||||
|
||||
const isInsideElementContentArea = function (bodyElm, clientX, clientY) {
|
||||
const clientWidth = getClientWidth(bodyElm);
|
||||
const clientHeight = getClientHeight(bodyElm);
|
||||
|
||||
return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight;
|
||||
};
|
||||
|
||||
const transpose = function (inline, elm, clientX, clientY) {
|
||||
const clientRect = getBoundingClientRect(elm);
|
||||
const deltaX = inline ? clientRect.left + elm.dom().clientLeft + getMarginLeft(elm) : 0;
|
||||
const deltaY = inline ? clientRect.top + elm.dom().clientTop + getMarginTop(elm) : 0;
|
||||
const x = clientX - deltaX;
|
||||
const y = clientY - deltaY;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
// Checks if the specified coordinate is within the visual content area excluding the scrollbars
|
||||
const isXYInContentArea = function (editor, clientX, clientY) {
|
||||
const bodyElm = Element.fromDom(editor.getBody());
|
||||
const targetElm = editor.inline ? bodyElm : Traverse.documentElement(bodyElm);
|
||||
const transposedPoint = transpose(editor.inline, targetElm, clientX, clientY);
|
||||
|
||||
return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y);
|
||||
};
|
||||
|
||||
const fromDomSafe = function (node) {
|
||||
return Option.from(node).map(Element.fromDom);
|
||||
};
|
||||
|
||||
const isEditorAttachedToDom = function (editor) {
|
||||
const rawContainer = editor.inline ? editor.getBody() : editor.getContentAreaContainer();
|
||||
|
||||
return fromDomSafe(rawContainer).map(function (container) {
|
||||
return Compare.contains(Traverse.owner(container), container);
|
||||
}).getOr(false);
|
||||
};
|
||||
|
||||
export default {
|
||||
isXYInContentArea,
|
||||
isEditorAttachedToDom
|
||||
};
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { AddOnManager } from './api/AddOnManager';
|
||||
import { window } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* Various error reporting helper functions.
|
||||
*
|
||||
* @class tinymce.ErrorReporter
|
||||
* @private
|
||||
*/
|
||||
|
||||
const PluginManager = AddOnManager.PluginManager;
|
||||
|
||||
const resolvePluginName = function (targetUrl, suffix) {
|
||||
for (const name in PluginManager.urls) {
|
||||
const matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js';
|
||||
if (matchUrl === targetUrl) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const pluginUrlToMessage = function (editor, url) {
|
||||
const plugin = resolvePluginName(url, editor.suffix);
|
||||
return plugin ?
|
||||
'Failed to load plugin: ' + plugin + ' from url ' + url :
|
||||
'Failed to load plugin url: ' + url;
|
||||
};
|
||||
|
||||
const displayNotification = function (editor, message) {
|
||||
editor.notificationManager.open({
|
||||
type: 'error',
|
||||
text: message
|
||||
});
|
||||
};
|
||||
|
||||
const displayError = function (editor, message) {
|
||||
if (editor._skinLoaded) {
|
||||
displayNotification(editor, message);
|
||||
} else {
|
||||
editor.on('SkinLoaded', function () {
|
||||
displayNotification(editor, message);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const uploadError = function (editor, message) {
|
||||
displayError(editor, 'Failed to upload image: ' + message);
|
||||
};
|
||||
|
||||
const pluginLoadError = function (editor, url) {
|
||||
displayError(editor, pluginUrlToMessage(editor, url));
|
||||
};
|
||||
|
||||
const initError = function (message, ...x: any[]) {
|
||||
const console = window.console;
|
||||
if (console) { // Skip test env
|
||||
if (console.error) { // tslint:disable-line:no-console
|
||||
console.error.apply(console, arguments); // tslint:disable-line:no-console
|
||||
} else {
|
||||
console.log.apply(console, arguments); // tslint:disable-line:no-console
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
pluginLoadError,
|
||||
uploadError,
|
||||
displayError,
|
||||
initError
|
||||
};
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr, Fun } from '@ephox/katamari';
|
||||
import { Element } from '@ephox/sugar';
|
||||
import Bookmarks from './bookmark/Bookmarks';
|
||||
import NodeType from './dom/NodeType';
|
||||
import Parents from './dom/Parents';
|
||||
import EditorFocus from './focus/EditorFocus';
|
||||
|
||||
/**
|
||||
* Makes sure that everything gets wrapped in paragraphs.
|
||||
*
|
||||
* @private
|
||||
* @class tinymce.ForceBlocks
|
||||
*/
|
||||
|
||||
const isBlockElement = function (blockElements, node) {
|
||||
return blockElements.hasOwnProperty(node.nodeName);
|
||||
};
|
||||
|
||||
const isValidTarget = function (blockElements, node) {
|
||||
if (NodeType.isText(node)) {
|
||||
return true;
|
||||
} else if (NodeType.isElement(node)) {
|
||||
return !isBlockElement(blockElements, node) && !Bookmarks.isBookmarkNode(node);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const hasBlockParent = function (blockElements, root, node) {
|
||||
return Arr.exists(Parents.parents(Element.fromDom(node), Element.fromDom(root)), function (elm) {
|
||||
return isBlockElement(blockElements, elm.dom());
|
||||
});
|
||||
};
|
||||
|
||||
// const is
|
||||
|
||||
const shouldRemoveTextNode = (blockElements, node) => {
|
||||
if (NodeType.isText(node)) {
|
||||
if (node.nodeValue.length === 0) {
|
||||
return true;
|
||||
} else if (/^\s+$/.test(node.nodeValue) && (!node.nextSibling || isBlockElement(blockElements, node.nextSibling))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const addRootBlocks = function (editor) {
|
||||
const settings = editor.settings, dom = editor.dom, selection = editor.selection;
|
||||
const schema = editor.schema, blockElements = schema.getBlockElements();
|
||||
let node = selection.getStart();
|
||||
const rootNode = editor.getBody();
|
||||
let rng;
|
||||
let startContainer, startOffset, endContainer, endOffset, rootBlockNode;
|
||||
let tempNode, wrapped, restoreSelection;
|
||||
let rootNodeName, forcedRootBlock;
|
||||
|
||||
forcedRootBlock = settings.forced_root_block;
|
||||
|
||||
if (!node || !NodeType.isElement(node) || !forcedRootBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
rootNodeName = rootNode.nodeName.toLowerCase();
|
||||
if (!schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase()) || hasBlockParent(blockElements, rootNode, node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current selection
|
||||
rng = selection.getRng();
|
||||
startContainer = rng.startContainer;
|
||||
startOffset = rng.startOffset;
|
||||
endContainer = rng.endContainer;
|
||||
endOffset = rng.endOffset;
|
||||
restoreSelection = EditorFocus.hasFocus(editor);
|
||||
|
||||
// Wrap non block elements and text nodes
|
||||
node = rootNode.firstChild;
|
||||
while (node) {
|
||||
if (isValidTarget(blockElements, node)) {
|
||||
// Remove empty text nodes and nodes containing only whitespace
|
||||
if (shouldRemoveTextNode(blockElements, node)) {
|
||||
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) {
|
||||
rng.setStart(startContainer, startOffset);
|
||||
rng.setEnd(endContainer, endOffset);
|
||||
selection.setRng(rng);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
};
|
||||
|
||||
const setup = function (editor) {
|
||||
if (editor.settings.forced_root_block) {
|
||||
editor.on('NodeChange', Fun.curry(addRootBlocks, editor));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setup
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Mode.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2018 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { Element, Class } from '@ephox/sugar';
|
||||
import Events from 'tinymce/core/api/Events';
|
||||
|
||||
const enum EditorMode {
|
||||
Design = 'design',
|
||||
ReadOnly = 'readonly'
|
||||
}
|
||||
|
||||
const setEditorCommandState = (editor: Editor, cmd: string, state: boolean) => {
|
||||
try {
|
||||
editor.getDoc().execCommand(cmd, false, state);
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
};
|
||||
|
||||
const toggleClass = (elm, cls, state: boolean) => {
|
||||
if (Class.has(elm, cls) && state === false) {
|
||||
Class.remove(elm, cls);
|
||||
} else if (state) {
|
||||
Class.add(elm, cls);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleReadOnly = (editor: Editor, state: boolean) => {
|
||||
toggleClass(Element.fromDom(editor.getBody()), 'mce-content-readonly', state);
|
||||
|
||||
if (state) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
const setMode = (editor: Editor, mode: EditorMode) => {
|
||||
if (mode === getMode(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editor.initialized) {
|
||||
toggleReadOnly(editor, mode === EditorMode.ReadOnly);
|
||||
} else {
|
||||
editor.on('init', function () {
|
||||
toggleReadOnly(editor, mode === EditorMode.ReadOnly);
|
||||
});
|
||||
}
|
||||
|
||||
Events.fireSwitchMode(editor, mode);
|
||||
};
|
||||
|
||||
const getMode = (editor: Editor) => editor.readonly ? EditorMode.ReadOnly : EditorMode.Design;
|
||||
|
||||
const isReadOnly = (editor: Editor) => editor.readonly === true;
|
||||
|
||||
export {
|
||||
EditorMode,
|
||||
setMode,
|
||||
getMode,
|
||||
isReadOnly
|
||||
};
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Env from './api/Env';
|
||||
import RangeCompare from './selection/RangeCompare';
|
||||
import Delay from './api/util/Delay';
|
||||
import { hasAnyRanges } from 'tinymce/core/selection/SelectionUtils';
|
||||
|
||||
/**
|
||||
* This class handles the nodechange event dispatching both manual and through selection change events.
|
||||
*
|
||||
* @class tinymce.NodeChange
|
||||
* @private
|
||||
*/
|
||||
|
||||
export default function (editor) {
|
||||
let 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.
|
||||
*/
|
||||
const isSameElementPath = function (startElm) {
|
||||
let 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) {
|
||||
let 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' || !RangeCompare.isEq(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 () {
|
||||
const startElm = editor.selection.getStart(true);
|
||||
|
||||
// When focusout from after cef element to other input element the startelm can be undefined.
|
||||
// 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 (!startElm || (!Env.range && editor.selection.isCollapsed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAnyRanges(editor) && !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() && hasAnyRanges(editor)) {
|
||||
// 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) {
|
||||
const selection = editor.selection;
|
||||
let 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr } from '@ephox/katamari';
|
||||
import { Remove, Element as SugarElement, Attr, SelectorFilter, SelectorFind } from '@ephox/sugar';
|
||||
import DragDropOverrides from './DragDropOverrides';
|
||||
import EditorView from './EditorView';
|
||||
import Env from './api/Env';
|
||||
import * as CaretContainer from './caret/CaretContainer';
|
||||
import CaretPosition from './caret/CaretPosition';
|
||||
import * as CaretUtils from './caret/CaretUtils';
|
||||
import { CaretWalker } from './caret/CaretWalker';
|
||||
import * as LineUtils from './caret/LineUtils';
|
||||
import NodeType from './dom/NodeType';
|
||||
import RangePoint from './dom/RangePoint';
|
||||
import CefFocus from './focus/CefFocus';
|
||||
import * as CefUtils from './keyboard/CefUtils';
|
||||
import VK from './api/util/VK';
|
||||
import { FakeCaret, isFakeCaretTarget } from './caret/FakeCaret';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import EditorFocus from 'tinymce/core/focus/EditorFocus';
|
||||
import { Range, Element, Node, HTMLElement, MouseEvent } from '@ephox/dom-globals';
|
||||
|
||||
const isContentEditableTrue = NodeType.isContentEditableTrue;
|
||||
const isContentEditableFalse = NodeType.isContentEditableFalse;
|
||||
const isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse;
|
||||
const isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse;
|
||||
|
||||
interface SelectionOverrides {
|
||||
showCaret: (direction: number, node: Element, before: boolean, scrollIntoView?: boolean) => Range;
|
||||
showBlockCaretContainer: (blockCaretContainer: Element) => void;
|
||||
hideFakeCaret: () => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
const getContentEditableRoot = (editor: Editor, node: Node): Node => {
|
||||
const root = editor.getBody();
|
||||
|
||||
while (node && node !== root) {
|
||||
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const SelectionOverrides = function (editor: Editor): SelectionOverrides {
|
||||
const isBlock = function (node) {
|
||||
return editor.dom.isBlock(node);
|
||||
};
|
||||
|
||||
const rootNode = editor.getBody();
|
||||
const fakeCaret = FakeCaret(editor.getBody(), isBlock, () => EditorFocus.hasFocus(editor));
|
||||
const realSelectionId = 'sel-' + editor.dom.uniqueId();
|
||||
let selectedContentEditableNode;
|
||||
|
||||
const isFakeSelectionElement = function (elm) {
|
||||
return editor.dom.hasClass(elm, 'mce-offscreen-selection');
|
||||
};
|
||||
|
||||
const getRealSelectionElement = function () {
|
||||
const container = editor.dom.get(realSelectionId);
|
||||
return container ? container.getElementsByTagName('*')[0] as HTMLElement : container;
|
||||
};
|
||||
|
||||
const setRange = function (range: Range) {
|
||||
// console.log('setRange', range);
|
||||
if (range) {
|
||||
editor.selection.setRng(range);
|
||||
}
|
||||
};
|
||||
|
||||
const getRange = function () {
|
||||
return editor.selection.getRng();
|
||||
};
|
||||
|
||||
const showCaret = (direction: number, node: Element, before: boolean, scrollIntoView: boolean = true): Range => {
|
||||
let e;
|
||||
|
||||
e = editor.fire('ShowCaret', {
|
||||
target: node,
|
||||
direction,
|
||||
before
|
||||
});
|
||||
|
||||
if (e.isDefaultPrevented()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scrollIntoView) {
|
||||
editor.selection.scrollIntoView(node, direction === -1);
|
||||
}
|
||||
|
||||
return fakeCaret.show(before, node);
|
||||
};
|
||||
|
||||
const getNormalizedRangeEndPoint = function (direction: number, range: Range): CaretPosition {
|
||||
range = CaretUtils.normalizeRange(direction, rootNode, range);
|
||||
|
||||
if (direction === -1) {
|
||||
return CaretPosition.fromRangeStart(range);
|
||||
}
|
||||
|
||||
return CaretPosition.fromRangeEnd(range);
|
||||
};
|
||||
|
||||
const showBlockCaretContainer = function (blockCaretContainer: HTMLElement) {
|
||||
if (blockCaretContainer.hasAttribute('data-mce-caret')) {
|
||||
CaretContainer.showCaretContainerBlock(blockCaretContainer);
|
||||
setRange(getRange()); // Removes control rect on IE
|
||||
editor.selection.scrollIntoView(blockCaretContainer[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const registerEvents = function () {
|
||||
// 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) {
|
||||
const range = getRange();
|
||||
|
||||
if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) {
|
||||
setRange(CefUtils.renderCaretAtRange(editor, range, false));
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('click', function (e) {
|
||||
let contentEditableRoot;
|
||||
|
||||
contentEditableRoot = getContentEditableRoot(editor, 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();
|
||||
});
|
||||
|
||||
editor.on('ResizeWindow FullscreenStateChanged', () => fakeCaret.reposition());
|
||||
|
||||
const handleTouchSelect = function (editor) {
|
||||
let moved = false;
|
||||
|
||||
editor.on('touchstart', function () {
|
||||
moved = false;
|
||||
});
|
||||
|
||||
editor.on('touchmove', function () {
|
||||
moved = true;
|
||||
});
|
||||
|
||||
editor.on('touchend', function (e) {
|
||||
const contentEditableRoot = getContentEditableRoot(editor, e.target);
|
||||
|
||||
if (isContentEditableFalse(contentEditableRoot)) {
|
||||
if (!moved) {
|
||||
e.preventDefault();
|
||||
setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const hasNormalCaretPosition = function (elm) {
|
||||
const caretWalker = CaretWalker(elm);
|
||||
|
||||
if (!elm.firstChild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startPos = CaretPosition.before(elm.firstChild);
|
||||
const newPos = caretWalker.next(startPos);
|
||||
|
||||
return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos);
|
||||
};
|
||||
|
||||
const isInSameBlock = function (node1, node2) {
|
||||
const block1 = editor.dom.getParent(node1, editor.dom.isBlock);
|
||||
const 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
|
||||
const hasBetterMouseTarget = function (targetNode, caretNode) {
|
||||
const targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock);
|
||||
const caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock);
|
||||
|
||||
// Click inside the suggested caret element
|
||||
if (targetBlock && editor.dom.isChildOf(targetBlock, caretBlock) && isContentEditableFalse(getContentEditableRoot(editor, targetBlock)) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock);
|
||||
};
|
||||
|
||||
handleTouchSelect(editor);
|
||||
|
||||
editor.on('mousedown', (e: MouseEvent) => {
|
||||
let contentEditableRoot;
|
||||
const targetElm = e.target as Element;
|
||||
|
||||
if (targetElm !== rootNode && targetElm.nodeName !== 'HTML' && !editor.dom.isChildOf(targetElm, rootNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentEditableRoot = getContentEditableRoot(editor, targetElm);
|
||||
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())) {
|
||||
hideFakeCaret();
|
||||
editor.selection.placeCaretAt(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
} else if (isFakeCaretTarget(targetElm) === false) {
|
||||
// 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();
|
||||
|
||||
const caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY);
|
||||
if (caretInfo) {
|
||||
if (!hasBetterMouseTarget(e.target, caretInfo.node)) {
|
||||
e.preventDefault();
|
||||
const range = showCaret(1, caretInfo.node as HTMLElement, caretInfo.before, false);
|
||||
editor.getBody().focus();
|
||||
setRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
let 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) {
|
||||
let rng;
|
||||
|
||||
rng = setContentEditableSelection(e.range, e.forward);
|
||||
if (rng) {
|
||||
e.range = rng;
|
||||
}
|
||||
});
|
||||
|
||||
const isPasteBin = (node: HTMLElement): boolean => {
|
||||
return node.id === 'mcepastebin';
|
||||
};
|
||||
|
||||
editor.on('AfterSetSelectionRange', function (e) {
|
||||
const rng = e.range;
|
||||
|
||||
if (!isRangeInCaretContainer(rng) && !isPasteBin(rng.startContainer.parentNode)) {
|
||||
hideFakeCaret();
|
||||
}
|
||||
|
||||
if (!isFakeSelectionElement(rng.startContainer.parentNode)) {
|
||||
removeContentEditableSelection();
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('copy', function (e) {
|
||||
const 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) {
|
||||
const realSelectionElement = getRealSelectionElement();
|
||||
if (realSelectionElement) {
|
||||
e.preventDefault();
|
||||
clipboardData.clearData();
|
||||
clipboardData.setData('text/html', realSelectionElement.outerHTML);
|
||||
clipboardData.setData('text/plain', realSelectionElement.outerText);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DragDropOverrides.init(editor);
|
||||
CefFocus.setup(editor);
|
||||
};
|
||||
|
||||
const addCss = function () {
|
||||
const 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;' +
|
||||
'}'
|
||||
);
|
||||
};
|
||||
|
||||
const isWithinCaretContainer = function (node: Node) {
|
||||
return (
|
||||
CaretContainer.isCaretContainer(node) ||
|
||||
CaretContainer.startsWithCaretContainer(node) ||
|
||||
CaretContainer.endsWithCaretContainer(node)
|
||||
);
|
||||
};
|
||||
|
||||
const isRangeInCaretContainer = function (rng: Range) {
|
||||
return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer);
|
||||
};
|
||||
|
||||
const setContentEditableSelection = function (range: Range, forward?: boolean) {
|
||||
let node;
|
||||
const $ = editor.$;
|
||||
const dom = editor.dom;
|
||||
let $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 (isFakeCaretTarget(caretPosition.getNode(true))) {
|
||||
return showCaret(-1, caretPosition.getNode(true), false, false);
|
||||
}
|
||||
|
||||
if (isFakeCaretTarget(caretPosition.getNode())) {
|
||||
return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd(), false);
|
||||
}
|
||||
} else {
|
||||
caretPosition = getNormalizedRangeEndPoint(1, range);
|
||||
|
||||
if (isFakeCaretTarget(caretPosition.getNode())) {
|
||||
return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd(), false);
|
||||
}
|
||||
|
||||
if (isFakeCaretTarget(caretPosition.getNode(true))) {
|
||||
return showCaret(1, caretPosition.getNode(true), false, 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 });
|
||||
if (e.isDefaultPrevented()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$realSelectionContainer = SelectorFind.descendant(SugarElement.fromDom(editor.getBody()), '#' + realSelectionId).fold(
|
||||
function () {
|
||||
return $([]);
|
||||
},
|
||||
function (elm) {
|
||||
return $([elm.dom()]);
|
||||
}
|
||||
);
|
||||
|
||||
targetClone = e.targetClone;
|
||||
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);
|
||||
|
||||
Arr.each(SelectorFilter.descendants(SugarElement.fromDom(editor.getBody()), '*[data-mce-selected]'), function (elm) {
|
||||
Attr.remove(elm, 'data-mce-selected');
|
||||
});
|
||||
|
||||
node.setAttribute('data-mce-selected', '1');
|
||||
selectedContentEditableNode = node;
|
||||
hideFakeCaret();
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
const removeContentEditableSelection = function () {
|
||||
if (selectedContentEditableNode) {
|
||||
selectedContentEditableNode.removeAttribute('data-mce-selected');
|
||||
SelectorFind.descendant(SugarElement.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove);
|
||||
selectedContentEditableNode = null;
|
||||
}
|
||||
|
||||
SelectorFind.descendant(SugarElement.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove);
|
||||
selectedContentEditableNode = null;
|
||||
};
|
||||
|
||||
const destroy = function () {
|
||||
fakeCaret.destroy();
|
||||
selectedContentEditableNode = null;
|
||||
};
|
||||
|
||||
const hideFakeCaret = function () {
|
||||
fakeCaret.hide();
|
||||
};
|
||||
|
||||
if (Env.ceFalse) {
|
||||
registerEvents();
|
||||
addCss();
|
||||
}
|
||||
|
||||
return {
|
||||
showCaret,
|
||||
showBlockCaretContainer,
|
||||
hideFakeCaret,
|
||||
destroy
|
||||
};
|
||||
};
|
||||
|
||||
export default SelectionOverrides;
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { AnnotationsRegistry } from 'tinymce/core/annotate/AnnotationsRegistry';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { Throttler, Option, Arr, Cell, Obj } from '@ephox/katamari';
|
||||
import { identify } from './Identification';
|
||||
|
||||
export interface AnnotationChanges {
|
||||
addListener: (name: string, f: AnnotationListener) => void;
|
||||
}
|
||||
|
||||
export type AnnotationListener = (state: boolean, name: string, data?: { uid: string, nodes: any[] }) => void;
|
||||
|
||||
export interface AnnotationListenerData {
|
||||
listeners: AnnotationListener[];
|
||||
previous: Cell<Option<string>>;
|
||||
}
|
||||
|
||||
export type AnnotationListenerMap = Record<string, AnnotationListenerData>;
|
||||
|
||||
const setup = (editor: Editor, registry: AnnotationsRegistry): AnnotationChanges => {
|
||||
const changeCallbacks = Cell<AnnotationListenerMap>({ });
|
||||
|
||||
const initData = (): AnnotationListenerData => ({
|
||||
listeners: [ ],
|
||||
previous: Cell(Option.none())
|
||||
});
|
||||
|
||||
const withCallbacks = (name: string, f: (listeners: AnnotationListenerData) => void) => {
|
||||
updateCallbacks(name, (data) => {
|
||||
f(data);
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
const updateCallbacks = (name: string, f: (inputData: AnnotationListenerData) => AnnotationListenerData) => {
|
||||
const callbackMap = changeCallbacks.get();
|
||||
const data = callbackMap.hasOwnProperty(name) ? callbackMap[name] : initData();
|
||||
const outputData = f(data);
|
||||
callbackMap[name] = outputData;
|
||||
changeCallbacks.set(callbackMap);
|
||||
};
|
||||
|
||||
const fireCallbacks = (name: string, uid: string, elements: any[]): void => {
|
||||
withCallbacks(name, (data) => {
|
||||
Arr.each(data.listeners, (f) => f(true, name, {
|
||||
uid,
|
||||
nodes: Arr.map(elements, (elem) => elem.dom())
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const fireNoAnnotation = (name: string): void => {
|
||||
withCallbacks(name, (data) => {
|
||||
Arr.each(data.listeners, (f) => f(false, name));
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE: Runs in alphabetical order.
|
||||
const onNodeChange = Throttler.last(() => {
|
||||
const callbackMap = changeCallbacks.get();
|
||||
const annotations = Arr.sort(Obj.keys(callbackMap));
|
||||
Arr.each(annotations, (name) => {
|
||||
updateCallbacks(name, (data) => {
|
||||
const prev = data.previous.get();
|
||||
identify(editor, Option.some(name)).fold(
|
||||
() => {
|
||||
if (prev.isSome()) {
|
||||
// Changed from something to nothing.
|
||||
fireNoAnnotation(name);
|
||||
data.previous.set(Option.none());
|
||||
}
|
||||
},
|
||||
({ uid, name, elements }) => {
|
||||
// Changed from a different annotation (or nothing)
|
||||
if (! prev.is(uid)) {
|
||||
fireCallbacks(name, uid, elements);
|
||||
data.previous.set(Option.some(uid));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
previous: data.previous,
|
||||
listeners: data.listeners
|
||||
};
|
||||
});
|
||||
});
|
||||
}, 30);
|
||||
|
||||
editor.on('remove', () => {
|
||||
onNodeChange.cancel();
|
||||
});
|
||||
|
||||
editor.on('nodeChange', () => {
|
||||
onNodeChange.throttle();
|
||||
});
|
||||
|
||||
const addListener = (name: string, f: AnnotationListener): void => {
|
||||
updateCallbacks(name, (data) => {
|
||||
return {
|
||||
previous: data.previous,
|
||||
listeners: data.listeners.concat([ f ])
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
addListener
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
setup
|
||||
};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { Unicode } from '@ephox/katamari';
|
||||
import { Node, Text, Traverse } from '@ephox/sugar';
|
||||
import { isCaretNode } from 'tinymce/core/fmt/FormatContainer';
|
||||
import FormatUtils from '../fmt/FormatUtils';
|
||||
import { isAnnotation } from './Identification';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
export const enum ChildContext {
|
||||
// Was previously used for br and zero width cursors. Keep as a state
|
||||
// because we'll probably want to reinstate it later.
|
||||
Skipping = 'skipping',
|
||||
Existing = 'existing',
|
||||
InvalidChild = 'invalid-child',
|
||||
Caret = 'caret',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
const isZeroWidth = (elem): boolean => {
|
||||
// TODO: I believe this is the same cursor used in tinymce (Unicode.zeroWidth)?
|
||||
return Node.isText(elem) && Text.get(elem) === Unicode.zeroWidth();
|
||||
};
|
||||
|
||||
const context = (editor: Editor, elem: any, wrapName: string, nodeName: string): ChildContext => {
|
||||
return Traverse.parent(elem).fold(
|
||||
() => ChildContext.Skipping,
|
||||
|
||||
(parent) => {
|
||||
// We used to skip these, but given that they might be representing empty paragraphs, it probably
|
||||
// makes sense to treat them just like text nodes
|
||||
if (nodeName === 'br' || isZeroWidth(elem)) {
|
||||
return ChildContext.Valid;
|
||||
} else if (isAnnotation(elem)) {
|
||||
return ChildContext.Existing;
|
||||
} else if (isCaretNode(elem)) {
|
||||
return ChildContext.Caret;
|
||||
} else if (!FormatUtils.isValid(editor, wrapName, nodeName) || !FormatUtils.isValid(editor, Node.name(parent), wrapName)) {
|
||||
return ChildContext.InvalidChild;
|
||||
} else {
|
||||
return ChildContext.Valid;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
context
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Option, Arr } from '@ephox/katamari';
|
||||
|
||||
import { AnnotationsRegistry, AnnotatorSettings } from './AnnotationsRegistry';
|
||||
import * as Markings from './Markings';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
const setup = (editor: Editor, registry: AnnotationsRegistry): void => {
|
||||
const identifyParserNode = (span): Option<AnnotatorSettings> => {
|
||||
const optAnnotation = Option.from(span.attributes.map[Markings.dataAnnotation()]) as Option<string>;
|
||||
return optAnnotation.bind(registry.lookup);
|
||||
};
|
||||
|
||||
editor.on('init', () => {
|
||||
editor.serializer.addNodeFilter('span', (spans) => {
|
||||
Arr.each(spans, (span) => {
|
||||
identifyParserNode(span).each((settings) => {
|
||||
if (settings.persistent === false) { span.unwrap(); }
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
setup
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { Decorator } from './Wrapping';
|
||||
import { Option } from '@ephox/katamari';
|
||||
|
||||
export interface AnnotatorSettings {
|
||||
decorate: Decorator;
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
export interface AnnotationsRegistry {
|
||||
register: (name: string, settings: AnnotatorSettings) => void;
|
||||
lookup: (name: string) => Option<AnnotatorSettings>;
|
||||
}
|
||||
|
||||
const create = (): AnnotationsRegistry => {
|
||||
const annotations = { };
|
||||
|
||||
const register = (name: string, settings: AnnotatorSettings): void => {
|
||||
annotations[name] = {
|
||||
name,
|
||||
settings
|
||||
};
|
||||
};
|
||||
|
||||
const lookup = (name: string): Option<AnnotatorSettings> => {
|
||||
return annotations.hasOwnProperty(name) ? Option.from(annotations[name]).map((a) => a.settings) : Option.none();
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
lookup
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
create
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { Arr, Option } from '@ephox/katamari';
|
||||
import { Attr, Class, Compare, Element, Node, SelectorFilter, SelectorFind, Traverse } from '@ephox/sugar';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
import * as Markings from './Markings';
|
||||
|
||||
// Given the current editor selection, identify the uid of any current
|
||||
// annotation
|
||||
const identify = (editor: Editor, annotationName: Option<string>): Option<{uid: string, name: string, elements: any[]}> => {
|
||||
const rng = editor.selection.getRng();
|
||||
|
||||
const start = Element.fromDom(rng.startContainer);
|
||||
const root = Element.fromDom(editor.getBody());
|
||||
|
||||
const selector = annotationName.fold(
|
||||
() => '.' + Markings.annotation(),
|
||||
(an) => `[${Markings.dataAnnotation()}="${an}"]`
|
||||
);
|
||||
|
||||
const newStart = Traverse.child(start, rng.startOffset).getOr(start);
|
||||
const closest = SelectorFind.closest(newStart, selector, (n) => {
|
||||
return Compare.eq(n, root);
|
||||
});
|
||||
|
||||
const getAttr = (c, property: string): Option<any> => {
|
||||
if (Attr.has(c, property)) {
|
||||
return Option.some(Attr.get(c, property));
|
||||
} else {
|
||||
return Option.none();
|
||||
}
|
||||
};
|
||||
|
||||
return closest.bind((c) => {
|
||||
return getAttr(c, `${Markings.dataAnnotationId()}`).bind((uid) =>
|
||||
getAttr(c, `${Markings.dataAnnotation()}`).map((name) => {
|
||||
const elements = findMarkers(editor, uid);
|
||||
return {
|
||||
uid,
|
||||
name,
|
||||
elements
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isAnnotation = (elem: any): boolean => {
|
||||
return Node.isElement(elem) && Class.has(elem, Markings.annotation());
|
||||
};
|
||||
|
||||
const findMarkers = (editor: Editor, uid: string): any[] => {
|
||||
const body = Element.fromDom(editor.getBody());
|
||||
return SelectorFilter.descendants(body, `[${Markings.dataAnnotationId()}="${uid}"]`);
|
||||
};
|
||||
|
||||
const findAll = (editor: Editor, name: string): Record<string, Element[]> => {
|
||||
const body = Element.fromDom(editor.getBody());
|
||||
const markers = SelectorFilter.descendants(body, `[${Markings.dataAnnotation()}="${name}"]`);
|
||||
const directory: Record<string, Element[]> = { };
|
||||
Arr.each(markers, (m) => {
|
||||
const uid = Attr.get(m, Markings.dataAnnotationId());
|
||||
const nodesAlready = directory.hasOwnProperty(uid) ? directory[uid] : [ ];
|
||||
directory[uid] = nodesAlready.concat([ m ]);
|
||||
});
|
||||
return directory;
|
||||
};
|
||||
|
||||
export {
|
||||
identify,
|
||||
isAnnotation,
|
||||
findAll
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Fun } from '@ephox/katamari';
|
||||
|
||||
const annotation = Fun.constant('mce-annotation');
|
||||
|
||||
const dataAnnotation = Fun.constant('data-mce-annotation');
|
||||
const dataAnnotationId = Fun.constant('data-mce-annotation-uid');
|
||||
|
||||
export {
|
||||
annotation,
|
||||
dataAnnotation,
|
||||
dataAnnotationId
|
||||
};
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { Range, Document } from '@ephox/dom-globals';
|
||||
import { Arr, Cell, Id, Option } from '@ephox/katamari';
|
||||
import { Attr, Class, Classes, Element, Insert, Node, Replication, Traverse, Html } from '@ephox/sugar';
|
||||
import { AnnotatorSettings } from 'tinymce/core/annotate/AnnotationsRegistry';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import GetBookmark from 'tinymce/core/bookmark/GetBookmark';
|
||||
import ExpandRange from 'tinymce/core/fmt/ExpandRange';
|
||||
|
||||
import RangeWalk from '../selection/RangeWalk';
|
||||
import { ChildContext, context } from './AnnotationContext';
|
||||
import * as Markings from './Markings';
|
||||
|
||||
export type DecoratorData = Record<string, any>;
|
||||
|
||||
export type Decorator = (
|
||||
uid: string,
|
||||
data: DecoratorData
|
||||
) => {
|
||||
attributes?: { },
|
||||
classes?: string[]
|
||||
};
|
||||
|
||||
// We want it to apply to trailing spaces (like removeFormat does) when dealing with non breaking spaces. There
|
||||
// will likely be other edge cases as well.
|
||||
const shouldApplyToTrailingSpaces = (rng: Range) => {
|
||||
return rng.startContainer.nodeType === 3 && rng.startContainer.nodeValue.length >= rng.startOffset && rng.startContainer.nodeValue[rng.startOffset] === '\u00A0';
|
||||
};
|
||||
|
||||
const applyWordGrab = (editor: Editor, rng: Range): void => {
|
||||
const r = ExpandRange.expandRng(editor, rng, [{ inline: true }], shouldApplyToTrailingSpaces(rng));
|
||||
rng.setStart(r.startContainer, r.startOffset);
|
||||
rng.setEnd(r.endContainer, r.endOffset);
|
||||
editor.selection.setRng(rng);
|
||||
};
|
||||
|
||||
const makeAnnotation = (eDoc: Document, { uid = Id.generate('mce-annotation'), ...data }, annotationName: string, decorate: Decorator): Element => {
|
||||
const master = Element.fromTag('span', eDoc);
|
||||
Class.add(master, Markings.annotation());
|
||||
Attr.set(master, `${Markings.dataAnnotationId()}`, uid);
|
||||
Attr.set(master, `${Markings.dataAnnotation()}`, annotationName);
|
||||
|
||||
const { attributes = { }, classes = [ ] } = decorate(uid, data);
|
||||
Attr.setAll(master, attributes);
|
||||
Classes.add(master, classes);
|
||||
return master;
|
||||
};
|
||||
|
||||
const annotate = (editor: Editor, rng: Range, annotationName: string, decorate: Decorator, data): any[] => {
|
||||
// Setup all the wrappers that are going to be used.
|
||||
const newWrappers = [ ];
|
||||
|
||||
// Setup the spans for the comments
|
||||
const master = makeAnnotation(editor.getDoc(), data, annotationName, decorate);
|
||||
|
||||
// Set the current wrapping element
|
||||
const wrapper = Cell(Option.none());
|
||||
|
||||
// Clear the current wrapping element, so that subsequent calls to
|
||||
// getOrOpenWrapper spawns a new one.
|
||||
const finishWrapper = () => {
|
||||
wrapper.set(Option.none());
|
||||
};
|
||||
|
||||
// Get the existing wrapper, or spawn a new one.
|
||||
const getOrOpenWrapper = () => {
|
||||
return wrapper.get().getOrThunk(() => {
|
||||
const nu = Replication.shallow(master);
|
||||
newWrappers.push(nu);
|
||||
wrapper.set(Option.some(nu));
|
||||
return nu;
|
||||
});
|
||||
};
|
||||
|
||||
const processElements = (elems) => {
|
||||
Arr.each(elems, processElement);
|
||||
};
|
||||
|
||||
const processElement = (elem) => {
|
||||
const ctx = context(editor, elem, 'span', Node.name(elem));
|
||||
|
||||
switch (ctx) {
|
||||
case ChildContext.InvalidChild: {
|
||||
finishWrapper();
|
||||
const children = Traverse.children(elem);
|
||||
processElements(children);
|
||||
finishWrapper();
|
||||
break;
|
||||
}
|
||||
|
||||
case ChildContext.Valid: {
|
||||
const w = getOrOpenWrapper();
|
||||
Insert.wrap(elem, w);
|
||||
break;
|
||||
}
|
||||
|
||||
// INVESTIGATE: Are these sensible things to do?
|
||||
case ChildContext.Skipping:
|
||||
case ChildContext.Existing:
|
||||
case ChildContext.Caret: {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processNodes = (nodes) => {
|
||||
const elems = Arr.map(nodes, Element.fromDom);
|
||||
processElements(elems);
|
||||
};
|
||||
|
||||
RangeWalk.walk(editor.dom, rng, (nodes) => {
|
||||
finishWrapper();
|
||||
processNodes(nodes);
|
||||
});
|
||||
|
||||
return newWrappers;
|
||||
};
|
||||
|
||||
const annotateWithBookmark = (editor: Editor, name: string, settings: AnnotatorSettings, data: { }): void => {
|
||||
editor.undoManager.transact(() => {
|
||||
const initialRng = editor.selection.getRng();
|
||||
if (initialRng.collapsed) {
|
||||
applyWordGrab(editor, initialRng);
|
||||
}
|
||||
|
||||
// Even after applying word grab, we could not find a selection. Therefore,
|
||||
// just make a wrapper and insert it at the current cursor
|
||||
if (editor.selection.getRng().collapsed) {
|
||||
const wrapper = makeAnnotation(editor.getDoc(), data, name, settings.decorate);
|
||||
// Put something visible in the marker
|
||||
Html.set(wrapper, '\u00A0');
|
||||
editor.selection.getRng().insertNode(wrapper.dom());
|
||||
editor.selection.select(wrapper.dom());
|
||||
} else {
|
||||
// The bookmark is responsible for splitting the nodes beforehand at the selection points
|
||||
// The "false" here means a zero width cursor is NOT put in the bookmark. It seems to be required
|
||||
// to stop an empty paragraph splitting into two paragraphs. Probably a better way exists.
|
||||
const bookmark = GetBookmark.getPersistentBookmark(editor.selection, false);
|
||||
const rng = editor.selection.getRng();
|
||||
annotate(editor, rng, name, settings.decorate, data);
|
||||
editor.selection.moveToBookmark(bookmark);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
annotateWithBookmark
|
||||
};
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr } from '@ephox/katamari';
|
||||
import ScriptLoader from './dom/ScriptLoader';
|
||||
import Tools from './util/Tools';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
/**
|
||||
* This class handles the loading of themes/plugins or other add-ons and their language packs.
|
||||
*
|
||||
* @class tinymce.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'
|
||||
* }]
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
|
||||
const each = Tools.each;
|
||||
|
||||
export interface UrlObject { prefix: string; resource: string; suffix: string; }
|
||||
|
||||
export interface AddOnManager {
|
||||
items: any[];
|
||||
urls: Record<string, string>;
|
||||
lookup: {};
|
||||
_listeners: any[];
|
||||
get: (name: string) => any;
|
||||
dependencies: (name: string) => any;
|
||||
requireLangPack: (name: string, languages: string) => void;
|
||||
add: (id: string, addOn: (editor: Editor, url: string) => any, dependencies?: any) => (editor: Editor, url: string) => any;
|
||||
remove: (name: string) => void;
|
||||
createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;
|
||||
addComponents: (pluginName: string, scripts: string[]) => void;
|
||||
load: (name: string, addOnUrl: string | UrlObject, success?: any, scope?: any, failure?: any) => void;
|
||||
waitFor: (name: string, callback: (...x: any[]) => any) => void;
|
||||
}
|
||||
|
||||
export function AddOnManager(): AddOnManager {
|
||||
const items = [];
|
||||
const urls: Record<string, string> = {};
|
||||
const lookup = {};
|
||||
let _listeners = [];
|
||||
|
||||
const get = (name: string) => {
|
||||
if (lookup[name]) {
|
||||
return lookup[name].instance;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const dependencies = (name: string) => {
|
||||
let result;
|
||||
|
||||
if (lookup[name]) {
|
||||
result = lookup[name].dependencies;
|
||||
}
|
||||
|
||||
return result || [];
|
||||
};
|
||||
|
||||
const requireLangPack = (name: string, languages: string) => {
|
||||
let 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(urls[name] + '/langs/' + language + '.js');
|
||||
}
|
||||
};
|
||||
|
||||
const add = (id: string, addOn: (editor: Editor, url: string) => any, dependencies?) => {
|
||||
items.push(addOn);
|
||||
lookup[id] = { instance: addOn, dependencies };
|
||||
const result = Arr.partition(_listeners, function (listener) {
|
||||
return listener.name === id;
|
||||
});
|
||||
|
||||
_listeners = result.fail;
|
||||
|
||||
each(result.pass, function (listener) {
|
||||
listener.callback();
|
||||
});
|
||||
|
||||
return addOn;
|
||||
};
|
||||
|
||||
const remove = (name: string) => {
|
||||
delete urls[name];
|
||||
delete lookup[name];
|
||||
};
|
||||
|
||||
const createUrl = (baseUrl: string | UrlObject, dep: string | UrlObject): UrlObject => {
|
||||
if (typeof dep === 'object') {
|
||||
return dep;
|
||||
}
|
||||
|
||||
return typeof baseUrl === 'string' ?
|
||||
{ prefix: '', resource: dep, suffix: '' } :
|
||||
{ prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix };
|
||||
};
|
||||
|
||||
const addComponents = (pluginName: string, scripts: string[]) => {
|
||||
const pluginUrl = this.urls[pluginName];
|
||||
|
||||
each(scripts, function (script) {
|
||||
ScriptLoader.ScriptLoader.add(pluginUrl + '/' + script);
|
||||
});
|
||||
};
|
||||
|
||||
const loadDependencies = function (name: string, addOnUrl: string | UrlObject, success: Function, scope: any) {
|
||||
const deps = dependencies(name);
|
||||
|
||||
each(deps, function (dep) {
|
||||
const newUrl = createUrl(addOnUrl, dep);
|
||||
|
||||
load(newUrl.resource, newUrl, undefined, undefined);
|
||||
});
|
||||
|
||||
if (success) {
|
||||
if (scope) {
|
||||
success.call(scope);
|
||||
} else {
|
||||
success.call(ScriptLoader);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const load = (name: string, addOnUrl: string | UrlObject, success?: Function, scope?: any, failure?: Function) => {
|
||||
if (urls[name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlString = typeof addOnUrl === 'string' ? addOnUrl : addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
|
||||
|
||||
if (urlString.indexOf('/') !== 0 && urlString.indexOf('://') === -1) {
|
||||
urlString = AddOnManager.baseURL + '/' + urlString;
|
||||
}
|
||||
|
||||
urls[name] = urlString.substring(0, urlString.lastIndexOf('/'));
|
||||
|
||||
if (lookup[name]) {
|
||||
loadDependencies(name, addOnUrl, success, scope);
|
||||
} else {
|
||||
ScriptLoader.ScriptLoader.add(urlString, () => loadDependencies(name, addOnUrl, success, scope), scope, failure);
|
||||
}
|
||||
};
|
||||
|
||||
const waitFor = (name: string, callback: Function) => {
|
||||
if (lookup.hasOwnProperty(name)) {
|
||||
callback();
|
||||
} else {
|
||||
_listeners.push({ name, callback });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
urls,
|
||||
lookup,
|
||||
_listeners,
|
||||
/**
|
||||
* 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,
|
||||
|
||||
dependencies,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
remove,
|
||||
|
||||
createUrl,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
waitFor
|
||||
};
|
||||
}
|
||||
|
||||
export namespace AddOnManager {
|
||||
export let language;
|
||||
export let languageLoad;
|
||||
export let baseURL;
|
||||
export const PluginManager = AddOnManager();
|
||||
export const ThemeManager = AddOnManager();
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { Arr, Obj, Option } from '@ephox/katamari';
|
||||
import { Remove } from '@ephox/sugar';
|
||||
import * as AnnotationChanges from 'tinymce/core/annotate/AnnotationChanges';
|
||||
import * as AnnotationFilter from 'tinymce/core/annotate/AnnotationFilter';
|
||||
import { create } from 'tinymce/core/annotate/AnnotationsRegistry';
|
||||
import { findAll, identify } from 'tinymce/core/annotate/Identification';
|
||||
import { annotateWithBookmark, Decorator, DecoratorData } from 'tinymce/core/annotate/Wrapping';
|
||||
|
||||
export type AnnotationListenerApi = AnnotationChanges.AnnotationListener;
|
||||
|
||||
/**
|
||||
* This is the annotator api.
|
||||
*
|
||||
* @class tinymce.Annotator
|
||||
*/
|
||||
|
||||
export interface Annotator {
|
||||
register: (name: string, settings: AnnotatorSettings) => void;
|
||||
annotate: (name: string, data: DecoratorData) => void;
|
||||
annotationChanged: (name: string, f: AnnotationListenerApi) => void;
|
||||
remove: (name: string) => void;
|
||||
// TODO: Use stronger types for Nodes when available.
|
||||
getAll: (name: string) => Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AnnotatorSettings {
|
||||
decorate: Decorator;
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
export default function (editor): Annotator {
|
||||
const registry = create();
|
||||
AnnotationFilter.setup(editor, registry);
|
||||
const changes = AnnotationChanges.setup(editor, registry);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Registers a specific annotator by name
|
||||
*
|
||||
* @method register
|
||||
* @param {String} name the name of the annotation
|
||||
* @param {Object} settings settings for the annotation (e.g. decorate)
|
||||
*/
|
||||
register: (name: string, settings: AnnotatorSettings) => {
|
||||
registry.register(name, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies the annotation at the current selection using data
|
||||
*
|
||||
* @method annotate
|
||||
* @param {String} name the name of the annotation to apply
|
||||
* @param {Object} data information to pass through to this particular
|
||||
* annotation
|
||||
*/
|
||||
annotate: (name: string, data: { }) => {
|
||||
registry.lookup(name).each((settings) => {
|
||||
annotateWithBookmark(editor, name, settings, data);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes the specified callback when the current selection matches the annotation or not.
|
||||
*
|
||||
* @method annotationChanged
|
||||
* @param {String} name Name of annotation to listen for
|
||||
* @param {function} callback Calback with (state, name, and data) fired when the annotation
|
||||
* at the cursor changes. If state if false, data will not be provided.
|
||||
*/
|
||||
annotationChanged: (name: string, callback: AnnotationListenerApi) => {
|
||||
changes.addListener(name, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes any annotations from the current selection that match
|
||||
* the name
|
||||
*
|
||||
* @param remove
|
||||
* @param {String} name the name of the annotation to remove
|
||||
*/
|
||||
remove: (name: string): void => {
|
||||
identify(editor, Option.some(name)).each(({ elements }) => {
|
||||
Arr.each(elements, Remove.unwrap);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve all the annotations for a given name
|
||||
*
|
||||
* @method getAll
|
||||
* @param {String} name the name of the annotations to retrieve
|
||||
* @return {Object} an index of annotations from uid => DOM nodes
|
||||
*/
|
||||
getAll: (name: string): Record<string, any> => {
|
||||
const directory = findAll(editor, name);
|
||||
return Obj.map(directory, (elems) => Arr.map(elems, (elem) => elem.dom()));
|
||||
}
|
||||
} as Annotator;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,602 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Env from './Env';
|
||||
import InsertContent from '../content/InsertContent';
|
||||
import DeleteCommands from '../delete/DeleteCommands';
|
||||
import * as FontCommands from '../commands/FontCommands';
|
||||
import NodeType from '../dom/NodeType';
|
||||
import InsertBr from '../newline/InsertBr';
|
||||
import SelectionBookmark from '../selection/SelectionBookmark';
|
||||
import Tools from './util/Tools';
|
||||
import { Selection } from './dom/Selection';
|
||||
import * as IndentOutdent from 'tinymce/core/commands/IndentOutdent';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils';
|
||||
import { HTMLElement } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
// Added for compression purposes
|
||||
const each = Tools.each, extend = Tools.extend;
|
||||
const map = Tools.map, inArray = Tools.inArray;
|
||||
|
||||
export default function (editor: Editor) {
|
||||
let dom: DOMUtils, selection: Selection, formatter;
|
||||
const commands = { state: {}, exec: {}, value: {} };
|
||||
let 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.
|
||||
*/
|
||||
const execCommand = function (command, ui, value, args) {
|
||||
let func, customCommand, state = false;
|
||||
|
||||
if (editor.removed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) {
|
||||
editor.focus();
|
||||
} else {
|
||||
SelectionBookmark.restore(editor);
|
||||
}
|
||||
|
||||
args = editor.fire('BeforeExecCommand', { command, ui, value });
|
||||
if (args.isDefaultPrevented()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
customCommand = command.toLowerCase();
|
||||
if ((func = commands.exec[customCommand])) {
|
||||
func(customCommand, ui, value);
|
||||
editor.fire('ExecCommand', { command, ui, value });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Plugin commands
|
||||
each(editor.plugins, function (p) {
|
||||
if (p.execCommand && p.execCommand(command, ui, value)) {
|
||||
editor.fire('ExecCommand', { command, ui, 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, ui, 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, ui, 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.
|
||||
*/
|
||||
const queryCommandState = function (command) {
|
||||
let 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.
|
||||
*/
|
||||
const queryCommandValue = function (command) {
|
||||
let 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.
|
||||
*/
|
||||
const addCommands = function (commandList, type?) {
|
||||
type = type || 'exec';
|
||||
|
||||
each(commandList, function (callback, command) {
|
||||
each(command.toLowerCase().split(','), function (command) {
|
||||
commands[type][command] = callback;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addCommand = function (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.
|
||||
*/
|
||||
const queryCommandSupported = function (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;
|
||||
};
|
||||
|
||||
const addQueryStateHandler = function (command, callback, scope) {
|
||||
command = command.toLowerCase();
|
||||
commands.state[command] = function () {
|
||||
return callback.call(scope || editor);
|
||||
};
|
||||
};
|
||||
|
||||
const addQueryValueHandler = function (command, callback, scope) {
|
||||
command = command.toLowerCase();
|
||||
commands.value[command] = function () {
|
||||
return callback.call(scope || editor);
|
||||
};
|
||||
};
|
||||
|
||||
const hasCustomCommand = function (command) {
|
||||
command = command.toLowerCase();
|
||||
return !!commands.exec[command];
|
||||
};
|
||||
|
||||
// Expose public methods
|
||||
extend(this, {
|
||||
execCommand,
|
||||
queryCommandState,
|
||||
queryCommandValue,
|
||||
queryCommandSupported,
|
||||
addCommands,
|
||||
addCommand,
|
||||
addQueryStateHandler,
|
||||
addQueryValueHandler,
|
||||
hasCustomCommand
|
||||
});
|
||||
|
||||
// Private methods
|
||||
|
||||
const execNativeCommand = function (command, ui?, value?) {
|
||||
if (ui === undefined) {
|
||||
ui = false;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
return editor.getDoc().execCommand(command, ui, value);
|
||||
};
|
||||
|
||||
const isFormatMatch = function (name) {
|
||||
return formatter.match(name);
|
||||
};
|
||||
|
||||
const toggleFormat = function (name, value?) {
|
||||
formatter.toggle(name, value ? { value } : undefined);
|
||||
editor.nodeChanged();
|
||||
};
|
||||
|
||||
const storeSelection = function (type?) {
|
||||
bookmark = selection.getBookmark(type);
|
||||
};
|
||||
|
||||
const restoreSelection = function () {
|
||||
selection.moveToBookmark(bookmark);
|
||||
};
|
||||
|
||||
// Add execCommand overrides
|
||||
addCommands({
|
||||
// Ignore these, added for compatibility
|
||||
'mceResetDesignMode,mceBeginUndoLevel' () { },
|
||||
|
||||
// Add undo manager logic
|
||||
'mceEndUndoLevel,mceAddUndoLevel' () {
|
||||
editor.undoManager.add();
|
||||
},
|
||||
|
||||
'Cut,Copy,Paste' (command) {
|
||||
const doc = editor.getDoc();
|
||||
let 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)) {
|
||||
let 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' () {
|
||||
if (selection.isCollapsed()) {
|
||||
const 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' (command) {
|
||||
let 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' (command) {
|
||||
let 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' (command) {
|
||||
toggleFormat(command);
|
||||
},
|
||||
|
||||
// Override commands to use the text formatter engine
|
||||
'ForeColor,HiliteColor' (command, ui, value) {
|
||||
toggleFormat(command, value);
|
||||
},
|
||||
|
||||
'FontName' (command, ui, value) {
|
||||
FontCommands.fontNameAction(editor, value);
|
||||
},
|
||||
|
||||
'FontSize' (command, ui, value) {
|
||||
FontCommands.fontSizeAction(editor, value);
|
||||
},
|
||||
|
||||
'RemoveFormat' (command) {
|
||||
formatter.remove(command);
|
||||
},
|
||||
|
||||
'mceBlockQuote' () {
|
||||
toggleFormat('blockquote');
|
||||
},
|
||||
|
||||
'FormatBlock' (command, ui, value) {
|
||||
return toggleFormat(value || 'p');
|
||||
},
|
||||
|
||||
'mceCleanup' () {
|
||||
const bookmark = selection.getBookmark();
|
||||
|
||||
editor.setContent(editor.getContent());
|
||||
selection.moveToBookmark(bookmark);
|
||||
},
|
||||
|
||||
'mceRemoveNode' (command, ui, value) {
|
||||
const 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' (command, ui, value) {
|
||||
let counter = 0;
|
||||
|
||||
dom.getParent(selection.getNode(), function (node) {
|
||||
if (node.nodeType === 1 && counter++ === value) {
|
||||
selection.select(node);
|
||||
return false;
|
||||
}
|
||||
}, editor.getBody());
|
||||
},
|
||||
|
||||
'mceSelectNode' (command, ui, value) {
|
||||
selection.select(value);
|
||||
},
|
||||
|
||||
'mceInsertContent' (command, ui, value) {
|
||||
InsertContent.insertAtCaret(editor, value);
|
||||
},
|
||||
|
||||
'mceInsertRawHTML' (command, ui, value) {
|
||||
selection.setContent('tiny_mce_marker');
|
||||
const content = editor.getContent() as string;
|
||||
editor.setContent(content.replace(/tiny_mce_marker/g, () => value));
|
||||
},
|
||||
|
||||
'mceToggleFormat' (command, ui, value) {
|
||||
toggleFormat(value);
|
||||
},
|
||||
|
||||
'mceSetContent' (command, ui, value) {
|
||||
editor.setContent(value);
|
||||
},
|
||||
|
||||
'Indent,Outdent' (command) {
|
||||
IndentOutdent.handle(editor, command);
|
||||
},
|
||||
|
||||
'mceRepaint' () {
|
||||
},
|
||||
|
||||
'InsertHorizontalRule' () {
|
||||
editor.execCommand('mceInsertContent', false, '<hr />');
|
||||
},
|
||||
|
||||
'mceToggleVisualAid' () {
|
||||
editor.hasVisual = !editor.hasVisual;
|
||||
editor.addVisual();
|
||||
},
|
||||
|
||||
'mceReplaceContent' (command, ui, value) {
|
||||
editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({ format: 'text' })));
|
||||
},
|
||||
|
||||
'mceInsertLink' (command, ui, value) {
|
||||
let 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' () {
|
||||
const editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue);
|
||||
if (editingHost) {
|
||||
const rng = dom.createRng();
|
||||
rng.selectNodeContents(editingHost);
|
||||
selection.setRng(rng);
|
||||
}
|
||||
},
|
||||
|
||||
'delete' () {
|
||||
DeleteCommands.deleteCommand(editor);
|
||||
},
|
||||
|
||||
'forwardDelete' () {
|
||||
DeleteCommands.forwardDeleteCommand(editor);
|
||||
},
|
||||
|
||||
'mceNewDocument' () {
|
||||
editor.setContent('');
|
||||
},
|
||||
|
||||
'InsertLineBreak' (command, ui, value) {
|
||||
InsertBr.insert(editor, value);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const alignStates = (name: string) => () => {
|
||||
const nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
|
||||
const matches = map(nodes, function (node) {
|
||||
return !!formatter.matchNode(node, name);
|
||||
});
|
||||
return inArray(matches, true) !== -1;
|
||||
};
|
||||
|
||||
// Add queryCommandState overrides
|
||||
addCommands({
|
||||
// Override justify commands
|
||||
'JustifyLeft': alignStates('alignleft'),
|
||||
'JustifyCenter': alignStates('aligncenter'),
|
||||
'JustifyRight': alignStates('alignright'),
|
||||
'JustifyFull': alignStates('alignjustify'),
|
||||
|
||||
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' (command) {
|
||||
return isFormatMatch(command);
|
||||
},
|
||||
|
||||
'mceBlockQuote' () {
|
||||
return isFormatMatch('blockquote');
|
||||
},
|
||||
|
||||
'Outdent' () {
|
||||
let 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' (command) {
|
||||
const list = dom.getParent(selection.getNode(), 'ul,ol') as HTMLElement;
|
||||
|
||||
return list &&
|
||||
(
|
||||
command === 'insertunorderedlist' && list.tagName === 'UL' ||
|
||||
command === 'insertorderedlist' && list.tagName === 'OL'
|
||||
);
|
||||
}
|
||||
}, 'state');
|
||||
|
||||
// Add undo manager logic
|
||||
addCommands({
|
||||
Undo () {
|
||||
editor.undoManager.undo();
|
||||
},
|
||||
|
||||
Redo () {
|
||||
editor.undoManager.redo();
|
||||
}
|
||||
});
|
||||
|
||||
addQueryValueHandler('FontName', () => FontCommands.fontNameQuery(editor), this);
|
||||
addQueryValueHandler('FontSize', () => FontCommands.fontSizeQuery(editor), this);
|
||||
}
|
||||
|
|
@ -0,0 +1,788 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr, Type } from '@ephox/katamari';
|
||||
import { AddOnManager } from './AddOnManager';
|
||||
import { Editor } from './Editor';
|
||||
import Env from './Env';
|
||||
import ErrorReporter from '../ErrorReporter';
|
||||
import DOMUtils from './dom/DOMUtils';
|
||||
import DomQuery from './dom/DomQuery';
|
||||
import FocusController from '../focus/FocusController';
|
||||
import I18n from './util/I18n';
|
||||
import Observable from './util/Observable';
|
||||
import Promise from './util/Promise';
|
||||
import Tools from './util/Tools';
|
||||
import URI from './util/URI';
|
||||
import { document } from '@ephox/dom-globals';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
/**
|
||||
* This class used as a factory for manager for tinymce.Editor instances.
|
||||
*
|
||||
* @example
|
||||
* tinymce.EditorManager.init({});
|
||||
*
|
||||
* @class tinymce.EditorManager
|
||||
* @mixes tinymce.util.Observable
|
||||
* @static
|
||||
*/
|
||||
|
||||
const DOM = DOMUtils.DOM;
|
||||
const explode = Tools.explode, each = Tools.each, extend = Tools.extend;
|
||||
let instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
|
||||
const legacyEditors = [];
|
||||
let editors = [];
|
||||
|
||||
const isValidLegacyKey = function (id) {
|
||||
// In theory we could filter out any editor id:s that clash
|
||||
// with array prototype items but that could break existing integrations
|
||||
return id !== 'length';
|
||||
};
|
||||
|
||||
const globalEventDelegate = function (e) {
|
||||
each(EditorManager.get(), function (editor) {
|
||||
if (e.type === 'scroll') {
|
||||
editor.fire('ScrollWindow', e);
|
||||
} else {
|
||||
editor.fire('ResizeWindow', e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleGlobalEvents = function (state) {
|
||||
if (state !== boundGlobalEvents) {
|
||||
if (state) {
|
||||
DomQuery(window).on('resize scroll', globalEventDelegate);
|
||||
} else {
|
||||
DomQuery(window).off('resize scroll', globalEventDelegate);
|
||||
}
|
||||
|
||||
boundGlobalEvents = state;
|
||||
}
|
||||
};
|
||||
|
||||
const removeEditorFromList = function (targetEditor: Editor) {
|
||||
const oldEditors = editors;
|
||||
|
||||
delete legacyEditors[targetEditor.id];
|
||||
for (let i = 0; i < legacyEditors.length; i++) {
|
||||
if (legacyEditors[i] === targetEditor) {
|
||||
legacyEditors.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
editors = Arr.filter(editors, function (editor) {
|
||||
return targetEditor !== editor;
|
||||
});
|
||||
|
||||
// Select another editor since the active one was removed
|
||||
if (EditorManager.activeEditor === targetEditor) {
|
||||
EditorManager.activeEditor = editors.length > 0 ? editors[0] : null;
|
||||
}
|
||||
|
||||
// Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
|
||||
if (EditorManager.focusedEditor === targetEditor) {
|
||||
EditorManager.focusedEditor = null;
|
||||
}
|
||||
|
||||
return oldEditors.length !== editors.length;
|
||||
};
|
||||
|
||||
const purgeDestroyedEditor = function (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 = {
|
||||
defaultSettings: {},
|
||||
|
||||
/**
|
||||
* Dom query instance.
|
||||
*
|
||||
* @property $
|
||||
* @type tinymce.dom.DomQuery
|
||||
*/
|
||||
$: DomQuery,
|
||||
|
||||
/**
|
||||
* Major version of TinyMCE build.
|
||||
*
|
||||
* @property majorVersion
|
||||
* @type String
|
||||
*/
|
||||
majorVersion: '@@majorVersion@@',
|
||||
|
||||
/**
|
||||
* Minor version of TinyMCE build.
|
||||
*
|
||||
* @property minorVersion
|
||||
* @type String
|
||||
*/
|
||||
minorVersion: '@@minorVersion@@',
|
||||
|
||||
/**
|
||||
* Release date of TinyMCE build.
|
||||
*
|
||||
* @property releaseDate
|
||||
* @type String
|
||||
*/
|
||||
releaseDate: '@@releaseDate@@',
|
||||
|
||||
/**
|
||||
* Collection of editor instances. Deprecated use tinymce.get() instead.
|
||||
*
|
||||
* @property editors
|
||||
* @type Object
|
||||
*/
|
||||
editors: legacyEditors,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
settings: {},
|
||||
|
||||
setup () {
|
||||
const self = this;
|
||||
let 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
|
||||
const scripts = document.getElementsByTagName('script');
|
||||
for (let 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
|
||||
const 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 = (<any> 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;
|
||||
|
||||
FocusController.setup(self);
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides the default settings for editor instances.
|
||||
*
|
||||
* @method overrideDefaults
|
||||
* @param {Object} defaultSettings Defaults settings object.
|
||||
*/
|
||||
overrideDefaults (defaultSettings) {
|
||||
let 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;
|
||||
|
||||
const pluginBaseUrls = defaultSettings.plugin_base_urls;
|
||||
for (const 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 (settings) {
|
||||
const self = this;
|
||||
let 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',
|
||||
' '
|
||||
);
|
||||
|
||||
const isInvalidInlineTarget = function (settings, elm) {
|
||||
return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
|
||||
};
|
||||
|
||||
const createId = function (elm) {
|
||||
let 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;
|
||||
};
|
||||
|
||||
const execCallback = function (name) {
|
||||
const callback = settings[name];
|
||||
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
return callback.apply(self, Array.prototype.slice.call(arguments, 2));
|
||||
};
|
||||
|
||||
const hasClass = function (elm, className) {
|
||||
return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
|
||||
};
|
||||
|
||||
const findTargets = function (settings) {
|
||||
let 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) {
|
||||
let 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;
|
||||
};
|
||||
|
||||
let provideResults = function (editors) {
|
||||
result = editors;
|
||||
};
|
||||
|
||||
const initEditors = function () {
|
||||
let initCount = 0;
|
||||
const editors = [];
|
||||
let targets;
|
||||
|
||||
const createEditor = function (id, settings, targetElm) {
|
||||
const editor: 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/Array} Editor instance to return or array of editor instances.
|
||||
* @example
|
||||
* // Adds an onclick event to an editor by id
|
||||
* tinymce.get('mytextbox').on('click', function(e) {
|
||||
* ed.windowManager.alert('Hello world!');
|
||||
* });
|
||||
*
|
||||
* // Adds an onclick event to an editor by index
|
||||
* tinymce.get(0).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 (id) {
|
||||
if (arguments.length === 0) {
|
||||
return editors.slice(0);
|
||||
} else if (Type.isString(id)) {
|
||||
return Arr.find(editors, function (editor) {
|
||||
return editor.id === id;
|
||||
}).getOr(null);
|
||||
} else if (Type.isNumber(id)) {
|
||||
return editors[id] ? editors[id] : null;
|
||||
} else {
|
||||
return 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 (editor) {
|
||||
const self = this;
|
||||
let existingEditor;
|
||||
|
||||
// Prevent existing editors from beeing added again this could happen
|
||||
// if a user calls createEditor then render or add multiple times.
|
||||
existingEditor = legacyEditors[editor.id];
|
||||
if (existingEditor === editor) {
|
||||
return editor;
|
||||
}
|
||||
|
||||
if (self.get(editor.id) === null) {
|
||||
// Add to legacy editors array, this is what breaks in HTML5 where ID:s with numbers are valid
|
||||
// We can't get rid of this strange object and array at the same time since it seems to be used all over the web
|
||||
if (isValidLegacyKey(editor.id)) {
|
||||
legacyEditors[editor.id] = editor;
|
||||
}
|
||||
|
||||
legacyEditors.push(editor);
|
||||
|
||||
editors.push(editor);
|
||||
}
|
||||
|
||||
toggleGlobalEvents(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 });
|
||||
|
||||
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 (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 (selector) {
|
||||
const self = this;
|
||||
let i, editor;
|
||||
|
||||
// Remove all editors
|
||||
if (!selector) {
|
||||
for (i = editors.length - 1; i >= 0; i--) {
|
||||
self.remove(editors[i]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove editors by selector
|
||||
if (Type.isString(selector)) {
|
||||
each(DOM.select(selector), function (elm) {
|
||||
editor = self.get(elm.id);
|
||||
|
||||
if (editor) {
|
||||
self.remove(editor);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove specific editor
|
||||
editor = selector;
|
||||
|
||||
// Not in the collection
|
||||
if (Type.isNull(self.get(editor.id))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (removeEditorFromList(editor)) {
|
||||
self.fire('RemoveEditor', { editor });
|
||||
}
|
||||
|
||||
if (editors.length === 0) {
|
||||
DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
|
||||
}
|
||||
|
||||
editor.remove();
|
||||
|
||||
toggleGlobalEvents(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 (cmd, ui, value) {
|
||||
const 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 () {
|
||||
each(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 (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 (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 (editor) {
|
||||
const activeEditor = this.activeEditor;
|
||||
|
||||
if (this.activeEditor !== editor) {
|
||||
if (activeEditor) {
|
||||
activeEditor.fire('deactivate', { relatedTarget: editor });
|
||||
}
|
||||
|
||||
editor.fire('activate', { relatedTarget: activeEditor });
|
||||
}
|
||||
|
||||
this.activeEditor = editor;
|
||||
}
|
||||
};
|
||||
|
||||
extend(EditorManager, Observable);
|
||||
|
||||
EditorManager.setup();
|
||||
|
||||
export default EditorManager;
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Observable from './util/Observable';
|
||||
import DOMUtils from './dom/DOMUtils';
|
||||
import Tools from './util/Tools';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { isReadOnly } from 'tinymce/core/Mode';
|
||||
import { Node, Event } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* This mixin contains the event logic for the tinymce.Editor class.
|
||||
*
|
||||
* @mixin tinymce.EditorObservable
|
||||
* @extends tinymce.util.Observable
|
||||
*/
|
||||
|
||||
const DOM = DOMUtils.DOM;
|
||||
let 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.
|
||||
*/
|
||||
const getEventTarget = function (editor: Editor, eventName: string): Node {
|
||||
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();
|
||||
};
|
||||
|
||||
const isListening = (editor: Editor) => !editor.hidden && !editor.readonly;
|
||||
|
||||
const fireEvent = (editor: Editor, eventName: string, e: Event) => {
|
||||
if (isListening(editor)) {
|
||||
editor.fire(eventName, e);
|
||||
} else if (isReadOnly(editor)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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".
|
||||
*/
|
||||
const bindEventDelegate = function (editor: Editor, eventName: string) {
|
||||
let eventRootElm, delegate;
|
||||
|
||||
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 () {
|
||||
let 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) {
|
||||
const target = e.target;
|
||||
const editors = editor.editorManager.get();
|
||||
let i = editors.length;
|
||||
|
||||
while (i--) {
|
||||
const body = editors[i].getBody();
|
||||
|
||||
if (body === target || DOM.isChildOf(target, body)) {
|
||||
fireEvent(editors[i], eventName, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
customEventRootDelegates[eventName] = delegate;
|
||||
DOM.bind(eventRootElm, eventName, delegate);
|
||||
} else {
|
||||
delegate = function (e) {
|
||||
fireEvent(editor, eventName, e);
|
||||
};
|
||||
|
||||
DOM.bind(eventRootElm, eventName, delegate);
|
||||
editor.delegates[eventName] = delegate;
|
||||
}
|
||||
};
|
||||
|
||||
let EditorObservable = {
|
||||
/**
|
||||
* Bind any pending event delegates. This gets executed after the target body/document is created.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
bindPendingEventDelegates () {
|
||||
const 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 (name, state) {
|
||||
const 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 () {
|
||||
const self = this;
|
||||
const body = self.getBody();
|
||||
const dom: DOMUtils = self.dom;
|
||||
let 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 && body && dom) {
|
||||
body.onload = null;
|
||||
dom.unbind(self.getWin());
|
||||
dom.unbind(self.getDoc());
|
||||
}
|
||||
|
||||
if (dom) {
|
||||
dom.unbind(body);
|
||||
dom.unbind(self.getContainer());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
EditorObservable = Tools.extend({}, Observable, EditorObservable);
|
||||
|
||||
export default EditorObservable;
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Uploader from '../file/Uploader';
|
||||
import ImageScanner from '../file/ImageScanner';
|
||||
import BlobCache from './file/BlobCache';
|
||||
import UploadStatus from '../file/UploadStatus';
|
||||
import ErrorReporter from '../ErrorReporter';
|
||||
import { Arr } from '@ephox/katamari';
|
||||
import { HTMLImageElement, Blob } from '@ephox/dom-globals';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import Settings from 'tinymce/core/api/Settings';
|
||||
|
||||
/**
|
||||
* Handles image uploads, updates undo stack and patches over various internal functions.
|
||||
*
|
||||
* @private
|
||||
* @class tinymce.EditorUpload
|
||||
*/
|
||||
|
||||
export default function (editor: Editor) {
|
||||
const blobCache = BlobCache();
|
||||
let uploader, imageScanner;
|
||||
const uploadStatus = UploadStatus();
|
||||
const urlFilters: Array<(img: HTMLImageElement) => boolean> = [];
|
||||
|
||||
const aliveGuard = function (callback) {
|
||||
return function (result) {
|
||||
if (editor.selection) {
|
||||
return callback(result);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
};
|
||||
|
||||
const cacheInvalidator = function (): string {
|
||||
return '?' + (new Date()).getTime();
|
||||
};
|
||||
|
||||
// Replaces strings without regexps to avoid FF regexp to big issue
|
||||
const replaceString = function (content: string, search: string, replace: string): string {
|
||||
let 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;
|
||||
};
|
||||
|
||||
const replaceImageUrl = function (content: string, targetUrl: string, replacementUrl: string): string {
|
||||
content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"');
|
||||
content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"');
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const replaceUrlInUndoStack = function (targetUrl: string, replacementUrl: string) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openNotification = function () {
|
||||
return editor.notificationManager.open({
|
||||
text: editor.translate('Image uploading...'),
|
||||
type: 'info',
|
||||
timeout: -1,
|
||||
progressBar: true
|
||||
});
|
||||
};
|
||||
|
||||
const replaceImageUri = function (image: HTMLImageElement, resultUri: string) {
|
||||
blobCache.removeByUri(image.src);
|
||||
replaceUrlInUndoStack(image.src, resultUri);
|
||||
|
||||
editor.$(image).attr({
|
||||
'src': Settings.shouldReuseFileName(editor) ? resultUri + cacheInvalidator() : resultUri,
|
||||
'data-mce-src': editor.convertURL(resultUri, 'src')
|
||||
});
|
||||
};
|
||||
|
||||
const uploadImages = function (callback) {
|
||||
if (!uploader) {
|
||||
uploader = Uploader(uploadStatus, {
|
||||
url: Settings.getImageUploadUrl(editor),
|
||||
basePath: Settings.getImageUploadBasePath(editor),
|
||||
credentials: Settings.getImagesUploadCredentials(editor),
|
||||
handler: Settings.getImagesUploadHandler(editor)
|
||||
});
|
||||
}
|
||||
|
||||
return scanForImages().then(aliveGuard(function (imageInfos) {
|
||||
let blobInfos;
|
||||
|
||||
blobInfos = Arr.map(imageInfos, function (imageInfo) {
|
||||
return imageInfo.blobInfo;
|
||||
});
|
||||
|
||||
return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) {
|
||||
const filteredResult = Arr.map(result, function (uploadInfo, index) {
|
||||
const image = imageInfos[index].image;
|
||||
|
||||
if (uploadInfo.status && Settings.shouldReplaceBlobUris(editor)) {
|
||||
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;
|
||||
}));
|
||||
}));
|
||||
};
|
||||
|
||||
const uploadImagesAuto = function (callback?) {
|
||||
if (Settings.isAutomaticUploadsEnabled(editor)) {
|
||||
return uploadImages(callback);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidDataUriImage = function (imgElm: HTMLImageElement) {
|
||||
if (Arr.forall(urlFilters, (filter) => filter(imgElm)) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (imgElm.getAttribute('src').indexOf('data:') === 0) {
|
||||
const dataImgFilter = Settings.getImagesDataImgFilter(editor);
|
||||
return dataImgFilter(imgElm);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const addFilter = (filter: (img: HTMLImageElement) => boolean) => {
|
||||
urlFilters.push(filter);
|
||||
};
|
||||
|
||||
const scanForImages = function () {
|
||||
if (!imageScanner) {
|
||||
imageScanner = 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;
|
||||
}));
|
||||
};
|
||||
|
||||
const destroy = function () {
|
||||
blobCache.destroy();
|
||||
uploadStatus.destroy();
|
||||
imageScanner = uploader = null;
|
||||
};
|
||||
|
||||
const replaceBlobUris = function (content: string) {
|
||||
return content.replace(/src="(blob:[^"]+)"/g, function (match, blobUri) {
|
||||
const resultUri = uploadStatus.getResultUri(blobUri);
|
||||
|
||||
if (resultUri) {
|
||||
return 'src="' + resultUri + '"';
|
||||
}
|
||||
|
||||
let blobInfo = blobCache.getByUri(blobUri);
|
||||
|
||||
if (!blobInfo) {
|
||||
blobInfo = Arr.foldl(editor.editorManager.get(), function (result, editor) {
|
||||
return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri);
|
||||
}, null);
|
||||
}
|
||||
|
||||
if (blobInfo) {
|
||||
const blob: Blob = blobInfo.blob();
|
||||
return 'src="data:' + blob.type + ';base64,' + blobInfo.base64() + '"';
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('setContent', function () {
|
||||
if (Settings.isAutomaticUploadsEnabled(editor)) {
|
||||
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) {
|
||||
const src = img.attr('src');
|
||||
|
||||
if (blobCache.getByUri(src)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultUri = uploadStatus.getResultUri(src);
|
||||
if (resultUri) {
|
||||
img.attr('src', resultUri);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
blobCache,
|
||||
addFilter,
|
||||
uploadImages,
|
||||
uploadImagesAuto,
|
||||
scanForImages,
|
||||
destroy
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { URL } from '@ephox/sand';
|
||||
import { navigator, window, matchMedia, document } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const nav = navigator, userAgent = nav.userAgent;
|
||||
let opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone;
|
||||
|
||||
const matchMediaQuery = function (query) {
|
||||
return 'matchMedia' in window ? matchMedia(query).matches : false;
|
||||
};
|
||||
|
||||
opera = false;
|
||||
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.
|
||||
const contentEditable = !iDevice || fileApi || parseInt(userAgent.match(/AppleWebKit\/(\d*)/)[1], 10) >= 534;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Constant that is true if the browser is Opera.
|
||||
*
|
||||
* @property opera
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
opera,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser is WebKit (Safari/Chrome).
|
||||
*
|
||||
* @property webKit
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
webkit,
|
||||
|
||||
/**
|
||||
* Constant that is more than zero if the browser is IE.
|
||||
*
|
||||
* @property ie
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
ie,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser is Gecko.
|
||||
*
|
||||
* @property gecko
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
gecko,
|
||||
|
||||
/**
|
||||
* Constant that is true if the os is Mac OS.
|
||||
*
|
||||
* @property mac
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
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,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser supports editing.
|
||||
*
|
||||
* @property contentEditable
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
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 ? ((<any> document).documentMode || 7) : 10,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser has a modern file api.
|
||||
*
|
||||
* @property fileApi
|
||||
* @type Boolean
|
||||
*/
|
||||
fileApi,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser supports contentEditable=false regions.
|
||||
*
|
||||
* @property ceFalse
|
||||
* @type Boolean
|
||||
*/
|
||||
ceFalse: (ie === false || ie > 8),
|
||||
|
||||
cacheSuffix: null,
|
||||
container: null,
|
||||
overrideViewPort: null,
|
||||
experimentalShadowDom: false,
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Events.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
|
||||
*/
|
||||
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { EditorMode } from 'tinymce/core/Mode';
|
||||
import { HTMLElement } from '@ephox/dom-globals';
|
||||
|
||||
const firePreProcess = (editor: Editor, args) => editor.fire('PreProcess', args);
|
||||
|
||||
const firePostProcess = (editor: Editor, args) => editor.fire('PostProcess', args);
|
||||
|
||||
const fireRemove = (editor: Editor) => editor.fire('remove');
|
||||
|
||||
const fireSwitchMode = (editor: Editor, mode: EditorMode) => editor.fire('SwitchMode', { mode });
|
||||
|
||||
const fireObjectResizeStart = (editor: Editor, target: HTMLElement, width: number, height: number) => {
|
||||
editor.fire('ObjectResizeStart', { target, width, height });
|
||||
};
|
||||
|
||||
const fireObjectResized = (editor: Editor, target: HTMLElement, width: number, height: number) => {
|
||||
editor.fire('ObjectResized', { target, width, height });
|
||||
};
|
||||
|
||||
export default {
|
||||
firePreProcess,
|
||||
firePostProcess,
|
||||
fireRemove,
|
||||
fireSwitchMode,
|
||||
fireObjectResizeStart,
|
||||
fireObjectResized
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Element } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns true if the specified element is part of the UI for example an button or text input.
|
||||
*
|
||||
* @static
|
||||
* @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.
|
||||
*/
|
||||
const isEditorUIElement = function (elm: Element) {
|
||||
// Needs to be converted to string since svg can have focus: #6776
|
||||
return elm.className.toString().indexOf('mce-') !== -1;
|
||||
};
|
||||
|
||||
export default {
|
||||
isEditorUIElement
|
||||
};
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Cell, Fun } from '@ephox/katamari';
|
||||
import ApplyFormat from '../fmt/ApplyFormat';
|
||||
import * as CaretFormat from '../fmt/CaretFormat';
|
||||
import FormatChanged from '../fmt/FormatChanged';
|
||||
import FormatRegistry from '../fmt/FormatRegistry';
|
||||
import MatchFormat from '../fmt/MatchFormat';
|
||||
import Preview from '../fmt/Preview';
|
||||
import RemoveFormat from '../fmt/RemoveFormat';
|
||||
import ToggleFormat from '../fmt/ToggleFormat';
|
||||
import FormatShortcuts from '../keyboard/FormatShortcuts';
|
||||
|
||||
/**
|
||||
* 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');
|
||||
*/
|
||||
|
||||
export default function (editor) {
|
||||
const formats = FormatRegistry(editor);
|
||||
const formatChangeState = Cell(null);
|
||||
|
||||
FormatShortcuts.setup(editor);
|
||||
CaretFormat.setup(editor);
|
||||
|
||||
return {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get: formats.get,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
register: formats.register,
|
||||
|
||||
/**
|
||||
* Unregister a specific format by name.
|
||||
*
|
||||
* @method unregister
|
||||
* @param {String} name Name of the format for example "bold".
|
||||
*/
|
||||
unregister: formats.unregister,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
apply: Fun.curry(ApplyFormat.applyFormat, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
remove: Fun.curry(RemoveFormat.remove, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
toggle: Fun.curry(ToggleFormat.toggle, editor, formats),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
match: Fun.curry(MatchFormat.match, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
matchAll: Fun.curry(MatchFormat.matchAll, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
matchNode: Fun.curry(MatchFormat.matchNode, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
canApply: Fun.curry(MatchFormat.canApply, editor),
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
formatChanged: Fun.curry(FormatChanged.formatChanged, editor, formatChangeState),
|
||||
|
||||
/**
|
||||
* 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'});
|
||||
*/
|
||||
getCssText: Fun.curry(Preview.getCssText, editor)
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Tinymce from './Tinymce';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const exportToModuleLoaders = (tinymce) => {
|
||||
if (typeof module === 'object') {
|
||||
try {
|
||||
module.exports = tinymce;
|
||||
} catch (_) {
|
||||
// It will thrown an error when running this module
|
||||
// within webpack where the module.exports object is sealed
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const exportToWindowGlobal = (tinymce) => {
|
||||
window.tinymce = tinymce;
|
||||
window.tinyMCE = tinymce;
|
||||
};
|
||||
|
||||
exportToWindowGlobal(Tinymce);
|
||||
exportToModuleLoaders(Tinymce);
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr, Option } from '@ephox/katamari';
|
||||
import EditorView from '../EditorView';
|
||||
import NotificationManagerImpl from '../ui/NotificationManagerImpl';
|
||||
import Delay from './util/Delay';
|
||||
|
||||
/**
|
||||
* 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'
|
||||
* });
|
||||
*/
|
||||
|
||||
export default function (editor) {
|
||||
const notifications = [];
|
||||
|
||||
const getImplementation = function () {
|
||||
const theme = editor.theme;
|
||||
return theme && theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl();
|
||||
};
|
||||
|
||||
const getTopNotification = function () {
|
||||
return Option.from(notifications[0]);
|
||||
};
|
||||
|
||||
const isEqual = function (a, b) {
|
||||
return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout;
|
||||
};
|
||||
|
||||
const reposition = function () {
|
||||
if (notifications.length > 0) {
|
||||
getImplementation().reposition(notifications);
|
||||
}
|
||||
};
|
||||
|
||||
const addNotification = function (notification) {
|
||||
notifications.push(notification);
|
||||
};
|
||||
|
||||
const closeNotification = function (notification) {
|
||||
Arr.findIndex(notifications, function (otherNotification) {
|
||||
return otherNotification === notification;
|
||||
}).each(function (index) {
|
||||
// Mutate here since third party might have stored away the window array
|
||||
// TODO: Consider breaking this api
|
||||
notifications.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
const open = function (args) {
|
||||
// Never open notification if editor has been removed.
|
||||
if (editor.removed || !EditorView.isEditorAttachedToDom(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Arr.find(notifications, function (notification) {
|
||||
return isEqual(getImplementation().getArgs(notification), args);
|
||||
}).getOrThunk(function () {
|
||||
editor.editorManager.setActive(editor);
|
||||
|
||||
const notification = getImplementation().open(args, function () {
|
||||
closeNotification(notification);
|
||||
reposition();
|
||||
});
|
||||
|
||||
addNotification(notification);
|
||||
reposition();
|
||||
return notification;
|
||||
});
|
||||
};
|
||||
|
||||
const close = function () {
|
||||
getTopNotification().each(function (notification) {
|
||||
getImplementation().close(notification);
|
||||
closeNotification(notification);
|
||||
reposition();
|
||||
});
|
||||
};
|
||||
|
||||
const getNotifications = function () {
|
||||
return notifications;
|
||||
};
|
||||
|
||||
const registerEvents = function (editor) {
|
||||
editor.on('SkinLoaded', function () {
|
||||
const serviceMessage = editor.settings.service_message;
|
||||
|
||||
if (serviceMessage) {
|
||||
open({
|
||||
text: serviceMessage,
|
||||
type: 'warning',
|
||||
timeout: 0,
|
||||
icon: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('ResizeEditor ResizeWindow', function () {
|
||||
Delay.requestAnimationFrame(reposition);
|
||||
});
|
||||
|
||||
editor.on('remove', function () {
|
||||
Arr.each(notifications.slice(), function (notification) {
|
||||
getImplementation().close(notification);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
registerEvents(editor);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Opens a new notification.
|
||||
*
|
||||
* @method open
|
||||
* @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc.
|
||||
*/
|
||||
open,
|
||||
|
||||
/**
|
||||
* Closes the top most notification.
|
||||
*
|
||||
* @method close
|
||||
*/
|
||||
close,
|
||||
|
||||
/**
|
||||
* Returns the currently opened notification objects.
|
||||
*
|
||||
* @method getNotifications
|
||||
* @return {Array} Array of the currently opened notifications.
|
||||
*/
|
||||
getNotifications
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { UrlObject, AddOnManager } from './AddOnManager';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
// TODO: Remove this when TypeScript 2.8 is out!
|
||||
// Needed because of this: https://github.com/Microsoft/TypeScript/issues/9944
|
||||
export interface PluginManager extends AddOnManager {
|
||||
add: (id: string, addOn: (editor: Editor, url: string) => any, dependencies?: any) => (editor: Editor, url: string) => any;
|
||||
createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;
|
||||
}
|
||||
|
||||
export default AddOnManager.PluginManager as PluginManager;
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Settings.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
|
||||
*/
|
||||
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import Tools from 'tinymce/core/api/util/Tools';
|
||||
import { HTMLImageElement } from '@ephox/dom-globals';
|
||||
import { Fun } from '@ephox/katamari';
|
||||
import { UploadHandler } from 'tinymce/core/file/Uploader';
|
||||
|
||||
const getBodySetting = (editor: Editor, name: string, defaultValue: string) => {
|
||||
const value = editor.getParam(name, defaultValue);
|
||||
|
||||
if (value.indexOf('=') !== -1) {
|
||||
const bodyObj = editor.getParam(name, '', 'hash');
|
||||
return bodyObj.hasOwnProperty(editor.id) ? bodyObj[editor.id] : defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const getIframeAttrs = (editor: Editor): Record<string, string> => {
|
||||
return editor.getParam('iframe_attrs', {});
|
||||
};
|
||||
|
||||
const getDocType = (editor: Editor): string => {
|
||||
return editor.getParam('doctype', '<!DOCTYPE html>');
|
||||
};
|
||||
|
||||
const getDocumentBaseUrl = (editor: Editor): string => {
|
||||
return editor.getParam('document_base_url', '');
|
||||
};
|
||||
|
||||
const getBodyId = (editor: Editor): string => {
|
||||
return getBodySetting(editor, 'body_id', 'tinymce');
|
||||
};
|
||||
|
||||
const getBodyClass = (editor: Editor): string => {
|
||||
return getBodySetting(editor, 'body_class', '');
|
||||
};
|
||||
|
||||
const getContentSecurityPolicy = (editor: Editor): string => {
|
||||
return editor.getParam('content_security_policy', '');
|
||||
};
|
||||
|
||||
const shouldPutBrInPre = (editor: Editor): boolean => {
|
||||
return editor.getParam('br_in_pre', true);
|
||||
};
|
||||
|
||||
const getForcedRootBlock = (editor: Editor): string => {
|
||||
// Legacy option
|
||||
if (editor.getParam('force_p_newlines', false)) {
|
||||
return 'p';
|
||||
}
|
||||
|
||||
const block = editor.getParam('forced_root_block', 'p');
|
||||
return block === false ? '' : block;
|
||||
};
|
||||
|
||||
const getForcedRootBlockAttrs = (editor: Editor): Record<string, string> => {
|
||||
return editor.getParam('forced_root_block_attrs', {});
|
||||
};
|
||||
|
||||
const getBrNewLineSelector = (editor: Editor): string => {
|
||||
return editor.getParam('br_newline_selector', '.mce-toc h2,figcaption,caption');
|
||||
};
|
||||
|
||||
const getNoNewLineSelector = (editor: Editor): string => {
|
||||
return editor.getParam('no_newline_selector', '');
|
||||
};
|
||||
|
||||
const shouldKeepStyles = (editor: Editor): boolean => {
|
||||
return editor.getParam('keep_styles', true);
|
||||
};
|
||||
|
||||
const shouldEndContainerOnEmptyBlock = (editor: Editor): boolean => {
|
||||
return editor.getParam('end_container_on_empty_block', false);
|
||||
};
|
||||
|
||||
const getFontStyleValues = (editor: Editor): string[] => Tools.explode(editor.getParam('font_size_style_values', ''));
|
||||
const getFontSizeClasses = (editor: Editor): string[] => Tools.explode(editor.getParam('font_size_classes', ''));
|
||||
|
||||
const getImagesDataImgFilter = (editor: Editor): (imgElm: HTMLImageElement) => boolean => {
|
||||
return editor.getParam('images_dataimg_filter', Fun.constant(true), 'function');
|
||||
};
|
||||
|
||||
const isAutomaticUploadsEnabled = (editor: Editor): boolean => {
|
||||
return editor.getParam('automatic_uploads', true, 'boolean');
|
||||
};
|
||||
|
||||
const shouldReuseFileName = (editor: Editor): boolean => {
|
||||
return editor.getParam('images_reuse_filename', false, 'boolean');
|
||||
};
|
||||
|
||||
const shouldReplaceBlobUris = (editor: Editor): boolean => {
|
||||
return editor.getParam('images_replace_blob_uris', true, 'boolean');
|
||||
};
|
||||
|
||||
const getImageUploadUrl = (editor: Editor): string => {
|
||||
return editor.getParam('images_upload_url', '', 'string');
|
||||
};
|
||||
|
||||
const getImageUploadBasePath = (editor: Editor): string => {
|
||||
return editor.getParam('images_upload_base_path', '', 'string');
|
||||
};
|
||||
|
||||
const getImagesUploadCredentials = (editor: Editor): boolean => {
|
||||
return editor.getParam('images_upload_credentials', false, 'boolean');
|
||||
};
|
||||
|
||||
const getImagesUploadHandler = (editor: Editor): UploadHandler => {
|
||||
return editor.getParam('images_upload_handler', null, 'function');
|
||||
};
|
||||
|
||||
const shouldUseContentCssCors = (editor: Editor): boolean => {
|
||||
return editor.getParam('content_css_cors', false, 'boolean');
|
||||
};
|
||||
|
||||
export default {
|
||||
getIframeAttrs,
|
||||
getDocType,
|
||||
getDocumentBaseUrl,
|
||||
getBodyId,
|
||||
getBodyClass,
|
||||
getContentSecurityPolicy,
|
||||
shouldPutBrInPre,
|
||||
getForcedRootBlock,
|
||||
getForcedRootBlockAttrs,
|
||||
getBrNewLineSelector,
|
||||
getNoNewLineSelector,
|
||||
shouldKeepStyles,
|
||||
shouldEndContainerOnEmptyBlock,
|
||||
getFontStyleValues,
|
||||
getFontSizeClasses,
|
||||
getImagesDataImgFilter,
|
||||
isAutomaticUploadsEnabled,
|
||||
shouldReuseFileName,
|
||||
shouldReplaceBlobUris,
|
||||
getImageUploadUrl,
|
||||
getImageUploadBasePath,
|
||||
getImagesUploadCredentials,
|
||||
getImagesUploadHandler,
|
||||
shouldUseContentCssCors
|
||||
};
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Tools from './util/Tools';
|
||||
import Env from './Env';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const each = Tools.each, explode = Tools.explode;
|
||||
|
||||
const keyCodeLookup = {
|
||||
f9: 120,
|
||||
f10: 121,
|
||||
f11: 122
|
||||
};
|
||||
|
||||
const modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access');
|
||||
|
||||
export default function (editor) {
|
||||
const self = this;
|
||||
const shortcuts = {};
|
||||
let pendingPatterns = [];
|
||||
|
||||
const parseShortcut = function (pattern) {
|
||||
let id, key;
|
||||
const shortcut: any = {};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
const createShortcut = function (pattern, desc?, cmdFunc?, scope?) {
|
||||
let 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)
|
||||
});
|
||||
};
|
||||
|
||||
const hasModifier = function (e) {
|
||||
return e.altKey || e.ctrlKey || e.metaKey;
|
||||
};
|
||||
|
||||
const isFunctionKey = function (e) {
|
||||
return e.type === 'keydown' && e.keyCode >= 112 && e.keyCode <= 123;
|
||||
};
|
||||
|
||||
const matchShortcut = function (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;
|
||||
};
|
||||
|
||||
const executeShortcutAction = function (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) {
|
||||
let 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) {
|
||||
const 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) {
|
||||
const shortcut = createShortcut(pattern);
|
||||
|
||||
if (shortcuts[shortcut.id]) {
|
||||
delete shortcuts[shortcut.id];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { UrlObject, AddOnManager } from './AddOnManager';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
|
||||
// TODO: Remove this when TypeScript 2.8 is out!
|
||||
// Needed because of this: https://github.com/Microsoft/TypeScript/issues/9944
|
||||
export interface ThemeManager extends AddOnManager {
|
||||
add: (id: string, addOn: (editor: Editor, url: string) => any, dependencies?: any) => (editor: Editor, url: string) => any;
|
||||
createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;
|
||||
}
|
||||
|
||||
export default AddOnManager.ThemeManager as ThemeManager;
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { AddOnManager } from './AddOnManager';
|
||||
import Annotator from './Annotator';
|
||||
import { Editor } from './Editor';
|
||||
import EditorCommands from './EditorCommands';
|
||||
import EditorManager from './EditorManager';
|
||||
import EditorObservable from './EditorObservable';
|
||||
import Env from './Env';
|
||||
import Shortcuts from './Shortcuts';
|
||||
import UndoManager from './UndoManager';
|
||||
import FocusManager from './FocusManager';
|
||||
import Formatter from './Formatter';
|
||||
import NotificationManager from './NotificationManager';
|
||||
import WindowManager from './WindowManager';
|
||||
import BookmarkManager from './dom/BookmarkManager';
|
||||
import RangeUtils from './dom/RangeUtils';
|
||||
import DomSerializer from './dom/Serializer';
|
||||
import ControlSelection from './dom/ControlSelection';
|
||||
import DOMUtils from './dom/DOMUtils';
|
||||
import DomQuery from './dom/DomQuery';
|
||||
import EventUtils from './dom/EventUtils';
|
||||
import ScriptLoader from './dom/ScriptLoader';
|
||||
import { Selection } from './dom/Selection';
|
||||
import Sizzle from './dom/Sizzle';
|
||||
import TreeWalker from './dom/TreeWalker';
|
||||
import Rect from './geom/Rect';
|
||||
import DomParser from './html/DomParser';
|
||||
import Entities from './html/Entities';
|
||||
import Node from './html/Node';
|
||||
import SaxParser from './html/SaxParser';
|
||||
import Schema from './html/Schema';
|
||||
import HtmlSerializer from './html/Serializer';
|
||||
import { Styles } from './html/Styles';
|
||||
import Writer from './html/Writer';
|
||||
import Factory from './ui/Factory';
|
||||
import Class from './util/Class';
|
||||
import Color from './util/Color';
|
||||
import Delay from './util/Delay';
|
||||
import EventDispatcher from './util/EventDispatcher';
|
||||
import I18n from './util/I18n';
|
||||
import JSON from './util/JSON';
|
||||
import JSONP from './util/JSONP';
|
||||
import JSONRequest from './util/JSONRequest';
|
||||
import LocalStorage from './util/LocalStorage';
|
||||
import Observable from './util/Observable';
|
||||
import Promise from './util/Promise';
|
||||
import Tools from './util/Tools';
|
||||
import URI from './util/URI';
|
||||
import VK from './util/VK';
|
||||
import XHR from './util/XHR';
|
||||
|
||||
let tinymce = EditorManager;
|
||||
|
||||
/**
|
||||
* @include ../../../../../tools/docs/tinymce.js
|
||||
*/
|
||||
const publicApi = {
|
||||
geom: {
|
||||
Rect
|
||||
},
|
||||
|
||||
util: {
|
||||
Promise,
|
||||
Delay,
|
||||
Tools,
|
||||
VK,
|
||||
URI,
|
||||
Class,
|
||||
EventDispatcher,
|
||||
Observable,
|
||||
I18n,
|
||||
XHR,
|
||||
JSON,
|
||||
JSONRequest,
|
||||
JSONP,
|
||||
LocalStorage,
|
||||
Color
|
||||
},
|
||||
|
||||
dom: {
|
||||
EventUtils,
|
||||
Sizzle,
|
||||
DomQuery,
|
||||
TreeWalker,
|
||||
DOMUtils,
|
||||
ScriptLoader,
|
||||
RangeUtils,
|
||||
Serializer: DomSerializer,
|
||||
ControlSelection,
|
||||
BookmarkManager,
|
||||
Selection,
|
||||
Event: EventUtils.Event
|
||||
},
|
||||
|
||||
html: {
|
||||
Styles,
|
||||
Entities,
|
||||
Node,
|
||||
Schema,
|
||||
SaxParser,
|
||||
DomParser,
|
||||
Writer,
|
||||
Serializer: HtmlSerializer
|
||||
},
|
||||
|
||||
ui: {
|
||||
Factory
|
||||
},
|
||||
|
||||
Env,
|
||||
AddOnManager,
|
||||
Annotator,
|
||||
Formatter,
|
||||
UndoManager,
|
||||
EditorCommands,
|
||||
WindowManager,
|
||||
NotificationManager,
|
||||
EditorObservable,
|
||||
Shortcuts,
|
||||
Editor,
|
||||
FocusManager,
|
||||
EditorManager,
|
||||
|
||||
// Global instances
|
||||
DOM: DOMUtils.DOM,
|
||||
ScriptLoader: ScriptLoader.ScriptLoader,
|
||||
PluginManager: AddOnManager.PluginManager,
|
||||
ThemeManager: AddOnManager.ThemeManager,
|
||||
|
||||
// Global utility functions
|
||||
trim: Tools.trim,
|
||||
isArray: Tools.isArray,
|
||||
is: Tools.is,
|
||||
toArray: Tools.toArray,
|
||||
makeMap: Tools.makeMap,
|
||||
each: Tools.each,
|
||||
map: Tools.map,
|
||||
grep: Tools.grep,
|
||||
inArray: Tools.inArray,
|
||||
extend: Tools.extend,
|
||||
create: Tools.create,
|
||||
walk: Tools.walk,
|
||||
createNS: Tools.createNS,
|
||||
resolve: Tools.resolve,
|
||||
explode: Tools.explode,
|
||||
_addCacheSuffix: Tools._addCacheSuffix,
|
||||
|
||||
// Legacy browser detection
|
||||
isOpera: Env.opera,
|
||||
isWebKit: Env.webkit,
|
||||
isIE: Env.ie,
|
||||
isGecko: Env.gecko,
|
||||
isMac: Env.mac
|
||||
};
|
||||
|
||||
tinymce = Tools.extend(tinymce, publicApi);
|
||||
|
||||
export default tinymce;
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import GetBookmark from '../bookmark/GetBookmark';
|
||||
import Levels, { UndoLevel } from '../undo/Levels';
|
||||
import Tools from './util/Tools';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { Event } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export interface UndoManager {
|
||||
data: UndoLevel[];
|
||||
typing: boolean;
|
||||
add: (level?: UndoLevel, event?: Event) => UndoLevel;
|
||||
beforeChange: () => void;
|
||||
undo: () => UndoLevel;
|
||||
redo: () => UndoLevel;
|
||||
clear: () => void;
|
||||
hasUndo: () => boolean;
|
||||
hasRedo: () => boolean;
|
||||
transact: (callback: () => void) => UndoLevel;
|
||||
ignore: (callback: () => void) => void;
|
||||
extra: (callback1: () => void, callback2: () => void) => void;
|
||||
}
|
||||
|
||||
export default function (editor: Editor) {
|
||||
let self: UndoManager = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
|
||||
|
||||
const isUnlocked = function () {
|
||||
return locks === 0;
|
||||
};
|
||||
|
||||
const setTyping = function (typing) {
|
||||
if (isUnlocked()) {
|
||||
self.typing = typing;
|
||||
}
|
||||
};
|
||||
|
||||
const setDirty = function (state) {
|
||||
editor.setDirty(state);
|
||||
};
|
||||
|
||||
const addNonTypingUndoLevel = function (e?) {
|
||||
setTyping(false);
|
||||
self.add({} as UndoLevel, e);
|
||||
};
|
||||
|
||||
const endTyping = function () {
|
||||
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) {
|
||||
const 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) {
|
||||
const 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) {
|
||||
const 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) {
|
||||
const 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
|
||||
const 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({} as UndoLevel, e);
|
||||
isFirstTypedCharacter = true;
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('MouseDown', function (e) {
|
||||
if (self.typing) {
|
||||
addNonTypingUndoLevel(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Special inputType, currently only Chrome implements this: https://www.w3.org/TR/input-events-2/#x5.1.2-attributes
|
||||
const isInsertReplacementText = (event) => event.inputType === 'insertReplacementText';
|
||||
// Safari just shows inputType `insertText` but with data set to null so we can use that
|
||||
const isInsertTextDataNull = (event) => event.inputType === 'insertText' && event.data === null;
|
||||
|
||||
// For detecting when user has replaced text using the browser built-in spell checker
|
||||
editor.on('input', (e) => {
|
||||
if (e.inputType && (isInsertReplacementText(e) || isInsertTextDataNull(e))) {
|
||||
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,
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
if (isUnlocked()) {
|
||||
beforeBookmark = GetBookmark.getUndoBookmark(editor.selection);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (level?: UndoLevel, event?: Event): UndoLevel {
|
||||
let i;
|
||||
const settings = editor.settings;
|
||||
let lastLevel, currentLevel;
|
||||
|
||||
currentLevel = Levels.createFromEditor(editor);
|
||||
level = level || {} as UndoLevel;
|
||||
level = Tools.extend(level, currentLevel);
|
||||
|
||||
if (isUnlocked() === false || editor.removed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
lastLevel = data[index];
|
||||
if (editor.fire('BeforeAddUndo', { level, 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 = GetBookmark.getUndoBookmark(editor.selection);
|
||||
|
||||
// Crop array if needed
|
||||
if (index < data.length - 1) {
|
||||
data.length = index + 1;
|
||||
}
|
||||
|
||||
data.push(level);
|
||||
index = data.length - 1;
|
||||
|
||||
const args = { level, 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 (): UndoLevel {
|
||||
let level: UndoLevel;
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
/**
|
||||
* Redoes the last action.
|
||||
*
|
||||
* @method redo
|
||||
* @return {Object} Redo level or null if no redo was performed.
|
||||
*/
|
||||
redo (): UndoLevel {
|
||||
let level: UndoLevel;
|
||||
|
||||
if (index < data.length - 1) {
|
||||
level = data[++index];
|
||||
Levels.applyToEditor(editor, level, false);
|
||||
setDirty(true);
|
||||
editor.fire('redo', { level });
|
||||
}
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all undo levels.
|
||||
*
|
||||
* @method clear
|
||||
*/
|
||||
clear () {
|
||||
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 () {
|
||||
// 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 () {
|
||||
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 (callback: () => void): UndoLevel {
|
||||
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.
|
||||
*/
|
||||
ignore (callback: () => void) {
|
||||
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 (callback1: () => void, callback2: () => void) {
|
||||
let 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Arr, Option } from '@ephox/katamari';
|
||||
import SelectionBookmark from '../selection/SelectionBookmark';
|
||||
import WindowManagerImpl from '../ui/WindowManagerImpl';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* });
|
||||
*/
|
||||
|
||||
export default function (editor) {
|
||||
const windows = [];
|
||||
|
||||
const getImplementation = function () {
|
||||
const theme = editor.theme;
|
||||
return theme && theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl();
|
||||
};
|
||||
|
||||
const funcBind = function (scope, f) {
|
||||
return function () {
|
||||
return f ? f.apply(scope, arguments) : undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const fireOpenEvent = function (win) {
|
||||
editor.fire('OpenWindow', {
|
||||
win
|
||||
});
|
||||
};
|
||||
|
||||
const fireCloseEvent = function (win) {
|
||||
editor.fire('CloseWindow', {
|
||||
win
|
||||
});
|
||||
};
|
||||
|
||||
const addWindow = function (win) {
|
||||
windows.push(win);
|
||||
fireOpenEvent(win);
|
||||
};
|
||||
|
||||
const closeWindow = function (win) {
|
||||
Arr.findIndex(windows, function (otherWindow) {
|
||||
return otherWindow === win;
|
||||
}).each(function (index) {
|
||||
// Mutate here since third party might have stored away the window array, consider breaking this api
|
||||
windows.splice(index, 1);
|
||||
|
||||
fireCloseEvent(win);
|
||||
|
||||
// Move focus back to editor when the last window is closed
|
||||
if (windows.length === 0) {
|
||||
editor.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getTopWindow = function () {
|
||||
return Option.from(windows[windows.length - 1]);
|
||||
};
|
||||
|
||||
const open = function (args, params) {
|
||||
editor.editorManager.setActive(editor);
|
||||
SelectionBookmark.store(editor);
|
||||
|
||||
const win = getImplementation().open(args, params, closeWindow);
|
||||
addWindow(win);
|
||||
return win;
|
||||
};
|
||||
|
||||
const alert = function (message, callback, scope) {
|
||||
const win = getImplementation().alert(message, funcBind(scope ? scope : this, callback), closeWindow);
|
||||
addWindow(win);
|
||||
};
|
||||
|
||||
const confirm = function (message, callback, scope) {
|
||||
const win = getImplementation().confirm(message, funcBind(scope ? scope : this, callback), closeWindow);
|
||||
addWindow(win);
|
||||
};
|
||||
|
||||
const close = function () {
|
||||
getTopWindow().each(function (win) {
|
||||
getImplementation().close(win);
|
||||
closeWindow(win);
|
||||
});
|
||||
};
|
||||
|
||||
const getParams = function () {
|
||||
return getTopWindow().map(getImplementation().getParams).getOr(null);
|
||||
};
|
||||
|
||||
const setParams = function (params) {
|
||||
getTopWindow().each(function (win) {
|
||||
getImplementation().setParams(win, params);
|
||||
});
|
||||
};
|
||||
|
||||
const getWindows = function () {
|
||||
return windows;
|
||||
};
|
||||
|
||||
editor.on('remove', function () {
|
||||
Arr.each(windows.slice(0), function (win) {
|
||||
getImplementation().close(win);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
// Used by the legacy3x compat layer and possible third party
|
||||
// TODO: Deprecate this, and possible switch to a immutable window array for getWindows
|
||||
windows,
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
open,
|
||||
|
||||
/**
|
||||
* 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!');
|
||||
*/
|
||||
alert,
|
||||
|
||||
/**
|
||||
* 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");
|
||||
* });
|
||||
*/
|
||||
confirm,
|
||||
|
||||
/**
|
||||
* Closes the top most window.
|
||||
*
|
||||
* @method close
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
getParams,
|
||||
|
||||
/**
|
||||
* Sets the params of the last opened window.
|
||||
*
|
||||
* @method setParams
|
||||
* @param {Object} params Params object to set for the last opened window.
|
||||
*/
|
||||
setParams,
|
||||
|
||||
/**
|
||||
* Returns the currently opened window objects.
|
||||
*
|
||||
* @method getWindows
|
||||
* @return {Array} Array of the currently opened windows.
|
||||
*/
|
||||
getWindows
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Fun } from '@ephox/katamari';
|
||||
import Bookmarks from '../../bookmark/Bookmarks';
|
||||
import { Selection } from './Selection';
|
||||
import { Node } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* This class handles selection bookmarks.
|
||||
*
|
||||
* @class tinymce.dom.BookmarkManager
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constructs a new BookmarkManager instance for a specific selection instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method BookmarkManager
|
||||
* @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
|
||||
*/
|
||||
export function BookmarkManager(selection: Selection) {
|
||||
return {
|
||||
/**
|
||||
* 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: Fun.curry(Bookmarks.getBookmark, selection) as (type: number, normalized?: boolean) => any,
|
||||
|
||||
/**
|
||||
* 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: Fun.curry(Bookmarks.moveToBookmark, selection) as (bookmark: any) => boolean,
|
||||
};
|
||||
}
|
||||
|
||||
export namespace BookmarkManager {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const isBookmarkNode = Bookmarks.isBookmarkNode as (node: Node) => boolean;
|
||||
}
|
||||
|
||||
export default BookmarkManager;
|
||||
|
|
@ -0,0 +1,565 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Element as SugarElement, Selectors } from '@ephox/sugar';
|
||||
import NodeType from '../../dom/NodeType';
|
||||
import RangePoint from '../../dom/RangePoint';
|
||||
import Env from '../Env';
|
||||
import Delay from '../util/Delay';
|
||||
import Tools from '../util/Tools';
|
||||
import VK from '../util/VK';
|
||||
import { Selection } from './Selection';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import Events from 'tinymce/core/api/Events';
|
||||
import { Element, Event, Node, document } from '@ephox/dom-globals';
|
||||
|
||||
interface ControlSelection {
|
||||
isResizable: (elm: Element) => boolean;
|
||||
showResizeRect: (elm: Element) => void;
|
||||
hideResizeRect: () => void;
|
||||
updateResizeRect: (evt: Event) => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const isContentEditableFalse = NodeType.isContentEditableFalse;
|
||||
const isContentEditableTrue = NodeType.isContentEditableTrue;
|
||||
|
||||
const getContentEditableRoot = function (root: Node, node: Node) {
|
||||
while (node && node !== root) {
|
||||
if (isContentEditableTrue(node) || isContentEditableFalse(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ControlSelection = (selection: Selection, editor: Editor): ControlSelection => {
|
||||
const dom = editor.dom, each = Tools.each;
|
||||
let selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle;
|
||||
let startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
|
||||
let width,
|
||||
height;
|
||||
const editableDoc = editor.getDoc(),
|
||||
rootDocument = document;
|
||||
const abs = Math.abs,
|
||||
round = Math.round,
|
||||
rootElement = editor.getBody();
|
||||
let 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
|
||||
const rootClass = '.mce-content-body';
|
||||
editor.contentStyles.push(
|
||||
rootClass + ' div.mce-resizehandle {' +
|
||||
'position: absolute;' +
|
||||
'border: 1px solid black;' +
|
||||
'box-sizing: content-box;' +
|
||||
'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' +
|
||||
'}'
|
||||
);
|
||||
|
||||
const isImage = function (elm) {
|
||||
return elm && (elm.nodeName === 'IMG' || editor.dom.is(elm, 'figure.image'));
|
||||
};
|
||||
|
||||
const isEventOnImageOutsideRange = function (evt, range) {
|
||||
return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range);
|
||||
};
|
||||
|
||||
const contextMenuSelectImage = function (evt) {
|
||||
const target = evt.target;
|
||||
|
||||
if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) {
|
||||
evt.preventDefault();
|
||||
editor.selection.select(target);
|
||||
}
|
||||
};
|
||||
|
||||
const getResizeTarget = function (elm) {
|
||||
return editor.dom.is(elm, 'figure.image') ? elm.querySelector('img') : elm;
|
||||
};
|
||||
|
||||
const isResizable = function (elm) {
|
||||
let selector = editor.settings.object_resizing;
|
||||
|
||||
if (selector === false || Env.iOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof selector !== 'string') {
|
||||
selector = 'table,img,figure.image,div';
|
||||
}
|
||||
|
||||
if (elm.getAttribute('data-mce-resize') === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elm === editor.getBody()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Selectors.is(SugarElement.fromDom(elm), selector);
|
||||
};
|
||||
|
||||
const resizeGhostElement = function (e) {
|
||||
let deltaX, deltaY, proportional;
|
||||
let 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 (isImage(selectedElm) && editor.settings.resize_img_proportional !== false) {
|
||||
proportional = !VK.modifierPressed(e);
|
||||
} else {
|
||||
proportional = VK.modifierPressed(e) || (isImage(selectedElm) && 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(getResizeTarget(selectedElmGhost), {
|
||||
width,
|
||||
height
|
||||
});
|
||||
|
||||
// Update resize helper position
|
||||
resizeHelperX = selectedHandle.startPos.x + deltaX;
|
||||
resizeHelperY = selectedHandle.startPos.y + deltaY;
|
||||
resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0;
|
||||
resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0;
|
||||
|
||||
dom.setStyles(resizeHelper, {
|
||||
left: resizeHelperX,
|
||||
top: resizeHelperY,
|
||||
display: 'block'
|
||||
});
|
||||
|
||||
resizeHelper.innerHTML = width + ' × ' + height;
|
||||
|
||||
// Update ghost X position if needed
|
||||
if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
|
||||
dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
|
||||
}
|
||||
|
||||
// Update ghost Y position if needed
|
||||
if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
|
||||
dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
|
||||
}
|
||||
|
||||
// Calculate how must overflow we got
|
||||
deltaX = rootElement.scrollWidth - startScrollWidth;
|
||||
deltaY = rootElement.scrollHeight - startScrollHeight;
|
||||
|
||||
// Re-position the resize helper based on the overflow
|
||||
if (deltaX + deltaY !== 0) {
|
||||
dom.setStyles(resizeHelper, {
|
||||
left: resizeHelperX - deltaX,
|
||||
top: resizeHelperY - deltaY
|
||||
});
|
||||
}
|
||||
|
||||
if (!resizeStarted) {
|
||||
Events.fireObjectResizeStart(editor, selectedElm, startW, startH);
|
||||
resizeStarted = true;
|
||||
}
|
||||
};
|
||||
|
||||
const endGhostResize = function () {
|
||||
resizeStarted = false;
|
||||
|
||||
const setSizeProp = function (name, value) {
|
||||
if (value) {
|
||||
// Resize by using style or attribute
|
||||
if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
|
||||
dom.setStyle(getResizeTarget(selectedElm), name, value);
|
||||
} else {
|
||||
dom.setAttrib(getResizeTarget(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);
|
||||
|
||||
showResizeRect(selectedElm);
|
||||
|
||||
Events.fireObjectResized(editor, selectedElm, width, height);
|
||||
dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style'));
|
||||
editor.nodeChanged();
|
||||
};
|
||||
|
||||
const showResizeRect = function (targetElm) {
|
||||
let 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) {
|
||||
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) {
|
||||
let handleElm;
|
||||
|
||||
const startDrag = function (e) {
|
||||
startX = e.screenX;
|
||||
startY = e.screenY;
|
||||
startW = getResizeTarget(selectedElm).clientWidth;
|
||||
startH = getResizeTarget(selectedElm).clientHeight;
|
||||
ratio = startH / startW;
|
||||
selectedHandle = handle;
|
||||
|
||||
handle.startPos = {
|
||||
x: targetWidth * handle[0] + selectedElmX,
|
||||
y: targetHeight * handle[1] + selectedElmY
|
||||
};
|
||||
|
||||
startScrollWidth = rootElement.scrollWidth;
|
||||
startScrollHeight = rootElement.scrollHeight;
|
||||
|
||||
selectedElmGhost = selectedElm.cloneNode(true);
|
||||
dom.addClass(selectedElmGhost, 'mce-clonedresizable');
|
||||
dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all');
|
||||
selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
|
||||
selectedElmGhost.unSelectabe = true;
|
||||
dom.setStyles(selectedElmGhost, {
|
||||
left: selectedElmX,
|
||||
top: selectedElmY,
|
||||
margin: 0
|
||||
});
|
||||
|
||||
selectedElmGhost.removeAttribute('data-mce-selected');
|
||||
rootElement.appendChild(selectedElmGhost);
|
||||
|
||||
dom.bind(editableDoc, 'mousemove', resizeGhostElement);
|
||||
dom.bind(editableDoc, 'mouseup', endGhostResize);
|
||||
|
||||
if (rootDocument !== editableDoc) {
|
||||
dom.bind(rootDocument, 'mousemove', resizeGhostElement);
|
||||
dom.bind(rootDocument, 'mouseup', endGhostResize);
|
||||
}
|
||||
|
||||
resizeHelper = dom.add(rootElement, 'div', {
|
||||
'class': 'mce-resize-helper',
|
||||
'data-mce-bogus': 'all'
|
||||
}, startW + ' × ' + startH);
|
||||
};
|
||||
|
||||
// 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
|
||||
// Edge doesn't have this issue however setting contenteditable will move the selection to that element on Edge 17 see #TINY-1679
|
||||
if (Env.ie === 11) {
|
||||
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');
|
||||
};
|
||||
|
||||
const hideResizeRect = function () {
|
||||
let 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateResizeRect = function (e) {
|
||||
let startElm, controlElm;
|
||||
|
||||
const isChildOrEqual = function (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('table,img,figure.image,hr')[0];
|
||||
|
||||
if (isChildOrEqual(controlElm, rootElement)) {
|
||||
disableGeckoResize();
|
||||
startElm = selection.getStart(true);
|
||||
|
||||
if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) {
|
||||
showResizeRect(controlElm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hideResizeRect();
|
||||
};
|
||||
|
||||
const isWithinContentEditableFalse = function (elm) {
|
||||
return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm));
|
||||
};
|
||||
|
||||
const unbindResizeHandleEvents = function () {
|
||||
for (const name in resizeHandles) {
|
||||
const handle = resizeHandles[name];
|
||||
|
||||
if (handle.elm) {
|
||||
dom.unbind(handle.elm);
|
||||
delete handle.elm;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const disableGeckoResize = function () {
|
||||
try {
|
||||
// Disable object resizing on Gecko
|
||||
editor.getDoc().execCommand('enableObjectResizing', false, false);
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
};
|
||||
|
||||
editor.on('init', function () {
|
||||
disableGeckoResize();
|
||||
|
||||
// Sniff sniff, hard to feature detect this stuff
|
||||
if (Env.ie && 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) {
|
||||
const 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) {
|
||||
const delayedSelect = function (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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const throttledUpdateResizeRect = Delay.throttle(function (e) {
|
||||
if (!editor.composing) {
|
||||
updateResizeRect(e);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('nodechange ResizeEditor ResizeWindow drop FullscreenStateChanged', 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', contextMenuSelectImage);
|
||||
|
||||
// Hide rect on focusout since it would float on top of windows otherwise
|
||||
// editor.on('focusout', hideResizeRect);
|
||||
});
|
||||
|
||||
editor.on('remove', unbindResizeHandleEvents);
|
||||
|
||||
const destroy = function () {
|
||||
selectedElm = selectedElmGhost = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isResizable,
|
||||
showResizeRect,
|
||||
hideResizeRect,
|
||||
updateResizeRect,
|
||||
destroy
|
||||
};
|
||||
};
|
||||
|
||||
export default ControlSelection;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Bookmarks from '../../bookmark/Bookmarks';
|
||||
import Tools from '../util/Tools';
|
||||
import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils';
|
||||
|
||||
/**
|
||||
* Utility class for various element specific functions.
|
||||
*
|
||||
* @private
|
||||
* @class tinymce.dom.ElementUtils
|
||||
*/
|
||||
|
||||
const each = Tools.each;
|
||||
|
||||
const ElementUtils = function (dom: DOMUtils) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const getAttribs = function (node) {
|
||||
const attribs = {};
|
||||
|
||||
each(dom.getAttribs(node), function (attr) {
|
||||
const 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.
|
||||
*/
|
||||
const compareObjects = function (obj1, obj2) {
|
||||
let 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 !Bookmarks.isBookmarkNode(node1) && !Bookmarks.isBookmarkNode(node2);
|
||||
};
|
||||
};
|
||||
|
||||
export default ElementUtils;
|
||||
|
|
@ -0,0 +1,610 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Env from '../Env';
|
||||
import Delay from '../util/Delay';
|
||||
import { document, window } from '@ephox/dom-globals';
|
||||
|
||||
export type EditorEvent<T> = T & {
|
||||
isDefaultPrevented: () => boolean;
|
||||
isPropagationStopped: () => boolean;
|
||||
isImmediatePropagationStopped: () => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* This class wraps the browsers native event logic with more convenient methods.
|
||||
*
|
||||
* @class tinymce.dom.EventUtils
|
||||
*/
|
||||
|
||||
const eventExpandoPrefix = 'mce-data-';
|
||||
const mouseEventRe = /^(?:mouse|contextmenu)|click/;
|
||||
const deprecated = {
|
||||
keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1,
|
||||
webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1
|
||||
};
|
||||
|
||||
// Checks if it is our own isDefaultPrevented function
|
||||
const hasIsDefaultPrevented = function (event) {
|
||||
return event.isDefaultPrevented === returnTrue || event.isDefaultPrevented === returnFalse;
|
||||
};
|
||||
|
||||
// Dummy function that gets replaced on the delegation state functions
|
||||
const returnFalse = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Dummy function that gets replaced on the delegation state functions
|
||||
const returnTrue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a native event to a callback on the speified target.
|
||||
*/
|
||||
const addEvent = function (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.
|
||||
*/
|
||||
const removeEvent = function (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 composedPath.
|
||||
*/
|
||||
const getTargetFromShadowDom = function (event, defaultTarget) {
|
||||
// When target element is inside Shadow DOM we need to take first element from composedPath
|
||||
// otherwise we'll get Shadow Root parent, not actual target element
|
||||
if (event.composedPath) {
|
||||
const composedPath = event.composedPath();
|
||||
if (composedPath && composedPath.length > 0) {
|
||||
return composedPath[0];
|
||||
}
|
||||
}
|
||||
|
||||
return defaultTarget;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a native event object or just adds the event specific methods on a custom event.
|
||||
*/
|
||||
const fix = function (originalEvent, data?) {
|
||||
let name;
|
||||
const event = data || {};
|
||||
|
||||
// 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 === undefined && originalEvent.clientX !== undefined) {
|
||||
const eventDoc = event.target.ownerDocument || document;
|
||||
const doc = eventDoc.documentElement;
|
||||
const 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.
|
||||
*/
|
||||
const bindOnReady = function (win, callback, eventUtils) {
|
||||
const doc = win.document, event = { type: 'ready' };
|
||||
|
||||
if (eventUtils.domLoaded) {
|
||||
callback(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const isDocReady = function () {
|
||||
// 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
|
||||
const readyHandler = function () {
|
||||
if (!eventUtils.domLoaded) {
|
||||
eventUtils.domLoaded = true;
|
||||
callback(event);
|
||||
}
|
||||
};
|
||||
|
||||
const waitForDomLoaded = function () {
|
||||
if (isDocReady()) {
|
||||
removeEvent(doc, 'readystatechange', waitForDomLoaded);
|
||||
readyHandler();
|
||||
}
|
||||
};
|
||||
|
||||
const tryScroll = function () {
|
||||
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.
|
||||
*/
|
||||
const EventUtils: any = function () {
|
||||
const self = this;
|
||||
let 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.
|
||||
*/
|
||||
const executeHandlers = function (evt, id) {
|
||||
let callbackList, i, l, callback;
|
||||
const 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) {
|
||||
let id, callbackList, i, name, fakeName, nativeHandler, capture;
|
||||
const win = window;
|
||||
|
||||
// Native event handler function patches the event and executes the callbacks for the expando
|
||||
const defaultNativeHandler = function (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) {
|
||||
let 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 }];
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let 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) {
|
||||
const nativeHandler = callbackList.nativeHandler;
|
||||
const 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) {
|
||||
let 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) {
|
||||
let i, children;
|
||||
const 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 () { });
|
||||
|
||||
export default EventUtils;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* NativeTypes.ts
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2018 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
import { Selection } from '@ephox/dom-globals';
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
export interface NativeSelection extends Selection {}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Fun } from '@ephox/katamari';
|
||||
import CaretRangeFromPoint from '../../selection/CaretRangeFromPoint';
|
||||
import NormalizeRange from '../../selection/NormalizeRange';
|
||||
import RangeCompare from '../../selection/RangeCompare';
|
||||
import * as RangeNodes from '../../selection/RangeNodes';
|
||||
import RangeWalk from '../../selection/RangeWalk';
|
||||
import SplitRange from '../../selection/SplitRange';
|
||||
import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils';
|
||||
import { Range, Document, Node } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* This class contains a few utility methods for ranges.
|
||||
*
|
||||
* @class tinymce.dom.RangeUtils
|
||||
*/
|
||||
export function RangeUtils(dom: DOMUtils) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const walk = function (rng, callback) {
|
||||
return RangeWalk.walk(dom, rng, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits the specified range at it's start/end points.
|
||||
*
|
||||
* @private
|
||||
* @param {Range/RangeObject} rng Range to split.
|
||||
* @return {Object} Range position object.
|
||||
*/
|
||||
const split = SplitRange.split;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const normalize = function (rng: Range): boolean {
|
||||
return NormalizeRange.normalize(dom, rng).fold(
|
||||
Fun.constant(false),
|
||||
function (normalizedRng) {
|
||||
rng.setStart(normalizedRng.startContainer, normalizedRng.startOffset);
|
||||
rng.setEnd(normalizedRng.endContainer, normalizedRng.endOffset);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
walk,
|
||||
split,
|
||||
normalize
|
||||
};
|
||||
}
|
||||
|
||||
export namespace RangeUtils {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const compareRanges = RangeCompare.isEq;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const getCaretRangeFromPoint = CaretRangeFromPoint.fromPoint as (clientX: number, clientY: number, doc: Document) => Range;
|
||||
|
||||
export const getSelectedNode = RangeNodes.getSelectedNode as (range: Range) => Node;
|
||||
export const getNode = RangeNodes.getNode;
|
||||
}
|
||||
|
||||
export default RangeUtils;
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import DOMUtils from './DOMUtils';
|
||||
import Tools from '../util/Tools';
|
||||
import { document } from '@ephox/dom-globals';
|
||||
|
||||
/*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.');
|
||||
* });
|
||||
*/
|
||||
|
||||
const DOM = DOMUtils.DOM;
|
||||
const each = Tools.each, grep = Tools.grep;
|
||||
|
||||
const isFunction = function (f) {
|
||||
return typeof f === 'function';
|
||||
};
|
||||
|
||||
const ScriptLoader: any = function () {
|
||||
const QUEUED = 0;
|
||||
const LOADING = 1;
|
||||
const LOADED = 2;
|
||||
const FAILED = 3;
|
||||
const states = {};
|
||||
const queue = [];
|
||||
const scriptLoadedCallbacks = {};
|
||||
const queueLoadedCallbacks = [];
|
||||
let loading = 0;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const loadScript = function (url, success, failure) {
|
||||
const dom = DOM;
|
||||
let elm, id;
|
||||
|
||||
// Execute callback when script is loaded
|
||||
const done = function () {
|
||||
dom.remove(id);
|
||||
|
||||
if (elm) {
|
||||
elm.onreadystatechange = elm.onload = elm = null;
|
||||
}
|
||||
|
||||
success();
|
||||
};
|
||||
|
||||
const error = function () {
|
||||
|
||||
// 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) { // tslint:disable-line:no-console
|
||||
// tslint:disable-next-line:no-console
|
||||
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);
|
||||
|
||||
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) {
|
||||
const state = states[url];
|
||||
|
||||
// Add url to load queue
|
||||
if (state === undefined) {
|
||||
queue.push(url);
|
||||
states[url] = QUEUED;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Store away callback for later execution
|
||||
if (!scriptLoadedCallbacks[url]) {
|
||||
scriptLoadedCallbacks[url] = [];
|
||||
}
|
||||
|
||||
scriptLoadedCallbacks[url].push({
|
||||
success,
|
||||
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) {
|
||||
let loadScripts;
|
||||
const failures = [];
|
||||
|
||||
const execCallbacks = function (name, url) {
|
||||
// Execute URL callback functions
|
||||
each(scriptLoadedCallbacks[url], function (callback) {
|
||||
if (isFunction(callback[name])) {
|
||||
callback[name].call(callback.scope);
|
||||
}
|
||||
});
|
||||
|
||||
scriptLoadedCallbacks[url] = undefined;
|
||||
};
|
||||
|
||||
queueLoadedCallbacks.push({
|
||||
success,
|
||||
failure,
|
||||
scope: scope || this
|
||||
});
|
||||
|
||||
loadScripts = function () {
|
||||
const 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) {
|
||||
// We need to clone the notifications and empty the pending callbacks so that callbacks can load more resources
|
||||
const notifyCallbacks = queueLoadedCallbacks.slice(0);
|
||||
queueLoadedCallbacks.length = 0;
|
||||
|
||||
each(notifyCallbacks, 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadScripts();
|
||||
};
|
||||
};
|
||||
|
||||
ScriptLoader.ScriptLoader = new ScriptLoader();
|
||||
|
||||
export default ScriptLoader;
|
||||
|
|
@ -0,0 +1,639 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Compare, Element as SugarElement } from '@ephox/sugar';
|
||||
import Env from '../Env';
|
||||
import BookmarkManager from './BookmarkManager';
|
||||
import CaretPosition from '../../caret/CaretPosition';
|
||||
import ControlSelection from './ControlSelection';
|
||||
import ScrollIntoView from '../../dom/ScrollIntoView';
|
||||
import EditorFocus from '../../focus/EditorFocus';
|
||||
import CaretRangeFromPoint from '../../selection/CaretRangeFromPoint';
|
||||
import EventProcessRanges from '../../selection/EventProcessRanges';
|
||||
import GetSelectionContent from '../../selection/GetSelectionContent';
|
||||
import MultiRange from '../../selection/MultiRange';
|
||||
import NormalizeRange from '../../selection/NormalizeRange';
|
||||
import SelectionBookmark from '../../selection/SelectionBookmark';
|
||||
import SetSelectionContent from '../../selection/SetSelectionContent';
|
||||
import Tools from '../util/Tools';
|
||||
import * as ElementSelection from '../../selection/ElementSelection';
|
||||
import { moveEndPoint, hasAnyRanges } from 'tinymce/core/selection/SelectionUtils';
|
||||
import { Editor } from 'tinymce/core/api/Editor';
|
||||
import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils';
|
||||
import { Selection as NativeSelection, HTMLElement, Node, Range, Element, ClientRect, Window } from '@ephox/dom-globals';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
*/
|
||||
|
||||
const each = Tools.each;
|
||||
|
||||
const isNativeIeSelection = (rng: any): boolean => {
|
||||
return !!(<any> rng).select;
|
||||
};
|
||||
|
||||
const isAttachedToDom = function (node: Node): boolean {
|
||||
return !!(node && node.ownerDocument) && Compare.contains(SugarElement.fromDom(node.ownerDocument), SugarElement.fromDom(node));
|
||||
};
|
||||
|
||||
const isValidRange = function (rng: Range) {
|
||||
if (!rng) {
|
||||
return false;
|
||||
} else if (isNativeIeSelection(rng)) { // Native IE range still produced by placeCaretAt
|
||||
return true;
|
||||
} else {
|
||||
return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer);
|
||||
}
|
||||
};
|
||||
|
||||
export interface Selection {
|
||||
bookmarkManager: any;
|
||||
controlSelection: ControlSelection;
|
||||
dom: any;
|
||||
win: Window;
|
||||
serializer: any;
|
||||
editor: any;
|
||||
collapse: (toStart?: boolean) => void;
|
||||
setCursorLocation: (node?: Node, offset?: number) => void;
|
||||
getContent: (args?: any) => any;
|
||||
setContent: (content: any, args?: any) => void;
|
||||
getBookmark: (type?: number, normalized?: boolean) => any;
|
||||
moveToBookmark: (bookmark: any) => boolean;
|
||||
select: (node: Node, content?: boolean) => Node;
|
||||
isCollapsed: () => boolean;
|
||||
isForward: () => boolean;
|
||||
setNode: (elm: Element) => Element;
|
||||
getNode: () => Element;
|
||||
getSel: () => NativeSelection;
|
||||
setRng: (rng: Range, forward?: boolean) => void;
|
||||
getRng: () => Range;
|
||||
getStart: (real?: boolean) => Element;
|
||||
getEnd: (real?: boolean) => Element;
|
||||
getSelectedBlocks: (startElm?: Element, endElm?: Element) => Element[];
|
||||
normalize: () => Range;
|
||||
selectorChanged: (selector: string, callback: (active: boolean, args: {
|
||||
node: Node;
|
||||
selector: String;
|
||||
parents: Element[];
|
||||
}) => void) => any;
|
||||
getScrollContainer: () => HTMLElement;
|
||||
scrollIntoView: (elm: Element, alignToTop?: boolean) => void;
|
||||
placeCaretAt: (clientX: number, clientY: number) => void;
|
||||
getBoundingClientRect: () => ClientRect;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.dom.Serializer} serializer DOM serialization class to use for getContent.
|
||||
* @param {tinymce.Editor} editor Editor instance of the selection.
|
||||
*/
|
||||
export const Selection = function (dom: DOMUtils, win: Window, serializer, editor: Editor): Selection {
|
||||
let bookmarkManager, controlSelection: ControlSelection;
|
||||
let selectedRange, explicitRange, selectorChangedData;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const setCursorLocation = (node?: Node, offset?: number) => {
|
||||
const rng = dom.createRng();
|
||||
|
||||
if (!node) {
|
||||
moveEndPoint(dom, rng, editor.getBody(), true);
|
||||
setRng(rng);
|
||||
} else {
|
||||
rng.setStart(node, offset);
|
||||
rng.setEnd(node, offset);
|
||||
setRng(rng);
|
||||
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'}));
|
||||
*/
|
||||
const getContent = (args) => GetSelectionContent.getContent(editor, args);
|
||||
|
||||
/**
|
||||
* 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>');
|
||||
*/
|
||||
const setContent = (content, args?) => SetSelectionContent.setContent(editor, content, 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.
|
||||
*/
|
||||
const getStart = (real?: boolean): Element => ElementSelection.getStart(editor.getBody(), getRng(), real);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const getEnd = (real?: boolean): Element => ElementSelection.getEnd(editor.getBody(), getRng(), real);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
*/
|
||||
const getBookmark = (type?: number, normalized?: boolean) => 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);
|
||||
*/
|
||||
const moveToBookmark = (bookmark): boolean => 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]);
|
||||
*/
|
||||
const select = (node: Node, content?: boolean) => {
|
||||
ElementSelection.select(dom, node, content).each(setRng);
|
||||
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.
|
||||
*/
|
||||
const isCollapsed = (): boolean => {
|
||||
const rng: any = getRng(), sel = 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.
|
||||
*/
|
||||
const collapse = (toStart?: boolean) => {
|
||||
const rng = getRng();
|
||||
|
||||
rng.collapse(!!toStart);
|
||||
setRng(rng);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the browsers internal selection object.
|
||||
*
|
||||
* @method getSel
|
||||
* @return {Selection} Internal browser selection object.
|
||||
*/
|
||||
const getSel = (): NativeSelection => win.getSelection ? win.getSelection() : (<any> win.document).selection;
|
||||
|
||||
/**
|
||||
* Returns the browsers internal range object.
|
||||
*
|
||||
* @method getRng
|
||||
* @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/
|
||||
*/
|
||||
const getRng = (): Range => {
|
||||
let selection, rng, elm, doc;
|
||||
|
||||
const tryCompareBoundaryPoints = function (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 (!win) {
|
||||
return null;
|
||||
}
|
||||
|
||||
doc = win.document;
|
||||
|
||||
if (typeof doc === 'undefined' || doc === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (editor.bookmark !== undefined && EditorFocus.hasFocus(editor) === false) {
|
||||
const bookmark = SelectionBookmark.getRng(editor);
|
||||
|
||||
if (bookmark.isSome()) {
|
||||
return bookmark.map((r) => EventProcessRanges.processRanges(editor, [r])[0]).getOr(doc.createRange());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ((selection = 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
|
||||
}
|
||||
|
||||
rng = EventProcessRanges.processRanges(editor, [rng])[0];
|
||||
|
||||
// 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 = dom.getRoot();
|
||||
rng.setStart(elm, 0);
|
||||
rng.setEnd(elm, 0);
|
||||
}
|
||||
|
||||
if (selectedRange && explicitRange) {
|
||||
if (tryCompareBoundaryPoints(rng.START_TO_START, rng, selectedRange) === 0 &&
|
||||
tryCompareBoundaryPoints(rng.END_TO_END, rng, 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 = explicitRange;
|
||||
} else {
|
||||
selectedRange = null;
|
||||
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.
|
||||
*/
|
||||
const setRng = (rng: Range, forward?: boolean) => {
|
||||
let sel, node, evt;
|
||||
|
||||
if (!isValidRange(rng)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is IE specific range
|
||||
const ieRange: any = isNativeIeSelection(rng) ? rng : null;
|
||||
if (ieRange) {
|
||||
explicitRange = null;
|
||||
|
||||
try {
|
||||
ieRange.select();
|
||||
} catch (ex) {
|
||||
// Needed for some odd IE bug #1843306
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sel = getSel();
|
||||
|
||||
evt = editor.fire('SetSelectionRange', { range: rng, forward });
|
||||
rng = evt.range;
|
||||
|
||||
if (sel) {
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.fire('AfterSetSelectionRange', { range: rng, forward });
|
||||
};
|
||||
|
||||
/**
|
||||
* 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'}));
|
||||
*/
|
||||
const setNode = (elm: Element): Element => {
|
||||
setContent(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);
|
||||
*/
|
||||
const getNode = (): Element => ElementSelection.getNode(editor.getBody(), getRng());
|
||||
|
||||
const getSelectedBlocks = (startElm: Element, endElm: Element) => ElementSelection.getSelectedBlocks(dom, getRng(), startElm, endElm);
|
||||
|
||||
const isForward = (): boolean => {
|
||||
const sel = getSel();
|
||||
let 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;
|
||||
};
|
||||
|
||||
const normalize = (): Range => {
|
||||
const rng = getRng();
|
||||
const sel = getSel();
|
||||
|
||||
if (!MultiRange.hasMultipleRanges(sel) && hasAnyRanges(editor)) {
|
||||
const normRng = NormalizeRange.normalize(dom, rng);
|
||||
|
||||
normRng.each(function (normRng) {
|
||||
setRng(normRng, isForward());
|
||||
});
|
||||
|
||||
return normRng.getOr(rng);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
const selectorChanged = (selector: string, callback: (active: boolean, args: { node: Node, selector: String, parents: Element[] }) => void) => {
|
||||
let currentSelectors;
|
||||
|
||||
if (!selectorChangedData) {
|
||||
selectorChangedData = {};
|
||||
currentSelectors = {};
|
||||
|
||||
editor.on('NodeChange', function (e) {
|
||||
const node = e.element, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
|
||||
|
||||
// Check for new matching selectors
|
||||
each(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, selector, 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, selector, parents });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add selector listeners
|
||||
if (!selectorChangedData[selector]) {
|
||||
selectorChangedData[selector] = [];
|
||||
}
|
||||
|
||||
selectorChangedData[selector].push(callback);
|
||||
|
||||
return exports;
|
||||
};
|
||||
|
||||
const getScrollContainer = (): HTMLElement => {
|
||||
let scrollContainer;
|
||||
let node = dom.getRoot();
|
||||
|
||||
while (node && node.nodeName !== 'BODY') {
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
scrollContainer = node;
|
||||
break;
|
||||
}
|
||||
|
||||
node = node.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
return scrollContainer;
|
||||
};
|
||||
|
||||
const scrollIntoView = (elm: HTMLElement, alignToTop?: boolean) => ScrollIntoView.scrollElementIntoView(editor, elm, alignToTop);
|
||||
const placeCaretAt = (clientX: number, clientY: number) => setRng(CaretRangeFromPoint.fromPoint(clientX, clientY, editor.getDoc()));
|
||||
|
||||
const getBoundingClientRect = (): ClientRect => {
|
||||
const rng = getRng();
|
||||
return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect();
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
win = selectedRange = explicitRange = null;
|
||||
controlSelection.destroy();
|
||||
};
|
||||
|
||||
const exports = {
|
||||
bookmarkManager: null,
|
||||
controlSelection: null,
|
||||
dom,
|
||||
win,
|
||||
serializer,
|
||||
editor,
|
||||
collapse,
|
||||
setCursorLocation,
|
||||
getContent,
|
||||
setContent,
|
||||
getBookmark,
|
||||
moveToBookmark,
|
||||
select,
|
||||
isCollapsed,
|
||||
isForward,
|
||||
setNode,
|
||||
getNode,
|
||||
getSel,
|
||||
setRng,
|
||||
getRng,
|
||||
getStart,
|
||||
getEnd,
|
||||
getSelectedBlocks,
|
||||
normalize,
|
||||
selectorChanged,
|
||||
getScrollContainer,
|
||||
scrollIntoView,
|
||||
placeCaretAt,
|
||||
getBoundingClientRect,
|
||||
destroy
|
||||
};
|
||||
|
||||
bookmarkManager = BookmarkManager(exports);
|
||||
controlSelection = ControlSelection(exports, editor);
|
||||
|
||||
exports.bookmarkManager = bookmarkManager;
|
||||
exports.controlSelection = controlSelection;
|
||||
|
||||
return exports;
|
||||
};
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import DomSerializer from '../../dom/DomSerializer';
|
||||
import Schema from '../html/Schema';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export default function (settings, editor?) {
|
||||
const domSerializer = DomSerializer(settings, editor);
|
||||
|
||||
// Return public methods
|
||||
return {
|
||||
/**
|
||||
* Schema instance that was used to when the Serializer was constructed.
|
||||
*
|
||||
* @field {tinymce.html.Schema} schema
|
||||
*/
|
||||
schema: domSerializer.schema as 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: domSerializer.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: domSerializer.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: domSerializer.serialize,
|
||||
|
||||
/**
|
||||
* 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: domSerializer.addRules,
|
||||
|
||||
/**
|
||||
* 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: domSerializer.setRules,
|
||||
|
||||
/**
|
||||
* 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: domSerializer.addTempAttr,
|
||||
|
||||
/**
|
||||
* Returns an array of all added temp attrs names.
|
||||
*
|
||||
* @method getTempAttrs
|
||||
* @return {String[]} Array with attribute names.
|
||||
*/
|
||||
getTempAttrs: domSerializer.getTempAttrs
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* 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());
|
||||
*/
|
||||
|
||||
export default function (startNode, rootNode) {
|
||||
let node = startNode;
|
||||
|
||||
const findSibling = function (node, startName, siblingName, shallow) {
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findPreviousNode = function (node, startName, siblingName, shallow) {
|
||||
let 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;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { URL } from '@ephox/sand';
|
||||
import Arr from '../../util/Arr';
|
||||
import Fun from '../../util/Fun';
|
||||
import Uuid from '../../util/Uuid';
|
||||
import { Blob } from '@ephox/dom-globals';
|
||||
import { Type } from '@ephox/katamari';
|
||||
|
||||
export interface BlobCache {
|
||||
create: (o: string | BlobInfoData, blob?: Blob, base64?: string, filename?: string) => BlobInfo;
|
||||
add: (blobInfo: BlobInfo) => void;
|
||||
get: (id: string) => BlobInfo;
|
||||
getByUri: (blobUri: string) => BlobInfo;
|
||||
findFirst: (predicate: (blobInfo: BlobInfo) => boolean) => any;
|
||||
removeByUri: (blobUri: string) => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export interface BlobInfoData {
|
||||
id?: string;
|
||||
name?: string;
|
||||
blob: Blob;
|
||||
base64: string;
|
||||
blobUri?: string;
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
export interface BlobInfo {
|
||||
id: () => string;
|
||||
name: () => string;
|
||||
filename: () => string;
|
||||
blob: () => Blob;
|
||||
base64: () => string;
|
||||
blobUri: () => string;
|
||||
uri: () => string;
|
||||
}
|
||||
|
||||
export default function (): BlobCache {
|
||||
let cache: BlobInfo[] = [];
|
||||
const constant = Fun.constant;
|
||||
|
||||
const mimeToExt = function (mime) {
|
||||
const mimes = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/png': 'png'
|
||||
};
|
||||
|
||||
return mimes[mime.toLowerCase()] || 'dat';
|
||||
};
|
||||
|
||||
const create = function (o: BlobInfoData | string, blob?: Blob, base64?: string, filename?: string): BlobInfo {
|
||||
if (Type.isString(o)) {
|
||||
const id = o as string;
|
||||
|
||||
return toBlobInfo({
|
||||
id,
|
||||
name: filename,
|
||||
blob,
|
||||
base64
|
||||
});
|
||||
} else if (Type.isObject(o)) {
|
||||
return toBlobInfo(o);
|
||||
} else {
|
||||
throw new Error('Unknown input type');
|
||||
}
|
||||
};
|
||||
|
||||
const toBlobInfo = function (o: BlobInfoData): BlobInfo {
|
||||
let id, name;
|
||||
|
||||
if (!o.blob || !o.base64) {
|
||||
throw new Error('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)
|
||||
};
|
||||
};
|
||||
|
||||
const add = function (blobInfo: BlobInfo) {
|
||||
if (!get(blobInfo.id())) {
|
||||
cache.push(blobInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const get = function (id: string): BlobInfo {
|
||||
return findFirst(function (cachedBlobInfo) {
|
||||
return cachedBlobInfo.id() === id;
|
||||
});
|
||||
};
|
||||
|
||||
const findFirst = function (predicate: (blobInfo: BlobInfo) => boolean) {
|
||||
return Arr.filter(cache, predicate)[0];
|
||||
};
|
||||
|
||||
const getByUri = function (blobUri: string): BlobInfo {
|
||||
return findFirst(function (blobInfo) {
|
||||
return blobInfo.blobUri() === blobUri;
|
||||
});
|
||||
};
|
||||
|
||||
const removeByUri = function (blobUri: string) {
|
||||
cache = Arr.filter(cache, function (blobInfo) {
|
||||
if (blobInfo.blobUri() === blobUri) {
|
||||
URL.revokeObjectURL(blobInfo.blobUri());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const destroy = function () {
|
||||
Arr.each(cache, function (cachedBlobInfo) {
|
||||
URL.revokeObjectURL(cachedBlobInfo.blobUri());
|
||||
});
|
||||
|
||||
cache = [];
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
add,
|
||||
get,
|
||||
getByUri,
|
||||
findFirst,
|
||||
removeByUri,
|
||||
destroy
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export interface GeomRect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
const 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.
|
||||
*/
|
||||
const relativePosition = function (rect, targetRect, rel) {
|
||||
let 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.
|
||||
*/
|
||||
const findBestRelativePosition = function (rect, targetRect, constrainRect, rels) {
|
||||
let 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.
|
||||
*/
|
||||
const inflate = function (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.
|
||||
*/
|
||||
const intersect = function (rect, cropRect) {
|
||||
let 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.
|
||||
*/
|
||||
const clamp = function (rect, clampRect, fixedSize?) {
|
||||
let 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.
|
||||
*/
|
||||
const create = function (x, y, w, h) {
|
||||
return { x, y, w, h };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new rectangle object form a clientRects object.
|
||||
*
|
||||
* @method fromClientRect
|
||||
* @param {ClientRect} clientRect DOM ClientRect object.
|
||||
* @return {Rect} New rectangle object.
|
||||
*/
|
||||
const fromClientRect = function (clientRect) {
|
||||
return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
|
||||
};
|
||||
|
||||
export default {
|
||||
inflate,
|
||||
relativePosition,
|
||||
findBestRelativePosition,
|
||||
intersect,
|
||||
clamp,
|
||||
create,
|
||||
fromClientRect
|
||||
};
|
||||
|
|
@ -0,0 +1,712 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import LegacyFilter from '../../html/LegacyFilter';
|
||||
import * as ParserFilters from '../../html/ParserFilters';
|
||||
import { paddEmptyNode, isPaddedWithNbsp, hasOnlyChild, isEmpty, isLineBreakNode } from '../../html/ParserUtils';
|
||||
import Node from './Node';
|
||||
import SaxParser from './SaxParser';
|
||||
import Schema from './Schema';
|
||||
import Tools from '../util/Tools';
|
||||
|
||||
export type ParserArgs = any;
|
||||
export type ParserFilterCallback = (nodes: Node[], name: string, args: ParserArgs) => void;
|
||||
|
||||
export interface ParserFilter {
|
||||
name: string;
|
||||
callbacks: ParserFilterCallback[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
|
||||
|
||||
export default function (settings?, schema = Schema()) {
|
||||
const nodeFilters = {};
|
||||
const attributeFilters = [];
|
||||
let matchedNodes = {};
|
||||
let matchedAttributes = {};
|
||||
|
||||
settings = settings || {};
|
||||
settings.validate = 'validate' in settings ? settings.validate : true;
|
||||
settings.root_name = settings.root_name || 'body';
|
||||
|
||||
const fixInvalidChildren = function (nodes) {
|
||||
let ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
|
||||
let 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 = 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 = 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 (!isEmpty(schema, nonEmptyElements, whitespaceElements, newParent)) {
|
||||
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 (isEmpty(schema, nonEmptyElements, whitespaceElements, parent) || 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(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(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.
|
||||
*/
|
||||
const filterNode = (node: Node): Node => {
|
||||
let 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.
|
||||
*/
|
||||
const addNodeFilter = (name: string, callback: (nodes: Node[], name: string, args: ParserArgs) => void) => {
|
||||
each(explode(name), function (name) {
|
||||
let list = nodeFilters[name];
|
||||
|
||||
if (!list) {
|
||||
nodeFilters[name] = list = [];
|
||||
}
|
||||
|
||||
list.push(callback);
|
||||
});
|
||||
};
|
||||
|
||||
const getNodeFilters = (): ParserFilter[] => {
|
||||
const out = [];
|
||||
|
||||
for (const name in nodeFilters) {
|
||||
if (nodeFilters.hasOwnProperty(name)) {
|
||||
out.push({ name, callbacks: nodeFilters[name] });
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const addAttributeFilter = (name: string, callback: (nodes: Node[], name: string, args: ParserArgs) => void) => {
|
||||
each(explode(name), function (name) {
|
||||
let i;
|
||||
|
||||
for (i = 0; i < attributeFilters.length; i++) {
|
||||
if (attributeFilters[i].name === name) {
|
||||
attributeFilters[i].callbacks.push(callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
attributeFilters.push({ name, callbacks: [callback] });
|
||||
});
|
||||
};
|
||||
|
||||
const getAttributeFilters = (): ParserFilter[] => [].concat(attributeFilters);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const parse = (html: string, args?: ParserArgs): Node => {
|
||||
let parser, nodes, i, l, fi, fl, list, name;
|
||||
let blockElements;
|
||||
const invalidChildren = [];
|
||||
let isInWhiteSpacePreservedElement;
|
||||
let node: Node;
|
||||
|
||||
args = args || {};
|
||||
matchedNodes = {};
|
||||
matchedAttributes = {};
|
||||
blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
|
||||
const nonEmptyElements = schema.getNonEmptyElements();
|
||||
const children = schema.children;
|
||||
const validate = settings.validate;
|
||||
const rootBlockName = 'forced_root_block' in args ? args.forced_root_block : settings.forced_root_block;
|
||||
const whiteSpaceElements = schema.getWhiteSpaceElements();
|
||||
const startWhiteSpaceRegExp = /^[ \t\r\n]+/;
|
||||
const endWhiteSpaceRegExp = /[ \t\r\n]+$/;
|
||||
const allWhiteSpaceRegExp = /[ \t\r\n]+/g;
|
||||
const isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
|
||||
|
||||
isInWhiteSpacePreservedElement = whiteSpaceElements.hasOwnProperty(args.context) || whiteSpaceElements.hasOwnProperty(settings.root_name);
|
||||
|
||||
const addRootBlocks = function () {
|
||||
let node = rootNode.firstChild, next, rootBlockNode;
|
||||
|
||||
// Removes whitespace at beginning and end of block so:
|
||||
// <p> x </p> -> <p>x</p>
|
||||
const trim = function (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);
|
||||
};
|
||||
|
||||
const createNode = function (name, type) {
|
||||
const node = new Node(name, type);
|
||||
let list;
|
||||
|
||||
if (name in nodeFilters) {
|
||||
list = matchedNodes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedNodes[name] = [node];
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
const removeWhitespaceBefore = function (node) {
|
||||
let textNode, textNodeNext, textVal, sibling;
|
||||
const 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;
|
||||
}
|
||||
};
|
||||
|
||||
const cloneAndExcludeBlocks = function (input) {
|
||||
let name;
|
||||
const output = {};
|
||||
|
||||
for (name in input) {
|
||||
if (name !== 'li' && name !== 'p') {
|
||||
output[name] = input[name];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
parser = SaxParser({
|
||||
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 (text) {
|
||||
node.append(createNode('#cdata', 4)).value = text;
|
||||
},
|
||||
|
||||
text (text, raw) {
|
||||
let textNode;
|
||||
|
||||
// Trim all redundant whitespace on non white space elements
|
||||
if (!isInWhiteSpacePreservedElement) {
|
||||
text = text.replace(allWhiteSpaceRegExp, ' ');
|
||||
|
||||
if (isLineBreakNode(node.lastChild, blockElements)) {
|
||||
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 (text) {
|
||||
node.append(createNode('#comment', 8)).value = text;
|
||||
},
|
||||
|
||||
pi (name, text) {
|
||||
node.append(createNode(name, 7)).value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
doctype (text) {
|
||||
let newNode;
|
||||
|
||||
newNode = node.append(createNode('#doctype', 10));
|
||||
newNode.value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
start (name, attrs, empty) {
|
||||
let 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 (name) {
|
||||
let 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;
|
||||
}
|
||||
|
||||
if (elementRule.removeEmpty && isEmpty(schema, nonEmptyElements, whiteSpaceElements, node)) {
|
||||
// Leave nodes that have a name like <a name="name">
|
||||
if (!node.attributes.map.name && !node.attr('id')) {
|
||||
tempNode = node.parent;
|
||||
|
||||
if (blockElements[node.name]) {
|
||||
node.empty().remove();
|
||||
} else {
|
||||
node.unwrap();
|
||||
}
|
||||
|
||||
node = tempNode;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (elementRule.paddEmpty && (isPaddedWithNbsp(node) || isEmpty(schema, nonEmptyElements, whiteSpaceElements, node))) {
|
||||
paddEmptyNode(settings, args, blockElements, node);
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
}, schema);
|
||||
|
||||
const 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;
|
||||
};
|
||||
|
||||
const exports = {
|
||||
schema,
|
||||
addAttributeFilter,
|
||||
getAttributeFilters,
|
||||
addNodeFilter,
|
||||
getNodeFilters,
|
||||
filterNode,
|
||||
parse
|
||||
};
|
||||
|
||||
ParserFilters.register(exports, settings);
|
||||
LegacyFilter.register(exports, settings);
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { Element } from '@ephox/sugar';
|
||||
import Tools from '../util/Tools';
|
||||
|
||||
export interface EntitiesMap { [name: string]: string; }
|
||||
|
||||
/**
|
||||
* Entity encoder class.
|
||||
*
|
||||
* @class tinymce.html.Entities
|
||||
* @static
|
||||
* @version 3.4
|
||||
*/
|
||||
|
||||
const makeMap = Tools.makeMap;
|
||||
|
||||
let namedEntities, baseEntities, reverseEntities;
|
||||
const attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
||||
const textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
||||
const rawCharsRegExp = /[<>&\"\']/g;
|
||||
const entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi;
|
||||
const asciiMap = {
|
||||
128: '\u20AC', 130: '\u201A', 131: '\u0192', 132: '\u201E', 133: '\u2026', 134: '\u2020',
|
||||
135: '\u2021', 136: '\u02C6', 137: '\u2030', 138: '\u0160', 139: '\u2039', 140: '\u0152',
|
||||
142: '\u017D', 145: '\u2018', 146: '\u2019', 147: '\u201C', 148: '\u201D', 149: '\u2022',
|
||||
150: '\u2013', 151: '\u2014', 152: '\u02DC', 153: '\u2122', 154: '\u0161', 155: '\u203A',
|
||||
156: '\u0153', 158: '\u017E', 159: '\u0178'
|
||||
};
|
||||
|
||||
// Raw entities
|
||||
baseEntities = {
|
||||
'\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code
|
||||
'\'': ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'\u0060': '`'
|
||||
};
|
||||
|
||||
// Reverse lookup table for raw entities
|
||||
reverseEntities = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
''': '\''
|
||||
};
|
||||
|
||||
// Decodes text by using the browser
|
||||
const nativeDecode = (text: string): string => {
|
||||
let elm;
|
||||
|
||||
elm = Element.fromTag('div').dom();
|
||||
elm.innerHTML = text;
|
||||
|
||||
return elm.textContent || elm.innerText || text;
|
||||
};
|
||||
|
||||
// Build a two way lookup table for the entities
|
||||
const buildEntitiesLookup = (items, radix?: number) => {
|
||||
let i, chr, entity;
|
||||
const 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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const encodeRaw = (text: string, attr?: boolean) => {
|
||||
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.
|
||||
*/
|
||||
const encodeAllRaw = (text: string): string => {
|
||||
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.
|
||||
*/
|
||||
const encodeNumeric = (text: string, attr?: boolean) => {
|
||||
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.
|
||||
*/
|
||||
const encodeNamed = (text: string, attr?: boolean, entities?: EntitiesMap) => {
|
||||
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.
|
||||
*/
|
||||
const getEncodeFunc = (name: string, entities?: EntitiesMap | string) => {
|
||||
const entitiesMap = buildEntitiesLookup(entities) || namedEntities;
|
||||
|
||||
const encodeNamedAndNumeric = (text: string, attr?: boolean): string => {
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) {
|
||||
if (baseEntities[chr] !== undefined) {
|
||||
return baseEntities[chr];
|
||||
}
|
||||
|
||||
if (entitiesMap[chr] !== undefined) {
|
||||
return entitiesMap[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) + ';';
|
||||
});
|
||||
};
|
||||
|
||||
const encodeCustomNamed = function (text: string, attr) {
|
||||
return encodeNamed(text, attr, entitiesMap);
|
||||
};
|
||||
|
||||
// Replace + with , to be compatible with previous TinyMCE versions
|
||||
const nameMap = makeMap(name.replace(/\+/g, ','));
|
||||
|
||||
// Named and numeric encoder
|
||||
if (nameMap.named && nameMap.numeric) {
|
||||
return encodeNamedAndNumeric;
|
||||
}
|
||||
|
||||
// Named encoder
|
||||
if (nameMap.named) {
|
||||
// Custom names
|
||||
if (entities) {
|
||||
return encodeCustomNamed;
|
||||
}
|
||||
|
||||
return encodeNamed;
|
||||
}
|
||||
|
||||
// Numeric
|
||||
if (nameMap.numeric) {
|
||||
return encodeNumeric;
|
||||
}
|
||||
|
||||
// Raw encoder
|
||||
return 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.
|
||||
*/
|
||||
const decode = (text: string): string => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
encodeRaw,
|
||||
encodeAllRaw,
|
||||
encodeNumeric,
|
||||
encodeNamed,
|
||||
getEncodeFunc,
|
||||
decode
|
||||
};
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export type ElementMap = Array<{ [name: string]: boolean; }>;
|
||||
export type Attributes = Array<{ [name: string]: string; }>;
|
||||
|
||||
const whiteSpaceRegExp = /^[ \t\r\n]*$/;
|
||||
const typeLookup = {
|
||||
'#text': 3,
|
||||
'#comment': 8,
|
||||
'#cdata': 4,
|
||||
'#pi': 7,
|
||||
'#doctype': 10,
|
||||
'#document-fragment': 11
|
||||
};
|
||||
|
||||
// Walks the tree left/right
|
||||
const walk = function (node: Node, root: Node, prev?: boolean): Node {
|
||||
let sibling;
|
||||
let parent;
|
||||
const startName = prev ? 'lastChild' : 'firstChild';
|
||||
const 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 !== root) {
|
||||
sibling = node[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
|
||||
// Walk up the parents to look for siblings
|
||||
for (parent = node.parent; parent && parent !== root; parent = parent.parent) {
|
||||
sibling = parent[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
class Node {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static create (name: string, attrs: Attributes): Node {
|
||||
let 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;
|
||||
}
|
||||
|
||||
public name: string;
|
||||
public type: number;
|
||||
public attributes: Attributes;
|
||||
public value: string;
|
||||
public shortEnded: boolean;
|
||||
public parent: Node;
|
||||
public firstChild: Node;
|
||||
public lastChild: Node;
|
||||
public next: Node;
|
||||
public prev: Node;
|
||||
|
||||
/**
|
||||
* Constructs a new Node instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Node
|
||||
* @param {String} name Name of the node type.
|
||||
* @param {Number} type Numeric type representing the node.
|
||||
*/
|
||||
constructor(name: string, type: number) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
|
||||
if (type === 1) {
|
||||
this.attributes = [] as Attributes;
|
||||
(this.attributes as any).map = {}; // Should be considered internal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public replace (node: Node): Node {
|
||||
const 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.
|
||||
*/
|
||||
public attr (name: string | Record<string, string>, value?: string): String | Node {
|
||||
const self = this;
|
||||
let attrs: Attributes, i;
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
for (i in name) {
|
||||
self.attr(i, name[i]);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if ((attrs = self.attributes)) {
|
||||
if (value !== undefined) {
|
||||
// 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, 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.
|
||||
*/
|
||||
public clone (): Node {
|
||||
const self = this;
|
||||
const clone = new Node(self.name, self.type);
|
||||
let 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
|
||||
*/
|
||||
public wrap (wrapper: Node): Node {
|
||||
const 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
|
||||
*/
|
||||
public unwrap () {
|
||||
const self = this;
|
||||
let 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.
|
||||
*/
|
||||
public remove (): Node {
|
||||
const 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.
|
||||
*/
|
||||
public append (node: Node): Node {
|
||||
const self = this;
|
||||
let 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.
|
||||
*/
|
||||
public insert (node: Node, refNode: Node, before?: boolean): Node {
|
||||
let 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.
|
||||
*/
|
||||
public getAll (name: string): Node[] {
|
||||
const self = this;
|
||||
let node;
|
||||
const 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.
|
||||
*/
|
||||
public empty (): Node {
|
||||
const self = this;
|
||||
let 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.
|
||||
* @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node.
|
||||
* @return {Boolean} true/false if the node is empty or not.
|
||||
*/
|
||||
public isEmpty (elements: ElementMap, whitespace?: ElementMap, predicate?: (node: Node) => boolean) {
|
||||
const self = this;
|
||||
let node = self.firstChild, i, name;
|
||||
|
||||
whitespace = whitespace || {} as ElementMap;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Predicate tells that the node is contents
|
||||
if (predicate && predicate(node)) {
|
||||
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.
|
||||
*/
|
||||
public walk (prev?: boolean): Node {
|
||||
return walk(this, null, prev);
|
||||
}
|
||||
}
|
||||
|
||||
export default Node;
|
||||
|
|
@ -0,0 +1,512 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Schema from './Schema';
|
||||
import Entities from './Entities';
|
||||
import Tools from '../util/Tools';
|
||||
|
||||
declare const unescape: any;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const isValidPrefixAttrName = function (name) {
|
||||
return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0;
|
||||
};
|
||||
|
||||
const trimComments = function (text) {
|
||||
return text.replace(/<!--|-->/g, '');
|
||||
};
|
||||
|
||||
const isInvalidUri = (settings, uri: string) => {
|
||||
if (settings.allow_html_data_urls) {
|
||||
return false;
|
||||
} else if (/^data:image\//i.test(uri)) {
|
||||
return settings.allow_svg_data_urls === false && /^data:image\/svg\+xml/i.test(uri);
|
||||
} else {
|
||||
return /^data:/i.test(uri);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const findEndTagIndex = function (schema, html, startIndex) {
|
||||
let 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.
|
||||
*/
|
||||
export function SaxParser(settings, schema = Schema()) {
|
||||
const noop = function () { };
|
||||
|
||||
settings = settings || {};
|
||||
|
||||
if (settings.fix_self_closing !== false) {
|
||||
settings.fix_self_closing = true;
|
||||
}
|
||||
|
||||
const comment = settings.comment ? settings.comment : noop;
|
||||
const cdata = settings.cdata ? settings.cdata : noop;
|
||||
const text = settings.text ? settings.text : noop;
|
||||
const start = settings.start ? settings.start : noop;
|
||||
const end = settings.end ? settings.end : noop;
|
||||
const pi = settings.pi ? settings.pi : noop;
|
||||
const doctype = settings.doctype ? settings.doctype : noop;
|
||||
|
||||
/**
|
||||
* Parses the specified HTML string and executes the callbacks for each item it finds.
|
||||
*
|
||||
* @example
|
||||
* SaxParser({...}).parse('<b>text</b>');
|
||||
* @method parse
|
||||
* @param {String} html Html string to sax parse.
|
||||
*/
|
||||
const parse = (html: string) => {
|
||||
let matches, index = 0, value, endRegExp;
|
||||
const stack = [];
|
||||
let attrList, i, textData, name;
|
||||
let isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
|
||||
let validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
|
||||
let attributesRequired, attributesDefault, attributesForced, processHtml;
|
||||
let anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
|
||||
const decode = Entities.decode;
|
||||
let fixSelfClosing;
|
||||
const filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster,xlink:href');
|
||||
const scriptUriRegExp = /((java|vb)script|mhtml):/i;
|
||||
|
||||
const processEndTag = function (name) {
|
||||
let 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) {
|
||||
end(name.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
};
|
||||
|
||||
const parseAttribute = function (match, name, value, val2, val3) {
|
||||
let attrRule, i;
|
||||
const 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) {
|
||||
let 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 (isInvalidUri(settings, 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,
|
||||
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) {
|
||||
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) {
|
||||
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, 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, 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 = findEndTagIndex(schema, html, tokenRegExp.lastIndex);
|
||||
tokenRegExp.lastIndex = index;
|
||||
continue;
|
||||
}
|
||||
|
||||
isValidElement = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidElement) {
|
||||
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) {
|
||||
textData = html.substr(index, matches.index - index);
|
||||
}
|
||||
|
||||
index = matches.index + matches[0].length;
|
||||
} else {
|
||||
textData = html.substr(index);
|
||||
index = html.length;
|
||||
}
|
||||
|
||||
if (isValidElement) {
|
||||
if (textData.length > 0) {
|
||||
text(textData, true);
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
comment(value);
|
||||
} else if ((value = matches[2])) { // CDATA
|
||||
cdata(trimComments(value));
|
||||
} else if ((value = matches[3])) { // DOCTYPE
|
||||
doctype(value);
|
||||
} else if ((value = matches[4])) { // PI
|
||||
pi(value, matches[5]);
|
||||
}
|
||||
|
||||
index = matches.index + matches[0].length;
|
||||
}
|
||||
|
||||
// Text
|
||||
if (index < html.length) {
|
||||
text(decode(html.substr(index)));
|
||||
}
|
||||
|
||||
// Close any open elements
|
||||
for (i = stack.length - 1; i >= 0; i--) {
|
||||
value = stack[i];
|
||||
|
||||
if (value.valid) {
|
||||
end(value.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
parse
|
||||
};
|
||||
}
|
||||
|
||||
export namespace SaxParser {
|
||||
export const findEndTag = findEndTagIndex;
|
||||
}
|
||||
|
||||
export default SaxParser;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Writer from './Writer';
|
||||
import Schema from './Schema';
|
||||
import Node from './Node';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export default function (settings?, schema = Schema()) {
|
||||
const writer = Writer(settings);
|
||||
|
||||
settings = settings || {};
|
||||
settings.validate = 'validate' in settings ? settings.validate : true;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const serialize = (node: Node) => {
|
||||
let handlers, validate;
|
||||
|
||||
validate = settings.validate;
|
||||
|
||||
handlers = {
|
||||
// #text
|
||||
3 (node) {
|
||||
writer.text(node.value, node.raw);
|
||||
},
|
||||
|
||||
// #comment
|
||||
8 (node) {
|
||||
writer.comment(node.value);
|
||||
},
|
||||
|
||||
// Processing instruction
|
||||
7 (node) {
|
||||
writer.pi(node.name, node.value);
|
||||
},
|
||||
|
||||
// Doctype
|
||||
10 (node) {
|
||||
writer.doctype(node.value);
|
||||
},
|
||||
|
||||
// CDATA
|
||||
4 (node) {
|
||||
writer.cdata(node.value);
|
||||
},
|
||||
|
||||
// Document fragment
|
||||
11 (node) {
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
walk(node);
|
||||
} while ((node = node.next));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writer.reset();
|
||||
|
||||
const walk = function (node: Node) {
|
||||
const handler = handlers[node.type];
|
||||
let 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();
|
||||
};
|
||||
|
||||
return {
|
||||
serialize
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Schema from './Schema';
|
||||
|
||||
export interface StyleMap { [s: string]: string | number; }
|
||||
export interface Styles {
|
||||
toHex(color: string): string;
|
||||
parse(css: string): StyleMap;
|
||||
serialize(styles: StyleMap, elementName?: string): string;
|
||||
}
|
||||
|
||||
const toHex = (match: string, r: string, g: string, b: string) => {
|
||||
const hex = (val: string) => {
|
||||
val = parseInt(val, 10).toString(16);
|
||||
|
||||
return val.length > 1 ? val : '0' + val; // 0 -> 00
|
||||
};
|
||||
|
||||
return '#' + hex(r) + hex(g) + hex(b);
|
||||
};
|
||||
|
||||
export function Styles(settings?, schema?: Schema): Styles {
|
||||
/*jshint maxlen:255 */
|
||||
/*eslint max-len:0 */
|
||||
const rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi;
|
||||
const urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi;
|
||||
const styleRegExp = /\s*([^:]+):\s*([^;]+);?/g;
|
||||
const trimRightRegExp = /\s+$/;
|
||||
let i;
|
||||
const encodingLookup = {};
|
||||
let encodingItems;
|
||||
let validStyles;
|
||||
let invalidStyles;
|
||||
const 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];
|
||||
}
|
||||
|
||||
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 (color: string): string {
|
||||
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 (css: string): StyleMap {
|
||||
const styles: any = {};
|
||||
let matches, name, value, isEncoded;
|
||||
const urlConverter = settings.url_converter;
|
||||
const urlConverterScope = settings.url_converter_scope || this;
|
||||
|
||||
const compress = function (prefix, suffix, noJoin?) {
|
||||
let 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;
|
||||
}
|
||||
|
||||
const 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.
|
||||
*/
|
||||
const canCompress = function (key) {
|
||||
let 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.
|
||||
*/
|
||||
const compress2 = function (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>
|
||||
const encode = function (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
|
||||
const decode = function (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;
|
||||
};
|
||||
|
||||
const decodeSingleHexSequence = function (escSeq) {
|
||||
return String.fromCharCode(parseInt(escSeq.slice(1), 16));
|
||||
};
|
||||
|
||||
const decodeHexSequences = function (value) {
|
||||
return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence);
|
||||
};
|
||||
|
||||
const processUrl = function (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) {
|
||||
const 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 (styles: StyleMap, elementName?: string): string {
|
||||
let css = '', name, value;
|
||||
|
||||
const serializeStyles = (name: string) => {
|
||||
let 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 + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = (name: string, elementName: string): boolean => {
|
||||
let 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import Entities from './Entities';
|
||||
import Tools from '../util/Tools';
|
||||
import { Attributes } from './Node';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const makeMap = Tools.makeMap;
|
||||
|
||||
export default function (settings?) {
|
||||
const html = [];
|
||||
let 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 (name: string, attrs?: Attributes, empty?: boolean) {
|
||||
let 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 (name: string) {
|
||||
let 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 (text: string, raw?: boolean) {
|
||||
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 (text: string) {
|
||||
html.push('<![CDATA[', text, ']]>');
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a comment node such as <!-- Comment -->.
|
||||
*
|
||||
* @method cdata
|
||||
* @param {String} text String to write out inside the comment.
|
||||
*/
|
||||
comment (text: string) {
|
||||
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 (name: string, text: string) {
|
||||
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 (text: string) {
|
||||
html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the internal buffer if one wants to reuse the writer.
|
||||
*
|
||||
* @method reset
|
||||
*/
|
||||
reset () {
|
||||
html.length = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the contents that got serialized.
|
||||
*
|
||||
* @method getContent
|
||||
* @return {String} HTML contents that got written down.
|
||||
*/
|
||||
getContent (): string {
|
||||
return html.join('').replace(/\n$/, '');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
const types = {};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 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 (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 (type) {
|
||||
return !!types[type.toLowerCase()];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns ui control module by name.
|
||||
*
|
||||
* @method get
|
||||
* @param {String} type Type get.
|
||||
* @return {Object} Module or undefined.
|
||||
*/
|
||||
get (type) {
|
||||
const lctype = type.toLowerCase();
|
||||
const controlType = types.hasOwnProperty(lctype) ? types[lctype] : null;
|
||||
if (controlType === null) {
|
||||
throw new Error('Could not find module for type: ' + type);
|
||||
}
|
||||
|
||||
return controlType;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (type, settings?) {
|
||||
let 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;
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue