make overlay component of rich text editor link picker in property editor and grid editor
This commit is contained in:
@@ -10,7 +10,8 @@ angular.module("umbraco.directives")
|
||||
configuration:"=",
|
||||
onMediaPickerClick: "=",
|
||||
onEmbedClick: "=",
|
||||
onMacroPickerClick: "="
|
||||
onMacroPickerClick: "=",
|
||||
onLinkPickerClick: "="
|
||||
},
|
||||
template: "<textarea ng-model=\"value\" rows=\"10\" class=\"mceNoEditor\" style=\"overflow:hidden\" id=\"{{uniqueId}}\"></textarea>",
|
||||
replace: true,
|
||||
@@ -208,6 +209,12 @@ angular.module("umbraco.directives")
|
||||
$(e.target).attr("data-mce-src", path + qs);
|
||||
});
|
||||
|
||||
//Create the insert link plugin
|
||||
tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){
|
||||
if(scope.onLinkPickerClick) {
|
||||
scope.onLinkPickerClick(editor, currentTarget, anchorElement);
|
||||
}
|
||||
});
|
||||
|
||||
//Create the insert media plugin
|
||||
tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){
|
||||
|
||||
@@ -488,7 +488,288 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
createLinkPicker: function(editor, $scope, onClick) {
|
||||
|
||||
function createLinkList(callback) {
|
||||
return function() {
|
||||
var linkList = editor.settings.link_list;
|
||||
|
||||
if (typeof(linkList) === "string") {
|
||||
tinymce.util.XHR.send({
|
||||
url: linkList,
|
||||
success: function(text) {
|
||||
callback(tinymce.util.JSON.parse(text));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(linkList);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function showDialog(linkList) {
|
||||
var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
|
||||
var win, linkListCtrl, relListCtrl, targetListCtrl;
|
||||
|
||||
function linkListChangeHandler(e) {
|
||||
var textCtrl = win.find('#text');
|
||||
|
||||
if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) {
|
||||
textCtrl.value(e.control.text());
|
||||
}
|
||||
|
||||
win.find('#href').value(e.control.value());
|
||||
}
|
||||
|
||||
function buildLinkList() {
|
||||
var linkListItems = [{
|
||||
text: 'None',
|
||||
value: ''
|
||||
}];
|
||||
|
||||
tinymce.each(linkList, function(link) {
|
||||
linkListItems.push({
|
||||
text: link.text || link.title,
|
||||
value: link.value || link.url,
|
||||
menu: link.menu
|
||||
});
|
||||
});
|
||||
|
||||
return linkListItems;
|
||||
}
|
||||
|
||||
function buildRelList(relValue) {
|
||||
var relListItems = [{
|
||||
text: 'None',
|
||||
value: ''
|
||||
}];
|
||||
|
||||
tinymce.each(editor.settings.rel_list, function(rel) {
|
||||
relListItems.push({
|
||||
text: rel.text || rel.title,
|
||||
value: rel.value,
|
||||
selected: relValue === rel.value
|
||||
});
|
||||
});
|
||||
|
||||
return relListItems;
|
||||
}
|
||||
|
||||
function buildTargetList(targetValue) {
|
||||
var targetListItems = [{
|
||||
text: 'None',
|
||||
value: ''
|
||||
}];
|
||||
|
||||
if (!editor.settings.target_list) {
|
||||
targetListItems.push({
|
||||
text: 'New window',
|
||||
value: '_blank'
|
||||
});
|
||||
}
|
||||
|
||||
tinymce.each(editor.settings.target_list, function(target) {
|
||||
targetListItems.push({
|
||||
text: target.text || target.title,
|
||||
value: target.value,
|
||||
selected: targetValue === target.value
|
||||
});
|
||||
});
|
||||
|
||||
return targetListItems;
|
||||
}
|
||||
|
||||
function buildAnchorListControl(url) {
|
||||
var anchorList = [];
|
||||
|
||||
tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
|
||||
var id = anchor.name || anchor.id;
|
||||
|
||||
if (id) {
|
||||
anchorList.push({
|
||||
text: id,
|
||||
value: '#' + id,
|
||||
selected: url.indexOf('#' + id) !== -1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (anchorList.length) {
|
||||
anchorList.unshift({
|
||||
text: 'None',
|
||||
value: ''
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'anchor',
|
||||
type: 'listbox',
|
||||
label: 'Anchors',
|
||||
values: anchorList,
|
||||
onselect: linkListChangeHandler
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateText() {
|
||||
if (!initialText && data.text.length === 0) {
|
||||
this.parent().parent().find('#text')[0].value(this.value());
|
||||
}
|
||||
}
|
||||
|
||||
selectedElm = selection.getNode();
|
||||
anchorElm = dom.getParent(selectedElm, 'a[href]');
|
||||
|
||||
data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
|
||||
data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';
|
||||
data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : '';
|
||||
data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : '';
|
||||
|
||||
if (selectedElm.nodeName === "IMG") {
|
||||
data.text = initialText = " ";
|
||||
}
|
||||
|
||||
if (linkList) {
|
||||
linkListCtrl = {
|
||||
type: 'listbox',
|
||||
label: 'Link list',
|
||||
values: buildLinkList(),
|
||||
onselect: linkListChangeHandler
|
||||
};
|
||||
}
|
||||
|
||||
if (editor.settings.target_list !== false) {
|
||||
targetListCtrl = {
|
||||
name: 'target',
|
||||
type: 'listbox',
|
||||
label: 'Target',
|
||||
values: buildTargetList(data.target)
|
||||
};
|
||||
}
|
||||
|
||||
if (editor.settings.rel_list) {
|
||||
relListCtrl = {
|
||||
name: 'rel',
|
||||
type: 'listbox',
|
||||
label: 'Rel',
|
||||
values: buildRelList(data.rel)
|
||||
};
|
||||
}
|
||||
|
||||
var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector();
|
||||
var dialogService = injector.get("dialogService");
|
||||
var currentTarget = null;
|
||||
|
||||
//if we already have a link selected, we want to pass that data over to the dialog
|
||||
if(anchorElm){
|
||||
var anchor = $(anchorElm);
|
||||
currentTarget = {
|
||||
name: anchor.attr("title"),
|
||||
url: anchor.attr("href"),
|
||||
target: anchor.attr("target")
|
||||
};
|
||||
|
||||
//locallink detection, we do this here, to avoid poluting the dialogservice
|
||||
//so the dialog service can just expect to get a node-like structure
|
||||
if(currentTarget.url.indexOf("localLink:") > 0){
|
||||
currentTarget.id = currentTarget.url.substring(currentTarget.url.indexOf(":")+1,currentTarget.url.length-1);
|
||||
}
|
||||
}
|
||||
|
||||
if(onClick) {
|
||||
onClick(currentTarget, anchorElm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
editor.addButton('link', {
|
||||
icon: 'link',
|
||||
tooltip: 'Insert/edit link',
|
||||
shortcut: 'Ctrl+K',
|
||||
onclick: createLinkList(showDialog),
|
||||
stateSelector: 'a[href]'
|
||||
});
|
||||
|
||||
editor.addButton('unlink', {
|
||||
icon: 'unlink',
|
||||
tooltip: 'Remove link',
|
||||
cmd: 'unlink',
|
||||
stateSelector: 'a[href]'
|
||||
});
|
||||
|
||||
editor.addShortcut('Ctrl+K', '', createLinkList(showDialog));
|
||||
this.showDialog = showDialog;
|
||||
|
||||
editor.addMenuItem('link', {
|
||||
icon: 'link',
|
||||
text: 'Insert link',
|
||||
shortcut: 'Ctrl+K',
|
||||
onclick: createLinkList(showDialog),
|
||||
stateSelector: 'a[href]',
|
||||
context: 'insert',
|
||||
prependToContext: true
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
insertLinkInEditor: function(editor, target, anchorElm) {
|
||||
|
||||
var href = target.url;
|
||||
|
||||
function insertLink() {
|
||||
if (anchorElm) {
|
||||
editor.dom.setAttribs(anchorElm, {
|
||||
href: href,
|
||||
title: target.name,
|
||||
target: target.target ? target.target : null,
|
||||
rel: target.rel ? target.rel : null,
|
||||
'data-id': target.id ? target.id : null
|
||||
});
|
||||
|
||||
editor.selection.select(anchorElm);
|
||||
editor.execCommand('mceEndTyping');
|
||||
} else {
|
||||
editor.execCommand('mceInsertLink', false, {
|
||||
href: href,
|
||||
title: target.name,
|
||||
target: target.target ? target.target : null,
|
||||
rel: target.rel ? target.rel : null,
|
||||
'data-id': target.id ? target.id : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!href) {
|
||||
editor.execCommand('unlink');
|
||||
return;
|
||||
}
|
||||
|
||||
//if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set
|
||||
if(target.id && (angular.isUndefined(target.isMedia) || !target.isMedia)){
|
||||
href = "/{localLink:" + target.id + "}";
|
||||
insertLink();
|
||||
return;
|
||||
}
|
||||
|
||||
// Is email and not //user@domain.com
|
||||
if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) {
|
||||
href = 'mailto:' + href;
|
||||
insertLink();
|
||||
return;
|
||||
}
|
||||
|
||||
// Is www. prefixed
|
||||
if (/^\s*www\./i.test(href)) {
|
||||
href = 'http://' + href;
|
||||
insertLink();
|
||||
return;
|
||||
}
|
||||
|
||||
insertLink();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//used for the media picker dialog
|
||||
angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController",
|
||||
function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) {
|
||||
var dialogOptions = $scope.model;
|
||||
|
||||
var searchText = "Search...";
|
||||
localizationService.localize("general_search").then(function (value) {
|
||||
searchText = value + "...";
|
||||
});
|
||||
|
||||
if(!$scope.model.title) {
|
||||
$scope.model.title = "Link picker";
|
||||
}
|
||||
|
||||
$scope.dialogTreeEventHandler = $({});
|
||||
$scope.model.target = {};
|
||||
$scope.searchInfo = {
|
||||
searchFromId: null,
|
||||
searchFromName: null,
|
||||
showSearch: false,
|
||||
results: [],
|
||||
selectedSearchResults: []
|
||||
};
|
||||
|
||||
if (dialogOptions.currentTarget) {
|
||||
$scope.model.target = dialogOptions.currentTarget;
|
||||
|
||||
//if we have a node ID, we fetch the current node to build the form data
|
||||
if ($scope.model.target.id) {
|
||||
|
||||
if (!$scope.model.target.path) {
|
||||
entityResource.getPath($scope.model.target.id, "Document").then(function (path) {
|
||||
$scope.model.target.path = path;
|
||||
//now sync the tree to this path
|
||||
$scope.dialogTreeEventHandler.syncTree({ path: $scope.model.target.path, tree: "content" });
|
||||
});
|
||||
}
|
||||
|
||||
contentResource.getNiceUrl($scope.model.target.id).then(function (url) {
|
||||
$scope.model.target.url = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function nodeSelectHandler(ev, args) {
|
||||
args.event.preventDefault();
|
||||
args.event.stopPropagation();
|
||||
|
||||
if (args.node.metaData.listViewNode) {
|
||||
//check if list view 'search' node was selected
|
||||
|
||||
$scope.searchInfo.showSearch = true;
|
||||
$scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id;
|
||||
$scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name;
|
||||
}
|
||||
else {
|
||||
eventsService.emit("dialogs.linkPicker.select", args);
|
||||
|
||||
if ($scope.currentNode) {
|
||||
//un-select if there's a current one selected
|
||||
$scope.currentNode.selected = false;
|
||||
}
|
||||
|
||||
$scope.currentNode = args.node;
|
||||
$scope.currentNode.selected = true;
|
||||
$scope.model.target.id = args.node.id;
|
||||
$scope.model.target.name = args.node.name;
|
||||
|
||||
if (args.node.id < 0) {
|
||||
$scope.model.target.url = "/";
|
||||
}
|
||||
else {
|
||||
contentResource.getNiceUrl(args.node.id).then(function (url) {
|
||||
$scope.model.target.url = url;
|
||||
});
|
||||
}
|
||||
|
||||
if (!angular.isUndefined($scope.model.target.isMedia)) {
|
||||
delete $scope.model.target.isMedia;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function nodeExpandedHandler(ev, args) {
|
||||
if (angular.isArray(args.children)) {
|
||||
|
||||
//iterate children
|
||||
_.each(args.children, function (child) {
|
||||
//check if any of the items are list views, if so we need to add a custom
|
||||
// child: A node to activate the search
|
||||
if (child.metaData.isContainer) {
|
||||
child.hasChildren = true;
|
||||
child.children = [
|
||||
{
|
||||
level: child.level + 1,
|
||||
hasChildren: false,
|
||||
name: searchText,
|
||||
metaData: {
|
||||
listViewNode: child,
|
||||
},
|
||||
cssClass: "icon umb-tree-icon sprTree icon-search",
|
||||
cssClasses: ["not-published"]
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$scope.switchToMediaPicker = function () {
|
||||
userService.getCurrentUser().then(function (userData) {
|
||||
$scope.mediaPickerOverlay = {
|
||||
view: "mediapicker",
|
||||
startNodeId: userData.startMediaId,
|
||||
show: true,
|
||||
submit: function(model) {
|
||||
var media = model.selectedImages[0];
|
||||
|
||||
$scope.model.target.id = media.id;
|
||||
$scope.model.target.isMedia = true;
|
||||
$scope.model.target.name = media.name;
|
||||
$scope.model.target.url = mediaHelper.resolveFile(media);
|
||||
|
||||
$scope.mediaPickerOverlay.show = false;
|
||||
$scope.mediaPickerOverlay = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hideSearch = function () {
|
||||
$scope.searchInfo.showSearch = false;
|
||||
$scope.searchInfo.searchFromId = null;
|
||||
$scope.searchInfo.searchFromName = null;
|
||||
$scope.searchInfo.results = [];
|
||||
}
|
||||
|
||||
// method to select a search result
|
||||
$scope.selectResult = function (evt, result) {
|
||||
result.selected = result.selected === true ? false : true;
|
||||
nodeSelectHandler(evt, {event: evt, node: result});
|
||||
};
|
||||
|
||||
//callback when there are search results
|
||||
$scope.onSearchResults = function (results) {
|
||||
$scope.searchInfo.results = results;
|
||||
$scope.searchInfo.showSearch = true;
|
||||
};
|
||||
|
||||
$scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler);
|
||||
$scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler);
|
||||
$scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<div ng-controller="Umbraco.Overlays.LinkPickerController">
|
||||
|
||||
<umb-control-group label="@content_urls">
|
||||
<input type="text"
|
||||
localize="placeholder"
|
||||
placeholder="@general_url"
|
||||
class="umb-editor umb-textstring"
|
||||
ng-model="model.target.url"
|
||||
ng-disabled="model.target.id"/>
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group label="@content_nodeName">
|
||||
<input type="text"
|
||||
localize="placeholder"
|
||||
placeholder="@placeholders_entername"
|
||||
class="umb-editor umb-textstring"
|
||||
ng-model="model.target.name" />
|
||||
</umb-control-group>
|
||||
|
||||
<umb-control-group label="@content_target">
|
||||
<select class="umb-editor umb-dropdown" ng-model="model.target.target">
|
||||
<option value=""></option>
|
||||
<option value="_blank">Opens the linked document in a new window or tab</option>
|
||||
<option value="_top">Opens the linked document in the full body of the window</option>
|
||||
<option value="_parent">Opens the linked document in the parent frame</option>
|
||||
</select>
|
||||
</umb-control-group>
|
||||
|
||||
<div class="umb-control-group">
|
||||
<h5>Link to page</h5>
|
||||
|
||||
<umb-tree-search-box
|
||||
hide-search-callback="hideSearch"
|
||||
search-callback="onSearchResults"
|
||||
search-from-id="{{searchInfo.searchFromId}}"
|
||||
search-from-name="{{searchInfo.searchFromName}}"
|
||||
show-search="{{searchInfo.showSearch}}"
|
||||
section="{{section}}">
|
||||
</umb-tree-search-box>
|
||||
|
||||
<br/>
|
||||
|
||||
<umb-tree-search-results
|
||||
ng-if="searchInfo.showSearch"
|
||||
results="searchInfo.results"
|
||||
select-result-callback="selectResult">
|
||||
</umb-tree-search-results>
|
||||
|
||||
<div ng-hide="searchInfo.showSearch">
|
||||
<umb-tree
|
||||
section="content"
|
||||
hideheader="true"
|
||||
hideoptions="true"
|
||||
eventhandler="dialogTreeEventHandler"
|
||||
isdialog="true"
|
||||
enablecheckboxes="true">
|
||||
</umb-tree>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="umb-control-group">
|
||||
<h5>Link to media</h5>
|
||||
<a href ng-click="switchToMediaPicker()" class="btn">Select media</a>
|
||||
</div>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="mediaPickerOverlay.show"
|
||||
model="mediaPickerOverlay"
|
||||
view="mediaPickerOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
</div>
|
||||
@@ -5,10 +5,24 @@
|
||||
|
||||
var vm = this;
|
||||
|
||||
vm.openLinkPicker = openLinkPicker;
|
||||
vm.openMediaPicker = openMediaPicker;
|
||||
vm.openMacroPicker = openMacroPicker;
|
||||
vm.openEmbed = openEmbed;
|
||||
|
||||
function openLinkPicker(editor, currentTarget, anchorElement) {
|
||||
vm.linkPickerOverlay = {
|
||||
view: "linkpicker",
|
||||
currentTarget: currentTarget,
|
||||
show: true,
|
||||
submit: function(model) {
|
||||
tinyMceService.insertLinkInEditor(editor, model.target, anchorElement);
|
||||
vm.linkPickerOverlay.show = false;
|
||||
vm.linkPickerOverlay = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function openMediaPicker(editor, currentTarget, userData) {
|
||||
vm.mediaPickerOverlay = {
|
||||
currentTarget: currentTarget,
|
||||
|
||||
@@ -5,11 +5,19 @@
|
||||
configuration="model.config.rte"
|
||||
value="control.value"
|
||||
unique-id="control.$uniqueId"
|
||||
on-link-picker-click="vm.openLinkPicker"
|
||||
on-media-picker-click="vm.openMediaPicker"
|
||||
on-embed-click="vm.openEmbed"
|
||||
on-macro-picker-click="vm.openMacroPicker">
|
||||
</div>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="vm.linkPickerOverlay.show"
|
||||
model="vm.linkPickerOverlay"
|
||||
position="right"
|
||||
view="vm.linkPickerOverlay.view">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="vm.mediaPickerOverlay.show"
|
||||
model="vm.mediaPickerOverlay"
|
||||
|
||||
@@ -229,6 +229,19 @@ angular.module("umbraco")
|
||||
syncContent(editor);
|
||||
});
|
||||
|
||||
tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) {
|
||||
$scope.linkPickerOverlay = {
|
||||
view: "linkpicker",
|
||||
currentTarget: currentTarget,
|
||||
show: true,
|
||||
submit: function(model) {
|
||||
tinyMceService.insertLinkInEditor(editor, model.target, anchorElement);
|
||||
$scope.linkPickerOverlay.show = false;
|
||||
$scope.linkPickerOverlay = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//Create the insert media plugin
|
||||
tinyMceService.createMediaPicker(editor, $scope, function(currentTarget, userData){
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
ng-model="model.value" rows="10"
|
||||
id="{{textAreaHtmlId}}"></textarea>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="linkPickerOverlay.show"
|
||||
model="linkPickerOverlay"
|
||||
view="linkPickerOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
|
||||
<umb-overlay
|
||||
ng-if="mediaPickerOverlay.show"
|
||||
model="mediaPickerOverlay"
|
||||
|
||||
@@ -204,11 +204,10 @@
|
||||
</command>
|
||||
|
||||
</commands>
|
||||
<plugins>
|
||||
<plugins>
|
||||
<plugin loadOnFrontend="true">code</plugin>
|
||||
<plugin loadOnFrontend="true">codemirror</plugin>
|
||||
<plugin loadOnFrontend="true">paste</plugin>
|
||||
<plugin loadOnFrontend="true">umbracolink</plugin>
|
||||
<plugin loadOnFrontend="true">anchor</plugin>
|
||||
<plugin loadOnFrontend="true">charmap</plugin>
|
||||
<plugin loadOnFrontend="true">table</plugin>
|
||||
@@ -238,7 +237,7 @@ param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|cla
|
||||
<config key="codemirror">
|
||||
{
|
||||
"indentOnInit": false,
|
||||
"path": "../../../../lib/codemirror",
|
||||
"path": "../../../../lib/codemirror",
|
||||
"config": {
|
||||
},
|
||||
"jsFiles": [
|
||||
@@ -248,4 +247,4 @@ param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|cla
|
||||
}
|
||||
</config>
|
||||
</customConfig>
|
||||
</tinymceConfig>
|
||||
</tinymceConfig>
|
||||
|
||||
Reference in New Issue
Block a user