From 22cb83e9e113625be95119ffbefcabf68722a2f1 Mon Sep 17 00:00:00 2001 From: baka Date: Mon, 28 Jan 2019 15:15:04 +0000 Subject: [PATCH] #3057 Adds foundation for the internal links plugin git-svn-id: https://svn.libreccm.org/ccm/trunk@5815 8810af33-2d31-482b-a856-94f89814c4df --- tools-ng/tinymce/Gruntfile.js | 3 +- .../src/plugins/ccmcmslinks/main/ts/Plugin.ts | 23 ++ .../main/ts/api/Commands.ts} | 15 +- .../ccmcmslinks/main/ts/api/Settings.ts | 79 ++++ .../ccmcmslinks/main/ts/core/Actions.ts | 137 +++++++ .../main/ts/core/Keyboard.ts} | 8 +- .../ccmcmslinks/main/ts/core/OpenUrl.ts | 47 +++ .../plugins/ccmcmslinks/main/ts/core/Utils.ts | 169 ++++++++ .../ccmcmslinks/main/ts/ui/Controls.ts | 77 ++++ .../plugins/ccmcmslinks/main/ts/ui/Dialog.ts | 386 ++++++++++++++++++ 10 files changed, 928 insertions(+), 16 deletions(-) create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/Plugin.ts rename tools-ng/tinymce/src/plugins/{charmap/main/ts/ui/Buttons.ts => ccmcmslinks/main/ts/api/Commands.ts} (55%) create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Settings.ts create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Actions.ts rename tools-ng/tinymce/src/plugins/{charmap/main/ts/api/Events.ts => ccmcmslinks/main/ts/core/Keyboard.ts} (66%) create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/OpenUrl.ts create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Utils.ts create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Controls.ts create mode 100644 tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Dialog.ts diff --git a/tools-ng/tinymce/Gruntfile.js b/tools-ng/tinymce/Gruntfile.js index ecaf3b685..5a432d32b 100644 --- a/tools-ng/tinymce/Gruntfile.js +++ b/tools-ng/tinymce/Gruntfile.js @@ -50,7 +50,8 @@ let plugins = [ "visualblocks", "visualchars", "wordcount", - "ccmcmsimages" + "ccmcmsimages", + "ccmcmslinks" ]; let themes = ["modern", "mobile", "inlite"]; diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/Plugin.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/Plugin.ts new file mode 100644 index 000000000..e4ba7292f --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/Plugin.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import PluginManager from 'tinymce/core/api/PluginManager'; +import Commands from './api/Commands'; +import Actions from './core/Actions'; +import Keyboard from './core/Keyboard'; +import Controls from './ui/Controls'; + +PluginManager.add('ccmcmslink', function (editor) { + Controls.setupButtons(editor); + Controls.setupMenuItems(editor); + Controls.setupContextToolbars(editor); + Actions.setupGotoLinks(editor); + Commands.register(editor); + Keyboard.setup(editor); +}); + +export default function () {} diff --git a/tools-ng/tinymce/src/plugins/charmap/main/ts/ui/Buttons.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Commands.ts similarity index 55% rename from tools-ng/tinymce/src/plugins/charmap/main/ts/ui/Buttons.ts rename to tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Commands.ts index 1fc6847eb..bd21a7d09 100644 --- a/tools-ng/tinymce/src/plugins/charmap/main/ts/ui/Buttons.ts +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Commands.ts @@ -5,19 +5,10 @@ * For commercial licenses see https://www.tiny.cloud/ */ -const register = function (editor) { - editor.addButton('charmap', { - icon: 'charmap', - tooltip: 'Special character', - cmd: 'mceShowCharmap' - }); +import Actions from '../core/Actions'; - editor.addMenuItem('charmap', { - icon: 'charmap', - text: 'Special character', - cmd: 'mceShowCharmap', - context: 'insert' - }); +const register = function (editor) { + editor.addCommand('mceLink', Actions.openDialog(editor)); }; export default { diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Settings.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Settings.ts new file mode 100644 index 000000000..cde4d6e45 --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/api/Settings.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +const assumeExternalTargets = function (editorSettings) { + return typeof editorSettings.link_assume_external_targets === 'boolean' ? editorSettings.link_assume_external_targets : false; +}; + +const hasContextToolbar = function (editorSettings) { + return typeof editorSettings.link_context_toolbar === 'boolean' ? editorSettings.link_context_toolbar : false; +}; + +const getLinkList = function (editorSettings) { + return editorSettings.link_list; +}; + +const hasDefaultLinkTarget = function (editorSettings) { + return typeof editorSettings.default_link_target === 'string'; +}; + +const getDefaultLinkTarget = function (editorSettings) { + return editorSettings.default_link_target; +}; + +const getTargetList = function (editorSettings) { + return editorSettings.target_list; +}; + +const setTargetList = function (editor, list) { + editor.settings.target_list = list; +}; + +const shouldShowTargetList = function (editorSettings) { + return getTargetList(editorSettings) !== false; +}; + +const getRelList = function (editorSettings) { + return editorSettings.rel_list; +}; + +const hasRelList = function (editorSettings) { + return getRelList(editorSettings) !== undefined; +}; + +const getLinkClassList = function (editorSettings) { + return editorSettings.link_class_list; +}; + +const hasLinkClassList = function (editorSettings) { + return getLinkClassList(editorSettings) !== undefined; +}; + +const shouldShowLinkTitle = function (editorSettings) { + return editorSettings.link_title !== false; +}; + +const allowUnsafeLinkTarget = function (editorSettings) { + return typeof editorSettings.allow_unsafe_link_target === 'boolean' ? editorSettings.allow_unsafe_link_target : false; +}; + +export default { + assumeExternalTargets, + hasContextToolbar, + getLinkList, + hasDefaultLinkTarget, + getDefaultLinkTarget, + getTargetList, + setTargetList, + shouldShowTargetList, + getRelList, + hasRelList, + getLinkClassList, + hasLinkClassList, + shouldShowLinkTitle, + allowUnsafeLinkTarget +}; \ No newline at end of file diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Actions.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Actions.ts new file mode 100644 index 000000000..0f7b806f9 --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Actions.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import VK from 'tinymce/core/api/util/VK'; +import Settings from '../api/Settings'; +import OpenUrl from './OpenUrl'; +import Utils from './Utils'; +import Dialog from '../ui/Dialog'; + +const getLink = function (editor, elm) { + return editor.dom.getParent(elm, 'a[href]'); +}; + +const getSelectedLink = function (editor) { + return getLink(editor, editor.selection.getStart()); +}; + +const getHref = function (elm) { + // Returns the real href value not the resolved a.href value + const href = elm.getAttribute('data-mce-href'); + return href ? href : elm.getAttribute('href'); +}; + +const isContextMenuVisible = function (editor) { + const contextmenu = editor.plugins.contextmenu; + return contextmenu ? contextmenu.isContextMenuVisible() : false; +}; + +const hasOnlyAltModifier = function (e) { + return e.altKey === true && e.shiftKey === false && e.ctrlKey === false && e.metaKey === false; +}; + +const gotoLink = function (editor, a) { + if (a) { + const href = getHref(a); + if (/^#/.test(href)) { + const targetEl = editor.$(href); + if (targetEl.length) { + editor.selection.scrollIntoView(targetEl[0], true); + } + } else { + OpenUrl.open(a.href); + } + } +}; + +const openDialog = function (editor) { + return function () { + Dialog.open(editor); + }; +}; + +const gotoSelectedLink = function (editor) { + return function () { + gotoLink(editor, getSelectedLink(editor)); + }; +}; + +const leftClickedOnAHref = function (editor) { + return function (elm) { + let sel, rng, node; + if (Settings.hasContextToolbar(editor.settings) && !isContextMenuVisible(editor) && Utils.isLink(elm)) { + sel = editor.selection; + rng = sel.getRng(); + node = rng.startContainer; + // ignore cursor positions at the beginning/end (to make context toolbar less noisy) + if (node.nodeType === 3 && sel.isCollapsed() && rng.startOffset > 0 && rng.startOffset < node.data.length) { + return true; + } + } + return false; + }; +}; + +const setupGotoLinks = function (editor) { + editor.on('click', function (e) { + const link = getLink(editor, e.target); + if (link && VK.metaKeyPressed(e)) { + e.preventDefault(); + gotoLink(editor, link); + } + }); + + editor.on('keydown', function (e) { + const link = getSelectedLink(editor); + if (link && e.keyCode === 13 && hasOnlyAltModifier(e)) { + e.preventDefault(); + gotoLink(editor, link); + } + }); +}; + +const toggleActiveState = function (editor) { + return function () { + const self = this; + editor.on('nodechange', function (e) { + self.active(!editor.readonly && !!Utils.getAnchorElement(editor, e.element)); + }); + }; +}; + +const toggleViewLinkState = function (editor) { + return function () { + const self = this; + + const toggleVisibility = function (e) { + if (Utils.hasLinks(e.parents)) { + self.show(); + } else { + self.hide(); + } + }; + + if (!Utils.hasLinks(editor.dom.getParents(editor.selection.getStart()))) { + self.hide(); + } + + editor.on('nodechange', toggleVisibility); + + self.on('remove', function () { + editor.off('nodechange', toggleVisibility); + }); + }; +}; + +export default { + openDialog, + gotoSelectedLink, + leftClickedOnAHref, + setupGotoLinks, + toggleActiveState, + toggleViewLinkState +}; \ No newline at end of file diff --git a/tools-ng/tinymce/src/plugins/charmap/main/ts/api/Events.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Keyboard.ts similarity index 66% rename from tools-ng/tinymce/src/plugins/charmap/main/ts/api/Events.ts rename to tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Keyboard.ts index 159705eba..ce371fff4 100644 --- a/tools-ng/tinymce/src/plugins/charmap/main/ts/api/Events.ts +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Keyboard.ts @@ -5,10 +5,12 @@ * For commercial licenses see https://www.tiny.cloud/ */ -const fireInsertCustomChar = function (editor, chr) { - return editor.fire('insertCustomChar', { chr }); +import Actions from './Actions'; + +const setup = function (editor) { + editor.addShortcut('Meta+K', '', Actions.openDialog(editor)); }; export default { - fireInsertCustomChar + setup }; \ No newline at end of file diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/OpenUrl.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/OpenUrl.ts new file mode 100644 index 000000000..d70c42115 --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/OpenUrl.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import Env from 'tinymce/core/api/Env'; +import { document, window } from '@ephox/dom-globals'; + +const appendClickRemove = function (link, evt) { + document.body.appendChild(link); + link.dispatchEvent(evt); + document.body.removeChild(link); +}; + +const open = function (url) { + // Chrome and Webkit has implemented noopener and works correctly with/without popup blocker + // Firefox has it implemented noopener but when the popup blocker is activated it doesn't work + // Edge has only implemented noreferrer and it seems to remove opener as well + // Older IE versions pre IE 11 falls back to a window.open approach + if (!Env.ie || Env.ie > 10) { + const link = document.createElement('a'); + link.target = '_blank'; + link.href = url; + link.rel = 'noreferrer noopener'; + + const evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + appendClickRemove(link, evt); + } else { + const win: any = window.open('', '_blank'); + if (win) { + win.opener = null; + const doc = win.document; + doc.open(); + doc.write(''); + doc.close(); + } + } +}; + +export default { + open +}; \ No newline at end of file diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Utils.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Utils.ts new file mode 100644 index 000000000..cecd2f012 --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/core/Utils.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import Tools from 'tinymce/core/api/util/Tools'; +import Settings from '../api/Settings'; + +const toggleTargetRules = function (rel, isUnsafe) { + const rules = ['noopener']; + let newRel = rel ? rel.split(/\s+/) : []; + + const toString = function (rel) { + return Tools.trim(rel.sort().join(' ')); + }; + + const addTargetRules = function (rel) { + rel = removeTargetRules(rel); + return rel.length ? rel.concat(rules) : rules; + }; + + const removeTargetRules = function (rel) { + return rel.filter(function (val) { + return Tools.inArray(rules, val) === -1; + }); + }; + + newRel = isUnsafe ? addTargetRules(newRel) : removeTargetRules(newRel); + return newRel.length ? toString(newRel) : null; +}; + +const trimCaretContainers = function (text) { + return text.replace(/\uFEFF/g, ''); +}; + +const getAnchorElement = function (editor, selectedElm?) { + selectedElm = selectedElm || editor.selection.getNode(); + if (isImageFigure(selectedElm)) { + // for an image conained in a figure we look for a link inside the selected element + return editor.dom.select('a[href]', selectedElm)[0]; + } else { + return editor.dom.getParent(selectedElm, 'a[href]'); + } +}; + +const getAnchorText = function (selection, anchorElm) { + const text = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({ format: 'text' }); + return trimCaretContainers(text); +}; + +const isLink = function (elm) { + return elm && elm.nodeName === 'A' && elm.href; +}; + +const hasLinks = function (elements) { + return Tools.grep(elements, isLink).length > 0; +}; + +const isOnlyTextSelected = function (html) { + // Partial html and not a fully selected anchor element + if (/]+>[^<]+<\/a>$/.test(html) || html.indexOf('href=') === -1)) { + return false; + } + + return true; +}; + +const isImageFigure = function (node) { + return node && node.nodeName === 'FIGURE' && /\bimage\b/i.test(node.className); +}; + +const link = function (editor, attachState) { + return function (data) { + editor.undoManager.transact(function () { + const selectedElm = editor.selection.getNode(); + const anchorElm = getAnchorElement(editor, selectedElm); + + const linkAttrs = { + href: data.href, + target: data.target ? data.target : null, + rel: data.rel ? data.rel : null, + class: data.class ? data.class : null, + title: data.title ? data.title : null + }; + + if (!Settings.hasRelList(editor.settings) && Settings.allowUnsafeLinkTarget(editor.settings) === false) { + linkAttrs.rel = toggleTargetRules(linkAttrs.rel, linkAttrs.target === '_blank'); + } + + if (data.href === attachState.href) { + attachState.attach(); + attachState = {}; + } + + if (anchorElm) { + editor.focus(); + + if (data.hasOwnProperty('text')) { + if ('innerText' in anchorElm) { + anchorElm.innerText = data.text; + } else { + anchorElm.textContent = data.text; + } + } + + editor.dom.setAttribs(anchorElm, linkAttrs); + + editor.selection.select(anchorElm); + editor.undoManager.add(); + } else { + if (isImageFigure(selectedElm)) { + linkImageFigure(editor, selectedElm, linkAttrs); + } else if (data.hasOwnProperty('text')) { + editor.insertContent(editor.dom.createHTML('a', linkAttrs, editor.dom.encode(data.text))); + } else { + editor.execCommand('mceInsertLink', false, linkAttrs); + } + } + }); + }; +}; + +const unlink = function (editor) { + return function () { + editor.undoManager.transact(function () { + const node = editor.selection.getNode(); + if (isImageFigure(node)) { + unlinkImageFigure(editor, node); + } else { + editor.execCommand('unlink'); + } + }); + }; +}; + +const unlinkImageFigure = function (editor, fig) { + let a, img; + img = editor.dom.select('img', fig)[0]; + if (img) { + a = editor.dom.getParents(img, 'a[href]', fig)[0]; + if (a) { + a.parentNode.insertBefore(img, a); + editor.dom.remove(a); + } + } +}; + +const linkImageFigure = function (editor, fig, attrs) { + let a, img; + img = editor.dom.select('img', fig)[0]; + if (img) { + a = editor.dom.create('a', attrs); + img.parentNode.insertBefore(a, img); + a.appendChild(img); + } +}; + +export default { + link, + unlink, + isLink, + hasLinks, + isOnlyTextSelected, + getAnchorElement, + getAnchorText, + toggleTargetRules +}; \ No newline at end of file diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Controls.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Controls.ts new file mode 100644 index 000000000..184b681ed --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Controls.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import Actions from '../core/Actions'; +import Utils from '../core/Utils'; + +const setupButtons = function (editor) { + editor.addButton('ccm-cms-link', { + active: false, + icon: 'link', + tooltip: 'Insert/edit link', + onclick: Actions.openDialog(editor), + onpostrender: Actions.toggleActiveState(editor) + }); + + editor.addButton('ccm-cms-unlink', { + active: false, + icon: 'unlink', + tooltip: 'Remove link', + onclick: Utils.unlink(editor), + onpostrender: Actions.toggleActiveState(editor) + }); + + if (editor.addContextToolbar) { + editor.addButton('ccm-cms-openlink', { + icon: 'newtab', + tooltip: 'Open link', + onclick: Actions.gotoSelectedLink(editor) + }); + } +}; + +const setupMenuItems = function (editor) { + editor.addMenuItem('ccm-cms-openlink', { + text: 'Open link', + icon: 'newtab', + onclick: Actions.gotoSelectedLink(editor), + onPostRender: Actions.toggleViewLinkState(editor), + prependToContext: true + }); + + editor.addMenuItem('ccm-cms-link', { + icon: 'link', + text: 'Link', + shortcut: 'Meta+K', + onclick: Actions.openDialog(editor), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); + + editor.addMenuItem('ccm-cms-unlink', { + icon: 'unlink', + text: 'Remove link', + onclick: Utils.unlink(editor), + stateSelector: 'a[href]' + }); +}; + +const setupContextToolbars = function (editor) { + if (editor.addContextToolbar) { + editor.addContextToolbar( + Actions.leftClickedOnAHref(editor), + 'ccm-cms-openlink | ccm-cms-link ccm-cms-unlink' + ); + } +}; + +export default { + setupButtons, + setupMenuItems, + setupContextToolbars +}; diff --git a/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Dialog.ts b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Dialog.ts new file mode 100644 index 000000000..3c7ddef83 --- /dev/null +++ b/tools-ng/tinymce/src/plugins/ccmcmslinks/main/ts/ui/Dialog.ts @@ -0,0 +1,386 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import Delay from 'tinymce/core/api/util/Delay'; +import Tools from 'tinymce/core/api/util/Tools'; +import XHR from 'tinymce/core/api/util/XHR'; +import Settings from '../api/Settings'; +import Utils from '../core/Utils'; + +let attachState = {}; + +const createLinkList = function (editor, callback) { + const linkList = Settings.getLinkList(editor.settings); + + if (typeof linkList === 'string') { + XHR.send({ + url: linkList, + success(text) { + callback(editor, JSON.parse(text)); + } + }); + } else if (typeof linkList === 'function') { + linkList(function (list) { + callback(editor, list); + }); + } else { + callback(editor, linkList); + } +}; + +const buildListItems = function (inputList, itemCallback?, startItems?) { + const appendItems = function (values, output?) { + output = output || []; + + Tools.each(values, function (item) { + const menuItem: any = { text: item.text || item.title }; + + if (item.menu) { + menuItem.menu = appendItems(item.menu); + } else { + menuItem.value = item.value; + + if (itemCallback) { + itemCallback(menuItem); + } + } + + output.push(menuItem); + }); + + return output; + }; + + return appendItems(inputList, startItems || []); +}; + +// Delay confirm since onSubmit will move focus +const delayedConfirm = function (editor, message, callback) { + const rng = editor.selection.getRng(); + + Delay.setEditorTimeout(editor, function () { + editor.windowManager.confirm(message, function (state) { + editor.selection.setRng(rng); + callback(state); + }); + }); +}; + +const showDialog = function (editor, linkList) { + const data: any = {}; + const selection = editor.selection; + const dom = editor.dom; + let anchorElm, initialText; + let win, + onlyText, + textListCtrl, + linkListCtrl, + relListCtrl, + targetListCtrl, + classListCtrl, + linkTitleCtrl, + value; + + const linkListChangeHandler = function (e) { + const 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()); + }; + + const buildAnchorListControl = function (url) { + const anchorList = []; + + Tools.each(editor.dom.select('a:not([href])'), function (anchor) { + const 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 + }; + } + }; + + const updateText = function () { + if (!initialText && onlyText && !data.text) { + this.parent() + .parent() + .find('#text')[0] + .value(this.value()); + } + }; + + const urlChange = function (e) { + const meta = e.meta || {}; + + if (linkListCtrl) { + linkListCtrl.value(editor.convertURL(this.value(), 'href')); + } + + Tools.each(e.meta, function (value, key) { + const inp = win.find('#' + key); + + if (key === 'text') { + if (initialText.length === 0) { + inp.value(value); + data.text = value; + } + } else { + inp.value(value); + } + }); + + if (meta.attach) { + attachState = { + href: this.value(), + attach: meta.attach + }; + } + + if (!meta.text) { + updateText.call(this); + } + }; + + const onBeforeCall = function (e) { + e.meta = win.toJSON(); + }; + + onlyText = Utils.isOnlyTextSelected(selection.getContent()); + anchorElm = Utils.getAnchorElement(editor); + + data.text = initialText = Utils.getAnchorText(editor.selection, anchorElm); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + + if (anchorElm) { + data.target = dom.getAttrib(anchorElm, 'target'); + } else if (Settings.hasDefaultLinkTarget(editor.settings)) { + data.target = Settings.getDefaultLinkTarget(editor.settings); + } + + if ((value = dom.getAttrib(anchorElm, 'rel'))) { + data.rel = value; + } + + if ((value = dom.getAttrib(anchorElm, 'class'))) { + data.class = value; + } + + if ((value = dom.getAttrib(anchorElm, 'title'))) { + data.title = value; + } + + if (onlyText) { + textListCtrl = { + name: 'text', + type: 'textbox', + size: 40, + label: 'Text to display', + onchange() { + data.text = this.value(); + } + }; + } + + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildListItems( + linkList, + function (item) { + item.value = editor.convertURL(item.value || item.url, 'href'); + }, + [{ text: 'None', value: '' }] + ), + onselect: linkListChangeHandler, + value: editor.convertURL(data.href, 'href'), + onPostRender() { + /*eslint consistent-this:0*/ + linkListCtrl = this; + } + }; + } + + if (Settings.shouldShowTargetList(editor.settings)) { + if (Settings.getTargetList(editor.settings) === undefined) { + Settings.setTargetList(editor, [ + { text: 'None', value: '' }, + { text: 'New window', value: '_blank' } + ]); + } + + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildListItems(Settings.getTargetList(editor.settings)) + }; + } + + if (Settings.hasRelList(editor.settings)) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildListItems(Settings.getRelList(editor.settings), function ( + item + ) { + if (Settings.allowUnsafeLinkTarget(editor.settings) === false) { + item.value = Utils.toggleTargetRules( + item.value, + data.target === '_blank' + ); + } + }) + }; + } + + if (Settings.hasLinkClassList(editor.settings)) { + classListCtrl = { + name: 'class', + type: 'listbox', + label: 'Class', + values: buildListItems( + Settings.getLinkClassList(editor.settings), + function (item) { + if (item.value) { + item.textStyle = function () { + return editor.formatter.getCssText({ + inline: 'a', + classes: [item.value] + }); + }; + } + } + ) + }; + } + + if (Settings.shouldShowLinkTitle(editor.settings)) { + linkTitleCtrl = { + name: 'title', + type: 'textbox', + label: 'Title', + value: data.title + }; + } + + win = editor.windowManager.open({ + title: 'Insert link', + data, + body: [ + { + name: 'href', + type: 'filepicker', + filetype: 'file', + size: 40, + autofocus: true, + label: 'Url', + onchange: urlChange, + onkeyup: updateText, + onpaste: updateText, + onbeforecall: onBeforeCall + }, + textListCtrl, + linkTitleCtrl, + buildAnchorListControl(data.href), + linkListCtrl, + relListCtrl, + targetListCtrl, + classListCtrl + ], + onSubmit(e) { + const assumeExternalTargets = Settings.assumeExternalTargets( + editor.settings + ); + const insertLink = Utils.link(editor, attachState); + const removeLink = Utils.unlink(editor); + + const resultData = Tools.extend({}, data, e.data); + /*eslint dot-notation: 0*/ + const href = resultData.href; + + if (!href) { + removeLink(); + return; + } + + if (!onlyText || resultData.text === initialText) { + delete resultData.text; + } + + // Is email and not //user@domain.com + if ( + href.indexOf('@') > 0 && + href.indexOf('//') === -1 && + href.indexOf('mailto:') === -1 + ) { + delayedConfirm( + editor, + 'The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?', + function (state) { + if (state) { + resultData.href = 'mailto:' + href; + } + insertLink(resultData); + } + ); + return; + } + + // Is not protocol prefixed + if ( + (assumeExternalTargets === true && !/^\w+:/i.test(href)) || + (assumeExternalTargets === false && /^\s*www[\.|\d\.]/i.test(href)) + ) { + delayedConfirm( + editor, + 'The URL you entered seems to be an external link. Do you want to add the required http:// prefix?', + function (state) { + if (state) { + resultData.href = 'http://' + href; + } + insertLink(resultData); + } + ); + return; + } + + insertLink(resultData); + } + }); +}; + +const open = function (editor) { + createLinkList(editor, showDialog); +}; + +export default { + open +};