make overlay component of rich text editor link picker in property editor and grid editor

This commit is contained in:
Mads Rasmussen
2015-12-14 15:23:36 +01:00
parent 80675eee75
commit 5d4f4376dd
9 changed files with 565 additions and 5 deletions

View File

@@ -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){

View File

@@ -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();
}
};
}

View File

@@ -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);
});
});

View File

@@ -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>

View File

@@ -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,

View File

@@ -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"

View File

@@ -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){

View File

@@ -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"

View File

@@ -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>