diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index d6c5a7fa8f..0fccd4eabb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -10,7 +10,8 @@ angular.module("umbraco.directives") configuration:"=", onMediaPickerClick: "=", onEmbedClick: "=", - onMacroPickerClick: "=" + onMacroPickerClick: "=", + onLinkPickerClick: "=" }, template: "", 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){ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 4d0c52e977..cdc7d77ca4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -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(); + } + }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js new file mode 100644 index 0000000000..7458f04011 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -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); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html new file mode 100644 index 0000000000..c8b21eae81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -0,0 +1,74 @@ +
+ + + + + + + + + + + + + +
+
Link to page
+ + + + +
+ + + + +
+ + +
+ +
+ +
+
Link to media
+ Select media +
+ + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index f2fa390650..e406f87aed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html index 136294dc64..7bceed6a60 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html @@ -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"> + + + + + + - + code codemirror paste - umbracolink anchor charmap table @@ -238,7 +237,7 @@ param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|cla { "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 } - \ No newline at end of file +