950 lines
24 KiB
JavaScript
950 lines
24 KiB
JavaScript
|
/**
|
|||
|
* InputfieldTinyMCE.js
|
|||
|
*
|
|||
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
|||
|
* https://processwire.com
|
|||
|
*
|
|||
|
* TinyMCE 6.x, Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
|
|||
|
* https://www.tiny.cloud/docs/tinymce/6/
|
|||
|
*
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Handler for image uploads
|
|||
|
*
|
|||
|
* @param blobInfo
|
|||
|
* @param progress
|
|||
|
* @returns {Promise<unknown>}
|
|||
|
*
|
|||
|
*/
|
|||
|
var InputfieldTinyMCEUploadHandler = (blobInfo, progress) => new Promise((resolve, reject) => {
|
|||
|
|
|||
|
var editor = tinymce.activeEditor;
|
|||
|
var $inputfield = $('#' + editor.id).closest('.InputfieldTinyMCE');
|
|||
|
var imageFieldName = $inputfield.attr('data-upload-field');
|
|||
|
var $imageInputfield = $('#wrap_Inputfield_' + imageFieldName);
|
|||
|
var pageId = $inputfield.attr('data-upload-page');
|
|||
|
var uploadUrl = ProcessWire.config.urls.admin + 'page/edit/?id=' + pageId + '&InputfieldFileAjax=1&ckeupload=1';
|
|||
|
|
|||
|
const xhr = new XMLHttpRequest();
|
|||
|
|
|||
|
xhr.withCredentials = true;
|
|||
|
xhr.upload.onprogress = (e) => { progress(e.loaded / e.total * 100); };
|
|||
|
xhr.open('POST', uploadUrl);
|
|||
|
|
|||
|
xhr.onload = () => {
|
|||
|
if(xhr.status === 403) {
|
|||
|
reject({ message: 'HTTP Error: ' + xhr.status, remove: true });
|
|||
|
return;
|
|||
|
} else if(xhr.status < 200 || xhr.status >= 300) {
|
|||
|
reject('HTTP Error: ' + xhr.status);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var response = JSON.parse(xhr.responseText);
|
|||
|
|
|||
|
if(!response) {
|
|||
|
reject('Invalid JSON in response: ' + xhr.responseText);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
resolve(response.url);
|
|||
|
};
|
|||
|
|
|||
|
xhr.onerror = () => {
|
|||
|
reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
|
|||
|
};
|
|||
|
|
|||
|
$imageInputfield.trigger('pwimageupload', {
|
|||
|
'name': blobInfo.filename(),
|
|||
|
'file': blobInfo.blob(),
|
|||
|
'xhr': xhr
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* InputfieldTinyMCE main
|
|||
|
*
|
|||
|
*/
|
|||
|
var InputfieldTinyMCE = {
|
|||
|
|
|||
|
/**
|
|||
|
* Debug mode?
|
|||
|
*
|
|||
|
*/
|
|||
|
debug: false,
|
|||
|
|
|||
|
/**
|
|||
|
* Are document events attached?
|
|||
|
*
|
|||
|
*/
|
|||
|
eventsReady: false,
|
|||
|
|
|||
|
/**
|
|||
|
* Is document ready?
|
|||
|
*
|
|||
|
*/
|
|||
|
isDocumentReady: false,
|
|||
|
|
|||
|
/**
|
|||
|
* Editor selectors to init at document ready
|
|||
|
*
|
|||
|
*/
|
|||
|
editorIds: [],
|
|||
|
|
|||
|
/**
|
|||
|
* Are we currently processing an editor init? (bool or string)
|
|||
|
*
|
|||
|
*/
|
|||
|
initializing: false,
|
|||
|
|
|||
|
/**
|
|||
|
* Allow lazy loaded editor init()? (adjusted by this class at runtime)
|
|||
|
*
|
|||
|
*/
|
|||
|
allowLazy: true,
|
|||
|
|
|||
|
/**
|
|||
|
* Ccallback functions
|
|||
|
*
|
|||
|
*/
|
|||
|
callbacks: { onSetup: [], onConfig: [], onReady: [] },
|
|||
|
|
|||
|
/**
|
|||
|
* Recognized class names
|
|||
|
*
|
|||
|
*/
|
|||
|
cls: {
|
|||
|
lazy: 'InputfieldTinyMCELazy',
|
|||
|
inline: 'InputfieldTinyMCEInline',
|
|||
|
normal: 'InputfieldTinyMCENormal',
|
|||
|
loaded: 'InputfieldTinyMCELoaded',
|
|||
|
editor: 'InputfieldTinyMCEEditor',
|
|||
|
placeholder: 'InputfieldTinyMCEPlaceholder'
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Console log
|
|||
|
*
|
|||
|
* @param a
|
|||
|
* @param b
|
|||
|
*
|
|||
|
*/
|
|||
|
log: function(a, b) {
|
|||
|
if(!this.debug) return;
|
|||
|
if(typeof b !== 'undefined') {
|
|||
|
if(typeof a === 'string') a = 'TinyMCE ' + a;
|
|||
|
console.log(a, b);
|
|||
|
} else {
|
|||
|
console.log('TinyMCE', a);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Add a setup callback function
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* InputfieldTinyMCE.onSetup(function(editor) {
|
|||
|
* // ...
|
|||
|
* });
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* @param callback
|
|||
|
*
|
|||
|
*/
|
|||
|
onSetup: function(callback) {
|
|||
|
this.callbacks.onSetup.push(callback);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Add a config callback function
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* InputfieldTinyMCE.onConfig(function(settings, $editor, $inputfield) {
|
|||
|
* // ...
|
|||
|
* });
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* @param callback
|
|||
|
*
|
|||
|
*/
|
|||
|
onConfig: function(callback) {
|
|||
|
this.callbacks.onConfig.push(callback);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Add a ready callback function
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* InputfieldTinyMCE.onReady(function(editor) {
|
|||
|
* // ...
|
|||
|
* });
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* @param callback
|
|||
|
*
|
|||
|
*/
|
|||
|
onReady: function(callback) {
|
|||
|
this.callbacks.onReady.push(callback);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Set editor initializing state
|
|||
|
*
|
|||
|
* @param {boolean|string} initializing Boolean or editor ID
|
|||
|
*
|
|||
|
*/
|
|||
|
setInitializing: function(initializing) {
|
|||
|
this.initializing = initializing;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Is editor initializing?
|
|||
|
*
|
|||
|
* @returns boolean|string False or editor id (string)
|
|||
|
*
|
|||
|
*/
|
|||
|
isInitializing: function() {
|
|||
|
return this.initializing;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Modify image dimensions
|
|||
|
*
|
|||
|
* @param editor
|
|||
|
* @param img
|
|||
|
* @param width
|
|||
|
*
|
|||
|
*/
|
|||
|
imageResized: function(editor, img, width) {
|
|||
|
var t = this;
|
|||
|
var src = img.src;
|
|||
|
var hidpi = img.className.indexOf('hidpi') > -1 ? 1 : 0;
|
|||
|
var basename = src.substring(src.lastIndexOf('/')+1);
|
|||
|
var path = src.substring(0, src.lastIndexOf('/')+1);
|
|||
|
var dot1 = basename.indexOf('.');
|
|||
|
var dot2 = basename.lastIndexOf('.');
|
|||
|
var crop = '';
|
|||
|
|
|||
|
if(basename.indexOf('-cropx') > -1 || basename.indexOf('.cropx') > -1) {
|
|||
|
// example: gonzo_the_great.205x183-cropx38y2-is.jpg
|
|||
|
// use image file as-is
|
|||
|
// @todo re-crop and resize from original?
|
|||
|
} else if(dot1 !== dot2) {
|
|||
|
// extract any existing resize data present to get original file
|
|||
|
// i.e. file.123x456-is-hidpi.jpg => file.jpg
|
|||
|
var basename2 = basename.substring(0, dot1) + basename.substring(dot2);
|
|||
|
src = path + basename2;
|
|||
|
}
|
|||
|
|
|||
|
var url = ProcessWire.config.urls.admin + 'page/image/resize' +
|
|||
|
'?json=1' +
|
|||
|
'&width=' + width +
|
|||
|
'&hidpi=' + hidpi +
|
|||
|
'&file=' + src;
|
|||
|
|
|||
|
t.log('Resizing image to width=' + width, url);
|
|||
|
|
|||
|
jQuery.getJSON(url, function(data) {
|
|||
|
editor.dom.setAttrib(img, 'src', data.src);
|
|||
|
// editor.dom.setAttrib(img, 'width', data.width);
|
|||
|
// editor.dom.setAttrib(img, 'height', data.height);
|
|||
|
t.log('Resized image to width=' + data.width, data.src);
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Called when an element has an align class applied to it
|
|||
|
*
|
|||
|
* This function ensures only 1 align class is applied at a time.
|
|||
|
*
|
|||
|
* @param editor
|
|||
|
*
|
|||
|
*/
|
|||
|
elementAligned: function(editor) {
|
|||
|
var selection = editor.selection;
|
|||
|
var node = selection.getNode();
|
|||
|
var className = node.className;
|
|||
|
var n;
|
|||
|
|
|||
|
// if only one align class then return now
|
|||
|
if(className.indexOf('align') === className.lastIndexOf('align')) return;
|
|||
|
|
|||
|
var alignNames = [];
|
|||
|
var classNames = className.split(' ');
|
|||
|
|
|||
|
for(n = 0; n < classNames.length; n++) {
|
|||
|
if(classNames[n].indexOf('align') === 0) {
|
|||
|
alignNames.push(classNames[n]);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// pop off last align class, which we will keep
|
|||
|
alignNames.pop();
|
|||
|
|
|||
|
for(n = 0; n < alignNames.length; n++) {
|
|||
|
className = className.replace(alignNames[n], '');
|
|||
|
}
|
|||
|
|
|||
|
node.className = className.trim();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Lazy load placeholder click event
|
|||
|
*
|
|||
|
* @param e
|
|||
|
* @returns {boolean}
|
|||
|
*
|
|||
|
*/
|
|||
|
clickPlaceholder: function(e) {
|
|||
|
var t = InputfieldTinyMCE;
|
|||
|
var $placeholder = jQuery(this);
|
|||
|
var $textarea = $placeholder.next('textarea');
|
|||
|
$placeholder.remove();
|
|||
|
t.log('placeholderClick', $placeholder);
|
|||
|
if($textarea.length) t.init('#' + $textarea.attr('id'), 'event.' + e.type);
|
|||
|
return false;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Init/populate lazy load placeholder elements within given target
|
|||
|
*
|
|||
|
* @param $placeholders Placeholders are wrapper that has them within
|
|||
|
*
|
|||
|
*/
|
|||
|
initPlaceholders: function($placeholders) {
|
|||
|
if(!$placeholders.length) return;
|
|||
|
var t = this;
|
|||
|
var $item = $placeholders.first();
|
|||
|
if(!$item.hasClass(t.cls.placeholder)) $placeholders = $item.find('.' + t.cls.placeholder);
|
|||
|
$placeholders.each(function() {
|
|||
|
var $placeholder = jQuery(this);
|
|||
|
var $textarea = $placeholder.next('textarea');
|
|||
|
t.log('initPlaceholder', $placeholder);
|
|||
|
$placeholder.children('.mce-content-body').html($textarea.val());
|
|||
|
$placeholder.on('click touchstart', t.clickPlaceholder);
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Init callback function
|
|||
|
*
|
|||
|
* @param editor
|
|||
|
* @param features
|
|||
|
*
|
|||
|
*/
|
|||
|
editorReady: function(editor, features) {
|
|||
|
|
|||
|
var t = this;
|
|||
|
var $editor = jQuery('#' + editor.id);
|
|||
|
var $inputfield = $editor.closest('.InputfieldTinyMCE');
|
|||
|
var inputTimeout = null;
|
|||
|
|
|||
|
if(!$inputfield.length) $inputfield = $editor.closest('.Inputfield');
|
|||
|
|
|||
|
function changed() {
|
|||
|
if(inputTimeout) clearTimeout(inputTimeout);
|
|||
|
inputTimeout = setTimeout(function() {
|
|||
|
$inputfield.trigger('change');
|
|||
|
}, 500);
|
|||
|
}
|
|||
|
|
|||
|
editor.on('Dirty', function() { changed() });
|
|||
|
editor.on('SetContent', function() { changed() });
|
|||
|
editor.on('input', function() { changed() });
|
|||
|
|
|||
|
// for image resizes
|
|||
|
if(features.indexOf('imgResize') > -1) {
|
|||
|
editor.on('ObjectResized', function(e, data) {
|
|||
|
// @todo account for case where image in figure is resized, and figure needs its width updated with the image
|
|||
|
if(e.target.nodeName === 'IMG') {
|
|||
|
t.imageResized(editor, e.target, e.width);
|
|||
|
changed();
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
for(var n = 0; n < t.callbacks.onReady.length; n++) {
|
|||
|
t.callbacks.onReady[n](editor);
|
|||
|
}
|
|||
|
|
|||
|
editor.on('ExecCommand', function(e, f) {
|
|||
|
if(e.command === 'mceFocus') return;
|
|||
|
t.log('command: ' + e.command, e);
|
|||
|
if(e.command === 'mceToggleFormat') {
|
|||
|
if(e.value && e.value.indexOf('align') === 0) {
|
|||
|
var editor = this;
|
|||
|
t.elementAligned(editor);
|
|||
|
}
|
|||
|
changed();
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
/*
|
|||
|
* uncomment to show inline init effect
|
|||
|
if(jQuery.ui) {
|
|||
|
if($editor.hasClass('InputfieldTinyMCEInline')) {
|
|||
|
$editor.effect('highlight', {}, 500);
|
|||
|
}
|
|||
|
}
|
|||
|
*/
|
|||
|
|
|||
|
/*
|
|||
|
editor.on('ResizeEditor', function(e) {
|
|||
|
// editor resized
|
|||
|
t.log('ResizeEditor');
|
|||
|
});
|
|||
|
*/
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* config.setup handler function
|
|||
|
*
|
|||
|
* @param editor
|
|||
|
*
|
|||
|
*/
|
|||
|
setupEditor: function(editor) {
|
|||
|
var t = InputfieldTinyMCE;
|
|||
|
var $editor = jQuery('#' + editor.id);
|
|||
|
|
|||
|
if($editor.hasClass(t.cls.loaded)) {
|
|||
|
t.log('mceInit called on input that is already loaded', editor.id);
|
|||
|
} else {
|
|||
|
$editor.addClass(t.cls.loaded);
|
|||
|
}
|
|||
|
|
|||
|
for(var n = 0; n < t.callbacks.onSetup.length; n++) {
|
|||
|
t.callbacks.onSetup[n](editor);
|
|||
|
}
|
|||
|
|
|||
|
/*
|
|||
|
editor.on('init', function() {
|
|||
|
// var n = performance.now();
|
|||
|
// t.log(editor.id + ': ' + (n - mceTimer) + ' ms');
|
|||
|
});
|
|||
|
*/
|
|||
|
|
|||
|
/*
|
|||
|
editor.on('Load', function() {
|
|||
|
// t.log('iframe loaded', editor.id);
|
|||
|
});
|
|||
|
*/
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Destroy given editors
|
|||
|
*
|
|||
|
* @param $editors
|
|||
|
*
|
|||
|
*/
|
|||
|
destroyEditors: function($editors) {
|
|||
|
var t = this;
|
|||
|
$editors.each(function() {
|
|||
|
var $editor = jQuery(this);
|
|||
|
if(!$editor.hasClass(t.cls.loaded)) return;
|
|||
|
var editorId = $editor.attr('id');
|
|||
|
var editor = tinymce.get(editorId);
|
|||
|
$editor.removeClass(t.cls.loaded).removeClass(t.cls.lazy);
|
|||
|
t.log('destroyEditor', editor.id);
|
|||
|
// $editor.css('display', 'none');
|
|||
|
editor.destroy();
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Destroy editors in given wrapper
|
|||
|
*
|
|||
|
* @param $wrapper
|
|||
|
*
|
|||
|
*/
|
|||
|
destroyEditorsIn($wrapper) {
|
|||
|
this.destroyEditors($wrapper.find('.' + this.cls.loaded));
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Reset given editors (destroy and re-init)
|
|||
|
*
|
|||
|
* @param $editors
|
|||
|
*
|
|||
|
*/
|
|||
|
resetEditors: function($editors) {
|
|||
|
var t = this;
|
|||
|
t.allowLazy = false;
|
|||
|
$editors.each(function() {
|
|||
|
var $editor = jQuery(this);
|
|||
|
if(!$editor.hasClass(t.cls.loaded)) return;
|
|||
|
var editorId = $editor.attr('id');
|
|||
|
var editor = tinymce.get(editorId);
|
|||
|
editor.destroy();
|
|||
|
$editor.removeClass(t.cls.loaded);
|
|||
|
// t.init('#' + editorId, 'resetEditors');
|
|||
|
});
|
|||
|
t.initEditors($editors);
|
|||
|
t.allowLazy = true;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Initialize given jQuery object editors
|
|||
|
*
|
|||
|
* @param $editors
|
|||
|
*
|
|||
|
*/
|
|||
|
initEditors: function($editors) {
|
|||
|
var t = this;
|
|||
|
$editors.each(function() {
|
|||
|
var $editor = jQuery(this);
|
|||
|
var editorId = $editor.attr('id');
|
|||
|
if($editor.hasClass(t.cls.loaded)) return;
|
|||
|
//t.log('init', id);
|
|||
|
t.init('#' + editorId, 'initEditors');
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Find and initialize editors within a wrapper
|
|||
|
*
|
|||
|
* @param $wrapper
|
|||
|
* @param selector Optional
|
|||
|
*
|
|||
|
*/
|
|||
|
initEditorsIn: function($wrapper, selector) {
|
|||
|
if(typeof selector === 'undefined') {
|
|||
|
selector =
|
|||
|
'.' + this.cls.lazy + ':visible, ' +
|
|||
|
'.' + this.cls.editor +
|
|||
|
':not(.' + this.cls.loaded + ')' +
|
|||
|
':not(.' + this.cls.lazy + ')' +
|
|||
|
':not(.' + this.cls.inline + ')';
|
|||
|
}
|
|||
|
var $placeholders = $wrapper.find('.' + this.cls.placeholder);
|
|||
|
var $editors = $wrapper.find(selector);
|
|||
|
if($placeholders.length) this.initPlaceholders($placeholders);
|
|||
|
if($editors.length) this.initEditors($editors);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Get config (config + custom settings)
|
|||
|
*
|
|||
|
* @param $editor Editor Textarea (Regular) or div (Inline)
|
|||
|
* @param $inputfield Editor Wrapping .Inputfield element
|
|||
|
* @param features
|
|||
|
* @returns {{}}
|
|||
|
*
|
|||
|
*/
|
|||
|
getConfig: function($editor, $inputfield, features) {
|
|||
|
|
|||
|
var configName = $inputfield.attr('data-configName');
|
|||
|
var globalConfig = ProcessWire.config.InputfieldTinyMCE;
|
|||
|
var settings = globalConfig.settings.default;
|
|||
|
var namedSettings = globalConfig.settings[configName];
|
|||
|
var dataSettings = $inputfield.attr('data-settings');
|
|||
|
|
|||
|
if(typeof settings === 'undefined') {
|
|||
|
settings = {};
|
|||
|
} else {
|
|||
|
settings = jQuery.extend(true, {}, settings);
|
|||
|
}
|
|||
|
|
|||
|
if(typeof namedSettings === 'undefined') {
|
|||
|
this.log('Can’t find ProcessWire.config.InputfieldTinyMCE.settings.' + configName);
|
|||
|
} else {
|
|||
|
jQuery.extend(settings, namedSettings);
|
|||
|
}
|
|||
|
|
|||
|
if(typeof dataSettings === 'undefined') {
|
|||
|
dataSettings = null;
|
|||
|
} else if(dataSettings && dataSettings.length > 2) {
|
|||
|
dataSettings = JSON.parse(dataSettings);
|
|||
|
jQuery.extend(settings, dataSettings);
|
|||
|
}
|
|||
|
|
|||
|
if(settings.inline) settings.content_css = null; // we load this separately for inline mode
|
|||
|
|
|||
|
for(var n = 0; n < this.callbacks.onConfig.length; n++) {
|
|||
|
this.callbacks.onConfig[n](settings, $editor, $inputfield);
|
|||
|
}
|
|||
|
|
|||
|
if(features.indexOf('pasteFilter') > -1) {
|
|||
|
if(globalConfig.pasteFilter === 'text') {
|
|||
|
settings.paste_as_text = true;
|
|||
|
} else if(globalConfig.pasteFilter.length) {
|
|||
|
settings.paste_preprocess = this.pastePreprocess;
|
|||
|
}
|
|||
|
}
|
|||
|
/*
|
|||
|
settings.paste_postprocess = function(editor, args) {
|
|||
|
console.log(args.node);
|
|||
|
args.node.setAttribute('id', '42');
|
|||
|
};
|
|||
|
*/
|
|||
|
|
|||
|
return settings;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Pre-process paste
|
|||
|
*
|
|||
|
* @param editor
|
|||
|
* @param args
|
|||
|
*
|
|||
|
*/
|
|||
|
pastePreprocess: function(editor, args) {
|
|||
|
|
|||
|
var t = InputfieldTinyMCE;
|
|||
|
var allow = ',' + ProcessWire.config.InputfieldTinyMCE.pasteFilter + ',';
|
|||
|
var regexTag = /<([a-z0-9]+)([^>]*)>/gi;
|
|||
|
var regexAttr = /([-_a-z0-9]+)=["']([^"']*)["']/gi;
|
|||
|
var html = args.content;
|
|||
|
var matchTag, matchAttr;
|
|||
|
var removals = [];
|
|||
|
var finds = [];
|
|||
|
var replaces = [];
|
|||
|
var startLength = html.length;
|
|||
|
|
|||
|
if(args.internal) {
|
|||
|
t.log('Skipping pasteFilter for interal copy/paste');
|
|||
|
return; // skip filtering for internal copy/paste operations
|
|||
|
}
|
|||
|
|
|||
|
if(allow === ',text,') {
|
|||
|
t.log('Skipping pasteFilter since paste_as_text settingw will be used');
|
|||
|
return; // will be processed by paste_as_text setting
|
|||
|
}
|
|||
|
|
|||
|
while((matchTag = regexTag.exec(html)) !== null) {
|
|||
|
|
|||
|
var tagOpen = matchTag[0]; // i.e. <strong>, <img src="..">, <h2>, etc.
|
|||
|
var tagName = matchTag[1]; // i.e. 'strong', 'img', 'h2', etc.
|
|||
|
var tagClose = '</' + tagName + '>'; // i.e. </strong>, </h2>
|
|||
|
var tagAttrs = matchTag[2]; // i.e. 'src="a.jpg" alt="alt"'
|
|||
|
var allowAttrs = false;
|
|||
|
|
|||
|
// first see if we can match a tag replacement
|
|||
|
var find = ',' + tagName + '='; // i.e. ',b=strong'
|
|||
|
var pos = allow.indexOf(find);
|
|||
|
|
|||
|
if(pos > -1) {
|
|||
|
// tag replacement
|
|||
|
var rule = allow.substring(pos + 1); // i.e. b=strong,and,more
|
|||
|
rule = rule.substring(0, rule.indexOf(',')); // i.e. b=strong
|
|||
|
rule = rule.split('=');
|
|||
|
var replaceTag = rule[1];
|
|||
|
finds.push(tagOpen);
|
|||
|
replaces.push('<' + replaceTag + '>');
|
|||
|
finds.push(tagClose);
|
|||
|
replaces.push('</' + replaceTag + '>');
|
|||
|
}
|
|||
|
|
|||
|
if(allow.indexOf(',' + tagName + '[') > -1) {
|
|||
|
// tag appears in whitelist with attributes
|
|||
|
allowAttrs = true;
|
|||
|
} else if(allow.indexOf(',' + tagName + ',') === -1) {
|
|||
|
// tag does not appear in whitelist
|
|||
|
removals.push(tagOpen);
|
|||
|
removals.push(tagClose);
|
|||
|
continue;
|
|||
|
} else {
|
|||
|
// tag appears in whitelist (no attributes)
|
|||
|
}
|
|||
|
|
|||
|
if(tagAttrs.length) {
|
|||
|
// tag has attributes
|
|||
|
if(!allowAttrs) {
|
|||
|
// attributes not allowed, replace with non-attribute tag
|
|||
|
finds.push(tagOpen);
|
|||
|
replaces.push('<' + tagName + '>');
|
|||
|
continue;
|
|||
|
}
|
|||
|
} else {
|
|||
|
// no attributes, nothing further to do
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
var attrRemoves = [];
|
|||
|
|
|||
|
while((matchAttr = regexAttr.exec(tagAttrs)) !== null) {
|
|||
|
var attrStr = matchAttr[0]; // i.e. alt="hello"
|
|||
|
var attrName = matchAttr[1]; // i.e. alt
|
|||
|
var attrVal = matchAttr[2]; // i.e. hello
|
|||
|
|
|||
|
if(allow.indexOf(',' + tagName + '[' + attrName + ']') > -1) {
|
|||
|
// matches whitelist of tag with allowed attribute
|
|||
|
} else if(allow.indexOf(',' + tagName + '[' + attrName + '=' + attrVal + ']') > -1) {
|
|||
|
// matches whitelist of tag with allowed attribute having allowed value
|
|||
|
} else {
|
|||
|
// attributes do not match whitelist
|
|||
|
attrRemoves.push(attrStr);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(attrRemoves.length) {
|
|||
|
var replaceOpenTag = tagOpen;
|
|||
|
for(var n = 0; n < attrRemoves.length; n++) {
|
|||
|
replaceOpenTag = replaceOpenTag.replace(attrRemoves[n], '');
|
|||
|
}
|
|||
|
finds.push(tagOpen);
|
|||
|
replaces.push(replaceOpenTag);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// console.log('removals', removals);
|
|||
|
|
|||
|
for(var n = 0; n < removals.length; n++) {
|
|||
|
html = html.replace(removals[n], '');
|
|||
|
}
|
|||
|
|
|||
|
for(var n = 0; n < finds.length; n++) {
|
|||
|
html = html.replace(finds[n], replaces[n]);
|
|||
|
// console.log(finds[n] + ' => ' + replaces[n]);
|
|||
|
}
|
|||
|
|
|||
|
while(html.indexOf('< ') > -1) html = html.replace('< ', '<');
|
|||
|
while(html.indexOf(' >') > -1) html = html.replace(' >', '>');
|
|||
|
while(html.indexOf(' ') > -1) html = html.replace(' ', ' ', html);
|
|||
|
|
|||
|
html = html.replaceAll(/<([-a-z0-9]+)[^>]*>\s*<\/\1>/ig, ''); // remove empty tags
|
|||
|
|
|||
|
t.log('Completed pasteFilter ' + startLength + ' => ' + html.length + ' bytes');
|
|||
|
|
|||
|
args.content = html;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Document ready events
|
|||
|
*
|
|||
|
*/
|
|||
|
initDocumentEvents: function() {
|
|||
|
var t = this;
|
|||
|
|
|||
|
jQuery(document)
|
|||
|
.on('click mouseover focus touchstart', '.' + t.cls.inline + ':not(.' + t.cls.loaded + ')', function(e) {
|
|||
|
// we initialize the inline editor only when moused over
|
|||
|
// so that a page can handle lots of editors at once without
|
|||
|
// them all being active
|
|||
|
if(InputfieldTinyMCE.isInitializing() !== false) return;
|
|||
|
t.init('#' + this.id, 'event.' + e.type);
|
|||
|
})
|
|||
|
.on('image-edit sort-stop', '.InputfieldTinyMCE', function(e) {
|
|||
|
// all "normal" editors that are also "loaded"
|
|||
|
var $editors = jQuery(this).find('.' + t.cls.normal + '.' + t.cls.loaded);
|
|||
|
if($editors.length) {
|
|||
|
t.log(e.type + '.resetEditors', $editors);
|
|||
|
// force all loaded to reset
|
|||
|
t.resetEditors($editors);
|
|||
|
}
|
|||
|
// all "normal" non-placeholder editors that are not yet "loaded"
|
|||
|
var $editors = jQuery(this).find('.' + t.cls.normal + ':not(.' + t.cls.loaded + '):not(.' + t.cls.placeholder + ')');
|
|||
|
if($editors.length) {
|
|||
|
t.log(e.type + '.initEditors', $editors);
|
|||
|
t.initEditors($editors);
|
|||
|
}
|
|||
|
})
|
|||
|
.on('reload', '.Inputfield', function() {
|
|||
|
var $inputfield = jQuery(this);
|
|||
|
var $editors = $inputfield.find('.' + t.cls.loaded);
|
|||
|
if($editors.length) {
|
|||
|
t.log('reload', $inputfield.attr('id'));
|
|||
|
t.destroyEditors($editors);
|
|||
|
}
|
|||
|
})
|
|||
|
.on('reloaded', '.Inputfield', function(e) {
|
|||
|
t.initEditorsIn(jQuery(this));
|
|||
|
/*
|
|||
|
var $inputfield = $(this);
|
|||
|
var s = '.' + t.cls.editor + ':not(.' + t.cls.loaded + '):not(.' + t.cls.lazy + ')';
|
|||
|
var $editors = $inputfield.find(s);
|
|||
|
if($editors.length) {
|
|||
|
t.log(e.type, $inputfield.attr('id'));
|
|||
|
t.initEditors($editors);
|
|||
|
}
|
|||
|
var $placeholders = $inputfield.find('.' + t.cls.placeholder);
|
|||
|
if($placeholders.length) t.initPlaceholders($placeholders);
|
|||
|
return false;
|
|||
|
*/
|
|||
|
})
|
|||
|
.on('sortstop', function(e) {
|
|||
|
var $editors = jQuery(e.target).find('.' + t.cls.loaded);
|
|||
|
if($editors.length) {
|
|||
|
t.log('sortstop');
|
|||
|
t.resetEditors($editors);
|
|||
|
}
|
|||
|
})
|
|||
|
.on('opened', '.Inputfield', function(e) {
|
|||
|
t.initEditorsIn(jQuery(this));
|
|||
|
})
|
|||
|
.on('clicklangtab wiretabclick', function(e, $newTab) {
|
|||
|
t.initEditorsIn($newTab);
|
|||
|
/*
|
|||
|
var $placeholders = $newTab.find('.' + t.cls.placeholder);
|
|||
|
var $editors = $newTab.find('.' + t.cls.lazy + ':visible');
|
|||
|
t.log(e.type, $newTab.attr('id'));
|
|||
|
if($placeholders.length) t.initPlaceholders($placeholders);
|
|||
|
if($editors.length) t.initEditors($editors);
|
|||
|
*/
|
|||
|
});
|
|||
|
|
|||
|
/*
|
|||
|
.on('sortstart', function() {
|
|||
|
var $editors = $(e.target).find('.InputfieldTinyMCELoaded');
|
|||
|
$editors.each(function() {
|
|||
|
var $editor = $(this);
|
|||
|
t.log('sortstart', $editor.attr('id'));
|
|||
|
});
|
|||
|
})
|
|||
|
*/
|
|||
|
|
|||
|
this.eventsReady = true;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Document ready
|
|||
|
*
|
|||
|
*/
|
|||
|
documentReady: function() {
|
|||
|
this.debug = ProcessWire.config.InputfieldTinyMCE.debug;
|
|||
|
this.isDocumentReady = true;
|
|||
|
this.log('documentReady', this.editorIds);
|
|||
|
while(this.editorIds.length > 0) {
|
|||
|
var editorId = this.editorIds.shift();
|
|||
|
this.init(editorId, 'documentReady');
|
|||
|
}
|
|||
|
this.initDocumentEvents();
|
|||
|
var $placeholders = jQuery('.' + this.cls.placeholder + ':visible');
|
|||
|
if($placeholders.length) this.initPlaceholders($placeholders);
|
|||
|
if(this.debug) {
|
|||
|
this.log('qty',
|
|||
|
'normal=' + jQuery('.' + this.cls.normal).length + ', ' +
|
|||
|
'inline=' + jQuery('.' + this.cls.inline).length + ', ' +
|
|||
|
'lazy=' + jQuery('.' + this.cls.lazy).length + ', ' +
|
|||
|
'loaded=' + jQuery('.' + this.cls.loaded).length + ', ' +
|
|||
|
'placeholders=' + $placeholders.length
|
|||
|
);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Initialize an editor
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* InputfieldTinyMCE.init('#my-textarea');
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* @param id Editor id or selector string
|
|||
|
* @param caller Optional name of caller (for debugging purposes)
|
|||
|
* @returns {boolean}
|
|||
|
*
|
|||
|
*/
|
|||
|
init: function(id, caller) {
|
|||
|
|
|||
|
var $editor, config, features, $inputfield, isFront = false,
|
|||
|
selector, isLazy, useLazy, _id = id, t = this;
|
|||
|
|
|||
|
if(!this.isDocumentReady) {
|
|||
|
this.editorIds.push(id);
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
this.setInitializing(id);
|
|||
|
|
|||
|
caller = (t.debug && typeof caller !== 'undefined' ? ' (caller=' + caller + ')' : '');
|
|||
|
|
|||
|
if(typeof id === 'string') {
|
|||
|
// literal id or selector string
|
|||
|
if(id.indexOf('#') === 0 || id.indexOf('.') === 0) {
|
|||
|
selector = id;
|
|||
|
id = '';
|
|||
|
} else {
|
|||
|
selector = '#' + id;
|
|||
|
}
|
|||
|
$editor = jQuery(selector);
|
|||
|
if(id === '') id = $editor.attr('id');
|
|||
|
|
|||
|
} else if(typeof id === 'object') {
|
|||
|
// element or jQuery element
|
|||
|
if(id instanceof jQuery) {
|
|||
|
$editor = id;
|
|||
|
} else {
|
|||
|
$editor = jQuery(id);
|
|||
|
}
|
|||
|
id = $editor.attr('id');
|
|||
|
selector = '#' + id;
|
|||
|
}
|
|||
|
|
|||
|
if(!$editor.length) {
|
|||
|
console.error('Cannot find element to init TinyMCE: ' + _id);
|
|||
|
this.setInitializing(false);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
if(id.indexOf('Inputfield_') === 0) {
|
|||
|
$inputfield = jQuery('#wrap_' + id);
|
|||
|
} else if($editor.hasClass('pw-edit-copy')) {
|
|||
|
$inputfield = $editor.closest('.pw-edit-InputfieldTinyMCE');
|
|||
|
isFront = true; // PageFrontEdit
|
|||
|
} else {
|
|||
|
$inputfield = jQuery('#wrap_Inputfield_' + id);
|
|||
|
}
|
|||
|
|
|||
|
if(!$inputfield.length) {
|
|||
|
$inputfield = $editor.closest('.InputfieldTinyMCE');
|
|||
|
if(!$inputfield.length) $inputfield = $editor.closest('.Inputfield');
|
|||
|
}
|
|||
|
|
|||
|
features = $inputfield.attr('data-features');
|
|||
|
if(typeof features === 'undefined') features = '';
|
|||
|
|
|||
|
useLazy = t.allowLazy && features.indexOf('lazyMode') > -1;
|
|||
|
isLazy = $editor.hasClass(t.cls.lazy);
|
|||
|
|
|||
|
if(useLazy && !isLazy && !$editor.is(':visible') && !$editor.hasClass(t.cls.inline)) {
|
|||
|
$editor.addClass(t.cls.lazy);
|
|||
|
this.log('init-lazy', id + caller);
|
|||
|
this.setInitializing(false);
|
|||
|
return true;
|
|||
|
} else if(isLazy) {
|
|||
|
$editor.removeClass(t.cls.lazy);
|
|||
|
}
|
|||
|
|
|||
|
this.log('init', id + caller);
|
|||
|
|
|||
|
config = this.getConfig($editor, $inputfield, features);
|
|||
|
config.selector = selector;
|
|||
|
config.setup = this.setupEditor;
|
|||
|
config.init_instance_callback = function(editor) {
|
|||
|
t.setInitializing('');
|
|||
|
setTimeout(function() { if(t.isInitializing() === '') t.setInitializing(false); }, 100);
|
|||
|
t.log('ready', editor.id);
|
|||
|
t.editorReady(editor, features);
|
|||
|
}
|
|||
|
|
|||
|
if(isFront) {
|
|||
|
// disable drag/drop image uploads when PageFrontEdit
|
|||
|
config.images_upload_url = '';
|
|||
|
config.automatic_uploads = false;
|
|||
|
config.paste_data_images = false;
|
|||
|
} else if(features.indexOf('imgUpload') > -1) {
|
|||
|
config.images_upload_handler = InputfieldTinyMCEUploadHandler;
|
|||
|
}
|
|||
|
|
|||
|
tinymce.init(config);
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
jQuery(document).ready(function() {
|
|||
|
InputfieldTinyMCE.documentReady();
|
|||
|
});
|
|||
|
|
|||
|
/*
|
|||
|
InputfieldTinyMCE.onSetup(function(editor) {
|
|||
|
editor.ui.registry.addButton('hello', {
|
|||
|
icon: 'user',
|
|||
|
text: 'Hello',
|
|||
|
onAction: function() {
|
|||
|
editor.insertContent('Hello World!')
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
*/
|